Making the Glyph Property High DPI Aware for TBitBtn and TSpeedButton

Finally, last step in making my (/your) Delphi application not only high-dpi aware but also high dpi button-glyph-display-size-as-expected-ware. In my last post I’ve shared how to programmatically upsize images in TImageList so that menus, popups, toolbars and other controls using image lists appear more appealing on high dpi displays. This time I’m dealing with button images, more precisely programmatically upsizing the bitmap set for the Glyph property.

Ok, so the theory (of the possible bad display on high dpi logical scales) is known – you are most probably developing your application on 96 DPI and your users report weird display issues – having your application’s UI appear to small or badly stretched. The answer is to make your application high-dpi aware and then manually fix a few quirks.

What’s left for me is to fix how bitmaps appear on buttons, specifically TBitBtn and TSpeedButton controls.

Upsizing the Glyph for TSpeedButton and TBitBtn

Those two button control types have a Glyph property you can use to specify a bitmap image that appears on the button. The Glyph property accepts a TBitmap type and this bitmap can hold up to 4 different images, one for 4 different states of the button: when up, when disabled, when clicked and when in down state. The NumGlyphs Indicates the number of images that are in the graphic specified in the Glyph property.

Note that the TBitBtn has also the Kind property you use to specify the appearance of the TBitBtn control and its response when the user clicks the button. Each of the several different Kind values would display a different graphics on the Button (by default). You can also specify a custom glyph using the Glyph property of course.

As the case was with image lists I’m simply going to upscale the bitmap in the Glyph property. Here’s the function used:

procedure TMainForm.ResizeButtonImagesforHighDPI(const container: TWinControl);
var
  b : TBitmap;
  i : integer;

  procedure ResizeGlyph(const sb : TSpeedButton; const bb : TBitBtn);
  var
    ng : integer;
  begin
    ng := 1;
    if Assigned(sb) then ng := sb.NumGlyphs;
    if Assigned(bb) then ng := bb.NumGlyphs;

    b := TBitmap.Create;
    try
      b.Width := ng * MulDiv(16, Screen.PixelsPerInch, 96);
      b.Height := MulDiv(16, Screen.PixelsPerInch, 96);
      b.Canvas.FillRect(b.Canvas.ClipRect);

      if Assigned(sb) AND (NOT sb.Glyph.Empty) then
      begin
        b.Canvas.StretchDraw(Rect(0, 0, b.Width, b.Height), sb.Glyph) ;
        sb.Glyph.Assign(b);
      end;

      if Assigned(bb) AND (NOT bb.Glyph.Empty) then
      begin
        b.Canvas.StretchDraw(Rect(0, 0, b.Width, b.Height), bb.Glyph) ;
        bb.Glyph.Assign(b);
      end;
    finally
      b.Free;
    end;
  end; (*ResizeGlyph*)
begin
  if Screen.PixelsPerInch = 96 then Exit;
  if Screen.PixelsPerInch * 100 / 96 <= 150 then Exit;

  for i := 0 to -1 + container.ControlCount do
  begin
    if container.Controls[i] IS TBitBtn then ResizeGlyph(nil, TBitBtn(container.Controls[i]));
    if container.Controls[i] IS TSpeedButton then ResizeGlyph(TSpeedButton(container.Controls[i]), nil);

    if container.Controls[i] is TWinControl then
      ResizeButtonImagesforHighDPI(TWinControl(container.Controls[i]));
  end;

end;

ResizeButtonImagesforHighDPI takes a TWinControl as a parameter and recursively loops through all the container type controls to find buttons and “fix” their Gylph. There’s this “ugly” ResizeGlyph function taking two parameters: a TBitBtn or a TSpeedButton. Since those two inherit from different controls I used this ugly way to do the trick.

As you know from the previous post, I’m handling high dpi issues in the AfterConstruction event for a form. So the code to call the function looks like:

procedure TForm1.AfterConstruction;
begin
  inherited;

  if Screen.PixelsPerInch <> 96 then
  begin
    ResizeButtonImagesforHighDPI(self);
  end;
