Управление веб-браузером WPF и масштабирование DPI

Я работаю с WPF-приложением, использующим элемент управления веб-браузером, и у меня возникают проблемы с масштабированием с высоким DPI.

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

Здесь показан пример захвата экрана из приложения WPF, в котором используются два элемента управления веб-браузером.

100% Масштабирование:

введите описание изображения здесь

150% Масштабирование:

введите описание изображения здесь

Обратите внимание, что во втором изображении масштабирование веб-браузера намного меньше, чем в первом изображении относительно основного содержимого формы (панель инструментов/меню/строка состояния).

Есть ли способ заставить элемент управления веб-браузера правильно использовать параметры высокого DPI, унаследованные от приложения?

Эта ссылка MSDN: Адресация проблем DPI

показывает подход на самом низком уровне (внизу документа), реализующий пользовательские интерфейсы веб-браузера, но мне интересно, может ли быть более чистый способ решить эту проблему.

Ответ 1

Я обнаружил, что я считаю лучшим способом достижения требуемой функциональности (при условии, что вам все равно нужно указать FEATURE_BROWSER_EMULATION в реестре, чтобы использовать самую последнюю версию IE).

Все, что вам нужно сделать, это создать новый ключ в HKCU\Software\Microsoft\Internet Explorer\Main\FeatureControl с именем FEATURE_96DPI_PIXEL и добавить туда свою исполняемую запись типа DWORD (32-bit), с именем приложения exe в качестве имени ключа и значением 1.

Проверьте настройки при запуске приложения, прежде чем создавать экземпляр компонента WebBrowser, и все будет в порядке.

Оригинальный пост (с другими возможными функциями):https://www.reddit.com/r/dotnet/comments/2j5m6m/wpf_webbrowser_control_alternatives/

Ответ 2

Вот код класса утилиты, который позволяет отключить контекстное меню WPF WebBrowser. Он также позволяет подавлять ошибки script (WPF WebBrowser - как подавить ошибки script) и изменить IE DOCHOSTUIFLAG.

Пример использования:

public partial class Player : Window
{
    private WebBrowserHostUIHandler _wbHandler;

    public Player()
    {
        InitializeComponent();
        ...
        _wbHandler = new WebBrowserHostUIHandler(MyWebBrower);
        _wbHandler.IsWebBrowserContextMenuEnabled = true;
    }
}

Код полезности:

public class WebBrowserHostUIHandler : Native.IDocHostUIHandler
{
    private const uint E_NOTIMPL = 0x80004001;
    private const uint S_OK = 0;
    private const uint S_FALSE = 1;

    public WebBrowserHostUIHandler(WebBrowser browser)
    {
        if (browser == null)
            throw new ArgumentNullException("browser");

        Browser = browser;
        browser.LoadCompleted += OnLoadCompleted;
        browser.Navigated += OnNavigated;
        IsWebBrowserContextMenuEnabled = true;
        Flags |= HostUIFlags.ENABLE_REDIRECT_NOTIFICATION;
    }

    public WebBrowser Browser { get; private set; }
    public HostUIFlags Flags { get; set; }
    public bool IsWebBrowserContextMenuEnabled { get; set; }
    public bool ScriptErrorsSuppressed { get; set; }

    private void OnNavigated(object sender, NavigationEventArgs e)
    {
        SetSilent(Browser, ScriptErrorsSuppressed);
    }

    private void OnLoadCompleted(object sender, NavigationEventArgs e)
    {
        Native.ICustomDoc doc = Browser.Document as Native.ICustomDoc;
        if (doc != null)
        {
            doc.SetUIHandler(this);
        }
    }

    uint Native.IDocHostUIHandler.ShowContextMenu(int dwID, Native.POINT pt, object pcmdtReserved, object pdispReserved)
    {
        return IsWebBrowserContextMenuEnabled ? S_FALSE : S_OK;
    }

