FMX tip: buttons with images that still have nicely-aligned captions

Unlike its VCL equivalent, the FireMonkey TSpeedButton has no Glyph property; similarly, TButton doesn’t have the image properties added to the VCL version in D2009, and in FMX, there’s no TBitBtn class to boot. The reason for this is that in principle, there is no need for class-specific image properties:

  1. Add a TImage to the design surface.
  2. Using the structure view (top left of the IDE), reparent the image to the desired button by dragging and dropping with the mouse. (It doesn’t matter whether the button is a TButton, which takes the keyboard focus, or a TSpeedButton which doesn’t.)
  3. Select the image and use the object inspector to set its HitTest property to False. This prevents it from receiving mouse clicks.

This approach works fine if the button doesn’t have a caption, but when it does, an issue arises concerning alignment: the text will remain centred in the button regardless of whether the image is aligned to a certain side, so that if the image is relatively large, text will appear partly on it, and partly on the normal background:

Poorly aligned button captions

If the button images are either left-aligned (as in this example) or right-aligned, you could manually fix up the alignment by prepending or appending spaces to the button’s Text property as appropriate, however this won’t work for top- or bottom-aligned images, and moreover, causes hassles if the button’s text comes from an assigned action. So, what would make for a more principled fix?

Well, internally, a button’s text is part of its style, which like the style for any other control is composed of more primitive FMX controls. The text, then, comes from a TText control, which is placed inside a layout control of some sort that also contains a TSubImage or TRectangle that defines the button background and border.

Given that, one way to fix the text alignment issue might be to reparent our TImage to the parent of the TText at runtime. If that sounds a bit hacky, that’s because it is, and worse, the changes made to control style handling in XE3 blogged about by Eugene Kryukov (EMBT) here make it precarious to boot. Happily however, there’s a better way: when the button’s style is loaded (or reloaded), adjust the internal TText’s Padding property so that it no longer overlaps the image.

To do this generically for a number of buttons on a form, select them all in the designer (they can be a mixture of TButton and TSpeedButton controls if you want), click to the Events tab of the Object Inspector, type ButtonApplyStyleLookup next to OnApplyStyleLookup, and press Enter. This will create a shared event handler for the buttons’ OnApplyStyleLookup event. In the code editor, add the following for its implementation:

procedure TForm1.ButtonApplyStyleLookup(Sender: TObject);
var
  Button: TCustomButton;
  Control: TControl;
  TextObj: TFmxObject;
begin
  Button := (Sender as TCustomButton);
  for Control in Button.Controls do
    if Control is TImage then
    begin
      TextObj := Button.FindStyleResource('text');
      if TextObj is TText then
        case Control.Align of
          TAlignLayout.alLeft:
            TText(TextObj).Padding.Left := Control.Width;
          TAlignLayout.alTop:
            TText(TextObj).Padding.Top := Control.Height;
          TAlignLayout.alRight:
            TText(TextObj).Padding.Right := Control.Width;
          TAlignLayout.alBottom:
            TText(TextObj).Padding.Bottom := Control.Height;
        end;
      Break;
    end;
end;

What we do here is cycle through the button’s controls looking for the TImage; when we find it, we then look for the TText style resource and adjust its Padding accordingly. Here’s the result:

Nicely aligned button captions

PS – if having read this far you’d still prefer a button class with a dedicated Bitmap property, check out Mike Sutton’s custom TSpeedButton descendant here.

