My First FireMonkey Custom Control: TBitmapSpeedButton

One thing which surprises me about the FireMonkey library is that the TSpeedButton has no option to display an image (indeed, none of the button controls do). Since I’m at the stage where I need such a control for the interface for MonkeyStyler I decided this would be the perfect opportunity to create my first FireMonkey custom control.
So, lets look at what we want: We want to descend from TSpeedButton, so we can easily add the control to a tool bar (actually a FireMonkey toolbar can accept any control, but the styling for a speed button is closest to what I want). We want a bitmap image, and we want the option to have the image to the left, right, top or bottom of the text, or to have the image centered with no text.
I started by creating the style. This gets the basics of styling out of the way for when we create the code to interact with it. Of course, at this stage styling can be quite basic, simply adding the controls we need and setting basic properties.
The Style
Copy the styling for SpeedButtonStyle to a new style file (bitmapspeedbutton.style) from Windows7.style (Note: I did this with a couple of clicks in MonkeyStyler but at the time of writing you’ll have to make do with cutting and pasting from the source files).
Here is the style for a TSpeedButton:

We want to add our image at the highest level below the root TLayout. At this position it can interact with the TText as we adjust it’s position. We could just add the TImage directly, but if we did, adjusting the Align property would change it’s size. And unless we get messy with the padding we would get undesired stretching of the image.
So, what we’ll do is add a TLayout, and add the TImage as a child:

