DBedrenko
DBedrenko

Reputation: 5029

How to write function that returns an existing TForm instance at runtime?

I'm trying to write a function that returns either of two TForm instances, according to the user-set configuration:

function TfrmMain.GetCurrentRamEditFrm: TForm;
{ Get the RAM Editor Form instance according to currenttly-set protocol. }
begin
  if frmSetup.GetCurrentProtocol() = FooBus then
    result := RAM_Editor_FooBus.frmRAM_Editor_FooBus
  else
    result := RAM_Editor_SXcp.frmRAM_Editor_SXcp;
end;

I need this function because this unit (Main.pas) reads/writes a lot of variables in the RAM Editor Form.

The compiler trips up on lines like:

GetCurrentRamEditFrm().StatusBar1.Panels[1].Text := get_text(96);

with the error message: Undeclared identifier 'StatusBar1'

If I provide the TForm instance explicitely there is no error:

RAM_Editor_SXcp.frmRAM_Editor_SXcp.StatusBar1.Panels[1].Text := get_text(96);

StatusBar is declared like this, in both Forms:

type
  TfrmRAM_Editor_SXcp = class(TForm)
    StatusBar1: TStatusBar; // i.e. the scope is "published"
    ...

Interestingly, the compiler doesn't mind the following:

GetCurrentRamEditFrm().show();

Upvotes: 1

Views: 345

Answers (3)

Caputo
Caputo

Reputation: 185

You can use RTTI to do that for you. One advantage of doing this with RTTI is that you do not need to change inheritance or apply interfaces. You will just need to set a published property on the forms

type
  TfrmRAM_Editor_SXcp = class(TForm)
  published
    property StatusBar: TStatusBar read FStatusBar write FStatusBar;
    //property StatusBar: TStatusBar read StatusBar1 write StatusBar1; //Alternative
    ...
  end;

constructor TfrmRAM_Editor_SXcp.Create(AOwner: TComponent);
begin
  FstatusBar := StatusBar1;
end;  

With that you can access the status bar using RTTI

function TfrmMain.GetRAMFrmStatusBar: TStatusBar;
begin
  if IsPublishedProp(GetCurrentRamEditFrm, 'StatusBar') then
    result :=  GetObjProp(GetCurrentRamEditFrm(), 'StatusBar') as TStatusBar
  else
    result := nil;
end;

Or create a setStatusBarText procedure

procedure TfrmMain.setRAMFrmStatusBarText(const panelId: integer; const text: string);
begin
  GetRAMFrmStatusBar.Panels[panelId].Text := text;
end;

Upvotes: 1

Dalija Prasnikar
Dalija Prasnikar

Reputation: 28516

Your function returns instance as TForm that does not know anything about StatusBar1 you have declared in TfrmRAM_Editor_SXcp.

GetCurrentRamEditFrm().show(); works because TForm class has method Show.

You would have to either create base form type that will declare all variables and methods you want to use or declare interface that both of your forms will share.

Solution 1:

type
  TBaseForm = class(TForm)
    StatusBar1: TStatusBar;

type
  TfrmRAM_Editor_SXcp = class(TBaseForm)
    // this type will automatically inherit StatusBar1

function TfrmMain.GetCurrentRamEditFrm: TBaseForm;
{ Get the RAM Editor Form instance according to currenttly-set protocol. }
begin
  if frmSetup.GetCurrentProtocol() = FooBus then
    result := RAM_Editor_FooBus.frmRAM_Editor_FooBus
  else
    result := RAM_Editor_SXcp.frmRAM_Editor_SXcp;
end;

Solution 2:

type
  IBaseForm = interface
    procedure SetStatus(const s: string);
  end;

type
  TfrmRAM_Editor_SXcp = class(TForm, IBaseForm)
    StatusBar1: TStatusBar; // i.e. the scope is "published"
    ...
    procedure SetStatus(const s: string);

procedure TfrmRAM_Editor_SXcp.SetStatus(const s: string);
begin
  StatusBar1.Panels[1].Text := s;
end;

function TfrmMain.GetCurrentRamEditFrm: IBaseForm;
{ Get the RAM Editor Form instance according to currenttly-set protocol. }
begin
  if frmSetup.GetCurrentProtocol() = FooBus then
    result := RAM_Editor_FooBus.frmRAM_Editor_FooBus
  else
    result := RAM_Editor_SXcp.frmRAM_Editor_SXcp;
end;

You can then use it like this:

GetCurrentRamEditFrm().SetStatus(get_text(96));

Of course, even if you don't go with interfaced solution, it would be good to introduce methods for functionality you need instead of grabbing UI elements like StatusBar directly. And if some day you do need to use interfaces instead of common base class if you already have methods in place then introducing interface would be very easy.

Upvotes: 10

David Heffernan
David Heffernan

Reputation: 612954

The compiler error is quite understandable because TForm does not have a member named StatusBar1. You introduced that in your derived forms, which I presume are of type TfrmRAM_Editor_FooBus and TfrmRAM_Editor_SXcp.

Now, if the two forms derive from a common base that introduces StatusBar1 you could return that common base class instead and your code would compile. This would look like this:

type
  TfrmRAM_Editor_Base = class(TForm)
    StatusBar1: TStatusBar;
    ....
  end;

  TfrmRAM_Editor_FooBus = class(TfrmRAM_Editor_Base)
    ....
  end;

  TfrmRAM_Editor_SXcp = class(TfrmRAM_Editor_Base)
    ....
  end;

function TfrmMain.GetCurrentRamEditFrm: TfrmRAM_Editor_Base;
{ Get the RAM Editor Form instance according to currently-set protocol. }
begin
  if frmSetup.GetCurrentProtocol() = FooBus then
    result := RAM_Editor_FooBus.frmRAM_Editor_FooBus
  else
    result := RAM_Editor_SXcp.frmRAM_Editor_SXcp;
end;

However, that doesn't feel like a great solution. The problem I have with this is that inheritance is a very rigid mechanism, and I am not happy at all with exposing UI controls to be used outside the form itself. In realise that Delphi's streaming mechanism forces design-time controls to be published and therefore visible from the outside, but in my view that was a terrible mistake that promotes poor design.

Personally I'd define an interface that can be used to set status text.

type
  ISetStatusText = interface
    [...add GUID here]
    procedure SetStatusText(const Value: string);
  end;

Have each form implement this interface and then you can query for it using as. Or perhaps better, have your GetCurrentRamEditFrm function return the interface rather than the form.

This avoids your having to expose the form's UI implementation details to all and sundry.

Upvotes: 6

Related Questions