A TBitButton equivalent for FireMonkey

While porting my VCL applications to FireMonkey I needed to find a replacement for the TBitButton from “Additional” palette and so decided to try building my own custom TButton with a graphic on it.

I was only looking for a stop gap minimal implementation as for any new work I would probably use some fabulous FireMonkey effect rather than a TBitButton look alike. You can find my TISBitButton inside the zip file containing my DFM to FMX convertor.

Creating my new TISBitButton was not as straight forward as my earlier custom string grid component.  In this instance the new component is essentially implemented as a style change to a TButton adding a TRoundRect containing a TImage to a TButton Style in the style designer and placing the information in a style file “IsBitButton.win.style”. I maintained the win.style extension so I can change the style for different platforms as detailed in the example at http://docwiki.embarcadero.com/RADStudio/en/Creating_a_FireMonkey_Component.

The style originally failed to set HitTest but this caused a problem (see later). The addition to the Style now consists of:

object TRoundRect
 StyleName = 'isbitbuttonimrext'
 Align = alRight
 Position.Point = '(70,0)'
 Width = 50.000000000000000000
 Height = 27.000000000000000000
 HitTest = False
 object ButtonImage: TImage
   StyleName = 'isbitbuttonimage'
   Align = alClient
   HitTest = False
   Width = 50.000000000000000000
   Height = 27.000000000000000000
 end
end

The extended style file is saved as “IsBitButton.win.style”

To wrap that style into the Delphi implementation we need to compile the style file into a res file by adding to the package project the file “IsBitButton.win.rc” containing the single line

ISBitButtonStyle RCDATA "IsBitButton.win.style"

The resulting “IsBitButton.win.res” file is included in the implementation section as follows

{$IFDEF MSWINDOWS}
{$R *.win.res}
{$ENDIF}

Registering the new object and adding an implementation of GetStyleObject completes the new object according to the example.

function TIsBitButton.GetStyleObject: TControl;
var
  S: TResourceStream;
const
  Style = 'ISBitButtonStyle';
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;

Compiling and installing the package containing the unit file “IsBitButton.pas” and the RCData file “IsBitButton.win.rc” allows us to add a BitButton to a form and set its onClick parameters etc. One issue that was noted at first was that the new image part of the button was not active – hovering over it did not set off the glow animation and clicking on this part of the button did not activate the onclick event. The HitTest is by default true so all it required was adding the line HitTest=false to each new object in the style file.

The bitmap applied to the object could be changed in the style editor but my intention was to expose this as a property of the component.  To achieve this

Step One: Establish the property in the component object


TIsBitButton = class(TButton)
private
  FBitmap:TBitmap;
  procedure SetBitmap(Value:TBitmap);
published
  property BitMap:TBitmap read FBitmap write SetBitmap;
end;

Step Two: Override ApplyStyle to link the bitbat with the style

procedure TIsBitButton.ApplyStyle;
var
  T: TFmxObject;
begin
  inherited;
  T := FindStyleResource('isbitbuttonimage');
  if (T <> nil) and (T is TImage) then
  begin
    if FBitmap<>nil then
      TImage(T).Bitmap.Assign(FBitmap)
    else
      Bitmap:=  TImage(T).Bitmap;
    repaint;
  end;
end;

This code locatates the TImage object using the StyleName ‘isbitbuttonimage’ allocated in the Style File and then copies the value

Step Three: Make sure SetBitMap updates the control

procedure TIsBitButton.SetBitmap(Value: TBitmap);
begin
  if FBitmap <> nil then
    FBitmap.Assign(Value);
  ApplyStyle;
  FRecalcUpdateRect:=true;
  Repaint;
end;

I think this should have been all that was required but editing the bitmap property of our new component in the FireMonkey IDE did not cause the on form image to change. The image was changed in the FMX file and when the application ran or the form reload the new bitmap was there but the current IDE presentation was not updated.

I eventually tracked this down to what I would consider a bug in the IDE property editor for images. The property editor seems to accept a pointer to the bitmap and make changes directly to that object rather than replacing the object. The result is that the SetBitmap routine is not called and the control is not updated.

Step Four: Implement the TBitMap.OnChange event

I then wrote an event handler for BitMapChanged and allocated it at create time to the TBitMap.OnChange event.

procedure TIsBitButton.BitMapChanged(Sender: TObject);
begin
  ApplyStyle;
  FRecalcUpdateRect:=true;
  Repaint;
end;

constructor TIsBitButton.Create(AOwner: TComponent);
var
  T: TFmxObject;
begin
  inherited;
  FBitmap:=TBitmap.Create(10,10);
  FBitmap.Canvas.DrawLine(PointF(2.0,2.0),Pointf(8.0,8.0),5);
  FBitmap.Canvas.DrawLine(PointF(8.0,2.0),Pointf(2.0,8.0),5);
  FBitmap.Canvas.DrawLine(PointF(2.0,8.0),Pointf(8.0,8.0),5);
  FBitmap.Canvas.DrawLine(PointF(2.0,2.0),Pointf(8.0,2.0),5);
  FBitmap.Canvas.DrawLine(PointF(8.0,2.0),Pointf(8.0,8.0),5);
  FBitmap.Canvas.DrawLine(PointF(2.0,2.0),Pointf(2.0,8.0),5);
  FBitmap.OnChange:=BitMapChanged;
end;

Ideally the create event should also find the current bitmap in the style object and apply that to the bitmap property but the style information cannot be accessed at this time.  The default bitmap is drawn in the constructor and will not reflect any style changes. I did try other logic leaving FBitmap nil unless specifically changed by the FMX file so a nil bitmap could be allocated the Style value but could not resolve the interdependencies.

Step 5: Package and install

requires
  rtl,
  fmx;
contains
  IsAdvStringGrid in 'IsAdvStringGrid.pas'
  IsBitButton in 'IsBitButton.pas'
  IsBitButton.win.style
  IsBitButton.win.rc

It is probably worth mentioning a couple of other issues that caused me grief. They seem so obvious now but not so at the time.

As suggested in the example I included the files “IsBitButton.win.rc” and “IsBitButton.win.style” in the Package file and compiling the package automatically produced the “IsBitButton.win.res” file.  Changing the “.rc” resulted in a new version of the “.res” file but changes to the “.style” did not result in a new automatically generated “.res” file.

I also ran into trouble when a file from an early attempt was included in a uses clause in part of the test project. This resulted in two style files being included with the same resource name. A warning was generated at runtime only and was easy to miss. The combination of these two issues meant that attempted changes to the style file seemed to cause erratic behaviour in the test project.

Posted in FMX Tagged with: , , ,
2 comments on “A TBitButton equivalent for FireMonkey
  1. J-L says:

    Thanks for this tutorial … FMX is hard for newbies

    • Roger says:

      Your welcome but a word of warning this example used XE2. When I came to compile it in XE3 things had changed quite a lot. I have not tried to make it work in XE3 as it was mostly part of an early play with FireMonkey.