WisdomSoft - for your serial experiences.

6.7 メニュー

標準的なウィンドウのメニューバーや、右クリックで表示されるショートカットメニューを表示する方法を解説します。WPF のメニューは、他のコントロールと同じ UIElement から派生しているため、他のコントロールとの融合が容易で、柔軟なレイアウトが可能です。

6.7.1 メニューを表示する

WPF では、ウィンドウ上に表示されるメニューも UIElement から派生しています。古い Windows API 時代から使われていたメニューは独自の機能として提供されていたため、コントロールとの統合が困難でした。そのため、メニューは他のコントロールとは異なるコードで管理しなければならず、柔軟性にも問題がありました。WPF では、メニューもまた UIElement として扱うことができるため、自由にレイアウトをしたり、他のコントロールと組み合わせることができます。

メニューを表示するには System.Windows.Controls.Menu クラスを使います。

System.Windows.Controls.Menu クラス
System.Object 
   System.Windows.Threading.DispatcherObject 
     System.Windows.DependencyObject 
       System.Windows.Media.Visual 
         System.Windows.UIElement 
           System.Windows.FrameworkElement 
             System.Windows.Controls.Control 
               System.Windows.Controls.ItemsControl 
                 System.Windows.Controls.Primitives.MenuBase 
                  System.Windows.Controls.Menu
public class Menu : MenuBase

Menu クラスは、任意の数の項目を保持することができる ItemsControl を継承しているため、Items プロパティから項目となるオブジェクトを追加することができます。当然、任意の UIElement を項目として表示することができるため、通常のテキストの他に、ボタンや図形などをメニュー項目として表示することも可能です。

このクラスのコンストラクタは、パラメータを受け取りません。

Menu クラスのコンストラクタ
public Menu ()

メニューは UIElement として配置できるので、必ずしもウィンドウの上部に貼り付ける必要はありません。ウィンドウ上のあらゆる場所に自由に配置することができます。しかし、通常はウィンドウの上部に配置することになるでしょう。

コード1
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;
using System.Windows.Media;

class Test {
	[STAThread]
	public static void Main() {
		Button button = new Button();
		button.Content = "未来人";

		Ellipse ellipse = new Ellipse();
		ellipse.Fill = Brushes.Red;
		ellipse.Width = 100;
		ellipse.Height = 50;

		Menu menu = new Menu();
		menu.Items.Add("宇宙人");
		menu.Items.Add(button);
		menu.Items.Add(ellipse);

		StackPanel panel = new StackPanel();
		panel.Children.Add(menu);

		Window wnd = new Window();
		wnd.Content = panel;

		Application app = new Application();
		app.Run(wnd);
	}
}
実行結果
コード1 実行結果

コード1は、生成した Menu オブジェクトの Items プロパティから文字列、ボタン、楕円形オブジェクトをそれぞれ追加しています。Menu クラスは、リストボックスなどと同様に ItemsControl を継承しているため、任意のオブジェクトを追加して表示することができます。実行結果を見れば、メニュー項目としてボタンや楕円形も適切に表示されていることが確認できます。

6.7.2 メニュー項目

Items プロパティに文字列やボタンを追加する方法では、メニュー項目のドロップダウンを表示させることができません。一般的なアプリケーションで見られる、メニュー項目をクリックするとドロップダウンが展開されて、サブ項目の一覧が表示される動作を実現するには System.Windows.Controls.MenuItem クラスをメニュー項目として追加する必要があります。

System.Windows.Controls.MenuItem クラス
System.Object 
   System.Windows.Threading.DispatcherObject 
     System.Windows.DependencyObject 
       System.Windows.Media.Visual 
         System.Windows.UIElement 
           System.Windows.FrameworkElement 
             System.Windows.Controls.Control 
               System.Windows.Controls.ItemsControl 
                 System.Windows.Controls.HeaderedItemsControl 
                  System.Windows.Controls.MenuItem
[StyleTypedPropertyAttribute(Property="ItemContainerStyle", StyleTargetType=typeof(MenuItem))] 
[LocalizabilityAttribute(LocalizationCategory.Menu)] 
[TemplatePartAttribute(Name="PART_Popup", Type=typeof(Popup))] 
public class MenuItem : HeaderedItemsControl, ICommandSource

