mardi 12 mai 2015

Creating a MenuItem with a Shortcut of Control+Plus

I'm using the legacy WinForms MenuItem control in an application, and would like to implement zoom in and zoom out menu items (with Control++ and Control+- keyboard shortcuts). MenuItem does have a Shortcut property, of type Shortcut, but that doesn't have a CtrlPlus option.

I decided to look at how 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 enum values (such as CtrlA being just Keys.Control + Keys.A). So I tried creating a custom Shortcut value that should be equal to Control+Plus:

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

zoomInMenuItem.Shortcut = CONTROL_PLUS;

However, this throws an InvalidEnumArgumentException when I try to assign the Shortcut property.

So I've decided to use reflection, and modify the (not public) MenuItemData's shortcut property and then call the (non-public) UpdateMenuItem method. This actually works (with a side effect of displaying as Control+Oemplus in the menu item):

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

While that method works, it uses reflection, and I'm not sure if it's future-proof.

I'm using MenuItem and not the newer ToolStripMenuItem because I need to have the RadioCheck property (among other reasons); switching away from that is not an option.

A form that has 2 menu items: * Zoom in (Ctrl+Oemplus) * Zoom out (Ctrl+OemMinus)

Here's some full code that creates the above dialog, which shows what I'm trying to accomplish (the most relevant code is in the OnLoad method):

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://ift.tt/1cOu2le)
            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;
    }
}

The above code works and does what I want, but I'm not sure if it is the right way to do it (using reflection to modify a private variable generally seems like the incorrect method). My questions are:

  • Is there a better way to set a MenuItem's shortcut to Control++?
  • Is this reflection-based method going to cause issues?




Aucun commentaire:

Enregistrer un commentaire