Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
952 views
in Technique[技术] by (71.8m points)

delphi - How to initialize a custom control?

I want to create my own custom control. Let's say I want to initialize its graphic properties. Obviously I cannot do that in Create because a canvas/handle is not YET allocated.

The same if my custom control contains a subcomponent (and I also set its visual properties).

There are quite several places on SO that discuss the creation of a custom control. They don't really agree on it.

AfterConstruction is out of question because the handle is not ready yet.

CreateWnd seem ok but it actually can be quite problematic as it can be called more than once (for example when you apply a new skin to the program). Probably, some boolean variable should be used to check if CreateWnd was called more than once.

SetParent has the same issue: if you change the parent of your custom control, whatever code you put in its SetParent will be executed again. A bool variable should fix the problem.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Principles

First al all, most of the visual properties of a control do not require the control to have a valid window handle in order to be set. It is a false assumption that they do.

Once the object that constitutes a control is created, i.e. the constructor has been executed, normally all (visual) properties like size, position, font, color, alignment, etc. can be set. Or they should be able to, preferably. For sub controls, also the Parent ideally must be set as soon as the constructor has run. For the component itself, that constructor would be the inherited constructor during its own constructor.

The reason this works is that all these kind of properties are stored within the fields of the Delphi object itself: they are not immediately passed to the Windows API. That happens in CreateWnd but no sooner than when all necessary parent window handles are resolved and assigned.

So the short answer is: the initial setup of a custom component is done in its constructor, because it is the only routine that runs once.

But the question (unintentionally) touches a wide range of topics on component building, because the complexity of an initial setup of a control depends entirely on the type of control and the properties that are to be set.

Example

Consider writing this (useless yet illustrative) component that consists of a panel with a combo box aligned on top of it. The panel should initially have: no caption, a custom height and a silver background. The combo box should have: a custom font size and a 'picklist' style.

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;

Framework conformity

A component writer could now consider it done, but it is not. He/she has the responsibility to write components properly as described by the comprehensive Delphi Component Writer's Guide.

Note that no less then four properties (indicated bold in the object inspector) are needlessly stored in the DFM because of an incorrect designtime component definition. Although invisible, the caption property still reads MyPanel1, which is against te requirements. This can be solved by removing the applicable control style. The ShowCaption, Color and ParentBackground properties lack a proper default property value.

Note too that all default properties of TPanel are present, but you might want some not te be, especially the ShowCaption property. This can be prevented by descending from the right class type. The standard controls in the Delphi framework mostly offer a custom variant, e.g. TCustomEdit instead of TEdit that are there for exactly this reason.

Our example compound control that is rid of these issues looks as follows:

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;

Of course, other implications due to setting up a component are possible.

Exceptions

Unfortunately there áre properties that require a control's valid window handle, because the control stores its value in Windows' native control. Take the Items property of the combo box above for example. Consider a deisgn time requirement of it been filled with some predefined text items. You then should need to override CreateWnd and add the text items the first time that it is called.

Sometimes the initial setup of a control depends on other controls. At design time you don't (want to) have control over the order in which all controls are read. In such case, you need to override Loaded. Consider a design time requirement of adding all menu-items from the PopupMenu property, if any, to the Items property of the combo box.

The example above, extended with these new features, results finally in:

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;

It is also possible that the component depends in some way on its parent. Then override SetParent, but also remember that any dependency on (properties of) its parent likely indicates a design issue which might require re-evaluation.

And surely there are other kind of dependencies imaginable. They then would require special handling somewhere else in the component code. Or another question here on SO. ??


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

1.4m articles

1.4m replys

5 comments

57.0k users

...