使用 Control+Plus 的快捷方式创建 MenuItem – 使用反射修改 MenuItem 的私有字段是最好的方法吗?

Creating a MenuItem with a Shortcut of Control+Plus – is using reflection to modify MenuItem's private fields the best method?

我正在使用旧版 MainMenu control (with MenuItems) control in an application, and would like to implement zoom in and zoom out menu items (with Control++ and Control+- keyboard shortcuts). (Note that I'm using MainMenu and not MenuStrip). MenuItem does have a Shortcut property, of type Shortcut,但它没有 CtrlPlus 选项。

我决定查看 Shortcut was implemented in the referencesource, and it looks like the way that each of those enum values is just a combination of several Keys 枚举值(例如 CtrlA 只是 Keys.Control + Keys.A)。所以我尝试创建一个应该等于 Control+Plus 的自定义快捷方式值:

const Shortcut CONTROL_PLUS = (Shortcut)(Keys.Control | Keys.Oemplus);

zoomInMenuItem.Shortcut = CONTROL_PLUS;

但是,当我尝试分配 Shortcut 属性 时会抛出 InvalidEnumArgumentException

所以我决定使用反射,并修改(不是public)MenuItemData's shortcut property and then call the (non-public) UpdateMenuItem方法。这实际上有效(具有在菜单项中显示为 Control+Oemplus 的副作用):

const Shortcut CONTROL_PLUS = (Shortcut)(Keys.Control | Keys.Oemplus);

var dataField = typeof(MenuItem).GetField("data", BindingFlags.NonPublic | BindingFlags.Instance);
var updateMenuItemMethod = typeof(MenuItem).GetMethod("UpdateMenuItem", BindingFlags.NonPublic | BindingFlags.Instance);
var menuItemDataShortcutField = typeof(MenuItem).GetNestedType("MenuItemData", BindingFlags.NonPublic)
    .GetField("shortcut", BindingFlags.NonPublic | BindingFlags.Instance);

var zoomInData = dataField.GetValue(zoomInMenuItem);
menuItemDataShortcutField.SetValue(zoomInData, CONTROL_PLUS);
updateMenuItemMethod.Invoke(zoomInMenuItem, new object[] { true });

虽然该方法有效,但它使用反射,我不确定它是否面向未来。

我正在使用 MenuItem and not the newer ToolStripMenuItem 因为我需要 RadioCheck 属性(还有其他原因);放弃它不是一种选择。

下面是创建上述对话框的一些完整代码,显示了我要完成的任务(最相关的代码在 OnLoad 方法中):

ZoomForm.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Reflection;

namespace ZoomMenuItemMCVE
{
    public partial class ZoomForm : Form
    {
        private double zoom = 1.0;
        public double Zoom {
            get { return zoom; }
            set {
                zoom = value;
                zoomTextBox.Text = "Zoom: " + zoom;
            }
        }

        public ZoomForm() {
            InitializeComponent();
        }

        protected override void OnLoad(EventArgs e) {
            const Shortcut CONTROL_PLUS = (Shortcut)((int)Keys.Control + (int)Keys.Oemplus);
            const Shortcut CONTROL_MINUS = (Shortcut)((int)Keys.Control + (int)Keys.OemMinus);

            base.OnLoad(e);

            //We set menu later as otherwise the designer goes insane (
            this.Menu = mainMenu;

            var dataField = typeof(MenuItem).GetField("data", BindingFlags.NonPublic | BindingFlags.Instance);
            var updateMenuItemMethod = typeof(MenuItem).GetMethod("UpdateMenuItem", BindingFlags.NonPublic | BindingFlags.Instance);
            var menuItemDataShortcutField = typeof(MenuItem).GetNestedType("MenuItemData", BindingFlags.NonPublic)
                .GetField("shortcut", BindingFlags.NonPublic | BindingFlags.Instance);

            var zoomInData = dataField.GetValue(zoomInMenuItem);
            menuItemDataShortcutField.SetValue(zoomInData, CONTROL_PLUS);
            updateMenuItemMethod.Invoke(zoomInMenuItem, new object[] { true });

            var zoomOutData = dataField.GetValue(zoomOutMenuItem);
            menuItemDataShortcutField.SetValue(zoomOutData, CONTROL_MINUS);
            updateMenuItemMethod.Invoke(zoomOutMenuItem, new object[] { true });
        }

        private void zoomInMenuItem_Click(object sender, EventArgs e) {
            Zoom *= 2;
        }

        private void zoomOutMenuItem_Click(object sender, EventArgs e) {
            Zoom /= 2;
        }
    }
}

ZoomForm.Designer.cs

namespace ZoomMenuItemMCVE
{
    partial class ZoomForm
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing) {
            if (disposing && (components != null)) {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent() {
            this.components = new System.ComponentModel.Container();
            System.Windows.Forms.MenuItem viewMenuItem;
            this.zoomTextBox = new System.Windows.Forms.TextBox();
            this.mainMenu = new System.Windows.Forms.MainMenu(this.components);
            this.zoomInMenuItem = new System.Windows.Forms.MenuItem();
            this.zoomOutMenuItem = new System.Windows.Forms.MenuItem();
            viewMenuItem = new System.Windows.Forms.MenuItem();
            this.SuspendLayout();
            // 
            // zoomTextBox
            // 
            this.zoomTextBox.Dock = System.Windows.Forms.DockStyle.Bottom;
            this.zoomTextBox.Location = new System.Drawing.Point(0, 81);
            this.zoomTextBox.Name = "zoomTextBox";
            this.zoomTextBox.ReadOnly = true;
            this.zoomTextBox.Size = new System.Drawing.Size(292, 20);
            this.zoomTextBox.TabIndex = 0;
            this.zoomTextBox.Text = "Zoom: 1.0";
            // 
            // mainMenu
            // 
            this.mainMenu.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
            viewMenuItem});
            // 
            // viewMenuItem
            // 
            viewMenuItem.Index = 0;
            viewMenuItem.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
            this.zoomInMenuItem,
            this.zoomOutMenuItem});
            viewMenuItem.Text = "View";
            // 
            // zoomInMenuItem
            // 
            this.zoomInMenuItem.Index = 0;
            this.zoomInMenuItem.Text = "Zoom in";
            this.zoomInMenuItem.Click += new System.EventHandler(this.zoomInMenuItem_Click);
            // 
            // zoomOutMenuItem
            // 
            this.zoomOutMenuItem.Index = 1;
            this.zoomOutMenuItem.Text = "Zoom out";
            this.zoomOutMenuItem.Click += new System.EventHandler(this.zoomOutMenuItem_Click);
            // 
            // ZoomForm
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(292, 101);
            this.Controls.Add(this.zoomTextBox);
            this.Name = "ZoomForm";
            this.Text = "ZoomForm";
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.MainMenu mainMenu;
        private System.Windows.Forms.TextBox zoomTextBox;
        private System.Windows.Forms.MenuItem zoomInMenuItem;
        private System.Windows.Forms.MenuItem zoomOutMenuItem;
    }
}