    uint Native.IDocHostUIHandler.GetHostInfo(ref Native.DOCHOSTUIINFO info)
    {
        info.dwFlags = (int)Flags;
        info.dwDoubleClick = 0;
        return S_OK;
    }

    uint Native.IDocHostUIHandler.ShowUI(int dwID, object activeObject, object commandTarget, object frame, object doc)
    {
        return E_NOTIMPL;
    }

    uint Native.IDocHostUIHandler.HideUI()
    {
        return E_NOTIMPL;
    }

    uint Native.IDocHostUIHandler.UpdateUI()
    {
        return E_NOTIMPL;
    }

    uint Native.IDocHostUIHandler.EnableModeless(bool fEnable)
    {
        return E_NOTIMPL;
    }

    uint Native.IDocHostUIHandler.OnDocWindowActivate(bool fActivate)
    {
        return E_NOTIMPL;
    }

    uint Native.IDocHostUIHandler.OnFrameWindowActivate(bool fActivate)
    {
        return E_NOTIMPL;
    }

    uint Native.IDocHostUIHandler.ResizeBorder(Native.COMRECT rect, object doc, bool fFrameWindow)
    {
        return E_NOTIMPL;
    }

    uint Native.IDocHostUIHandler.TranslateAccelerator(ref System.Windows.Forms.Message msg, ref Guid group, int nCmdID)
    {
        return S_FALSE;
    }

    uint Native.IDocHostUIHandler.GetOptionKeyPath(string[] pbstrKey, int dw)
    {
        return E_NOTIMPL;
    }

    uint Native.IDocHostUIHandler.GetDropTarget(object pDropTarget, out object ppDropTarget)
    {
        ppDropTarget = null;
        return E_NOTIMPL;
    }

    uint Native.IDocHostUIHandler.GetExternal(out object ppDispatch)
    {
        ppDispatch = Browser.ObjectForScripting;
        return S_OK;
    }

    uint Native.IDocHostUIHandler.TranslateUrl(int dwTranslate, string strURLIn, out string pstrURLOut)
    {
        pstrURLOut = null;
        return E_NOTIMPL;
    }

    uint Native.IDocHostUIHandler.FilterDataObject(IDataObject pDO, out IDataObject ppDORet)
    {
        ppDORet = null;
        return E_NOTIMPL;
    }

