WinForms ContextMenuStrip 中的复选标记区域在高 dpi 时很窄

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

我们有一个适用于 .NET Framework 4.7.2 的 WinForms 应用程序。主窗体包含一个基于 ToolStrip component. One of the buttons displays a drop-down menu based on the ContextMenuStrip 组件的工具栏。整个结构如下所示:

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;

使用应用配置文件中的标准 <System.Windows.Forms.ApplicationConfigurationSection> 元素(根据 this Microsoft guide)在应用中启用高 dpi 支持。

这个drop-down菜单在96dpi的普通屏幕上看起来还不错,但是在high-res屏幕上画面就不太好了,因为勾选标记的灰色区域宽度不够:

如何解决这个问题?

很难具体设置 image/check 边距的宽度。当我查看 .NET 源代码时,似乎有两种选择。选项 1) ToolStripMenuItem 对象之一必须具有虚拟图像(例如,高度 = 1 像素,宽度 = 所需边距宽度),以便图像将边距强制为所需宽度。这种方法的缺点是,如果 Font 大小发生变化,则需要生成新的虚拟图像。选项 2) 使用反射访问某些 private 字段,并不理想,但这是两个选项中更容易的一个。这是选项 #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;
            }
        }
    }
}

为了避免与 ContextMenuStrip 中复选标记的标准实施相关的任何进一步的不兼容性和副作用,我们决定简单地使用我们自己的图像作为复选标记:

它看起来比默认的更酷,因为默认实现在复选标记周围绘制选择矩形(为什么?!):

切换检查状态的核心部分代码如下:

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

为了更好的效果,我们还去掉了ContextMenuStrip左边不需要的灰色区域:

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

private static DropDownToolbarRenderer fDropDownToolbarRenderer = new DropDownToolbarRenderer();

TextSizeMenu.Renderer = fDropDownToolbarRenderer;