Posts from January 2012

TBitmapSpeedButton: Loading Images from the Style

Recently recently blogged about creating a TBitmapSpeedButton - a button which could show a bitmap image. When I came to use it I realised an ideological flaw. I was using the old VCL style model of loading a bitmap at design time to use in the application.

That works fine for VCL, but in FireMonkey it’s good practice to offload the visuals to the style. What would be ideal is if, in the form designer, I can specify a style resource for the image to be used. Now I, or my visual designer, can choose an image to use without needing access to my source code. If the look of the software needs updating I can just update the style file with new images and the app will look different without having to edit the source code (or at least the FMX file, which as far as I’m concerned is the same thing.

So, I’ve updated my component with two new properties:

property ImageTypeTImageType read FImageType write SetImageType;
    
property ImageStyleLookupString read FImageStyleLookup write SetImageStyleLookup

TImageType is either itBitmap or itStyleLookup and tells the component where to get the bitmap data from.

The only interesting bit of new code is the new UpdateImage method:

procedure TBitmapSpeedButton.UpdateImage;
var 
ObjTFMXObject;
begin
  
if FImageType itBitmap then
    
if FImage <> nil then
      FImage
.Bitmap.Assign(FBitmap)
    else
  else 
//itResource
  
begin
    Obj 
:= nil;
    if (
FScene <> nil) and (FScene.GetStyleBook <> nil) and (FScene.GetStyleBook.Root <> nilthen
      Obj 
:= TControl(FScene.GetStyleBook.Root.FindStyleResource(FImageStyleLookup));
    if 
Obj nil then
      
if Application.DefaultStyles <> nil then
        Obj 
:= TControl(Application.DefaultStyles.FindStyleResource(FImageStyleLookup));
    if (
Obj <> nil) and (Obj is TImage) and (FImage <> nilthen
      FImage
.Bitmap.Assign(TImage(Obj).Bitmap);
  
end;
end

which shows how to find a style resource either from the current stylebook, or if that isn’t found, from the applications default style.

Download updated sources

 

MonkeyStyler Preview Screenshots

MonkeyStyler is my stand alone FireMonkey style designer. I’ve spent the last week or so making it look presentable, and here are the first screenshots. Please note that this is still ‘alpha’ software and much is planned to change, in terms of visuals, usabality and functionality.

So, here’s the complete screenshot.

File and Element Manager
Lets deal with each area individually. First the file and element manager.

You can load a number of files at once, create new files, close and save open ones, and switch between files. The ability to have multiple files open means you can easily copy style elements between them (or indeed copy a style element within a file to create a duplicate).

(Note that I refer to individual elements within a style file as style elements or just elements. Hence the ‘buttonstyle’ for a TButton is one element, ‘checkboxstyle’ another and so on).

The element manager lists all available elements within the currently selected file and enables you to copy elements within and between files (as described above), create, delete and rename elements.

And, of course select elements, which shows the selected element in the control viewer:

Control Viewer

The control viewer shows a live preview of your style. Note here that you’re not actually looking at a TButton. You’re seeing the components which make up the style for one. You can’t actually interact with it (eg. mouseovers, clicks etc. do nothing), but the Components button shows the drop down shown in the screenshot.

With this components drop-down you can see which components make up a style. You can also show and hide individual components by checking or clearing the relevant check box. This is a great way to see how a style is actually made up.

A neat feature is that the check boxes for animations and effects run the animation or effect. Have an animation which takes a second to run and you will see it running for a second in the preview. In other words, while the preview may not be an actual control, all the components (and animations and effects) which make it up are ‘live’ and working. An animation which is set to loop will be constantly running and looping in the preview.

Property Editor

And finally, we come to the property editor. This should all look pretty familiar from Delphi/C++ Builder. Use the drop down to select which component within the element to edit and it’s properties will be displayed. Sub-properties can be expanded and edited as you would expect.

As for editing properties there is one feature you’ll notice immediately: all edits are live. Start typing into a ‘Text’ property and the text on the control preview will change live as you type. Adjust the width, alignment or cornerstyle (or anything else) and the preview will update immediately, and without the need to hit enter, or otherwise tab away from the editor.

Another useful feature is the drop-down property editors. Edit a numeric property and you’ll see a slider appear, for a color or gradient property you’ll get a drop down color or gradient editor and so on. And these are also live: you’ll see preview control change live as you drag the slider, adjust colors or edit the gradient. There’s no modal dialog. No need to hit enter. You get instant feedback and quickly get the values you want. (And even if you have a drop down editor, you are still free to type in/cut and paste values if you want).

You also get:
A button which brings down a menu for adding/editing animations;
No nasty modal error messages. Set an illegal value and you’ll get a drop-down error message which simply clears as you keep typing;
Buttons to add and delete components.

And, like I said, much will change, much will improve and much will be added.

Enjoy.

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 alreadyImages 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(AOwnerTComponent);
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(StyleName11); // 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 = (iaTopiaLeftiaRightiaBottomiaCenter);

type
  [ComponentPlatformsAttribute
(pidWin32 or pidWin64 or pidOSX32)]
  TBitmapSpeedButton 
= class(TSpeedButton)
  private
    
FImageAlignTImageAlign;
    
FTextVisibleBoolean;
    
procedure SetImageAlign(const ValueTImageAlign);
    
procedure SetTextVisible(const ValueBoolean);
  protected
    
FImageLayoutTLayout;
    
FImageTImage;
    
FBitmapTBitmap;
    
procedure ApplyStyle;override;
    
procedure EVBitmapChange(SenderTObject);
  public
    
constructor Create(AOwnerTComponent);override;
    
destructor Destroy;override;
  
published
    property ImageAlign
TImageAlign read FImageAlign write SetImageAlign default iaCenter;
    
property TextVisibleBoolean read FTextVisible write SetTextVisible;
    
property BitmapTBitmap read FBitmap write FBitmap;
  
end

The first bit of interesting code is the ApplyStyle method:

procedure TBitmapSpeedButton.ApplyStyle;
var 
TTFMXObject;
begin
  inherited
;
  
:= FindStyleResource('imagelayout');
  if (
<> nil) and (T is TLayoutthen
    FImageLayout 
:= TLayout(T);
  
:= FindStyleResource('image');
  if (
<> nil) and (T is TImagethen
  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 ValueBoolean);
begin
  FTextVisible 
:= Value;
  if (
FTextObject <> nil) and (FTextObject is TTextthen
    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(SenderTObject);
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.ControlsFMX.LayoutsFMX.ObjectsFMX.TypesClasses;

type TImageAlign = (iaTopiaLeftiaRightiaBottomiaCenter);

type
  [ComponentPlatformsAttribute
(pidWin32 or pidWin64 or pidOSX32)]
  TBitmapSpeedButton 
= class(TSpeedButton)
  private
    
FImageAlignTImageAlign;
    
FTextVisibleBoolean;
    
FImageHeightSingle;
    
FImageWidthSingle;
    
FImagePaddingSingle;
    
procedure SetImageAlign(const ValueTImageAlign);
    
procedure SetTextVisible(const ValueBoolean);
    
procedure SetImageHeight(const ValueSingle);
    
procedure SetImagePadding(const ValueSingle);
    
procedure SetImageWidth(const ValueSingle);
  protected
    
FImageLayoutTLayout;
    
FImageTImage;
    
FBitmapTBitmap;
    
procedure ApplyStyle;override;
    
procedure EVBitmapChange(SenderTObject);
    
procedure UpdateImageLayout;
  public
    
constructor Create(AOwnerTComponent);override;
    
destructor Destroy;override;
  
published
    property ImageAlign
TImageAlign read FImageAlign write SetImageAlign default iaCenter;
    
property TextVisibleBoolean read FTextVisible write SetTextVisible;
    
property BitmapTBitmap read FBitmap write FBitmap;
    
property ImageWidthSingle read FImageWidth write SetImageWidth;
    
property ImageHeightSingle read FImageHeight write SetImageHeight;
    
property ImagePaddingSingle read FImagePadding write SetImagePadding;
  
end;

procedure Register;

implementation

procedure Register
;
begin
  RegisterComponents
('SolentFMX'[TBitmapSpeedButton]);
end;

{ TBitmapSpeedButton }

procedure TBitmapSpeedButton
.ApplyStyle;
var 
TTFMXObject;
begin
  inherited
;
  
:= FindStyleResource('imagelayout');
  if (
<> nil) and (T is TLayoutthen
    FImageLayout 
:= TLayout(T);
  
:= FindStyleResource('image');
  if (
<> nil) and (T is TImagethen
  begin
    FImage 
:= TImage(T);
    
FImage.Bitmap.Assign(FBitmap);
  
end;
  
SetTextVisible(FTextVisible);
  
UpdateImageLayout;
end;

constructor TBitmapSpeedButton.Create(AOwnerTComponent);
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(SenderTObject);
begin
  
if FImage <> nil then
    FImage
.Bitmap.Assign(FBitmap);
end;

procedure TBitmapSpeedButton.SetImageAlign(const ValueTImageAlign);
begin
  FImageAlign 
:= Value;
  
UpdateImageLayout;
end;

procedure TBitmapSpeedButton.SetImageHeight(const ValueSingle);
begin
  FImageHeight 
:= Value;
  if 
FImage <> nil then
    FImage
.Height := Value;
  
UpdateImageLayout;
end;

procedure TBitmapSpeedButton.SetImagePadding(const ValueSingle);
begin
  FImagePadding 
:= Value;
  
UpdateImageLayout;
end;

procedure TBitmapSpeedButton.SetImageWidth(const ValueSingle);
begin
  FImageWidth 
:= Value;
  
UpdateImageLayout;
end;

procedure TBitmapSpeedButton.SetTextVisible(const ValueBoolean);
begin
  FTextVisible 
:= Value;
  if (
FTextObject <> nil) and (FTextObject is TTextthen
    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;
      
iaTopFImageLayout.Align := TAlignLayout.alTop;
      
iaRightFImageLayout.Align := TAlignLayout.alRight;
      
iaBottomFImageLayout.Align := TAlignLAyout.alBottom;
    else
      
FImageLayout.Align := TAlignLayout.alCenter;
    
end;
  
end;

  if 
FImageLayout <> nil then
    
if ImageAlign in [iaLeftiaRight] then
      FImageLayout
.Width := FImageWidth+FImagePadding*2
    
else if ImageAlign in [iaTopiaBottom] then
      FImageLayout
.Height := FImageHeight+FImagePadding*2;
end;

initialization
  RegisterFMXClasses
([TBitmapSpeedButton]);
end