Search code examples
delphivcldelphi-5

Why does CM_CONTROLLISTCHANGE performs for indirect parent controls?


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.


Solution

  • 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.