8 thoughts on “FMX tip: buttons with images that still have nicely-aligned captions

  1. Your text is centered in its padded area though, and not left-aligned, so it looks a bit “off” with that variable spacing with the icon. In a more general solution, having text ellipsis would also be preferable to having the text come out to the edge of the button (f.i. In localized texts)
    Also, you need more code to work with RTL languages where the icon location and text alignment needs be reversed. Might be better off having a TBitBtn to handle all those details…

    • Your text is centered in its padded area though, and not left-aligned

      I think it looks better like that, though if you’re bothered, alter the TText’s Align property accordingly.

      Also, you need more code to work with RTL languages

      Er, that’ll be rather premature given the framework itself doesn’t support them. Personally, basic bugs in (say) FMX.Platform.Mac have a far higher priority.

      Might be better off having a TBitBtn to handle all those details

      Given you’ve listed precisely two ‘details’, again, I’d rather do it without writing a custom control.

      • But unless I misunderstood, you have to attach that style event to all buttons in all forms? For a simple app that’s okay, but for a larger app, that’s a lot maintainance and grunt work (which needs to be thought of every time you add a button to a form). And that does nothing for the visual aspect at design time (f.i. when adjusting button size so that labels fit).

        A proper TBitBtn would solve all the above, as well as be able to ensure a consistent look across the application (rather than some buttons with text centers, other left-aligned, etc. depending on the mood of whoever was editing the form).

        • A proper TBitBtn would solve all the above

          You’re such an idealist… Way before I come to write reams of code for the perfect FMX TBitBtn, I’ll be rewriting the entire FMX menu code (its appaling, as I’ve mentioned various times before on this blog), fix TComboBox, fix TTreeView, continue to fix niggling issues in FMX.Platform.Mac.pas, and seriously investigate the text issues on Windows (not looking to forward to that one especially). In the meantime, I think a simple event handler (or more realistically, interposer class – event handlers are just easier to talk about in a simple ‘tips and tricks’ blogpost) will do…

          • Has it become that complex to write a composite component in FMX? It didn’t use to require reams of code… Another structural misdesign then I suppose.

            The text issue is a shame yes, and lack of attention given to it is ridiculous: the fix was given to them years ago now, and before that, was discussed publicly by Firefox Mozilla and Chrome dev team (around the time FF4 bumped on the issue) , as well as blogged about even before FMX appeared by Barry Kelly in is blurry ville blurb.

        • Moreover: I don’t want any EMBT employee either spending a single minute on a FMX TBitBtn until far more urgent issues are fixed in the code actually shipped.

  2. I know this is slightly off topic, but I’ve found your blog so useful and somewhere on it you’d talked about not being able to read the DFM bitmap and I figured it out and thought I’d share the code back with you…

      TcLegacyBitmap = class(TBitmap)
       protected
         procedure ReadLegacyTransparency(Reader:TReader);
         procedure ReadLegacyBitmap(TheFile:TStream);
         procedure ReadLegacyItem(Reader: TReader);
         procedure DefineProperties(Filer:TFiler); override;
       public
         LegacyTransparency:Boolean;
      end;
    
    
    { TcLegacyBitmap }
    
    procedure TcLegacyBitmap.DefineProperties(Filer: TFiler);
    begin
      inherited;
      //read legacy properties
      Filer.DefineProperty('ResamplerClassName',ReadLegacyItem,nil,False);
      Filer.DefineBinaryProperty('Bitmap',ReadLegacyBitmap, Nil, False);
      Filer.DefineBinaryProperty('Data',ReadLegacyBitmap, Nil, False);
      Filer.DefineBinaryProperty('Glyph.Data',ReadLegacyBitmap, Nil, False);
      Filer.DefineProperty('Transparent',ReadLegacyTransparency, Nil, False);
    end;
    
    procedure TcLegacyBitmap.ReadLegacyBitmap(TheFile: TStream);
    var
       b,m:AnsiChar;
    begin
       b:=#0;
       //find end of header
       while (b+m  'BM') and (TheFile.Position < TheFile.Size) do
        begin
           TheFile.Read(B,1);
           if b='B' then
            begin
              TheFile.Read(M,1);
              if m='M' then
               begin
                  theFile.Position:=TheFile.Position-2;
                  break;
               end
              else if m='B' then
                TheFile.Position:=TheFile.Position-1;
            end;
        end;
       if (b+m)='BM' then
        begin
           LoadFromStream(TheFile);
           if LegacyTransparency and not IsEmpty then
              ReplaceColorWithAlpha;
        end;
    end;
    
    procedure TcLegacyBitmap.ReadLegacyItem(Reader: TReader);
    begin
         //we just toss it because it's not relevent.
         reader.ReadString;
    end;
    
    procedure TcLegacyBitmap.ReadLegacyTransparency(Reader: TReader);
    begin
         LegacyTransparency:=Reader.ReadBoolean;
         if LegacyTransparency and not IsEmpty then
            ReplaceColorWithAlpha;
    end;
    

Leave a comment