Resolving Window Border Glitches During MFC DPI Changes
When developing Windows Desktop applications with C++ and the MFC Ribbon framework (CMFCRibbonBar), dynamic high-DPI scaling can introduce layout anomalies. A specific issue occurs when moving an application window from a high-DPI display (e.g., 200%) to a lower-DPI display (e.g., 150% or 100%): a transparent or unpainted border appears around the window frames.

Observed Behavior:
- Initial Start: If the application is launched directly on the lower-DPI monitor, the window dimensions and layout are computed correctly.
- Dynamic Downgrade: The layout anomaly occurs only during a live DPI reduction at runtime.
- Standard Methods: Attempting to force layout updates (
RecalcLayout,UpdateWindow, or invoking custom scaling adjustments) inside theWM_DPI_CHANGEDhandler does not resolve the issue.
Root Cause Analysis
During a dynamic DPI change, the operating system resizes the top-level window. The MFC Ribbon framework modifies the standard window layout by hooking into the Non-Client Area (NC) calculation to implement custom title bars and visual themes.
When downgrading the DPI, a timing mismatch occurs. Windows scales down the outer window bounds, but MFC’s internal metric caches temporarily retain the larger frame dimensions from the previous DPI tier. When Windows sends the WM_NCCALCSIZE message to determine the size of the client area, MFC applies outdated padding values, shifting the client area boundaries inward. Because the outer window has already shrunk, this geometric discrepancy results in an unpainted, transparent margin.
Since this calculations happen inside CFrameWndEx::OnNcCalcSize, adjustments within the standard OnDpiChanged function are ineffective. The layout metrics must be corrected where the window boundaries are calculated.
Technical Solution: Intercepting WM_NCCALCSIZE
The most reliable approach is to utilize the native Windows default window procedure (DefWindowProc) as a reference point. By passing the message to the OS first, we obtain the mathematically correct border padding for the current DPI tier. We then overwrite the incorrect side-boundary metrics calculated by MFC.
By leaving the top boundary (lpncsp->rgrc.top) unaltered, the MFC Ribbon retains full control over the custom title bar height, while the left, right, and bottom edges are aligned with the native Windows framework.
Step 1: Add the handler to your header (MainFrm.h)
// In your CMainFrame class definition
afx_msg void OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS FAR* lpncsp);
Step 2: Register the message in your implementation (MainFrm.cpp)
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWndEx)
// ... your other message maps
ON_WM_NCCALCSIZE()
END_MESSAGE_MAP()
Step 3: Implementation
void CMainFrame::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS FAR* lpncsp)
{
if (bCalcValidRects && lpncsp != NULL)
{
// 1. Backup the original outer window dimensions
RECT rcDefaultWindow = lpncsp->rgrc;
// 2. Query the native Windows API for correct DPI-scaled boundaries
::DefWindowProc(m_hWnd, WM_NCCALCSIZE, bCalcValidRects, reinterpret_cast<LPARAM>(lpncsp));
RECT rcCorrectClient = lpncsp->rgrc;
// 3. Restore the original state before invoking MFC
lpncsp->rgrc = rcDefaultWindow;
// 4. Invoke the MFC base class for standard Ribbon setup
CFrameWndEx::OnNcCalcSize(bCalcValidRects, lpncsp);
// 5. Override the incorrect left, right, and bottom padding metrics.
// The top value remains managed by MFC for proper title bar height.
lpncsp->rgrc.left = rcCorrectClient.left;
lpncsp->rgrc.right = rcCorrectClient.right;
lpncsp->rgrc.bottom = rcCorrectClient.bottom;
return;
}
// Fallback path for invalid parameters
CFrameWndEx::OnNcCalcSize(bCalcValidRects, lpncsp);
}
Startup and Runtime Performance Impact
Integrating DefWindowProc calls into the window message structure raises potential concerns regarding application initialization speed and performance optimization.
However, this implementation introduces no measurable overhead:
- Algorithmic Execution: Processing
WM_NCCALCSIZEviaDefWindowProcinvolves pure bounding-box arithmetic in memory. It does not trigger disk I/O, graphics rendering pipelines, or CPU context switches. - Low Frequency: The message only fires during structural window changes (creation, maximizing, or dynamic DPI mutations). It does not execute during continuous operations or inside the hot rendering loop (
WM_PAINT), leaving the active runtime performance intact.
By enforcing the operating system metrics for the window boundaries, the outdated MFC caching behavior is bypassed, ensuring correct window dimensions across varying display scaling factors.