Пользовательская панель задач VSTO в системе с несколькими DPI показывает содержимое дважды - PullRequest
0 голосов
/ 30 апреля 2018

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

enter image description here

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

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

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

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

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

Ответы [ 4 ]

0 голосов
/ 09 мая 2018

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

Чтобы обойти эту ошибку, вы можете отключить масштабирование DPI. Вы говорите, что пытались вызвать SetProcessDpiAwareness, но задокументировано, что эта функция перестала работать после того, как для приложения была установлена ​​поддержка DPI, и приложение, которое вы используете, явно настроено, потому что оно работает для родительского окна. В таком случае вы должны вызывать SetThreadDpiAwarenessContext, как в этой оболочке C # . К сожалению, у меня нет установки для установки нескольких мониторов в 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.

Таким образом, для создания подкласса окна 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;
    }
}
0 голосов
/ 03 мая 2018

Поскольку ваш плагин работает в размещенной среде, нет никакой возможности вносить изменения, влияющие на что-либо на уровне процесса. Тем не менее, существуют Win32 API для работы с дочерними окнами. Процесс может иметь различные контексты осведомленности о DPI среди окон верхнего уровня. Доступно после The Anniversary Update (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 в надстройке. Осведомленность о DPI и WPF наверняка есть моменты. Но размещение WPF внутри elementhost может дать вам дополнительный контроль над проблемой DPI. Особенно при применении Win32 API и возможности использовать дескриптор окна elementhost и переопределять получаемые им сообщения WIN32.

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

0 голосов
/ 07 мая 2018

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

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

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

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

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

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

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

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
Драйверы
igdumdim64.dll, igd10iumd64.dll, igd10iumd64.dll, igdumdim32, igd10iumd32, igd10iumd32
Driver c: \ windows \ system32 \ drivers \ igdkmd64.sys (20.19.15.4326, 7.44 МБ (7 806 352 байта), 19.06.2016 23:32)

enter image description here

0 голосов
/ 30 апреля 2018

Попробуйте добавить следующий код в ctor вашей формы:

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

Также вам может пригодиться Создание приложения с поддержкой DPI .

...