Search code examples
delphidpihdpi

Delphi High DPI switch between own scaling and Windows scaling


Some of my customers want to be able to scale my application manually (when Windows dpi is set to 96), so I had to implement scaling. Unfortunately these customers cannot go with setting Windows DPI to an other value and let WIndows scale my app because some very important applications they use do not behave well at all on resolutions <> 96 DPI.

I managed to make my Delphi 10.1 application scale quite well even on 200% but the higher the factor becomes the more some proportions become "not so nice looking". Many 3rd party components need special treatment for scaling and even then do not scale 100% accurate. Although applications scaled by windows look a little blurry on high resolutions, all proportions are 100% accurate and imho the app looks much more professional.

So I asked myself whether it is possible to create a setting that allows to tell Windows to do the scaling as a default and only scale on my own if the customer wants a scaling that is different from the current Windows scaling. This setting is hosted in the Windows manifest of the executable that is read on start of the application. Is there a way to change it at runtime (early startup of the application)? Creating two executables with different manifests is surely no good solution.

Thanks for any help


Solution

  • Thanks to Sertac Akyuz I found a solution to my problem. In the Initialization part of the unit containing the scaling code I can switch between DPI-Awareness and Non-DPI-Awareness. It is important not to have this setting in the application manifest, which can be achieved by supplying a custom manifest like this (use control decoration and run with rights of current user):

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
     <dependency>
       <dependentAssembly>
         <assemblyIdentity
           type="win32"
           name="Microsoft.Windows.Common-Controls"
           version="6.0.0.0"
           publicKeyToken="6595b64144ccf1df"
           language="*"
           processorArchitecture="*"/>
       </dependentAssembly>
     </dependency>
     <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
       <security>
         <requestedPrivileges>
           <requestedExecutionLevel
             level="asInvoker"
             uiAccess="false"/>
           </requestedPrivileges>
       </security>
     </trustInfo>
    </assembly>
    

    This is the actual code switching depending on a registry key:

    // Set DPI Awareness depending on a registry setting
    with TRegIniFile.create('SOFTWARE\' + SRegName) do
    begin
      setting := readInteger('SETTINGS', 'scale', 0);
      Free;
    end;
    handle := LoadLibrary('shcore.dll');
    if handle <> 0 then
    begin
      setProcessDPIAwareness := GetProcAddress(handle, 'SetProcessDpiAwareness');
      if Assigned(setProcessDPIAwareness) then
      begin
        if setting < 2 then
          // setting <2 means no scaling vs Windows
          setProcessDPIAwareness(0)
        else
          // setting 2: 120%, 3: 140% vs. Windows
          // The actual used scaling factor multiplies by windows DPI/96
          setProcessDPIAwareness(1);
      end;
      FreeLibrary(handle);
      // Get windows scaling as Screen.PixelsPerInch was read before swiching DPI awareness
      // Our scaling routines now work with WinDPI instead of Screen.PixelsPerInch
      WinDPI:= Screen.MonitorFromWindow(application.handle).PixelsPerInch;
    end;
    

    The last line of this snippet retrieves the current DPI for the current monitor as screen.pixelsperinch seems to be initialized before and does always return 96 as for a non dpi aware application. I use the value of winDPI in all subsequent scaling calculations and it works perfect.