MenuItem クラスもまた、ItemsControl クラスを継承していることに注目してください。MenuItem は、メニューの項目として任意のオブジェクトを表示することができるコントロールですが、加えて任意の数のサブ項目を Items プロパティから追加することができます。

このクラスのコンストラクタは、パラメータを受け取りません。

MenuItem クラスのコンストラクタ
public MenuItem ()

仕組みは、タブコントロールの TabControl クラスと TabItem クラスの関係と同じです。MenuItem クラスは、メニューの項目に特化した機能を提供します。

MenuItem オブジェクトがメニュー項目として追加された場合は Header プロパティに設定されているオブジェクトが項目として表示されます。

HeaderedItemsControl クラス Header プロパティ
[BindableAttribute(true)] 
public Object Header { get; set; }

Header プロパティは Object 型なので、文字列や UIElement など任意のオブジェクトを設定することができます。このプロパティが返すオブジェクトが、項目として表示されます。

ドロップダウンに表示されるサブ項目の一覧は、MenuItem オブジェクトの Items プロパティに設定されているオブジェクトが利用されます。

コード2
using System;
using System.Windows;
using System.Windows.Controls;

class Test {
	[STAThread]
	public static void Main() {
		MenuItem fileItem = new MenuItem();

		fileItem.Header = "ファイル";
		fileItem.Items.Add("開く");
		fileItem.Items.Add("終了");

		Menu menu = new Menu();
		menu.Items.Add(fileItem);

		StackPanel panel = new StackPanel();
		panel.Children.Add(menu);

		Window wnd = new Window();
		wnd.Content = panel;

		Application app = new Application();
		app.Run(wnd);
	}
}
実行結果
コード2 実行結果

コード2は、menu オブジェクトの Items プロパティに MenuItem 型の fileItem オブジェクトを設定しています。fileItem 変数には、Header プロパティに "ファイル" という文字列を設定しているため、トップレベルのメニューには「ファイル」という項目として表示されます。そして、表示された「ファイル」メニュー項目をクリックすると、fileItem 変数の Items プロパティが保有している項目のリストが表示されます。このプログラムでは、単純に文字列を設定しているので、Items プロパティに追加した文字列がそのまま表示されます。

MenuItem クラスの Items には任意の項目を設定することができるため、MenuItem オブジェクトのサブ項目に MenuItem オブジェクトを設定することも可能です。MenuItem をサブ項目として表示させたり、ドロップダウンからさらに深いドロップダウンを表示させたい場合などに必要となるでしょう。

アプリケーションが、メニュー項目のクリックに反応するには、MenuItem オブジェクトの項目がクリックされると発生する Click イベントを利用します。

MenuItem クラス Click イベント
public event RoutedEventHandler Click

特別な事情がない限り、メニューの項目には MenuItem オブジェクトを設定し、MenuItem オブジェクトの Click イベントに反応することで、一般的なメニューの振る舞いを実現することができます。

コード3
using System;
using System.Windows;
using System.Windows.Controls;

class Test : Window {
	[STAThread]
	public static void Main() {
		Window wnd = new Test();
		Application app = new Application();
		app.Run(wnd);
	}

	Label label;

	public Test() {
		label = new Label();
		label.Content = "メニューから項目を選択してください";

		MenuItem item1 = new MenuItem();
		MenuItem item2 = new MenuItem();

		item1.Header = "開く";
		item1.Click += menuItemClick;
		item2.Header = "終了";
		item2.Click += menuItemClick;

		MenuItem fileItem = new MenuItem();
		fileItem.Header = "ファイル";
		fileItem.Items.Add(item1);
		fileItem.Items.Add(item2);

		Menu menu = new Menu();
		menu.Items.Add(fileItem);

		StackPanel panel = new StackPanel();
		panel.Children.Add(menu);
		panel.Children.Add(label);

		Content = panel;
	}
	private void menuItemClick(Object sender, RoutedEventArgs e) {
		MenuItem item = (MenuItem)sender;
		label.Content = "「" + item.Header + "」が選択されました";
	}
}
実行結果
コード3 実行結果

