How Can I track the Last DPI which is sent by WM_DPICHANGED in WPF?

MERUN KUMAR MAITY 531 Reputation points
2024-06-23T21:10:17.6933333+00:00

I want to scale my WPF application according to available screen DPI. I know there Per Monitor DPI aware available but that are completely different what I want to achieve. I want to disable scaling for my WPF application even if windows has some scale per monitor or system-wide scale. I understand that if the system scaling is disabled then what scale factor Windows decides to use is the main goal of mine

To scale my WPF application according to initial DPI at the startup time I need to get the current DPI. And The current DPI for a window always equals the last DPI sent by WM_DPICHANGED.

here is my code :

public partial class MainWindow : Window
    {
        private HwndSource hwndSource;   
        public MainWindow()
        {
           InitializeComponent();
        }
        protected override void OnSourceInitialized(EventArgs e)
        {    
            hwndSource = PresentationSource.FromVisual((Visual) sender) as HwndSource;
            hwndSource.AddHook(new HwndSourceHook(WndProc));
        }
        private const int WM_DPICHANGED = 0x02E0;
        private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (msg == WM_DPICHANGED)
            {
                handled = true;
            }
            return IntPtr.Zero;
        }
    }

Have a look on this Code. This is how I disable System scaling. But the down factor is, Initial DPI is not detected when I restart my Application at different screen resolution (DPI). It is not receiving WM_DPICHANGED at the startup and It has no effect on the initial rendering.

I just need to find the last DPI sent by WM_DPICHANGED. If somehow I track that, then I can easily scale my application according to that. So, there no initial DPI problem.

I know that, I talk a lot and the whole question is quite confusing. So, here I make the summery of the question and what I want as answer. I divided this into two part -

  1. How to track the Last DPI which is sent by WM_DPICHANGED?
    1. After getting the last WM_DPICHANGED, want to apply that in my WPF interface scaling so, that the Initial DPI is detected and my Application scale by itself not by the System / OS.

There lot of C# based solution available in Stack overflow but none of them are working and quite old not works in current Windows 11 environment. Please try to add/modify my given Win32 based code. Other pure C# based code solution also welcome if each step is described properly.

Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,709 questions
Windows API - Win32
Windows API - Win32
A core set of Windows application programming interfaces (APIs) for desktop and server applications. Previously known as Win32 API.
2,511 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,616 questions
C++
C++
A high-level, general-purpose programming language, created as an extension of the C programming language, that has object-oriented, generic, and functional features in addition to facilities for low-level memory manipulation.
3,626 questions
0 comments No comments
{count} votes

2 answers

