Designable Components Beyond TPanel

Making a custom TPanel descendant behave well at design time is the easy case — most other base classes need extra work to get the same drag-drop, property-editing, and live-preview behavior in the IDE. This post follows up on my earlier designable-TPanel post and covers what changes when you’re building a designable component from TCustomControl, TGraphicControl, or a non-visual base class instead.

The problem

The designable-TPanel post covered the common case: subclass TPanel, override a few methods, register the component, and the IDE designer treats it well because TPanel already does most of the heavy lifting around child control hosting and paint invalidation. Most of the interesting components I end up building aren’t panels, though — they’re custom-drawn controls, non-visual data components, or controls that need to host children without being containers in the TWinControl sense. Each of those starts from a different base class, and the design-time behavior you get for free is different every time.

TGraphicControl: no window handle, no children

TGraphicControl is the base for lightweight controls that just draw themselves — no window handle, which makes it cheap, but also means it can’t host child controls and can’t receive keyboard focus directly. If your designable component is something like a custom gauge or indicator, this is usually the right base class, but the IDE designer needs explicit help drawing a design-time-only visual cue, since there’s no window to show selection handles against by default beyond what the designer already provides.

type

  TStatusLamp = class(TGraphicControl)

  private

    FLit: Boolean;

    procedure SetLit(Value: Boolean);

  protected

    procedure Paint; override;

  public

    constructor Create(AOwner: TComponent); override;

  published

    property Lit: Boolean read FLit write SetLit default False;

  end;

constructor TStatusLamp.Create(AOwner: TComponent);

begin

  inherited Create(AOwner);

  Width := 20;

  Height := 20;

end;

procedure TStatusLamp.SetLit(Value: Boolean);

begin

  if FLit <> Value then

  begin

    FLit := Value;

    Invalidate;

  end;

end;

procedure TStatusLamp.Paint;

begin

  Canvas.Brush.Color := IfThen(FLit, clLime, clGray);

  Canvas.Ellipse(0, 0, Width, Height);

  // Design-time-only visual aid: a dashed outline so the control

  // is easy to click and select even at 20×20 pixels

  if csDesigning in ComponentState then

  begin

    Canvas.Pen.Style := psDash;

    Canvas.Brush.Style := bsClear;

    Canvas.Rectangle(0, 0, Width, Height);

  end;

end;

The csDesigning in ComponentState check is the pattern to reach for whenever a component needs to look or behave differently inside the IDE than it does at runtime — it’s checked constantly throughout the VCL source itself, and it’s the cleanest way to add design-time affordances without any risk of them leaking into a compiled build.

TCustomControl: windowed, but still no children by default

TCustomControl gives you a real window handle — useful if the component needs to receive focus, respond to keyboard input, or host a system control internally — but it still doesn’t give you child-hosting behavior automatically the way TWinControl descendants like TPanel do. If the goal is a component that both draws itself and accepts child controls dropped onto it in the designer, you need to explicitly declare that support:

type

  TCardControl = class(TCustomControl)

  protected

    procedure Paint; override;

  end;

procedure TCardControl.Paint;

begin

  Canvas.Brush.Color := clWhite;

  Canvas.Pen.Color := clSilver;

  Canvas.RoundRect(0, 0, Width, Height, 8, 8);

end;

Declaring the component this way compiles fine and looks correct, but dropping a TButton onto it at design time won’t actually parent the button to the card — it’ll parent to whatever control was underneath. To fix that, register the component with RegisterCustomControl support hooks, or more directly, override CreateParams to ensure the window has the right styles and confirm the class supports the standard child-drop behavior other TWinControl descendants get by default, since TCustomControl already extends TWinControl and does support children — the missing piece is usually just that the designer needs a nonzero ClientRect to have somewhere to drop onto, which an empty Paint override with no drawn boundary can obscure. Giving the control a visible border in its Paint method, like the RoundRect above, is often what actually fixes the dropping problem in practice, since it’s a visual affordance issue rather than a plumbing one.

Non-visual components and the property editor

For non-visual designable components — data modules, connection wrappers, anything that shows up as an icon on the form rather than a rectangle — the design-time work shifts from drawing to property editing. A raw string property for something like a connection string is technically fine but painful to use in the Object Inspector. Writing a custom property editor is where the real design-time investment goes:

type

  TConnectionStringProperty = class(TStringProperty)

  public

    function GetAttributes: TPropertyAttributes; override;

    procedure Edit; override;

  end;

function TConnectionStringProperty.GetAttributes: TPropertyAttributes;

begin

  Result := [paDialog];

end;

procedure TConnectionStringProperty.Edit;

var

  NewValue: string;

begin

  NewValue := GetValue;

  if PromptForConnectionString(NewValue) then

    SetValue(NewValue);

end;

Registering this against the component’s property, in the package’s register unit, gives the Object Inspector an ellipsis button next to the property that opens a proper dialog instead of a raw text field:

procedure Register;

begin

  RegisterComponents(‘GerixSoft’, [TMyConnection]);

  RegisterPropertyEditor(TypeInfo(string), TMyConnection,

    ‘ConnectionString’, TConnectionStringProperty);

end;

This is the same registration file where the original TPanel descendant from the earlier post was registered — non-visual components go through the same RegisterComponents call, they just don’t need a Paint method or any of the TGraphicControl/TCustomControl decisions above, since they never render on the form surface at all.

Conclusion

The base class you start from decides how much design-time behavior you get for free: TPanel and other TWinControl descendants hand you child hosting and standard selection handling, TGraphicControl and TCustomControl need explicit csDesigning checks and visible bounds to be usable in the designer, and non-visual components trade drawing concerns for property-editor concerns entirely — know which category your component falls into before writing the first line of it.

Leave a Reply

Your email address will not be published. Required fields are marked *