end;

All my non modal application forms (visually) inherit from a single form – so I have to call this only from one place.

The Case Of (Create & Display Lazy) Modal Forms

Unlucky for me, all my forms that are displayed modally do not inherit from a single form – so I have to fix those one-by-one or as they appear. When I have a form I call to be displayed modally this form is created when needed, displayed and freed after (in most cases. Sometimes I let them live for the application lifetime).

I do not want to edit dozens of modal forms (and then remember this for new modal forms) – I want a simple place to handle the appearance of a modal form and act (resize button glyphs). And, again, thanks Delphi, the single place actually exists: Screen. OnActiveFormChange.

The TScreen’s OnActiveFormChange would fire immediately after a new form becomes active in a multi-form application. I only have to check if this is a modal form and fix its buttons. Here’s the implementation part:

procedure TMainForm.ScreenOnActiveFormChange(Sender: TObject);
var
  af : TCustomForm;
begin
  af := Screen.ActiveCustomForm;

  if Screen.PixelsPerInch <> 96 then
  begin
    if Assigned(af) AND (fsModal in af.FormState) AND (af.Tag <> PixelsPerInch) then
    begin
      ResizeButtonImagesforHighDPI(af);
      af.Tag := PixelsPerInch; //hijacking the Tag property - so no usage for something else!
    end;
  end;
end;

//the OnActiveFormChange is set in main form’s OnCreate event:

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Screen.OnActiveFormChange := ScreenOnActiveFormChange;
end;

Do note how I’m hijacking the (modal) form’s Tag property to ensure ResizeButtonImagesforHighDPI is not called several times if the buttons for that form have already been resized. Not ideal, but does the trick for me as I normally do not use the Tag property for anything.

And that’s it folks. A few tweaks to my application and it is finally looking how it should be on 300% and alike DPI values.

5 thoughts on “Making the Glyph Property High DPI Aware for TBitBtn and TSpeedButton

  1. Rabatscher Michael

    I use some code like this to scale my images and everything else:

    procedure THighDpIForm.DoCreate;
    begin
    fOrigDPI := cDesignDPI;
    fDelphiScaleFact := DelphiScaleFact;

    inherited;

    if fOrigDPI self.PixelsPerInch then
    DoScale;

    assert(scaled = True, ‘Housten we have a problem at: ‘ + ClassName);
    end;

    This is part of my “HighDPIForm” class which I use instead of tform (it’s derrived from that)

    It’s also way nicer to not use stretchdraw but rather use gid+ smooth stretching (bilinear or
    bicubic interpolation)

    Actually you should also check for the WM_DPICHANGED message that is sent to all
    windows in case the user changes the scaling.

    Reply
    1. zarkogajic Post author

      Hi Erik, for me the font is set ok by Delphi – no manual fixing needed. Can you provide some more info on the problem you are facing?

      Reply
  2. Andreas

    Thanks for posting this! 🙂 However, in Delphi 10.3.2, this did not work for me, but after setting the CopyMode to cmSrcCopy the stretched glyph did show up.

    Reply
  3. Stephen Aberle

    This is helpful as I begin trying to thread my way through the maze of High DPI. A couple of questions:

    1) What about a setup where one has several displays with different resolutions? Do you have suggestions about how to handle that situation? With my system, this works fine on startup, but when I move the form to a monitor with lower resolution, the glyphs are way too big.

    2) This method resizes glyphs in controls like TSpinEdit that contain TSpeedButtons. In that situation, the resulting glyphs wind up way too big. My quick & dirty workaround is to substitute the following for line 44 of the current ResizeButtonImagesforHighDPI method:

    if (container.Controls[i] is TWinControl) and
    not (container.Controls[i] is TCustomEdit) then

    … but this will no doubt miss some cases I want to catch, and perhaps catch some cases I want to miss.

    Reply

Leave a Reply to Stephen Aberle Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.