Skip to content

[WPF] ContextMenuのCheckBoxをThreeStateで表示する

ContextMenuに3つの状態を遷移するチェックボックスを付けたくなりました。
簡単にできないか調べたのですができないようだったので自作。
大した作業ではないのですが、もしまた作ることになったときに面倒なのでまとめておきます。
標準のMenuItemはIsThreeStateを使えない、CheckBoxと同様にIsThreeStateを付けてくれればこんなことをしなくてもいいのですが。

具体的には、MenuItemを継承して状態フラグを追加したMenuItemを作っておいて、それに表示用のStyleをくっつけるだけです。

完成図

こんな感じに3パターンでチェックボックスを表示できます。









テキストの位置がズレてますが3ステートが主題なので無視しています。
たぶんスタイルになにか足りてません。わかったら直します。

MenuItemを継承

Styleから参照できるように依存関係プロパティを追加するだけ。
ここではフラグをbool!で宣言しています。

using System.Windows;
using System.Windows.Controls;

namespace Sample_ThreeStateMenuItem
{
    /// <summary>チェックボックスで3つの状態を表示できるMenuItem</summary>
    public class ThreeStateMenuItem : MenuItem
    {
        /// <summary>MenuItemの状態の依存関係プロパテ意</summary>
        public static readonly DependencyProperty ThreeStateProperty =
            DependencyProperty.Register("ThreeState", typeof(bool?), typeof(ThreeStateMenuItem), new PropertyMetadata(false));

        /// <summary>MenuItemの状態のCLRプロパティ</summary>
        public bool? ThreeState
        {
            get { return (bool?)GetValue(ThreeStateProperty); }
            set { SetValue(ThreeStateProperty, value); }
        }
    }
}

ThreeStateMenuItem用のスタイルを作成

3つの状態を表示するためのスタイルを作ります。
状態フラグをbool!型で宣言しているので、falseのときには非表示、trueのときにはチェック、nullのときにはハイフン、ということにしてあります。

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:Sample_ThreeStateMenuItem">

    <!-- ThreeStateMenuItem のスタイル -->
    <Style x:Key="ThreeStateMenuItemStyle" TargetType="{x:Type local:ThreeStateMenuItem}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:ThreeStateMenuItem}">
                    <Border x:Name="Border" BorderThickness="1">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" SharedSizeGroup="Icon"/>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="Auto" SharedSizeGroup="Shortcut"/>
                                <ColumnDefinition Width="13"/>
                            </Grid.ColumnDefinitions>
                            <Canvas Grid.Column="0" Width="13" Height="13" Margin="6,0,6,0" Background="Transparent">
                                <!-- Check Mark -->
                                <Path x:Name="PART_CheckMark" Grid.Column="0" Width="7" Height="7" Margin="3,3,0,0"
                                  Visibility="Collapsed" IsHitTestVisible="False"
                                  SnapsToDevicePixels="False" Stroke="Green" StrokeThickness="2"
                                  Data="M 0 4 L 3 7 M 3 7 L 7 0"/>
                                <!-- Indeterminate Mark -->
                                <Path x:Name="PART_IndeterminateMark" Grid.Column="0" Width="7" Height="7" Margin="3,3,0,0"
                                  Visibility="Collapsed" IsHitTestVisible="False"
                                  SnapsToDevicePixels="False" Stroke="Green" StrokeThickness="2"
                                  Data="M 0 4 L 7 4"/>
                            </Canvas>
                            <ContentPresenter x:Name="Icon" Margin="6,0,6,0" VerticalAlignment="Center" ContentSource="Icon" />
                            <ContentPresenter x:Name="HeaderHost" Grid.Column="1" ContentSource="Header" RecognizesAccessKey="True" />
                            <TextBlock x:Name="InputGestureText" Grid.Column="2" Text="{TemplateBinding InputGestureText}" Margin="4" VerticalAlignment="Center"/>
                        </Grid>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="ThreeState" Value="True">
                            <Setter TargetName="PART_CheckMark" Property="Visibility" Value="Visible"/>
                        </Trigger>
                        <Trigger Property="ThreeState" Value="{x:Null}">
                            <Setter TargetName="PART_IndeterminateMark" Property="Visibility" Value="Visible"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    
</ResourceDictionary>

動作を確認するために状態を切り替える処理を作る

こっちは主題ではないので適当にコンテキストメニューを開くたびに状態をひとつずつずらしていくようにします。

まずはXaml。

<Window x:Class="Sample_ThreeStateMenuItem.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Sample_ThreeStateMenuItem"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <!-- ContextMenu -->
    <Window.ContextMenu>
        <ContextMenu Placement="Center" x:Name="WindowContextMenu" Opened="WindowContextMenu_Opened">
            <!-- Resource -->
            <ContextMenu.Resources>
                <ResourceDictionary>
                    <!-- Style -->
                    <ResourceDictionary.MergedDictionaries>
                        <ResourceDictionary Source="/Sample_ThreeStateMenuItem;component/ThreeStateMenuItemStyle.xaml"/>
                    </ResourceDictionary.MergedDictionaries>
                </ResourceDictionary>
            </ContextMenu.Resources>
            <!-- Menu -->
            <local:ThreeStateMenuItem x:Name="ContextMenu_ThreeState"
                                      Style="{StaticResource ThreeStateMenuItemStyle}"
                                      Header="Three State"/>
        </ContextMenu>
    </Window.ContextMenu>
</Window>

コードビハインドはこんな感じ。

using System.Windows;
using System.Windows.Controls;

namespace Sample_ThreeStateMenuItem
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        /// <summary>コンテキストメニューを開いた際に呼び出されるイベント</summary>
        private void WindowContextMenu_Opened(object sender, RoutedEventArgs e)
        {
            ContextMenu menu = sender as ContextMenu;
            if (menu != null)
            {
                for (int i = 0; i < menu.Items.Count; ++i)
                {
                    ThreeStateMenuItem item = menu.Items[i] as ThreeStateMenuItem;
                    if (item != null)
                    {
                        // メニューを開くたびに状態をひとつ遷移する
                        if (item.ThreeState == true) item.ThreeState = false;
                        else if (item.ThreeState == false) item.ThreeState = null;
                        else item.ThreeState = true;
                    }
                }
            }
        }
    }
}

おしまい。

Be First to Comment

コメントを残す

メールアドレスが公開されることはありません。

*

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください