Last Updated 2011/09/21
WPF のコマンドについて調べようと、WEB サイトをウロウロしましたが、どこみても何かが足りないという感じです。個々には参考になるのですが、実践的にアプリケーションを作ろうとすると十分とはいえません。そこで、コマンドに関連する事項の全体像を解説してみたいと思います。
CommandParameter と CommandTarget プロパティ
まず、WPF SDK の「コマンド実行の概要」-「コマンドとは」の中の文章を以下に収録します。
「コマンドと、ボタンまたはタイマに割り当てられた単純なイベントハンドラとの違いは、コマンドはアクションのセマンティクス(意味)と発行元がロジックから切り離されるという点です。これにより、複数のさまざまなソースで同じコマンドロジックを呼び出せるようになり、コマンドロジックはさまざまな対象用にカスタマイズできるようになります。」この文章のポイントは次の二点です。
セマンティック "semantic" は日本語にしにくい言葉ですが、文中のように短く表現するなら「意味」でいいと思います。しかし、今度は「意味」の意味が分かりにくいですね。ともあれ、「何をするか」という抽象的な概念と理解しておくほかないと思います。たとえば、「ファイルを開く」などです。どのファイルをどのように開くかはロジックのほうで定義します。
「複数のさまざまなソース」は、メニュー項目とそれに対応するツールボタンと考えていいでしょう。Delphi や C++ Builder で使っている VCL "Visual Component Library" には、ActionList クラスというものがあって、複数のソースに同じ設定ができる機能が提供されていますが、それを参考にしたものと思います。
.Net Framework は VCL をベースにして作られたものであることはこのサイトで説明しているとおりです。VCL を使った経験のあるものとしては、ActionList クラスのほうが便利だと思います。というのは、イベントハンドラだけでなく、ツールボタンのイメージ、キャプション、ショートカットキー、ツールチップなどを統括的に設定することができるからです。
「同じコマンドロジックを呼び出せる」は、複数のソースでイベントハンドラを共有することができるという意味のほかに、メニュー項目とツールボタンとを無効あるいは有効にするロジックも共有できるという意味もあります。つまり、「同じコードは二度書かない」がプログラミングの基本ですから、それを実現しようとするものです。
さて、ここからは WPF 的に解説することにしましょう。
コマンドは実行するアクションで、ICommand インターフェースを継承しなければなりません。
コマンドソースは、ICommandSource インターフェースを継承するクラスで、MenuItem、Button、KeyGesture などのクラスがありますが、これらはコマンドを発行するオブジェクトです。つまり、Command、CommandParameter、CommandTarget プロパティを持つクラスです。
コマンドターゲットは、コマンドを実行する対象の要素です。たとえば、RichTextBox クラスにおける EditingCommands.ToggleBold コマンドのターゲットは RichTextBox クラスです。
コマンドバインディングは、コマンドにイベントハンドラをバインドするための仕組みです。
コマンドは通常、ボタンコントロール、メニュー項目、ツールボタンなどにバインドして使うことになりますが、コードから実行することもできます。定義済みのコマンドの項で説明するコマンドは RoutedCommand あるいは RoutedUICommand オブジェクトを保持しますので、その Execute メソッドを直接呼び出すことができます。
たとえば、EditingCommands クラスの ToggleBold フィールドは現在選択状態にあるテキストをボールド体に設定または設定を解除するコマンドオブジェクトを保持します。イメージとしては次のようなコードになります。
EditingCommands.ToggleBold.Execute(null, richTextBox1);
ただし、定義済みコマンドの一部については、コマンドを実行する機能がコントロール自体に与えられているので、コードを書く必要はありません。たとえば、RichTextBox コントロールは ToggleBold コマンドをネイティブにサポートしますので、メニュー項目やツールボタンにコマンドをバインドするだけで使えます。
特定のコントロールに提供されていないコマンドについては、アプリケーション側でコードを書くほかありませんが、それが通常の手順といえます。つまり、CanExceute(必須ではない)および Execute メソッドの実装がイベントハンドラとなります。
WPF の機能の中で、定義済みのコマンドというべきものは以下のとおりです。
これらはあらためて宣言することなく、コマンドとして使うことができます。
それぞれ、RoutedCommand または RoutedUICommand オブジェクトを保持します。
以下のコードは、ツールバーのボタンとコマンドとをバインドします。テキストの一部を選択状態にすると、このボタンを有効にし、ボタンをクリックすると実際に選択状態のテキストを中央そろえにします。コマンドの対象が RichTextBox コントロールであれば、このためのコードを 1 行も書く必要はありません。
<Button Command="EditingCommands.AlignCenter"> <Image Source="images/AlignCenter.png"></Image> </Button>
一方、ApplicationCommands.Open コマンドのように、キージェスチャとキャプションを定義するだけのものは実際の処理をするためのコードを書かなければなりません。つまり、RichTextBox コントロールに対する EditingCommands.AlignCenter コマンドのようなアリガタミはないということです。
前項の続きですが、ApplicationCommands.Open をツールボタンとバインドするだけでほかに何もしません。
<Button Command="ApplicationCommands.Open"> <Image Source="images/FileOpen.png"></Image> </Button>
何かをするためには以下のように、コマンドに対応する CanExecute および Executed メソッド(イベントハンドラと考えてもよい)を設定し、コードを書かなければなりません。
<Window.CommandBindings> <CommandBinding Command="ApplicationCommands.Open" CanExecute="openCommand_CanExecute" Executed="openCommand_Executed" /> </Window.CommandBindings>
つまり、コントロールに実装のない定義済みコマンドは、コマンドの宣言だけで、それをどう処理するかのコードは書かなければならないというわけです。コマンドの宣言自体は、1 行のコードにすぎませんので、これが「アリガタミはない」の意味です。
「定義済みのコマンド」で説明しましたが、定義済みのコマンドはいろいろあっても実用上使えるとはいえないものもあります。そこで、ここではカスタムコマンドを作る手順を説明します。実用性のないコマンドではしかたないので、「上書き保存」するコマンドを定義することにしましょう。
1. まず、クラス内で RoutedCommand オブジェクトを宣言・作成する
public static readonly RoutedCommand Save = new RoutedCommand("SaveCommand", typeof(Window1));
コンストラクタの引数については私が公開している WPF クラスライブラリリファレンスを参照してください。
ところで、RoutedCommand クラスと RoutedUICommand クラスとの違いは、Text プロパティがあるかどうかだけです。これはメニュー項目やキャプション付きのツールボタンのキャプションになるものです。ちなみに、定義済みのコマンドの多くは RoutedUICommand オブジェクトイですが、こうしなければならない特別な理由はありません。
2. XAML コードでコマンドと UI 要素とをバインドする
以下は、擬似的なコードです。具体的にはサンプルプロジェクトを見てください。
<Window.CommandBindings> <CommandBinding Command="{x:Static custom:Window1.Save}" CanExecute="SaveCommandCanExecute" Executed="SaveCommandExecuted"/> </Window.CommandBindings> <MenuItem Header="ファイル(_F)"> <MenuItem Header="上書き保存(_S)" Command="{x:Static c:Window1.Save}" /> </MenuItem> <Button Width="24" Height="24" ToolTip="上書き保存" Command="{x:Static c:Window1.Save}"> <Image Source="images/saveHS.png" Width="16" Height="16" /> </Button>
3. 分離コードに CanExecute と Executed メソッドを実装する
以下は、擬似的なコードです。具体的にはサンプルプロジェクトを見てください。
// コマンドを関連付けるビジュアル要素(メニューやツールボタン)を有効または無効にする private void SaveCommandCanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } // コマンドを呼び出したときに実際に実行する処理を書く private void SaveCommandExecuted(object sender, ExecutedRoutedEventArgs e) { .... }
コマンドを使えるようにするクラス、つまり、Command プロパティを持つクラスは ICommandSource インターフェースを継承しなければなりません。たとえば、ButtonBase クラスは継承しますが、Slider クラスは継承しません。
Slider クラスをコマンドソースとして使いたい場合は、Slider クラスおよび ICommandSource インターフェースを継承するクラスを作成し、Command、CommandParameter、CommandTarget の各プロパティを依存関係プロパティとして設定しなければなりません。
WPF SDK の中に、Slider クラスをコマンドソースとして使えるようにするサンプルプロジェクト「ICommandSource の実装のサンプル」がありますので、参照してください。
以下のコードは、私が公開しているサンプルプルジェクトの一部ですが、ICommand インターフェースから派生するクラスは、CanExecute および Executed メソッドを実装し、CanExecuteChangedイベントのイベントハンドラの設定を可能にするコードが必要です。
// Print コマンドの定義 public class Print : ICommand { public bool CanExecute(object parameter) { return true; } // Print コマンドの実行 // ここではメッセージボックスを表示するだけ public void Execute(object parameter) { MessageBox.Show("Print"); } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } } // end of Print class
しかし、実用上、ICommand インターフェースから派生するクラスを作成する手順は面倒な割りにメリットが少ない感じがします。「カスタムコマンド」で説明したとおり、コマンドの宣言はわずか 1 行で済む話ですので、どういう場面でクラスを作成するほうがいいのかはよく分かりません。あえていえば、クラスの定義を別ファイルにしておいて、必要なときにプロジェクトに追加し、使うということが考えられます。かなり面倒な処理が必要ならこうすることも理解できますが、どうでしょうね。現実的には、実情に合わせて選択するということかもしれません。
コマンドとは直接関係はありませんが、関連性が高いと考え、取り上げることにしました。
定義済みのコマンド、たとえば、ApplicationCommands.Open をバインドすると、UI 要素のキャプションに定義済みのテキスト、この場合は「開く」が表示され、キージェスチャとして [Ctrl] + O が表示されます。しかし、アクセスキーの設定はないし、キャプションとしても通常は単に「開く」ではなく、「既存のファイルを開く」などにすることが多いものです。
つまり、定義済みのコマンドは宣言文が不要というだけの効果しかないとも言えます。ともあれ、UI 要素のキャプションとアクセスキーを設定するコードを示します。
<MenuItem Name="mnuFileOpen" Command="ApplicationCommands.Open" Header="既存のファイルを開く(_O)"/>
1. キャプション
メニュー項目のキャプションは、Header プロパティで設定します。そのとき、アクセスキーの設定も可能で、アクセスキーとする文字の前にアンダースコア "_" を付けるだけです。
従来の .Net Framework ではアンパサンド "&" を使いますが、WPF は XAML 構文を使用することから、XAML 構文ではアンパサンドに特殊な意味を持たせていますので、使えなくなりました。
2. アクセスキー
キャプションの項を参照
3. ジェスチャ
ジェスチャ "gesture" は、ショートカットキーと理解してもいいですが、WPF ではキージェスチャとマウスジェスチャとがありますので、両方を合わせてジェスチャと呼ぶことにしたのかもしれません。
「ファイルを上書き保存」するメニュー項目に対して、ショートカットキーとして [Ctrl] + S を設定することが多いものです。これをキージェスチャと呼びます。一方、マウスジェスチャのほうはマウスジェスチャを設定した定義済みコマンドがないことからも分かるように、あまり使う機会はないと思います。
4. マウスジェスチャ
使う機会は少ないと思いますが、説明だけはしておきます。次のコードは [Ctrl] を押しながらマウスホイールを回転すると ApplicationCommands.Open コマンドを呼び出します。
<Window.InputBindings> <MouseBinding Command="ApplicationCommands.Open" Gesture="Control+WheelClick"/> </Window.InputBindings>
5. マルチキージェスチャ
マルチキージェスチャという言葉は WPF SDK の中にはありませんが、たとえば、Visual Studio の [表示] メニューを開くと、[クラスビュー] の項のキージェスチャは [Ctrl] + W, C になっています。これがそうです。つまり、[Ctrl] + W を押したあと、単独で C を押すとクラスビューが表示されます。
マルチキージェスチャを現在の WPF の機能でもできるのではないかと考え、テストしてみました。Gesture プロパティに "Ctrl+W C" とか、"Ctrl+W;C" とか設定するとエラーになりますが、いろいろと試すうちに、カンマで区切る "Ctrl+W,C" がエラーにならないことに気付きました。しかし、"Ctrl+W" のほうは有効ですが、マルチキージェスチャとして使うことはできません。ヒョットすると、Microsoft はカンマで区切るマルチキージェスチャの設定を可能にすることを考えていたが、何かの都合でやめたのかもしれません。
ともあれ、現在の WPF はマルチキージェスチャをサポートしませんので、自前で何とかするほかありません。これを実現するには KeyConverter クラスから派生するクラスを作れば何とかなりそうだと考えていたところ、以下のサイトの中にズバリの解決策があることが分かりました。
http://kentb.blogspot.com/2009/03/multikeygesture.html
これは、Kent Boogaart 氏のブログで、"MultiKeyGesture" というタイトルのページがそうです。ソースコードも含まれていますので、テストしたところ、正常に動作します。興味のある人は試して見る価値はあると思います。
6. InputGestureText プロパティ
定義済みコマンドの中には、メニュー項目にキージェスチャをあらわす文字列を提供するものがあります。たとえば、ApplicationCommands.Open コマンドの場合は [Ctrl+O] です。カスタムコマンドをメニュー項目にバインドすると、その Gesture プロパティが表示されるのかと思っていると表示されません。ApplicationCommnda.Open コマンドは RoutedUICommand オブジェクトを保持しますが、キージェスチャをどうやって設定しているのでしょうか。
この謎はいまだ解けていませんが、キージェスチャをあらわす文字列を設定する方法は分かります。MenuItem クラスの InputGestureText プロパティを使って設定できます。
このあたりが、VCL のほうがすぐれていると考える理由です。RoutedCommand または RoutedUICommand クラスのほうにキージェスチャを設定できるようにすればスマートになると思うのですが。
最近、分かったことですが、WPF の開発には 200 名ぐらいの人がかかわったそうです。これだけいると、一貫性を持たせるのは難しいのかもしれませんね。
7. ツールチップ
ツールボタンはメニュー項目とは異なり、文字列による情報を提供しませんので、ツールチップを設定するほうが望ましいといえます。以下は、ツールボタンにツールチップを設定するコード例です。
<Button Width="24" Height="24" ToolTip="ファイルを開く" Command="ApplicationCommands.Open"> <Image Source="images/openHS.png" Width="16" Height="16" /> </Button>
現在の状態に応じて、UI 要素を無効化、つまり、使えなくしたい場合があります。たとえば、テキストエディタにおいて、[上書き保存] する機能はテキストの内容を変更したときにだけ有効にし、それ以外の場合は無効のままにすることが多いものです。こうすることで、不要な操作をさせないようにするなどのメリットがあります。
RoutedCommand または RoutedUICommand クラスは、CanExecute および Executed メソッドを提供しますが、CanExecute メソッドで UI 要素を無効にするかどうかを判定し、無効にする場合はイベントデータの CanExecute プロパティに false を設定します。ちなみに、CanExecute メソッドを設定しない場合は常に true となります。
ICommandSource インターフェースを継承するクラスは、CommandParameter と CommandTarget プロパティを含みます。たとえば、Button や MenuItem クラスです。
CommandParamter プロパティはコマンドに渡すパラメータを指定しますが、設定する値は Object 型ですので、任意のオブジェクト(数値、文字列、UI 要素、構造体、クラス型など)を設定することができます。
CommandTarget プロパティは、コマンドを実行する対象の UI 要素を指定します。CommandTarget プロパティを適用するのは、コマンドが RoutedCommand オブジェクトの場合だけ、つまり、CanExecute と Executed イベントが発生するオブジェクトだけです。それ以外の場合、コマンドは CommandTarget プロパティの設定を無視します。CommandTarget プロパティを設定しない場合は、現在キーボードフォーカスを持つ要素がコマンドの実行対象となります。
以下は、textBox1 をパラメータとして設定するコード例です。
<MenuItem Name="mnuFilePrint" Header="印刷(_P)..." Command="{x:Static c:Window1.Print}" CommandParameter="{Binding ElementName=textBox1}" />
コマンドをテストするためのサンプルプロジェクトを作りました。内容は決して模範的なものではなく、不都合な部分も残す形にしています。定義済みのコマンドができることとできないこととを明らかにするためです。
下図は、アプリケーションを起動直後に [ファイル] メニューを開いたところです。[開く] メニューを選択すると、ファイルを開くダイアログボックスを表示しますので、テキストファイルを選択してください。ただし、ファイルの文字コードは "shift-jis" 限定ですので、注意してください。ファイルの内容を読み込んで、TextBox コントロールに表示します。[表示]-[文字のサイズ] には、フォントのサイズを変更するメニュー項目があります。
[開く] は ApplicationCommands.Open をバインドしただけの状態です。キャプションにアクセスキーの設定がないところに留意してください。また、この項目には MouseBinding オブジェクトを関連付けました。[Ctrl] を押しながらマウスホイールを回転すると、「開く」メニュー項目を開きます。
いくつかのメニュー項目が無効になっていますが、[名前を付けて保存] は、テキストファイルを読み込むと有効になります。また、[上書き保存] はテキストの内容を変更すると有効になります。
[上書き保存] は ApplicationCommands.Save を使っていますが、定義済みコマンドの使い方の典型的な例です。
[印刷] は ICommand インターフェースから派生するクラスにしました。
[アプリケーションの終了] は RoutedUICommand クラスを使いました。キージェスチャの [Alt+F4] は Windows 標準の機能ですから、実装は不要です。単に、キージェスチャをあらわす文字列を表示しているにすぎません。
[表示]-[文字のサイズ] は、RoutedCommand クラスを使いました。
CustomCommandTest.lzh (59,153 bytes)
−以上−