WPF Web Browser Control和DPI缩放

17

我正在使用一个使用Web浏览器控件的WPF应用程序,我在高DPI缩放方面遇到了问题。

似乎Web浏览器控件没有正确地尊重系统的DPI设置,而WPF应用程序的其余部分可以正确地缩放UI。这意味着在更高的比例水平上,WPF界面变得更大,而Web浏览器内容保持原来的大小,看起来更小了。

这里是一个使用两个Web浏览器控件的WPF应用程序的屏幕截图示例。

100%缩放:

enter image description here

150%缩放:

enter image description here

请注意,第二张图片中Web浏览器的缩放比第一张图片要小得多,相对于主窗体内容(工具栏/菜单/状态栏)。

是否有一种方法可以强制Web浏览器控件以继承自应用程序的高DPI设置适当地使用它们?

这个MSDN链接: 解决DPI问题

显示了一种非常底层的方法(在文档底部),实现自定义Web浏览器COM接口,但我想知道是否有更简洁的方法来解决这个问题。


将Web浏览器包装在ViewBox中是否有帮助? - Ben Steele
1
@BenSteele WebBrowser控件可能已经在Viewbox中。Viewbox会使WebBrowser变大,但它不会缩放网页内容以匹配大小。 - Kyle Delaney
4个回答

17
我找到了我认为是实现所需功能的最佳方法 (假设您无论如何需要在注册表中指定FEATURE_BROWSER_EMULATION才能使用最新版本的IE)
你需要做的就是在HKCU\Software\Microsoft\Internet Explorer\Main\FeatureControl中创建一个名为FEATURE_96DPI_PIXEL的新键,并将应用程序 exe 作为键名添加到其中,并将类型设置为DWORD (32-bit),数值为1
在实例化WebBrowser组件之前,在应用程序启动时检查设置,一切都应该没问题。
原始帖子(包含其他可能的功能): https://www.reddit.com/r/dotnet/comments/2j5m6m/wpf_webbrowser_control_alternatives/

根据MSDN的说明,该功能已经过时,不应再使用。 - Funbit
@Funbit 是的,没错。但我怀疑在实际删除IE之前,他们不会将它删除掉。而且这是唯一一个真正有效并且让我花费了近两天时间来解决的解决方案。 - Miro Hudak

6
这是一个实用工具类的代码,可以让你禁用WPF的WebBrowser上下文菜单。它还允许你抑制脚本错误(WPF WebBrowser控件-如何抑制脚本错误?)并更改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
}

2
我有点困惑。脚本错误和上下文菜单与 DPI 缩放有什么关系? - Kyle Delaney
@KyleDelaney 实际上这已经很老了,我也不记得为什么它在过去的同样问题中有所帮助。 - gmetax
2
如果我理解正确,上面的代码允许您设置WebBrowser控件的DPI_AWARE标志,以及任何其他“隐藏”的标志。它还允许您停用上下文菜单并抑制脚本错误,但这些不是相关的内容。 - karfus

1
我的解决方案是使用较新版本的.NET - 4.6.2具有改进的DPI支持,因此当应用程序清单中应用高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>

在你的AssemblyInfo.cs中启用DPI感知:
[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缩放方面的改进,包括支持多监视器缩放(以前只支持主监视器),并自动处理大多数托管控件,包括Web浏览器控件。 鉴于今天大多数计算机都基于Windows更新的.NET 4.7.x或4.6.2,因此应该将针对4.6.2视为WPF的基线。
注意:如果在您的应用程序运行时在Windows中切换DPI设置,则还有事件可以捕获,告诉您DPI更改的情况。虽然应用程序实际上不会接受更改,但至少可以让用户知道DPI已更改,并且他们必须重新启动以调整到新的DPI设置。

2
似乎“在针对.NET 4.6.2或更高版本后,一切都能正常工作”的说法并不正确。我在.NET 4.7上的VSTO中有一个WPF WebBrowser,它仍然存在这些问题(而且我无法更改进程DPI感知度,因为Office在运行插件之前设置了它)。有什么想法吗? - Keith
@rick 你是不是指的是 "AssemblyInfo.cs" 而不是 "Properties.cs"? - Sergioet

0
你需要从操作系统的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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接