多DPI系统上的VSTO自定义任务窗口显示内容两次

Dan*_*rth 12 c# powerpoint vsto ms-office office-interop

我正在使用VSTO构建办公室插件.在具有不同DPI设置的多个监视器的系统上,我的自定义任务窗格的内容在具有较高DPI设置的监视器上绘制两次:

在此输入图像描述

只有较小的版本实际上响应用户输入.较大的版本似乎只是一个放大的图像.

我尝试过使用各种DPI相关设置,例如:

  • AutoScaleMode在我的用户控制上.我尝试了所有选项,没有变化.
  • 将过程设置为DPI可识别 - 或不 - 使用SetProcessDpiAwareness.我尝试了所有选项,没有变化.
  • 使用app.manifest并设置dpiAwaretruefalse.没变.

新的Web Addins没有这个问题.此外,内部任务窗格没有此问题.

这是一个已知的问题吗?我怎样才能解决这个问题?

mni*_*tic 5

这似乎是 Office 产品处理消息的方式中的一个错误WM_DPICHANGED。应用程序应该枚举其所有子窗口并重新缩放它们以响应消息,但不知何故无法正确处理加载项窗格。

您可以通过禁用 DPI 缩放来解决该错误。您说您尝试调用SetProcessDpiAwareness,但一旦为应用程序设置了 DPI 感知,该函数就会失败,并且您正在使用的应用程序显然已设置它,因为它适用于父窗口。然后您应该做的是 invoke SetThreadDpiAwarenessContext,就像在这个 C# 包装器中一样。不幸的是,我没有 Win10 multimon 设置来自己测试这一点,但这应该在应用程序运行时起作用。试试这个插件,它有一个按钮可以设置线程 DPI 感知上下文,看看它是否适合您。


应用程序挂钩方法

由于SetThreadDpiAwarenessContext在您的系统上可能不可用,解决该问题的一种方法是让主窗口忽略该WM_DPICHANGED消息。这可以通过安装应用程序挂钩来更改消息或通过子类化窗口来完成。应用程序挂钩是一种稍微简单一些的方法,陷阱也较少。基本上,这个想法是拦截主应用程序GetMessage更改WM_DPICHANGEDWM_NULL,这将使应用程序丢弃该消息。缺点是这种方法仅适用于已发布的消息,但WM_DPICHANGED应该是其中之一。

因此,要安装应用程序挂钩,您的加载项代码将类似于:

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
}
Run Code Online (Sandbox Code Playgroud)

请注意,这很大程度上是未经测试的代码,如果它可以阻止消息,WM_DPICHANGED您可能必须确保在应用程序退出之前通过删除挂钩来进行清理。


子类化方法

如果您想要阻止的消息没有发布到窗口,而是发送,则应用程序挂钩方法将不起作用,并且必须对主窗口进行子类化。这次我们将把代码放在用户控件中,因为主窗口需要在调用之前完全初始化SetWindowLong

因此,为了子类化 Power Point 窗口,我们的用户控件(位于插件内)看起来像这样(请注意,我为此使用 OnPaint,但您可以使用任何内容,只要保证窗口在调用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;
    }
}
Run Code Online (Sandbox Code Playgroud)


Eug*_*iev 0

尝试将以下代码添加到表单的ctor中:

[DllImport("User32.dll")]
public static extern int SetProcessDPIAware();
Run Code Online (Sandbox Code Playgroud)

此外,您可能会发现创建 DPI 感知应用程序线程很有帮助。