Sign in Join
Wiki copytemporary

Cocoa DPI: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11*(macOS only)*
22
33# Cocoa DPI
44
55The native OS DPI for Cocoa is 72. The LCL-Cocoa binding uses the value 96 instead. This is because other widget sets (Windows, Gtk2, Qt) use 96 DPI for 100% scaling as well. As a result font sizes scale equally across all LCL widget sets including Cocoa and also the High-DPI system works correctly.
66
77On the other hand, there is the disadvantage that fonts of the same size in a LCL application appear bigger by the ratio 96/72=4/3 than in a native Cocoa application.
88
99## High-DPI on Cocoa (Retina)
1010
1111High-DPI on LCL-Cocoa is achieved with TControl.GetCanvasScaleFactor ([backingScaleFactor](https://developer.apple.com/documentation/appkit/nswindow/1419459-backingscalefactor)) function result, unlike on LCL-win32 where the DPI/PixelsPerInch value is increased and TControl.GetCanvasScaleFactor is 1. On Retina, TControl.GetCanvasScaleFactor returns 2.
1212
1313If a cross-platform application should support pixel-precise drawing on Windows&Mac, it has to support the TControl.GetCanvasScaleFactor function result. All bitmap images must be drawn to a control's canvas with StretchDraw() and they need to be in TControl.GetCanvasScaleFactor-times higher resolution (for Retina a 2-times higher resolution) than the target rect in TCanvas. See below for sample code.
1414
1515The TImageList component can be taken as an example of a component that fully supports High-DPI on all OS/WS in the LCL, including Cocoa+Retina.
1616
1717## Remedy for the difference between LCL-Win32 High-DPI and LCL-Cocoa Retina
1818
1919As described above, High-DPI is achieved with the increase of TControl.GetCanvasScaleFactor instead of the increase of TFont.PixelsPerInch.
2020
2121As a result, a (physical) 1px line cannot be painted directly on Cocoa control's canvas. A 1px (LCL) line will appear as 2px (physical Retina).
2222
2323In order to draw a 1px line (or any other pixel-precise graphics) on physical Retina display, you have to create a TBitmap with TControl.GetCanvasScaleFactor-times higher size and then paint this bitmap on the control's canvas with StretchDraw().
2424
2525Here is a sample code:
2626
27-`procedure TForm1.FormPaint(Sender: TObject);`\
28-`var`\
29-`  B: TBitmap;`\
30-`  F: Double;`\
31-`begin`\
32-`  F := Self.GetCanvasScaleFactor;`\
33-`  B := TBitmap.Create;`\
34-`  try`\
35-`    B.SetSize(Round(100*F), Round(100*F));`\
36-`    B.Canvas.Brush.Color := clWhite;`\
37-`    B.Canvas.FillRect(B.Canvas.ClipRect);`\
38-`    B.Canvas.Font.PixelsPerInch := Round(Self.PixelsPerInch*F);`\
39-`    B.Canvas.Font.Size := 10;`\
40-`    // now do your drawing`\
41-`    B.Canvas.Pen.Color := clRed;`\
42-`    B.Canvas.MoveTo(0, 0);`\
43-`    B.Canvas.LineTo(B.Width, B.Height);`\
44-`    B.Canvas.Font.Color := clBlue;`\
45-`    B.Canvas.TextOut(Round(10*F), Round(10*F), 'Hello');`\
46-`    Self.Canvas.StretchDraw(Rect(0, 0, 100, 100), B);`\
47-`  finally`\
48-`    B.Free;`\
49-`  end;`\
50-`end;`
27+``` pascal
28+procedure TForm1.FormPaint(Sender: TObject);
29+var
30+ B: TBitmap;
31+ F: Double;
32+begin
33+ F := Self.GetCanvasScaleFactor;
34+ B := TBitmap.Create;
35+ try
36+ B.SetSize(Round(100*F), Round(100*F));
37+ B.Canvas.Brush.Color := clWhite;
38+ B.Canvas.FillRect(B.Canvas.ClipRect);
39+ B.Canvas.Font.PixelsPerInch := Round(Self.PixelsPerInch*F);
40+ B.Canvas.Font.Size := 10;
41+ // now do your drawing
42+ B.Canvas.Pen.Color := clRed;
43+ B.Canvas.MoveTo(0, 0);
44+ B.Canvas.LineTo(B.Width, B.Height);
45+ B.Canvas.Font.Color := clBlue;
46+ B.Canvas.TextOut(Round(10*F), Round(10*F), 'Hello');
47+ Self.Canvas.StretchDraw(Rect(0, 0, 100, 100), B);
48+ finally
49+ B.Free;
50+ end;
51+end;
52+```
5153
5254## Possible remedies for the difference between LCL 96 DPI and native Cocoa 72 DPI
5355
5456### For a specific control
5557
5658If you want to use the OS native font sizes for some specific control (e.g. TMemo with editing text where the user is able to select the font size from a combo box and you want the TMemo text size be the same height as in a native Cocoa application - e.g. TextView), just set the Font.PixelsPerInch accordingly:
5759
58-`Memo1.Font.PixelsPerInch := 72;`
60+``` pascal
61+Memo1.Font.PixelsPerInch := 72;
62+```
5963
6064### For the whole application
6165
6266If you want your whole application to appear with 72 DPI and you are fine with the disadvantage of different font size from other LCL widgetsets you can set the value of CocoaBasePPI variable to 72 in the initialization of your application. (It is a variable in CocoaInt.pas.)
6367
6468In this case fonts with custom sizes (Font.Size\<\>0) will have the same height like native Cocoa applications but in the same LCL application they will appear smaller on Cocoa than on Windows/Linux.
6569
6670If you write a cross-platform application and you are fine with the above difference and you want to use the High-DPI scaling system of the LCL, you have to follow these 2 steps in addition to setting the CocoaBasePPI value as described above:
6771
68721. Design your forms always at 96 DPI. Be sure your Lazarus IDE runs internally at 96 DPI as well = do not change the CocoaBasePPI value directly by editing CocoaInt.pas.
6973
7074(It is recommended to design your forms at 96 DPI anyway to avoid DPI modifications in LFMs for your VCS.)
7175
72761. Set "Application.Scaled := False;" on Mac. This disables the High-DPI scaling on Mac, thus 72 PPI is used everywhere and controls don't get scaled down. Keep it enabled for other OS/WS to support scaling there. You will need to use compiler conditions in your application's LPR file:
7377
74-`{$IFNDEF LCLCocoa}`\
75-`  Application.Scaled := True;`\
76-`{$ENDIF}`
78+``` pascal
79+{$IFNDEF LCLCocoa}
80+ Application.Scaled := True;
81+{$ENDIF}
82+```
7783
7884## Why LCL-Cocoa uses GetCanvasScaleFactor for High-DPI instead of DPI/PixelsPerInch
7985
8086Principally it is possible to emulate LCL-Win32 High-DPI with PixelsPerInch on LCL-Cocoa. As a result, the GetCanvasScaleFactor will be unnecessary (it will be 1 on LCL-Cocoa) and Font.PixelsPerInch will scale according to the physical scaling.
8187
8288In this case all TControl and TCanvas coordinates must be scaled between the LCL and native Cocoa. This is possible because Cocoa uses floating-point coordinates.
8389
8490The biggest problem and the show-stopper is when more displays are used that have different PPI settings. Because native Cocoa keeps the PPI value constant and changes the backingScaleFactor between the monitors, a form doesn't need to be rescaled when moved from one monitor to another. This is different from Windows where the form keeps the physical resolution between monitors and it has to be rescaled by the LCL when moved to another monitor.
8591
8692If LCL-Cocoa emulated LCL-win32 behavior, the LCL coordinates (Left/Top/Width/Height) of the form and all its controls would get invalid and had to be reassigned when the form is moved to a different monitor. That would result in a lot of Resize/Paint calls and probably also result in the form flashing/blinking until the correct coordinates are all sorted.
8793
8894The current solution (change of GetCanvasScaleFactor value) does not suffer from the above problem.
8995
9096# See Also
9197
9298- <https://www.qt.io/blog/2013/04/25/retina-display-support-for-mac-os-ios-and-x11>
9399
94100---
95101*From the Free Pascal wiki, page "Cocoa DPI", as it stood on 2020-01-29 after a change by Skalogryz, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
96102Category: Cocoa
97103Category: High-DPI
98104Category: macOS

Back to history