14

I am building an office addin using VSTO. On systems with multiple monitors with different DPI settings, the contents of my custom task pane is drawn twice on the monitor with the higher DPI settings:

enter image description here

Only the smaller version is actually responding to user input. The larger version seems to be simply an upscaled image.

I have tried playing around with diverse DPI related settings like:

  • AutoScaleMode on my user control. I tried all options, no change.
  • Setting the process to DPI aware - or not - using SetProcessDpiAwareness. I tried all options, no change.
  • Using an app.manifest and setting dpiAware to true and false. No change.

The new Web Addins don't have this problem. Also, the internal task panes don't have this problem.

Is this a known problem? How can I fix this?

Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443

4 Answers4

5

This seems to be a bug in Office products in the way they handle the processing of the WM_DPICHANGED message. The application is supposed to enumerate all of its child windows and rescale them in response to the message but it's somehow failing to process add-in panes properly.

What you can do to work around the bug is disable DPI scaling. You say you tried invoking SetProcessDpiAwareness, but that function is documented to fail once DPI awareness has been set for an app, and the app you're using clearly has it set because it works for the parent window. What you are supposed to do then is invoke SetThreadDpiAwarenessContext, like in this C# wrapper. Unfortunately I don't have a Win10 multimon setup to test this myself, but that's supposed to work as the application is running. Try this add-in, it has a button to set thread DPI awareness context, and see if that works for you.


The application hook approach

Since SetThreadDpiAwarenessContext may not be available on your system, one way to deal with the problem is to make the main window ignore the WM_DPICHANGED message. This can be done either by installing an application hook to change the message or by subclassing the window. An application hook is a slightly easier approach with fewer pitfalls. Basically the idea is to intercept the main application's GetMessage and change WM_DPICHANGED to WM_NULL, which will make the application discard the message. The drawback is that this approach only works for posted messages, but WM_DPICHANGED should be one of those.

So to install an application hook, your add-in code would look something like:

public partial class ThisAddIn
{
    public enum HookType : int
    {
        WH_JOURNALRECORD = 0,
        WH_JOURNALPLAYBACK = 1,
        WH_KEYBOARD = 2,
        WH_GETMESSAGE = 3,
        WH_CALLWNDPROC = 4,
        WH_CBT = 5,
        WH_SYSMSGFILTER = 6,
        WH_MOUSE = 7,
        WH_HARDWARE = 8,
        WH_DEBUG = 9,
        WH_SHELL = 10,
        WH_FOREGROUNDIDLE = 11,
        WH_CALLWNDPROCRET = 12,
        WH_KEYBOARD_LL = 13,
        WH_MOUSE_LL = 14
    }

    delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll", SetLastError = true)]
    static extern IntPtr SetWindowsHookEx(HookType hookType, HookProc lpfn, IntPtr hMod, uint dwThreadId);
    [DllImport("user32.dll")]
    static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);


    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int X;
        public int Y;
    }
    public struct MSG
    {
        public IntPtr hwnd;
        public uint message;
        public IntPtr wParam;
        public IntPtr lParam;
        public uint time;
        public POINT pt;
    }

    HookProc cbGetMessage = null;

    private UserControl1 myUserControl1;
    private Microsoft.Office.Tools.CustomTaskPane myCustomTaskPane;
    private void ThisAddIn_Startup(object sender, System.EventArgs e)
    {
        this.cbGetMessage = new HookProc(this.MyGetMessageCb);
        SetWindowsHookEx(HookType.WH_GETMESSAGE, this.cbGetMessage, IntPtr.Zero, (uint)AppDomain.GetCurrentThreadId());

        myUserControl1 = new UserControl1();
        myCustomTaskPane = this.CustomTaskPanes.Add(myUserControl1, "My Task Pane");
        myCustomTaskPane.Visible = true;


    }

    private IntPtr MyGetMessageCb(int code, IntPtr wParam, IntPtr lParam)
    {
        unsafe
        {
            MSG* msg = (MSG*)lParam;
            if (msg->message == 0x02E0)
                msg->message = 0;
        }

        return CallNextHookEx(IntPtr.Zero, code, wParam, lParam);
    }

    private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
    {
    }

    #region VSTO generated code

    private void InternalStartup()
    {
        this.Startup += new System.EventHandler(ThisAddIn_Startup);
        this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
    }

    #endregion
}

Please note that this is largely untested code, and if it works in blocking the WM_DPICHANGED message you will probably have to make sure to clean up by removing the hook before application exit.


