0

We have a WinForms app for the classic .NET Framework 4.7. One of the forms contains an instance of the WebBrowser control to render HTML documents. It turned out that if someone changes the Text Size option in Microsoft Help Viewer in Visual Studio

Text Size option in Microsoft Help Viewer

, this also affects the size of text in our WebBrowser-based viewer.

I suspect it is a global setting for the MSIE rendering engine the WebBrowser control is based on. As such, there can be other apps in which the user may change this setting and this will affect our app.

Is there a way to completely ignore this setting while rendering our HTMLs? We tried to specify explicit text sizes for the HTML tags in our HTML pages, but it seems, this does not help. It looks like the MSIE HTML renderer applies its scale factor after pages were rendered using the specified text sizes.

If it is not possible to ignore that global setting, is there an API we can use to control this Text Size parameter from our app? We could use it to provide settings like Microsoft Help Viewer does, and at least could provide the user with a similar choice list to adjust text size for comfort reading.

If possible, provide a solution in C# or VB.NET.

TecMan
  • 2,743
  • 2
  • 30
  • 64
  • It should be recorded in `HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Zoom`. IIRC, If you set both `ResetTextSizeOnStartup` and `ResetTextSizeOnStartup2` to `1`. Both Zoom and Text size will be reset to default when the WebBrowser is restarted. The first setting should be related to the Text size, the other to the Zoom value. – Jimi May 28 '21 at 14:49
  • @Jimi, I found another thing while playing with a tool recording registry changes. We need the REG_BINARY IEFontSize parameter of the key `HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\International\Scripts\3`. Google search confirmed [that](https://techgenix.com/controlinternetexplorerandwindowshelpfontsize/). – TecMan May 28 '21 at 14:54
  • Yes, but that entry is the one that's reset when you set `ResetTextSizeOnStartup = 0x0001`. It's then reset to `0x0002` (Normal). -- So, you can just add a MenuItem that allows to reset default values, or just ignore default settings and reset to normal view. – Jimi May 28 '21 at 15:12
  • @Jimi, but actually only `IEFontSize` specifies the effective value, so we can read/write only it. Microsoft Help Viewer does not touch the Zoom hive according my registry change records, so I think `IEFontSize` is the only parameter we need. Even if someone will reset it with `ResetTextSizeOnStartup` and `ResetTextSizeOnStartup2`, we still can use the default `IEFontSize` value without any problems. Right? – TecMan May 28 '21 at 15:38
  • That's up to you. `IEFontSize` stores a generic scale (1 to 4), the `ZoomFactor` entry a value that scales the whole viewport. You can implement the complete functionality (giving means to set both -- ZoomFactor is `100000` for 100%, `125000` for 125% etc.), or ignore both and reset to default when a WebBrowser session is started. In any case, you'll override general User settings from within your app. -- It looks like you only refer to Help Viewer, but a User can also change the Zoom level in the Internet Explorer preferences, so I assume you want to handle both (or reset both). – Jimi May 28 '21 at 15:53
  • @Jimi, another part of the problem. If we change `IEFontSize`, the current instance of WebBrowser doesn't use the new setting when we call its `Refresh` method. We need to recreate the control to use the new text size value, but we lose the navigation history at that. However, MS Help Viewer uses the updated text size value when we hit F5 to update the current topic, and the navigation history remains untouched. How to do this in our software? – TecMan May 29 '21 at 06:53

2 Answers2

1

An example about synchronizing the WebBrowser Control's viewport scale, both Zoom and Text Size, to the settings applied by other applications (e.g., Internet Explorer or, as in this case, Help Viewer).

The Problem:

  • When Zoom a FontSize scale options are applied by an application that uses the Internet Explorer Engine to render HTML content, the WebBrowser Control, when first initialized, applies these settings to its view.

Expected Behavior:

  • The Application should provide means to change Zoom and FontSize scale, independently of what other applications may have set.
  • The new settings should be applied without recreating the Control's Handle

In the sample code, a specialized class, WebBrowserHelper, contains a method that reads from the Registry the current Zoom and FontSize settings, to update the MenuItems to the current value:

HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Zoom
=> ZoomFactor Key

HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\International\Scripts\3
=> IEFontSize Key

The ZoomFactor Key stores Zoom levels multiplied by 1000: e.g., 150% Zoom has a value of 150000, 75% Zoom has a value of 75000

The WebBrowserHelper's SetZoom() method uses the WebBrowser ActiveX instance to set the Zoom level, calling its ExecWb method, passing as OLECMDID argument the OLECMDID_OPTICAL_ZOOM value, the OLECMDEXECOPT argument is set to OLECMDEXECOPT_DONTPROMPTUSER and the pvaIn argument (the Zoom value) is set to the Integer value of the Zoom level specified.

▶ This also updates the ZoomFactor Registry Key.

public static void SetZoom(WebBrowser browser, int zoomValue)
{
    dynamic activex = browser.ActiveXInstance;
    activex.ExecWB(OLECMDID_OPTICAL_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, zoomValue, 0);
}

The IEFontSize and IEFontSizePrivate Keys store a binary value, in the rage [0-4]. These values loosely correspond to the x-small, small, medium, large, x-large CSS settings.
The FontSize scale is applied to the Document Body, using a multiplier: [Value + 1] * 4.

The SetTextScale() method uses the WebBrowser ActiveX instance to set the FonSize scale, calling its ExecWb() method, passing as OLECMDID argument the OLECMDID_ZOOM value, the OLECMDEXECOPT argument is set to OLECMDEXECOPT_DONTPROMPTUSER and the pvaIn argument (the Font scale value) is set to an Integer value in the range [0, 4] as specified.

▶ This also updates the IEFontSize Registry Key.

public static void SetTextScale(WebBrowser browser, int textValue)
{
    dynamic activex = browser.ActiveXInstance;
    activex.ExecWB(OLECMDID_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, textValue, 0);
}

Another option is to reset both Zoom and FontSize to default values before a WebBrowser session is started.
You can call the WebBrowserHelper.InternetResetZoomAndFont() method. Setting both arguments to true, it will reset the Zoom level to 100% and FontSize to Medium.

WebBrowserHelper.InternetResetZoomAndFont(true, true);

▶ Here, I'm adding some ToolStripMenuItems that allow to set the Zoom value and FontSize - which is set to a pre-defined scale - as applied by Internet Explorer 11.

  • All ToolStripMenuItem that sets the Zoom are grouped in a collection, named zoomMenuItems. All use the same Click event handler, zoomToolStripMenuItems_Click
  • All ToolStripMenuItem that sets the FontSize scale are grouped in a collection named textMenuItems. All use the same Click event handler, textToolStripMenuItems_Click
  • The Zoom and Fontscale values are set to the Tag property of each ToolStripMenuItem (you can of course use any other means to associate a value to a specific ToolStripMenuItem)
  • The current values of Zoom and FontSize as stored in a named Value Tuple, browserViewSettings

Assume the WebBrowser Control is named webBrowser1:

public partial class SomeForm : Form
{
    private List<ToolStripMenuItem> textMenuItems = null;
    private List<ToolStripMenuItem> zoomMenuItems = null;
    private (int Zoom, int TextSize) browserViewSettings = (100, 2);

    public SomeForm()
    {
        InitializeComponent();

        textMenuItems = new List<ToolStripMenuItem> { textSmallestMenuItem, textSmallerMenuItem, textMediumMenuItem, textLargerMenuItem, textLargestMenuItem };
        zoomMenuItems = new List<ToolStripMenuItem> { zoom75MenuItem, zoom100MenuItem, zoom125MenuItem, zoom150MenuItem, zoom175MenuItem, zoom200MenuItem, zoom250MenuItem, zoom300MenuItem, zoom400MenuItem };
        // On startup, reads the current settings from the Registry
        // and updates the MenuItems, to reflect the current values
        UpdateMenus(true);
    }

    // MenuItems that sets the Font scale value
    private void textToolStripMenuItems_Click(object sender, EventArgs e)
    {
        var item = sender as ToolStripMenuItem;
        int newTextValue = Convert.ToInt32(item.Tag);
        if (newTextValue != browserViewSettings.TextSize) {
            browserViewSettings.TextSize = newTextValue;
            UpdateDocumentSettings(this.webBrowser1);
            UpdateMenus(false);
        }
    }

    // MenuItems that sets the Zoom level value
    private void zoomToolStripMenuItems_Click(object sender, EventArgs e)
    {
        var item = sender as ToolStripMenuItem;
        int newZoomValue = Convert.ToInt32(item.Tag);
        if (newZoomValue != browserViewSettings.Zoom) {
            browserViewSettings.Zoom = newZoomValue;
            UpdateDocumentSettings(this.webBrowser1);
            UpdateMenus(false);
        }
    }

    // Sets the new selected values and the related MenuItem
    private void UpdateDocumentSettings(WebBrowser browser)
    {
        if (browser == null || browser.Document == null) return;
        WebBrowserFeatures.WebBrowserHelper.SetZoom(browser, browserViewSettings.Zoom);
        WebBrowserFeatures.WebBrowserHelper.SetTextScale(browser, browserViewSettings.TextSize);
    }

    // Updates the MenuItem to the current values 
    private void UpdateMenus(bool refresh)
    {
        if (refresh) {
            browserViewSettings = WebBrowserFeatures.WebBrowserHelper.InternetGetViewScale();
        }
        
        zoomMenuItems.ForEach(itm => {
            int refValue = Convert.ToInt32(itm.Tag);
            itm.Checked = refValue == browserViewSettings.Zoom;
            if (itm.Checked) {
                zoomToolStripMenuItem.Text = $"Zoom ({refValue}%)";
            }
        });
        textMenuItems.ForEach(itm => itm.Checked = Convert.ToInt32(itm.Tag) == browserViewSettings.TextSize);
    }

WebBrowserHelper class:

using System.Security;
using System.Security.AccessControl;
using Microsoft.Win32;

public class WebBrowserHelper
{
    private const int OLECMDID_ZOOM = 19;
    private const int OLECMDID_OPTICAL_ZOOM = 63;
    private const int OLECMDEXECOPT_DONTPROMPTUSER = 2;

    // Applies the Zoom value. It also updates the Registry
    public void SetZoom(WebBrowser browser, int zoomValue)
    {
        dynamic activex = browser.ActiveXInstance;
        activex.ExecWB(OLECMDID_OPTICAL_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, zoomValue, 0);
    }

    // Applies the FontSize scale. It also updates the Registry
    public void SetTextScale(WebBrowser browser, int textValue)
    {
        dynamic activex = browser.ActiveXInstance;
        activex.ExecWB(OLECMDID_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, textValue, 0);
    }

    private static string keyZoomName = @"Software\Microsoft\Internet Explorer\Zoom";
    private static string keyTextName = @"Software\Microsoft\Internet Explorer\International\Scripts\3";
    private static string keyValueTextReset = "ResetZoomOnStartup";
    private static string keyValueZoomReset = "ResetZoomOnStartup2";

    public static  (int Zoom, int TextSize) InternetGetViewScale()
    {
        int zoomValue, textValue;
        using (var zoomKey = Registry.CurrentUser.OpenSubKey(keyZoomName,
            RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey)) {
            zoomValue = (int)zoomKey.GetValue("ZoomFactor", 100000) / 1000;
        }
      
        using (var textKey = Registry.CurrentUser.OpenSubKey(keyTextName,
            RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey)) {
            var keyBValue = BitConverter.GetBytes(2);
            textValue = BitConverter.ToInt32((byte[])textKey.GetValue("IEFontSize", keyBValue), 0);
        }
        return (zoomValue, textValue);
    }

    public static void InternetResetZoomAndFont(bool resetZoom, bool resetFontSize)
    {
        int keyZoomValue = resetZoom ? 1 : 0;
        int keyFontValue = resetFontSize ? 1 : 0;
        using (var zoomKey = Registry.CurrentUser.OpenSubKey(keyZoomName,
            RegistryKeyPermissionCheck.ReadWriteSubTree,
            RegistryRights.WriteKey)) {
            zoomKey.SetValue(keyValueZoomReset, keyZoomValue, RegistryValueKind.DWord);
            zoomKey.SetValue(keyValueTextReset, keyFontValue, RegistryValueKind.DWord);
        }
        var current = InternetGetViewScale();
        if (resetZoom) current.Zoom = 100;
        if (resetFontSize) current.TextSize = 2;
        InternetSetViewScale(current.Zoom, current.TextSize);
    }
}

This is how it works:

WebBrowser control Zoom and FontSize

Jimi
  • 29,621
  • 8
  • 43
  • 61
  • Thank you for your detailed answer!! However, what we need is the `OLECMDID_ZOOM` parameter. See my answer. – TecMan May 31 '21 at 14:55
  • I just didn't understand this: "The WebBrowser Control should be able to refresh itself when Zoom and FontSize scale are changed, both by the current application or when a third-party application applies new settings". It looks like you wanted to provide an event or a callback method in your WebBrowserHelper class to provide notifications about outer change of the related registry setting, right? – TecMan May 31 '21 at 14:57
  • Yes, there are a number of things here, more that it's actually necessary: I wasn't sure what you wanted to actually do, so I posted a lot of different methods to do different things. -- BTW, if you want to apply `OLECMDID_ZOOM`, you can do as shown here, in relation to `OLECMDID_OPTICAL_ZOOM`: `dynamic activex = browser.ActiveXInstance; activex.ExecWB(OLECMDID_ZOOM, OLECMDEXECOPT_DONTPROMPTUSER, textValue, 0);`. That's all, you don't need `IOleCommandTarget`, marshalling and other COM stuff. -- The registry things are there in case it's needed, these methods update the Registry on their own. – Jimi May 31 '21 at 15:36
  • RegistryKey change notifications are not handled here, just navigating to another page or pressing F5 to refresh the current page applies the new values. I can add a method that handles `RegNotifyChangeKeyValue` -- Anyway, I'll update the code to reflect what I think you want to do. As mentioned, you cannot handle just the FontSize change, you have to also handle the Zoom change. – Jimi May 31 '21 at 15:39
  • All right, I've updated the whole thing, to reflect what I think you want in the end. The Text scale is applied using `OLECMDID_ZOOM` (it's the same thing as before, just uses the automated process, as `OLECMDID_OPTICAL_ZOOM` was doing before): see the modified `SetTextScale()` method (which replaces all the code you have posted and is safer). – Jimi May 31 '21 at 16:47
  • If you want to receive notifications when the relevant Registry Keys are changed by another application, let me know. – Jimi May 31 '21 at 16:54
  • Thank you very much for doing all this for us! You understand our needs correctly. Some remarks. 1) Our tests show that the Zoom setting in IE does not affect our WebBrowser at all, so we think that `OLECMDID_ZOOM` is enough. 2) Yes, you can show how to process the registry key changes the right way. – TecMan Jun 01 '21 at 05:24
