【问题标题】:How to initialize a custom control?如何初始化自定义控件?
【发布时间】:2019-11-13 12:26:20
【问题描述】:

我想创建自己的自定义控件。假设我想初始化它的图形属性。显然我不能在 Create 中这样做,因为尚未分配画布/句柄。

如果我的自定义控件包含一个子组件(并且我还设置了它的视觉属性),也是如此。

SO 上有很多地方讨论了自定义控件的创建。他们并没有真正同意。

AfterConstruction 没有问题,因为句柄还没有准备好。

CreateWnd 看起来不错,但实际上可能会出现很多问题,因为它可以被多次调用(例如,当您将新皮肤应用到程序时)。可能应该使用一些布尔变量来检查 CreateWnd 是否被多次调用。

SetParent 也有同样的问题:如果您更改自定义控件的父级,则您在其 SetParent 中放置的任何代码都将再次执行。一个 bool 变量应该可以解决这个问题。

【问题讨论】:

  • 通常,创建子控件并将其Parent 属性设置为主控件将在主控件的构造函数中正常工作,前提是子控件不需要有效的@987654322 @正确的方式,这并不常见。如果它确实需要HWND,您可以让您的主控件覆盖虚拟SetParent() 方法以在构建后设置其自己的Parent 时更新子控件。或者CreateWnd()/CreateWindowHandle() 将起作用,因为这是当您的主控件在没有分配HWND 的情况下访问其Handle 属性时创建自己的HWND
  • 附带说明,CreateWnd()CreateWindowHandle() 之前被调用,因为它是基类TWinControl.CreateWnd() 调用CreateWindowHandle()。在记录您的消息之前,您正在调用 inherited
  • @RemyLebeau-动态创建组件时如何设置属性(包括Parent)?然后只调用 Create 和 AfterConstruction。
  • 只需在父类的构造函数中创建子控件,并在创建时设置其属性即可。并在父级的构造函数中设置父级的属性。如果您在这样做时遇到问题,那么父母/孩子可能正在做不应该做的事情。您需要展示您的真实代码并解释实际问题是什么
  • ".. 当动态创建组件时?只调用 Create 和 AfterConstruction .." 设置父级是导致调用 CreateWnd 的原因 - 因为那时控件需要一个手柄。 IOW 您的测试不完整,请在构造函数中设置父级后记录事件的顺序。

标签: delphi


【解决方案1】:

所以,我做了这个显示创建顺序的测试。

UNIT cvTester;

{--------------------------------------------------------------------------------------------------

 This file tests the initialization order of a custom control.
--------------------------------------------------------------------------------------------------}

INTERFACE
{$WARN GARBAGE OFF}    { Silent the: 'W1011 Text after final END' warning }

USES
  System.SysUtils, System.Classes, vcl.Controls, vcl.Forms, Vcl.StdCtrls, Vcl.ExtCtrls;


TYPE
  TCustomCtrlTest = class(TPanel)
    private
    protected
      Initialized: boolean;
      Sub: TButton;
    public
      constructor Create(AOwner: TComponent); override;
      procedure Loaded; override;
      procedure AfterConstruction; override;
      procedure CreateWnd; override;
      procedure CreateWindowHandle(const Params: TCreateParams); override;
      procedure WriteToString(s: string);
      procedure SetParent(AParent: TWinControl); override;
    published
  end;



procedure Register;

IMPLEMENTATION
USES System.IOUtils;

procedure Register;
begin
  RegisterComponents('Mine', [TCustomCtrlTest]);
end;