The subclassing approach

If the message you want to block is not posted to the window, but sent instead, the application hook method is not going to work and the main window will have to be subclassed instead. This time we will place our code within the user control because the main windows needs to be fully initialized before invoking SetWindowLong.

So to subclass the Power Point window, our user control (which is within the addin) would look something like (note that I am using OnPaint for this but you can use whatever as long as it's guaranteed that the window is initialized at the time of invoking SetWindowLong):

public partial class UserControl1 : UserControl
{
    const int GWLP_WNDPROC = -4;
    [DllImport("user32", SetLastError = true)]
    extern static IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
    [DllImport("user32", SetLastError = true)]
    extern static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr lpNewLong);
    [DllImport("user32", SetLastError = true)]
    extern static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr lpNewLong);
    delegate IntPtr WindowProc(IntPtr hwnd, uint uMsg, IntPtr wParam, IntPtr lParam);
    private IntPtr origProc = IntPtr.Zero;
    private WindowProc wpDelegate = null;
    public UserControl1()
    {
        InitializeComponent();
        this.Paint += UserControl1_Paint;

    }

    void UserControl1_Paint(object sender, PaintEventArgs e)
    {
        if (origProc == IntPtr.Zero)
        {
            //Subclassing
            this.wpDelegate = new WindowProc(MyWndProc);
            Process process = Process.GetCurrentProcess();
            IntPtr wpDelegatePtr = Marshal.GetFunctionPointerForDelegate(wpDelegate);
            if (IntPtr.Size == 8)
            {
                origProc = SetWindowLongPtr(process.MainWindowHandle, GWLP_WNDPROC, wpDelegatePtr);
            }
            else
            {
                origProc = SetWindowLong(process.MainWindowHandle, GWLP_WNDPROC, wpDelegatePtr);
            }
        }
    }


    //Subclassing
    private IntPtr MyWndProc(IntPtr hwnd, uint uMsg, IntPtr wParam, IntPtr lParam)
    {
        if (uMsg == 0x02E0) //WM_DPICHANGED
            return IntPtr.Zero;
        IntPtr retVal = CallWindowProc(origProc, hwnd, uMsg, wParam, lParam);
        return retVal;
    }
}
mnistic
  • 10,866
  • 2
  • 19
  • 33
  • @JeremyThompson that's literally what tooltip on upvote button says, so probably no need to state that explicitly :) – Evk May 10 '18 at 06:55
  • Thanks for your answer. Unfortunately, my Windows 10 version is a bit too old for `SetThreadDpiAwarenessContext` to exist, so I can't test if that would solve this issue. But even if it would, it would still not solve it for older versions. However, the linked add-in showed me something else completely: It is in fact a problem with PowerPoint. The Excel Add-in from the link works correctly, however, if I am using the same code in a PowerPoint Add-in I am having the same issues as with my own Add-ins. – Daniel Hilgarth May 10 '18 at 07:11
  • @DanielHilgarth OK well I can confirm that everything's fine on Windows 7, so that leaves Windows 8.1 (which is when they introduced per-monitor DPI) through Windows 10 Anniversary. Have you tried disabling DPI for PowerPoint through the registry: https://blogs.technet.microsoft.com/mspfe/2013/11/21/disabling-dpi-scaling-on-windows-8-1-the-enterprise-way/ – mnistic May 10 '18 at 10:27
  • The other thing I can suggest is installing an application hook to ignore the WM_DPICHANGED message. Let me know if you need more information on that. – mnistic May 10 '18 at 10:29
  • @mnistic: If you have info on how to do that, that would be great! – Daniel Hilgarth May 11 '18 at 13:54
  • BTW: If this info helps to actually solve this problem, I will start another bounty and will award it to you. Unfortunately, the existing bounty expired and has been automatically awarded to an arbitrary answer – Daniel Hilgarth May 11 '18 at 13:56
  • @DanielHilgarth see the update. I cannot test this code so let me know if it works. If it doesn't it means that `WM_DPICHANGED` is not posted but sent, which will mean we have to employ the subclassing approach. – mnistic May 12 '18 at 14:41
  • @mnistic Sorry for the late reply. I just tested it. While it compiles, it doesn't seem to have any effect – Daniel Hilgarth May 17 '18 at 09:18
  • @DanielHilgarth OK the subclassing approach should definitely work, see the edit. I tested it with `WM_DISPLAYCHANGE`, and you would hope that `WM_DPICHANGE` is the same kind of message. If it doesn't work I'm afraid I can't debug it until I get myself a Win10 dev rig, even though I'd like to -- seems like an interesting bug. – mnistic May 17 '18 at 18:47
  • Did you have any success with this method ? We work for a company called UpSlide that develop plugins for Office and we haven't found a fix in code yet. Note that today, if you have the latest versions of Office 2016 and Windows 10, there is a new option in Office : https://upslide.zendesk.com/hc/en-us/articles/360003084614-UpSlide-panes-are-not-displayed-properly This is the article we send to our clients. This solution is not really satisfactory because compatibility mode makes office ugly (blurry)... – misterfrb Jun 07 '18 at 14:18
  • Unfortunately I still don't have a Win10 machine to test this on (at work or at home), so you might want to ping the OP. (but I did test it with WM_DISPLAYCHANGE and it worked like a charm with that) Also, it should be easy enough to test yourself if you happen to have a Win10 multi monitor setup, just copy/paste the code from the "subclassing approach" section into a new PowerPoint add-in project. – mnistic Jun 07 '18 at 14:49
  • @mnistic I could try your solution, it works well, until the pane width is manually resized, or until the window gets maximized/windowed. Your solution succesfully prevents the bug because it catches the message that is sent when the window changes screen, but the pane will still get broken when other messages are received. btw our latest tests on updated machines (windows 10, office 2016 insider) show that the bug is apparently fixed for windows forms pane, even in 'best appearance' mode. We still have some issues with our panes that host WPF controls, and only with specific scaling changes – misterfrb Jun 08 '18 at 13:53
  • Ah, OK. Thanks for reporting back! – mnistic Jun 08 '18 at 14:18
