June 12, 2018
by Tibor Lorántfy
modified at December 12, 2019

HDPI support using DG module

macOS HDPI Environment

So called “Retina” display gives users ultra-sharp text and graphics without making the interface too small to see. To achive this the system renders the user interface with four times as many pixels (twice the vertical and twice the horizontal resolutions).

ARCHICAD supports perfect high resolution display on macOS platform since version 20. Since then the preferred source image format of icon resources is vector graphic (SVG) and that allows that the system can scale the whole user interface without making it blurry.

To ensure that your AddOn’s user interface supports HDPI resolution on macOS plaform, just use SVG icon resources. For more details about SVG icons visit my previous post: ARCHICAD GUI Icon Style Guide

Windows HDPI Environment

On HDPI displays running legacy applications without scaling will result in UI elements being too small. To take advantage of high resolution displays applications need to be scaled. Scaling can be performed either by the operating system or by the application.

In the past Microsoft, usually made legacy applications available to run on the newest Windows systems. For these applications Windows performs the scaling. This resulted in admissible sized UI elements with one noticeable drawback: blurry look.

To help achieve a good looking UI a new HDPI API has been provided by Microsoft. This allows developers to control how their new applications resize their UI. This API was introduced with Windows 8.1, and it has been continuously improved in the Windows 10 updates.

HDPI Features in Windows

Windows 8.1 added developer support that enables desktop applications to not only become aware of different monitor DPI settings, but to also respond to any dynamic DPI changes.

When the DPI settings of the operating system are changed, the system fonts and system UI elements change in size. This is the primary reason that applications need to consider the system DPI setting in their rendering and layout code. Applications that are not DPI–aware can potentially exhibit some visual artefacts such as mismatched font sizes, clipped text, or clipped UI elements.

From Windows XP to Windows 8, Windows operated on a system-wide DPI.
Windows 8.1 introduced per-monitor DPI. On logon, Windows selects the optimal DPI for each monitor of the system. Users can still override these DPI values in Control Panel.
From Windows 8.1, desktop applications fall into three categories with respect to DPI:

  • DPI–unaware applications
    This class of applications are unaware of different system DPIs. The Desktop Window Manager (DWM) virtualizes and scales these applications to account for high DPI.
    Windows Vista introduced a feature called DPI virtualization, which provides a level of automatic scaling support to applications that are not DPI–aware. With this feature, Windows scales the size of the text and UI elements of applications that are not DPI-aware so that they are appropriately sized on high DPI settings without changes in the application.
    In Windows Vista through Windows 8, this feature provides “virtualized” system metrics and UI elements to DPI–unaware applications, as if they are running at 96 DPI. The application then renders to a 96 DPI off-screen surface and DWM scales the resulting application window to match the DPI setting. For example, if the DPI display setting is 144, DWM scales the application’s window by 150%, or 144/96.
    In Windows Vista and later versions, when DPI virtualization is enabled, applications that are not DPI-aware are scaled, and applications receive virtualized data from the system APIs, such as the GetSystemMetrics function.
  • System–DPI aware applications
    These applications render at the system DPI to avoid being scaled. They cannot respond to dynamic changes in DPI during a single session. System–DPI aware applications render optimally on the primary display, and DWM does not scale and virtualize them. However, if the user moves the application to a display with a higher or lower DPI, DWM scales it up or down. The effect is that the window and content size are appropriate for every display, but the scaling introduces blurriness.
  • Per-monitor–DPI aware applications
    Per monitor–DPI aware applications are a new class of applications in Windows 8.1. These applications dynamically scale up or down when a user changes the DPI or moves the application between monitors that have different DPIs. For this kind of applications Windows sends notification to dialogs and windows when they are moved between displays with different scale value. The application then responds to this notification by resizing windows and controls.

DPI awareness can be declared through the application manifest file/resource or by calling the SetProcessDpiAwareness function.

Legacy Applications

ARCHICAD versions before version 22 run in the DPI-unaware mode. By default, the UI of these applications are rendered on a 96 DPI offscreen and then scaled/stretched up to the appropriate size corresponding the display DPI resulting a more or less blurry image.

On large size displays the blurry appearance can be disturbing. If sharp look of UI is important and small size is acceptable for the user, then the scaling might be switched off in the Compatibility settings panel on the Application Properties dialog.

Windows Application Properties

HDPI Applications

ARCHICAD 22 is set to Per-monitor–DPI aware mode. This means that all scaling operations are performed by the application and the operating system does not do UI stretching. To achieve the best appearance the Override HDPI scaling checkbox should be turned off (this is the default setting) for these applications.

Overriding Scaling Mode

Although changing the default scaling mode for DPI-aware applications is not reasonable, legacy applications may take advantage of modifying these settings. Previous (DPI-Unaware) ARCHICAD versions can benefit only by selecting the Application mode and this way disabling the UI stretching.

  • Application
    This forces the process to run in per-monitor DPI awareness mode. Effectively it disables display scaling. In this mode the legacy applications appear without UI stretching.
  • System
    This mode should be used with System-Aware applications.
  • System (Enhanced)
    GDI Scaling