コード3は、メニュー項目がクリックされると、画面上のラベルにどの項目がクリックされたのかをテキストとして表示するというプログラムです。このプログラムでは Menu オブジェクトに追加するトップレベルの「ファイル」項目となる fileItem 変数と、「ファイル」項目に追加されてる「開く」項目 item1 変数、「終了」項目 item2 変数が用意されています。

item1 と item2 オブジェクトの Click イベントに、menuItemClick() メソッドを参照するデリゲートを追加しています。そのため、「開く」項目か、まはた「終了」項目が選択されると、menuItemClick() メソッドが呼び出されます。

6.7.3 チェック可能な項目

MenuItem クラスの IsCheckable プロパティを true に設定することで、メニュー項目にチェックを入れることができるようになります。

MenuItem クラス IsCheckable プロパティ
[BindableAttribute(true)] 
public bool IsCheckable { get; set; }

既定でこのプロパティは false に設定されています。チェック可能なメニュー項目は、何らかの状態を切り替えることを目的とするようなメニュー項目に適しています。

メニュー項目がチェックされているかどうかは IsChecked プロパティから設定・取得できます。

MenuItem クラス IsChecked プロパティ
[BindableAttribute(true)] 
public bool IsChecked { get; set; }

このプロパティが true ならばチェック状態、false ならば非チェック状態となります。

チェック状態の変更を監視するには Checked イベント Unchecked イベントを利用します。継承関係はありませんが、これらの機能はチェックボックスと同じです。

MenuItem クラス Checked イベント
public event RoutedEventHandler Checked
MenuItem クラス UnChecked イベント
public event RoutedEventHandler Unchecked

項目がチェックされると Checked イベントが発生し、チェックが外されると Unchecked イベントが発生します。

コード4
using System;
using System.Windows;
using System.Windows.Controls;

class Test : Window {
	[STAThread]
	public static void Main() {
		Window wnd = new Test();
		Application app = new Application();
		app.Run(wnd);
	}

	Label label;

	public Test() {
		label = new Label();
		label.Content = "項目をチェックしてください";

		MenuItem checkItem = new MenuItem();
		checkItem.Header = "チェック項目";
		checkItem.IsCheckable = true;
		checkItem.Checked += menuItemChecked;
		checkItem.Unchecked += menuItemUnchecked;

		Menu menu = new Menu();
		menu.Items.Add(checkItem);

		StackPanel panel = new StackPanel();
		panel.Children.Add(menu);
		panel.Children.Add(label);

		Content = panel;
	}
	private void menuItemChecked(Object sender, RoutedEventArgs e) {
		label.Content = "チェックされました";
	}
	private void menuItemUnchecked(Object sender, RoutedEventArgs e) {
		label.Content = "解除されました";
	}
}
実行結果
コード4 実行結果

コード4は、メニューにチェック可能な項目を表示し、チェック状態が変更されると下部のラベルのテキストが変更するプログラムです。

6.7.4 メニュー項目の分割

関連するメニュー同士をグループ化して区切り線を入れたい場合は System.Windows.Controls.Separator クラスを用います。Separator クラスは、メニューの分割だけではなく、あらゆるコントロールの分割に利用することができます。 

System.Windows.Controls.Separator クラス
System.Object 
   System.Windows.Threading.DispatcherObject 
     System.Windows.DependencyObject 
       System.Windows.Media.Visual 
         System.Windows.UIElement 
           System.Windows.FrameworkElement 
             System.Windows.Controls.Control 
              System.Windows.Controls.Separator
[LocalizabilityAttribute(LocalizationCategory.None, Readability=Readability.Unreadable)]
public class Separator : Control

このクラスのコンストラクタは、パラメータを受け取りません。

Separator クラスのコンストラクタ
public Separator ()

Separator は、分割線を表示するだけのコントロールなので、特別な挙動は何もありません。

コード5
using System;
using System.Windows;
using System.Windows.Controls;

