为什么 CM_CONTROLLISTCHANGE 执行间接父控件?
Why does CM_CONTROLLISTCHANGE performs for indirect parent controls?
我注意到如果我有一个主面板 container/parent (MainPanel
),向它添加一个子面板 (ChildPanel
),将在 [= 上执行 CM_CONTROLLISTCHANGE
13=](在 TWinControl.InsertControl()
中)很好。
但是如果我将子控件 (ChildButton
) 插入到 ChildPanel
,主 MainPanel
将再次触发 CM_CONTROLLISTCHANGE
!
这是为什么?当将 ChildButton
插入 ChildPanel
时,我期望 CM_CONTROLLISTCHANGE
仅针对 ChildPanel
触发。
MCVE
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls, StdCtrls;
type
TMainPanel = class(ExtCtrls.TCustomPanel)
private
procedure CMControlListChange(var Message: TCMControlListChange); message CM_CONTROLLISTCHANGE;
end;
TForm1 = class(TForm)
Button1: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
private
public
MainPanel: TMainPanel;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TMainPanel.CMControlListChange(var Message: TCMControlListChange);
begin
if Message.Inserting then
begin
Form1.Memo1.Lines.Add('TMainPanel.CMControlListChange: Inserting ' + Message.Control.ClassName);
// Parent is always nil
if Message.Control.Parent = nil then Form1.Memo1.Lines.Add('*** Parent=nil');
end;
inherited;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
ChildPanel: TPanel;
ChildButton: TButton;
begin
FreeAndNil(MainPanel);
MainPanel := TMainPanel.Create(Self);
MainPanel.SetBounds(0, 0, 200, 200);
MainPanel.Parent := Self;
ChildPanel := TPanel.Create(Self);
ChildPanel.Parent := MainPanel;
ChildButton := TButton.Create(Self);
ChildButton.Parent := ChildPanel; // Why do I get CM_CONTROLLISTCHANGE in "MainPanel"?
end;
end.
DFM
object Form1: TForm1
Left = 192
Top = 114
Width = 685
Height = 275
Caption = 'Form1'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Shell Dlg 2'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Button1: TButton
Left = 592
Top = 8
Width = 75
Height = 25
Caption = 'Button1'
TabOrder = 0
OnClick = Button1Click
end
object Memo1: TMemo
Left = 456
Top = 40
Width = 209
Height = 193
TabOrder = 1
end
end
P.S:我不知道这是否重要,但我在 Delphi 5.
这个问题使用调试器其实很容易回答。您本可以很容易地自己完成此操作。启用调试 DCU 并在 TMainPanel.CMControlListChange
中的 if
语句内设置断点。
第一次触发此断点是在插入子面板时。如您所料,添加了主面板的直接子项,即子面板。断点第二次触发是兴趣点。那就是添加子面板的子项的时候了。
当这个断点触发时,调用栈是这样的:
TMainPanel.CMControlListChange((45100, 420EC, True, 0))
TControl.WndProc((45100, 35922156, 1, 0, 8428, 548, 1, 0, 0, 0))
TWinControl.WndProc((45100, 35922156, 1, 0, 8428, 548, 1, 0, 0, 0))
TWinControl.CMControlListChange((45100, 35922156, 1, 0, 8428, 548, 1, 0, 0, 0))
TControl.WndProc((45100, 35922156, 1, 0, 8428, 548, 1, 0, 0, 0))
TWinControl.WndProc((45100, 35922156, 1, 0, 8428, 548, 1, 0, 0, 0))
TControl.Perform(45100,35922156,1)
TWinControl.InsertControl(420EC)
TControl.SetParent(43DD4)
TForm1.Button1Click(???)
此时我们可以通过双击每个项目来简单地检查调用堆栈。我将从 TForm1.Button1Click
开始,这证实了我们确实在回应 ChildButton.Parent := ChildPanel
。在列表中工作。
我们来到 TWinControl.InsertControl
上面的两个项目,当我们双击这个项目时,我们发现:
Perform(CM_CONTROLLISTCHANGE, Integer(AControl), Integer(True));
这里,AControl
是按钮,Self
是子面板。让我们继续到 TWinControl.CMControlListChange
。现在,这是处理该消息的地方,我们仍然有 Self
作为子面板。这个函数的主体是:
procedure TWinControl.CMControlListChange(var Message: TMessage);
begin
if FParent <> nil then FParent.WindowProc(Message);
end;
这就是谜题的答案。 VCL 将消息向上传播到父链。然后该调用到达调用堆栈的顶部,TMainPanel.CMControlListChange
,其中 Self
现在是主面板,在对 TWinControl.CMControlListChange
.[=27 的调用中是 FParent
=]
我知道我可以简单地指出 TWinControl.CMControlListChange
就可以直接回答问题。但我真的想指出,通过相对简单的调试,这些问题很容易解决。
请注意,我已经调试了这个 Delphi 6,它是我拥有的最接近 Delphi 5 的可用版本,但此处概述的原则和答案在所有版本中仍然有效。
我注意到如果我有一个主面板 container/parent (MainPanel
),向它添加一个子面板 (ChildPanel
),将在 [= 上执行 CM_CONTROLLISTCHANGE
13=](在 TWinControl.InsertControl()
中)很好。
但是如果我将子控件 (ChildButton
) 插入到 ChildPanel
,主 MainPanel
将再次触发 CM_CONTROLLISTCHANGE
!
这是为什么?当将 ChildButton
插入 ChildPanel
时,我期望 CM_CONTROLLISTCHANGE
仅针对 ChildPanel
触发。
MCVE
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ExtCtrls, StdCtrls;
type
TMainPanel = class(ExtCtrls.TCustomPanel)
private
procedure CMControlListChange(var Message: TCMControlListChange); message CM_CONTROLLISTCHANGE;
end;
TForm1 = class(TForm)
Button1: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
private
public
MainPanel: TMainPanel;
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TMainPanel.CMControlListChange(var Message: TCMControlListChange);
begin
if Message.Inserting then
begin
Form1.Memo1.Lines.Add('TMainPanel.CMControlListChange: Inserting ' + Message.Control.ClassName);
// Parent is always nil
if Message.Control.Parent = nil then Form1.Memo1.Lines.Add('*** Parent=nil');
end;
inherited;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
ChildPanel: TPanel;
ChildButton: TButton;
begin
FreeAndNil(MainPanel);
MainPanel := TMainPanel.Create(Self);
MainPanel.SetBounds(0, 0, 200, 200);
MainPanel.Parent := Self;
ChildPanel := TPanel.Create(Self);
ChildPanel.Parent := MainPanel;
ChildButton := TButton.Create(Self);
ChildButton.Parent := ChildPanel; // Why do I get CM_CONTROLLISTCHANGE in "MainPanel"?
end;
end.
DFM
object Form1: TForm1
Left = 192
Top = 114
Width = 685
Height = 275
Caption = 'Form1'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Shell Dlg 2'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Button1: TButton
Left = 592
Top = 8
Width = 75
Height = 25
Caption = 'Button1'
TabOrder = 0
OnClick = Button1Click
end
object Memo1: TMemo
Left = 456
Top = 40
Width = 209
Height = 193
TabOrder = 1
end
end
P.S:我不知道这是否重要,但我在 Delphi 5.
这个问题使用调试器其实很容易回答。您本可以很容易地自己完成此操作。启用调试 DCU 并在 TMainPanel.CMControlListChange
中的 if
语句内设置断点。
第一次触发此断点是在插入子面板时。如您所料,添加了主面板的直接子项,即子面板。断点第二次触发是兴趣点。那就是添加子面板的子项的时候了。
当这个断点触发时,调用栈是这样的:
TMainPanel.CMControlListChange((45100, 420EC, True, 0)) TControl.WndProc((45100, 35922156, 1, 0, 8428, 548, 1, 0, 0, 0)) TWinControl.WndProc((45100, 35922156, 1, 0, 8428, 548, 1, 0, 0, 0)) TWinControl.CMControlListChange((45100, 35922156, 1, 0, 8428, 548, 1, 0, 0, 0)) TControl.WndProc((45100, 35922156, 1, 0, 8428, 548, 1, 0, 0, 0)) TWinControl.WndProc((45100, 35922156, 1, 0, 8428, 548, 1, 0, 0, 0)) TControl.Perform(45100,35922156,1) TWinControl.InsertControl(420EC) TControl.SetParent(43DD4) TForm1.Button1Click(???)
此时我们可以通过双击每个项目来简单地检查调用堆栈。我将从 TForm1.Button1Click
开始,这证实了我们确实在回应 ChildButton.Parent := ChildPanel
。在列表中工作。
我们来到 TWinControl.InsertControl
上面的两个项目,当我们双击这个项目时,我们发现:
Perform(CM_CONTROLLISTCHANGE, Integer(AControl), Integer(True));
这里,AControl
是按钮,Self
是子面板。让我们继续到 TWinControl.CMControlListChange
。现在,这是处理该消息的地方,我们仍然有 Self
作为子面板。这个函数的主体是:
procedure TWinControl.CMControlListChange(var Message: TMessage);
begin
if FParent <> nil then FParent.WindowProc(Message);
end;
这就是谜题的答案。 VCL 将消息向上传播到父链。然后该调用到达调用堆栈的顶部,TMainPanel.CMControlListChange
,其中 Self
现在是主面板,在对 TWinControl.CMControlListChange
.[=27 的调用中是 FParent
=]
我知道我可以简单地指出 TWinControl.CMControlListChange
就可以直接回答问题。但我真的想指出,通过相对简单的调试,这些问题很容易解决。
请注意,我已经调试了这个 Delphi 6,它是我拥有的最接近 Delphi 5 的可用版本,但此处概述的原则和答案在所有版本中仍然有效。