Пользовательская панель задач VSTO в мульти DPI-системе отображает контент дважды

Я занимаюсь строительством офиса с помощью VSTO. В системах с несколькими мониторами с различными настройками DPI содержимое моей настраиваемой области задач дважды отображается на мониторе с более высокими настройками DPI:

enter image description here

Только меньшая версия фактически отвечает на ввод пользователя. Большая версия, по-видимому, представляет собой просто масштабированное изображение.

Я пробовал играть с различными параметрами, связанными с DPI, такими как:

  • AutoScaleMode для моего пользовательского AutoScaleMode управления. Я пробовал все варианты, никаких изменений.
  • Настройка процесса для DPI - или нет - с помощью SetProcessDpiAwareness. Я пробовал все варианты, никаких изменений.
  • Используя app.manifest и установив dpiAware в true и false. Без изменений.

У новых веб-аддинов нет этой проблемы. Кроме того, внутренние панели задач не имеют этой проблемы.

Это известная проблема? Как я могу это исправить?

Ответ 1

Кажется, это ошибка в продуктах Office, так как они обрабатывают обработку сообщения WM_DPICHANGED. Предполагается, что приложение должно перечислить все дочерние окна и перемасштабировать их в ответ на сообщение, но оно каким-то образом не сможет правильно обработать панели надстройки.

Что вы можете сделать, чтобы обойти ошибку, отключить масштабирование DPI. Вы говорите, что пытались вызвать SetProcessDpiAwareness, но эта функция документирована с ошибкой после того, как была установлена осведомленность о DPI для приложения, и приложение, которое вы используете, явно установило его, потому что оно работает для родительского окна. То, что вы должны сделать, это вызвать SetThreadDpiAwarenessContext, как в этой SetThreadDpiAwarenessContext С#. К сожалению, у меня нет настройки для нескольких мониторов Win10, чтобы проверить это самостоятельно, но это должно работать как приложение работает. Попробуйте эту надстройку, у нее есть кнопка для установки контекста осведомленности потока DPI и посмотрите, работает ли это для вас.


Подход приложения

Поскольку SetThreadDpiAwarenessContext может быть недоступен в вашей системе, один из способов решения этой проблемы - заставить главное окно игнорировать сообщение WM_DPICHANGED. Это можно сделать либо путем установки крюка приложения для изменения сообщения, либо путем подкласса окна. Крючок приложения - это немного более легкий подход с меньшим количеством ошибок. В основном идея состоит в том, чтобы перехватить основное приложение GetMessage и изменить WM_DPICHANGED на 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 вам, вероятно, придется почистить его, удалив крючок перед выходом приложения.


Подклассический подход

Если сообщение, которое вы хотите заблокировать, не отправляется в окно, а отправляется вместо этого, метод привязки приложения не будет работать, и основное окно нужно будет подклассифицировать. На этот раз мы SetWindowLong наш код в пользовательский элемент управления, потому что основные окна должны быть полностью инициализированы перед SetWindowLong.

Поэтому для подкласса окна Power Point наш пользовательский элемент управления (который находится внутри addin) будет выглядеть примерно так (обратите внимание, что я использую 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;
    }
}

Ответ 2

Поскольку ваш админ работает в среде размещения, нет никакой помощи в внесении изменений, влияющих на что-либо на уровне процесса. Тем не менее, существуют API Win32 для работы с дочерними окнами. Процесс может иметь разные контексты DPI-осведомленности среди окон верхнего уровня. Доступно с момента обновления Anniversary (Windows 10, версия 1703).

Я сам его не тестировал, поэтому я могу указать только на наиболее подходящее направление. "Если вы хотите выбрать диалог или HWND в диалоговом окне из автоматического масштабирования DPI, вы можете использовать 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 в addin. У DPI-осведомленности и WPF есть моменты наверняка. Но размещение WPF внутри elementhost может дать вам дополнительный контроль над проблемой DPI. Особенно при применении API Win32 и возможности использования дескриптора окна элементаhost и переопределения сообщений WIN32, которые он получает.

Надеюсь, это поможет.

Ответ 3

Это гипотеза и, мы надеемся, указывает на коренную причину; проблема в том, что насосы сообщений фильтруются в приложениях VSTO Office.

Может быть красная сельдь, так как я никогда не видел, чтобы WndProc messages вызывали двойной рендеринг, но я никогда раньше не видел двойной рендеринг!

Однако, проблемы с фокусировкой и/или неуправляемые элементы управления заставили меня вспомнить это поведение.

Первоначально я столкнулся с этой странной проблемой с одной из моих надстроек Excel: BUG: не удается выбрать даты в DatePicker, которые выходят за пределы плавающей надстройки VSTO

Ханс Пассант определил первопричину:

Что никогда не является проблемой, так это то, что вы полагаетесь на насос сообщений в Excel для отправки сообщений Windows, сообщений, которые заставляют эти элементы управления реагировать на ввод. Это не так в WPF, как в Winforms, у них есть собственный цикл отправки, который фильтрует сообщения перед их доставкой в окно.

Я ответил на несколько вопросов с этой информацией. Этот QA показывает один из способов исправления диспетчеризации сообщений, например, Excel CustomTaskPane с контролем WebBrowser - проблемы с клавиатурой/фокусом

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], чтобы помочь вам исправить это.


редактировать

Я не могу воспроизвести это! Это ПК специфично. Попробуйте обновить видеодрайвер или попробуйте компьютер с другой видеокартой. Вот мои характеристики видеокарты:

Название Intel (R) HD Graphics 520
Тип адаптера Intel (R) HD Graphics Семейство
Драйверы
igdumdim64.dll, igd10iumd64.dll, igd10iumd64.dll, igdumdim32, igd10iumd32, igd10iumd32
Драйвер c:\windows\system32\drivers\igdkmd64.sys(20.19.15.4326, 7,44 МБ (7 806 352 байта), 19.06.2016, 23:32)

enter image description here