Delphi High-DPI Road: Ensuring Your UI Looks Correctly for TImage, TColorBox, Owner Drawn TComboBox , TStatusBar and some more

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

  1. There’s a Scaled property every Delphi form has – and you want it to be set to True.
  2. 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.

TImage

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.

TStatusBar

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);

TCategoryButtons

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?

Here’s what I did to Resize TImageList Bitmaps to Fit High-DPI Scaling Size (for Menus, Toolbars, Trees, etc.) and then Making the Glyph Property High DPI Aware for TBitBtn and TSpeedButton.

15 thoughts on “Delphi High-DPI Road: Ensuring Your UI Looks Correctly for TImage, TColorBox, Owner Drawn TComboBox , TStatusBar and some more

  1. Uwe Raabe

    I have made the observation that TCheckBox and TRadioButton don’t scale their glyphs when moved between monitors with different DPI settings. In my case I have two monitors with 96 dpi (one of them being the main monitor) and one with 192 dpi. Running a per monitor dpi aware application written with Delphi 10.1 Berlin looks wrong on the high dpi monitor because the glyphs are too small (probably the same size as for 96 dpi).

    Reply
  2. Marius

    Isn’t ChangeScale much more elegant?

    procedure TFrmFrame.ScaleComponent;
    begin
    if Screen.PixelsPerInch 96
    then ChangeScale(Screen.PixelsPerInch, 96);
    end;

    Reply
  3. Jessie Potts

    Just wanted to mention that, for me, in Delphi 10.2 Tokyo on Windows 10 Screen.PixelsPerInch is always 96 regardless of scaling level (100%, 125%, 200%). If I want to determine the actual PixelsPerInch, I need utilize Monitor.PixelsPerInch.

    Reply
    1. Adrian

      Are you sure ? Did you remember to SIGN OUT and sign back in after adjusting Windows Scaling ?

      If I change scaling and then in a test program have it display the Screen.PixelsPerInch value it stays the same regardless until I SIGN OUT and SIGN IN and then it shows the correct value to represent the Scaling.

      You will notice this effect too with TCheckBox and some other components not sizing correctly after adjust Windows scaling value until you SIGN OUT and SIGN back in then the TCheckBox etc is sized correctly.

      Reply
      1. zarkogajic Post author

        Hi Adrian,

        Yes, I’m sure. Yes, I did sign-out/sign-in. Anyhow if it works for you by simply signout/signin – that better – less to be done in your source code.

        Reply
        1. Tony

          In 10.1 Berlin Update 2, Screen.Pixels per inch does not change (logout or not, doesn’t matter). I used the app you referenced in the previous post (https://community.embarcadero.com/blogs/entry/new-per-dpi-awareness-in-vcl-applications) and added a line to show Screen.ppi and Monitor.ppi.

          I launched it on a normal screen with no scaling, then I moved it to the second monitor which is scaled to 200%. Below are the results.

          So it would appear that to support per-DPI awareness you need to use Monitor, rather than Screen.

          — Monitors info —
          Monitor 0 ppi: 192 left: 1920 top: 0 width: 3840 height: 2160
          Primary Monitor 1 ppi: 96 left: 0 top: 0 width: 1920 height: 1080
          ————————————–
          Screen.PixelsPerInch 96
          Monitor.PixelsPerInch 96
          ————————————–

          — BeforeMonitorDpiChanged —
          Old Dpi 96 New Dpi 192
          Form size:
          left: 2441 top: 564 width: 644 height: 433
          ————————————–
          — AfterMonitorDpiChanged —
          Old Dpi 96 New Dpi 192
          Form size:
          left: 2441 top: 564 width: 1288 height: 866
          ————————————–

          — Monitors info —
          Monitor 0 ppi: 192 left: 1920 top: 0 width: 3840 height: 2160
          Primary Monitor 1 ppi: 96 left: 0 top: 0 width: 1920 height: 1080
          ————————————–
          Screen.PixelsPerInch 96
          Monitor.PixelsPerInch 192
          ————————————–

          Reply
          1. Mark F. Madura

            I’ve been going around in circles for the past couple of weeks trying to figure out a way to handle this (or at least similar) behaviour.

            I can get the first-time display of a form to have correct size/ppi in a mixed mode dpi environment. I can also handle updating the size/ppi when the form is moved from one display to another, or if the dpi changes.

            However, I’m having problems displaying a form for the first time when:
            – The main form is on display [0].
            – I’ve shown a modal form (1st Form) over the main form then moved it to display [1].
            – I then try to show a modal form (2nd Form) over 1st Form.
            The process fails and the 2nd Form ends up on display [0] with incorrect sizing.

            This behaviour also affects forms that I try to restore (from ini key values) when the restored display is a ‘moved to’ display. The DpiChanged message fires multiple times and the ‘old/new’ ppi gets screwed up.

            The program’s manifest is configured for PerMonitor v2. I’m running on Windows 10 Pro x64 versiion 1909 build 18363.592.

  4. Michael Mc

    Why is the font rendering so ugly with VCL-based high-dpi apps? Even with the latest Delphi 10.2.3, even non-bitmapped font rendering in VCL apps look pixelated on-screen, including the IDE itself. The control spacing seems to be addressed for high-dpi, but the font rendering is just… bad.

    I also develop with QtWidgets and I do not see the same issues with Qt’s high-dpi handling, nor of course with the Microsoft widget libraries.

    Reply
    1. Carlo

      Because you are not compiling your application with a manifest that declares it as a dpi-aware application: so windows assumes it has to do all the scaling by itself, by simply stretching the bitmap.

      you have to set your application as “DPI-Aware” in the project properties!

      Reply
  5. Martijn

    Hi,

    When developing on a system with text set to 125%, Delphi modifies the PixelsPerInch property to 120.

    Does this mean you have to modify all routines to use 120 instead of 96?

    Reply

Leave a 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.