    public static void SetSilent(WebBrowser browser, bool silent)
    {
        Native.IOleServiceProvider sp = browser.Document as Native.IOleServiceProvider;
        if (sp != null)
        {
            Guid IID_IWebBrowserApp = new Guid("0002DF05-0000-0000-C000-000000000046");
            Guid IID_IWebBrowser2 = new Guid("D30C1661-CDAF-11d0-8A3E-00C04FC9E26E");

            object webBrowser;
            sp.QueryService(ref IID_IWebBrowserApp, ref IID_IWebBrowser2, out webBrowser);
            if (webBrowser != null)
            {
                webBrowser.GetType().InvokeMember("Silent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.PutDispProperty, null, webBrowser, new object[] { silent });
            }
        }
    }
}

internal static class Native
{
    [ComImport, Guid("BD3F23C0-D43E-11CF-893B-00AA00BDCE1A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IDocHostUIHandler
    {
        [PreserveSig]
        uint ShowContextMenu(int dwID, POINT pt, [MarshalAs(UnmanagedType.Interface)] object pcmdtReserved, [MarshalAs(UnmanagedType.Interface)] object pdispReserved);

        [PreserveSig]
        uint GetHostInfo(ref DOCHOSTUIINFO info);

        [PreserveSig]
        uint ShowUI(int dwID, [MarshalAs(UnmanagedType.Interface)] object activeObject, [MarshalAs(UnmanagedType.Interface)] object commandTarget, [MarshalAs(UnmanagedType.Interface)] object frame, [MarshalAs(UnmanagedType.Interface)] object doc);

        [PreserveSig]
        uint HideUI();

        [PreserveSig]
        uint UpdateUI();

        [PreserveSig]
        uint EnableModeless(bool fEnable);

        [PreserveSig]
        uint OnDocWindowActivate(bool fActivate);

        [PreserveSig]
        uint OnFrameWindowActivate(bool fActivate);

        [PreserveSig]
        uint ResizeBorder(COMRECT rect, [MarshalAs(UnmanagedType.Interface)] object doc, bool fFrameWindow);

        [PreserveSig]
        uint TranslateAccelerator(ref System.Windows.Forms.Message msg, ref Guid group, int nCmdID);

        [PreserveSig]
        uint GetOptionKeyPath([Out, MarshalAs(UnmanagedType.LPArray)] string[] pbstrKey, int dw);

        [PreserveSig]
        uint GetDropTarget([In, MarshalAs(UnmanagedType.Interface)] object pDropTarget, [MarshalAs(UnmanagedType.Interface)] out object ppDropTarget);

        [PreserveSig]
        uint GetExternal([MarshalAs(UnmanagedType.IDispatch)] out object ppDispatch);

        [PreserveSig]
        uint TranslateUrl(int dwTranslate, [MarshalAs(UnmanagedType.LPWStr)] string strURLIn, [MarshalAs(UnmanagedType.LPWStr)] out string pstrURLOut);

        [PreserveSig]
        uint FilterDataObject(IDataObject pDO, out IDataObject ppDORet);
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct DOCHOSTUIINFO
    {
        public int cbSize;
        public int dwFlags;
        public int dwDoubleClick;
        public IntPtr dwReserved1;
        public IntPtr dwReserved2;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct COMRECT
    {
        public int left;
        public int top;
        public int right;
        public int bottom;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal class POINT
    {
        public int x;
        public int y;
    }

    [ComImport, Guid("3050F3F0-98B5-11CF-BB82-00AA00BDCE0B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface ICustomDoc
    {
        [PreserveSig]
        int SetUIHandler(IDocHostUIHandler pUIHandler);
    }

    [ComImport, Guid("6D5140C1-7436-11CE-8034-00AA006009FA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IOleServiceProvider
    {
        [PreserveSig]
        uint QueryService([In] ref Guid guidService, [In] ref Guid riid, [MarshalAs(UnmanagedType.IDispatch)] out object ppvObject);
    }
}

[Flags]
public enum HostUIFlags
{
    DIALOG = 0x00000001,
    DISABLE_HELP_MENU = 0x00000002,
    NO3DBORDER = 0x00000004,
    SCROLL_NO = 0x00000008,
    DISABLE_SCRIPT_INACTIVE = 0x00000010,
    OPENNEWWIN = 0x00000020,
    DISABLE_OFFSCREEN = 0x00000040,
    FLAT_SCROLLBAR = 0x00000080,
    DIV_BLOCKDEFAULT = 0x00000100,
    ACTIVATE_CLIENTHIT_ONLY = 0x00000200,
    OVERRIDEBEHAVIORFACTORY = 0x00000400,
    CODEPAGELINKEDFONTS = 0x00000800,
    URL_ENCODING_DISABLE_UTF8 = 0x00001000,
    URL_ENCODING_ENABLE_UTF8 = 0x00002000,
    ENABLE_FORMS_AUTOCOMPLETE = 0x00004000,
    ENABLE_INPLACE_NAVIGATION = 0x00010000,
    IME_ENABLE_RECONVERSION = 0x00020000,
    THEME = 0x00040000,
    NOTHEME = 0x00080000,
    NOPICS = 0x00100000,
    NO3DOUTERBORDER = 0x00200000,
    DISABLE_EDIT_NS_FIXUP = 0x00400000,
    LOCAL_MACHINE_ACCESS_CHECK = 0x00800000,
    DISABLE_UNTRUSTEDPROTOCOL = 0x01000000,
    HOST_NAVIGATES = 0x02000000,
    ENABLE_REDIRECT_NOTIFICATION = 0x04000000,
    USE_WINDOWLESS_SELECTCONTROL = 0x08000000,
    USE_WINDOWED_SELECTCONTROL = 0x10000000,
    ENABLE_ACTIVEX_INACTIVATE_MODE = 0x20000000,
    DPI_AWARE = 0x40000000
}

Ответ 3

В конечном итоге для меня было решение использовать более позднюю версию .NET - 4.6.2, в которой улучшена поддержка DPI, и поэтому эта проблема в приложении, которое я создал, разрешилась при применении настроек High DPI в манифесте приложения.

Если вы ориентируетесь на .NET 4.6.2 или более позднюю версию, масштабирование DPI неявно включено. Тебе больше ничего не нужно.

Если вы нацелены на более ранние версии, либо добавьте в манифест:

<application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    </windowsSettings>
</application>

и включите функцию DPI Awareness в своем Properties.cs:

[assembly: DisableDpiAwareness]

или (как я делал по разным причинам) с помощью кода, который должен быть вызван из app.xaml.cs:

    public static bool SetPerMonitorDpiAwareness(ProcessDpiAwareness type = ProcessDpiAwareness.Process_Per_Monitor_DPI_Aware)
    {
        try
        {
            // for this to work make sure [assembly: DisableDpiAwareness]
            ProcessDpiAwareness awarenessType;
            GetProcessDpiAwareness(Process.GetCurrentProcess().Handle, out awarenessType);
            var result = SetProcessDpiAwareness(type);
            GetProcessDpiAwareness(Process.GetCurrentProcess().Handle, out awarenessType);

            return awarenessType == type;
        }
        catch
        {
            return false;
        }            
    }

Чтобы позвонить куда-нибудь в коде запуска App.xaml.cs:

        try
        {   // Multi-Monitor DPI awareness for screen captures
            // requires [assembly: DisableDpiAwareness] set in assemblyinfo
            bool res = WindowUtilities.SetPerMonitorDpiAwareness(ProcessDpiAwareness.Process_Per_Monitor_DPI_Aware);
        }
        catch {  /* fails not supported on Windows 7 and older */ }

Опять же, все это больше не нужно после нацеливания на .NET 4.6.2 или более позднюю версию, и все просто работает. Явный код позволил лучше контролировать, какой именно профиль использовать.

В .NET 4.6.2 представлен ряд улучшений с масштабированием DPI, включая поддержку масштабирования нескольких мониторов (ранее поддерживался только основной монитор), и он автоматически работает правильно с большинством размещенных элементов управления, включая элемент управления веб-браузера. Учитывая, что большинство компьютеров в настоящее время используют либо .NET 4.7.x, либо 4.6.2 на основе обновления Windows с целью 4.6.2, следует рассматривать их как базовый уровень для WPF IMHO.

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

Ответ 4

Вам необходимо определить процент увеличения в браузере по dpi ОС, а затем установить масштаб в браузере ActiveX. К сожалению, оболочка не отображается в WPF, поэтому вам нужно добавить ссылку на "Microsoft Internet Controls" и использовать ее для отражения.

Private Sub Browser_LoadCompleted(sender As Object, e As NavigationEventArgs)
    Dim source = PresentationSource.FromDependencyObject(sender)
    Dim matrix As Matrix = source.CompositionTarget.TransformToDevice
    If matrix.Determinant > 1 Then
        Dim zoomLevel As Integer = matrix.Determinant * 100
        Dim ie As SHDocVw.InternetExplorer = GetType(WebBrowser).GetField("_axIWebBrowser2", BindingFlags.Instance Or BindingFlags.NonPublic).GetValue(sender)
        ie.ExecWB(SHDocVw.OLECMDID.OLECMDID_OPTICAL_ZOOM, SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER, zoomLevel, IntPtr.Zero)
    End If
End Sub

Дополнительное чтение:

https://dzimchuk.net/best-way-to-get-dpi-value-in-wpf/

https://weblog.west-wind.com/posts/2016/Aug/22/Detecting-and-Setting-Zoom-Level-in-the-WPF-WebBrowser-Control