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