【问题标题】:How to catch the moment when the parent control has been resized?如何捕捉调整父控件大小的时刻?
【发布时间】:2011-10-18 21:38:31
【问题描述】:

我有一个从 TWinControl 派生的可视组件。当它的父控件被调整大小时,我需要在我的组件中做一些工作。一般情况下,我的组件的“Align”属性是 alNone。

如何捕捉调整父控件大小的事件?有可能吗?

【问题讨论】:

    标签: delphi resize components parent-child parent


    【解决方案1】:

    我正在寻找类似问题的解决方案。但在我的情况下,我不能对对齐有这样的限制,而且子类化似乎有点过分了(对齐的东西看起来也过分了,现在我看着它)

    于是我想出了以下想法:

    type
      TMyComponent = class(TControl)
      private
        FParentLastWidth: integer;
      ...
        procedure Invalidate; override;
      ...
      end;
    
    procedure TMyComponent.Invalidate;
    begin
      if (Parent <> nil) and (FParentLastWidth <> Parent.Width) then
      begin
        FParentLastWidth := Parent.Width;
        // do whatever when the parent resizes
      end;
      inherited;
    end;
    

    使用您正在跟踪的任何尺寸添加或替换 FParentLastWidth(我只需要在父宽度更改时做出反应。您可以将其视为一种优化,以免对对您的组件没有任何影响的各种更改做出反应)

    【讨论】:

    • 请记住,当父控件调整in one direction 的大小时,子控件并不总是被重新绘制。
    • 你是对的。我在父表单上有一些自定义绘图内容,这可以解释为什么它在我所有尝试过的情况下都有效。删除那些,只有当子控件受到调整大小的影响时才能正常工作。
    【解决方案2】:

    警告:完全重写。谢谢罗!!

    使用 SetWindowSubClass 的示例。

    unit Example;
    
    interface
    
    uses
      Windows, Classes, Controls, StdCtrls, Messages, CommCtrl, ExtCtrls;
    
    type
      TExampleClass = class(TlistBox)
      private
        procedure ActivateParentWindowProc;
        procedure RevertParentWindowProc;
      protected
        procedure SetParent(AParent: TWinControl); override;
      public
        procedure Notification(AComponent: TComponent; Operation: TOperation); override;
    
    
      end;
    
    function SubClassWindowProc(hWnd: HWND; uMsg: UINT; wParam: WPARAM;
             lParam: LPARAM; uIdSubclass: UINT_PTR; dwRefData: DWORD_PTR): LRESULT; stdcall;
    implementation
    
    
    procedure TExampleClass.ActivateParentWindowProc;
    begin
      SetWindowSubClass( Parent.Handle, SubClassWindowProc, NativeInt(Self), 0);
    end;
    
    
    procedure TExampleClass.Notification(AComponent: TComponent;
      Operation: TOperation);
    begin
      inherited Notification(AComponent, Operation);
      if (Operation = opRemove) and (AComponent = Parent) then
      begin
        RevertParentWindowProc;
      end;
    end;
    
    
    procedure TExampleClass.RevertParentWindowProc;
    begin
      RemoveWindowSubclass( Parent.Handle,
                            SubClassWindowProc, NativeInt(Self));
    end;
    
    procedure TExampleClass.SetParent(AParent: TWinControl);
    begin
      if Assigned(Parent) then
      begin
        RevertParentWindowProc;
      end;
      inherited SetParent(AParent);
      if Assigned(AParent) then
      begin
        ActivateParentWindowProc;
      end
      else
      begin
        RevertParentWindowProc;
      end;
    
    end;
    
    function SubClassWindowProc(hWnd: HWND; uMsg: UINT;
      wParam: WPARAM; lParam: LPARAM; uIdSubclass: UINT_PTR;
      dwRefData: DWORD_PTR): LRESULT;
    begin
      if uMsg = WM_SIZE then
      begin
        // ...
    
      end;
    
      Result := DefSubclassProc(hWnd, uMsg, wParam, lParam);
    
    
    end;
    
    end.
    

    【讨论】:

    • 在专注于答案的实现时,您忽略了问题,即通知 parent 控件发生的事件。您不能覆盖外部类的方法,尤其是当您在产品交付之前不知道这些类是什么时。
    • @Fabricio Araujo:这段代码有问题,因为 SetWindowSubClass 总是返回 FALSE。可能是因为在 Parent 的窗口句柄仍然不存在时分配了 Parent 属性?
    • @Fabricio Araujo:我稍微编辑了您的解决方案(添加了调整大小处理程序)。当我从代码创建组件的实例时效果很好,但是当我将它放在表单上时,它不起作用,请参阅我之前的评论。
    • @Andrew:我想是的。我会在今天晚些时候尝试解决这个问题。
    • 我正在尝试像这样动态地创建一个实例LB: TExampleClassprocedure TForm1.FormCreate(Sender: TObject); begin LB := TExampleClass.Create(Self); LB.SetBounds(Edit1.Left, Edit1.Top + Edit1.Height + Edit1.Top, Edit1.Width, LB.Height); LB.Parent := Self; end;,它在关闭应用程序时崩溃。我做错了什么?
    【解决方案3】:

    如果 TWinControl(父级)的大小发生更改,则在 WM_SIZE 处理程序中调用 TWinControl.Realign。这会通过TWinControl.AlignControls 冒泡到迭代所有将 Align 属性设置为其他任何内容的子控件,然后是alNone。当设置为alCustom 时,子控件的SetBounds 将使用不变的参数被调用,即使它们的大小由于锚的参与而改变或没有改变。

    所以,将 Align 设置为 alCustom,您就会收到关于父级调整大小的通知

      TChild = class(T...Control)
      private
        FInternalAlign: Boolean;
        function GetAlign: TAlign;
        procedure ParentResized;
        procedure SetAlign(Value: TAlign);
      protected
        procedure RequestAlign; override;
      public
        constructor Create(AOwner: TComponent); override;
        procedure SetBounds(ALeft, ATop, AWidth, AHeight: Integer); override;
      published
        property Align: TAlign read GetAlign write SetAlign default alCustom;
      end;
    
    constructor TChild.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      Align := alCustom;
    end;
    
    function TChild.GetAlign: TAlign;
    begin
      Result := inherited Align;
    end;
    
    procedure TChild.ParentResized;
    begin
    end;
    
    procedure TChild.RequestAlign;
    begin
      FInternalAlign := True;
      try
        inherited RequestAlign;
      finally
        FInternalAlign := False;
      end;
    end;
    
    procedure TChild.SetAlign(Value: TAlign);
    begin
      if Value = alNone then
        Value := alCustom;
      inherited Align := Value;
    end;
    
    procedure TChild.SetBounds(ALeft, ATop, AWidth, AHeight: Integer);
    begin
      if not FInternalAlign then
        if (Align <> alCustom) or ((ALeft = Left) and (ATop = Top) and
            (AWidth = Width) and (AHeight = Height)) then
          ParentResized;
      inherited SetBounds(ALeft, ATop, AWidth, AHeight);
    end;
    

    我现在能想到的唯一缺点是 Align 属性永远不能是alNone,这可能会使组件的用户感到困惑。当内部继承属性仍设置为alCustom 时,很容易显示或返回alNone,但这不是建议,只会更加混乱。只需将alCustom 设置视为此组件的一项功能即可。

    注意:通过这种结构,组件的用户仍然可以自己实现自定义对齐。

    这是我的测试代码。也许您想为自己添加一些测试。

    unit Unit1;
    
    interface
    
    uses
      Windows, SysUtils, Classes, Controls, Forms, Dialogs, StdCtrls, ExtCtrls;
    
    type
      TForm1 = class(TForm)
        TestButton: TButton;
        Panel1: TPanel;
        procedure FormCreate(Sender: TObject);
        procedure TestButtonClick(Sender: TObject);
      private
        FChild: TControl;
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.dfm}
    
    type
      TChild = class(TGraphicControl)
      private
        FInternalAlign: Boolean;
        function GetAlign: TAlign;
        procedure ParentResized;
        procedure SetAlign(Value: TAlign);
      protected
        procedure Paint; override;
        procedure RequestAlign; override;
      public
        constructor Create(AOwner: TComponent); override;
        procedure SetBounds(ALeft, ATop, AWidth, AHeight: Integer); override;
      published
        property Align: TAlign read GetAlign write SetAlign default alCustom;
      end;
    
    { TChild }
    
    constructor TChild.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      Align := alCustom;
    end;
    
    function TChild.GetAlign: TAlign;
    begin
      Result := inherited Align;
    end;
    
    procedure TChild.Paint;
    begin
      Canvas.TextRect(ClientRect, 2, 2, 'Parent resize count = ' + IntToStr(Tag));
    end;
    
    procedure TChild.ParentResized;
    begin
      Tag := Tag + 1;
      Invalidate;
    end;
    
    procedure TChild.RequestAlign;
    begin
      FInternalAlign := True;
      try
        inherited RequestAlign;
      finally
        FInternalAlign := False;
      end;
    end;
    
    procedure TChild.SetAlign(Value: TAlign);
    begin
      if Value = alNone then
        Value := alCustom;
      inherited Align := Value;
    end;
    
    procedure TChild.SetBounds(ALeft, ATop, AWidth, AHeight: Integer);
    begin
      if not FInternalAlign then
        if (Align <> alCustom) or ((ALeft = Left) and (ATop = Top) and
            (AWidth = Width) and (AHeight = Height)) then
          ParentResized;
      inherited SetBounds(ALeft, ATop, AWidth, AHeight);
    end;
    
    { TForm1 }
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      FChild := TChild.Create(Self);
      FChild.SetBounds(10, 10, 200, 50);
      FChild.Parent := Self;
    end;
    
    procedure TForm1.TestButtonClick(Sender: TObject);
    var
      OldCount: Integer;
    begin
      OldCount := FChild.Tag;
    
      Width := Width + 25;                                                     //1
      MoveWindow(Handle, Left, Top, Width + 25, Height, True);                 //2
      SetWindowPos(Handle, HWND_TOP, Left, Top, Width + 25, Height,
        SWP_NOMOVE or SWP_NOSENDCHANGING or SWP_SHOWWINDOW);                   //3
    
      FChild.Anchors := [akLeft, akTop, akRight];
      Width := Width + 25;                                                     //4
      MoveWindow(Handle, Left, Top, Width + 25, Height, True);                 //5
      SetWindowPos(Handle, HWND_TOP, Left, Top, Width + 25, Height,
        SWP_NOMOVE or SWP_NOSENDCHANGING or SWP_SHOWWINDOW);                   //6
    
      FChild.Anchors := [akLeft, akTop];
      Panel1.Anchors := [akLeft, akTop, akRight];
      FChild.Parent := Panel1;                                                 //7
      Width := Width + 25;                                                     //8
      MoveWindow(Handle, Left, Top, Width + 25, Height, True);                 //9
      SetWindowPos(Handle, HWND_TOP, Left, Top, Width + 25, Height,
        SWP_NOMOVE or SWP_NOSENDCHANGING or SWP_SHOWWINDOW);                   //10
    
      FChild.Align := alRight;
      Width := Width + 25;                                                     //11
      MoveWindow(Handle, Left, Top, Width + 25, Height, True);                 //12
      SetWindowPos(Handle, HWND_TOP, Left, Top, Width + 25, Height,
        SWP_NOMOVE or SWP_NOSENDCHANGING or SWP_SHOWWINDOW);                   //13
    
      if FChild.Tag = OldCount + 13 then
        ShowMessage('Test succeeded')
      else
        ShowMessage('Test unsuccessful');
    end;
    
    end.
    

    【讨论】:

    • 我已经测试了所有的解决方案,这个是我认为最稳定的一个。谢谢。
    【解决方案4】:

    是的,Andrew,我认为将您的组件附加到父级的消息循环(子类化它)是要走的路。为此,您可以使用 TControl.WindowProc 属性。 doc 解释说您必须保存原始文件并稍后恢复(在组件的析构函数中),并将消息传递给原始处理程序,即您的替换应该看起来像

    procedure TMyComponent.SubclassedParentWndProc(Var Msg: TMessage);
    begin
       FOldParentWndProc(Msg);
       if(Msg.Message = WM_SIZE)then begin
          ...
       end; 
    end;
    

    如果您想以“老套”的方式进行操作,请将SetWindowLongPtr API 与GWLP_WNDPROC 一起使用,但AFAIK 引入WindowProc 的原因正是为了更容易对组件进行子类化,即没有任何问题使用它。

    【讨论】:

    • 替换窗口过程与附加到父消息循环不同。事实上,附加到父级的消息循环是没有意义的。 Windows 没有消息循环。线程确实具有由消息循环抽取的消息队列。在这种情况下,WM_SIZE 消息不是通过队列传递的,它是一条已发送的消息。
    • 是的,我的措辞有误......但想法本身(挂钩到父母的 wndprc)应该可以工作。
    • 使用 WindowProc is 有问题,这与使用 SetWindowLong 的问题相同:创建控件 1,创建控件 2,删除控件 1,然后观察程序崩溃因为它试图调用不再存在的控件的窗口过程。请改用SetWindowSubclass
    • @Rob Kennedy:我快到了,但是如何控制子类 ID - 因为每个控件都有一个?由于冲突,我不能使用 ComponentIndex 或 ControlIndex - 你的建议是什么?
    • @Fabricio 对象的地址应该是唯一的吗?
    【解决方案5】:

    以下是可以帮助您的示例:

    procedure TForm1.Button1Click(Sender: TObject);
    var newMethod: TMethod;
    begin
      newMethod.Code := MethodAddress('OnResizez'); //your action for parent resize
      newMethod.Data := Pointer(self);
      SetMethodProp(button1.Parent, 'OnResize',  newMethod); //set event to button1.parent
    end;
    
    procedure TForm1.OnResizez(Sender: TObject);
    begin
      button1.Width :=   button1.Width+1; //action on resize
    end;
    

    【讨论】:

    • 这不是个好主意 - OP 编写自定义控件并且控件不应该使用它的父 事件属性;如果控件的用户也想使用事件怎么办?
    • 这只是一个例子。解决方案是:将 resize 方法分配给所有 TWinControl 并在 OnResize 触发时为具有接口 IResizeAction(或类似的东西)的孩子调用所有 OnParentResize。
    • 我能否以某种方式挂钩消息循环并捕获 WM_SIZE 以获取父句柄?
    • @Andrew 它不是通过消息循环到达的。 WM_SIZE 是同步发送的。我想你可以替换父级的窗口过程,但我希望有比这更好的解决方案。
    猜你喜欢
    • 1970-01-01
    • 2012-09-30
    • 2011-05-27
    • 1970-01-01
    • 1970-01-01
    • 2013-11-30
    • 1970-01-01
    • 2012-01-01
    • 2015-01-19
    相关资源
    最近更新 更多