ContextMenuStripに関する各種Tips・その1

2013/3/12・2013/03/15:本文に追記しました
2013/03/22:その2(ContextMenuStripに関する各種Tips・その2(スクロール) - hnx8 開発室)を書きました。
2016/02/03追記:番外記事(ToolStripMenuItemにKeyDownイベントを追加する - hnx8 開発室)を書きました。

C#(dotNetFramework)のContextMenuStripは、挙動におかしな点があったりして、そのままコンテキストメニューやドロップダウンメニューに利用するにはいささか不便な点があります。
今回HNXgrepにプロジェクト機能を盛り込んだ際にいろいろと嵌ったので、ノウハウとして公開します。
.NET Framework 2.0のWindowsForm向けです。最近の4.0/4.5では直ってるorこのノウハウが通用しないかもしれません。

IMEがONだとToolStripMenuItemのAltアクセスキーが効かない

VisualStudio2010なども同じ不具合を抱えていますが、IMEによる日本語入力を有効にしている状態では、ContextMenuStripの各メニュー項目に仕込まれている「終了(&X)」といったAltショートカットキーが反応しません。(フォーカス元のコントロールにてIME変換途中の状態となります)
ContextMenuStripを拡張して、PreProcessMessageにてIME有効時のアクセスキー操作を横取りしてアクセスキー処理を呼び出すことで、解決できます。

class ContextMenuStripEx : ContextMenuStrip
{
    /// <summary>IME有効時、Altアクセスキー操作が効くようにする</summary>
    public override bool PreProcessMessage(ref Message msg)
    {
        const int WM_KEYDOWN = 0x100;
        const int VK_PROCESSKEY = 0xE5; //IME-ON時のキー入力で発生
        if (msg.Msg == WM_KEYDOWN)
        {
            if ((int)msg.WParam == VK_PROCESSKEY)
            {   //IMEへのキー操作をすべてよこどりして対処
                uint key = MapVirtualKey((uint)msg.LParam >> 16 & 0xFFFF, 3);
                this.ProcessDialogChar((char)key);
                return true;
            }
        }
        return base.PreProcessMessage(ref msg);
    }
    [DllImport("user32.dll")]
    static extern uint MapVirtualKey(uint uCode, uint uMapType);

エスケープキーなどでドロップダウンメニューを閉じると、画面全体でAltアクセスキーが効かなくなることがある

ToolStripItem以外の項目にフォーカスがある状態で、ドロップダウンメニューをAltアクセスキーで開き、Escapeキーで閉じると、アクセスキーが効かなくなることがあります。
(Altキーをもう一度押す/カーソル左右キー操作によって閉じた場合は問題ないようです)
これもContextMenuStripのEscapeキー操作を横取りし、自前で閉じることで回避できます。

class ContextMenuStripEx : ContextMenuStrip
{
    /// <summary>Escapeキーで閉じたときにアクセスキーが効かなくなる不具合への対策</summary>
    protected override bool ProcessDialogKey(Keys keyData)
    {   //Escapeで閉じるのは自前で行う
        if (keyData == Keys.Escape)
        {
            this.Close();
            return true;
        }
        return base.ProcessDialogKey(keyData);
    }

(3/15追記)
ListViewなどを右クリックすることで表示されたContextMenuStripについては、カーソル左右キーの操作で閉じた場合にやはりアクセスキーが効かなくなります。
これも自前で閉じなおすことで回避できるようなので、同様に対策を入れてみましょう。以下のようなソースになります。

class ContextMenuStripEx : ContextMenuStrip
{
    /// <summary>Escapeキー/矢印キーで閉じたときにアクセスキーが効かなくなる不具合への対策</summary>
    protected override bool ProcessDialogKey(Keys keyData)
    {
        if (keyData == Keys.Escape)
        {   //Escapeで閉じるのは自前で行う
            this.Close();
            return true;
        }
        bool ret = base.ProcessDialogKey(keyData);
        if (this.Visible == false && (keyData == Keys.Left || keyData == Keys.Right))
        {   //カーソル[←][→]で閉じられた際には、閉じなおす
            this.Close();
            return true;
        }
        return ret;
    }

キー操作でドロップダウンメニューを閉じたとき、ToolStripDropDownButtonにフォーカスが残ったままの見た目になる

ToopStripやStatusStripにドロップダウンボタンを仕込んだ場合に発生します。(そもそもMenuStrip以外のコントロールにドロップダウンボタンを組み込むこと自体が間違ってる気もしますが・・・・)
見た目の問題のみで操作上の実害はないのですが、複数のドロップダウンボタンが同時にフォーカスを持ってるように見えて気持ちが悪いので、以下のように無理やりフォーカスを外すと回避できます。

[ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.ToolStrip | ToolStripItemDesignerAvailability.StatusStrip)]
public class ToolStripDropDownButtonEx : ToolStripDropDownButton
{
    protected override void OnDropDownClosed(System.EventArgs e)
    {   //閉じ終わった後、Enabledを操作することでselectedをむりやりfalseにする
        bool originalenabled = base.Enabled;
        base.Enabled = !originalenabled;
        base.Enabled = originalenabled;
        base.OnDropDownClosed(e);
    }

ToolStripSplitButtonのプルダウンメニューをAltアクセスキーで開けるようにするには

Windows標準の挙動を若干ねじ曲げている気もしますが・・・[対象(F)|▽]みたいなプルダウンつきボタンについて、Altキー操作ではボタン本体クリックではなくプルダウンを開きたい場合、Altキーが押されているか否かで処理を分けるのが安直で手っ取り早いです。

[ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.ToolStrip | ToolStripItemDesignerAvailability.StatusStrip)]
public class ToolStripSplitButtonEx : ToolStripSplitButton
{
    /// <summary>Altキーからの操作だったらDropDownを開く</summary>
    protected override void OnButtonClick(EventArgs e)
    {
        if ((Control.ModifierKeys & Keys.Alt) == Keys.Alt)
        {
            this.ShowDropDown();
        }
        else
        {
            base.OnButtonClick(e);
        }
    }

ToolStripMenuItemをクリックした際、処理内容によってはContextMenuStripが閉じないようにしたい場合は

残念ながら、ToolStripMenuItem.Clickイベントは、ContextMenuStripが閉じた後に呼び出されるので手遅れになってしまいます。
AutoCloseプロパティをtrueにするといろいろ弊害が出るので、ContextMenuStrip.ItemClickedイベントでクリック時の処理を行い(こちらのイベントはメニューが閉じる前に呼び出されます)、ContextMenuStrip.Closingイベントにて閉じる処理をキャンセルするのが手っ取り早いです。
以下のソースは、チェック状態を保持するメニュー項目をクリックした際にはメニューを閉じなくする機能を盛り込んだサンプルです。ContextMenuStrip自体を拡張しています。

class ContextMenuStripEx : ContextMenuStrip
{
    /// <summary>メニュークリックによりメニューを閉じないようにするか
    /// OnClosingまでのタイミング(ItemClickedイベントなど)でtrueをセットすると、メニューを閉じないようにする
    /// ※ToolStripMenuItem.Clickイベントでは手遅れ(すでに閉じられている)なので、ItemClickedイベントでtrueをセットすること
    /// </summary>
    public bool CancelClickClose { get; set; }