For detailed explanation visit the following link:
https://blogs.windows.com/buildingapps/2017/04/04/high-dpi-scaling-improvements-desktop-applications-windows-10-creators-update

Monitor Aware Scaling

DPI-aware applications resize their UI according to the scale settings of the monitor they are displayed on. Windows API provides functions to query the scaling settings of the attached displays. On multiple monitor environments the operating system sends messages to the windows when they are moved between displays with different scale settings.

HDPI in DG module based Add-Ons

Starting with the DG module, applications run in Per-Monitor DPI Aware mode. Scaling of ARCHICAD dialogs and windows are performed inside the DG module. When a scale change is applied to a dialog the size of all controls, fonts, drawings are updated by the DG module. This allows most of the dialog code outside the module to remain unchanged, because there is nothing to do on the application side in order to response to a scale change event.

Those dialogs drawing either on user item surfaces, owner drawn controls or handling mouse movements may have to deal with scaling.

Native and Logical Coordinate Systems

In order to keep most of the existing DG dialogs and windows unchanged a ‘logical’ coordinate system was defined (which equals with the pixel coordinate system on a 100% scaled display). DG dialogs and dialog items work in this logical coordinate system. The Dialog Manager maps the logical coordinates and sizes to screen pixel coordinates. This way most of the dialogs (except some special cases) do not need to know about the scaling factor of the display. If a dialog was dragged to a different scale display the control sizes and positions are unchanged in the logical coordinate space, but they are changed in the screen coordinate space according to the display scaling factor. In the previous AC versions, the dialog and control coordinates and sizes in the grc resource files were defined in pixels, now these values can be thought as ‘logical’ display units (which are equal to pixels when displays scales are set to 100%).

Although introducing the logical coordinate system proved to be a good idea it has some drawbacks, because there are some parts and properties of UI elements which cannot be correctly measured in logical units. Typically, these are the dialog frame sizes, dialog position, and dialog non-client measures. The client area of dialogs is the work area in the dialog that the controls can use or occupy. The non-client area is the dialog caption and the dialog frame. Some controls also have non-client area, for example the internal scrollbars or the header area of a list box control, or a tab area of a tab control are on the non-client area of these controls. The size of these elements is determined by Windows and cannot be modified by the application. Nevertheless the operating system does not modify the sizes of these non-client area elements with scale change. This can be seen on the following picture which shows the same dialog captured in three displays with three different scales.

Windows Scaled Dialogs

The client area expressed in logical units is scale independent: 315×180 units as it is defined in the grc file.
The client area expressed in pixels is different as the dialog size is changed by DG:

  • 100% → 315×180 pixels
  • 125% → 394×225 pixels (the horizontal size is rounded up, it would be 393.75 pixels).
  • 150% → 473×270 pixels (the horizontal size is rounded up here also from 472.5 pixels).

The dialog border sizes and caption height are unchanged in all the three cases. This means that mapping the frame size from the pixel space to logical space would result different logical values on different scales which is undesirable. Likewise converting the fixed 1-pixel dialog border to logical units is problematic because it resulted less than 1.0 unit which can hardly be stored in an integer coordinate value.
For these reasons DG provide different units and function set to manipulate client and frame coordinates. In the previous versions of DG, all of the coordinates were specified in short variables. Now the short variables are used for logical units only (dialog client size, control sizes, control positions, font sizes, etc.). Dialog frame sizes and dialog positions are stored in a new DG::NativeUnit object. Converting logical coordinates and sizes to native units (pixels, screen pixel positions) and back can be performed only by applying the scaling factor. The scaling factor of a dialog can be requested from the DG::Panel.

Class DG::Panel
{
    double  Panel::GetScaleFactor (void) const;
};

The dialog and control accessor functions working in logical coordinate space use short coordinate values, while functions working in screen space use DG::NativeUnit coordinates.

