プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

WPFのメニューとアクセスキーとIME状態

メニュー関連の前の記事: WPFでのメニューとキーボード操作時のフォーカス移動の話

この記事ではContextMenuについてだけ記述していますが、Menuでも同様の現象が発生しますし同様の方法で対処可能です。

現象

ContextMenuを定義し、MenuItemにアクセスキーを指定します。

<Window x:Class="WpfTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow">
    <Window.ContextMenu>
        <ContextMenu>
            <MenuItem Header="ファイルを開く(_O)"
                      Click="MenuItem_Click" />
        </ContextMenu>
    </Window.ContextMenu>
</Window>

Clickイベント発生の確認にMessageBoxでも出しておきます。

using System.Windows;

namespace WpfTest
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void MenuItem_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Click");
        }
    }
}

このコードをビルドして実行すると、次のことに気付きます。

  • IMEの状態にかかわらず、右クリックやShift+F10でコンテキストメニューが開く
  • IMEがOFF状態なら、oキーを押すとアクセスキーが発動する
  • IMEがON状態なら、oキーを押してもIME変換が入り'お'になるのでアクセスキーが発動しない
  • IMEがON状態でも、Altキーを押しながらShift+F10でコンテキストメニューを開いてoキーを押すとアクセスキーが発動する

普通のItemsControl(と言うよりSelector?)なら TextSearch Class で特定の要素にアクセスできるため、IMEがONでも意味はあります。
しかしMenuItemには大体アクセスキーを設定するでしょうからIMEは全くの無用です。
そういうわけで、ContextMenuを開いた時にIMEを無効状態にしましょう。

属性構文で設定する

InputMethod.PreferredImeState Attached Property を設定すれば、ContextMenuを開いた時にIMEを無効化出来ます。

<!--前略-->
<ContextMenu InputMethod.PreferredImeState="Off">
    <!--中略-->
</ContextMenu>
<!--後略-->

ただしContextMenuを閉じた後でもIME状態がOFFのままになっています。
TextSearchを使用しているListBoxItemなどのContextMenuでは、IME状態を復元したいところです。
また、ContextMenuを開いている間に半角/全角キーや変換キーを押すとIMEを有効にできてしまいます。

なお InputMethod.IsInputMethodSuspended Attached PropertyInputMethod.IsInputMethodEnabled Attached Property も試しましたが、ContextMenuに設定しても効果がありませんでした。

イベントで処理する

ContextMenuは、開いた時にキーボードフォーカスを得て、閉じた時にキーボードフォーカスを失います。カーソルキーなどを押しているとMenuItem間でキーボードフォーカスが移動します。
UIElement.IsKeyboardFocusWithinChanged Event という今回の目的に合うイベントがあるので、そこで状態の保存復元をしましょう。
キー入力によるIME有効化を防ぐ処理も入れてしまいましょう。(TextBox の IME を無効にする - (憂国のプログラマ Hatena版 改め) 周回遅れのブルース を参考にしました。)

<Window x:Class="WpfTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow">
    <Window.ContextMenu>
        <ContextMenu IsKeyboardFocusWithinChanged="ContextMenu_IsKeyboardFocusWithinChanged"
                     PreviewKeyDown="ContextMenu_PreviewKeyDown">
            <MenuItem Header="ファイルを開く(_O)"
                      Click="MenuItem_Click" />
        </ContextMenu>
    </Window.ContextMenu>
</Window>
using System.Windows;
using System.Windows.Input;

namespace WpfTest
{
    public partial class MainWindow
    {
        // コンストラクタやClickイベントは省略

        private InputMethodState _lastState;

        private void ContextMenu_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            var inputMethod = InputMethod.Current;
            if (((UIElement)sender).IsKeyboardFocusWithin)
            {
                this._lastState = inputMethod.ImeState;
                inputMethod.ImeState = InputMethodState.Off;
            }
            else
            {
                inputMethod.ImeState = this._lastState;
            }
        }

        private void ContextMenu_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            e.Handled = (e.Key == Key.ImeProcessed);
        }
    }
}

Behaviorにする

IME状態の保存・復元のためだけの状態をWindowに持たせたくないので、Expression Blend SDKの Behavior Generic Class を継承して自作Behaviorにしてしまいましょう。

<Window x:Class="WpfTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:local="clr-namespace:WpfTest"
        Title="MainWindow">
    <Window.ContextMenu>
        <ContextMenu>
            <i:Interaction.Behaviors>
                <local:TemporaryDisableImeBehavior />
            </i:Interaction.Behaviors>

            <MenuItem Header="ファイルを開く(_O)"
                      Click="MenuItem_Click" />
        </ContextMenu>
    </Window.ContextMenu>
</Window>
using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;

namespace WpfTest
{
    public class TemporaryDisableImeBehavior : Behavior<UIElement>
    {
        private InputMethodState _lastState;

        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.IsKeyboardFocusWithinChanged += AssociatedObject_IsKeyboardFocusWithinChanged;
            this.AssociatedObject.PreviewKeyDown += AssociatedObject_PreviewKeyDown;
        }

        protected override void OnDetaching()
        {
            this.AssociatedObject.IsKeyboardFocusWithinChanged -= AssociatedObject_IsKeyboardFocusWithinChanged;
            this.AssociatedObject.PreviewKeyDown -= AssociatedObject_PreviewKeyDown;
            base.OnDetaching();
        }

        private void AssociatedObject_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            var inputMethod = InputMethod.Current;
            if (((UIElement)sender).IsKeyboardFocusWithin)
            {
                this._lastState = inputMethod.ImeState;
                inputMethod.ImeState = InputMethodState.Off;
            }
            else
            {
                inputMethod.ImeState = this._lastState;
            }
        }
        private void AssociatedObject_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            e.Handled = (e.Key == Key.ImeProcessed);
        }
    }
}