3

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.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
zig
  • 4,524
  • 1
  • 24
  • 68
  • 1
    All unhandled windows messages propagate up through the chain of parents. This is so if the child doesn't handle a message, the parent has a chance to do so, and so on. – Dsm Jul 25 '17 at 14:09
  • 1
    @Dsm Are you quite sure about that? How does that happen? Some VCL code? Or is that Windows functionality. I must admit that I am sceptical of your claim as written. – David Heffernan Jul 26 '17 at 06:38
  • @Dsm, `CM_CONTROLLISTCHANGE` is a custom *notification* VCL message AFAIK (Not sure if it acts same as native Window messages). I was unable to find *how* it is *"propagate up through the chain of parents"* in the sources. So I still can't tell what's going on and how. My main issue is that I can't tell who is the `Parent` of the inserted control in the `CMControlListChange` message handler (but that might be another question). – zig Jul 26 '17 at 08:57
  • @DavidHeffernan, I always understood this to be the way windows works (not VCL), but I must admit it is just something I picked up over the years and took at face value, so I am prepared to accept that I am wrong, if indeed I am. – Dsm Jul 26 '17 at 09:02
  • @zig AFAIK this is independent of whether or not this is a custom message. – Dsm Jul 26 '17 at 09:04
  • 1
    @Dsm Hard to see how it could be so. Consider, for instance, a class private message. Because they are class private, they can only be sent to a window of that class. If they get sent to a window of a different class, then that message ID could be the ID for a different class private message. So I can only conclude that what you state in that first comment is plainly wrong. I suggest that it would be prudent to remove all of these comments to avoid confusion for readers. – David Heffernan Jul 26 '17 at 09:04
  • 1
    @Dsm, *"All unhandled windows messages propagate up through the chain of parents"* - is wrong. the VCL specifically propagate `CM_CONTROLLISTCHANGE` up the parent chain. but not for any message. for example `CM_CONTROLCHANGE` is sent *only* to the immediate parent not up in the chain. – zig Jul 26 '17 at 10:58
  • I don't think the Delphi 5 tag is important here, since you have the same behavior in modern versions. – zig Jul 26 '17 at 11:44

1 Answers1

4

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.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Thanks! Obviously, I have debugged this carefully but I missed the `TWinControl.CMControlListChange` part. newer versions have `CM_CONTROLLISTCHANGING` where you can determine the Parent but Delphi 5 does not. Do you know if I can tell *who* called `CM_CONTROLLISTCHANGE` and determine the `Message.Control` Parent at that point? (probably this could not be done?) – zig Jul 26 '17 at 09:45
  • What are you actually trying to do? If I knew that I might be able to suggest a way to do it. – David Heffernan Jul 26 '17 at 10:08
  • Something similar to "TFlowPanel". when ever a child control is inserted I add it to my `FControlList` and then align controls in the FlowPanel according to that `FControlList`. problem is that if I put a child control on an immediate child of that FlowPanel, it is also added to the `FControlList`... – zig Jul 26 '17 at 10:11
  • I think that might be tricky to achieve. You could post a message when you get `CM_CONTROLLISTCHANGE` and then maintain your list when that message is processed. But that has its own issues. – David Heffernan Jul 26 '17 at 10:24
  • Interesting idea. Thanks. but the problem is that by then `AlignControl` will be already called (without the new control in the list) which I'm trying to avoid (calling it again in that posted message). Maybe I could handle `CM_CONTROLCHANGE` where I know the Parent, but that will have the same effect. Thanks again. – zig Jul 26 '17 at 10:34
  • Actually `CM_CONTROLCHANGE` is not a bad idea... it seems `TCustomGridPanel` for example, also uses this message to insert or remove controls to its `FControlCollection`. Also, this message is fired **only** for the immediate parent. not up in the parent chain like `CM_CONTROLLISTCHANGE`. – zig Jul 26 '17 at 11:31
  • BTW (Sorry for nagging), `Two items up we come to TWinControl.InsertControl`...`Here, AControl is the button, and Self is the child panel.` - How can you tell that `AControl` is the button and `Self` is the child panel at that point? I don't see any information about this in the debugger... – zig Jul 26 '17 at 11:38
  • I know because it read the code between those two points in the call stack – David Heffernan Jul 26 '17 at 11:39