I noticed that if I have a main container/parent (MainPanel
), adding a child panel to it (ChildPanel
), will perform CM_CONTROLLISTCHANGE
on MainPanel
(in TWinControl.InsertControl()
) which is fine.
But if I insert a child control (ChildButton
) to ChildPanel
, a CM_CONTROLLISTCHANGE
will be fired again for the main MainPanel
!
Why is that? I was expecting the CM_CONTROLLISTCHANGE
to fire only for ChildPanel
when inserting ChildButton
into 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: I don't know if it matters, but I'm on Delphi 5.
This question is actually very easy to answer using the debugger. You could have done this yourself quite readily. Enable Debug DCUs and set a breakpoint inside the if
statement in TMainPanel.CMControlListChange
.
The first time this breakpoint fires is when the child panel is inserted. This is as you expect, an immediate child of the main panel is being added, the child panel. The second time that the breakpoint fires is the point of interest. That's when the child of the child panel is added.
When this breakpoint fires, the call stack is like this:
TMainPanel.CMControlListChange((45100, $22420EC, 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($22420EC) TControl.SetParent($2243DD4) TForm1.Button1Click(???)
At this point we can simply inspect the call stack by double clicking on each item. I'd start at TForm1.Button1Click
which confirms that we are indeed responding to ChildButton.Parent := ChildPanel
. The work your way up the list.
Two items up we come to TWinControl.InsertControl
and when we double click on this item we find:
Perform(CM_CONTROLLISTCHANGE, Integer(AControl), Integer(True));
Here, AControl
is the button, and Self
is the child panel. Let's continue up as far as TWinControl.CMControlListChange
. Now, this is where that message is handled, and still we have Self
being the child panel. The body of this function is:
procedure TWinControl.CMControlListChange(var Message: TMessage);
begin
if FParent <> nil then FParent.WindowProc(Message);
end;
And this is the answer to the puzzle. The VCL propagates the message up the parent chain. That call then leads to the top of the call stack, TMainPanel.CMControlListChange
, where Self
is now the main panel, which was FParent
in the call to TWinControl.CMControlListChange
.
I know that I could have simply pointed at TWinControl.CMControlListChange
and that would have answered the question directly. But I really want to make the point that such questions are quite readily resolved by relatively simple debugging.
Note that I have debugged this Delphi 6 which is the closest readily available version to Delphi 5 that I have, but the principles outlined here, and the answer remain valid in all versions.