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.