Show: Delphi C++
Display Preferences

Creating a FireMonkey Component (Delphi)

From RAD Studio XE2
Jump to: navigation, search

Go Up to FireMonkey Components Guide


Contents

Example: Dialog Button Panel

Both Windows and Mac OS X promote interface guidelines, which include the order and placement of standard Do/Don't/Cancel buttons in dialog boxes. A Save dialog box with "Save", "Don't Save", and "Cancel" as the choices is a common example. The guidelines for these two platforms are slightly different. The TDialogButtonPanel custom component created here encapsulates those differences with platform-specific style files, included as RCDATA.

Create the Projects

Start with a clean slate by closing all files in the IDE ( File > Close All). Choose a location and name for a new folder to hold all the component files.

First, create a package project:

  1. Select File > New > Package - Delphi
  2. Select File > Save Project As and navigate to the folder location. Create a new folder, navigate into that folder, and save the project as DialogButtonsPackage
  3. Right-click the newly created project group in the Project Manager and select Save Project Group As. Save as DialogButtonsGroup in the same directory.

Next, create the skeleton component. Choose Component > New Component to open the New Component wizard. In its pages:

  1. Choose FireMonkey for Delphi as the framework and personality
  2. Select TPanel as the ancestor
  3. Set the class name to TDialogButtonPanel. For the unit name, click the ellipsis, make sure you're in the component folder, and save the unit file name as DialogButtons
  4. Choose Add unit to DialogButtonsPackage.dproj project and Finish
  5. If prompted to Save DialogButtons As, click Save
  6. If prompted to enable the FireMonkey framework, click Yes

This creates a unit with the component class, and a Register procedure to add that class to the component palette.

Now create a work/test project:

  1. Right-click the project group and choose Add New Project.
  2. In the New Items dialog box, select Delphi Projects > FireMonkey HD Application
  3. Select File > Save As, navigate to the component folder, and save the form unit as PanelTest
  4. Select File > Save Project As, ensure you're in the component folder, and save the project as PanelTester
  5. Right-click the project, select Add New > FireMonkey HD Form. Save the unit as TestDialog
  6. Right-click the project, select Add New > FireMonkey HD Form. Save the unit as PanelDesign

Design the Panel

In the PanelDesign form, sketch out the new component.

Current Windows and Mac OS X design guidelines place dialog buttons in the bottom right corner. The order of the buttons differs by platform:

  • Windows: Do, Don't, Cancel
  • Mac: Don't, Cancel, Do.
    Also there should be extra distance between Don't and Cancel when there is potential data loss.

Bottom-right-aligned buttons that move as the form is resized can be achieved with the following nested layout:

  • A TPanel aligned alBottom, with the correct Height; that contains:
    • A TLayout aligned alRight (so Height does not matter), with the correct Width; that contains:
      • Another TLayout aligned alTop, with Top Padding and the correct Height; that finally contains:
        • A TButton with Default= True, for Do
        • A second TButton with Cancel = True
        • A third TButton for Don't

(Note that this button order matches neither Windows nor Mac. You can adjust the order later.) Each button is aligned alLeft, so they "stack" to the left, and each has appropriate Left Padding. To get a better sense of the width of the buttons, include a placeholder word for the verb in the Do/Don't button labels. The total width of the buttons, their horizontal padding, and extra space to the right of the last button must be assigned as the Width of the right-aligned layout. The Height of the other two layouts must reflect the height of the buttons, the padding above, and for the outer panel, the space below them.

