High DPI

From Free Pascal wiki
Jump to navigationJump to search

Deutsch (de) English (en) español (es) русский (ru)

Introduction

DPI (Dots Per Inch) is the relation between size in pixels and the actual display size. Here dot is an equivalent for pixel in printing terminology. Applications can either use pixel sizes, or take into account the actual display size. In this second case, sizes are given in points.

Most of today operating systems use default DPI set to 96 and allow to change it to higher value manually. The physical DPI can be determined from display through EDID protocol from physical size data and actual resolution. But the physical DPI is not used automatically by system so if you connect video output to monitor with different size then sceen resolution and visual size of controls are not automatically changed.

Usually DPI is presented as one value but it can be different for horizontal and vertical axes if pixel is not square.

In addition to basic application DPI awareness you can add own DPI options to your application to allow users to set custom per application DPI to overcome wrong system DPI setting.

Lazarus DPI related properties


Pixels and points

For example 300 DPI means that there are 300 pixels (or dots) per inch. There are 72 points per inch, so :

300 pixels ↔ 1 inch

300/72 pixels ↔ 1 point

4.16 pixels ↔ 1 point

Now with 96 DPI :

96 pixels ↔ 1 inch

1.33 pixel ↔ 1 point

Now with 144 DPI :

144 pixels ↔ 1 inch

2 pixels ↔ 1 point

Setting High DPI

Windows

On Windows 95 and later, it is possible to change the DPI ratio to make elements bigger. High DPI means any custom DPI setting with more than 96 DPI (the default setting) *.

High DPI awareness means that an application takes this DPI setting into account.

Windows Vista and Windows 7

In Windows 7 go to "Control Panel > Appearance and Personalization > Display" (or just Control Panel > Display in recent updates).

Select Smaller 100% (default), Medium 125% or Larger 150%. If you select 100% (96 DPI) this is the default Windows DPI setting, (High DPI is not the default).

If you select 125% (120 DPI) the option "Use Windows XP style DPI scaling" is enabled. Applications you run under this setting are scaled as if running under Windows XP.

If you select 150% (144 DPI) the option "Use Windows XP style DPI scaling" is disabled (DPI Virtualization is enabled), and applications you run under this setting must be High DPI Awareness to prevent system scaling which will produce a blurred image.

You can also set your custom DPI setting via the option "Set custom text size (DPI)" and enable/disable the DPI Virtualization.

Windows 8 Metro Applications

For Windows 8 Metro Applications read this http://blogs.msdn.com/b/b8/archive/2012/03/21/scaling-to-different-screens.aspx

Windows 10

Windows 10 "Control Panel > Appearance and Personalization > Display" have more options. You can have different font sizes for each element: Title bar, Menu, Dialog box and so on. Ensure you test twice in order to check if everything works under different sizes.

Now is based on Font Size, not DPI. The DPI option is not recommended, but still there. So, instead of changing the size of all elements in desktop, this will change just the font size (And of course everything else is changed to fit).

Remember that under Windows 10 there are Universal Applications (WinRT) and the classic desktop applications (Win32). We're talking here about desktop applications.

Linux

On Linux DPI setting is more complicated and depends on used software and their version.

You can discover your current monitor DPI by command:

xdpyinfo|grep dots

You can change DPI to new value by command:

xrandr --dpi 144x144

To preserve setting after reboot you need to add the command as script to /etc/X11/Xsession.d/77set_dpi.

More information:

Examples

Fixed Font Sizes (not HighDPI)

Here is a form with an undefined font size (set to zero, which is the default value). It has been designed at 96 DPI (100%), and it looks like this :

Testdpi100.png

Now, at 120 DPI (125%), it becomes :

Testdpi125.png

As you can see, the font gets bigger and so the text is clipped. The window title gets bigger, but the client area of the window remains the same size. Note that these changes in size can occur by using an application with a different Windows theme, or with another operating system.

To avoid this, you must set the font size to a non-zero value. Note that Font.Size is expressed in points and Font.Height is expressed in pixels. In fact, only the value of Font.Height is stored, and Font.Size changes according to current DPI value. So if we set the font size, it will be fixed to a certain size in pixels.

If we try again with a fixed font size of 9 points, then at 96 DPI (100%), we get this :

