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

Pok*_*u22 10 .net c# reflection menuitem winforms

我在应用程序中使用传统MainMenu控件(带MenuItems)控件,并希望实现放大和缩小菜单项(使用Control+ +Control+ -键盘快捷键).(注意我正在使用MainMenu而不是MenuStrip).MenuItem确实有一个Shortcut类型的属性,Shortcut但没有CtrlPlus选项.

我决定看一下在Shortcutreferencesource中是如何实现的,看起来每个枚举值的方式只是几个Keys枚举值的组合(例如CtrlA只是Keys.Control + Keys.A).所以我尝试创建一个自定义的快捷方式值,该值应该等于Control + Plus:

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

zoomInMenuItem.Shortcut = CONTROL_PLUS;
Run Code Online (Sandbox Code Playgroud)

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

所以我决定使用反射,修改(不公开)MenuItemDatashortcut属性,然后调用(非公共)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 });
Run Code Online (Sandbox Code Playgroud)

虽然该方法有效,但它使用反射,我不确定它是否适合未来.

我正在使用MenuItem而不是更新,ToolStripMenuItem因为我需要拥有RadioCheck属性(以及其他原因); 切换远离那是不可取的.

包含2个菜单项的表单:*放大(Ctrl + Oemplus)*缩小(Ctrl + OemMinus)

这是一些创建上述对话框的完整代码,它显示了我正在尝试完成的内容(方法中最相关的代码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 (http://stackoverflow.com/q/28461091/3991344)
            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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 有没有更好的方法将MenuItem的快捷方式设置为Control+ +
  • 这种基于反射的方法会导致问题吗?

Han*_*ant 8

它使用反射,我不确定它是否适合未来

你会逃脱它,没有什么特别危险的事情发生在引擎盖下.MainMenu/MenuItem类是一成不变的,永远不会改变.对于Windows版本,您是面向未来的,这种快捷行为实际上并未由Windows实现,而是已添加到MenuItem中.它实际上是使其工作的Form.ProcessCmdKey()方法.在没有黑客攻击菜单项的情况下,这是你的提示.将此代码粘贴到表单类中:

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);
}
Run Code Online (Sandbox Code Playgroud)

你得到的快捷键描述是gawdawful,没有正常的人知道"Oemplus"可能意味着什么.不要使用自动生成的,自己编写.您无法使用设计器执行此操作,也不会让您在项目文本和快捷键描述之间输入制表符.但代码没问题:

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

我正在使用MenuItem而不是更新的ToolStripMenuItem,因为......

这不是一个很好的理由.ToolStripMenuItem确实没有为radiobutton行为提供开箱即用的实现,但是很容易添加自己.Winforms使您可以轻松创建自己的ToolStrip项目类,这些项目在设计时可用,并且可以在运行时选择.在项目中添加一个新类并粘贴下面显示的代码.编译.在设计时使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)