Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

VSTO custom taskpane on multi DPI system shows content twice

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?

like image 842
Daniel Hilgarth Avatar asked Apr 30 '18 12:04

Daniel Hilgarth


3 Answers

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

like image 66
Jeremy Thompson Avatar answered Nov 01 '22 13:11

Jeremy Thompson


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;
    }
}
like image 40
mnistic Avatar answered Nov 01 '22 13:11

mnistic


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.

like image 2
Robin Avatar answered Nov 01 '22 15:11

Robin