2011. március 30., szerda

Create and Manage dynamic Forms at Runtime using Class References


Problem/Question/Abstract:

How to dynamicaly create and manage different Forms at runtime in a global manner?

Answer:

If you need to create dynamic forms at runtime and you want to manage them in a global manner, you may have the problem that you don't know how to administrate different form classes. For this case, Delphi comes with special class types of all common objects. But before I go into details, let me create a scenario in which this article may helps you.

  "I'll create an application for my customer to let him administrate serveral kinds of data in a local database. Each data category (such as employees, articles, ...) have to been implemented in a specified form with individual edit fields and tools. I don't like to create a MDI bases application (for some reasons) but the customers should have the possibilty to create more than one form for each category (e.g. opens up to 10 forms with customer informations and 3 forms with article informations). He should refer to each form after a while, so all forms have to been non-modular > the customer can hide or minimize each form. In normal MDI application, Delphi helps you to manage the MDI childs form via the 'ActiveMDIChild' property for example, but in non MDI applications you had to manage all child forms by yourself."

To find a workable solution we had to abstract the layer in which we could manage several kinds of forms. Each Delphi form inherites from TCustomForm so our first solution is to create a method who we pass a form reference to memorize - but how to keep such references? By the way, it's also possible to create a form manually and then pass the handle direct to the management component, but we'll create a method which automatically creates each kind of form. At the end of this article we've created a VCL component called TWindowManager which makes all of the discussed stuff, but now - let's start:

function TWindowManager.CreateForm(const Form: TFormClass;
  Name: string; Show: Boolean = False): TCustomForm;
