Last Updated 2011/09/21
下図はごく一般的な形式のアプリケーションを実行したところです。フォーム上部にメニューバーがあって、下部にステータスバーがある形式ですね(ここではあえてツールバーを含めませんでした)。テスト用のアプリケーションとして初めて作ったものですが、メニューバーを作るだけで大変で、どこから手をつけていいのかウロウロするばかりでした。
アプリケーション作成の第一歩からつまずいたわけですが、このページでは従来の .Net Framework アプリケーションと WPF アプリケーションとの構築方法の違いなどを含めて説明したいと思います。
フォームにメニューバーを追加しようとしたら最初からつまづいてしまいました。Windows アプリケーション作成時に使えるメニューデザイナがありません。メニューコントロールはありますが、メニューバーコントロールに相当するものがありません。メニュ−バーは通常、フォームの上部に配置しますが、Docking プロパティに相当するものもありません。WPF アプリケーションと従来のアプリケーションとの違いをもっとも強く感じたところです。そうこうしているうちに、DockPanel コントロールがメニューバーとステータスバーの配置に適していることが分かりました。
メニュー項目にはテキストだけでなく、画像を表示することが標準になっています。そこで、MenuItem クラスの中にある Icon プロパティに画像を設定すればいいのだなと思っていると大間違いです。Icon プロパティの設定値は Object 型ですが、実態はコントロール(通常は Image コントロール)を指定しなければなりません。
<MenuItem Header="編集(_E)" Width="Auto"> <MenuItem Header="コピー(_C)" Command="Copy"> <MenuItem.Icon> <Image Source="images/CopyHS.png" Width="16" Height="16" Stretch="Uniform"></Image> </MenuItem.Icon> </MenuItem> <MenuItem Name="mnuEditCut" Command="Cut"> <MenuItem.Icon> <Image Source="images/CutHS.png" Width="16" Height="16" Stretch="Uniform"> </Image> </MenuItem.Icon> .... </MenuItem>
ここで注意することがあります。Source プロパティで指定する画像ファイルは背景に対して透過的に描画される形式を選択しなければなりません。透過的でない画像を自動的に透過的に描画する機能はありません。
背景に対して透過的に描画するには、アルファ値を設定したビットマップ(.bmp ファイル)、透過色の設定のある PNG または GIF を使うことになります。ただし、ビットマップはファイルサイズが大きいですから、圧縮形式の PNG または GIF を使うべきです。さらにいえば、GIF は 256 色までしかサポートしませんからフルカラー画像を使おうとすると PNG に限られるということになります。Visual Studio に付属のイメージライブラリでも多くの PNG を収録しています。上記のコードの Source プロパティに指定した CopyHS.png もそのうちの一つです。
PNG はいろいろな経緯があって普及しませんでしたが、WPF の登場によって一気に重要度がましたといっていいでしょう。手持ちのビットマップを PNG に変換したい場合は、それに対応したツールが無料で公開されていますので、Vector のサイトをあたってください。
Software のページで、ビットマップを PNG に変換するツール ToPng を公開しています。
もう一つ、注意することがあります。上図の「コピー(C)」と「切り取り」を見てください。「切り取り」のほうにアクセスキーがありませんね。「コピー」のほうは Header プロパティを明示的に設定していますが、「切り取り」のほうは Command="Cut" の機能を使ったからです。コマンドについては、このページで後述する「コマンドとイベントハンドラ」で詳しく説明します。
メニュー項目に画像を設定する手順は前の項目で説明しました。次に、メニュー項目の IsEnabled プロパティを false に指定したとき、画像もそれらしく表示したいものです。しかし、これを実現する簡単な手順はありません。そこで、クラスを作ってみました。不完全なコードですが、次のとおりです。
using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; namespace emanual.Wpf.Controls { public class MenuItemImage : Image { static MenuItemImage() { IsEnabledProperty.OverrideMetadata(typeof(MenuItemImage), new FrameworkPropertyMetadata(true, new PropertyChangedCallback(OnIsEnabledChanged))); } //--------------------------------------------------------------------------------------------- private static void OnIsEnabledChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { var control = obj as MenuItemImage; var isEnable = Convert.ToBoolean(args.NewValue); var image = control.Source.Clone(); if (control != null) { if (!isEnable) { // グレースケール画像に変換し、Source プロパティに設定する control.Source = new FormatConvertedBitmap((BitmapSource)image, PixelFormats.Gray32Float, null, 0); // 背景に対して透過的に描画するため control.OpacityMask = new ImageBrush(image); } else { control.Source = image; } } } } // end of MenuItemImage class } // end of namespace
<Window x:Class="MenuTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ctrl="clr-namespace:emanual.Wpf.Controls" ....> <Menu> <MenuItem Name="mnuTest" Header="テスト" Click="mnuTest_Click" IsEnabled="False"> <MenuItem.Icon> <ctrl:MenuItemImage Source="images/Test.png" /> </MenuItem.Icon> </MenuItem> </Menu> </Window>
ちなみに、このMenuItemImage クラスは ToolBar コントロール用としても使うことができます。
Windows アプリケーション用語としては、アクセラレータキーと呼んでいるものです。アクセスキーという言葉は元々 Visual Basic 用語でしたが、WPF アプリケーション用語として先祖返りしたといったところです。ともあれ、WPF ではアクセスキーの指定の仕方が変わりました。Windows アプリケーションではアクセスキーにしたい英文字の前にアンパサンド "&" をつけますが、WPF ではアンダースコア "_" を付けます。たとえば、「ファイル(&F)」ではなく、「ファイル(_F)」とします。
メニュー項目に対してアクセスキーを設定する手順は、MenuItem オブジェクトの Header プロパティを利用します。
<MenuItem Header="ファイル(_F)" Width="Auto">
ところで、System.Windows.Controls.AccessText クラスがありますが、これを明示的に使う必要はありません。Header プロパティには暗黙的に適用されるからです。もちろん、AccessText クラスを使う場面はありますので、このようなクラスがあることは覚えておいてください。
ところで、WPF アプリケーションでは、アクセスキーをあらわす文字に下線が表示されません。しかし、[Alt] キーを押すと、下線が表示され、選択可能になります。
ショートカットとは、「メニュー項目に画像を設定する」の図の「コピー(C)」の横にある Ctrl+C がそうです。定義済みのコマンドを指定すると自動的に設定されます。もちろん、MenuItem クラスの InputGestureText プロパティを設定すれば独自の設定も可能です。
<MenuItem Header="検索(_F)" InputGestureText="Ctrl+F" Click="mnuEditFind_Click"/>
メニュー項目の IsCheckable プロパティに true を指定しておいて、IsChecked プロパティに true を指定すると、そのメニュー項目を選択したときにチェックマークが入ります。IsCheckable プロパティは XAML で指定できますが、IsChecked プロパティはイベントハンドラで設定することになるでしょう。
GroupBox コントロール内に複数の RadioButton コントロールを配置し、RadioButton の 1 つを選択するとバレットマークがオンになります。次に、別の RadioButton を選択すると以前のマークが取り消されて新しく選択したほうにマークが付きます。RadioButton コントロールをラジオボタンと呼ぶ由来は、ラジオのチャンネルを選択するとき、一つのボタンを押すとそれまで押されていたボタンが開放されることに由来します。
最近はこういうタイプのスイッチが少なくなりましたので、ピンとこない人も多いかもしれませんね。
さて、Windows フォームアプリケーションにはメニュー項目をグループ化する機能はありません。また、WPF アプリケーションにもないようです。RadioButton コントロールには GroupName プロパティがあってグループ化が可能ですが、MenuItem クラスにはありません。どうしてなんでしょう。メニュー項目をグループ化する例としては、画像を拡大・縮尺する場合、拡大率を指定する複数のメニュー項目、たとえば、100%、200%、300% などがあって、選択を変えればほかの選択状態を解除したいですよね。ともあれ、コードをゴリゴリ書けば技術的には可能ですからこれ以上の説明はしないでおきます。
セパレータは、メニュー項目の区切り線です。セパレータを入れたい位置に Separator 要素を挿入するだけです。
<MenuItem Header="上書き保存(_S)..." /> <Separator /> <MenuItem Header="終了(_X)" /></pre>
セパレートのスタイルを独自の設定にしたい場合は、SeparatorStyleKey プロパティを利用します。下図は、セパレータを赤色に設定したところです。OverridesDefaultStyle プロパティに true を指定していますので、すべてのセパレータのスタイルとして適用されます。
WPF は自由性は高いのですが、それだけ面倒な手続きが必要です。区切り線の色と幅を変更するために以下のようなコードを書かなければなりません。
<Window.Resources> <Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator"> <Setter Property="OverridesDefaultStyle" Value="true" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Separator}"> <Border Background="Red" Width="{TemplateBinding Width}" Height="2"/> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources>
「WPF の基礎知識」でも少し触れましたが、メニュー項目を選択したとき、イベントハンドラを呼び出し、その中で所定の処理を実行する手順は Windows フォームアプリケーションの場合と同じです。しかし、WPF にはコマンドという概念が導入されました。
以下のコードは、Windows フォームアプリケーションの場合のメニュー項目の設定内容とほぼ同じものです。つまり、メニュー項目の mnuEditCopy を選択すると、mnuEditCopy_Click イベントが発生しますので、このイベントハンドラ内に選択状態にあるテキストをクリップボードにコピーするコードを書くことになります。
<MenuItem Name="mnuEditCopy" Header="コピー(_C)" Click="mnuEditCopy_Click"> <MenuItem.Icon> <Image Source="images/CopyHS.png" Width="16" Height="16" Stretch="Uniform"></Image> </MenuItem.Icon> </MenuItem>
上記のコードは以下のように書くこともできます。
<MenuItem Header="コピー(_C)" Command="Copy"> <MenuItem.Icon> <Image Source="images/CopyHS.png" Width="16" Height="16" Stretch="Uniform" /> </MenuItem.Icon> </MenuItem>
このメニュー項目を選択したときの処理を実行するイベントハンドラがないことに気付くと思います。ここで指定した Command には WPF の定義済みのコマンド "Copy" を設定しました。このコマンドはアプリケーション側が何もしなくても WPF 内部で処理してくれます。つまり、このコマンドを指定しただけで、TextBox コントロール内のテキストに選択状態のテキストがない場合は、このメニュー項目を無効にし、選択状態のテキストがあればメニュー項目を有効にして、実際のメニュー項目を選択するとテキストをクリップボードにコピーしてくれます。
定義済みのコマンドについては、「WPF の基礎知識」の「定義済みのコマンド」の項で説明しましたので、再録はしませんが、通常のアプリケーションでほしい機能はほとんど用意されていると思います。
私自身は定義済みコマンドを積極的に使おうとは思いません。というのは、内部的に何をやるのかがハッキリしないことと、結局手を入れないといけないからです。すべてのコマンドを調べたわけではありませんが、Copy、Cut、Pasete は使えます。しかし、ほかはどうでしょうか。
定義済みコマンドの Copy、Cut、Paste などは何をすべきかがハッキリしていますから、WPF が処理までやってくれますが、New とか Save はどうでしょう。この場合はさすがに自前で処理しなければなりませんが、CommandBinding クラスを利用する方法があります。これについては説明が長くなるので、サンプルプロジェクトを見てください。
ステータスバーには通常、複数のパネル(StatusBarItem コントロール)を配置します。そのうち、左端のパネルはフォームの左端に位置し、そのほかのパネルは右詰めにして、フォームの幅が変化すると、左端のパネルを伸縮して調整したいものです。System.Windows.Forms.StatusStrip コントロールを使う場合は、左端のパネルの Spring プロパティに true を指定すると、伸縮可能になりますが、WPF の StatusBarItem コントロールにはそのようなプロパティはありません。
さて、どうしたものかと WEB サイトをあたっていると、同様の趣旨の質問がたくさん挙がっています。みなさん、悩んでいるということなのでしょう。アチコチあたるうちに見つけました。しかし、Grid コントロールを使ってずいぶん面倒なことをしています。動作をテストしたところ、少しチラツキがありますが、期待の動作はします。
チラツクほど面倒なことをしているという点に、いまいち納得がいかないので、結局自作することにしました。右そろえする複数の(一つでもいいですが)パネルを StackPanel コントロールに納めて、その HorizontalAlignment プロパティに Right を指定すると、右詰めになります。もちろん、フォームをリサイズしても追随します。実際の動作はサンプルプロジェクトをご覧ください。
【重 要】
ここで重要なことを書きます。偶然ですが、大変なことに気付きました。StatusPanel コントロールは内部的に DockPanel コントロールを作成し、StatusBarItem オブジェクトをその中に配置しているということです。WPF SDK のどこにもこのことは書いていませんので、間違いではないかと思い、WEB サイトをあたったところ、あの有名な Charles Petzold のサイトで、この事実を知って「腰が抜けるほど驚いた」との記述がありました。
上記で書いたとおり、この問題は世界中の人が悩んでいたことであって、それをアナウンスしないでほっておく Microsoft の罪は大きいと思います。
要するに、DockPanel コントロールは最後に配置した子要素を残りの領域に拡大する性質がありますから、これを利用すると左端の パネルを伸縮することができます。伸縮したいパネルを最後に配置するところがミソです。
<Window x:Class="FontTest2.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="500"> <DockPanel> <StatusBar DockPanel.Dock="Bottom"> <StatusBarItem Width="60" DockPanel.Dock="Right">パネル3</StatusBarItem> <Separator DockPanel.Dock="Right" /> <StatusBarItem Width="60" DockPanel.Dock="Right">パネル2</StatusBarItem> <Separator DockPanel.Dock="Right" /> <StatusBarItem>パネル1</StatusBarItem> </StatusBar> <TextBox></TextBox> </DockPanel> </Window>
以下のコードは WPF SDK の中にあるサンプルコードの一部ですが、ステータスバーにセパレータを設定しても表示されません。
<StatusBar DockPanel.Dock="Bottom" Height="30"> <StatusBarItem> <TextBlock>item 1</TextBlock> </StatusBarItem> <StatusBarItem> <Separator /> ← セパレータはここ </StatusBarItem> <StatusBarItem> <TextBlock>item 2</TextBlock> </StatusBarItem> </StatusBar>
このコードは一見すると正しいように見えますが、間違っています。Separator 要素は StatusBarItem 要素の子要素にする必要はありません。正しくは以下のようにします。
<StatusBar DockPanel.Dock="Bottom" Height="30"> <StatusBarItem> <TextBlock>item 1</TextBlock> </StatusBarItem> <Separator /> ← セパレータはここ <StatusBarItem> <TextBlock>item 2</TextBlock> </StatusBarItem> </StatusBar>
時間のかかる処理をする場合、進捗状況を表示するためにプログレスバーを使うことがよくあります。ここでは、ステータスバーの 1 つの区画内にプログレスバーを表示する手順を説明します。
Windows フォームアプリケーションであれば、実行時にプログレスバーを作成し、特定のパネル内に表示しますが、WPF アプリケーションではコントロールのレイアウトが面倒なので、チョット異なる手順を採用することにします。
1 つの区間内にあらかじめプログレスバーを配置しておきます。アプリケーションの起動時は非表示にしておき、プログレスバーを使うときに表示状態にし、処理が終了すると非表示に戻します。
下図の上は、デザイン時の状態、中は実行時の状態、下はプログレスバーを表示したところです。
デザイン時、つまり、XAML では ProgressBar コントロールの Visibility プロパティに Visibility.Hidden を指定するとコントロールの占有領域を確保したまま非表示になります。プログレスバーを起動するときに、Hidden から Visible に変更すると表示状態になり、プログレスバーが不要になった時点で再び Hidden に戻せば非表示になります。
次のサンプルプロジェクトは Visual Studio 2010 で作りました。フリーウエアですので、使用上の制限はありません。
MenuTest.zip (68,506 bytes)
なお、ProgressBar コントロールには問題があります。これについては、ProgressBar コントロールのページで説明します。
−以上−