class Test {
	[STAThread]
	public static void Main() {
		MenuItem fileItem = new MenuItem();
		fileItem.Header = "ファイル";
		fileItem.Items.Add("新規作成");
		fileItem.Items.Add("開く");
		fileItem.Items.Add(new Separator());
		fileItem.Items.Add("保存");
		fileItem.Items.Add("名前を付けて保存");
		fileItem.Items.Add(new Separator());
		fileItem.Items.Add("終了");

		Menu menu = new Menu();
		menu.Items.Add(fileItem);

		StackPanel panel = new StackPanel();
		panel.Children.Add(menu);

		Window wnd = new Window();
		wnd.Content = panel;

		Application app = new Application();
		app.Run(wnd);
	}
}
実行結果
コード5 実行結果

コード5は、一般的なツールアプリケーションで見られる「ファイル」メニューを再現しています。ファイル作成を行う項目郡、保存を行う項目郡などをグループ化するために、分割線を表示しています。

6.7.5 ショートカットメニュー

コントロール上で右クリックしたときに表示されるショットカットメニューは System.Windows.Controls.ContextMenu クラスを使います。このクラスもまた、Menu クラスと同様に ItemsControl クラスから派生しています。基本的な使い方は Menu と同じであると考えてください。

System.Windows.Controls.ContextMenu クラス
System.Object 
   System.Windows.Threading.DispatcherObject 
     System.Windows.DependencyObject 
       System.Windows.Media.Visual 
         System.Windows.UIElement 
           System.Windows.FrameworkElement 
             System.Windows.Controls.Control 
               System.Windows.Controls.ItemsControl 
                 System.Windows.Controls.Primitives.MenuBase 
                  System.Windows.Controls.ContextMenu
public class ContextMenu : MenuBase

このクラスのコンストラクタは、パラメータを受け取りません。

ContextMenu クラスのコンストラクタ
public ContextMenu ()

ContextMenu コントロールは、他のコントロールとは違い親を保有することができません。このクラスのオブジェクトを、他のコントロールのコンテンツとして追加したり、パネルに追加しようとした場合は例外が発生します。

ContextMenu クラスのオブジェクトは、FrameworkElement クラスが提供する ContextMenu プロパティに設定できます。ContextMenu プロパティは、オブジェクトが右クリックされたときに表示するショットカットメニューを設定するためのプロパティです。 

FrameworkElement クラス ContextMenu プロパティ
public ContextMenu ContextMenu { get; set; }

右クリックしたときにショートカットメニューを表示させたいオブジェクトの ContextMenu プロパティに、ContextMenu オブジェクトを設定してください。

コード6
using System;
using System.Windows;
using System.Windows.Controls;

class Test {
	[STAThread]
	public static void Main() {
		ContextMenu contextMenu = new ContextMenu();
		contextMenu.Items.Add("赤薔薇様");
		contextMenu.Items.Add("黄薔薇様");
		contextMenu.Items.Add("白薔薇様");

		Window wnd = new Window();
		wnd.ContextMenu  = contextMenu;

		Application app = new Application();
		app.Run(wnd);
	}
}
実行結果
コード6 実行結果

コード6では、Window オブジェクトの CohntextMenu プロパティにショットカットメニューを設定しています。そのため、このプログラムではウィンドウのクライアント領域上で右クリックするとショートカットメニューが表示されます。

6.7.6 コマンドを使う

5.4 コマンド」で前述したように、WPF では、イベントによって発生した何らかのアクションを抽象化し、アクションを処理するイベントハンドラと関連付けるめの機能を CommandBinding クラスで提供しています。ApplicationCommands クラスで提供している基本的なコマンド群と、コマンドを処理するイベントハンドらを結ぶ CommandBinding を作成すれば、複数のメニュー項目の間でコマンドを共有することができます。

例えば、Menu クラスで表示するメニュー項目と ContextMenu が表示するメニュー項目が、意味的に同じ処理をするというプログラムは一般的です。メニュー項目は Click イベントでイベントを処理することができましたが、同じ処理を行うメニュー項目のイベントハンドラを個別に用意するのは不適切です。