Sort by: Most helpful
  1. Pinaki Ghatak 3,110 Reputation points Microsoft Employee
    2024-06-24T05:43:08.0966667+00:00

    Hello @MERUN KUMAR MAITY

    Let’s break down your requirements and address them step by step.

    Tracking the Last DPI Sent by WM_DPICHANGED:

    • The WM_DPICHANGED message is sent to a window when the DPI of the monitor hosting the window changes. The current DPI for a window always equals the last DPI sent by WM_DPICHANGED.
      • To track the last DPI, you can modify your existing code. Here’s how you can retrieve the monitor-dependent DPI while handling WM_DPICHANGED:
    private const int WM_DPICHANGED = 0x02E0;
    
    // Inside your WndProc method:
    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        if (msg == WM_DPICHANGED)
        {
            // Get the monitor handle
            IntPtr hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONULL);
            MONITORINFOEX mix = new MONITORINFOEX();
            mix.cbSize = Marshal.SizeOf(mix);
            GetMonitorInfo(hMonitor, ref mix);
    
            // Get the DPI from the monitor
            DEVMODE dm = new DEVMODE();
            dm.dmSize = (short)Marshal.SizeOf(dm);
            EnumDisplaySettings(mix.szDevice, ENUM_CURRENT_SETTINGS, ref dm);
    
            // Now 'dm.dmPelsWidth' and 'dm.dmPelsHeight' contain the DPI
            // You can use this information for scaling your application.
            // ...
            
            handled = true;
        }
        return IntPtr.Zero;
    }
    
    • This modification will allow you to retrieve the DPI when WM_DPICHANGED is received.

    Applying the Last DPI for WPF Interface Scaling:

    • Once you have the DPI value, you can use it to scale your WPF application. You can adjust your UI elements based on this DPI value.
    • For example, you can set the LayoutTransform property of your UI elements to scale them proportionally based on the DPI.
      • Remember that you’ll need to handle this scaling logic in your WPF controls or views.

    Windows 11 Environment:

    • While some older solutions may not work in Windows 11, the approach I provided above should be compatible with Windows 11 as well.
      • Make sure to test your application thoroughly to ensure it behaves as expected on both Windows 10 and Windows 11.

    Feel free to integrate the modified code snippet into your application


    I hope that this response has addressed your query and helped you overcome your challenges. If so, please mark this response as Answered. This will not only acknowledge our efforts, but also assist other community members who may be looking for similar solutions.


  2. Hongrui Yu-MSFT 945 Reputation points Microsoft Vendor
    2024-06-24T09:48:42.72+00:00

    Hi,@MERUN KUMAR MAITY. Welcome to Microsoft Q&A. 

    The WM_DPICHANGED message will not have DPI information when a window is newly created.

    The WM_DPICHANGED message is sent to a window when the DPI setting for a window changes, typically as a result of the user moving the window to a monitor with a different DPI setting or changing the DPI setting on the current monitor.

     

    So for the initial DPI acquisition, choose GetDpiForWindow and GetDeviceCaps

    1.GetDpiForWindow is used for modern Windows versions.

    2.For older Windows versions, GetDeviceCaps retrieves the DPI.

     

    You can refer to the following code to achieve your desired effect

     

    
        public partial class MainWindow : Window
    
        {
    
            private HwndSource hwndSource;
    
            // Win32 constants
    
            private const int WM_DPICHANGED = 0x02E0;
    
     
    
            // Importing Win32 functions   
    
            [DllImport("user32.dll")]
    
            private static extern int GetDpiForWindow(IntPtr hwnd);
    
     
    
            [DllImport("user32.dll")]
    
            private static extern IntPtr GetDC(IntPtr hwnd);
    
     
    
            [DllImport("gdi32.dll")]
    
            private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
    
     
    
            [DllImport("user32.dll")]
    
            private static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);
    
     
    
            private const int LOGPIXELSX = 88;
    
     
    
            public MainWindow() { InitializeComponent(); }
    
     
    
            protected override void OnSourceInitialized(EventArgs e)
    
            {
    
                base.OnSourceInitialized(e);
    
     
    
                // Hook into the window message processing
    
                hwndSource = PresentationSource.FromVisual(this) as HwndSource;
    
                if (hwndSource != null)
    
                { hwndSource.AddHook(WndProc); }
    
                // Get the initial DPI and scale the UI
    
                IntPtr hwnd = new WindowInteropHelper(this).Handle;
    
                int dpi = GetDpiForWindow(hwnd);
    
                if (dpi == 0) // Fallback for older Windows versions
    
                {
    
                    IntPtr hdc = GetDC(IntPtr.Zero);
    
                    dpi = GetDeviceCaps(hdc, LOGPIXELSX);
    
                    ReleaseDC(IntPtr.Zero, hdc);
    
                }
    
                ApplyDpiScaling(dpi);
    
            }
    
     
    
     
    
     
    
            private void ApplyDpiScaling(int dpi)
    
            {
    
                double scalingFactor = dpi / 96.0; // 96 is the default DPI
    
                this.Width = this.Width * scalingFactor;
    
                this.Height = this.Height * scalingFactor;
    
                MyButton.LayoutTransform = new ScaleTransform(scalingFactor, scalingFactor);
    
     
    
            }
    
     
    
            private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    
            {
    
                if (msg == WM_DPICHANGED)
    
                {
    
                    int newDpi = wParam.ToInt32() & 0xFFFF; // LOWORD of wParam
    
                    ApplyDpiScaling(newDpi);
    
                    handled = true;
    
                }
    
                return IntPtr.Zero;
    
            }
    
        }
    
    

     

    
        <Grid>
    
            <Button  Content="HAHA" x:Name="MyButton" Width="100" Height="100"></Button>
    
        </Grid>
    
    

    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.