How to Create a Per-Monitor DPI-Aware Application
- 7 minutes to read
Per-monitor DPI-aware applications dynamically scale all their visual elements when a user moves an application window between two monitors with different DPIs. The host operating system sends the WM_DPICHANGED message to a DPI-aware application every time the target DPI changes (that is, when an application window is halfway between two monitors with different DPIs, and the application adjusts its UI scale factor.
Use the following steps to add per-monitor DPI awareness to a VCL application:
Customize the Windows application manifest;
Inherit all application forms from DevExpress DPI-aware forms;
Prepare and dynamically switch several UI icon lists designed for different monitor DPI values (you can also use vector images as icons);
Manually scale geometric primitives in all Custom Draw event handlers.
Enabling DPI Awareness in the Application Manifest
The first step in making your VCL application per-monitor DPI-aware includes proper application manifest adjustment. Depending on the RAD Studio version, you can use one of the following options:
- RAD Studio 10 Seattle and newer: Check Enable High-DPI on the Application tab in project settings:
- RAD Studio XE2 versions and newer: Provide the custom application manifest via the project settings:
- All supported versions, including RAD Studio 2010 and XE: Link the manifest included in the dxDPIAwareManifest.res resource file shipped with the Express Cross Platform Library.
Using DPI-Aware Application Forms
The Express Cross Platform Library provides the TdxCustomForm and TdxForm form classes that you can use for creating per-monitor DPI-aware VCL applications instead of the standard TCustomForm and TForm classes, respectively.
All TdxCustomForm and TdxForm descendants automatically handle the WM_DPICHANGED message sent by an operating system, updating their scaling factor in response to any DPI changes. Use one of these classes as a base class for your application forms:
TfrmMain = class(TdxForm)
btnColor: TcxButton;
btnCommandLink: TcxButton;
btnBMPGlyph: TcxButton;
btnSVGGlyph: TcxButton;
end;
All DevExpress VCL controls use the form’s scaling factor to automatically adjust their font sizes as well as the visual element dimensions. All further DPI awareness adaptation steps are not necessary if you are using only per-monitor DPI-aware controls (without custom drawing) and only vector UI icons in your application.
Preparing and Managing UI Icon Lists
You can follow one of the following approaches to avoid UI icon blurring in your application on changing the monitor DPI:
Use a single image list populated with vector (SVG) images that scale without quality loss.
Use a standard VCL TVirtualImageList component (available in RAD Studio 10.3 Rio or newer) populated with icons designed for each target DPI.
Use several image lists that contain UI icon sets designed for a particular target DPI and dynamically switch between these image lists. It is usually sufficient to create four or five image lists to cover the most popular monitor DPI values.
The rest of this section covers the steps required to dynamically switch between several image lists.
For instance, you can create four image lists and populate them with UI icons designed for the following DPI values:
DPI Value | Scaling Factor | Example Icon Bitmap Dimensions, in pixels |
---|---|---|
96 | 100% (unscaled UI) | 16x16 |
120 | 125% | 20x20 |
144 | 150% | 24x24 |
192 | 200% | 32x32 |
Set the SourceDPI property to the target DPI value for each of the image lists to scale the on-screen dimensions of their icons according to the current monitor DPI.
To substitute UI icon sets automatically at runtime, you can either rely on the built-in functionality provided by DPI-aware forms or override the DPI-aware form’s ScaleFactorChanged procedure that is called every time your application receives the WM_DPICHANGED system message.
The built-in UI icon set dynamic swapping functionality is implemented by the DPI-aware form’s UpdateImageLists procedure internally called by ScaleFactorChanged. To use it, you need to name the image lists designed for various high-DPI modes by appending the corresponding target DPI value numbers to the name of the “base” image list containing images designed for unscaled application UI.
You can also implement the dynamic UI icon set substitution by overriding the DPI-aware form’s ScaleFactorChanged procedure that is called every time your application receives the WM_DPICHANGED system message. This procedure chooses the most appropriate icon set depending on the current form DPI that you can calculate by applying a value of 96 to the form’s ScaleFactor property (that is, the DPI value at which no UI scaling occurs):
function TfrmMain.GetMostSuitableImageList: TcxImageList;
var
ATargetDPI: Integer;
begin
ATargetDPI := ScaleFactor.Apply(96); // Calculates the form's DPI
if ATargetDPI >= 192 then
Result := ilImages200 // Chooses the icon set designed for UI elements scaled by 200%
else if ATargetDPI >= 144 then
Result := ilImages150 // Chooses the icon set designed for UI elements scaled by 150%
else if ATargetDPI >=120 then
Result := ilImages125 // Chooses the icon set designed for UI elements scaled by 125%
else
Result := ilImages; // Chooses the icon set designed for unscaled UI elements (100%)
end;
// Overrides the DPI-aware form's ScaleFactorChanged procedure
procedure TfrmMain.ScaleFactorChanged(M, D: Integer);
var
AImageList: TcxImageList;
begin
AImageList := GetMostSuitableImageList; // Chooses the appropriate image list for substitution
btnBMPGlyph.OptionsImage.Images := AImageList; // Substitutes the btnBMPGlyph button's image list
AImageList.GetImage(0, btnCommandLink.OptionsImage.Glyph); // Applies the first image within the chosen list as the btnCommandLink button's glyph
btnCommandLink.OptionsImage.Glyph.SourceDPI := AImageList.SourceDPI; // Updates the glyph's source DPI with a new value
end;
Manual Geometry Scaling in Custom Draw Handlers
This step is required only if you are handling any Custom Draw events provided by DevExpress VCL controls in your application. You need to manually scale all geometry that you paint within Custom Draw event handlers.
The following code example shows how to paint a DPI-aware TcxButton component displaying a red square in the center by handling the button’s OnCustomDraw event:
procedure TfrmMain.btnColorCustomDraw(Sender: TObject; ACanvas: TcxCanvas; AViewInfo: TcxButtonViewInfo; var AHandled: Boolean);
var
AColorBoxOffset, ALineWidth: Integer;
AColorBoxRect: TRect;
begin
// Call the new DrawScaledButton method instead of DrawButton to apply the form's scaling factor to the painted button
AViewInfo.Painter.LookAndFeelPainter.DrawScaledButton(ACanvas, AViewInfo.Bounds, '', AViewInfo.State, ScaleFactor);
ALineWidth := ScaleFactor.Apply(1); // Applies the form's scaling factor to the line width
AColorBoxOffset := ScaleFactor.Apply(6); // Applies the form's scaling factor to the box offset
AColorBoxRect := cxRectInflate(AViewInfo.Bounds, -AColorBoxOffset);
ACanvas.FrameRect(AColorBoxRect, clBlack, ALineWidth); // Draws the red square's outline
AColorBoxRect := cxRectInflate(AColorBoxRect, -ALineWidth); // Shrinks the color-filled square to fit into the painted outline
ACanvas.FillRect(AColorBoxRect, clRed); // Fills the outline with the red color
AHandled := True;
end;