同一の MenuItem クラスであれば Click イベントで起動するイベントハンドラを共有する方法もありますが、Click イベントの型と異なるイベントから同一の意味を持つ処理を起動させることはできません。この場合は、個別にイベントハンドラを実装し、イベントハンドらから共通するメソッドを呼び出すという作業が必要になります。

コマンドを用いれば、コントロールで発生するイベントと、意味的なレベルで共通する作業を接続する管理が不要になります。MenuItem クラスは、自分がクリックされたときに起動するコマンドを表す Command プロパティを提供しています。

MenuItem クラス Command プロパティ
[LocalizabilityAttribute(LocalizationCategory.NeverLocalize)] 
[BindableAttribute(true)] 
public ICommand Command { get; set; }

このプロパティに有効なコマンドが設定されている場合、項目がクリックされると指定されているコマンドが実行されます。メニュー項目から辿った親コントロールがコマンドに対応する CommandBinding を保有している場合、CommandBinding に関連付けられているイベントハンドラが呼び出されます。このとき、CanExecute の結果が false の場合、項目は自動的に無効化されます。そのため、イベントを実行できるかどうかによる、コントロールの無効化と有効化の処理もコマンドに統合することができます。

コード7
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

class Test : Window {
	[STAThread]
	public static void Main() {
		Test wnd = new Test();
		Application app = new Application();
		app.Run(wnd);
	}

	public Test() {
		CommandBinding cmdBinding =
			new CommandBinding(ApplicationCommands.Close);
		cmdBinding.Executed += executed;
		cmdBinding.CanExecute += canExecute;

		MenuItem exitMenuItem = new MenuItem();
		exitMenuItem.Header = "終了";
		exitMenuItem.Command = ApplicationCommands.Close;

		MenuItem fileMenuItem = new MenuItem();
		fileMenuItem.Header = "ファイル";
		fileMenuItem.Items.Add(exitMenuItem);

		Menu menu = new Menu();
		menu.Items.Add(fileMenuItem);

		StackPanel panel = new StackPanel();
		panel.Children.Add(menu);

		MenuItem exitContextItem = new MenuItem();
		exitContextItem.Header = "終了";
		exitContextItem.Command = ApplicationCommands.Close;

		ContextMenu contextMenu = new ContextMenu();
		contextMenu.Items.Add(exitContextItem);
		contextMenu.CommandBindings.Add(cmdBinding);

		Content = panel;
		ContextMenu = contextMenu;
		CommandBindings.Add(cmdBinding);
	}

	public void executed(Object sender, ExecutedRoutedEventArgs e) {
		Close();
	}
	public void canExecute(Object sender, CanExecuteRoutedEventArgs e) {
		e.CanExecute = true;
	}
}
実行結果
コード7 実行結果

コード7は、Menu コントロール内で表示される MenuItem オブジェクトと、ContextMenu コントロール内で表示される MenuItem オブジェクトに ApplicationCommands.Close コマンドを設定しています。そのため、これらのメニュー項目は Close が実行可能かどうかを調べ、実行することができる場合にのみクリックすることができます。

Menu の親となる Test オブジェクト自身と、ContextMenu オブジェクトには、CommandBindings プロパティから executed() メソッドと canExecute() を呼び出す Close コマンドと関連付けた CommandBinding オブジェクトを設定しています。canExecute() は常に true を返すため、メニュー項目はクリック可能になりますが、状況に応じて false を返せば、コマンドを実行できないときは項目が無効化されるようになります。

メニュー項目をクリックすると、executed() メソッドが呼び出されることが確認できます。このプログラムでは、ApplicationCommands.Close コマンドの意味に合わせて、Close コマンドが発生するとウィンドウを閉じてアプリケーションを終了させるようにしています。ウィンドウをプログラムから閉じるには Close() メソッドを呼びします。

Window クラス Close() メソッド
public void Close ()

ウィンドウ上部に表示されるメニューバーの「終了」ボタンからでも、右クリックで表示される「終了」ボタンからでも、適切に Close コマンドが発生していることを確認してください。