I'm having a bit of a problem with a very complicated WinForms application written in C#. I want the application to let Windows auto-scale when the DPI is changed but I still need to hook the WM_DPICHANGED event in order to scale some custom drawn text.
The dilemma is that if I leave the application DPI unaware the WM_DPICHANGED message is never intercepted in the DefWndProc and the proper DPI scale can never be retrieved but the form "auto-scales" the way I want. But if I make the application DPI Aware then the WM_DPICHANGED message is intercepted and the proper DPI can be calculated but the form will not "auto-scale".
As I said the application is very complex and uses a lot of third-party controls so I am unable to take the time to re-write the app in WPF or try and scale the application myself.
How can I get the app to intercept the WM_DPICHANGED message, calculate the proper DPI and still allow Windows to manage the form scaling?
Program.cs:
static class Program
{
[STAThread]
static void Main()
{
if (Environment.OSVersion.Version.Major >= 6)
{
// If the line below is commented out then the app is no longer DPI aware and the
// WM_DPICHANGED event will never fire in the DefWndProc of the form
int retValue = SetProcessDpiAwareness(ProcessDPIAwareness.ProcessPerMonitorDPIAware);
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
private enum ProcessDPIAwareness
{
ProcessDPIUnaware = 0,
ProcessSystemDPIAware = 1,
ProcessPerMonitorDPIAware = 2
}
[DllImport("shcore.dll")]
private static extern int SetProcessDpiAwareness(ProcessDPIAwareness value);
}
Form1.cs:
protected override void DefWndProc(ref Message m)
{
switch (m.Msg)
{
case 0x02E0: //WM_DPICHANGED
{
int newDpi = m.WParam.ToInt32() & 0xFFFF;
float scaleFactor = (float)newDpi / (float)96;
}
break;
}
base.DefWndProc(ref m);
}
UPDATE: I am using Windows 10 with multiple monitor setup. All monitor are the same model with a base resolution of 1920x1080. I set one of my monitors to be at 125% of the size using the display settings.
Instead of capturing the WM_DPICHANGED
event, what about just asking the current DPI settings whenever you need it (in Paint
events or whatever)?
This is also not obvious, though. If you search StackOverflow, usually you can find the following answer:
using (Graphics screen = Graphics.FromHwnd(IntPtr.Zero))
{
IntPtr hdc = screen.GetHdc();
int dpiX = GetDeviceCaps(hdc, DeviceCaps.LOGPIXELSX);
screen.ReleaseHdc(hdc);
}
However, it will return always 96, regardless of actual DPI settings, unless...
It seems that the GetDeviceCaps function is not fully documented at MSDN. At least I discovered that pinvoke.net mentions a few further options that can be obtained by the function. At the end I came out with the following solution:
public static int GetSystemDpi()
{
using (Graphics screen = Graphics.FromHwnd(IntPtr.Zero))
{
IntPtr hdc = screen.GetHdc();
int virtualWidth = GetDeviceCaps(hdc, DeviceCaps.HORZRES);
int physicalWidth = GetDeviceCaps(hdc, DeviceCaps.DESKTOPHORZRES);
screen.ReleaseHdc(hdc);
return (int)(96f * physicalWidth / virtualWidth);
}
}
And the required additional code in the examples above:
private enum DeviceCaps
{
/// <summary>
/// Logical pixels inch in X
/// </summary>
LOGPIXELSX = 88,
/// <summary>
/// Horizontal width in pixels
/// </summary>
HORZRES = 8,
/// <summary>
/// Horizontal width of entire desktop in pixels
/// </summary>
DESKTOPHORZRES = 118
}
/// <summary>
/// Retrieves device-specific information for the specified device.
/// </summary>
/// <param name="hdc">A handle to the DC.</param>
/// <param name="nIndex">The item to be returned.</param>
[DllImport("gdi32.dll")]
private static extern int GetDeviceCaps(IntPtr hdc, DeviceCaps nIndex);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With