You can try creating this nested layout manually; or you can drop a panel, two layouts, and three buttons on the form, save it, and edit the .fmx file so that the objects are arranged like this:

  object Panel1: TPanel
    Align = alBottom
    Position.Point = '(0,352)'
    Width = 600.000000000000000000
    Height = 48.000000000000000000
    TabOrder = 0
    object Layout1: TLayout
      Align = alRight
      Position.Point = '(312,0)'
      Width = 288.000000000000000000
      Height = 48.000000000000000000
      object Layout2: TLayout
        Align = alTop
        Position.Point = '(0,12)'
        Width = 288.000000000000000000
        Height = 22.000000000000000000
        Padding.Rect = '(0,12,0,0)'
        object Button1: TButton
          Align = alLeft
          Position.Point = '(12,0)'
          Width = 80.000000000000000000
          Height = 22.000000000000000000
          Padding.Rect = '(12,0,0,0)'
          TabOrder = 0
          StaysPressed = False
          IsPressed = False
          Text = 'Do Verb'
          Default = True
        end
        object Button2: TButton
          Align = alLeft
          Position.Point = '(104,0)'
          Width = 80.000000000000000000
          Height = 22.000000000000000000
          Padding.Rect = '(12,0,0,0)'
          TabOrder = 1
          StaysPressed = False
          IsPressed = False
          Text = 'Cancel'
          Cancel = True
        end
        object Button3: TButton
          Align = alLeft
          Position.Point = '(196,0)'
          Width = 80.000000000000000000
          Height = 22.000000000000000000
          Padding.Rect = '(12,0,0,0)'
          TabOrder = 2
          StaysPressed = False
          IsPressed = False
          Text = 'Don'#39't Verb'
        end
      end
    end
  end

Resize the form in the Form Designer to verify that the layout behaves properly.

Convert to Styles

You can manually recreate the layout as a style with the Style Designer. At the same time, you can add some final details:

  • Use a TRectangle as the top-level component to render the TPanel
    • Set the Fill.Kind and Stroke.Kind to bkNone so that the panel is transparent by default.
  • Set the ModalResult code for each button:
    • mrOK (which is 1) for the Do button
    • mrCancel (which is 2) for the Cancel button
    • There is no built-in code for Don't, but the negative of mrOK, which equals -1, is appropriate
  • Set the StyleName for each button: "DoButton", "CancelButton", and "DontButton"
  • Place the buttons in the appropriate order for each platform.
    • For the Mac, place extra Right Padding on the first Don't button, and increase the Width of the right-aligned layout to match.

As before, you can modify source code directly as a shortcut. Each .style file contains the content of the style book inside a root TLayout component:

