2

We have a WinForms app for .NET Framework 4.7.2. The main form contains a toolbar based on the ToolStrip component. One of the buttons displays a drop-down menu based on the ContextMenuStrip component. The whole construction looks like this:

private ToolStrip MainToolStrip;
private ToolStripDropDownButton TextSizeButton;
private ContextMenuStrip TextSizeMenu;

MainToolStrip = new ToolStrip();
TextSizeButton = new ToolStripDropDownButton();
TextSizeMenu = new ContextMenuStrip();

MainToolStrip.Items.AddRange(new ToolStripItem[] {..., TextSizeButton, ...});

TextSizeButton.DropDown = TextSizeMenu;

High dpi support is enabled in the app using the standard <System.Windows.Forms.ApplicationConfigurationSection> element in the app config file (according to this Microsoft guide).

This drop-down menu looks good on a normal 96dpi screen, but the picture is not so good on a high-res screen because the width of the gray area for check mark is not enough:

enter image description here

How to fix this issue?

TecMan
  • 2,743
  • 2
  • 30
  • 64

2 Answers2

1

To avoid any further incompatibilities and side effects related to the standard implementation of check marks in ContextMenuStrip, we decided to simply use our own image as the check mark:

enter image description here enter image description here

It looks even cooler than the default one because the default implementation draws the selection rectangle around the check mark (why?!):

enter image description here

The core part of code that switches the check state is the following:

private Bitmap ImageCheck = CreateToolButtonResBitmap("check.png");
private Bitmap ImageEmpty = CreateToolButtonResBitmap("10tec-empty.png");

private void SetMenuItemChecked(ToolStripMenuItem item, bool check)
{
    if (check)
        item.Image = ImageCheck;
    else
        item.Image = ImageEmpty;
}

And we also remove the unneeded gray area at the left of ContextMenuStrip for better effect:

internal class DropDownToolbarRenderer : ToolStripProfessionalRenderer
{
    protected override void OnRenderImageMargin(ToolStripRenderEventArgs e)
    {
    }
}

private static DropDownToolbarRenderer fDropDownToolbarRenderer = new DropDownToolbarRenderer();

TextSizeMenu.Renderer = fDropDownToolbarRenderer;
TecMan
  • 2,743
  • 2
  • 30
  • 64
0

It is difficult to specifically set the width of the image/check margin. When I looked at the .NET source code, it seemed like there were two options. Option 1) One of the ToolStripMenuItem objects has to have a dummy image (e.g. height = 1 pixel, width = desired margin width), so that the image would force the margin to be the desired width. The disadvantage of this approach is that if the Font size is changed then it requires generating a new dummy image. Option 2) use reflection to access certain private fields, not ideal, but this was the easier of the two options. Here is some code for option #2:

//</summary>Keeps the checkboxes square dimensions. The image margin is at least the height, or the width of the widest ToolStripItem image.</summary>
private class ContextMenuStrip2 : ContextMenuStrip {
    private static FieldInfo fieldScaledDefaultImageMarginWidth = null;
    private static FieldInfo fieldScaledDefaultImageSize = null;
    private static bool scaledFields = false;
    static ContextMenuStrip2() {
        try {
            Type ty = typeof(ToolStripDropDownMenu);
            fieldScaledDefaultImageMarginWidth = ty.GetField("scaledDefaultImageMarginWidth", BindingFlags.Instance | BindingFlags.NonPublic);
            fieldScaledDefaultImageSize = ty.GetField("scaledDefaultImageSize", BindingFlags.Instance | BindingFlags.NonPublic);
            scaledFields = (fieldScaledDefaultImageMarginWidth != null && fieldScaledDefaultImageSize != null);
        } catch {}
    }


    private int currentImageMarginWidth = -1;
    private bool isAdjusting = false;

    public ContextMenuStrip2() : base() {
    }

    protected override void OnLayout(LayoutEventArgs e) {
        base.OnLayout(e);

        if (!isAdjusting && scaledFields && this.Items.Count > 0) {
            int wMax = 0;
            foreach (ToolStripItem i in Items) {
                int h = i.Height + 3;
                if (h > wMax)
                    wMax = h;
                if (i.Image != null) {
                    int w = i.Image.Width + 4;
                    if (w > wMax)
                        wMax = w;
                }
            }

            if (wMax != currentImageMarginWidth) {
                currentImageMarginWidth = wMax;
                fieldScaledDefaultImageMarginWidth.SetValue(this, wMax);
                fieldScaledDefaultImageSize.SetValue(this, new Size(1000, 0)); // must do this to cancel out extraImageWidth in CalculateInternalLayoutMetrics()
                isAdjusting = true;
                ResumeLayout(true);
                isAdjusting = false;
            }
        }
    }
}
Loathing
  • 5,109
  • 3
  • 24
  • 35
  • You can override [ToolStripProfessionalRenderer.OnRenderImageMargin()](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.toolstripprofessionalrenderer.onrenderimagemargin) and [OnRenderItemImage()](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.toolstripprofessionalrenderer.onrenderitemimage) or [OnRenderItemCheck()](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.toolstripprofessionalrenderer.onrenderitemcheck) -- Or make the app scale on Dpi instead of Font. Or both. – Jimi Jun 01 '21 at 20:14
  • @jimi That doesn't change the width of the image margin. – Loathing Jun 01 '21 at 20:23
  • You can render the Image or check symbol where you want. The menu items can also be moved, as shown [here](https://stackoverflow.com/a/64130479/7444103). You can render the entire menu as you please. – Jimi Jun 01 '21 at 20:26
  • There's also the `Toolstrip.DisableHighDpiImprovements` setting, in app.config. It's not clear what options the OP has chosen, or modified. – Jimi Jun 01 '21 at 20:37
  • When you override `OnRenderImageMargin()`, you can create a new `ToolStripRenderEventArgs` object (it's not sealed), inflate the `e.AffectedBounds` rectangle, pass the new Rectangle to the `ToolStripRenderEventArgs` Constructor and call `base.OnRenderImageMargin(newArgs);` with the new object (last comment, I promise :). – Jimi Jun 01 '21 at 20:45
  • @jimi Calling `base.OnRenderImageMargin(newArgs);` doesn't increase the width of the image margin, but draws over the text portion. You are conflating rendering and layout. – Loathing Jun 01 '21 at 21:05
  • Nope. I'm ignoring default Layout and rendering stuff where I want it to be using my measures. As mentioned (and described and shown in the post I linked), you can also move the Text of the same measure. That's why you handle a custom `ToolStripProfessionalRenderer`. – Jimi Jun 01 '21 at 21:15