March 18, 2014

On the fly form

This article explain how to create a form on the fly. Such a form is created by code, without using the designer.

You will surely ask why you would do that! Granted the Delphi form designer is very easy to use. But in some cases it is not practical because it creates several files and makes more difficult, for example, to hide the form in a component.

This is exactly the case I had: in a component I needed a small helper form. I wanted the code to be in the component source itself. You may already use an "on the fly" form without knowing: InputQuery and InputBox are already such a form. Their code is located in the Dialogs unit.

Actually, there is not much magic to use to create an "on the fly" form. After all a form is just an object as any other object. The only special thing is related to the constructor. The form constructor insist on loading a form from a resource as it had been created with the designer and his DFM file. If there is no DFM and you call Create, you get an exception EResNotFound with a resource name equal to your form class name.

Instead of Create, you have to call CreateNew. This is easy but somewhat misleading. This is why the code I present below override the standard constructor named Create and call CreateNew. This way you can use the standard constructor without worrying.

The sample code I use to demonstrate the "on the fly" form is very easy. The form is a simple form named TComboBoxInputForm with a combobox and a pair of buttons for OK/Cancel. I created an Execute method which calls ShowModal after having initialized the combobox with an array of const values passed as argument.

Here is the code showing the use:

procedure TForm1.Button1Click(Sender: TObject);
var
    Form    : TComboBoxInputForm;
    AResult : String;
begin
    Form := TComboBoxInputForm.Create(Self);
    try
        if Form.Execute(['Delphi', 123, 2.3, TRUE],
                        Edit1.Text, AResult,
                        'Select value') then
            Edit1.Text := AResult;
    finally
        FreeAndNil(Form);
    end;
end;

This code uses TComboBoxInputForm to ask the user to select a value in a list.

The source code is very simple and looks like any other form, except since there is no DFM file, all used components are created and initialized from the constructor.

type
    TComboBoxInputForm = class(TCustomForm)
        ComboBox     : TComboBox;
        OKButton     : TButton;
        CancelButton : TButton;
    protected
        procedure OKButtonClick(Sender: TObject);
        procedure CancelButtonClick(Sender: TObject);
    public
        constructor Create(AOwner : TComponent); override;
        function Execute(const Values   : array of const;
                         const ADefault : String;
                         out   AResult  : String;
                         const ACaption : String = '') : Boolean;
    end;

    TForm1 = class(TForm)
        Button1: TButton;
        Edit1: TEdit;
        procedure Button1Click(Sender: TObject);
    end;

var
    Form1: TForm1;

implementation

{$R *.dfm}

{ TComboBoxInputForm }

constructor TComboBoxInputForm.Create(AOwner: TComponent);
begin
    // We need the following lines to avoid the EResNotFound exception
    // because we have no DFM resource for our form
    GlobalNameSpace.BeginWrite;
    try
        CreateNew(AOwner);
    finally
        GlobalNameSpace.EndWrite;
    end;
    BorderStyle          := bsToolWindow;
    ComboBox             := TComboBox.Create(Self);
    ComboBox.Parent      := Self;
    ComboBox.Top         := 16;
    ComboBox.Left        := 32;
    ComboBox.Style       := csDropDownList;
    OKButton             := TButton.Create(Self);
    OKButton.Parent      := Self;
    OKButton.Top         := ComboBox.Top + ComboBox.Height + 8;
    OKButton.Left        := ComboBox.Left;
    OKButton.Width       := 50;
    OKButton.Caption     := '&OK';
    OKButton.Default     := TRUE;
    OKButton.OnClick     := OKButtonClick;
    CancelButton         := TButton.Create(Self);
    CancelButton.Parent  := Self;
    CancelButton.Top     := OKButton.Top;
    CancelButton.Left    := OkButton.Left + OKButton.Width + 16;
    CancelButton.Width   := OKButton.Width;
    CancelButton.Caption := '&Cancel';
    CancelButton.Cancel  := TRUE;
    CancelButton.OnClick := CancelButtonClick;
    ComboBox.Width       := OKButton.Width + CancelButton.Width + 16;
    ClientWidth          := ComboBox.Left + ComboBox.Width + ComboBox.Left;
    ClientHeight         := ComboBox.Top + OKButton.Top + OKButton.Height;
end;

procedure TComboBoxInputForm.OKButtonClick(
    Sender : TObject);
begin
    Close;
    ModalResult := mrOK;
end;

procedure TComboBoxInputForm.CancelButtonClick(
    Sender : TObject);
begin
    Close;
    ModalResult := mrCancel;
end;

function TComboBoxInputForm.Execute(
    const Values   : array of const;
    const ADefault : String;
    out   AResult  : String;
    const ACaption : String) : Boolean;
var
    I    : Integer;
const
    BoolToStr : array [Boolean] of String = ('FALSE', 'TRUE');
begin
    Caption   := ACaption;
    ComboBox.Items.Clear;
    I := Low(Values);
    while I <= High(Values) do begin
        case Values[I].VType of
        vtUnicodeString: ComboBox.Items.Add(Values[I].VPWideChar);
        vtBoolean:       ComboBox.Items.Add(BoolToStr[Values[I].VBoolean]);
        vtInteger:       ComboBox.Items.Add(IntToStr(Values[I].VInteger));
        vtExtended:      ComboBox.Items.Add(FloatToStr(Values[I].VExtended^));
        end;
        Inc(I);
    end;
    // Preselect default item
    I := ComboBox.Items.IndexOf(ADefault);
    if I >= 0 then
        ComboBox.ItemIndex := I;

    Result := ShowModal = mrOK;
    if Result then
        AResult := ComboBox.Text
    else
        AResult := ADefault;
end;






Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be

2 comments:

Thomas Mueller said...

And for generating the code creating the components there is GExperts Components to Code wizard...

Unknown said...

So you basically created custom dialog form by using modal form and modal results.

But why don't you work with modal results in proper way?
Proper way to achieve this would be to assign modal results to each Button. So whenever you click on any of these butons selected modal result is set to its parent (Modal Form in your case).
And as soon as ModalResult of any Modal Form is changed the Form gets automatically closed.
And if you set CancelButton.Cancel to True pressing escape would act the same as would clicking on that Button.

This way you get rid of both OnClick events from your Buttons and make your form to actually behave as it is intendet for modalForms.