先日、ちょっとしたツールを作る必要があって、折角だからと思ってWPFで作ってみました。WPFの概要については、++C++; // 未確認飛行物体 C - Windows Presentation Foundation 概要(WPF)辺りを見ると簡潔で判りやすいのではないでしょうか。使ってみた感じでは、Direct Xを汎用化したというよりも、GDIを拡張・汎用化・高速化した、といった感じのライブラリです。Visual Studio 2008以降でGUIアプリケーションを作るなら、今までのSystem.Windows.Forms
を使う前に検討する価値はあると思います。
ま、前置きはさておき、WPFでメニューを作る時に戸惑って手間取ったので、ちょっと覚え書き。半ば自分用なので、前提知識の細々とした説明はしません。++C++; // 未確認飛行物体 C - .NET Framework 3.0で全体像を掴んだら、MSDNライブラリのWPFの項、Windows Presentation Foundationを必要に応じて参照すると良いでしょう。今回の話は、主にここのコマンド実行の概要に書いてある話の要約です。
メニュー
作ったツールにもメニューを付けようと思いました。メニューを作る方法自体はすぐ見つかります(何しろツールボックスにも入ってますから)。例えば、まっさらなウィンドウにドックパネルを作って、その一番上にメニューを付けるには、次のようなXAML文書を作ります。
<!-- MainWindow.xaml --> <Window x:Class="CommandSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="コマンドのサンプル" WindowStartupLocation="CenterScreen"> <DockPanel> <Menu DockPanel.Dock="Top"> <MenuItem Header="ファイル(_F)"> <MenuItem Header="新規作成(_N)" /> <Separator /> <MenuItem Header="他の形式で形式で入力(_I)" /> <Separator /> <MenuItem Header="終了(_X)" /> </MenuItem> </Menu> </DockPanel> </Window>
ファイルポップアップメニューの中に、三つのアイテムがあるだけの単純なメニューです。なお、このXAMLソースは、CommandSample
名前空間のMainWindow
ウィンドウに適用する事が想定されています(1行目のx:Class
属性指定)。
単純なメニューコマンド処理
これだけではメニューは表示されるだけで何ら役に立ちません。このメニューを機能させる一番簡単で単純な(しかし使われない?)方法は、メニューアイテムのClick
イベントを処理する事です。
<!-- MainWindow.xaml --> <Window x:Class="CommandSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="コマンドのサンプル" WindowStartupLocation="CenterScreen"> <DockPanel> <Menu DockPanel.Dock="Top"> <MenuItem Header="ファイル(_F)"> <MenuItem Header="新規作成(_N)" Click="FileNew" /> <Separator /> <MenuItem Header="他の形式で入力(_I)" Click="FileImport" /> <Separator /> <MenuItem Header="終了(_X)" Click="FileExit" /> </MenuItem> </Menu> </DockPanel> </Window>
上のように、Click
属性指定でイベントハンドラの名前を値にすれば、クリックされた時にそのイベントハンドラが呼び出されるようになります。
// MainWindow.xaml.cs using System.Windows; // …… namespace CommandSample { public partial class MainWindow : Window { // …… private void FileNew( object sender, RoutedEventArgs e ) { // 新規作成の処理 } private void FileImport( object sender, RoutedEventArgs e ) { // 別形式入力の処理 } private void FileExit( object sender, RoutedEventArgs e ) { // 終了の処理 } // …… } }
イベントハンドラはRoutedEventHandler
型です。EventHandler
ではEventArgs
を第二引数に取っていた所を、RoutedEventArgs
を第二引数に取る点が違います。でも、殆ど同じですね。
これでもまあ動くのですが、どうやらWPFで推奨されるメニューコマンド処理手法はこれではないようです。WPFでは、メニューから実行されるコマンドというのはメニューに固有の概念ではなく、メニューというものは、もっと一般化された「コマンド」を呼び出す一つの手段であると解釈されるようです。
コマンド実行の概念
コマンド実行の概要に拠れば、コマンド実行には四つの主要な概念があるんだとか。
- コマンドは、実行されるアクションです。
- コマンド ソースは、コマンドを呼び出すオブジェクトです。
- コマンドの対象は、コマンドが実行されているオブジェクトです。
- コマンド バインディングは、コマンド ロジックをコマンドに割り当てるオブジェクトです。
コマンドは、先程挙げた例だと、「新規作成する」「他の形式で入力する」「終了する」といった、抽象化された操作です。このコマンドは必ずしもメニューから実行されるものではありません。メニューや、キージェスチャ(キーボードショートカット……例えばCtrl+Nとか)、それにボタンから呼び出されたりします。こういった、コマンドを呼び出すものをコマンドソースと言うようです。コマンドの対象というのは、まあ文字通りですが、そのコマンドを何に対して行うのかを示します。「終了する」なんてのは必ずアプリケーション全体対象ですから気にする必要はありませんが、例えば「コピー」操作なんかは対象を決めないとやりようがありませんね。そんな訳で、コマンドの対象はXAMLの中で明示的に示す事もできます。特に指定しなければ(今回は指定しません)、フォーカスのあるコントロールになります。コマンドバインディングは、コマンドとイベントハンドラとの結びつきの事です。後述します。
コマンド
コマンドは、前述の通り抽象化された操作です。特定のアプリケーションや、実際に行われる処理が何であるかなどに依存しない概念です。この為、一般的なコマンド群というのは、WPFで既に定義されています。その一つがSystem.Windows.Input.ApplicationCommands
です。ここには、NewとかOpenとかSaveとか、何かのデータオブジェクトを扱うようなアプリケーションなら、その大半に存在するような操作のコマンドが定義されています。さっきの例では「新規作成する」というコマンドがありました。これにはApplicationCommands.New
がぴったりです。とりあえずこれを「新規作成」メニューアイテムのコマンドとします。
<!-- MainWindow.xaml --> <Window x:Class="CommandSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="コマンドのサンプル" WindowStartupLocation="CenterScreen"> <DockPanel> <Menu DockPanel.Dock="Top"> <MenuItem Header="ファイル(_F)"> <MenuItem Header="新規作成(_N)" Command="New" /> <Separator /> <MenuItem Header="他の形式で入力(_I)" /> <Separator /> <MenuItem Header="終了(_X)" /> </MenuItem> </Menu> </DockPanel> </Window>
ApplicationCommands
などのWPFクラスのメンバは、文字列で書いても勝手に変換してくれるので、すっきり書く事ができます。
コマンドバインディング
繰り返しますがコマンドは最初から特定の処理と結びついている訳ではありません。メニューアイテムが選択されてコマンドが呼ばれても、そのままでは何も実行されません。コマンドバインディングは、コマンドと特定のイベントハンドラとの結びつきを記述します。
<!-- MainWindow.xaml --> <Window x:Class="CommandSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="コマンドのサンプル" WindowStartupLocation="CenterScreen"> <Window.CommandBindings> <CommandBinding Command="New" Executed="FileNewCommandExecuted" CanExecute="FileCommandsCanExecute" /> </Window.CommandBindings> <DockPanel> <Menu DockPanel.Dock="Top"> <MenuItem Header="ファイル(_F)"> <MenuItem Header="新規作成(_N)" Command="New" /> <Separator /> <MenuItem Header="他の形式で入力(_I)" /> <Separator /> <MenuItem Header="終了(_X)" /> </MenuItem> </Menu> </DockPanel> </Window>
Command
にはこのバインディングで結びつけるコマンドを、Executed
にはコマンドが実行された時のイベントハンドラを、CanExecute
にはコマンドの実行可否を判断するイベントハンドラを指定します。CanExecute
は必要なければ無くてもいいようです。
// MainWindow.xaml.cs using System.Windows; using System.Windows.Input; // …… namespace CommandSample { public partial class MainWindow : Window { // …… private void FileNewCommandExecuted( object sender, ExecutedRoutedEventArgs e ) { // 新規作成の処理 } private void FileCommandsCanExecute( object sender, CanExecuteRoutedEventArgs e ) { e.CanExecute = true; return; } // …… } }
イベントハンドラは上のように記述します。FileCommandsCanExecute
はファイルコマンドの実行可否を判断するものですが、ファイルコマンドはすべていつでも実行可能なので、e.CanExecute
にはtrue
を入れて返します。
ここまですると、きちんとメニューから新規作成コマンドを実行できるようになります。
カスタムコマンド
当然ながら、ApplicationCommands
には無い(更に、EditCommands
などにも無い)コマンドが考えられます。これまでの例では、「他の形式で入力」などがそうです。「終了する」コマンドも、「(ファイルを)閉じる」コマンド(ApplicationCommands.Close
)とは微妙にニュアンスが異なる気がします。
自分でコマンドを作るのも、実は簡単です。適当なクラスにRoutedCommand
のスタティックインスタンスを作って、それをApplicationCommands
などのクラスのコマンドの代わりに使えばいいのです。
// MainWindow.xaml.cs using System.Windows; using System.Windows.Input; // …… namespace CommandSample { public partial class MainWindow : Window { public static RoutedCommand FileImportCommand = new RoutedCommand(); public static RoutedCommand FileExitCommand = new RoutedCommand(); // …… private void FileNewCommandExecuted( object sender, ExecutedRoutedEventArgs e ) { // 新規作成の処理 } private void FileImportCommandExecuted( object sender, ExecutedRoutedEventArgs e ) { // 別形式入力の処理 } private void FileImportCommandCanExecute( object sender, CanExecuteRoutedEventArgs e ) { // 状況に応じてe.CanExecuteにtrue/falseを入れる } private void FileExitCommandExecuted( object sender, ExecutedRoutedEventArgs e ) { // 終了の処理 } private void FileCommandsCanExecute( object sender, CanExecuteRoutedEventArgs e ) { e.CanExecute = true; return; } // …… } }
XAMLソースの記述も殆ど同じように変えるだけです。ただ、デフォルトの名前空間はWPFのものなので、今回コマンドを作ったクラスの名前空間を参照できるようにする必要があります。
<!-- MainWindow.xaml --> <Window x:Class="CommandSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:cs="clr-namespace:CommandSample" Title="コマンドのサンプル" WindowStartupLocation="CenterScreen"> <Window.CommandBindings> <CommandBinding Command="New" Executed="FileNewCommandExecuted" CanExecute="FileCommandsCanExecute" /> <CommandBinding Command="{x:Static cs:MainWindow.FileImportCommand}" Executed="FileImportCommandExecuted" CanExecute="FileImportCommandCanExecute" /> <CommandBinding Command="{x:Static cs:MainWindow.FileExitCommand}" Executed="FileExitCommandExecuted" CanExecute="FileCommandsCanExecute" /> </Window.CommandBindings> <DockPanel> <Menu DockPanel.Dock="Top"> <MenuItem Header="ファイル(_F)"> <MenuItem Header="新規作成(_N)" Command="New" /> <Separator /> <MenuItem Header="他の形式で入力(_I)" Command="{x:Static cs:MainWindow.FileImportCommand}" /> <Separator /> <MenuItem Header="終了(_X)" Command="{x:Static cs:MainWindow.FileExitCommand}" /> </MenuItem> </Menu> </DockPanel> </Window>
これで他のコマンドも動くようになります。
キージェスチャでコマンド呼び出し
コマンドを呼び出せるのは、前述の通りメニューアイテムだけではありません。ここでは一般的によく使われるであろうキージェスチャにも対応したいと思います。
尤も、ApplicationCommands
のコマンドには、既定のインプットジェスチャがあったりして、ここまでの記述だけで、新規作成コマンドはCtrl+Nで呼び出せるはずです。しかし、カスタムコマンドである「他の形式で入力」コマンドもキージェスチャだけで呼び出せるようにしたいと思うかも知れません(思わないとは思いますが、まあ「ファイルを開く」コマンドはもう既定のキージェスチャがついてしまっているので、説明用です)。これは入力バインディングの一つとしてキーバインディングを定義する事で実現されます。
<!-- MainWindow.xaml --> <Window x:Class="CommandSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:cs="clr-namespace:CommandSample" Title="コマンドのサンプル" WindowStartupLocation="CenterScreen"> <Window.CommandBindings> <CommandBinding Command="New" Executed="FileNewCommandExecuted" CanExecute="FileCommandsCanExecute" /> <CommandBinding Command="{x:Static cs:MainWindow.FileImportCommand}" Executed="FileImportCommandExecuted" CanExecute="FileImportCommandCanExecute" /> <CommandBinding Command="{x:Static cs:MainWindow.FileExitCommand}" Executed="FileExitCommandExecuted" CanExecute="FileCommandsCanExecute" /> </Window.CommandBindings> <Window.InputBindings> <KeyBinding Key="I" Modifiers="Control" Command="{x:Static cs:MainWindow.FileImportCommand}" /> </Window.InputBindings> <DockPanel> <Menu DockPanel.Dock="Top"> <MenuItem Header="ファイル(_F)"> <MenuItem Header="新規作成(_N)" Command="New" /> <Separator /> <MenuItem Header="他の形式で入力(_I)" InputGestureText="Ctrl+I" Command="{x:Static cs:MainWindow.FileImportCommand}" /> <Separator /> <MenuItem Header="終了(_X)" Command="{x:Static cs:MainWindow.FileExitCommand}" /> </MenuItem> </Menu> </DockPanel> </Window>
プログラムコード中からコマンド呼び出し
プログラムコードの中からも、コマンドを呼び出したい事があります。例えば、ファイルを開いたり、他の形式で入力を受け付ける場合には、その前にまず状態を初期化しておきたいでしょう。この時FileNewCommandExecuted
と全く同じ内容を書くのはやりたくありません。
コマンドは、メニューやボタンが無くても、プログラム中から直に呼び出せます。コマンドのインスタンスのExecute
メソッドを呼べばいいだけです。
// MainWindow.xaml.cs using System.Windows; using System.Windows.Input; // …… namespace CommandSample { public partial class MainWindow : Window { public static RoutedCommand FileImportCommand = new RoutedCommand(); public static RoutedCommand FileExitCommand = new RoutedCommand(); // …… private void FileNewCommandExecuted( object sender, ExecutedRoutedEventArgs e ) { // 新規作成の処理 } private void FileImportCommandExecuted( object sender, ExecutedRoutedEventArgs e ) { ApplicationCommands.New.Execute( null, this ); // 別形式入力の処理 } private void FileImportCommandCanExecute( object sender, CanExecuteRoutedEventArgs e ) { // 状況に応じてe.CanExecuteにtrue/falseを入れる } private void FileExitCommandExecuted( object sender, ExecutedRoutedEventArgs e ) { // 終了の処理 } private void FileCommandsCanExecute( object sender, CanExecuteRoutedEventArgs e ) { e.CanExecute = true; return; } // …… } }
RoutedCommand.Execute
の第一引数は、任意のパラメータ(object
型)です。このパラメータはイベントハンドラの引数e
のParameter
メンバとしてイベントハンドラに渡されるので、必要なら適当なクラスのインスタンスを渡す事で呼び出し元と呼び出し先との間でデータのやりとりができます。