Area for check mark in WinForms ContextMenuStrip is narrow on high dpi

Viewed 175

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?

2 Answers

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;

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;
            }
        }
    }
}
Related