    /// <summary>チェック保持メニュー項目の場合は閉じないように制御する</summary>
    protected override void OnItemClicked(ToolStripItemClickedEventArgs e)
    {
        CancelClickClose = (e.ClickedItem is ToolStripMenuItem && (e.ClickedItem as ToolStripMenuItem).CheckOnClick);
        //OnItemClickedから先の処理で、OnClosing,Item.OnClickなどの各種処理が動く               
        base.OnItemClicked(e);
    }
    /// <summary>メニューを閉じないよう指定しているときは、閉じないように制御する</summary>
    protected override void OnClosing(ToolStripDropDownClosingEventArgs e)
    {
        if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked && CancelClickClose)
        {
            e.Cancel = true;
        }
        base.OnClosing(e);
    }

ToolStripMenuItemを右クリックしたときにもClickイベントが発動してしまう

ContextMenuStrip/ToolStripMenuItemは、他のButtonなどのコントロールとは異なり、マウス左ボタンクリックではなく右ボタンクリックでもClickイベントが発動します・・・・。
また当然ながら、Clickイベントはマウスボタンが押され終わってから(離された後に)発動するため、ClickイベントでMouseButtons.Rightを調べても、右クリックにより押されたかどうか判別することはできません。
Click時に押されたボタンの種類を知る必要がある場合は、マウスボタンが押され始めたときに発生するMouseDownイベントで、押されたボタンの情報を退避しておきましょう。

ToolStripMenuItemを右クリックしたときにはClickイベントが発動しないようにする(3/12追記)

※なお、右クリック時にClickイベントが発動しないようにするのは、ContextMenuStripの拡張だけでは難しいようです。
ContextMenuStrip.OnItemClickedメソッドをオーバーライドし、右クリック時にはbase.OnItemClickedを呼び出さないようにすることで、ContextMenuStripとしてのクリック処理を無効にすることはできます。
が、ToolStripMenuItem.OnClickはそれとは別個に呼び出されてしまうようなので、結局ContextMenuStrip/ToolStripMenuItemそれぞれに右クリック時の対処を入れる必要がありそうです。

かなり強引ですが、ContextMenuStrip.OnMouseUpを拡張することで右クリックを無効にできます。(ToolStripMenuItemのClickイベントも発動しなくなります)

class ContextMenuStripEx : ContextMenuStrip
{
    protected override void OnMouseUp(MouseEventArgs mea)
    {
        if (mea.Button != System.Windows.Forms.MouseButtons.Right)
        {
            base.OnMouseUp(mea);
        }
    }

そのかわりに、右クリックではContextMenuStrip/ToolStripMenuItemともMouseUpイベントが発動しなくなります。それ以外の副作用があるかどうかは未検証なので、ためしにHNXgrepの最新βバージョンに組み込んでみて実験してみます(おい)。

(Tipsその2に続きます)