An interposer class is a class with exactly the same name as a standard class, descended
from it, and declared in a unit that comes later in the uses clause. Every TEdit
or TDateTimeField on a form that uses that unit becomes your class instead of the
original - no new component, no package to install, no change to the form file.
It is the quickest way to change how a component behaves across a whole application, and it works the same in the VCL, FireMonkey and TMS WEB Core.
RegisterComponents, a palette entry, and then replacing every existing control
on every form - losing its events and properties along the way. An interposer is just a
unit in the uses clause; the controls already on your forms are upgraded where they
stand.An edit box that selects all its text when you tab into it:
unit Interposers.Edits;
interface
uses
Vcl.StdCtrls;
type
TEdit = class (Vcl.StdCtrls.TEdit)
protected
procedure DoEnter; override;
end;
implementation
procedure TEdit.DoEnter;
begin
inherited;
SelectAll;
end;
end.
Then add the unit to the form, after Vcl.StdCtrls:
uses
Winapi.Windows, System.SysUtils, System.Classes, Vcl.Controls, Vcl.Forms,
Vcl.StdCtrls,
Interposers.Edits;
Every edit on that form now selects its contents on entry. Nothing in the form designer
changes; the form file still says TEdit.
TEdit = class (Vcl.StdCtrls.TEdit) -
the unit prefix tells the compiler which TEdit is the ancestor; without it the
class would try to descend from itself.RegisterClass needed.A database that stores every date-time as UTC is easy to get wrong: each read must convert
to local time and each write back to UTC. Miss one and the time is out by hours. Interposing
TDateTimeField does the conversion once, for every persistent date-time field on
every form and data module that uses the unit:
unit Interposers.Fields;
interface
uses
System.Classes, Data.DB;
type
TDateTimeField = class (Data.DB.TDateTimeField)
private
class var GStore_As_UTC : Boolean;
var FStore_As_UTC : Boolean;
protected
function GetAsDateTime : TDateTime; override;
procedure SetAsDateTime (Value : TDateTime); override;
procedure GetText (var Text : string; DisplayText : Boolean); override;
public
constructor Create (AOwner : TComponent); override;
class property Default_Store_As_UTC : Boolean read GStore_As_UTC
write GStore_As_UTC;
property Store_As_UTC : Boolean read FStore_As_UTC
write FStore_As_UTC;
end;
implementation
uses
System.SysUtils, System.DateUtils;
constructor TDateTimeField.Create (AOwner : TComponent);
begin
inherited;
FStore_As_UTC := GStore_As_UTC;
end;
function TDateTimeField.GetAsDateTime : TDateTime;
begin
Result := inherited GetAsDateTime;
if FStore_As_UTC and (not IsNull)
then Result := TTimeZone.Local.ToLocalTime (Result);
end;
procedure TDateTimeField.SetAsDateTime (Value : TDateTime);
begin
if FStore_As_UTC
then inherited SetAsDateTime (TTimeZone.Local.ToUniversalTime (Value))
else inherited SetAsDateTime (Value);
end;
procedure TDateTimeField.GetText (var Text : string; DisplayText : Boolean);
var
LFormat : string;
begin
if FStore_As_UTC and (not IsNull)
then begin
if DisplayText
then LFormat := DisplayFormat
else LFormat := '';
DateTimeToString (Text, LFormat, AsDateTime);
end
else inherited;
end;
initialization
TDateTimeField.GStore_As_UTC := True;
end.
Data-aware controls, AsDateTime, Value, AsString and
DisplayText all go through these methods, so the rest of the application only ever
sees local time. A field that holds a real local time can opt out with
Store_As_UTC := False.
Points to watch:
Data.DB.TDateTimeField.TDateField descends from
Data.DB.TDateTimeField, not from the interposer, which is what you want - a pure
date has no time zone. Use date fields for dates.TSQLTimeStampField rather than TDateTimeField; those need their own
interposer or a mapping rule.AsVariant reads the stored value - convert explicitly if you use
it.I use the same field in TMS WEB Core, where the XData server stores UTC and the browser shows local time; only the time zone calls differ.
Interposers can add fields, properties and methods as well as override. A common need is a
combo box that shows descriptions but stores a code - Active on screen,
A in the database:
unit Interposers.Web_Combo;
interface
uses
System.Classes, WEBLib.StdCtrls;
type
TWebComboBox = class (WEBLib.StdCtrls.TWebComboBox)
private
FCodes : TStringList;
function Get_Selected_Code : string;
procedure Set_Selected_Code (const AValue : string);
public
constructor Create (AOwner : TComponent); override;
destructor Destroy; override;
procedure Load_Pairs (APairs : TStrings);
property Selected_Code : string read Get_Selected_Code
write Set_Selected_Code;
end;
implementation
constructor TWebComboBox.Create (AOwner : TComponent);
begin
inherited;
FCodes := TStringList.Create;
end;
destructor TWebComboBox.Destroy;
begin
FCodes.Free;
inherited;
end;
procedure TWebComboBox.Load_Pairs (APairs : TStrings);
var
I : Integer;
begin
Items.Clear;
FCodes.Clear;
for I := 0 to APairs.Count - 1
do begin
FCodes.Add (APairs.Names [I]);
Items.Add (APairs.ValueFromIndex [I]);
end;
end;
function TWebComboBox.Get_Selected_Code : string;
begin
if ItemIndex < 0
then Result := ''
else Result := FCodes [ItemIndex];
end;
procedure TWebComboBox.Set_Selected_Code (const AValue : string);
begin
ItemIndex := FCodes.IndexOf (AValue);
end;
end.
Using it:
LPairs := TStringList.Create;
try
LPairs.Add ('A=Active');
LPairs.Add ('S=Suspended');
LPairs.Add ('C=Closed');
Combo_Status.Load_Pairs (LPairs);
finally
LPairs.Free;
end;
Combo_Status.Selected_Code := Query_Status.AsString;
...
Query_Status.AsString := Combo_Status.Selected_Code;
Every combo box already dropped on your TMS forms gains Load_Pairs and
Selected_Code, without replacing a single control. My own version goes further -
integer codes, a default code, yes/no lists and writing straight to a bound field - but the
principle is the same.
Private methods cannot be overridden - they are invisible outside their own unit, and an interposer always lives in another one. Two things get close:
Much of the VCL's behaviour sits in private message methods, such as
TControl's handlers for CM_MOUSEENTER and
CM_MOUSELEAVE. Messages are dispatched by number, not by method name, so an
interposer can declare its own handler for the same message. Call inherited and
the private original still runs:
unit Interposers.Panels;
interface
uses
Winapi.Messages, Vcl.Controls, Vcl.ExtCtrls;
type
TPanel = class (Vcl.ExtCtrls.TPanel)
private
procedure CM_Mouse_Enter (var Message : TMessage); message CM_MOUSEENTER;
procedure CM_Mouse_Leave (var Message : TMessage); message CM_MOUSELEAVE;
end;
implementation
procedure TPanel.CM_Mouse_Enter (var Message : TMessage);
begin
inherited;
BevelOuter := bvRaised;
end;
procedure TPanel.CM_Mouse_Leave (var Message : TMessage);
begin
inherited;
BevelOuter := bvNone;
end;
end.
Every panel now lifts when the mouse is over it. The same approach works for any
WM_ or CM_ message, whatever visibility the original handler has.
This is VCL only - FireMonkey and TMS WEB Core do not dispatch Windows messages.
A property's getter and setter are usually private, so they cannot be overridden. An
interposer can instead redeclare the property itself with its own getter and setter. Code
that reads or assigns the property now goes through yours, and inside them
inherited PropertyName still reaches the original.
A good use is an event the interposer needs for itself. The combo box below always runs its own change handler first, and then whatever handler the form assigned - the form's code does not change at all:
type
TComboBox = class (Vcl.StdCtrls.TComboBox)
private
FUser_On_Change : TNotifyEvent;
procedure Internal_Change (Sender : TObject);
function Get_On_Change : TNotifyEvent;
procedure Set_On_Change (AValue : TNotifyEvent);
protected
procedure Loaded; override;
public
constructor Create (AOwner : TComponent); override;
property OnChange : TNotifyEvent read Get_On_Change
write Set_On_Change;
end;
implementation
constructor TComboBox.Create (AOwner : TComponent);
begin
inherited;
inherited OnChange := Internal_Change;
end;
procedure TComboBox.Loaded;
var
LStreamed : TNotifyEvent;
begin
inherited;
LStreamed := inherited OnChange;
if TMethod (LStreamed).Code <> @TComboBox.Internal_Change
then FUser_On_Change := LStreamed;
inherited OnChange := Internal_Change;
end;
function TComboBox.Get_On_Change : TNotifyEvent;
begin
Result := FUser_On_Change;
end;
procedure TComboBox.Set_On_Change (AValue : TNotifyEvent);
begin
FUser_On_Change := AValue;
inherited OnChange := Internal_Change;
end;
procedure TComboBox.Internal_Change (Sender : TObject);
begin
{ the interposer's own work - eg. writing the selected code to a field }
if Assigned (FUser_On_Change)
then FUser_On_Change (Sender);
end;
Two things to know:
Loaded collects
any handler set in the Object Inspector and puts the interposer's own handler back in
front of it.My TMS WEB Core combo box uses exactly this to keep its bound field in step while the
form keeps its own OnChange.
Class helpers also add methods to an existing class without touching its declaration, and they are often the better choice. The difference:
AsChar property on TStringField.Store_As_UTC flag on each field - or when the original must behave
differently.The two mix well - one unit can carry helpers for the conveniences and interposers for the behaviour.
An interposer can itself be interposed. A reporting unit can declare
TStringField = class (Data_Fields.TStringField) on top of an application-wide
Data_Fields interposer, adding report-only behaviour for forms that list both
units - in that order.
TEdit.Create in a unit without the
interposer creates the original class. The interposer is a compile-time choice per unit,
not a global replacement.TEdit. Name
the units clearly (Interposers.Edits, not Utils2) so the reason
can be found.Sender is Vcl.StdCtrls.TEdit is true for the
interposed class, so existing code keeps working.Stephen Posey named and described the technique in Interposer Classes, The Delphi
Magazine, Issue 33 (May 1998). I used it in print the following year, in
Better Calculated Fields, The Delphi Magazine, Issue 43 (March 1999) - interposing
TStringField and TTable so calculated fields could be read and written
at any time. That article is
republished
here. The technique has not changed since Delphi 3.
Please donate if helpful