在哪里初始化Subcomponent.Parent?

时间:2019-07-02 20:19:04

标签: delphi

我想创建自己的自定义控件。假设我要初始化其图形属性。显然,我无法在“创建”中执行此操作,因为尚未分配画布/句柄。如果我的自定义控件包含一个子组件(并且我还设置了它的视觉属性),则相同。

SO上有很多地方讨论自定义控件的创建。他们对此并不完全同意。

因此,我进行了显示创建顺序的测试。 结论是:

x <-data.frame(ID=c(rep(2,5),rep(4,3)), TIME =c(1,22,33,43,85,-48,1,30),
           CEA = c(1.32,1.42,1.81,2.33,2.23,29.7,23.34,18.23),
           CA.15.3 = c(14.62,14.59,16.8,22.34,36.33,56.02,94.09,121.5),
           CA.125 = c(33.98,27.56,30.31,NA,39.57,1171.00,956.50,825.30),
           CA.19.9 = c(6.18,7.11,5.72, NA, 7.38,39.30,118.20,98.26),
           CA.72.4 = c(rep(NA,5),1.32, NA, NA),
           NSE = c(NA, 13.21, rep(NA,6)))


### these are the columns we want to input
cols.to.impute <- colnames(x)[! colnames(x) %in% c("ID","TIME")]

### is the next id the same?
x$diffidf <- NA
x$diffidf[1:(nrow(x)-1)] <- diff(x$ID)
x$diffidf[x$diffidf > 0] <- NA

### is the previous id the same?
x$diffidb <- NA
x$diffidb[2:nrow(x)] <- diff(x$ID)
x$diffidb[x$diffidb > 0] <- NA

### diff in time with next observation
x$difftimef <- NA
x$difftimef[1:(nrow(x)-1)] <- diff(x$TIME)

### diff in time with previous observation
x$difftimeb <- NA
x$difftimeb[2:nrow(x)] <- diff(x$TIME)

### if next (previous) id is not the same time difference is not meaningful
x$difftimef[is.na(x$diffidf)] <- NA
x$difftimeb[is.na(x$diffidb)] <- NA

### we do not need diffid anymore (due to previous statement)
x$diffidf <- x$diffidb <- NULL

### if next (previous) point in time is more than 14 days it is not useful for imputation
x$difftimef[abs(x$difftimef) > 14] <- NA
x$difftimeb[abs(x$difftimeb) > 14] <- NA

### create variable usef that tells us whether we should attempt to use the forward observation for imputation
### it is 1 only if difftime forward is less than difftime backward
x$usef <- NA
x$usef[!is.na(x$difftimef) & x$difftimef < x$difftimeb] <- 1
x$usef[!is.na(x$difftimef) & is.na(x$difftimeb)] <- 1
x$usef[is.na(x$difftimef) & !is.na(x$difftimeb)] <- 0

if (!is.na(x$usef[nrow(x)]))
    stop("\nlast observation usef is not missing\n")

### now we get into column specific operations.

for (col in cols.to.impute){

### we will store the results in x$imputed, and copy into c[,col] at the end
    x$imputed <- x[,col]

### x$usef needs to be modified depending on the specific column, so we define a local version of it
    x$usef.local <- x$usef
### if a variable is not missing no point in looking at usef.local, so we make it missing
    x$usef.local[!is.na(x[,col])] <- NA

### when usef.local is 1 but the next observation is missing it cannot be used for imputation, so we
### make it 0. but a value of 0 does not mean we can use the previous observation because that may
### be missing too. so first we make usef 0 and next we check the previous observation and if that
### is missing too we make usef missing

    x$previous.value <- c(NA,x[1:(nrow(x)-1),col])
    x$next.value <- c(x[2:nrow(x),col],NA)

    x$next.missing <- is.na(x$next.value)
    x$previous.missing <- is.na(x$previous.value)

    x$usef.local[x$next.missing & x$usef.local == 1] <- 0
    x$usef.local[x$previous.missing & x$usef.local == 0] <- NA

### now we can impute properly: use next value when usef.local is 1 and previous value when usef.local is 0

    tmp <- rep(FALSE,nrow(x))
    tmp[x$usef.local == 1] <-  TRUE
    x$imputed[tmp] <- x$next.value[tmp]

    tmp <- rep(FALSE,nrow(x))
    tmp[x$usef.local == 0] <-  TRUE
    x$imputed[tmp] <- x$previous.value[tmp]

    ### copy to column
    x[,col] <- x$imputed
}

### get rid of useless temporary stuff
x$previous.value <- x$previous.missing <- x$next.value <- x$next.missing <- x$imputed <- x$usef.local <- NULL

  ID TIME   CEA CA.15.3  CA.125 CA.19.9 CA.72.4   NSE difftimef difftimeb usef
1  2    1  1.32   14.62   33.98    6.18      NA    NA        NA        NA   NA
2  2   22  1.42   14.59   27.56    7.11      NA 13.21        11        NA    1
3  2   33  1.81   16.80   30.31    5.72      NA 13.21        10        11    1
4  2   43  2.33   22.34   30.31    5.72      NA    NA        NA        10    0
5  2   85  2.23   36.33   39.57    7.38      NA    NA        NA        NA   NA
6  4  -48 29.70   56.02 1171.00   39.30    1.32    NA        NA        NA   NA
7  4    1 23.34   94.09  956.50  118.20      NA    NA        NA        NA   NA
8  4   30 18.23  121.50  825.30   98.26      NA    NA        NA        NA   NA
> 

我曾经在Create中创建子组件,并在CreateWnd中设置MySubcomponents.Parent = self。但这不是一个好地方,因为并不总是调用CreateWnd。 AfterConstruction也毫无疑问,因为句柄尚未准备好。

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

   Executing the program
      Create
      AfterConstruction
      SetParent
      SetParent (post inherited)
      SetParent
      SetParent (post inherited)
      Loaded
      CreateWnd
      CreateWindowHandle
      CreateWnd (post inherited)
      SetParent
      SetParent (post inherited)

   Dynamic creation
      Create
      AfterConstruction

   Reconstructing the form
      Not tested yet

2 个答案:

答案 0 :(得分:1)

原理

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

一旦创建了构成控件的对象,即执行了构造函数,通常就可以设置所有(可视)属性,例如大小,位置,字体,颜色,对齐方式等。或者他们应该能够,最好。对于子控件,理想情况下也必须在构造函数运行后立即设置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中不必要地存储了至少四个属性(在对象检查器中以粗体表示)。尽管不可见,但caption属性仍显示为 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的另一个问题。 ?

答案 1 :(得分:0)

因此,我进行了显示创建顺序的测试。

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)中的句柄的代码,并使用布尔var来防止执行两次该代码(请参见上面的SetParent)。