上面的代码可以正常工作,并且做了我想要的,但我不确定这样做是否正确(使用反射修改私有变量通常看起来是不正确的方法)。我的问题是:

不要为菜单项分配快捷键。将 5 个空格和文本 Ctrl + 或 Ctrl - 添加到菜单项文本。

然后使用此代码手动处理快捷方式处理:

private void Form1_KeyDown(object sender, KeyEventArgs e)
{
    if (e.Control && e.KeyCode == Keys.Oemplus)
    { 
    }
    else if (e.Control && e.KeyCode == Keys.OemMinus)
    { 
    }
}

it uses reflection, and I'm not sure if it's future-proof

你会侥幸逃脱的,在幕后并没有发生特别危险的事情。 MainMenu/MenuItem classes 是一成不变的,永远不会再改变。您是面向未来的 Windows 版本,此快捷方式实际上并未由 Windows 实现,但已添加到 MenuItem。实际上是 Form.ProcessCmdKey() 方法使它起作用。这是你在不修改菜单项的情况下做的提示。将此代码粘贴到您的表单中 class:

protected override bool ProcessCmdKey(ref Message msg, Keys keyData) {
    if (keyData == (Keys.Control | Keys.Oemplus)) {
        zoomInMenuItem.PerformClick();
        return true;
    }
    if (keyData == (Keys.Control | Keys.OemMinus)) {
        zoomOutMenuItem.PerformClick();
        return true;
    }
    return base.ProcessCmdKey(ref msg, keyData);
}