3

This is a hypothesis and hopefully points you to the root cause; the problem is Message Pumps being filtered in VSTO Office apps.

Could be a red herring as I've never seen WndProc messages cause double rendering but I've never seen double rendering before!

However, setting the focus problems and/or un-clickable controls made me remember this behaviour.

Originally I came across this weird issue with one of my Excel Add-Ins: BUG: Can't choose dates on a DatePicker that fall outside a floating VSTO Add-In

Hans Passant identified the root cause:

What's never not a problem is that you rely on the message pump in Excel to dispatch Windows messages, the messages that make these controls respond to input. This goes wrong in WPF as much as Winforms, they have their own dispatch loop that filters messages before they are delivered to the window.

I've answered a few questions with this information. This QA shows one way to correct the message pump dispatching, eg Excel CustomTaskPane with WebBrowser control - keyboard/focus issues

protected override void WndProc(ref Message m)
{
  const int NotifyParent = 528; //might be different depending on problem
  if(m.Msg == NotifyParent && !this.Focused)
  {
    this.Focus();
  }
  base.WndProc(ref m);
}

If this isn't the root cause at least you can cross it off the troubleshooting steps, it's an "off the beaten track" diagnostic technique.

If at all possible I'd love an [mcve] to help you fix it.


Edit

I cannot reproduce it! It's PC specific. Try upgrading your Video Driver or try a machine with a different video card. Here are my video card specs:

Name Intel(R) HD Graphics 520
Adapter Type Intel(R) HD Graphics Family
Drivers
igdumdim64.dll,igd10iumd64.dll,igd10iumd64.dll,igdumdim32,igd10iumd32,igd10iumd32
Driver c:\windows\system32\drivers\igdkmd64.sys (20.19.15.4326, 7.44 MB (7,806,352 bytes), 19/06/2016 11:32 PM)

enter image description here

halfer
  • 19,824
  • 17
  • 99
  • 186