constructor TCustomCtrlTest.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Sub:= TButton.Create(Self);
  Sub.Parent:= Self;            // Typically, creating a sub-control and setting its Parent property to your main control will work just fine inside of your main control's constructor, provided that the sub-control does not require a valid HWND right way. Remy Lebeau

  WriteToString('Create'+ #13#10);
end;


procedure TCustomCtrlTest.Loaded;
begin
  inherited;
  WriteToString('Loaded'+ #13#10);
end;


procedure TCustomCtrlTest.AfterConstruction;
begin
  inherited;
  WriteToString('AfterConstruction'+ #13#10);
end;


procedure TCustomCtrlTest.CreateWnd;
begin
  WriteToString(' CreateWnd'+ #13#10);
  inherited;
  WriteToString(' CreateWnd post'+ #13#10);

  Sub.Visible:= TRUE;
  Sub.Align:= alLeft;
  Sub.Caption:= 'SOMETHING';
  Sub.Font.Size:= 20;
end;


procedure TCustomCtrlTest.CreateWindowHandle(const Params: TCreateParams);
begin
  inherited CreateWindowHandle(Params);
  WriteToString('  CreateWindowHandle'+ #13#10);
end;


procedure TCustomCtrlTest.SetParent(AParent: TWinControl);
begin
  WriteToString('SetParent'+ #13#10);
  inherited SetParent(AParent);
  WriteToString('SetParent post'+ #13#10);
  if NOT Initialized then { Make sure we don't call this code twice }
   begin
    Initialized:= TRUE;
    SetMoreStuffHere;
   end;
end;




procedure TCustomCtrlTest.WriteToString(s: string);
begin
 System.IOUtils.TFile.AppendAllText('test.txt', s);
 // The output will be in Delphi\bin folder when the control is used inside the IDE (dropped on a form) c:\Delphi\Delphi XE7\bin\
 // and in app's folder when running inside the EXE file.
end;
end.

顺序是:

 Dropping control on a form:
    Create
    AfterConstruction
    SetParent
     CreateWnd
      CreateWindowHandle
     CreateWnd post
    SetParent post

  Deleting control from form:
    SetParent
    SetParent post

  Cutting ctrol from form and pasting it back:
    SetParent
    SetParent post
    Create
    AfterConstruction
    SetParent
     CreateWnd
      CreateWindowHandle
     CreateWnd post
    SetParent post
    SetParent
    SetParent post
    Loaded

 Executing the program
    Create
    AfterConstruction
    SetParent
    SetParent post
    SetParent
    SetParent post
    Loaded
     CreateWnd
      CreateWindowHandle
     CreateWnd post

 Dynamic creation
   Create
   AfterConstruction
   SetParent
    CreateWnd
     CreateWindowHandle
    CreateWnd post
   SetParent post

 Reconstructing the form
    Not tested yet

我最终选择的解决方案是在 SetParent(或 CreateWnd)中初始化需要句柄的代码,并使用布尔变量来防止执行该代码两次(参见上面的 SetParent)。

【讨论】:

    【解决方案2】:

    原则

    首先,控件的大多数视觉属性要求控件具有有效的窗口句柄才能进行设置。这是一个错误的假设。

    一旦创建了构成控件的对象,即执行了构造函数,通常可以设置所有(视觉)属性,如大小、位置、字体、颜色、对齐方式等。或者他们应该能够,最好是。对于子控件,理想情况下,Parent 也必须在构造函数运行后立即设置。对于组件本身,该构造函数将是其自己的构造函数期间继承的构造函数。

    这样做的原因是所有这些类型的属性都存储在 Delphi 对象本身的字段中:它们不会立即传递给 Windows API。这发生在CreateWnd,但不会早于所有必要的父窗口句柄都被解析和分配。

    所以简短的回答是:自定义组件的初始设置是在其构造函数中完成的,因为它是唯一运行一次的例程。

    但这个问题(无意中)涉及组件构建的广泛主题,因为控件初始设置的复杂性完全取决于控件的类型和要设置的属性。

    示例

    考虑编写这个(无用但说明性的)组件,它由一个面板和一个在其顶部对齐的组合框组成。面板最初应该有:没有标题、自定义高度和银色背景。组合框应具有:自定义字体大小和“选择列表”样式。

    type
      TMyPanel = class(TPanel)
      private
        FComboBox: TComboBox;
      public
        constructor Create(AOwner: TComponent); override;
      end;
    
    constructor TMyPanel.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      Color := clSilver;
      ShowCaption := False;
      Height := 100;
      FComboBox := TComboBox.Create(Self);
      FComboBox.Parent := Self;
      FComboBox.Align := alTop;
      FComboBox.Style := csDropDownList;
      FComboBox.Font.Size := 12;
    end;
    

    框架一致性

    组件编写者现在可以认为它已完成,但事实并非如此。他/她有责任按照综合 Delphi Component Writer's Guide 的描述正确编写组件。

    请注意,由于设计时组件定义不正确,至少有四个属性(在对象检查器中以粗体表示)不必要地存储在 DFM 中。虽然不可见,但标题属性仍为 MyPanel1,这违反了要求。这可以通过删除适用的control style 来解决。 ShowCaptionColorParentBackground 属性缺少正确的 default property value

    还要注意TPanel 的所有默认属性都存在,但您可能不希望出现一些默认属性,尤其是ShowCaption 属性。这可以通过从正确的类类型下降来防止。 Delphi 框架中的标准控件大多提供自定义变体,例如TCustomEdit 而不是 TEdit 正是出于这个原因。

    摆脱这些问题的示例复合控件如下所示:

    type
      TMyPanel = class(TCustomPanel)
      private
        FComboBox: TComboBox;
      public
        constructor Create(AOwner: TComponent); override;
      published
        property Color default clSilver;
        property ParentBackground default False;
      end;
    
    constructor TMyPanel.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      Color := clSilver;
      ControlStyle := ControlStyle - [csSetCaption];
      Height := 100;
      FComboBox := TComboBox.Create(Self);
      FComboBox.Parent := Self;
      FComboBox.Align := alTop;
      FComboBox.Style := csDropDownList;
      FComboBox.Font.Size := 12;
    end;
    

    当然,由于设置组件而产生的其他影响也是可能的。

    例外情况

    不幸的是,有些属性需要控件的有效窗口句柄,因为控件将其值存储在 Windows 的本机控件中。以上面组合框的Items 属性为例。考虑用一些预定义的文本项填充它的设计时间要求。然后您应该需要覆盖CreateWnd并在第一次调用它时添加文本项。

    有时控件的初始设置取决于其他控件。在设计时,您不(想要)控制读取所有控件的顺序。在这种情况下,您需要覆盖Loaded。考虑将所有菜单项从PopupMenu 属性(如果有)添加到组合框的Items 属性的设计时间要求。

    上面的例子,加上这些新特性,最终得到:

    type
      TMyPanel = class(TCustomPanel)
      private
        FInitialized: Boolean;
        FComboBox: TComboBox;
        procedure Initialize;
      protected
        procedure CreateWnd; override;
        procedure Loaded; override;
      public
        constructor Create(AOwner: TComponent); override;
      published
        property Color default clSilver;
        property ParentBackground default False;
        property PopupMenu;
      end;
    
    constructor TMyPanel.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      Color := clSilver;
      ControlStyle := ControlStyle - [csSetCaption];
      Height := 100;
      FComboBox := TComboBox.Create(Self);
      FComboBox.Parent := Self;
      FComboBox.Align := alTop;
      FComboBox.Style := csDropDownList;
      FComboBox.Font.Size := 12;
    end;
    
    procedure TMyPanel.CreateWnd;
    begin
      inherited CreateWnd;
      if not FInitialized then
        Initialize;
    end;
    
    procedure TMyPanel.Initialize;
    var
      I: Integer;
    begin
      if HandleAllocated then
      begin
        if Assigned(PopupMenu) then
          for I := 0 to PopupMenu.Items.Count - 1 do
            FComboBox.Items.Add(PopupMenu.Items[I].Caption)
        else
          FComboBox.Items.Add('Test');
        FInitialized := True;
      end;
    end;
    
    procedure TMyPanel.Loaded;
    begin
      inherited Loaded;
      Initialize;
    end;
    

    组件也有可能以某种方式依赖于其父级。然后覆盖SetParent,但还要记住,对其父级(的属性)的任何依赖都可能表明可能需要重新评估的设计问题。

    当然还有其他可以想象的依赖关系。然后他们需要在组件代码的其他地方进行特殊处理。或者关于SO的另一个问题。 ?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-08-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-19
      • 1970-01-01
      相关资源
      最近更新 更多