你得到的快捷键描述很糟糕,没有正常人知道 "Oemplus" 可能是什么意思。不要使用自动生成的,写你自己的。你不能用设计器做到这一点,它不会让你在项目文本和快捷键描述之间输入制表符。但是代码没有问题:

public ZoomForm() {
    InitializeComponent();
    zoomInMenuItem.Text = "Zoom in\tCtrl +";
    zoomOutMenuItem.Text = "Zoom out\tCtrl -";
}

I'm using MenuItem and not the newer ToolStripMenuItem because...

这不是一个很好的理由。 ToolStripMenuItem 确实没有为单选按钮行为提供开箱即用的实现,但您自己添加它非常容易。 Winforms 使创建您自己的 ToolStrip 项 classes 变得简单,这些 ToolStrip 项可以在设计时使用并且可以按照您在运行时选择的方式运行。向您的项目添加一个新的 class 并粘贴如下所示的代码。编译。在设计时使用 Insert > RadioItem 上下文菜单项插入一个,使用 Edit DropdownItems... 上下文菜单项可以轻松添加多个。您可以设置 Group 属性 来指示哪些项目属于一起并且应该作为一个单选组。

using System;
using System.Windows.Forms;
using System.Windows.Forms.Design;

[ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.MenuStrip | ToolStripItemDesignerAvailability.ContextMenuStrip)]
public class ToolStripRadioItem : ToolStripMenuItem {
    public int Group { get; set; }

    protected override void OnClick(EventArgs e) {
        if (!this.DesignMode) {
            this.Checked = true;
            var parent = this.Owner as ToolStripDropDownMenu;
            if (parent != null) {
                foreach (var item in parent.Items) {
                    var sibling = item as ToolStripRadioItem;
                    if (sibling != null && sibling != this and sibling.Group == this.Group) sibling.Checked = false;
                }
            }
        }
        base.OnClick(e);
    }
}

添加以下 class(例如 "MenuItemEx.cs"):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace System.Windows.Forms
{
    public class MenuItemEx : MenuItem
    {
        public MenuItemEx()
        {

        }

        private Keys myShortcut = Keys.None;
        public new Keys Shortcut
        {
            get { return myShortcut; }
            set { myShortcut = value; UpdateShortcutText(); }
        }

        private string myText = string.Empty;

        public new string Text
        {
            get { return myText; }
            set
            {
                myText = value;
                UpdateShortcutText();
            }
        }

        private void UpdateShortcutText()
        {
            base.Text = myText;

            if (myShortcut != Keys.None)
                base.Text += "\t" + myShortcut.ToString(); // you can adjust that
        }
    }
}

将 "ZoomForm.Designer.cs" 中的每个 MenuItem 替换为 MenuItemEx

将以下代码添加到表单的 OnLoad 中:

zoomInMenuItem.Shortcut = Keys.Control | Keys.Oemplus;
zoomOutMenuItem.Shortcut = Keys.Control | Keys.OemMinus;

然后将以下代码添加到您的表单中 class:

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
    MenuItem item = FindMenuItem(mainMenu.MenuItems, keyData);

    if (item != null)
    {
        item.PerformClick();
        return true;
    }

    return base.ProcessCmdKey(ref msg, keyData);
}

private MenuItem FindMenuItem(Menu.MenuItemCollection collection, Keys shortcut)
{
    foreach (MenuItem item in collection)
    {
        if (item is MenuItemEx && (item as MenuItemEx).Shortcut == shortcut)
            return item;

        MenuItem sub = FindMenuItem(item.MenuItems, shortcut);

        if (sub != null)
            return sub;
    }

    return null;
}

如果需要,您还可以将自己的 属性 用于快捷方式的显示字符串添加到 MenuItemEx class。您还可以在设计器中查看和设置此属性。如果您使用 MenuItemEx.

,您甚至可以获得上述 属性 Shortcut 的新快捷输入对话框