Multi-monitor DPI Awareness for MFC on Windows 10

2020-12-14 22:51:45 Aulddays

Windows started limited support of different DPI settings on multi-monitors ever since Windows 8. And the functionality was then enhanced on Windows 10 1703. As a result, if you have an external motinor, you may set it to be on a DPI scale different from the main monitor, say 150% or 200% for a 4K monitor, so that windows on this monitor do not seem too small. Many applications support high DPI scale now, however, they are not automatically multi-monitor dpi aware. In fact, most applications (even those already with high DPI support) would only look well on the "main" display. If moved to a secondary display, the text and images would propably look quite blurry. It must be also Per-monitor DPI aware to handle this.

Blurry text on multi monitor, even for high dpi aware applications
Blurry text on external monitor,
even for high dpi aware applications

AulddaysDpiHelper is a utility to help MFC applications to better support Per-Monitor DPI Awareness. The utility:

  1. Brings support for Per-monitor DPI awareness V2
  2. Is easy to integrate
  3. Manages and updates the global settings so that window elements render properly on multi-monitors in most cases
  4. Exposes advanced API for fine-grained or complicated requirements
  5. Built with backward compatibility. The demo app even runs smoothly on WinXP if compiled by proper versions of toolsets.

EASY API

The EASY API provides an easy way to integrate to an existing app and works in most cases. Follow these steps:

Get the code

Goto AulddaysDpiHelper's github repository and get a latest copy.

Add files

Add these three files under the AulddaysDpiHelper dir to your project:

AulddaysDpiHelper.h, AulddaysDpiHelper.cpp and AulddaysDpiHelper.manifest

Project setting

Goto project Perperties -> Manifest Tool -> Input and Output, set DPI Awareness to None. Yes, set to None, I mean it.

This works because DPI Awareness V2 has been taken good care of in AulddaysDpiHelper.manifest with proper backword compatibility!

* If your application is Dialog Based, it should now work well because Windows properly handles the control scales in dialogs as well as non-client areas of regular windows for DPI Awareness V2 applications. Read on for other window types.

Adapt you code

Add an updateDpi() method to the mainframe class to do the main job (assuming the window has a standard CMFCToolBar, a CMFCMenuBar and a CMFCStatusBar):

void CMainFrame::updateDpi(bool resizeWindow)
{
   // update AFX_GLOBAL_DATA
   AulddaysDpiHelper::updateGlobal(GetSafeHwnd());
 
   // Resize of the main window
   if (resizeWindow)
   {
      WINDOWPLACEMENT wp;
      wp.length = sizeof(wp);
      GetWindowPlacement(&wp);
      double newdpi = AulddaysDpiHelper::getDpi(GetSafeHwnd());
      if (wp.showCmd == SW_SHOWNORMAL && m_curdpi != 0 && newdpi != 0 &&
         m_curdpi != newdpi)
      {
         int diff = (int)((wp.rcNormalPosition.right - wp.rcNormalPosition.left)
            * (newdpi - m_curdpi) / m_curdpi / 2);
         if (diff >= 0 || wp.rcNormalPosition.right - wp.rcNormalPosition.left + diff > 2)
         {
            wp.rcNormalPosition.left -= diff;
            wp.rcNormalPosition.right += diff;
         }
         diff = (int)((wp.rcNormalPosition.bottom - wp.rcNormalPosition.top) *
            (newdpi - m_curdpi) / m_curdpi);
         if (diff >= 0 || wp.rcNormalPosition.bottom - wp.rcNormalPosition.top + diff > 2)
         {
            wp.rcNormalPosition.bottom += diff;
         }
      }
      SetWindowPlacement(&wp);
   }
   m_curdpi = AulddaysDpiHelper::getDpi(GetSafeHwnd());
 
   // update the control bars
   m_wndMenuBar.AdjustLayout();
   m_wndToolBar.updateDpi();
   m_wndStatusBar.AdjustLayout();
}

The original CMFCToolBar is not so DPI aware, and AulddaysToolBar fixes it.

CMFCToolBar m_wndToolBar;
   =>
AulddaysToolBar m_wndToolBar;

And the CMainFrame::updateDpi() should be called in 3 places:

  • At the end of CMainFrame::OnCreate()
  • ON_WM_SETTINGCHANGE() => OnSettingChange()
  • ON_MESSAGE(WM_DPICHANGED, &CMainFrame::OnDpichanged) => OnDpichanged()

Complete code can be found in the demo project in the repository.

Adapt you CView

At this time, the control bars of the application should scale properly. However, you own CView may also need adaption. The way to do this varies a lot depending on how the very CView was implemented. Here are some tips:

Using AFX_GLOBAL_DATA

AulddaysDpiHelper::updateGlobal() called by CMainFrame::updateDpi() updates some of the global metrics data in AFX_GLOBAL_DATA depending on dpi settins of current monitor, which can be accessed by GetGlobalData().

CDemo_SDIView::OnDraw() in the code repository serves an example of this. In that method, a line of text is drawn using a font twice the size of GetGlobalData()->fontRegular (font size of the menu text).

AulddaysDpiHelper::getScale(HWND hwnd)

This functions returns the DPI scale ratio of current monitor. For example, a return value of 1.25 indicates a 125% scale. This can be used to precisely determine the sizes of elements in your CView.

To be continued

In the next part of this article, let's take a deep look on how these things are done.

Also, there is a major drawback of the EASY API: it modifies the global metrics settings, and this will not work well if you have multiple top-most windows among multi monitors. See the next part on the ADVANCED API to help to selve this.

View: Original Article; from dev.aulddays.com