Testdpi100fixedM12P9.png

Now if the same program is run at 120 DPI (125%), it becomes :

Testdpi125fixedM12P9.png

The result is the almost the same. The title bar is bigger, but the client area and the font size is the same. Note that in fact, the size in points of the font has changed.

The conclusion from this is that it is possible to avoid inconsistency in the display by fixing font sizes. But we do not take into account that the graphical elements may be smaller according to actual DPI of the screen. With DPI awareness, it is possible to make an application behave as if it knew the real size of the pixels.


DPI Aware Application (For Vista +)

CPickSniff is an application to capture screen colors. We will use it as an example to see how High DPI works in Windows.

Default DPI

This is the app running at 96 DPI (100%). It's the default mode, when scaling isn't necessary.

cpicksniff defaultdpi.png

Windows DPI Scaling

This is same app running at 144 DPI (150%) without a manifest, so Windows scales it like a bitmap. The result is a blurred image.

cpicksniff blured.png

With Manifest

Running at 144 DPI (150%). This time the app includes a manifest but the application contains no code to handle scaling. Items aren't scaled whereas fonts are scaled (Windows does this automatically), so text is clipped.

cpicksniff nohighdpi.png

High DPI

Finally with both a manifest and a coded scaling handler, the app is in High DPI.

cpicksniff highdpi.png

STEP 1 - Declare High DPI Awareness

To do this we need a manifest file that includes the declaration, with Lazarus 0.9.30 we can do this by going to Options > Project Options > then selecting the options "Use Manifest to Enable Themes (Windows)" and "Dpi Aware application (for Vista +)".

In current lazarus versions there are more settings and the names of the settings has been changed since the first appear in version 0.9.30. Now you have more options. Select the appropriate for your needs.

Next choose a method of scaling: Scaled property, AutoAdjustLayout or ScaleDPI, see step 2.

STEP 2 - Scale Forms and Controls (Scaled property method)

With Lazarus 1.7 we can scale the controls by checking the option "Scaled" of the Form in the object inspector.

STEP 2 - Scale Forms and Controls (AutoAdjustLayout method)

To do this we can call AutoAdjustLayout OnCreate event of each form:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Self.AutoAdjustLayout(lapAutoAdjustForDPI, Self.DesignTimeDPI, Screen.PixelsPerInch, Self.Width, ScaleX(Self.Width, Self.DesignTimeDPI));
end;

Note the last parameter, you can scale the form according to DPI or maybe keep width and height, you choose if scale it or not.

Or we can use a procedure to scale all forms (maybe not recommended because sometimes want to update the width and height and sometimes not, you decide):

uses
  Forms, Controls, Graphics;
 
procedure HighDPI;
var
  i: integer;
begin
  for i := 0 to Screen.FormCount - 1 do
    Screen.Forms[i].AutoAdjustLayout(lapAutoAdjustForDPI, Screen.Forms[i].DesignTimeDPI, Screen.PixelsPerInch, Screen.Forms[i].Width, ScaleX(Screen.Forms[i].Width, Screen.Forms[i].DesignTimeDPI));
end;

Please note the uses clause since we need these units (for example if you add HighDPI to the lpr file directly instead of creating a new unit).

In your LPR:

begin
  RequireDerivedFormResource:=True;
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  HighDPI; // Call it here below all forms creation
  Application.Run;
end.

STEP 2 - Scale Forms and Controls (ScaleDPI method)

This is a custom method, the first that was added to this wiki article.

To do this we can call ScaleDPI procedure in the OnCreate event of each form in your project.

First copy the code below and save it to a text file "uscaledpi.pas":

unit uscaledpi;

{$mode objfpc}{$H+}

interface

uses
  Forms, Graphics, Controls;

procedure HighDPI(FromDPI: integer);
procedure ScaleDPI(Control: TControl; FromDPI: integer);

implementation

procedure HighDPI(FromDPI: integer);
var
  i: integer;
begin
  if Screen.PixelsPerInch = FromDPI then
    exit;

  for i := 0 to Screen.FormCount - 1 do
    ScaleDPI(Screen.Forms[i], FromDPI);
end;

procedure ScaleDPI(Control: TControl; FromDPI: integer);
var
  i: integer;
  WinControl: TWinControl;