begin
  if not Form.InheritsFrom(TCustomForm) then
    raise Exception.Create('Invalid FormClass - must be a descendant
      of TCustomForm!');
      Result := TCustomForm(Form.Create(Application));
      if Name <> '' then
        Result.Name := Name;
      // insert code here, to store the reference
      if Show then
        Result.Show;
end;

Okay, but how to use it? First, we've created a normal Delphi application and added a new form called DynForm1 for example. Delphi automatically creates the following entry in the pas unit:

type
  TDynForm1 = class(TForm)
    ...
  end;

For the next step we had to refer to the new unit by included the corresponding unit name to the uses clause. To dynamically create the new form at runtime, you can call the method in a way like:

procedure TMainForm.ButtonDyn1Click(Sender: TObject);
begin
  // create a new (dynamic) form.
  WindowManager.CreateForm(TDynForm1, True);
end;

Don't marvel about the name WindowManager or TWindowManager in the source examples, I've pasted it direct from the component source I've explained earlier.

Do you notice that we have passed the formclass to the method instead of the name or anythink else? It's possible, because the parameter type of the method is TFormClass which is implemented as TFormClass = class of TForm in Delphi's Forms unit.

Now we need a solution to store the form reference:

type
  { TWindowItem }

  PWindowItem = ^TWindowItem;
  TWindowItem = packed record
    Form: Pointer;
  end;

Note:

It's also possible to use a TStringList for example and create items which holds the form handles (or references direct) but it's not a good solutions if you want to search for already existing form (names). Since Version 3 (I'm not sure exactly) Delphi comes with a special container class which gives you some more specific descendants from the TList class. You can use the TObjectList class, derive from it and overwritte the maintenance methods. In this article I use a normal record to store all informations - it's less code to write and you can easily add improved custom informations to store.

The sourcecode of the TWindowManager comes from a Delphi3 implementation I've wrote - if I've some spare time, I'll update it to the newer technology!

Our WindowManager also published a method to directly add already existing form references, so you don't need to create your forms using the CreateForm method:

function TWindowManager.Add(const Form: TCustomForm): Boolean;
var
  WindowItem: PWindowItem;
begin
  Result := True;
  try
    New(WindowItem);
    WindowItem^.Form := Form;
    FWindowList.Add(WindowItem);
  except // wrap up
    Result := True;
  end; // try/except
end;

FWindowList is declared as FWindowList: TList to hold a list of reference records. Followed you'll see to complete sourcode of the TWindowManager - try to understand the individual methods - they are simple. The main trick is the use off class references I've mentioned earlier.

The main component

unit WindowMng;

interface

uses
  Classes, Forms, SysUtils, Windows;

type
  { TWinNotifyEvent }

  TWinNotifyEvent = procedure(Sender: TObject; Form: TCustomForm) of object;

  { TWindowItem }

    // I used a packed record to be more flexible for futher improvements
    // which may need to store additional informations.

  PWindowItem = ^TWindowItem;
  TWindowItem = packed record
    Form: Pointer;
  end;

  { TWindowManager }

  TWindowManager = class(TComponent)
  private
    { Private declarations }
    FAutoNotification: Boolean;
    FLastIndex: Integer;
    FWindowList: TList;
    FOnFormAdded: TWinNotifyEvent;
    FOnFormHandled: TNotifyEvent;
    FOnFormRemoved: TWinNotifyEvent;
  protected
    { Protected declarations }
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
    function GetFormByIndex(Index: Integer): TCustomForm; virtual;
    function GetWindowItemByIndex(Index: Integer): PWindowItem; virtual;
    function GetWindowItemByForm(const Form: TCustomForm): PWindowItem; virtual;
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function Add(const Form: TCustomForm): Boolean; overload;
    function Count: Integer;
    function CreateForm(const Form: TFormClass; Name: string; Show: Boolean = False):
      TCustomForm; overload;
    function CreateForm(const Form: TFormClass; Show: Boolean = False): TCustomForm;
      overload;
    function Exists(const Form: TCustomForm): Boolean;
    function Remove(const Form: TCustomForm): Boolean;
    function Restore(const Index: Integer): Boolean; overload;
    function Restore(const Form: TCustomForm): Boolean; overload;
    property Forms[Index: Integer]: TCustomForm read GetFormByIndex; default;
  published
    { Published declarations }
    property AutoNotification: Boolean read FAutoNotification write FAutoNotification;
    property OnFormAdded: TWinNotifyEvent read FOnFormAdded write FOnFormAdded;
    property OnFormHandled: TNotifyEvent read FOnFormHandled write FOnFormHandled;
    property OnFormRemoved: TWinNotifyEvent read FOnFormRemoved write FOnFormRemoved;
  end;

procedure Register;

implementation

// -----------------------------------------------------------------------------

procedure Register;
begin
  RegisterComponents('Freeware', [TWindowManager]);
end;

// -----------------------------------------------------------------------------

{ TWindowManager }

constructor TWindowManager.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FAutoNotification := False;
  FLastIndex := -1;
  FWindowList := TList.Create;
end;

destructor TWindowManager.Destroy;
begin
  FWindowList.Free;
  inherited Destroy;
end;

procedure TWindowManager.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  if (FAutoNotification) and (AComponent <> nil) and (Operation = opRemove)
    and (AComponent is TCustomForm) and (Exists(TCustomForm(AComponent))) then
    Remove(TCustomForm(AComponent));
  inherited Notification(AComponent, Operation);
end;

function TWindowManager.Add(const Form: TCustomForm): Boolean;
var
  WindowItem: PWindowItem;
begin
  Result := False;
  if not Exists(Form) then
  try
    New(WindowItem);
    WindowItem^.Form := Form;
    FWindowList.Add(WindowItem);
    if FAutoNotification then
      Form.FreeNotification(Self);
    Result := True;
    if assigned(FOnFormAdded) then
      FOnFormAdded(Self, Form);
    if assigned(FOnFormHandled) then
      FOnFormHandled(Self);
  except // wrap up
  end; // try/except
end;

function TWindowManager.Count: Integer;
begin
  Result := FWindowList.Count;
end;

function TWindowManager.CreateForm(const Form: TFormClass; Name: string; Show: Boolean
  = False): TCustomForm;
begin
  if not Form.InheritsFrom(TCustomForm) then
    raise
      Exception.Create('Invalid FormClass - must be a descendant of TCustomForm!');
  Result := TCustomForm(Form.Create(Application));
  if Name <> '' then
    Result.Name := Name;
  Add(Result);
  if Show then
    Result.Show;
end;

function TWindowManager.CreateForm(const Form: TFormClass; Show: Boolean = False):
  TCustomForm;
begin
  Result := CreateForm(Form, '', Show);
end;

function TWindowManager.Exists(const Form: TCustomForm): Boolean;
begin
  Result := GetWindowItemByForm(Form) <> nil;
end;

function TWindowManager.GetFormByIndex(Index: Integer): TCustomForm;
var
  WindowItem: PWindowItem;
begin
  Result := nil;
  WindowItem := GetWindowItemByIndex(Index);
  if WindowItem <> nil then
    Result := TCustomForm(WindowItem^.Form);
end;

function TWindowManager.GetWindowItemByIndex(Index: Integer): PWindowItem;
begin
  Result := nil;
  if Index < Count then
    Result := PWindowItem(FWindowList[Index]);
end;

function TWindowManager.GetWindowItemByForm(const Form: TCustomForm): PWindowItem;
var
  iIndex: Integer;
begin
  Result := nil;
  FLastIndex := -1;
  for iIndex := 0 to FWindowList.Count - 1 do
    if GetWindowItemByIndex(iIndex)^.Form = Form then
    begin
      FLastIndex := iIndex;
      Result := GetWindowItemByIndex(FLastIndex);
      Break;
    end;
end;

function TWindowManager.Remove(const Form: TCustomForm): Boolean;
var
  WindowItem: PWindowItem;
begin
  Result := False;
  WindowItem := GetWindowItemByForm(Form);
  if WindowItem <> nil then
  try
    FWindowList.Delete(FLastIndex);
    Dispose(WindowItem);
    Result := True;
    if assigned(FOnFormRemoved) then
      FOnFormRemoved(Self, Form);
    if assigned(FOnFormHandled) then
      FOnFormHandled(Self);
  except // wrap up
  end; // try/except
end;

function TWindowManager.Restore(const Form: TCustomForm): Boolean;
begin
  Result := False;
  if (Form <> nil) and (Exists(Form)) then
  try
    if IsIconic(Form.Handle) then
      Form.WindowState := wsNormal;
    Form.SetFocus;
    Result := True;
  except // wrap up
  end; // try/except
end;

function TWindowManager.Restore(const Index: Integer): Boolean;
begin
  Result := Restore(GetFormByIndex(Index));
end;

end.

To show you the in more detail how to work with this component, followed you'll find a demo application with two additional forms. You don't need to install the component to a package, I'll create it at runtime:

The project file

program WMDemo;

uses
  Forms,
  MainFrm in 'MainFrm.pas' {MainForm},
  WindowMng in 'WindowMng.pas',
  DynFrm1 in 'DynFrm1.pas' {DynForm1},
  DynFrm2 in 'DynFrm2.pas' {DynForm2};

{$R *.res}

begin
  Application.Initialize;
  Application.CreateForm(TMainForm, MainForm);
  Application.Run;
end.

The MainForm file

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, WindowMng;

type
  TMainForm = class(TForm)
    ButtonDyn1: TButton;
    GroupBoxForms: TGroupBox;
    ListBoxForms: TListBox;
    ButtonHelloWorld: TButton;
    ButtonDyn2: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure ButtonDyn1Click(Sender: TObject);
    procedure ListBoxFormsDblClick(Sender: TObject);
    procedure ButtonHelloWorldClick(Sender: TObject);
    procedure ButtonDyn2Click(Sender: TObject);
  private
    { Private declarations }
    WindowManager: TWindowManager;
    procedure RedrawFormList(Sender: TObject);
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

uses
  DynFrm1, DynFrm2;

{$R *.dfm}

procedure TMainForm.FormCreate(Sender: TObject);
begin
  // create WindowManager
  WindowManager := TWindowManager.Create(Self);

  // enable 'AutoNotification'. If this feature is turned on,
  // WindowManager will receive a notification if a form was closed
  // by the user, so it can fire events to recorgnize this.
  // We use the 'OnFormHandled' event to redraw out ListBox.
  WindowManager.AutoNotification := True;

  // link event handler to update out ListBox.
  WindowManager.OnFormHandled := RedrawFormList;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  // destroy WindowManager
  WindowManager.Free;
end;

procedure TMainForm.RedrawFormList(Sender: TObject);
var
  i: Integer;
begin
  // get all available forms and display them.
  // we also stores the object reference to enable the 'restore' function
  // if the user double-clicked on an item.
  ListBoxForms.Clear;
  for i := 0 to WindowManager.Count - 1 do
    ListBoxForms.AddItem(WindowManager.Forms[i].Name, WindowManager.Forms[i]);
end;

procedure TMainForm.ButtonDyn1Click(Sender: TObject);
begin
  // create a new (dynamic) form.
  WindowManager.CreateForm(TDynForm1, True);
end;

procedure TMainForm.ButtonDyn2Click(Sender: TObject);
begin
  // create a new (dynamic) form.
  WindowManager.CreateForm(TDynForm2, True);
end;

procedure TMainForm.ListBoxFormsDblClick(Sender: TObject);
var
  ClickForm: TCustomForm;
begin
  // extract the 'clicked' form.
  with ListBoxForms do
    ClickForm := TCustomForm(Items.Objects[ItemIndex]);

  // restore the form to the top order.
  // we used the WindowManager method 'Restore' to be sure
  // that the form will be restored also if it was iconized
  // before.
  WindowManager.Restore(ClickForm);
end;

procedure TMainForm.ButtonHelloWorldClick(Sender: TObject);
begin
  // check, if any registered forms exists.
  if WindowManager.Count = 0 then
  begin
    ShowMessage('No dynamic Forms exists - please create one!');
    Exit;
  end;

  // check, if the first available form is 'DynForm1'.
  // if true, call the HelloWorld method.
  if WindowManager.Forms[0] is TDynForm1 then
    TDynForm1(WindowManager.Forms[0]).HelloWorld
  else
    ShowMessage('The first Form is not a "Dynamic Form I"!');
end;

end.

The MainForm resource file

object MainForm: TMainForm
  Left = 290
    Top = 255
    BorderStyle = bsSingle
    Caption = 'MainForm'
    ClientHeight = 229
    ClientWidth = 510
    Color = clBtnFace
    Font.Charset = DEFAULT_CHARSET
    Font.Color = clWindowText
    Font.Height = -11
    Font.Name = 'MS Sans Serif'
    Font.Style = []
    OldCreateOrder = False
    Position = poScreenCenter
    OnCreate = FormCreate
    OnDestroy = FormDestroy
    DesignSize = (
    510
    229)
    PixelsPerInch = 96
    TextHeight = 13
    object ButtonDyn1: TButton
    Left = 16
      Top = 16
      Width = 121
      Height = 25
      Caption = 'Create Dynamic Form I'
      TabOrder = 0
      OnClick = ButtonDyn1Click
  end
  object GroupBoxForms: TGroupBox
    Left = 16
      Top = 56
      Width = 481
      Height = 169
      Anchors = [akLeft, akTop, akRight, akBottom]
      Caption = 'Available Forms (Double-Click to restore)'
      TabOrder = 1
      object ListBoxForms: TListBox
      Left = 2
        Top = 15
        Width = 477
        Height = 152
        Align = alClient
        BorderStyle = bsNone
        ItemHeight = 13
        ParentColor = True
        TabOrder = 0
        OnDblClick = ListBoxFormsDblClick
    end
  end
  object ButtonHelloWorld: TButton
    Left = 344
      Top = 16
      Width = 153
      Height = 25
      Caption = 'Fire ''HelloWorld'' on DynForm1'
      TabOrder = 2
      OnClick = ButtonHelloWorldClick
  end
  object ButtonDyn2: TButton
    Left = 144
      Top = 16
      Width = 121
      Height = 25
      Caption = 'Create Dynamic Form II'
      TabOrder = 3
      OnClick = ButtonDyn2Click
  end
end

The DynForm1 file

unit DynFrm1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs;

type
  TDynForm1 = class(TForm)
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    { Private declarations }
  public
    { Public declarations }
    procedure HelloWorld;
  end;

var
  DynForm1: TDynForm1;

implementation

{$R *.dfm}

procedure TDynForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  // be sure that our form will be freed.
  Action := caFree;
end;

procedure TDynForm1.HelloWorld;
begin
  ShowMessage('HelloWorld method was fired!');
end;

end.

The DynForm2 file

unit DynFrm2;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs;

type
  TDynForm2 = class(TForm)
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  DynForm2: TDynForm2;

implementation

{$R *.dfm}

procedure TDynForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  // be sure that our form will be freed.
  Action := caFree;
end;

end.

Hope this article helps you to understand how dynamic forms can be created and managed.

Nincsenek megjegyzések:

Megjegyzés küldése