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

时间:2018-04-30 12:28:37

标签: c# ms-office vsto powerpoint office-interop

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

enter image description here

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

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

    我的用户控件上有
  • AutoScaleMode。我尝试了所有选项,没有改变。
  • 使用SetProcessDpiAwareness将流程设置为DPI识别与否。我尝试了所有选项,没有改变。
  • 使用app.manifest并将dpiAware设置为truefalse。没有变化。

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

这是一个已知问题吗?我该如何解决这个问题?

4 个答案:

答案 0 :(得分:3)

这似乎是Office产品中处理WM_DPICHANGED消息处理方式的错误。应用程序应枚举其所有子窗口并重新调整它们以响应消息,但它无法正确处理加载窗格。

解决这个问题的方法是禁用DPI缩放。您说您尝试调用SetProcessDpiAwareness,但是一旦为应用设置了DPI感知,该功能就会记录为失败,并且您正在使用的应用程序清楚地设置它,因为它适用于父窗口。你应该做的是调用SetThreadDpiAwarenessContext,就像this C# wrapper一样。不幸的是,我没有Win10 multimon设置来自己测试,但这应该在应用程序运行时起作用。试试this add-in,它有一个按钮来设置线程DPI感知上下文,看看它是否适合你。

应用程序挂钩方法

由于您的系统可能无法使用SetThreadDpiAwarenessContext,因此处理问题的一种方法是使主窗口忽略WM_DPICHANGED消息。这可以通过安装应用程序挂钩来更改消息或通过子类化窗口来完成。应用程序挂钩是一种稍微简单的方法,具有更少的陷阱。基本上,我们的想法是拦截主要应用程序GetMessagechange WM_DPICHANGED to WM_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
}

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

子类化方法

如果要阻止的消息未发布到窗口,而是发送,则应用程序挂钩方法不起作用,主窗口必须为subclassed instead。这次我们将代码放在用户控件中,因为主窗口需要在调用SetWindowLong之前完全初始化。

因此,为了对Power Point窗口进行子类化,我们的用户控件(在插件中)会看起来像(请注意我使用的是OnPaint,但只要它能保证使用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;
    }
}

答案 1 :(得分:2)

由于您的插件正在托管环境中运行,因此在进行更改时会对进程级别的任何内容产生影响。但是,有处理子窗口的Win32 API。进程可能在其顶级窗口中具有不同的DPI感知上下文。自周年纪念更新(Windows 10,版本1703)以来可用。

我自己没有测试过,所以我只能指出你最相关的方向。 "如果要在自动DPI缩放中的对话框中选择对话框或HWND,可以使用SetDialogDpiChangeBehavior / SetDialogControlDpiChangeBehavior"

此处有更多信息:https://blogs.windows.com/buildingapps/2017/04/04/high-dpi-scaling-improvements-desktop-applications-windows-10-creators-update/#bEKiRLjiB4dZ7ft9.97

已经很多年了,因为我已经参加过低级别的win32对话 - 但我确信你可以在任何窗口句柄上使用这些API,而无需创建实际对话。一个对话框和一个普通窗口,如果我没记错的话,默认的消息循环处理程序和一些不同的默认窗口样式就不同了。

从外观上看,似乎你在插件中使用了WPF。 DPI意识和WPF有其确定的时刻。但是在元素主机中托管WPF可能会让您对DPI问题有额外的控制权。特别是在应用Win32 API时,能够使用elementhost的窗口句柄并覆盖它接收的WIN32消息。

我希望这有任何帮助。

答案 2 :(得分:1)

这是一个假设,希望能指出你的根本原因;问题是在VSTO Office应用程序中过滤消息泵。

可能是红鲱鱼,因为我从未见过WndProc messages导致双重渲染,但我以前从未见过双重渲染!

但是,设置焦点问题和/或不可点击的控件让我记住了这种行为。

最初我遇到了一个我的Excel加载项的奇怪问题: BUG: Cant choose dates on a DatePicker that fall outside a floating VSTO Add-In

Hans Passant确定了根本原因:

  

什么是永远不会问题在于您依靠Excel中的消息泵来分派Windows消息,使这些控件响应输入的消息。这与WPF中的错误一样,它们有自己的调度循环,可以在将消息传递到窗口之前对其进行过滤。

我已经用这些信息回答了几个问题。此QA显示了一种纠正消息泵调度的方法,例如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);
}

如果这不是根本原因,至少你可以将其从故障排除步骤中删除,这是一种“偏离常规”的诊断技术。

如果可能的话,我会爱[mcve]来帮助你修复它。

修改

我无法重现它!这是PC特定的。尝试升级视频驱动程序或尝试使用其他视频卡的计算机。这是我的视频卡规格:

  

名称英特尔(R)HD Graphics 520
  适配器类型英特尔(R)高清显卡系列
  <强>驱动器
  igdumdim64.dll,igd10iumd64.dll,igd10iumd64.dll,igdumdim32,igd10iumd32,igd10iumd32
  驱动程序 c:\ windows \ system32 \ drivers \ igdkmd64.sys(20.19.15.4326,7.44)   MB(7,806,352字节),19/06/2016 11:32 PM)

enter image description here

答案 3 :(得分:0)

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

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

此外,您可能会发现Creating a DPI-Aware Application主题有用。