Jeremy Thompson
  • 61,933
  • 36
  • 195
  • 321
  • To reproduce, simply create a new VSTO-Addin with a task pane and move it to the monitor with the higher DPI. – Daniel Hilgarth May 07 '18 at 09:59
  • Ok, can you please tag the office tech? Ok it's late let me try in 12 hours. Trusting this code doesn't work? What were your results, mine may differ? – Jeremy Thompson May 07 '18 at 12:10
  • Not sure what you mean by "office tech"? – Daniel Hilgarth May 07 '18 at 12:35
  • I am not getting any notify parent messages, so it makes no difference if I add this code or not. – Daniel Hilgarth May 07 '18 at 14:40
  • See my edit, "office tech" I just meant tag the question Excel, PP, Word and WPF or Winform (is it powerpoint?, my repro is an Excel - Winform AddIn - .Net 4.0). If you can get me a repro happy to help. Also I'm on Windows 7 right now, maybe that's why I cant repro. – Jeremy Thompson May 08 '18 at 04:48
  • It's PowerPoint currently and it doesn't matter if WinForms or WPF. It happens with both. However, based on my understanding, Win7 doesn't support different DPI values for different monitors, so that's probably why you can't repro. – Daniel Hilgarth May 08 '18 at 05:06
  • Thanks for repro, it's a video card issue. Call Microsoft Support either PSS/CSS. If it's a bug, which it is, it's free. The driver is responsible. – Jeremy Thompson May 09 '18 at 12:26
  • Why are you saying that it's a video card driver issue? – Daniel Hilgarth May 09 '18 at 13:53
  • I can reproduce it on a client's system and also via remote desktop – Daniel Hilgarth May 09 '18 at 13:54
  • I just dont think its a VSTO issue, its lower level. Take this advice with a grain of salt, I'm sorry I just dont have a rig right now to repro it. I believe you, IMHO it is a bug with DPI. – Jeremy Thompson May 10 '18 at 02:32
  • It's in fact a PowerPoint issue. The problem does not exist for Excel Add-ins – Daniel Hilgarth May 10 '18 at 07:08
  • @DanielHilgarth Some people in my team also have the same problem with Excel. We think it depends if you have windows 10's 1803 update. We don't see the same behaviour as you, though. For us, prior 1803, he have the bug in ppt and xl as well. After 1803, we don't have it at all. (ppt or excel / compatibility or best appearance mode) Maybe your Excel display settings are in compatibility mode ? the option is accessible via a new button located at right bottom of the screen (see https://upslide.zendesk.com/hc/en-us/articles/360003084614-UpSlide-panes-are-not-displayed-properly ) – misterfrb Jun 07 '18 at 15:09
2

Since your addin is running in a hosted environment, there's no help in making changes affecting anything on process level. However, there are Win32 APIs in place to dealing with child windows. A process may have different DPI-awareness contexts amongst it's top-level windows. Available since The Anniversary Update (Windows 10, version 1703).

I haven't tested it myself, so I can only point you in the most relevant direction. "When you want to opt a dialog or an HWND in a dialog out of automatic DPI scaling you can use SetDialogDpiChangeBehavior/SetDialogControlDpiChangeBehavior"

More info here: https://blogs.windows.com/buildingapps/2017/04/04/high-dpi-scaling-improvements-desktop-applications-windows-10-creators-update/#bEKiRLjiB4dZ7ft9.97

It's been quite many years, since I've dwelved in low level win32 dialogs - but I'm quite sure you can use those API's on any window handle without creating an actual dialog. A dialog and a normal window, just differs in the default message loop handler and a few different default window styles, if I remember correctly.

By the looks of it, it seems you use WPF in the addin. DPI awareness and WPF has it's moments for sure. But hosting the WPF inside a elementhost, might give you additional control over the DPI issue. Especially when applying Win32 APIs, and being able to use the window handle of the elementhost and override WIN32 messages it receives.

I hope this is of any help.

Robin
  • 616
  • 3
  • 9
  • Thanks for the hint, but I am unsure how I would apply this. In the demo from which I created the screenshot, I am not using WPF, it is just a simply WinForms user control. – Daniel Hilgarth May 07 '18 at 14:42
0

Try to add the following code to the ctor of your form:

[DllImport("User32.dll")]
public static extern int SetProcessDPIAware();

Also you may find the Creating a DPI-Aware Application thread helpful.

Eugene Astafiev
  • 47,483
  • 3
  • 24
  • 45
  • No, this doesn't change anything either. The problem is that PowerPoint already is DPI aware. – Daniel Hilgarth Apr 30 '18 at 17:15
  • Have you tried using .net 4.7. as a target framework? Does it help? – Eugene Astafiev Apr 30 '18 at 18:05
  • I did and it doesn't help. But this wouldn't be a solution for my project anyway as we are targeting 4.0 because of corporate clients still running on old Win 7 installations. – Daniel Hilgarth Apr 30 '18 at 18:05
  • Worth also noting: you must execute this method (or it's succession from shcore.dll, `setprocessdpiawareness`) before the UI loop (`Application.Run`) has started in _most_ cases. – caesay May 04 '18 at 13:55