2

Windows 11 introduced a new option that allows to customize the screen scale:

custom scaling option

When a custom scaling is applied reading and writing form width and height behaves bad.

In particular, in my Delphi application, every time a form is closed I store in an INI file the form width and height, to restore them the next time the form is shown.

// store to file code
IniFile.WriteInteger('FORM','Width',AForm.Width);
IniFile.WriteInteger('FORM','Top',AForm.Top);

// restore from file code
  StoredHeight:= IniFile.readInteger('FORM','Height',AForm.Height);
  StoredWidth := IniFile.readInteger('FORM','Width',AForm.Width);

if StoredWidth >= Screen.Width then
  AForm.Width := Screen.Width
else
  begin
    AForm.Width := StoredWidth;
    if StoredLeft < 0 then
    AForm.left  := 0
    else
      AForm.left := StoredLeft;
  end;
  if StoredHeight >= Screen.Height then
  AForm.Height := Screen.Height
  else
    begin
      AForm.Height :=  StoredHeight;
      if StoredTop < 0 then
      AForm.Top := 0
      else
        AForm.Top := StoredTop;
    end;

The problem is that when TForm.Height and TForm.Width are stored to file they are increased by the custom scaling percentage (or at least to a number close to it), while when the values are set they are correctly applied. As a consequence every time the form is shown it is bigger and bigger.

Is it normal that I must deal with scaling and I must compensate it (by dividing the values by the scale factor)? I would expect this is transparent to me.

Moreover does anyone know how to retrieve the custom scaling setting from the windows API? I googled unsuccessfully.

Thanks!

Tom Brunberg
  • 20,312
  • 8
  • 37
  • 54
UnDiUdin
  • 14,924
  • 39
  • 151
  • 249
  • 1
    You can get that information with https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdpiforwindow – Dalija Prasnikar Sep 06 '22 at 08:41
  • thanks for the link, still i do not understand why TForm.Height that should return the value in pixels does return a wrong value – UnDiUdin Sep 06 '22 at 15:15
  • 1
    If you don't get expected results, then this is most likely due to missing or wrong manifest entry for High DPI support. See https://stackoverflow.com/questions/23551112/how-can-i-set-the-dpiaware-property-in-a-windows-application-manifest-to-per-mo It is also possible that numbers are additionally scaled because they are being applied before VCL automatic scaling kicks in. – Dalija Prasnikar Sep 07 '22 at 07:52
  • I believe, it was possible to set custom scaling even in Win XP. – Torbins Sep 08 '22 at 11:31
  • 2
    There is bigger problem with your code: imagine someone has changed scaling when your app wasn't running. When your app will run next time it will be too big or too small. That is why it is better to convert all sizes to 100% scaling before storing them in files. And then convert them to current scaling, before usage. – Torbins Sep 08 '22 at 11:35
  • thanks for your replies I still do not understand why my application has always performed well in storing and retrieving form height and width and with WIndows 11, when a custom scaling is set it now fails. It seems the VCL is not aware of this new setting? – UnDiUdin Sep 08 '22 at 13:43
  • I decided to start a bounty on this question. Thank you! – UnDiUdin Sep 08 '22 at 13:44

1 Answers1

4

There is an improvement in Delphi 11.0 Alexandria. The IDE now fully supports high DPI scaling on high-resolution screens. This high DPI support includes support in the code editor, and when designing forms, both VCL and FMX. There is a setting to control the scaling in the VCL form designer and so you can set it to unscaled by default.

You have to compile your application to support high DPI (‘DPI aware’). If an application is ‘DPI unaware’, Windows will scale it up, but upscaling adds blurriness. It is much better if an app is ‘DPI aware’ so the scaling applied to its window(s) are crisply.

You can scale the VCL form designer to any DPI (any scale). This is done using the same scaling tech that the VCL uses when scaling itself at runtime. This setting is in Tools > Options > User Interface > Form Designer > High DPI. When you change it, you’ll need to close and reopen the form designer to have an effect.

By default, when you open a form, the form is designed at 96 DPI – that is, at 100%. One key bit of knowledge is that when a form is scaled, the Left, Height etc properties are changed. This is exactly the same as when you run an app and it is scaled; those values are multiplied by the screen scale.

Windows (and so the VCL) uses integer coordinates for its sizes and locations. This means that any scaling may not always be exactly precise. In practice, it’s fine when scaling once (such as when an app is run, and it scales up from the low-res coordinates with which it was designed.) It’s also fine scaling a few times, such as moving from one monitor to another after launching. It matters more when scaling many times. So it is fine to design at high DPI at any scale, and run even at a lower scale – the VCL will scale your app correctly – but it is important to avoid scaling over and over again, which will happen if every time you open a form in the designer it’s opened at a different DPI.

