share
Stack OverflowHow do I make my GUI behave well when Windows font scaling is greater than 100%
[+111] [4] UnDiUdin
[2011-11-28 13:51:33]
[ windows delphi windows-7 ]
[ https://stackoverflow.com/questions/8296784/how-do-i-make-my-gui-behave-well-when-windows-font-scaling-is-greater-than-100 ]

When choosing large font sizes in the Windows control panel (like 125%, or 150%) then there are problems in a VCL application, every time something has been set pixelwise.

Take the TStatusBar.Panel. I have set its width so that it contains exactly one label, now with big fonts the label "overflows". Same problem with other components.

Some new laptops from Dell ship already with 125% as default setting, so while in the past this problem was quite rare now it is really important.

What can be done to overcome this problem?

Hi. Do you have any advice/update/insights now that Windows AND Delphi has better support for HiDPI? - IceCold
[+65] [2011-11-28 13:55:20] David Heffernan

Your settings in the .dfm file will be scaled up correctly, so long as Scaled [1] is True.

If you are setting dimensions in code then you need to scale them by Screen.PixelsPerInch divided by Form.PixelsPerInch. Use MulDiv to do this.

function TMyForm.ScaleDimension(const X: Integer): Integer;
begin
  Result := MulDiv(X, Screen.PixelsPerInch, PixelsPerInch);
end;

This is what the form persistence framework does when Scaled is True.

In fact, you can make a cogent argument for replacing this function with a version that hard codes a value of 96 for the denominator. This allows you to use absolute dimension values and not worry about the meaning changing if you happen to change font scaling on your development machine and re-save the .dfm file. The reason that matters is that the PixelsPerInch property stored in the .dfm file is the value of the machine on which the .dfm file was last saved.

const
  SmallFontsPixelsPerInch = 96;

function ScaleFromSmallFontsDimension(const X: Integer): Integer;
begin
  Result := MulDiv(X, Screen.PixelsPerInch, SmallFontsPixelsPerInch);
end;

So, continuing the theme, another thing to be wary of is that if your project is developed on multiple machines with different DPI values, you will find that the scaling that Delphi uses when saving .dfm files results in controls wandering over a series of edits. At my place of work, to avoid this, we have a strict policy that forms are only ever edited at 96dpi (100% scaling).

In fact my version of ScaleFromSmallFontsDimension also makes allowance for the possibility of the form font differing at runtime from that set at designtime. On XP machines my application's forms use 8pt Tahoma. On Vista and up 9pt Segoe UI is used. This provides yet another degree of freedom. The scaling must account for this because the absolute dimension values used in the source code are assumed to be relative to the baseline of 8pt Tahoma at 96dpi.

If you use any images or glyphs in your UI then these need to scale too. A common example would be the glyphs that are used on toolbars and menus. You'll want to provide these glyphs as icon resources linked to your executable. Each icon should contain a range of sizes and then at runtime you choose the most appropriate size and load it into an image list. Some details on that topic can be found here: How do I load icons from a resource without suffering from aliasing? [2]

Another useful trick is to define dimensions in relative units, relative to TextWidth or TextHeight. So, if you want something to be around 10 vertical lines in size you can use 10*Canvas.TextHeight('Ag'). This is a very rough and ready metric because it doesn't allow for line spacing and so on. However, often all you need to do is be able to arrange that the GUI scales correctly with PixelsPerInch.

You should also mark your application as being high DPI aware [3]. The best way to do this is through the application manifest. Since Delphi's build tools don't allow you to customise the manifest you use this forces you to link your own manifest resource.

<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
    <asmv3:windowsSettings
         xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
      <dpiAware>true</dpiAware>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>

The resource script looks like this:

1 24 "Manifest.txt"

where Manifest.txt contains the actual manifest. You would also need to include the comctl32 v6 section and set requestedExecutionLevel to asInvoker. You then link this compiled resource to your app and make sure that Delphi doesn't try to do the same with its manifest. In modern Delphi you achieve that by setting the Runtime Themes project option to None.

The manifest is the right way to declare your app to be high DPI aware. If you just want to try it out quickly without messing with your manifest, call SetProcessDPIAware [4]. Do so as the very first thing you do when your app runs. Preferably in one of the early unit initialization sections, or as the first thing in your .dpr file.

If you don't declare your app to be high DPI aware then Vista and up will render it in a legacy mode for any font scaling above 125%. This looks quite dreadful. Try to avoid falling into that trap.

Windows 8.1 per monitor DPI update

As of Windows 8.1, there is now OS support for per-monitor DPI settings (http://msdn.microsoft.com/en-ca/magazine/dn574798.aspx). This is a big issue for modern devices which might have different displays attached with very different capabilities. You might have a very high DPI laptop screen, and a low DPI external projector. Supporting such a scenario takes even more work than described above.

[1] http://docwiki.embarcadero.com/VCL/en/Forms.TCustomForm.Scaled
[2] https://stackoverflow.com/questions/8112953/how-do-i-load-icons-from-a-resource-without-suffering-from-aliasing
[3] http://msdn.microsoft.com/en-us/library/windows/desktop/dd464646%28v=vs.85%29.aspx
[4] http://msdn.microsoft.com/en-us/library/ms633543.aspx

(2) That is not always true. In fact, Setting Scaled=true, and then setting the High DPI aware can also cause some strange breakage in most delphi applications. I have spend hundreds of hours trying to get my apps to work in high DPI and have found that it's better to have the awful looking pixelation than controls cropped, moved off screen, extra or missing scrollbars on various controls, etc. - Warren P
@WarrenP I think those problems are particular to your app. My personal experience is that my Delphi app displays and scales perfectly even at 200% font scaling. - David Heffernan
I am about to post some screenshots of Delphi IDE itself misbehaving at HIGH DPI. - Warren P
(3) @WarrenP So what? It's perfectly possible to use Delphi to build apps that behave better than the Delphi IDE. - David Heffernan
(1) I have seen lots of dialogs with fixed borders created with Delphi 5,6,7 and the scaled setting true to fail. Hiding ok,cancel buttons etc. Even some dialogs in Delphi2006 it think was bitten by this. Mixing native Delphi components and windows components also give strange effects. I always develop the GUI in a 125% font scaling and put the scaled property to false. - LU RD
True enough. If you're smarter and better at writing Delphi apps than the Delphi team, you could probably do it. I have no doubt that you, DavidH, are capable of doing such apps. I merely doubt 90% of mortal Delphi users are capable of doing better. - Warren P
@WarrenP I'd bet that most of the misbehaving is from custom controls (I mean controls painted by the VCL rather than Windows). This is yet another justification of my policy of preferring true native Windows controls to custom painted alternatives. - David Heffernan
by testing on other machines I see that even if 125% is set the behaviour is ok. So which is the other setting in Control panel that I need to change to reproduce the problem on other pcs? (Note: I use DevExpress controls with skins) - UnDiUdin
"So which is the other setting in Control panel that I need to change to reproduce the problem on other pcs?" Not sure what you mean. - David Heffernan
"I use DevExpress controls with skins" Definitely a good chance of having problems with that if you mark yourself high DPI aware. That's when Warren's more pragmatic viewpoint looks good!! - David Heffernan
@DavidHeffernan I mean that I tried to reproduce a problem on another Win7 machine. I put 125% also there but the forms look good. So is it there another setting in Control Panel where I can set PixelPerInch? - UnDiUdin
Control Panel\Appearance and Personalization\Display - David Heffernan
(2) Great stuff. +1 for fantastic information. My opinion (don't do it) is second in importance to the need to know HOW to do it when you do want to do this... - Warren P
@David Heffernan, on your personal profile page you a helpful section called "Answers that I frequently refer to" section. It seems like the link to this post is faulty. - Lars
@Lars Thanks. I think something changed in the SO code. I've fixed it up. There were lots of others that failed too. - David Heffernan
(2) I think that currently (I'm on 10.1 Berlin Update 2), you need to use "Monitor.PixelsPerInch" rather than "Screen.PixelsPerInch" in order to support systems that have multiple screens with different resolutions. "Screen.ppi" will always return the same value regardless of the monitor the program is on. - Tony
1
[+58] [2011-11-28 16:10:03] Warren P [ACCEPTED]

Note: Please see the other answers as they contain very valuable techniques. My answer here only provides caveats and cautions against assuming DPI-awareness is easy.

I generally avoid DPI-aware scaling with TForm.Scaled = True. DPI awareness is only important to me when it becomes important to customers who call me and are willing to pay for it. The technical reason behind that point of view is that DPI-awareness or not, you are opening a window into a world of hurt. Many standard and third party VCL controls do not work well in High DPI. The notable exception that the VCL parts that wrap Windows Common Controls work remarkably well at high DPI. A huge number of third party and built-in Delphi VCL custom controls do not work well, or at all, at high DPI. If you plan to turn on TForm.Scaled be sure to test at 96, 125, and 150 DPI for every single form in your project, and every single third party and built in control that you use.

Delphi itself is written in Delphi. It has the High DPI awareness flag turned on, for most forms, although even as recently as in Delphi XE2, the IDE authors themselves decided NOT to turn that High DPI Awareness manifest flag on. Note that in Delphi XE4 and later, the HIGH DPI awareness flag is turned on, and the IDE looks good.

I suggest that you do not use TForm.Scaled=true (which is a default in Delphi so unless you've modified it, most of your forms have Scaled=true) with the High DPI Aware flags (as shown in David's answers) with VCL applications that are built using the built-in delphi form designer.

I have tried in the past to make a minimal sample of the kind of breakage you can expect to see when TForm.Scaled is true, and when Delphi form scaling has a glitch. These glitches are not always and only triggered by a DPI value other than 96. I have been unable to determine a complete list of other things, that includes Windows XP font size changes. But since most of these glitches appear only in my own applications, in fairly complex situations, I have decided to show you some evidence you can verify yourselves.

Delphi XE looks like this when you set the DPI Scaling to "Fonts @ 200%" in Windows 7, and Delphi XE2 is similarly broken on Windows 7 and 8, but these glitches appear to be fixed as of Delphi XE4:

enter image description here

enter image description here

These are mostly Standard VCL controls that are misbehaving at high DPI. Note that most things have not been scaled at all, so the Delphi IDE developers have decided to ignore the DPI awareness, as well as turning off the DPI virtualization. Such an interesting choice.

Turn off DPI virtualization only if want this new additional source of pain, and difficult choices. I suggest you leave it alone. Note that Windows common controls mostly seem to work fine. Note that the Delphi data-explorer control is a C# WinForms wrapper around a standard Windows Tree common control. That's a pure microsoft glitch, and fixing it might either require Embarcadero to rewrite a pure native .Net tree control for their data explorer, or to write some DPI-check-and-modify-properties code to change item heights in the control. Not even microsoft WinForms can handle high DPI cleanly, automatically and without custom kludge code.

Update: Interesting factoid: While the delphi IDE appears not to be "virtualized", it is not using the manifest content shown by David to achieve "non-DPI-virtualization". Perhaps it is using some API function at runtime.

Update 2: In response to how I would support 100%/125% DPI, I would come up with a two-phase plan. Phase 1 is to inventory my code for custom controls that need to be fixed for high DPI, and then make a plan to fix them or phase them out. Phase 2 would be to take some areas of my code which are designed as forms without layout management and change them over to forms that use some kind of layout management so that DPI or font height changes can work without clipping. I suspect that this "inter-control" layout work would be far more complex in most applications than the "intra-control" work.

Update: In 2016, the latest Delphi 10.1 Berlin is working well on my 150 dpi workstation.


(5) That API function would be SetProcessDPIAware. - David Heffernan
(2) Excellent. Thanks for the new factoid. I suggest you modify your answer to suggest that as one possible route. It might be that customers might even want to configure that option (turn it off if it doesn't work for them). - Warren P
Delphi's splash screen uses DPI Virtualization, probably because the call to SetDPIAware is after the Splash form has already been made visible. - Warren P
FWIW I'm not sure you can apportion blame for the data explorer control. It could be either or even both parties that are to blame. Anyway, at the very least I think one should aim to support 100% and 125% scaling. How would you go about doing that? - David Heffernan
(6) RAD Studio is a big mix of standard VCL controls, custom controls, .NET WinForms and FireMonkey forms. It is not surprising, that there are problems. And that is why RAD Studio isn't a good example. - Torbins
+1 I know your viewpoint differs from mine, but you make very good points - David Heffernan
Most of my large delphi apps are a mixed bag too. - Warren P
Your latest update makes me chuckle. First of all you defend Emba for not nothering to support high DPI. Then when they do, they get kudos for it! - David Heffernan
I wouldn't say that I defended them for not doing it. I used their inability (until very recently) to get it right as a data point in my argument that it's Harder than Many Developers First Estimate It to Be. If you've never stepped in this particular pile of dragon droppings, you might be feeling better than all of this. My only point is lots of people get stuck here, for a long long time. Note that I am now picking on Microsoft who can't even get Sql Management Studio to scale with DPI, in SQL 2012. - Warren P
This answer is looking progressively more out of touch with reality. The latest machine that I bought is an Ultrabook with 2500 pixels horizontal. In order to make text readable it needs to run at 175% dpi. Non dpi aware apps look apalling. Such resolutions will be the norm in the next couple of years. This head in the sand approach is what leads to crappy apps looking terrible on hardware that could so easily make them look beautiful. - David Heffernan
(1) If you're right, it's the VCL itself that has its head in the sand. Even Microsoft has its head in the sand. The only framework that I have ever used that does a remotely passable job at this is COCOA on the Mac. - Warren P
@Warren VCL does not have head in sand. Well, maybe a bit. Going to need a big redesign for per monitor dpi though. Not even sure I want to go there. - David Heffernan
I had some hope that Firemonkey would achieve sane resolution independance when they were in their Vector phase. Now they're doing multi-resolution bitmaps for per device resolution. That way lies madness too. - Warren P
@DavidHeffernan i was looking into handling per-monitor dpi notification messages. It was trivial to listen for the WM_DPICHANGED, and i could run my ancestor form's version of ChangeScale. But as long as TScreen (and TForm) cache screen dpi at startup (FPixelsPerInch := GetDeviceCaps(DC, LOGPIXELSY);) we can't actually use the new values. - Ian Boyd
I've been playing more with Firemonkey, and now that it uses DirectWrite on Windows, the resolution scaling, and the font quality issues are gone, and things are quite lovely. - Warren P
This answer looks worse and worse as time goes by. Try running DPI unaware apps (Delphi IDE is one such) on a 4k monitor with 200% scaling running Windows 10, say. Compare the fuzziness to the clean crispness of, e.g. explorer, VS, Office. And the part at the end where you commend Emba and criticise MS is particularly odd. Emba are years behind and not getting anywhere. Delphi 10 Seattle is still not DPI aware. FWIW, my answer is not aging too well either. The lack of decent per-monitor DPI coverage in my answer is poor. - David Heffernan
So you have evaluated Embarcadero Delphi 10 Seattle's DPI awareness features? Could you write a blog post about that? Due to the changes in Delphi 10, I've amended my update to just state that I've only tested XE4 through XE8 on high dpi workstations so far. - Warren P
I have done some basic testing on Delphi 10 Seattle's DPI awareness, and it does what is claimed, at least for the demos I created. - Warren P
With enough work, you can create Delphi applications with Delphi 10.1 Berlin that scale well on High DPI displays. However, the IDE itself is still not High DPI compatible. - Frederik Slijkerman
After some study on 10.1 berlin, I believe its High DPI feature is still incorrect at the TForm level. - Warren P
2
[+42] [2012-03-26 13:58:44] Ian Boyd

It's also important to note that honoring the user's DPI is only a subset of your real job:

honoring the user's font size

The user's DPI is a side-effect of their font choice.

  • if you honor the user's font size, you will by definition be honoring their DPI (good)
  • if you only honor the user's DPI, you will not be honoring their font choice (bad)

Windows developers need to stop thinking that honoring DPI is something they want to do. You don't want to honor their DPI. DPI is not a setting you want to be honoring. If you honor DPI you are doing it wrong.

You want to honor their font. (which affects DPI)


For decades, Windows has solved this issue with the notion performing layout using Dialog Units, rather than pixels. A "dialog unit" is defined so that font's average character is

  • 4 dialog units (dlus) wide, and
  • 8 dialog units (dlus) high

enter image description here

Delphi does ship with a (buggy) notion of TCustomForm.Scaled [1], where a form tries to automatically adjust based on the

  • Windows DPI settings of the user, verses
  • the DPI setting on the machine of the developer who last saved the form

That doesn't solve the problem when the user uses a font different from what you designed the form with, e.g.:

  • developer designed the form with MS Sans Serif 8pt (where the average character is 6.21px x 13.00px at 96dpi)

  • user running with Tahoma 8pt (where the average character is 5.94px x 13.00px at 96dpi)

    As was the case with anyone developing an application for Windows 2000 or Windows XP.

or

  • developer designed the form with Tahoma 8pt (where the average character is 5.94px x 13.00px at 96dpi)
  • a user running with Segoe UI 9pt (where the average character is 6.67px x 15px, at 96dpi)

Using TCustomForm.Scaled is bad. It's a bad idea. It's a bad choice. It's a bad design. You need to turn .Scaled of all your forms to False at design time.


As a good developer your goal is to honor your user's font preferences. This means that you also need to scale all controls on your form to match the new font size:

  • expand everything horizontally by 12.29% (6.67/5.94)
  • stretch everything vertically by 15.38% (15/13)

Scaled won't handle this for you.

It gets worse when:

  • designed your form at Segoe UI 9pt (the Windows Vista, Windows 7, Windows 8 default)
  • user is running Segoe UI 14pt, (e.g. my preference) which is 10.52px x 25px

Now you have to scale everything

  • horizontally by 57.72%
  • vertically by 66.66%

Scaled won't handle this for you.


If you're smart you can see how honoring DPI is irrelavent:

  • form designed with Segoe UI 9pt @ 96dpi (6.67px x 15px)
  • user running with Segoe UI 9pt @ 150dpi (10.52px x 25px)

You should not be looking at the user's DPI setting, you should be looking at their font size. Two users running:

  • Segoe UI 14pt @ 96dpi (10.52px x 25px)
  • Segoe UI 9pt @ 150dpi (10.52px x 25px)

are running the same font. DPI is just one thing that affects font size; the user's preferences are the other.

StandardizeFormFont

Clovis noticed that i reference a function StandardizeFormFont that fixes the font on a form, and scales it to the new font size. It's not a standard function, but an entire set of functions that accomplish the simple task that Borland never handled.

function StandardizeFormFont(AForm: TForm): Real;
var
    preferredFontName: string;
    preferredFontHeight: Integer;
begin
    GetUserFontPreference({out}preferredFontName, {out}preferredFontHeight);

    //e.g. "Segoe UI",     
    Result := Toolkit.StandardizeFormFont(AForm, PreferredFontName, PreferredFontHeight);
end;

There is no single "font setting" in Windows. Windows has 6 different fonts:

Font How to retrieve
Icon Title SystemParametersInfo(SPI_GETICONTITLELOGFONT)
Caption SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfCaptionFont
Small Caption SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfSmCaptionFont
Menu SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfMenuFont
Status SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfStatusFont
Message SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfMessageFont

But we know from experience that our forms should follow the Icon Title Font setting

procedure GetUserFontPreference(out FaceName: string; out PixelHeight: Integer);
var
   font: TFont;
begin
   font := Toolkit.GetIconTitleFont;
   try
      FaceName := font.Name; //e.g. "Segoe UI"

      //Dogfood testing: use a larger font than we're used to; to force us to actually test it    
      if IsDebuggerPresent then
         font.Size := font.Size+1;
    
      PixelHeight := font.Height; //e.g. -16
   finally
      font.Free;
   end;
end;

Once we know the font size we will scale the form to, we get the form's current font height (in pixels), and scale up by that factor.

For example, if i am setting the form to -16, and the form is currently at -11, then we need to scale the entire form by:

-16 / -11 = 1.45454%

The standardization happens in two phases. First scale the form by the ratio of the new:old font sizes. Then actually change the controls (recursively) to use the new font.

function StandardizeFormFont(AForm: TForm; FontName: string; FontHeight: Integer): Real;
var
    oldHeight: Integer;
begin
    Assert(Assigned(AForm));

    if (AForm.Scaled) then
    begin
        OutputDebugString(PChar('WARNING: StandardizeFormFont: Form "'+GetControlName(AForm)+'" is set to Scaled. Proper form scaling requires VCL scaling to be disabled, unless you implement scaling by overriding the protected ChangeScale() method of the form.'));
    end;

    if (AForm.AutoScroll) then
    begin
        if AForm.WindowState = wsNormal then
        begin
            OutputDebugString(PChar('WARNING: StandardizeFormFont: Form "'+GetControlName(AForm)+'" is set to AutoScroll. Form designed size will be suseptable to changes in Windows form caption height (e.g. 2000 vs XP).'));
                    if IsDebuggerPresent then
                        Windows.DebugBreak; //Some forms would like it (to fix maximizing problem)
        end;
    end;

    if (not AForm.ShowHint) then
    begin
        AForm.ShowHint := True;
        OutputDebugString(PChar('INFORMATION: StandardizeFormFont: Turning on form "'+GetControlName(AForm)+'" hints. (ShowHint := True)'));
                    if IsDebuggerPresent then
                        Windows.DebugBreak; //Some forms would like it (to fix maximizing problem)
    end;

    oldHeight := AForm.Font.Height;

    //Scale the form to the new font size
//  if (FontHeight <> oldHeight) then    For compatibility, it's safer to trigger a call to ChangeScale, since a lot of people will be assuming it always is called
    begin
        ScaleForm(AForm, FontHeight, oldHeight);
    end;

    //Now change all controls to actually use the new font
    Toolkit.StandardizeFont_ControlCore(AForm, g_ForceClearType, FontName, FontHeight,
            AForm.Font.Name, AForm.Font.Size);

    //Return the scaling ratio, so any hard-coded values can be multiplied
    Result := FontHeight / oldHeight;
end;

Here's the job of actually scaling a form. It works around bugs in Borland's own Form.ScaleBy method. First it has to disable all anchors on the form, then perform the scaling, then re-enable the anchors:

TAnchorsArray = array of TAnchors;

procedure ScaleForm(const AForm: TForm; const M, D: Integer);
var
    aAnchorStorage: TAnchorsArray;
    RectBefore, RectAfter: TRect;
    x, y: Integer;
    monitorInfo: TMonitorInfo;
    workArea: TRect;
begin
    if (M = 0) and (D = 0) then
        Exit;

    RectBefore := AForm.BoundsRect;

    SetLength(aAnchorStorage, 0);
    aAnchorStorage := DisableAnchors(AForm);
    try
        AForm.ScaleBy(M, D);
    finally
        EnableAnchors(AForm, aAnchorStorage);
    end;

    RectAfter := AForm.BoundsRect;

    case AForm.Position of
    poScreenCenter, poDesktopCenter, poMainFormCenter, poOwnerFormCenter,
    poDesigned: //i think i really want everything else to also follow the nudging rules...why did i exclude poDesigned
        begin
            //This was only nudging by one quarter the difference, rather than one half the difference
//          x := RectAfter.Left - ((RectAfter.Right-RectBefore.Right) div 2);
//          y := RectAfter.Top - ((RectAfter.Bottom-RectBefore.Bottom) div 2);
            x := RectAfter.Left - ((RectAfter.Right-RectAfter.Left) - (RectBefore.Right-RectBefore.Left)) div 2;
            y := RectAfter.Top - ((RectAfter.Bottom-RectAfter.Top)-(RectBefore.Bottom-RectBefore.Top)) div 2;
        end;
    else
        //poDesigned, poDefault, poDefaultPosOnly, poDefaultSizeOnly:
        x := RectAfter.Left;
        y := RectAfter.Top;
    end;

    if AForm.Monitor <> nil then
    begin
        monitorInfo.cbSize := SizeOf(monitorInfo);
        if GetMonitorInfo(AForm.Monitor.Handle, @monitorInfo) then
            workArea := monitorInfo.rcWork
        else
        begin
            OutputDebugString(PChar(SysErrorMessage(GetLastError)));
            workArea := Rect(AForm.Monitor.Left, AForm.Monitor.Top, AForm.Monitor.Left+AForm.Monitor.Width, AForm.Monitor.Top+AForm.Monitor.Height);
        end;

//      If the form is off the right or bottom of the screen then we need to pull it back
        if RectAfter.Right > workArea.Right then
            x := workArea.Right - (RectAfter.Right-RectAfter.Left); //rightEdge - widthOfForm

        if RectAfter.Bottom > workArea.Bottom then
            y := workArea.Bottom - (RectAfter.Bottom-RectAfter.Top); //bottomEdge - heightOfForm

        x := Max(x, workArea.Left); //don't go beyond left edge
        y := Max(y, workArea.Top); //don't go above top edge
    end
    else
    begin
        x := Max(x, 0); //don't go beyond left edge
        y := Max(y, 0); //don't go above top edge
    end;

    AForm.SetBounds(x, y,
            RectAfter.Right-RectAfter.Left, //Width
            RectAfter.Bottom-RectAfter.Top); //Height
end;

and then we have to recursively actually use the new font:

procedure StandardizeFont_ControlCore(AControl: TControl; ForceClearType: Boolean;
        FontName: string; FontSize: Integer;
        ForceFontIfName: string; ForceFontIfSize: Integer);
const
    CLEARTYPE_QUALITY = 5;
var
    i: Integer;
    RunComponent: TComponent;
    AControlFont: TFont;
begin
    if not Assigned(AControl) then
        Exit;

    if (AControl is TStatusBar) then
    begin
        TStatusBar(AControl).UseSystemFont := False; //force...
        TStatusBar(AControl).UseSystemFont := True;  //...it
    end
    else
    begin
        AControlFont := Toolkit.GetControlFont(AControl);

        if not Assigned(AControlFont) then
            Exit;

        StandardizeFont_ControlFontCore(AControlFont, ForceClearType,
                FontName, FontSize,
                ForceFontIfName, ForceFontIfSize);
    end;

{   If a panel has a toolbar on it, the toolbar won't paint properly. So this idea won't work.
    if (not Toolkit.IsRemoteSession) and (AControl is TWinControl) and (not (AControl is TToolBar)) then
        TWinControl(AControl).DoubleBuffered := True;
}

    //Iterate children
    for i := 0 to AControl.ComponentCount-1 do
    begin
        RunComponent := AControl.Components[i];
        if RunComponent is TControl then
            StandardizeFont_ControlCore(
                    TControl(RunComponent), ForceClearType,
                    FontName, FontSize,
                    ForceFontIfName, ForceFontIfSize);
    end;
end;

With the anchors being recursively disabled:

function DisableAnchors(ParentControl: TWinControl): TAnchorsArray;
var
    StartingIndex: Integer;
begin
    StartingIndex := 0;
    DisableAnchors_Core(ParentControl, Result, StartingIndex);
end;


procedure DisableAnchors_Core(ParentControl: TWinControl; var aAnchorStorage: TAnchorsArray; var StartingIndex: Integer);
var
    iCounter: integer;
    ChildControl: TControl;
begin
    if (StartingIndex+ParentControl.ControlCount+1) > (Length(aAnchorStorage)) then
        SetLength(aAnchorStorage, StartingIndex+ParentControl.ControlCount+1);

    for iCounter := 0 to ParentControl.ControlCount - 1 do
    begin
        ChildControl := ParentControl.Controls[iCounter];
        aAnchorStorage[StartingIndex] := ChildControl.Anchors;

        //doesn't work for set of stacked top-aligned panels
//      if ([akRight, akBottom ] * ChildControl.Anchors) <> [] then
//          ChildControl.Anchors := [akLeft, akTop];

        if (ChildControl.Anchors) <> [akTop, akLeft] then
            ChildControl.Anchors := [akLeft, akTop];

//      if ([akTop, akBottom] * ChildControl.Anchors) = [akTop, akBottom] then
//          ChildControl.Anchors := ChildControl.Anchors - [akBottom];

        Inc(StartingIndex);
    end;

    //Add children
    for iCounter := 0 to ParentControl.ControlCount - 1 do
    begin
        ChildControl := ParentControl.Controls[iCounter];
        if ChildControl is TWinControl then
            DisableAnchors_Core(TWinControl(ChildControl), aAnchorStorage, StartingIndex);
    end;
end;

And anchors being recursively re-enabled:

procedure EnableAnchors(ParentControl: TWinControl; aAnchorStorage: TAnchorsArray);
var
    StartingIndex: Integer;
begin
    StartingIndex := 0;
    EnableAnchors_Core(ParentControl, aAnchorStorage, StartingIndex);
end;


procedure EnableAnchors_Core(ParentControl: TWinControl; aAnchorStorage: TAnchorsArray; var StartingIndex: Integer);
var
    iCounter: integer;
    ChildControl: TControl;
begin
    for iCounter := 0 to ParentControl.ControlCount - 1 do
    begin
        ChildControl := ParentControl.Controls[iCounter];
        ChildControl.Anchors := aAnchorStorage[StartingIndex];

        Inc(StartingIndex);
    end;

    //Restore children
    for iCounter := 0 to ParentControl.ControlCount - 1 do
    begin
        ChildControl := ParentControl.Controls[iCounter];
        if ChildControl is TWinControl then
            EnableAnchors_Core(TWinControl(ChildControl), aAnchorStorage, StartingIndex);
    end;
end;

With the work of actually changing a controls font left to:

procedure StandardizeFont_ControlFontCore(AControlFont: TFont; ForceClearType: Boolean;
        FontName: string; FontSize: Integer;
        ForceFontIfName: string; ForceFontIfSize: Integer);
const
    CLEARTYPE_QUALITY = 5;
var
    CanChangeName: Boolean;
    CanChangeSize: Boolean;
    lf: TLogFont;
begin
    if not Assigned(AControlFont) then
        Exit;

{$IFDEF ForceClearType}
    ForceClearType := True;
{$ELSE}
    if g_ForceClearType then
        ForceClearType := True;
{$ENDIF}

    //Standardize the font if it's currently
    //  "MS Shell Dlg 2" (meaning whoever it was opted into the 'change me' system
    //  "MS Sans Serif" (the Delphi default)
    //  "Tahoma" (when they wanted to match the OS, but "MS Shell Dlg 2" should have been used)
    //  "MS Shell Dlg" (the 9x name)
    CanChangeName :=
            (FontName <> '')
            and
            (AControlFont.Name <> FontName)
            and
            (
                (
                    (ForceFontIfName <> '')
                    and
                    (AControlFont.Name = ForceFontIfName)
                )
                or
                (
                    (ForceFontIfName = '')
                    and
                    (
                        (AControlFont.Name = 'MS Sans Serif') or
                        (AControlFont.Name = 'Tahoma') or
                        (AControlFont.Name = 'MS Shell Dlg 2') or
                        (AControlFont.Name = 'MS Shell Dlg')
                    )
                )
            );

    CanChangeSize :=
            (
                //there is a font size
                (FontSize <> 0)
                and
                (
                    //the font is at it's default size, or we're specifying what it's default size is
                    (AControlFont.Size = 8)
                    or
                    ((ForceFontIfSize <> 0) and (AControlFont.Size = ForceFontIfSize))
                )
                and
                //the font size (or height) is not equal
                (
                    //negative for height (px)
                    ((FontSize < 0) and (AControlFont.Height <> FontSize))
                    or
                    //positive for size (pt)
                    ((FontSize > 0) and (AControlFont.Size <> FontSize))
                )
                and
                //no point in using default font's size if they're not using the face
                (
                    (AControlFont.Name = FontName)
                    or
                    CanChangeName
                )
            );

    if CanChangeName or CanChangeSize or ForceClearType then
    begin
        if GetObject(AControlFont.Handle, SizeOf(TLogFont), @lf) <> 0 then
        begin
            //Change the font attributes and put it back
            if CanChangeName then
                StrPLCopy(Addr(lf.lfFaceName[0]), FontName, LF_FACESIZE);
            if CanChangeSize then
                lf.lfHeight := FontSize;

            if ForceClearType then
                lf.lfQuality := CLEARTYPE_QUALITY;
            AControlFont.Handle := CreateFontIndirect(lf);
        end
        else
        begin
            if CanChangeName then
                AControlFont.Name := FontName;
            if CanChangeSize then
            begin
                if FontSize > 0 then
                    AControlFont.Size := FontSize
                else if FontSize < 0 then
                    AControlFont.Height := FontSize;
            end;
        end;
    end;
end;

That's a whole lot more code than you thought it was going to be; i know. The sad thing is that there is no Delphi developer on earth, except for me, who actually makes their applications correct.

Dear Delphi Developer: Set your Windows font to Segoe UI 14pt, and fix your buggy application

Note: Any code is released into the public domain. No attribution required.

[1] https://docwiki.embarcadero.com/Libraries/Sydney/en/Vcl.Forms.TCustomForm.Scaled

(1) Thanks for answer, but what do you suggest for the real world? Implement a resize of all the controls manually? - UnDiUdin
@user193655 i plan to write an long, involved, essay answer on the subject...sometime...based on 3 years of learning it's taken to find the decent Delphi solution. But in the end i have one function to fix a form: StandardizeFormFont(Self) - Ian Boyd
@IanBoyd, I have read 3 posts where you refer to StandardizeForm()/StandardizeFormFont() This code is open source? - Clóvis Valadares Junior
@ClóvisValadaresJunior No, i wrote a set of functions, that culminiate in the single helper function StandardizeFormFont. There's a lot of detailed work in it, because you have to undo some of Delphi's buggy form-sizing code, in order to size it correctly. i'll add it as an addendum here. - Ian Boyd
Ian, Thanks for your work! Are your functions available as an unit? - Casady
@Casady No, my Tookit.pas unit is a huge collection of a few thousand helper functions and utilities. i posted the relevent pieces in the answer; except for Toolkit.GetIconTitleFont, which i will leave as an exercise for the reader :) - Ian Boyd
Ian - I'm trying to use the functions you posted and it almost worked. Got a couple of pointer exceptions here and there, thought, so I didn't went further. - Leonardo Herrera
(4) "The sad thing is that there is no Delphi developer on earth, except for me, who actually makes their applications correct." That's a very arrogant statement that is incorrect. From my answer: In fact my version of ScaleFromSmallFontsDimension also makes allowance for the possibility of the form font differing at runtime from that set at designtime. The scaling must account for this because the absolute dimension values used in the source code are assumed to be relative to the baseline of 8pt Tahoma at 96dpi. Your's is a good answer mind you, +1. - David Heffernan
@DavidHeffernan i swear i saw a comment/answer from you somewhere that said you wouldn't spend the time and energy for DPI support unless the client specifically asks for it. - Ian Boyd
(1) @Ian Not me that said that. Sounds like Warren. - David Heffernan
@DavidHeffernan How does ScaleFromSmallFontsDimension handle other font sizes? If Screen.PixelsPerInch is 96, and SmallFontsPixelsPerInch is 96, then isn't that just multiplying a value by unity? - Ian Boyd
The more complex version in my code accounts for relative font metrics, just as you do. Even that's not perfect though. Because font metrics are for average characters. And a specific piece of text may vary significantly from the average. Specifically text may be proportionally wider in one font than in another. That never tends to be an issue in my experience. - David Heffernan
The code that checks AutoScroll, at least on my computer, won't work if the form border style is not resizable. If you check the VCL source for SetBorderStyle, you'll see that AutoScroll is reset to false if the border isn't resizable. So the check will always think the form is ok for fixed dimension forms, even if AutoScroll was true in the DFM file. That's a problem, because the DFM is also still storing the problematic Width/Height properties and not ClientWidth/ClientHeight. - James Johnston
This code is amazing Ian, thank you so much for sharing it! One question though: in your experience what's the best place to call StandardizeFormFont(Self)? I found it best to put it to the end of each FormCreate (after initializing things and maybe resizing a few controls) but then the form position corrections are not called. - Steve
(1) @Steve i call StandardizeFormFont(Self) pretty much as the first thing in the form's OnCreate handler. You should then also override the protected procedure ChangeScale(M, D: Integer); and apply your scaling and repositioning of items there. Inside ChangeScale is where i keep a running FCurrentScaleFactor := FCurrentScaleFactor * M/D so i always know my current runtime scaling. If you ever try to position things with hard-coded pixel values, you should actually be doing something like Button1.Top := Round(Self.Top + 16*FCurrentScaleFactor); so you're not using hard-coded pixels. - Ian Boyd
@WarrenP What led you here two years later? - Ian Boyd
This is a good question, and I like to go back and read other answers that have helpful stuff. It's fun. - Warren P
(2) Recently ran across this question and answer. I've collected all Ian's code into a working unit here: pastebin.com/dKpfnXLc and posted bout it on Google+ here: goo.gl/0ARdq9 Posting here in case anyone finds this useful. - W.Prins
A related question that should interest readers of this question: goo.gl/cNnATH - W.Prins
"Set your Windows font to Segoe UI 14pt, and fix your buggy application" - Why is the poor developer's fault? Isn't this Embarcadero's fault? - IceCold
(1) @NAZCA It is Embarcardero's fault. But the user doesn't care that your development tool doesn't support fonts other than MS Sans Serif 8pt @ 96dpi. So it's up to us to fix it. Perhaps one day Delphi will support the Windows 95 taskbar; but i'm not hopeful. - Ian Boyd
@IanBoyd "Perhaps one day Delphi will support the Windows 95 taskbar" in 2017?? ;-) (I do see now that you mean any taskbar) PS: I arrived here because I'm upgrading a D7 app to Berlin, and the app is being used on monitors where the users sometimes change the scaling... - Dave Nottage
revisiting this many years later as I have an app from D2010 age using DevEx, which needless to say looks horrible on scaled 4k monitor. To me, this answer seems like the best route to attempt, but then I have not worked with the latest Delphi - 11.2 and the latest DevEx. Can anyone offer any advice, i.e. if I just upgrade to the latest and shiniest will all the scaling trouble simply go away ? - Peter
3
[+11] [2011-11-29 13:40:28] avra

Here is my gift. A function that can help you with horizontal positioning of elements in your GUI layouts. Free for all.

function CenterInParent(Place,NumberOfPlaces,ObjectWidth,ParentWidth,CropPercent: Integer): Integer;
  {returns formated centered position of an object relative to parent.
  Place          - P order number of an object beeing centered
  NumberOfPlaces - NOP total number of places available for object beeing centered
  ObjectWidth    - OW width of an object beeing centered
  ParentWidth    - PW width of an parent
  CropPercent    - CP percentage of safe margin on both sides which we want to omit from calculation
  +-----------------------------------------------------+
  |                                                     |
  |        +--------+       +---+      +--------+       |
  |        |        |       |   |      |        |       |
  |        +--------+       +---+      +--------+       |
  |     |              |             |            |     |
  +-----------------------------------------------------+
  |     |<---------------------A----------------->|     |
  |<-C->|<------B----->|<-----B----->|<-----B---->|<-C->|
  |                    |<-D>|
  |<----------E------------>|

  A = PW-C   B = A/NOP  C=(CP*PW)/100  D = (B-OW)/2
  E = C+(P-1)*B+D }

var
  A, B, C, D: Integer;
begin
  C := Trunc((CropPercent*ParentWidth)/100);
  A := ParentWidth - C;
  B := Trunc(A/NumberOfPlaces);
  D := Trunc((B-ObjectWidth)/2);
  Result := C+(Place-1)*B+D;
end;

(2) I am glad you like it Warren. It is about 15 years old when there were no solutions available for the problem I had to solve. And even today there can be a situation where it can be applied. B-) - avra
4