Why does CM_CONTROLLISTCHANGE perform for indirect parent controls?
I noticed that if I have a main container / parent ( MainPanel
), adding a child panel ( ChildPanel
) to it will execute CM_CONTROLLISTCHANGE
on MainPanel
(in TWinControl.InsertControl()
), which is fine,
But if I insert a child control ( ChildButton
) in ChildPanel
, a CM_CONTROLLISTCHANGE
will run again for the main one MainPanel
!
Why? I expected CM_CONTROLLISTCHANGE
to only work for ChildPanel
when inserted 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
PS: I don't know if it matters, but I'm on Delphi 5.
source to share
This question is actually very easy to answer with a debugger. You could do it yourself pretty easily. Turn on Debug DCU and set a breakpoint inside the statement if
at TMainPanel.CMControlListChange
.
The first time this breakpoint is hit is when a child panel is inserted. This is as you would expect, adding an immediate child of the main panel - the child panel. The second point where the breakpoint is triggered is of interest. This is when adding a child of the child panel.
When this breakpoint is triggered, the call stack looks 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 check the call stack by double clicking on each item. I started with TForm1.Button1Click
, which confirms what we are really responding to ChildButton.Parent := ChildPanel
. Work on your list.
Two items up we come to TWinControl.InsertControl
, and when we double-click on that item, we find:
Perform(CM_CONTROLLISTCHANGE, Integer(AControl), Integer(True));
Here AControl
is a button and a Self
is a child panel. Let's continue until TWinControl.CMControlListChange
. Now that this message is being processed, and yet we Self
will have a child panel. The body of this function:
procedure TWinControl.CMControlListChange(var Message: TMessage);
begin
if FParent <> nil then FParent.WindowProc(Message);
end;
And this is the answer to the riddle. The VCL passes the message to the parent chain. This call then leads to the top of the call stack TMainPanel.CMControlListChange
, where Self
it is now the main panel that was FParent
in the call TWinControl.CMControlListChange
.
I know that I could just point to TWinControl.CMControlListChange
and that would answer the question directly. But what I really want to say is that issues like this are fairly easy to solve with relatively simple debugging.
Please note that I debugged this Delphi 6 which is the most available Delphi 5 version I have, but the principles outlined here and the answer are still valid across all versions.
source to share