禁用表单仍然允许子控件接收输入

Disabling the form still allow childs controls to receive input

最近几天 delphi 让我很头疼,我想做的很简单,在某个时间点阻塞接口,然后在其他时间点启用。

但就像听起来一样简单,我无法弄清楚为什么某些东西是设计允许的,所以澄清一下:

1) 创建项目

2) 在表单中放置一个编辑和一个按钮,编辑的 Tab 顺序必须在前

3)配置编辑和写入的OnExit事件:

Enabled := False; 

4)配置按钮的OnClick事件,写入:

ShowMessage('this is right?');

基本上就是这样,现在编译,焦点将在编辑处,按 Tab 键,表单将按我们的要求禁用,因此根据 Tab 键顺序,下一个获得焦点的控件是按钮 (但我们禁用了表单),现在按 space 应该会出现消息。

所以问题是:这样对吗?这种行为的合理解释是什么?

提前致谢。

TButtonTEdit 都是 TWinControl 后代 - 这意味着它们是 windowed 控件。创建它们时,它们会分配自己的 HWND 并且操作系统会在它们获得焦点时直接向它们发送消息。禁用它们的包含表单会阻止主表单接收输入消息或接收焦点,但它不会禁用任何其他 windowed 控件 如果它已经具有输入焦点.

如果这些控件没有输入焦点,则包含表单有责任在用户输入(单击、Tab 键等)指示时将输入焦点转移到它们。如果表单被禁用并且这些控件未获得焦点,则表单将不会接收允许其转移焦点的输入消息。但是,如果焦点 转移到 windowed 控件,那么所有用户输入都会直接转到该控件,即使他们的 parent 控件的 window已禁用 - 它们实际上是自己独立的 windows.

我不确定您观察到的行为是否是一个错误 - 它可能不是预期的,但它是标准行为。通常不会期望禁用一个 window 也会禁用同一应用程序中的其他人。

问题是有两个独立的层次结构在起作用。在 VCL 级别上,Button 是一个 child 控件并具有一个 parent(表单)。但是,在 OS 级别上,两者是分开的 windows 并且 OS 不知道(组件级别)parent/child 关系。这将是类似的情况:

procedure TForm1.Button1Click(Sender: TObject);
var
  form2 : TForm1;
begin
  self.Enabled := false;
  form2 := TForm1.Create(self);
  try
    form2.ShowModal;
  finally
    form2.Free;
  end;
end;

您真的希望 form2 在显示时被禁用,仅仅是因为它的 TComponent 所有者是 Form1 吗?当然不是。窗口控件大同小异。

Windows 本身也可以有一个 parent/child 关系,但这与组件所有权 (VCL parent/child) 是分开的,并且不一定以相同的方式表现。 From MSDN:

The system passes a child window's input messages directly to the child window; the messages are not passed through the parent window. The only exception is if the child window has been disabled by the EnableWindow function. In this case, the system passes any input messages that would have gone to the child window to the parent window instead. This permits the parent window to examine the input messages and enable the child window, if necessary.

强调我的 - 如果您禁用 child window 那么它的消息将被路由到 parent 以便有机会检查它们并对其采取行动。反之则不然——禁用的 parent 不会阻止 child 接收消息。

一个相当繁琐的解决方法可能是制作您自己的一组 TWinControl 行为如下:

 TSafeButton = class(TButton)
   protected
     procedure WndProc(var Msg : TMessage); override;
 end;

 {...}

procedure TSafeButton.WndProc(var Msg : TMessage);
  function ParentForm(AControl : TWinControl) : TWinControl;
  begin
    if Assigned(AControl) and (AControl is TForm) then
      result := AControl
    else
      if Assigned(AControl.Parent) then
        result := ParentForm(AControl.Parent)
      else result := nil;
  end;
begin
  if Assigned(ParentForm(self)) and (not ParentForm(self).Enabled) then
    Msg.Result := 0
  else
    inherited;
end;

这会沿着 VCL parent 树向上走,直到它找到一个表单 - 如果找到并且该表单被禁用,那么它也会拒绝对 windowed 控件的输入。杂乱无章,而且可能更具选择性(也许某些消息不应该被忽略...)但这将是可行的开始。

进一步挖掘,这似乎是不一致的 with the documentation :

Only one window at a time can receive keyboard input; that window is said to have the keyboard focus. If an application uses the EnableWindow function to disable a keyboard-focus window, the window loses the keyboard focus in addition to being disabled. EnableWindow then sets the keyboard focus to NULL, meaning no window has the focus. If a child window, or other descendant window, has the keyboard focus, the descendant window loses the focus when the parent window is disabled. For more information, see Keyboard Input.

这似乎并没有发生,甚至明确地将按钮的 window 设置为 child 并带有 :

 oldParent := WinAPI.Windows.SetParent(Button1.Handle, Form1.Handle);
 // here, in fact, oldParent = Form1.Handle, so parent/child HWND
 // relationship is correct by default.

多一点(用于重现)- 相同的场景 Edit 选项卡聚焦到按钮,退出处理程序启用 TTimer。这里表单被禁用,但按钮保留焦点,即使这似乎确认 Form1 的 HWND 确实是按钮的 parent window,它应该失去焦点。

procedure TForm1.Timer1Timer(Sender: TObject);
var
  h1, h2, h3 : cardinal;
begin      
  h1 := GetFocus;       // h1 = Button1.Handle 
  h2 := GetParent(h1);  // h2 = Form1.Handle
  self.Enabled := false;      
  h3 := GetFocus;       // h3 = Button1.Handle
end;

我们将按钮移动到面板的情况下,一切似乎(大部分)都按预期工作。面板被禁用并且按钮失去焦点,但焦点随后移动到 parent 表单(WinAPI 建议它应该为 NULL)。

procedure TForm1.Timer1Timer(Sender: TObject);
var
  h1, h2, h3 : cardinal;
begin      
  h1 := GetFocus;       // h1 = Button1.Handle 
  h2 := GetParent(h1);  // h2 = Panel1.Handle
  Panel1.Enabled := false;      
  h3 := GetFocus;       // h3 = Form1.Handle
end;

部分问题似乎出在这里 - 看起来顶级表单本身负责控制散焦。这有效,除非表单本身被禁用:

procedure TWinControl.CMEnabledChanged(var Message: TMessage);
begin
  if not Enabled and (Parent <> nil) then RemoveFocus(False);
                 // ^^ False if form itself is being disabled!
  if HandleAllocated and not (csDesigning in ComponentState) then
    EnableWindow(WindowHandle, Enabled);
end;
procedure TWinControl.RemoveFocus(Removing: Boolean);
var
  Form: TCustomForm;
begin
  Form := GetParentForm(Self);
  if Form <> nil then Form.DefocusControl(Self, Removing);
end

在哪里

procedure TCustomForm.DefocusControl(Control: TWinControl; Removing: Boolean);
begin
  if Removing and Control.ContainsControl(FFocusedControl) then
    FFocusedControl := Control.Parent;
  if Control.ContainsControl(FActiveControl) then SetActiveControl(nil);
end;

这部分解释了上述观察到的行为 - 焦点移动到 parent 控件并且活动控件失去焦点。它仍然没有解释为什么“EnableWindow”无法将焦点关闭到按钮的 child window。这确实开始看起来像一个 WinAPI 问题...