Class DG::Dialog
{
    // The following member functions of the DG::Dialog work with logical units, and use short values: void Move (short hDisp, short vDisp); void Resize (short hGrow, short vGrow, FixPoint fixPoint = TopLeft, bool keepOld = false); void SetClientSize (short width, short height, FixPoint fixPoint = TopLeft, bool keepOld = false); void SetClientWidth (short width, FixPoint fixPoint = TopLeft, bool keepOld = false); void SetClientHeight (short height, FixPoint fixPoint = TopLeft, bool keepOld = false); short GetClientWidth (void) const; short GetClientHeight (void) const; // These member functions work in the native (pixel) space: void Move (const DG::NativeUnit& hDisp, const DG::NativeUnit& vDisp); void SetClientPosition (const NativePoint& pt); void SetClientPosition (const NativeUnit& hPos, const NativeUnit& vPos); void SetClientRect (const NativeRect& rect, FixPoint fixPoint = TopLeft, bool keepOld = false); NativePoint GetClientPosition (void) const; NativeRect GetClientRect (void) const; void SetFramePosition (const NativePoint& pt); void SetFramePosition (const NativeUnit& hPos, const NativeUnit& vPos); void SetFrameRect (const NativeRect& rect, FixPoint fixPoint = TopLeft, bool keepOld = false); void SetFrameSize (const NativeUnit& width, const NativeUnit& height, FixPoint fixPoint = TopLeft, bool keepOld = false); void SetFrameWidth (const NativeUnit& width, FixPoint fixPoint = TopLeft, bool keepOld = false); void SetFrameHeight (const NativeUnit& height, FixPoint fixPoint = TopLeft, bool keepOld = false); NativePoint GetFramePosition (void) const; NativeRect GetFrameRect (void) const; NativeUnit GetFrameWidth (void) const; NativeUnit GetFrameHeight (void) const; }; 

As it can be seen the frame sizes and coordinates cannot be retrieved in logical units, but client size can be can be retrieved either in logical or native units.

Using DG::NativeUnits

In general, there is no need to use the native units while implementing DG dialog boxes. Although there are some exceptions:

  • Opening a context menu somewhere on a screen or above dialogs or controls.
  • Placing a popup dialog aligned to a control or the mouse coordinate.
  • Storing a window/dialog position.

The native (screen origin relative pixel) position of a control can be calculated by adding the dialog client position with the scaled control position. DG can retrieve control coordinates in screen origin relative native units by calling the following function:

    DG::NativeRect     Item::GetNativeRectInScreenSpace (void) const;

Aligning of a dialog to a specified coordinate or control can be done by calling one of following functions:

void    DG::Utils::PlaceDialogNextToNativeRect (DG::Dialog& dialog, const DG::NativeRect& nativeRect, Alignment alignment);
void    DG::Utils::PlaceDialogNextToItem (DG::Dialog& dialog, const DG::Item& item, Alignment alignment);
void    DG::Utils::PlaceDialogNextToItem (DG::Dialog& dialog, short dialogId, short itemId, Alignment alignment);
void    DG::Utils::PlaceDialogNextToItems (DG::Dialog& dialog, const DG::Item& leftItem, const DG::Item& rightItem);
void    DG::Utils::PlaceDialogToMousePos (Dialog& dialog, const DG::NativeUnit& xOffset, const DG::NativeUnit& yOffset, Alignment alignment);

Mouse coordinates can now be retrieved in native and logical units also:

class DG_DLL_EXPORT MousePosData
{
    Point          GetMouseOffsetInLogicalUnits (void) const;
    NativePoint    GetMouseOffsetInNativeUnits (void) const;
};

Unresolved Issues

Most of the issues are experienced on a multiple monitor environment. Some of them are listed below:

  • Some controls are displayed with correct size on the primary display only. The main reason of these problems is that Windows sets the size of some UI elements according to the primary display scale regardless of which display is the control located on. Unfortunately, there is no way to set the correct size of these elements.
    • Dialog title/caption size, nonclient area of windows
    • Radio button and checkbox image
    • Embedded scrollbar of list box, list view and tree view controls
  • System (file, print) dialogs are opened in System Aware mode and look blurry on the secondary monitor.
  • Controls managed by windows cannot be freely resized and tweaked. The toolbar is a good example for this. Docked toolbars may lose snapped state if the application window is moved to a display with different scale.
  • The DGListView control had to be rewritten in ARCHICAD 22 and now it has some limitations we did not have in the past. In the rewritten control the list view items have some border that can neither be turned off nor changed in size.
  • Coordinate rounding problems. Depending on the scaling factor some coordinates in the pixel space do not have a corresponding logical coordinate value. This makes annoying artefacts on some UI elements.
  • There are some controls having borders, scrollbars and parts that have fixed and scale independent size in the pixel space. Unlike the other controls the client area size of these controls do change in the logical coordinate space with a scale change event and sometimes it cannot be correctly mapped to logical units on > 100% scales. A good example for this is the list box control with a vertical scroll bar. The tab fields of these controls should be recalculated after scale change. The dialog is notified about the scale change event by the PanelObserver::PanelScaleChanged function.

Dos and don’ts

  • Keep the number of open icons in the memory to a minimum. Windows allows maximum number of 10000 for GDI objects. Each icon increases this counter by 3. In ARCHICAD 22 there was an image cache implemented which made available to drastically decrease the count of open images, but there are dialogs still collecting and keeping icons for long time. For more information visit this link:
    https://msdn.microsoft.com/en-us/library/windows/desktop/ms724291(v=vs.85).aspx
  • Add some extra space to the list view control to allow space for list view items with the borders to fit.
  • Always sign out from Windows after changing the scale factor of the main display. Failing to do this may result in ugly and mis-sized UI elements.
  • Avoid using native operating system drawing functions. Use the NativeContext drawing API instead. Some drawing functions use the scaling factor which can be requested from dialogs or from the drawing notification event.
  • Dynamically generated icons should be regenerated after a scale change event.
  • The layout of multiple column list controls with vertical scrollbars need to be recalculated after scale change.
  • Owner drawn controls may need to take account of the current scale.