So you want to go down the high-DPI road? Feeling alone? I did 🙂 The classical answer “it works on my machine” will not be sufficient here. Your non high-dpi aware Delphi application might look nice on your development machine, but it certainly looks super small or ugly stretched on your client’s shiny new 4K resolution laptop – and it really does not work – at least not how you and your client would expect!
Luckily, as presented in the previous post, there are *only* a couple of steps you need to do to make your application react correctly on different display scaling levels. If you’ve gone through the links in the mentioned article you know that
- There’s a Scaled property every Delphi form has – and you want it to be set to True.
- You also know you need to include a custom manifest file (as a resource) specifying your application is high DPI aware. If you are using Delphi 10 Seattle or newer – you simply tick the “Enable High-DPI” check inside the Manifest file group box in Project – Options – Application.
You are all set. You rebuild the application, run on your development machine – and you see no changes. Of course, that’s to expect as your development machine display DPI value is the same at design and run time. What you then do is go to Control Panel – Screen Resolution – and change (depending on your Windows version) the scaling level to 150%. Or even better, find a 4K display device (say a laptop) with default scaling set (probably to something) higher than 100% (let’s say 250% or even more). You now run the application – if you are lucky – all will be ok. The user interface controls are no longer super small nor they appear stretched.
However, some elements do not look right! You’ll notice your menu glyphs are still small (barely visible). All buttons of type TBitBtn or TSpeedButton where you used the Glyph property to display a nice image of 16×16 pixels – those images appear super small. All the controls that you create dynamically do not look correctly – they are either not at the correct location or you do not see the full width of them. Your nice custom drawn combo boxes appear to be ok, but as you drop down the pick list – it does not display correctly. And. so. on. In more complex form design you could even see sections of your user interface completely not re-sized / re-scaled.
What follows are some “tricks” and compromises I’ve used to fix my application and make it really high-dpi aware. But first, some theory.
Your arsenal: the Scaled property, Screen.PixelPerInch property, ChangeScale method, ScaleBy method, MulDiv method.
Again, if you’ve read the mentioned articles in the previous post you know that every Delphi form has a property called Scaled but also a property called PixelsPerInch. To be honest, I’ve completely ignored the PixelsPerInch property for years, did not even knew it was there (you can even change it at design time, omg). The Help states that PixelsPerInch represents the proportion of the font on the system on which the form was designed.
What actually happens (when the Scaled property is set to true) is that when the application starts the saved/stored value of the PixelsPerInch will be compared to the value of Screen.PixelsPerInch. Now, TScreen’s PixelsPerInch indicates the number of screen pixels that make up a logical inch in the vertical direction. Aha! Now we are onto something. When scaling is used Windows changes the logical number of pixels per inch – so the Screen.PixelsPerInch will be higher for scaled displays. On 250% sizing level 16 pixels would appear as 40 pixels.
At the moment of form construction, if the form’s stored PixelsPerInch is different from Screen.PixelsPerInch a series of methods would be called. Those methods would make the form controls react on different scale by updating their position, size, margins, padding and alike. In fact, the TControl class has the ChangeScale method which should “fix” the control so it displays correctly on higher values for logical pixels per inch property. In most cases this would work ok, but in some and for some controls, unfortunately, the ChangeScale method does not affect all the needed properties of “itself”.
When the above does not happen automagically it is up to you to call the ScaleBy method which will in turn call the ChangeScale and fix your controls.
Finally, the MulDiv function is what is used by Delphi, and therefore can be used by you, to calculate the needed size of a value (say font size, control height, etc).
Btw, to get the value of 250 (when scaling is at 250%) you can use:
Screen.PixelsPerInch * 100 / 96;
In the above image, controls on the left are not manually fixed, while those on the right are. A visible difference. Here’s the left part zoomed:
And here’s how it “originally” looks on 96%:
Controls that play along
Ok, as I said, most of your UI will/should look correctly on 250% DPI display settings. All TEdit, TLabel, TCheckBox, TPanel, TDBGrid and alike “old”/standard controls would scale themselves as expected. I’m not talking about those here. It might happen that those do not scale right – but only if inside some container that somehow refused to get notified for the scaling change. If that is the case you need to manually fix stuff.
Controls that do not play that well
While enabling my Delphi application for high-DPI I’ve noted some controls would fail to be properly sized or displayed. Those include: TColorBox, TStatusBar, TComboBox with Style set to csOwnerDrawFixed / csOwnerDrawVariable. And buttons with Gylps, oh, buttons with glyphs. And TImageLists I use for menus and toolbars and alike – oh my that was a long drive. Also, 3rd party! Lucky you if you are using some control from a 3rd party vendor and the vendor thought of high dpi when designing the control (saying: did correctly implement the ChangeScale method).
Where and how do you fix those? Well, for the how part: you need to figure out what property of the control is responsible for final display appearance . For the where part: I’m using the AfterConstruction method of the form. Why AfterConstruction? I’m heavily using form inheritance. I needed an even that is fired when all forms are constructed and when all run time controls are added to the user interface. In most cases I do all the setup in OnCreate so AfterConstruction is when everything is finished.
To have access to AfterConstruction you need to override it by adding a line like this in form’s interface public section:
interface TBaseForm = class(TForm) ... public procedure AfterConstruction; override; ...
Then have the implementation like:
procedure TBaseForm.AfterConstruction; begin inherited; if Screen.PixelsPerInch <> 96 then //as I’m designing at 96 DPI begin // manual fixing for high dpi scaling goes here end; end;
But, let’s go one step at a time.
So, on 96 DPI your 50×50 image looks ok. But on 250%, 240 logical DPI, this image should actual be 125 x 125 px to look sharp and nice. Do you have your splash form image in various sizes? I do not. I could have, but really I would need to have various sizes and then decide depending on the DPI what image to serve from resources (for example). A quick a dirty approach is to make the following for your TImage images: AutoSize=false, Stretch=true.
TColorBox / TComoBox.Style = csOwnerDrawFixed / csOwnerDrawVariable
While on the first look the combo looks ok, when you drop down the pick list you will notice the height of elements in the list is not correct. The TColorBox, for example, would fix itself for size and position (alignment, margins) but it would not fix the value of the ItemHeight property.
A quick fix here is to set correctly the ItemHeigh property by a simple line like this:
ColorBox2.ItemHeight := MulDiv(ColorBox2.ItemHeight, Screen.PixelsPerInch, 96);
Note: MulDiv function multiplies two values and then divides the result by a third value. The final result is rounded to the nearest integer. If the height of each item in my color box is 16 for me at design time (when PixelsPerInch is 16), then the Height on 250% should be 16 x 240 / 96 = 40. Note: 240 is 250% of 96.
If you have a status bar and you have panels – the width of each panel would not be correctly set, nor will be the height of the entire status bar, so the following does the trick:
StatusBar1.Height := MulDiv(StatusBar1.Height, Screen.PixelsPerInch, 96); for i := 0 to -1 + StatusBar1.Panels.Count do StatusBar1.Panels[i].Width := MulDiv(StatusBar1.Panels[i].Width, Screen.PixelsPerInch, 96);
Again, this control has the ButtonHeight property which will not be fixed by the ChangeScale method.
Any RUN TIME Dynamically Created Control
Delphi will not, will not (on purpose two times) automagically scale controls that you dynamically create and place inside some container. You would need to take care of this. Taking care here involves setting the “correct” Left, Top properties (if Align is alNone) and also calling the ScaleBy method to say to the control to scale itself. Therefore something like this:
cb2 := TCheckBox.Create(self); cb2.Caption := 'Check box 2'; cb2.Parent := pnlHighDPI; cb2.Left := CategoryButtons2.Left; cb2.Top := CategoryButtons2.Top + CategoryButtons2.Height + MulDiv(10, Screen.PixelsPerInch, 96); cb2.ScaleBy(Screen.PixelsPerInch, 96);
In the above I wanted for the check box to appear 10 pixels below the category buttons control. 10 pixels under 96 is 25 pixels when on 250% logical DPI.
Menus, ToolBars, Buttons (using Glyphs), TImageLists (basically all where you have images)
And just when it gets supper interesting – we stop here as the answer to those will be provided in the next post 😉
Hint: in an ideal world you would want to have larger glyphs/images. When on 250%, the image on your TSpeedButton or menu item or toolbar item having the 16×16 glyph would look small. You would need a 40×40 px graphics. But then, should (and could) you include all various image/glyph sizes in your executable? Do you even have your glyphs that are not 16×16?