You can read more about the DPI Scaling, in the following embarcadero blog: https://blogs.embarcadero.com/new-in-rad-studio-11-high-dpi-ide-and-form-designing/

First thing to try, is to disable the Scaled property of each Form of your application (you can find this property on object inspector of the Form), and then test if the width and heigth values are written correctly. If this doesn't fix your problem, then I am suggesting some workarounds.

Here is a workaround, until embarcadero fixes this issue.

You can save the width and height values when closing the form, after you normalize them (using MulDiv function) depending of the dpi the end user has set.

The code would be something like this:

procedure AForm.FormClose(Sender: TObject; var Action: TCloseAction);
var
  IniFile: TIniFile;
begin
  IniFile := TIniFile.Create( ChangeFileExt( Application.ExeName, '.ini' ) );
  try

    IniFile.WriteInteger('FORM','Width',MulDiv(AForm.Width, Screen.PixelsPerInch, PixelsPerInch));
    IniFile.WriteInteger('FORM','Height',MulDiv(AForm.Height, Screen.PixelsPerInch, PixelsPerInch))
  finally
    IniFile.Free;
  end;

end;

Although, you could try using Monitor.PixelsPerInch instead of Screen.PixelsPerInch. That way it will work for cases when different custom scaling is applied in different monitors.

Another workaroung, would be to read the custom scaling property from the registry, and save width and height values after normalizing them. The custom scaling property, is saved in the following registry entry: HKEY_CURRENT_USER\Control Panel\Desktop in the LogPixels key. This registry key controls the DPI scaling level in Windows 10 and 11.

LogPixels = 96,  means 100% scaling
LogPixels = 120, means 125% scaling
LogPixels = 144, means 150% scaling
LogPixels = 192, means 200% scaling etc.

So the workaround is to read this registry key, and scale down (or up) the width and the height you are going to write to the ini file.

The code would be something like this:

procedure AForm.FormClose(Sender: TObject; var Action: TCloseAction);
var
  IniFile: TIniFile;
  RegKey: TRegistry;
  intScalingSize: Integer;   //Here I will save the scaling size stored in the registry. (eg. 96, 144 etc)
  reaScalingSize: Real;      //Here I will save the scaling size percentage (eg. for 96 I will store 1.0, for 144 I will store 1.5 etc)
  intFormUnscaledWidth, intFormUnscaledHeight: Integer;  //Here I will store the unscaled width and height.
begin
  IniFile := TIniFile.Create( ChangeFileExt( Application.ExeName, '.ini' ) );
  RegKey := TRegistry.Create;
  intScalingSize := 0;
  try
    RegKey.RootKey := HKEY_CURRENT_USER;
    if RegKey.OpenKey('Control Panel\Desktop', False) then
      begin
        intScalingSize := RegKey.ReadInteger('LogPixels');
        RegKey.CloseKey;
      end
      else
        intScalingSize := 96;
    // LogPixels = 96,  means 100% scaling
    // LogPixels = 120, means 125% scaling
    // LogPixels = 144, means 150% scaling
    // LogPixels = 192, means 200% scaling
    //Use linear interpolation to map scaling to percentage
    reaScalingSize := 100 + ((100/96)*(intScalingSize - 96));

    intFormUnscaledWidth := Round(AForm.Width / reaScalingSize);
    intFormUnscaledHeight := Round(AForm.Height / reaScalingSize);

    IniFile.WriteInteger('FORM','Width',intFormUnscaledWidth);
    IniFile.WriteInteger('FORM','Height',intFormUnscaledHeight);
  finally
    RegKey.Free;
    IniFile.Free;
  end;

I hope this workaround will help you, until embarcadero fixes this issue.

George Betsis
  • 450
  • 2
  • 4
  • Thanks for the answer, I will try this as soon as possible and in case it works I will award you the bounty. Bye! – UnDiUdin Sep 15 '22 at 06:48
  • Reading LogPixels from the registry is a bad idea, because someone could change windows scaling when the app wasn't running. If you want to save scaled values, you should also have additional key with current scale in your ini. – Torbins Sep 15 '22 at 08:05
  • I awarded the bounty for this answer. I used this approach `IniFile.WriteInteger('FORM','Width',MulDiv(AForm.Width, Screen.PixelsPerInch, PixelsPerInch));` and it works. The only problem with it is that it is not perfect on multiple monitors with different scaling, but my app still does not support multiple monitors well, so this is something for the future. Thanks! – UnDiUdin Sep 15 '22 at 08:23
  • You could try using Monitor.PixelsPerInch instead of Screen.PixelsPerInch. That way it will work for cases when different custom scaling is applied in different monitors. – George Betsis Sep 15 '22 at 18:30