begin
  if Screen.PixelsPerInch = FromDPI then
    exit;

  with Control do
  begin
    Left := ScaleX(Left, FromDPI);
    Top := ScaleY(Top, FromDPI);
    Width := ScaleX(Width, FromDPI);
    Height := ScaleY(Height, FromDPI);
  end;

  if Control is TWinControl then
  begin
    WinControl := TWinControl(Control);
    if WinControl.ControlCount = 0 then
      exit;
    for i := 0 to WinControl.ControlCount - 1 do
      ScaleDPI(WinControl.Controls[i], FromDPI);
  end;
end;

end.

Copy the "uscaledpi.pas" file to the main folder of your project:

 MyProject\uscaledpi.pas
 

In the "uses" section of your project you need to add "uScaleDPI":

unit form1;
  
{$mode objfpc}{$H+}
  
interface
  
uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs,
  uScaleDPI; // This includes ScaleDPI procedure

The OnCreate event of each form calls the procedure in this way:

procedure TForm1.FormCreate(Sender: TObject);
begin
  ScaleDPI(Self,96); // 96 is the DPI you designed the Form1  
end;

Scale All Forms

You can resize all forms at once without having to touch each form's OnCreate event. In order to do this open your project source (typically the Project1.lpr file) and add uScaleDPI in the uses clause.

Then call the procedure HighDPI below the code that initializes the forms:

begin
  RequireDerivedFormResource := True;
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.CreateForm(TForm2, Form2);
  Application.CreateForm(TForm3, Form3);
  HighDPI(96);  // 96 is the DPI you designed the Form1, Form2 & Form3
  Application.Run;
end.

The result looks like this:

program Project1;

