Skip to main content

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:

  1. Customize the Windows application manifest;

  2. Inherit all application forms from DevExpress DPI-aware forms;

  3. Prepare and dynamically switch several UI icon lists designed for different monitor DPI values (you can also use vector images as icons);

  4. 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;