Set the TImage’s properties:
Height = 24 (our default image size)
Width = 24
Align = alCenter (so it will be centered in the TLayout)
StyleName = image (so we can access it from code)
HitTest = False (to let mouse data pass through to the underlying components)
WrapMode = iwFit (Should be set already. Images will be enlarged/reduced to the correct size)
For the parent TLayout:
StyleName = imagelayout (so we can access it from code)
and for the root TLayout:
StyleName = speedbuttonbitmapstyle (the component name minus the leading T and with style appended - see below)
.
Save the file, and load it into the StyleBook for your test project (also, point your form’s StyleBook property to the StyleBook).
The code
Start by creating the code for the class:
type TBitmapSpeedButton = class(TSpeedButton)
...
and add the Create method:
constructor TBitmapSpeedButton.Create(AOwner: TComponent);
begin
inherited;
Height := 28;
Width := 28;
end;
Add some code to your test project to create a TSpeedButton in code:
BSB := TBitmapSpeedButton.Create(Self);
BSB.Parent := Self;
BSB.Align := TAlignLayout.alCenter;
Run it and you’ll see ... nothing. Okay, so the default style for a speed button is invisible unless you hover over it (perhaps not the best choice for a custom component, but we’re here now, so we’ll have to live with it). Actually you can see it by hovering your mouse over, you’ll just need to be good at finding where we centered it to.
What happened here? If you listen to some descriptions of FireMonkey custom control, they’ll give you lots of code to load a style into a FireMonkey control. But if you read such stuff ignore it. FireMonkey actually does all that stuff for you. If you dig into the FMX.Types unit and look at the source to TStyledControl.GetStyleObject you’ll see (amongst a lot of other stuff):
StyleName := ClassName + 'style';
Delete(StyleName, 1, 1); // just remove T
So, FireMonkey takes the ClassName of you control (TSpeedButton), appends ‘style’ and removes the preceding ‘T’, giving us ‘SpeedButtonstyle’ and automatically loads the appropriately named style (unless you, or your users, set the StyleLookup property, in which case it automatically loads that one instead).
(Aside: TStyledControl is the parent of all controls which can have styling applied).
Functionality
So, we have a control which looks like a TBitmapSpeedButton, we just need to add some code so it behaves like one.
Lets flesh out the interface section:
type TImageAlign = (iaTop, iaLeft, iaRight, iaBottom, iaCenter);
type
[ComponentPlatformsAttribute(pidWin32 or pidWin64 or pidOSX32)]
TBitmapSpeedButton = class(TSpeedButton)
private
FImageAlign: TImageAlign;
FTextVisible: Boolean;
procedure SetImageAlign(const Value: TImageAlign);
procedure SetTextVisible(const Value: Boolean);
protected
FImageLayout: TLayout;
FImage: TImage;
FBitmap: TBitmap;
procedure ApplyStyle;override;
procedure EVBitmapChange(Sender: TObject);
public
constructor Create(AOwner: TComponent);override;
destructor Destroy;override;
published
property ImageAlign: TImageAlign read FImageAlign write SetImageAlign default iaCenter;
property TextVisible: Boolean read FTextVisible write SetTextVisible;
property Bitmap: TBitmap read FBitmap write FBitmap;
end;
The first bit of interesting code is the ApplyStyle method:
procedure TBitmapSpeedButton.ApplyStyle;
var T: TFMXObject;
begin
inherited;
T := FindStyleResource('imagelayout');
if (T <> nil) and (T is TLayout) then
FImageLayout := TLayout(T);
T := FindStyleResource('image');
if (T <> nil) and (T is TImage) then
begin
FImage := TImage(T);
FImage.Bitmap.Assign(FBitmap);
end;
SetTextVisible(FTextVisible);
UpdateImageLayout;
end;
Most custom controls will need to override this virtual procedure. It is called whenever a style is loaded or modified, and it is here where we need to grab any objects which we will be manipulating, in our case the TImage (image) and it’s parent TLayout (imagelayout) objects.
Go back to our interface section and look at the Bitmap property. A naive implementation might use a getter and setter to fetch/modify the bitmap object contained within the styles TImage object. There’s two problems here: first the style isn’t applied at the time the object is created, but slightly later (I presume on some kind of OnIdle event). So, anyone instantiating your object:
BSB := TBitmapSpeedButton.Create(Self);
BSB.Parent := Self;
BSB.Bitmap.LoadFromFile('MyImage.png');
Will at best get ignored, or at worst get an access violation, depending on whether you tested the validity of your FImage field.
The second issue is that if you style ever gets updated the bitmap data saved in the styles TImage will get deleted and you’ll get a new, empty, TImage object.
So, what we need to do is ‘cache’ any data which will be sent to styling objects. In our case, that’s the TextVisible and ImageAlign property data in addition to that for Bitmap.
Look back at the ApplyStyle code above and you’ll see that I’m reloading the Bitmap and TextVisible data and calling UpdateImageLayout which will apply the ImageAlign and a few other features still to be added. Thus if a new style gets loaded the display will be updated to reflect the components state.
But, this code only operates when the style is applied, we also need to update the styles properties when a user sets our properties. So, we also have setters for ImageAlign and TextVisible, e.g.:
procedure TBitmapSpeedButton.SetTextVisible(const Value: Boolean);
begin
FTextVisible := Value;
if (FTextObject <> nil) and (FTextObject is TText) then
TText(FTextObject).Visible := Value;
end;
(FTextObject is inherited from our TSpeedButton parent (though, oddly, it isn’t declared as a TText, even though the TSpeedButton pretty much ignores it if it isn’t one).
For FBitmap we key into it’s OnChange event, with our EVBitmapChange handler (by convention I prefix event handlers with EV):
procedure TBitmapSpeedButton.EVBitmapChange(Sender: TObject);
begin
if FImage <> nil then
FImage.Bitmap.Assign(FBitmap);
end;
Conclusion
So, that wraps up the interesting stuff. I’ve added a few more properties, which you can see in the full source below. And I’ll sum up that I’m rather pleased with what I can achieve in FireMonkey in only 152 lines of code (plus the style).
Links:
Full source zip (.pas and .style files).
Documentation for the completed component.
Debugging tips:
Check you have the style loaded into the forms StyleBook component.
Check that the forms StyleBook property points to the StyleBook component (it’s not set by default).
Enjoy, Mike.
Update: TBitmapSpeedButton: Loading Images from the Style
Full .pas source:
unit Solent.BitmapSpeedButton;
interface
uses FMX.Controls, FMX.Layouts, FMX.Objects, FMX.Types, Classes;
type TImageAlign = (iaTop, iaLeft, iaRight, iaBottom, iaCenter);
type
[ComponentPlatformsAttribute(pidWin32 or pidWin64 or pidOSX32)]
TBitmapSpeedButton = class(TSpeedButton)
private
FImageAlign: TImageAlign;
FTextVisible: Boolean;
FImageHeight: Single;
FImageWidth: Single;
FImagePadding: Single;
procedure SetImageAlign(const Value: TImageAlign);
procedure SetTextVisible(const Value: Boolean);
procedure SetImageHeight(const Value: Single);
procedure SetImagePadding(const Value: Single);
procedure SetImageWidth(const Value: Single);
protected
FImageLayout: TLayout;
FImage: TImage;
FBitmap: TBitmap;
procedure ApplyStyle;override;
procedure EVBitmapChange(Sender: TObject);
procedure UpdateImageLayout;
public
constructor Create(AOwner: TComponent);override;
destructor Destroy;override;
published
property ImageAlign: TImageAlign read FImageAlign write SetImageAlign default iaCenter;
property TextVisible: Boolean read FTextVisible write SetTextVisible;
property Bitmap: TBitmap read FBitmap write FBitmap;
property ImageWidth: Single read FImageWidth write SetImageWidth;
property ImageHeight: Single read FImageHeight write SetImageHeight;
property ImagePadding: Single read FImagePadding write SetImagePadding;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('SolentFMX', [TBitmapSpeedButton]);
end;
{ TBitmapSpeedButton }
procedure TBitmapSpeedButton.ApplyStyle;
var T: TFMXObject;
begin
inherited;
T := FindStyleResource('imagelayout');
if (T <> nil) and (T is TLayout) then
FImageLayout := TLayout(T);
T := FindStyleResource('image');
if (T <> nil) and (T is TImage) then
begin
FImage := TImage(T);
FImage.Bitmap.Assign(FBitmap);
end;
SetTextVisible(FTextVisible);
UpdateImageLayout;
end;
constructor TBitmapSpeedButton.Create(AOwner: TComponent);
begin
inherited;
FBitmap := TBitmap.Create(0,0);
FBitmap.OnChange := EVBitmapChange;
FImageAlign := iaCenter;
Height := 28;
Width := 28;
ImageWidth := 24;
ImageHeight := 24;
ImagePadding := 2;
end;
destructor TBitmapSpeedButton.Destroy;
begin
FBitmap.Free;
inherited;
end;
procedure TBitmapSpeedButton.EVBitmapChange(Sender: TObject);
begin
if FImage <> nil then
FImage.Bitmap.Assign(FBitmap);
end;
procedure TBitmapSpeedButton.SetImageAlign(const Value: TImageAlign);
begin
FImageAlign := Value;
UpdateImageLayout;
end;
procedure TBitmapSpeedButton.SetImageHeight(const Value: Single);
begin
FImageHeight := Value;
if FImage <> nil then
FImage.Height := Value;
UpdateImageLayout;
end;
procedure TBitmapSpeedButton.SetImagePadding(const Value: Single);
begin
FImagePadding := Value;
UpdateImageLayout;
end;
procedure TBitmapSpeedButton.SetImageWidth(const Value: Single);
begin
FImageWidth := Value;
UpdateImageLayout;
end;
procedure TBitmapSpeedButton.SetTextVisible(const Value: Boolean);
begin
FTextVisible := Value;
if (FTextObject <> nil) and (FTextObject is TText) then
TText(FTextObject).Visible := Value;
end;
procedure TBitmapSpeedButton.UpdateImageLayout;
begin
if FImage <> nil then
begin
FImage.Width := ImageWidth;
FImage.Height := ImageHeight;
case ImageAlign of
iaLeft:FImageLayout.Align := TAlignLayout.alLeft;
iaTop: FImageLayout.Align := TAlignLayout.alTop;
iaRight: FImageLayout.Align := TAlignLayout.alRight;
iaBottom: FImageLayout.Align := TAlignLAyout.alBottom;
else
FImageLayout.Align := TAlignLayout.alCenter;
end;
end;
if FImageLayout <> nil then
if ImageAlign in [iaLeft, iaRight] then
FImageLayout.Width := FImageWidth+FImagePadding*2
else if ImageAlign in [iaTop, iaBottom] then
FImageLayout.Height := FImageHeight+FImagePadding*2;
end;
initialization
RegisterFMXClasses([TBitmapSpeedButton]);
end.
Previous Comments
#2 from .(JavaScript must be enabled to view this email address) on February 13, 2012
I don’t see how simply dropping a TImage onto a button will have nearly the same functionality. The above basically does all the layout stuff you’d have to do manually for every component using your method.
Also, I’ve now extended my component to read the image from a style. Simply change the style to a new bitmap and the app is updated - no need to change the source code. I always prefer to do a bit of coding up front to save me lots of time later.
#3 from .(JavaScript must be enabled to view this email address) on February 20, 2012
No problem Mike, I figured with FM letting you create a custom control out of other components would basically do away with this kind of thing, I get your point on the other functionality however.
RSS feed

#1 from .(JavaScript must be enabled to view this email address) on February 13, 2012
Wow all that code when all you needed to do was drop a timage onto a button/control?
Create any custom control that way, and styles still apply to them.