{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  {$ENDIF}{$ENDIF}
  Interfaces, Forms,
  Unit1, Unit2, Unit3,
  uScaleDPI;

{$R *.res}

begin
  RequireDerivedFormResource := True;
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.CreateForm(TForm2, Form2);
  Application.CreateForm(TForm3, Form3);
  HighDPI(96);
  Application.Run;
end.

Advanced

Some controls have more properties or different property names like TToolBar buttons (ButtonHeight / ButtonWidth instead Width / Height). Also if you use fixed font sizes the behaviour can change in different OSs.

You can edit the ScaleDPI procedure to include code to scale all controls in the way you want.

This is the uscaledpi used in LazPaint. This is very useful for scaling ToolBars and ToolBox.

This is not the final High DPI unit, for example you can use under Windows different LCL widgets, like Qt and this can change the final result.

Link: uscaledpi.pas in LazPaint

Using AutoSize

You can enable the 'AutoSize' option for each control you have (including Forms). Then test the effect under the different DPI modes, with different 'skinning' themes (if available in your target OS) and different font sizes. This can be very useful technique, and has been employed to solve several Lazarus IDE HighDPI issues.

For example using the default AutoSize and ChildSizing most controls can be automatically sized and positioned. But the spacing must be scaled:

  with WinControl.ChildSizing do
  begin
    HorizontalSpacing := ScaleX(HorizontalSpacing, FromDPI);
    LeftRightSpacing := ScaleX(LeftRightSpacing, FromDPI);
    TopBottomSpacing := ScaleY(TopBottomSpacing, FromDPI);
    VerticalSpacing := ScaleY(VerticalSpacing, FromDPI);
  end;

For more information see:

The uscaledpi.pas with this additional code:

 unit uscaledpi;

{$mode objfpc}{$H+}

interface

uses
  Forms, Graphics, Controls;

procedure HighDPI(FromDPI: integer);
procedure ScaleDPI(Control: TControl; FromDPI: integer);

implementation

procedure HighDPI(FromDPI: integer);
var
  i: integer;
begin
  if Screen.PixelsPerInch = FromDPI then
    exit;

  for i := 0 to Screen.FormCount - 1 do
    ScaleDPI(Screen.Forms[i], FromDPI);
end;

procedure ScaleDPI(Control: TControl; FromDPI: integer);
var
  i: integer;
  WinControl: TWinControl;
begin
  if Screen.PixelsPerInch = FromDPI then
    exit;

  with Control do
  begin
    Left := ScaleX(Left, FromDPI);
    Top := ScaleY(Top, FromDPI);
    Width := ScaleX(Width, FromDPI);
    Height := ScaleY(Height, FromDPI);
  end;

  if Control is TWinControl then
  begin
    WinControl := TWinControl(Control);
    if WinControl.ControlCount = 0 then
      exit;

    with WinControl.ChildSizing do
    begin
      HorizontalSpacing := ScaleX(HorizontalSpacing, FromDPI);
      LeftRightSpacing := ScaleX(LeftRightSpacing, FromDPI);
      TopBottomSpacing := ScaleY(TopBottomSpacing, FromDPI);
      VerticalSpacing := ScaleY(VerticalSpacing, FromDPI);
    end;

    for i := 0 to WinControl.ControlCount - 1 do
      ScaleDPI(WinControl.Controls[i], FromDPI);
  end;
end;

end.


Per-Monitor DPI Aware Application (since Windows 8.1)

Windows 8.1 introduced the possibility to have different scaling factors for each display connected to a computer (e.g. different DPI values per monitor). In order to support per-monitor DPI-awareness some additional changes are required.

STEP 1 - Declare Per-Monitor High DPI Awareness

In order to declare per-monitor DPI awareness new values were added to the dpiAware property of the application manifest (see [1], [2]). Lazarus does not yet support these values as an UI option (see issue 30170), so you have to declare the value by supplying you own application manifest.

To do so create 2 files with the following content:

highDPI.rc
1 24 "highDPI.xml"
highDPI.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
    <asmv3:windowsSettings
         xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
      <dpiAware>True/PM</dpiAware>
    </asmv3:windowsSettings>
  </asmv3:application>
</asmv1:assembly>

The True/PM value for the dpiAware property tells Windows that the application is per-monitor DPI aware and DPI scaling should be disabled completely.

Add the following line to your projects .lpr file:

{$R highDPI.rc}

It will compile the resource file using windres and include it in your project.

STEP 2 - Scale Forms and Controls when the Form is Moved to a Different Monitor

Scaling works as described before, however you also have to react to monitor changes. This is done by listening for the <codeWM_DPICHANGED message, which is sent to a window when the effective dots per inch has changed, i.e. it was moved to another monitor (see [3] for details).

To receive window messages declare the global variable var PrevWndProc: WNDPROC; and add the following code to the FormCreate procedure of your form (see also Win32/64 Interface#Processing non-user messages in your window):

PrevWndProc := Windows.WNDPROC(SetWindowLongPtr(Self.Handle,GWL_WNDPROC,PtrInt(@WndCallback)));

The procedure WndCallback is the callback that should be executed when a message is recevied and should look as follows:

function WndCallback(Ahwnd: HWND; uMsg: UINT; wParam: WParam; lParam: LParam):LRESULT; stdcall;
var
  windowPos: TRect;
begin
  if uMsg=WM_DPICHANGED then begin
    // https://msdn.microsoft.com/de-de/library/windows/desktop/dn312083.aspx
    //   wParam: hiword/loword contain new x/y DPI
    //   lparam: pointer to RECT structure with suggested window dimensions
    ScaleDPI(Form1, Form1.currDPI, lo(wParam), hi(wParam));
    Form1.currDPI := lo(wParam);
    windowPos := TRect(Pointer(lParam)^);
    Form1.Top := windowPos.Top;
    Form1.Left := windowPos.Left;
    result := 0;
    exit;
  end;

  result := CallWindowProc(PrevWndProc, Ahwnd, uMsg, WParam, LParam);
end;

Whenever the WM_DPICHANGED message is received your form will be rescaled and repositioned to match the new DPI value. Note that Form1.currDPI should be declared as a public integer variable of your form ("Form1" in this example) and should be initially set using currDPI := Screen.PixelsPerInch; from within the FormCreate procedure.

Notes
  • Font sizes might be a bit tricky. Either a) specifically set all font sizes an make sure the ScaleDPI procedure adjusts them all or b) (method preferred by the author) set all controls to use the parents font size and adapt the ScaleDPI procedure to only adjust the font size of TForms.
  • Title bars won't be rescaled when moving the window to a different monitor. This seems to be a known issue (see [4],[5]) with no easy solution.

External Links