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


我在delphi的最后几天遇到了很多麻烦,我试图做的很简单,在某个点阻止界面并在其他一些点之后启用。

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

1)创建一个项目

2)在表格中放置一个编辑和一个按钮,编辑顺序必须先编辑

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

Enabled := False; 

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

ShowMessage('this is right?');

基本上就是它,现在编译,它将在编辑时按下,按下选项卡,表格将根据我们的要求被禁用,因此相应于标签顺序,下一个获得焦点的控件是按钮(但我们禁用了表单),现在按空格键,消息应该出现。

所以问题是:这是对的吗?这个行为的逻辑解释是什么?

提前thx。


9011
2018-02-09 11:42


起源

不,那不对。 - TLama
@Carcigenicate,这种行为是不正确的。我测试了这个场景,可以在Delphi XE3中重现它。如果禁用表单,则无法单击该按钮(也无论如何与表单进行交互)。这是一个错误。 OP问道 这是正确的吗 ? 我说 不,那是不对的。 - TLama
表格变成了 充分 在它变为非活动状态后禁用(在您显示消息的情况下)。但你可以缩小问题范围,例如在表单上放置一个编辑框;如果从计时器刻度事件中禁用表单,则表单上唯一的聚焦编辑框将保持启用状态。我怀疑这是设计的。 - TLama
散焦主动控制(ActiveControl := nil;在形式禁用之前,对我有用,但我会等待一些VCL黑客的更复杂的答案。 - TLama
我喜欢 ActiveControl := nil; 它看起来很干净。但您可能希望保存ActiveControl,以便在再次启用表单时可以还原它。 - Disillusioned


答案:


TButton 和 TEdit 是 TWinControl 后代 - 这意味着他们是 窗口 控制。当它们被创建时,它们被分配了自己的 HWND 并且操作系统在有焦点时直接向他们发送消息。禁用其包含表单可防止主窗体接收输入消息或接收焦点,但不会禁用任何其他窗口控件 如果它已经有输入焦点

如果这些控件没有输入焦点,则在用户输入(单击,制表键等)指示时,包含表单的责任是将输入焦点传输给它们。如果表单被禁用并且这些控件没有聚焦,那么表单将不会收到允许它传输焦点的输入消息。如果焦点  然而,转移到窗口控件,然后所有用户输入直接进入该控件,即使他们的父控件的窗口被禁用 - 它们实际上是他们自己的单独窗口。

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

问题在于有两个独立的层次结构在起作用。在VCL级别,Button是一个子控件并具有父级(表单)。但是,在操作系统级别,两者都是单独的窗口,并且操作系统不知道(组件级别)父/子关系。这将是类似的情况:

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本身也可以具有父/子关系,但这与组件所有权(VCL父/子)分开,并且不一定以相同的方式运行。 来自MSDN

系统将子窗口的输入消息直接传递给   儿童窗;消息不通过父窗口传递。    唯一的例外是子窗口已被禁用 通过   EnableWindow功能。在这种情况下,系统传递任何输入   将转到子窗口到父窗口的消息   代替。这允许父窗口检查输入消息   并在必要时启用子窗口。

强调我的 - 如果你禁用一个子窗口,那么它的消息将被路由到父窗口,以便有机会检查并对它们采取行动。反之亦然 - 禁用的父母不会阻止孩子接收消息。

一个相当繁琐的解决方法可能是制作自己的一套 TWinControls表现得像这样:

 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父树,直到它找到一个表单 - 如果它发生并且表单被禁用,那么它也拒绝对窗口控件的输入。凌乱,可能更有选择性(也许一些消息不应该被忽略......)但它可能是一些可行的开始。

进一步挖掘,这似乎是不一致的 随着文档 :

一次只能有一个窗口可以接收键盘输入;那个窗口是   据说有键盘焦点。如果应用程序使用   EnableWindow函数禁用键盘焦点窗口,即窗口   除了被禁用之外,还会丢失键盘焦点。 EnableWindow   然后将键盘焦点设置为NULL,这意味着没有窗口具有焦点。   如果子窗口或其他后代窗口具有键盘焦点,   当父窗口出现时,后代窗口会失去焦点   禁用。有关更多信息,请参阅键盘输入。

这似乎没有发生,甚至明确地将按钮的窗口设置为具有以下内容的子项:

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

多一点(对于repro) - 同样的情况 Edit 选项卡焦点到按钮,退出处理程序启用TTimer。这里表单被禁用,但按钮保持焦点,即使这似乎确认Form1的HWND确实是按钮的父窗口,它应该失去焦点。

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;

在这种情况下 我们将按钮移动到面板中,一切似乎都按预期工作(大多数)。面板被禁用,按钮失去焦点,但焦点移动到父窗体(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;

这部分解释了上述观察到的行为 - 焦点移动到父控件并且活动控件失去焦点。它仍然无法解释为什么'EnableWindow`无法将焦点锁定到按钮的子窗口。这确实看起来像WinAPI问题......


10
2018-02-09 13:06



如果您禁用,例如一个小组,所有的孩子都被禁用了(而那个专注的小孩失去了它的焦点),为什么一个表格应该是一个例外呢? - TLama
@TLama我也希望如此,仍然认为这不应该被允许,或者如果真的是设计,应该有一种方法来配置这个bahaviour,无论如何,所以回到我原来的问题, stackoverflow.com/questions/28372900/...,真的没有办法阻止来自表单和他的TWinControl子组件的这些不需要的消息? - kabstergo
实际上,AFAIK操作系统知道这些窗口的父/子关系。根据 MSDN,“当父窗口被禁用时,后代窗口会失去焦点”。 OnExit可能在按钮实际聚焦之前被触发(这是有意义的)。 - GabrielF
你的第一个代码示例是关于 所有权,而不是父母/子女的关系。我认为这不是问题。 - kobik
@GabrielF子窗口(HWND父/子)在其父窗口时不会被禁用。我会添加更多信息。 - J...