5

I'm going to set up this question and answer it myself, so that others could search and find the correct answer easierly. I had to google for hours and compile the end result from multiple sources...

So the question is - how to enable Per-Monitor v2 DPI awareness in a ClickOnce scenario (WPF, c# specifically)?

Per-Monitor v2 was added in Win 10 Creators Update (1703). ClickOnce notoriously does not support DPI awareness declared in an app.manifest file, like this.

Community
  • 1
  • 1
Marko
  • 2,266
  • 4
  • 27
  • 48

3 Answers3

18

First of all, anyone who want's to cry out - simply target .NET 4.6.2, per monitor DPI awareness is enabled by default - that is simply not true.
What is enabled by default in .NET 4.6.2 is the boilerplate code behind the scenes - quite nasty c++ hooks in window declaration code to enable support for per-monitor dpi awareness.
You still have to declare that you support per monitor dpi awareness via an app.manifest, but ClickOnce does not support it.
(do note that earlier versions of .NET will not support per monitor dpi awareness, even with the app manifest, unless you manually add the boilerplate code)

Now to the answer:

  1. Make sure your project targets .NET 4.6.2. (easisest to use Visual Studio 2017 for this)
  2. Disable DPI awareness in the assembly properties. Trust me on this, we shall re-enable it later in code. To do this, open AssemblyInfo.cs under your project Properties node (expand the spanner icon inside the SolutionExplorer, usually on the right side). Add the following code to the very last line: [assembly: DisableDpiAwareness]. (this will require a using System.Windows.Media; statement, simply click the light bulb that appears when hovering over the red squiggly line and add the suggested using statement)
  3. Add an app.manifest file and declare support for Win 10 and other OS versions. Right-click your project in SolutionExplorer -> add -> new item -> Application manifest file. Open the created manifest file and in the middle there is a section with OS versions, uncomment them as such: OS versions in app.manifest Do not uncomment the section about DPI awareness (a bit further down)! ClickOnce will throw errors if you do.
  4. You will now need the following c# code somewhere in your project, I recommend creating a new static class for this:

    internal static class NativeMethods
    {
        [DllImport("user32.dll", SetLastError = true)]
        internal static extern bool SetProcessDpiAwarenessContext(int dpiFlag);
    
        [DllImport("SHCore.dll", SetLastError = true)]
        internal static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness);
    
        [DllImport("user32.dll")]
        internal static extern bool SetProcessDPIAware();
    
        internal enum PROCESS_DPI_AWARENESS
        {
            Process_DPI_Unaware = 0,
            Process_System_DPI_Aware = 1,
            Process_Per_Monitor_DPI_Aware = 2
        }
    
        internal enum DPI_AWARENESS_CONTEXT
        {
            DPI_AWARENESS_CONTEXT_UNAWARE = 16,
            DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = 17,
            DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = 18,
            DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = 34
        }
    }
    
  5. The final part is calling the above p/invoke methods and declaring dpi awareness support. We need to do this before any other code runs. For this, right-click app.xaml in SolutionExplorer and select View Code. Then add this code:

    protected override void OnStartup(StartupEventArgs e)
    {
        if (Environment.OSVersion.Version >= new Version(6, 3, 0)) // win 8.1 added support for per monitor dpi
        {
            if (Environment.OSVersion.Version >= new Version(10, 0, 15063)) // win 10 creators update added support for per monitor v2
            {
                 NativeMethods.SetProcessDpiAwarenessContext((int)NativeMethods.DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
            }
            else NativeMethods.SetProcessDpiAwareness(NativeMethods.PROCESS_DPI_AWARENESS.Process_Per_Monitor_DPI_Aware);
        }
        else NativeMethods.SetProcessDPIAware();
    
        base.OnStartup(e);
    }
    
  6. Make sure the rest of your code handles DPI properly. Starting with .NET 4.6.2 you can use the OnDpiChanged event and VisualTreeHelper.GetDpi() method.

Enjoy :)

Marko
  • 2,266
  • 4
  • 27
  • 48
  • I tried this and everything seems to work except that the in-app scrollbars (scrollviewer) do not scale. Would you have any idea why? If I declare V2 support via manifest, the scrollbars look fine but ofcourse deployment breaks.I've verified that the call to SetProcessDpiAwarenessContext with V2 succeeds. (windows 10, latest update, targeting 4.6.2) – randomThought May 15 '18 at 16:58
  • does this solution work on windows 7? it worked for windows 10 perfectly – ibr Jan 02 '19 at 12:44
6

@Marko - great answer! But here is one small detail: if you are mixing WinForms with WPF you have to add WinForms configuration. Please, update your answer, I don't have enough point to leave a comment...

<System.Windows.Forms.ApplicationConfigurationSection>
     <add key="DpiAwareness" value="PerMonitorV2" />
</System.Windows.Forms.ApplicationConfigurationSection>

referal: https://learn.microsoft.com/en-us/dotnet/framework/winforms/high-dpi-support-in-windows-forms

Andrey
  • 81
  • 1
  • 4
  • Thankyou for posting this. It appears that this article has been recently updated (08/30/2022) and the new configuration settings work rather well (I was up until now using answer as given by @Marko, but as this recently stopped working, revisited the methods and ways of doing the DPI Awareness thing). – damichab Dec 23 '22 at 01:58
1

DPI awareness declared in an app.manifest is claimed to be one of the .NET 4.7.2 features.

Vlad
  • 793
  • 6
  • 15
  • Win 10 v1803 and targeting .NET 4.7.2 does indeed work with the following manifest: `` `` `true/pm` `PerMonitorV2, PerMonitor, System` `` `` Sadly, this does not work for Server 2008r2 with .NET 4.7.2 installed (possibly Win 7 as well), because the manifest cannot be parsed. – Marko Sep 18 '18 at 15:26