1

I have an FMX application (but should be the same in VCL) with a TabControl showing 10 tabs. The tabs are set to visible or not visible depending on application state and user rights.

It works well, but I don't like

  • that everying is together and muddled up in the main form

  • and tab contents are initialized even if they never become visible.

So I thought about using frames which are created when their tab becomes visible.

Each frame can only exist once and it should be easily possible to manipulate one frame from another (access controls on the other frame).

I like elegant solutions and short code :)

This is what I already found, quite nice but it is very old: Replacing TabSheets with Frames - by Dan Miser

maf-soft
  • 2,335
  • 3
  • 26
  • 49

1 Answers1

0

This solution has a global variable for each frame. All frames can use the main form unit in their implementation part and have easy access to other frames and all their controls, even without adding the other frames to the uses clause.

Tabs start invisible and their frame is uninitialized. TabA.Activate; shows the tab and sets focus. TabA.Frame.Label1 easily accesses a control on that Frame. TabA.Visible:= False; hides the tab and frees the frame.

Generics are really helpful here, I like it.

Ideas for improvements are very welcome...

type
  TFormMain = class(TForm)
    TabControl: TTabControl;
    TabInfo: TTabItem;
    procedure FormCreate(Sender: TObject);
  private
    procedure AddTab<T: TTabItem>(out Tab: T);
  public
    TabA: TTabItemFrame<TFrameA>;
    TabB: TTabItemFrame<TFrameB>;
    TabC: TTabItemFrame<TFrameC>;
  end;

var
  FormMain: TFormMain;

implementation

procedure TFormMain.FormCreate(Sender: TObject);
begin
  AddTab(TabA);
  AddTab(TabB);
  AddTab(TabC);

  TabA.Activate;
end;

procedure TFormMain.AddTab<T>(out Tab: T);
begin
  Tab:= TabControl.Add(T) as T;
end;

---------------------------------------------------------------------
unit _FrameBase;

interface

uses
  System.Classes, FMX.Forms, FMX.TabControl;

type
  TFrameBase = class abstract(TFrame)
  public
    class function GetTitle: string; virtual; abstract;
  end;

  TTabItemFrame<T: TFrameBase> = class(TTabItem)
  private
    FFrame: T;
  protected
    procedure Hide; override;
    procedure Show; override;
  public
    constructor Create(AOwner: TComponent); override;
    function Activate: T;
    property Frame: T read FFrame;
  end;

implementation

{ TTabItemFrame }

constructor TTabItemFrame<T>.Create(AOwner: TComponent);
begin
  inherited;
  Text:= T.GetTitle;
  Visible:= False;
end;

function TTabItemFrame<T>.Activate: T;
begin
  Visible:= True;
  TabControl.ActiveTab:= Self;
  Result:= FFrame;
end;

procedure TTabItemFrame<T>.Hide;
begin
  inherited;
  FFrame.DisposeOf;
  FFrame:= nil;
end;

procedure TTabItemFrame<T>.Show;
begin
  inherited;
  FFrame:= T.Create(Self);
  FFrame.Parent:= Self;
end;

end.

---------------------------------------------------------------------

type
  TFrameA = class(TFrameBase)
    Label1: TLabel;
  public
    class function GetTitle: string; override;
  end;

implementation

// if it's necessary to access components or methods of
// any other frame or the main form directly
uses
  _FormMain;

//...

Update: I decided to use forms instead of frames for my FMX application because frames cannot use styles at design time. A side effect is that instead of the class function I can use the form caption for the tab title.

Embedding a form into a tabitem is a little tricky:

constructor TFormTabBase.Create(AOwner: TComponent);
begin
  inherited;
  while ChildrenCount > 0 do Children[0].Parent:= AOwner as TTabItem;
end;

procedure TTabItemForm<T>.Show;
begin
  inherited;
  FFormTab:= T.Create(Self);
  Text:= FFormTab.Caption;
end;
maf-soft
  • 2,335
  • 3
  • 26
  • 49
  • 2
    Not sure I would free on hide because once you have created the frame it is not worth having to create it again. Instead I would test if the appropriate var is assigned and then create if it has not already been created. That means you just need to override show, not hide. Personally I don't like the _FormMain being used in the implementation part. I know that sometimes it is necessary/pragmatic, particularly when maintaining legacy code, but to me it usually indicates bad design. Generally though the idea stacks up. – Dsm Feb 20 '18 at 16:28
  • @Dsm, thanks, I agree. It depends on the requirements, if freeing the frame is good or not, this is just an example. And yes, it's generally no good design to directly access other frames components; instead there should be clear interfaces between the frames. In my scenario, the pragmatic way was ok this time and I wanted to share it because of the helpful use of generics. And don't forget, this was intended to be an improvement to having everything together in the main form... – maf-soft Feb 20 '18 at 18:26
  • I would still remove **uses _FormMain;** It adds nothing to your answer and may encourage bad practice. – Dsm Feb 21 '18 at 09:20
  • I still agree that you will not win any prizes with this design, but adding the necessary indirection, interfaces, messages, would encourage former users of the "everything in TabControl" approach not to consider it because it becomes more complicated (and perhaps impossible or too much work to transform an existing solution, which was the intention in the question). – maf-soft Feb 22 '18 at 10:12
  • Regarding your suggestion of removal of `uses _FormMain`: then you maybe missed a crucial point why I did it exactly like this and with Generics. The intention was to make it very easy to directly access controls/methods of other frames without using typecasts or even without including their unit. If it's bad to access frames directly from other frames, it's as bad to access them from the main form, so the whole concept of having those typed variables there would be wrong - but that's the main part of my answer :) @Dsm – maf-soft Feb 22 '18 at 10:19