0

Here is the setter of the TextSize property of our enhanced WebBrowser control that does what we need:

set
{
    IOleCommandTarget m_WBOleCommandTarget = GetOleCommandTarget();
    if (m_WBOleCommandTarget != null)
    {
        if (((int)value > (int)-1) && ((int)value < (int)5))
        {
            IntPtr pRet = m_NullPointer;
            try
            {
                pRet = Marshal.AllocCoTaskMem((int)1024);
                Marshal.GetNativeVariantForObject((int)value, pRet);

                int hr = m_WBOleCommandTarget.Exec(m_NullPointer,
                    (uint)OLECMDID.OLECMDID_ZOOM,
                    (uint)OLECMDEXECOPT.OLECMDEXECOPT_DONTPROMPTUSER,
                    pRet, m_NullPointer);
                Marshal.FreeCoTaskMem(pRet);
                pRet = m_NullPointer;
                if (hr == Hresults.S_OK)
                    m_enumTextSize = (TopicTextSize)value;
            }
            catch (Exception)
            {
            }
            finally
            {
                if (pRet != m_NullPointer)
                    Marshal.FreeCoTaskMem(pRet);
            }
        }
    }
}

public enum OLECMDID
{
    ...
    OLECMDID_ZOOM = 19,
    ...
}

public IOleCommandTarget GetOleCommandTarget()
{
    dynamic ax = this.ActiveXInstance;

    // IHtmlDocument2 also implements IOleCommandTarget
    var qi = (IOleCommandTarget)ax.Document;
    return qi;
}

We use the OLECMDID_ZOOM parameter to work with the IEFontSize registry setting.

The source code for our solution was found in the following CodeProject article:

The most complete C# Webbrowser wrapper control

It contains other code snippets that can be helpful in other situations.

TecMan
  • 2,743
  • 2
  • 30
  • 64