object _1: TLayout
  { All objects would appear here, instead of this comment. Comments aren't allowed in .style files! }
end

So you can make the changes above in the Form Designer, save the form, and copy the object tree from the .fmx and paste it into a .style file. You can then load the file in the Style Designer to verify. Use whatever combination of the Form Designer, file editing, and Style Designer you like. When done, save from the Style Designer to strip out an extraneous info.

The wrapper TLayout is required to load the styles in the Style Designer, but to actually use the style as the default style for a custom component, you'll want to remove the wrapper so that the rectangle (in this case) can be accessed directly. The end result should be two separate platform-specific files:

DialogButtonPanel.win.style DialogButtonPanel_mac.style
  object TRectangle
    StyleName = 'DialogButtonPanelStyle'
    Align = alBottom
    Position.Point = '(0,264)'
    Width = 600.000000000000000000
    Height = 46.000000000000000000
    ClipChildren = True
    HitTest = False
    Fill.Kind = bkNone
    Stroke.Kind = bkNone
    Sides = [sdTop, sdLeft, sdBottom, sdRight]
    object TLayout
      Align = alRight
      Position.Point = '(261,0)'
      Width = 288.000000000000000000
      Height = 46.000000000000000000
      object TLayout
        Align = alTop
        Position.Point = '(0,12)'
        Width = 288.000000000000000000
        Height = 22.000000000000000000
        Padding.Rect = '(0,12,0,0)'
        object TButton
          StyleName = 'DoButton'
          Align = alLeft
          Position.Point = '(12,0)'
          Width = 80.000000000000000000
          Height = 22.000000000000000000
          Padding.Rect = '(12,0,0,0)'
          TabOrder = 0
          StaysPressed = False
          IsPressed = False
          ModalResult = 1
          Text = 'Do Verb'
          Default = True
        end
        object TButton
          StyleName = 'DontButton'
          Align = alLeft
          Position.Point = '(104,0)'
          Width = 80.000000000000000000
          Height = 22.000000000000000000
          Padding.Rect = '(12,0,0,0)'
          TabOrder = 1
          StaysPressed = False
          IsPressed = False
          ModalResult = -1
          Text = 'Don'#39't Verb'
        end
        object TButton
          StyleName = 'CancelButton'
          Align = alLeft
          Position.Point = '(196,0)'
          Width = 80.000000000000000000
          Height = 22.000000000000000000
          Padding.Rect = '(12,0,0,0)'
          TabOrder = 2
          StaysPressed = False
          IsPressed = False
          ModalResult = 2
          Text = 'Cancel'
          Cancel = True
        end
      end
    end
  end
  object TRectangle
    StyleName = 'DialogButtonPanelStyle'
    Align = alBottom
    Position.Point = '(0,264)'
    Width = 600.000000000000000000
    Height = 46.000000000000000000
    ClipChildren = True
    HitTest = False
    Fill.Kind = bkNone
    Stroke.Kind = bkNone
    Sides = [sdTop, sdLeft, sdBottom, sdRight]
    object TLayout
      Align = alRight
      Position.Point = '(249,0)'
      Width = 300.000000000000000000
      Height = 46.000000000000000000
      object TLayout
        Align = alTop
        Position.Point = '(0,12)'
        Width = 300.000000000000000000
        Height = 22.000000000000000000
        Padding.Rect = '(0,12,0,0)'
        object TButton
          StyleName = 'DontButton'
          Align = alLeft
          Position.Point = '(12,0)'
          Width = 80.000000000000000000
          Height = 22.000000000000000000
          Padding.Rect = '(12,0,12,0)'
          TabOrder = 0
          StaysPressed = False
          IsPressed = False
          ModalResult = -1
          Text = 'Don'#39't Verb'
        end
        object TButton
          StyleName = 'CancelButton'
          Align = alLeft
          Position.Point = '(116,0)'
          Width = 80.000000000000000000
          Height = 22.000000000000000000
          Padding.Rect = '(12,0,0,0)'
          TabOrder = 1
          StaysPressed = False
          IsPressed = False
          ModalResult = 2
          Text = 'Cancel'
          Cancel = True
        end
        object TButton
          StyleName = 'DoButton'
          Align = alLeft
          Position.Point = '(208,0)'
          Width = 80.000000000000000000
          Height = 22.000000000000000000
          Padding.Rect = '(12,0,0,0)'
          TabOrder = 2
          StaysPressed = False
          IsPressed = False
          ModalResult = 1
          Text = 'Do Verb'
          Default = True
        end
      end
    end
  end

Style-Resources as RCDATA

Each .style file needs a corresponding platform-specific (one-liner) .rc file, with root names that match the component unit.

For example, here are a Windows .rc file and a Mac .rc file:

  • DialogButtons.win.rc
DialogButtonPanelStyle RCDATA "DialogButtonPanel.win.style"
  • DialogButtons.mac.rc
DialogButtonPanelStyle RCDATA "DialogButtonPanel_mac.style"
  1. Activate the DialogButtonsPackage.bpl project by double-clicking it in the Project Manager.
  2. For each .rc file:
    • Select File > New > Other > Other Files > Text File and in the New File dialog, select .rc Resource File.
    • Add the one line, and save with the correct file name.
    The .rc files should show up in the project tree under the Contains node. (For ease of editing, you can also add the .style files to the project, but doing so is not necessary for the compile; the .style files should be found automatically since they are in the same directory as the .rc files.)
  3. Back in DialogButtons.pas, to load these styles, declare an override to TStyledControl.GetStyleObject and implement it:
  protected
    function GetStyleObject: TControl; override;
 
implementation
 
uses
  Types;
 
{$IFDEF MACOS}
{$R *.mac.res}
{$ENDIF}
{$IFDEF MSWINDOWS}
{$R *.win.res}
{$ENDIF}
 
function TDialogButtonPanel.GetStyleObject: TControl;
var
  S: TResourceStream;
const
  Style = 'DialogButtonPanelStyle';
begin
  if (FStyleLookup = '') then
  begin
    if FindRCData(HInstance, Style) then
    begin
      S := TResourceStream.Create(HInstance, Style, RT_RCDATA);
      try
        Result := TControl(CreateObjectFromStream(nil, S));
        Exit;
      finally
        S.Free;
      end;
    end;
  end;
  Result := inherited GetStyleObject;
end;

The appropriate .res compiled from the .rc file is included with conditional platform directives. The name of the RCDATA item, the same in both files, is a constant in the function.

Note: The StyleName of the root (TRectangle) component in the .style file is coincidentally the same as the RCDATA name. The StyleName is required if style is incorporated in a style book, and self-documents the style, so it is good practice to include it. But when loaded directly, the StyleName of the root is not used and is superfluous.

The function loads the style only if the StyleLookup is blank. It calls FindRCData with the current module's handle—at design-time, it's the package .bpl; at runtime, it's the program's .exe or .dll—because attempting to access non-existent RCDATA raises an exception. If found, a stream is created to read it, and the style-resource object is instantiated from it and returned. Otherwise, the inherited behavior to find a named style or find the default style by class name is used.

With the code in place, right-click the package project in the Project Manager and select Install. This will compile it and add the component to the palette.

Size and Alignment

To test this particular component, the buttons need to be placed in a form opened as a modal dialog box with ShowModal.

  1. Activate the PanelTester.exe project, and open the main form, PanelTest.pas. This form will open the dialog form.
  2. Rename the class (auto-generated as Form1) to TPanelTestForm with Refactor > Rename. That also automatically changes the global reference variable to PanelTestForm.
  3. Then in TestDialog.pas, rename the form (Form2) to TTestDialogForm so that its reference is TestDialogForm.
  4. Switch the dialog form to the Form Designer. Drop a TDialogButtonPanel, found under the Samples category in the Tool Palette, onto the form.
    Note that the panel is neither correctly sized nor bottom-aligned. This is because:
    • The size and alignment of the control that is created is separate from the style-resource component that is used to render it.
    • So even though the root style-resource (the TRectangle) is set to alBottom, in most cases you would not want that because the style-resource is a child of the control, and you would not want the visual realization of that control to be restricted to some part of it, no matter how you try to resize or align the control.
    • When style-resources are applied, their Align property is always set to alContents, so that the style-resources occupy the entire content box of the control they are rendering.
    But what you actually want to do is set the Align property of the custom component, not its style-resource. To do that, declare and implement a constructor. Also declare the default value for the property, and set the Height and Width (with alBottom the Width does not actually matter, but you can set it to a representative minimum):
  public
    constructor Create(AOwner: TComponent); override;
  published
    property Align default TAlignLayout.alBottom;
constructor TDialogButtonPanel.Create(AOwner: TComponent);
begin
  inherited;
  Height := 46;
  Width := 300;
  Align := TAlignLayout.alBottom;
end;
5. To see these changes, re-install the component package: right-click the package project in the Project Manager and select Install again.
6. Then back in the dialog box form, delete the component and add another from the palette. The panel should now be properly sized and aligned.
7. To see your component in action, go back to the main form and drop a button (to open the dialog box) and a label (to see the modal result code).
8. Set the OnClick event handler for the button, as follows:
uses
  TestDialog;
 
procedure TPanelTestForm.Button1Click(Sender: TObject);
var
  ModalResult: Integer;
begin
  ModalResult := TestDialogForm.ShowModal;
  Label1.Text := IntToStr(ModalResult);
end;
9. Now run the PanelTester.exe project and click the button to open the dialog box. Clicking any of its three buttons closes the dialog box, and returns the corresponding modal result.

Custom Properties

The component needs an additional property for the dialog's action verb, like "Save" to appear on the Do button. That verb is also combined with "Don't" on the Don't button. (More complex internationalization is beyond the scope of this topic.)

It would be simpler if you could rely on the presence of the button, and delegate to its Text to hold the verb. But the buttons are not there until the style is applied, and if the style is reapplied, the existing buttons are discarded. So the component needs to hold onto this string itself. Declare and implement a DoVerb property:

uses
  System.SysUtils, System.Classes, FMX.Types, FMX.Controls, System.UITypes;
 
type
  TDialogButtonPanel = class(TPanel)
  private
    FDoVerb: string;
    function GetDoVerb: string;
    procedure SetDoVerb(AString: string);
  protected
    function GetStyleObject: TControl; override;
    procedure ApplyStyle; override;
  public
    constructor Create(AOwner: TComponent); override;
  published
    property DoVerb: string read GetDoVerb write SetDoVerb;
    property Align default TAlignLayout.alBottom;
  end;
 
const
  DoButtonName = 'DoButton';
  DontButtonName = 'DontButton';
  CancelButtonName = 'CancelButton';
  mrDont = -mrOK;
constructor TDialogButtonPanel.Create(AOwner: TComponent);
begin
  inherited;
  Height := 46;
  Width := 300;
  Align := TAlignLayout.alBottom;
  DoVerb := 'Verb';
end;
 
function TDialogButtonPanel.GetDoVerb: string;
var
  Base: TFmxObject;
begin
  Base := FindStyleResource(DoButtonName);
  if Base is TTextControl then
    FDoVerb := TTextControl(Base).Text;
  Result := FDoVerb;
end;
 
procedure TDialogButtonPanel.SetDoVerb(AString: string);
var
  Base: TFmxObject;
resourcestring
  Dont = 'Don'#39't %s';
begin
  FDoVerb := AString;
  Base := FindStyleResource(DoButtonName);
  if Base is TTextControl then
    TTextControl(Base).Text := AString;
  Base := FindStyleResource(DontButtonName);
  if Base is TTextControl then
    TTextControl(Base).Text := Format(Dont, [AString]);
end;
 
procedure TDialogButtonPanel.ApplyStyle;
begin
  inherited;
  SetDoVerb(FDoVerb);
end;

The StyleName for each of the three buttons is declared as a constant, along with the "negative OK" modal result code for the Don't button. The FDoVerb field is added to the object to contain the verb string.

At run time, the access sequence goes like this:

  1. When the application is executed, all its forms are created, including the dialog box form containing the button panel. The panel is instantiated by its object line when loading from the .fmx. The constructor writes the default verb to the property, calling SetDoVerb. The field is set, and no buttons are found.
  2. Assigning the property values from the .fmx, SetDoVerb is called again with the string saved by the designer (e.g. "Save"), changing the field.
  3. When the dialog box is opened, it is painted, and one of the preliminary steps of painting a control is to apply its style if necessary, which includes the first time the control is painted. This ends up calling ApplyStyle, which is overridden here to reapply the verb string from the field. It redundantly reassigns the field, but now that the buttons are present, it also sets the verb in the Do button, and combines "Don't" and the verb in the Don't button.
  4. Rendering the buttons does not go through GetDoVerb, because the buttons that comprise the style are rendered directly, and they already have the appropriate text, assigned by the setter method. The getter method is used primarily by the Form Designer and any run-time code that gets the panel's properties. For completeness, the method will attempt to update the field from the Do button if it exists; but unless the button is manipulated directly (bypassing encapsulation), its text and the field should already be synchronized.

See Also

Personal tools
Previous Versions
In other languages