次のコードがあるとします:
<MenuItem x:Name="MenuItem_Root" Header="Root">
<MenuItem x:Name="MenuItem_Item1" IsCheckable="True" Header="item1" />
<MenuItem x:Name="MenuItem_Item2" IsCheckable="True" Header="item2"/>
<MenuItem x:Name="MenuItem_Item3" IsCheckable="True" Header="item3"/>
</MenuItem>
XAMLで、相互に排他的なチェック可能なメニュー項目を作成する方法はありますか?ユーザーがitem2をチェックする場所は、アイテムの1と3は自動的にオフになります。
メニューのクリックイベントを監視し、チェックされた項目を特定し、他のメニュー項目のチェックを外すことで、コードビハインドでこれを実現できます。もっと簡単な方法があると思います。
何か案は?
これはあなたが探しているものではないかもしれませんが、MenuItem
クラスのGroupName
プロパティのようなものを使用できるようにするRadioButton
クラスの拡張を書くことができます。同様にToggleButton
コントロールを拡張するための this の便利な例を少し変更し、状況に合わせて少し修正してこれを思いつきました。
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace WpfTest
{
public class MenuItemExtensions : DependencyObject
{
public static Dictionary<MenuItem, String> ElementToGroupNames = new Dictionary<MenuItem, String>();
public static readonly DependencyProperty GroupNameProperty =
DependencyProperty.RegisterAttached("GroupName",
typeof(String),
typeof(MenuItemExtensions),
new PropertyMetadata(String.Empty, OnGroupNameChanged));
public static void SetGroupName(MenuItem element, String value)
{
element.SetValue(GroupNameProperty, value);
}
public static String GetGroupName(MenuItem element)
{
return element.GetValue(GroupNameProperty).ToString();
}
private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//Add an entry to the group name collection
var menuItem = d as MenuItem;
if (menuItem != null)
{
String newGroupName = e.NewValue.ToString();
String oldGroupName = e.OldValue.ToString();
if (String.IsNullOrEmpty(newGroupName))
{
//Removing the toggle button from grouping
RemoveCheckboxFromGrouping(menuItem);
}
else
{
//Switching to a new group
if (newGroupName != oldGroupName)
{
if (!String.IsNullOrEmpty(oldGroupName))
{
//Remove the old group mapping
RemoveCheckboxFromGrouping(menuItem);
}
ElementToGroupNames.Add(menuItem, e.NewValue.ToString());
menuItem.Checked += MenuItemChecked;
}
}
}
}
private static void RemoveCheckboxFromGrouping(MenuItem checkBox)
{
ElementToGroupNames.Remove(checkBox);
checkBox.Checked -= MenuItemChecked;
}
static void MenuItemChecked(object sender, RoutedEventArgs e)
{
var menuItem = e.OriginalSource as MenuItem;
foreach (var item in ElementToGroupNames)
{
if (item.Key != menuItem && item.Value == GetGroupName(menuItem))
{
item.Key.IsChecked = false;
}
}
}
}
}
次に、XAMLで次のように記述します。
<MenuItem x:Name="MenuItem_Root" Header="Root">
<MenuItem x:Name="MenuItem_Item1" YourNamespace:MenuItemExtensions.GroupName="someGroup" IsCheckable="True" Header="item1" />
<MenuItem x:Name="MenuItem_Item2" YourNamespace:MenuItemExtensions.GroupName="someGroup" IsCheckable="True" Header="item2"/>
<MenuItem x:Name="MenuItem_Item3" YourNamespace:MenuItemExtensions.GroupName="someGroup" IsCheckable="True" Header="item3"/>
</MenuItem>
これは少し面倒ですが、それを実装するために(もちろん、拡張クラスを除いて)追加の手続き型コードを書くことを強制しないという特典があります。
クレジットは、元のToggleButtonソリューションを作成したBrad Cunninghamに贈られます。
ビヘイビアーを使用することもできます。このように:
<MenuItem Header="menu">
<MenuItem x:Name="item1" Header="item1" IsCheckable="true" ></MenuItem>
<MenuItem x:Name="item2" Header="item2" IsCheckable="true"></MenuItem>
<MenuItem x:Name="item3" Header="item3" IsCheckable="true" ></MenuItem>
<i:Interaction.Behaviors>
<local:MenuItemButtonGroupBehavior></local:MenuItemButtonGroupBehavior>
</i:Interaction.Behaviors>
</MenuItem>
public class MenuItemButtonGroupBehavior : Behavior<MenuItem>
{
protected override void OnAttached()
{
base.OnAttached();
GetCheckableSubMenuItems(AssociatedObject)
.ToList()
.ForEach(item => item.Click += OnClick);
}
protected override void OnDetaching()
{
base.OnDetaching();
GetCheckableSubMenuItems(AssociatedObject)
.ToList()
.ForEach(item => item.Click -= OnClick);
}
private static IEnumerable<MenuItem> GetCheckableSubMenuItems(ItemsControl menuItem)
{
var itemCollection = menuItem.Items;
return itemCollection.OfType<MenuItem>().Where(menuItemCandidate => menuItemCandidate.IsCheckable);
}
private void OnClick(object sender, RoutedEventArgs routedEventArgs)
{
var menuItem = (MenuItem)sender;
if (!menuItem.IsChecked)
{
menuItem.IsChecked = true;
return;
}
GetCheckableSubMenuItems(AssociatedObject)
.Where(item => item != menuItem)
.ToList()
.ForEach(item => item.IsChecked = false);
}
}
まだ評判がないので、これを一番下に追加します...
Patrickの回答と同じくらい役に立ちますが、アイテムのチェックを解除できないことは保証されません。そのためには、CheckedハンドラーをClickハンドラーに変更し、次のように変更する必要があります。
static void MenuItemClicked(object sender, RoutedEventArgs e)
{
var menuItem = e.OriginalSource as MenuItem;
if (menuItem.IsChecked)
{
foreach (var item in ElementToGroupNames)
{
if (item.Key != menuItem && item.Value == GetGroupName(menuItem))
{
item.Key.IsChecked = false;
}
}
}
else // it's not possible for the user to deselect an item
{
menuItem.IsChecked = true;
}
}
Samilarの回答がないため、ここに私の解決策を投稿します。
_public class RadioMenuItem : MenuItem
{
public string GroupName { get; set; }
protected override void OnClick()
{
var ic = Parent as ItemsControl;
if (null != ic)
{
var rmi = ic.Items.OfType<RadioMenuItem>().FirstOrDefault(i =>
i.GroupName == GroupName && i.IsChecked);
if (null != rmi) rmi.IsChecked = false;
IsChecked = true;
}
base.OnClick();
}
}
_
XAMLでは、通常のMenuItemとして使用します。
_<MenuItem Header="OOO">
<local:RadioMenuItem Header="111" GroupName="G1"/>
<local:RadioMenuItem Header="222" GroupName="G1"/>
<local:RadioMenuItem Header="333" GroupName="G1"/>
<local:RadioMenuItem Header="444" GroupName="G1"/>
<local:RadioMenuItem Header="555" GroupName="G1"/>
<local:RadioMenuItem Header="666" GroupName="G1"/>
<Separator/>
<local:RadioMenuItem Header="111" GroupName="G2"/>
<local:RadioMenuItem Header="222" GroupName="G2"/>
<local:RadioMenuItem Header="333" GroupName="G2"/>
<local:RadioMenuItem Header="444" GroupName="G2"/>
<local:RadioMenuItem Header="555" GroupName="G2"/>
<local:RadioMenuItem Header="666" GroupName="G2"/>
</MenuItem>
_
かなりシンプルでクリーン。そしてもちろん、いくつかの追加コードによってGroupName
を依存関係プロパティにすることができます。これは他のコードと同じです。
ところで、チェックマークが気に入らない場合は、チェックマークを好きなように変更できます。
_public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var p = GetTemplateChild("Glyph") as Path;
if (null == p) return;
var x = p.Width/2;
var y = p.Height/2;
var r = Math.Min(x, y) - 1;
var e = new EllipseGeometry(new Point(x,y), r, r);
// this is just a flattened dot, of course you can draw
// something else, e.g. a star? ;)
p.Data = e.GetFlattenedPathGeometry();
}
_
このRadioMenuItem
をプログラムで十分に使用した場合、以下に示す別のより効率的なバージョンがあります。リテラルデータは、前のコードスニペットのe.GetFlattenedPathGeometry().ToString()
から取得されます。
_private static readonly Geometry RadioDot = Geometry.Parse("M9,5.5L8.7,7.1 7.8,8.3 6.6,9.2L5,9.5L3.4,9.2 2.2,8.3 1.3,7.1L1,5.5L1.3,3.9 2.2,2.7 3.4,1.8L5,1.5L6.6,1.8 7.8,2.7 8.7,3.9L9,5.5z");
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var p = GetTemplateChild("Glyph") as Path;
if (null == p) return;
p.Data = RadioDot;
}
_
最後に、プロジェクトで使用するためにラップする場合は、IsCheckable
クラスの自動チェックメカニズムが無線チェック状態になるため、MenuItem
プロパティを基本クラスから非表示にする必要があります間違った動作をマークします。
_private new bool IsCheckable { get; }
_
したがって、初心者が次のようにXAMLをコンパイルしようとすると、VSでエラーが発生します。
//これは間違った使い方です。
_
<local:RadioMenuItem Header="111" GroupName="G1" IsCheckable="True"/>
_//これは間違った使い方です。
はい、これはすべてのMenuItemをRadioButtonにすることで簡単に行うことができます。これは、MenuItemのテンプレートを編集することで実行できます。
[ドキュメントアウトライン]左ペインの[メニューアイテム]を右クリックし、[編集テンプレート]> [編集コピー]を選択します。これにより、Window.Resourcesの下に編集用のコードが追加されます。
さて、あなたは非常に簡単な2つの変更を行う必要があります。
a。 RadioButtonをリソースとともに追加して、その円の部分を非表示にします。
b。 MenuItem BorderパーツのBorderThickness = 0を変更します。
これらの変更をコメントとして以下に示します。生成された残りのスタイルはそのまま使用する必要があります。
<Window.Resources>
<LinearGradientBrush x:Key="MenuItemSelectionFill" EndPoint="0,1" StartPoint="0,0">
<GradientStop Color="#34C5EBFF" Offset="0"/>
<GradientStop Color="#3481D8FF" Offset="1"/>
</LinearGradientBrush>
<Geometry x:Key="Checkmark">M 0,5.1 L 1.7,5.2 L 3.4,7.1 L 8,0.4 L 9.2,0 L 3.3,10.8 Z</Geometry>
<ControlTemplate x:Key="{ComponentResourceKey ResourceId=SubmenuItemTemplateKey, TypeInTargetAssembly={x:Type MenuItem}}" TargetType="{x:Type MenuItem}">
<Grid SnapsToDevicePixels="true">
<Rectangle x:Name="Bg" Fill="{TemplateBinding Background}" RadiusY="2" RadiusX="2" Stroke="{TemplateBinding BorderBrush}" StrokeThickness="1"/>
<Rectangle x:Name="InnerBorder" Margin="1" RadiusY="2" RadiusX="2"/>
<!-- Add RadioButton around the Grid
-->
<RadioButton Background="Transparent" GroupName="MENUITEM_GRP" IsHitTestVisible="False" IsChecked="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=MenuItem}}">
<RadioButton.Resources>
<Style TargetType="Themes:BulletChrome">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
</RadioButton.Resources>
<!-- Add RadioButton Top part ends here
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="24" SharedSizeGroup="MenuItemIconColumnGroup" Width="Auto"/>
<ColumnDefinition Width="4"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="37"/>
<ColumnDefinition SharedSizeGroup="MenuItemIGTColumnGroup" Width="Auto"/>
<ColumnDefinition Width="17"/>
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="Icon" ContentSource="Icon" Margin="1" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
<!-- Change border thickness to 0
-->
<Border x:Name="GlyphPanel" BorderBrush="#CDD3E6" BorderThickness="0" Background="#E6EFF4" CornerRadius="3" Height="22" Margin="1" Visibility="Hidden" Width="22">
<Path x:Name="Glyph" Data="{StaticResource Checkmark}" Fill="#0C12A1" FlowDirection="LeftToRight" Height="11" Width="9"/>
</Border>
<ContentPresenter Grid.Column="2" ContentSource="Header" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
<TextBlock Grid.Column="4" Margin="{TemplateBinding Padding}" Text="{TemplateBinding InputGestureText}"/>
</Grid>
</RadioButton>
<!-- RadioButton closed , thats it !
-->
</Grid>
...
</Window.Resources>
スタイルを適用し、
<MenuItem IsCheckable="True" Header="Open" Style="{DynamicResource MenuItemStyle1}"
答えがどれも私のニーズを満たさなかったので、私は自分の解決策を投入すると思いました。私の完全な解決策はここにあります...
ただし、基本的な考え方は、ItemContainerStyleを使用することです。
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Icon" Value="{DynamicResource RadioButtonResource}"/>
<EventSetter Event="Click" Handler="MenuItemWithRadioButtons_Click" />
</Style>
</MenuItem.ItemContainerStyle>
また、MenuItemがクリックされたときにRadioButtonがチェックされるように、次のイベントクリックを追加する必要があります(そうでない場合、RadioButtonを正確にクリックする必要があります)。
private void MenuItemWithRadioButtons_Click(object sender, System.Windows.RoutedEventArgs e)
{
MenuItem mi = sender as MenuItem;
if (mi != null)
{
RadioButton rb = mi.Icon as RadioButton;
if (rb != null)
{
rb.IsChecked = true;
}
}
}
XAMLでこれを行う組み込みの方法はありません。独自のソリューションを展開するか、利用可能な場合は既存のソリューションを取得する必要があります。
私は数行のコードを使用してこれを達成しました:
最初に変数を宣言します。
MenuItem LastBrightnessMenuItem =null;
メニュー項目のグループを検討する場合、単一のイベントハンドラーを使用する可能性があります。この場合、次のロジックを使用できます。
private void BrightnessMenuClick(object sender, RoutedEventArgs e)
{
if (LastBrightnessMenuItem != null)
{
LastBrightnessMenuItem.IsChecked = false;
}
MenuItem m = sender as MenuItem;
LastBrightnessMenuItem = m;
//Handle the rest of the logic here
}
シンプルなMVVMベースのソリューションは、シンプルなIValueConverterおよびCommandParameterMenuItemごと。
MenuItemを別のタイプのコントロールとして再スタイルする必要はありません。バインドされた値がCommandParameterと一致しない場合、MenuItemsは自動的に選択解除されます。
DataContext(ViewModel)のintプロパティ(MenuSelection)にバインドします。
<MenuItem x:Name="MenuItem_Root" Header="Root">
<MenuItem x:Name="MenuItem_Item1" IsCheckable="True" Header="item1" IsChecked="{Binding MenuSelection, ConverterParameter=1, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
<MenuItem x:Name="MenuItem_Item2" IsCheckable="True" Header="item2" IsChecked="{Binding MenuSelection, ConverterParameter=2, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
<MenuItem x:Name="MenuItem_Item3" IsCheckable="True" Header="item3" IsChecked="{Binding MenuSelection, ConverterParameter=3, Converter={StaticResource MatchingIntToBooleanConverter}, Mode=TwoWay}" />
</MenuItem>
値コンバーターを定義します。これにより、バインドされた値がコマンドパラメータに対してチェックされ、逆も同様です。
public class MatchingIntToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var paramVal = parameter as string;
var objVal = ((int)value).ToString();
return paramVal == objVal;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool)
{
var i = System.Convert.ToInt32((parameter ?? "0") as string);
return ((bool)value)
? System.Convert.ChangeType(i, targetType)
: 0;
}
return 0; // Returning a zero provides a case where none of the menuitems appear checked
}
}
リソースを追加
<Window.Resources>
<ResourceDictionary>
<local:MatchingIntToBooleanConverter x:Key="MatchingIntToBooleanConverter"/>
</ResourceDictionary>
</Window.Resources>
幸運を!
MenuItem.IsCheckedを変数にバインドすると、相互に排他的なメニュー項目が表示されることがわかりました。
ただし、奇妙な点が1つあります。選択したメニュー項目をクリックすると、通常の赤い長方形で示されているように、無効になります。 IsCheckedをtrueに戻すだけで、選択解除を防止するMenuItem.Clickのハンドラーを追加して解決しました。
コード...列挙型にバインドしているので、バインドされたプロパティが指定されたパラメーターと等しい場合にtrueを返す列挙コンバーターを使用します。 XAMLは次のとおりです。
<MenuItem Header="Black"
IsCheckable="True"
IsChecked="{Binding SelectedColor, Converter={StaticResource EnumConverter}, ConverterParameter=Black}"
Click="MenuItem_OnClickDisallowUnselect"/>
<MenuItem Header="Red"
IsCheckable="True"
IsChecked="{Binding SelectedColor, Converter={StaticResource EnumConverter}, ConverterParameter=Red}"
Click="MenuItem_OnClickDisallowUnselect"/>
そして、これが背後にあるコードです:
private void MenuItem_OnClickDisallowUnselect(object sender, RoutedEventArgs e)
{
var menuItem = e.OriginalSource as MenuItem;
if (menuItem == null) return;
if (! menuItem.IsChecked)
{
menuItem.IsChecked = true;
}
}
RoutedUICommands、パブリック列挙型プロパティ、およびDataTriggersを使用する別のアプローチを次に示します。これはかなり冗長なソリューションです。残念ながら、Style.Triggersを小さくする方法はありません。BindingValueが唯一の違いであるとだけ言う方法がわからないためです。 (ところで、MVVMersの場合、これはひどい例です。単純にするために、すべてをMainWindowクラスに入れました。)
MainWindow.xaml:
<Window x:Class="MutuallyExclusiveMenuItems.MainWindow"
xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
xmlns:view="clr-namespace:MutuallyExclusiveMenuItems"
Title="MainWindow" Height="350" Width="525">
<Window.CommandBindings>
<CommandBinding Command="{x:Static view:MainWindow.MenuItem1Cmd}"
CanExecute="CanExecute"
Executed="MenuItem1Execute" />
<CommandBinding Command="{x:Static view:MainWindow.MenuItem2Cmd}"
CanExecute="CanExecute"
Executed="MenuItem2Execute" />
<CommandBinding Command="{x:Static view:MainWindow.MenuItem3Cmd}"
CanExecute="CanExecute"
Executed="MenuItem3Execute" />
</Window.CommandBindings>
<Window.InputBindings>
<KeyBinding Command="{x:Static view:MainWindow.MenuItem1Cmd}" Gesture="Ctrl+1"/>
<KeyBinding Command="{x:Static view:MainWindow.MenuItem2Cmd}" Gesture="Ctrl+2"/>
<KeyBinding Command="{x:Static view:MainWindow.MenuItem3Cmd}" Gesture="Ctrl+3"/>
</Window.InputBindings>
<DockPanel>
<DockPanel DockPanel.Dock="Top">
<Menu>
<MenuItem Header="_Root">
<MenuItem Command="{x:Static view:MainWindow.MenuItem1Cmd}"
InputGestureText="Ctrl+1">
<MenuItem.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding CurrentMenuItem, Mode=OneWay}"
Value="{x:Static view:MainWindow+CurrentItemEnum.EnumItem1}">
<Setter Property="MenuItem.IsChecked" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<MenuItem Command="{x:Static view:MainWindow.MenuItem2Cmd}"
InputGestureText="Ctrl+2">
<MenuItem.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding CurrentMenuItem, Mode=OneWay}"
Value="{x:Static view:MainWindow+CurrentItemEnum.EnumItem2}">
<Setter Property="MenuItem.IsChecked" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<MenuItem Command="{x:Static view:MainWindow.MenuItem3Cmd}"
InputGestureText="Ctrl+3">
<MenuItem.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding CurrentMenuItem, Mode=OneWay}"
Value="{x:Static view:MainWindow+CurrentItemEnum.EnumItem3}">
<Setter Property="MenuItem.IsChecked" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
</MenuItem>
</Menu>
</DockPanel>
</DockPanel>
</Window>
MainWindow.xaml.cs:
using System.Windows;
using System.Windows.Input;
using System.ComponentModel;
namespace MutuallyExclusiveMenuItems
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
#region Enum Property
public enum CurrentItemEnum { EnumItem1, EnumItem2, EnumItem3 };
private CurrentItemEnum _currentMenuItem;
public CurrentItemEnum CurrentMenuItem
{
get { return _currentMenuItem; }
set
{
_currentMenuItem = value;
OnPropertyChanged("CurrentMenuItem");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion Enum Property
#region Commands
public static RoutedUICommand MenuItem1Cmd =
new RoutedUICommand("Item_1", "Item1cmd", typeof(MainWindow));
public void MenuItem1Execute(object sender, ExecutedRoutedEventArgs e)
{
CurrentMenuItem = CurrentItemEnum.EnumItem1;
}
public static RoutedUICommand MenuItem2Cmd =
new RoutedUICommand("Item_2", "Item2cmd", typeof(MainWindow));
public void MenuItem2Execute(object sender, ExecutedRoutedEventArgs e)
{
CurrentMenuItem = CurrentItemEnum.EnumItem2;
}
public static RoutedUICommand MenuItem3Cmd =
new RoutedUICommand("Item_3", "Item3cmd", typeof(MainWindow));
public void MenuItem3Execute(object sender, ExecutedRoutedEventArgs e)
{
CurrentMenuItem = CurrentItemEnum.EnumItem3;
}
public void CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
#endregion Commands
}
}
GroupNameをある値に設定したRadioButtonを含むMenuItemのテンプレートを作成するだけです。 RadioButtonのテンプレートを変更して、MenuItemのデフォルトのチェックグリフ(Expression Blendで簡単に抽出できる)のようにすることもできます。
それでおしまい!
これはさらにもう1つの方法です–まったく簡単ではありませんが、MVVMと互換性があり、バインド可能で、ユニットテストが高度です。プロジェクトにコンバーターを追加する自由があり、コンテキストメニューが開くたびに新しいアイテムのリストの形で小さなゴミを気にしない場合、これは非常にうまく機能します。これは、コンテキストメニューで相互に排他的なチェック済みアイテムのセットを提供する方法についての元の質問を満たしています。
これをすべてユーザーコントロールに抽出したい場合は、再利用可能なライブラリコンポーネントにして、アプリケーション全体で再利用できます。使用されるコンポーネントは、単純なグリッド、1つのテキストブロック、およびコンテキストメニューを持つType3.Xamlです。グリッドの任意の場所を右クリックして、メニューを表示します。
AllValuesEqualToBooleanConverterという名前の値コンバーターを使用して、各メニュー項目の値をグループの現在の値と比較し、現在選択されているメニュー項目の横にチェックマークを表示します。
メニューには、メニューの選択肢を表す単純なクラスが使用されています。サンプルコンテナーは、文字列と整数のプロパティを持つタプルを使用しており、人間が読み取れるテキストのスニペットを機械に優しい値と組み合わせて、非常に簡単に結合できます。文字列を単独で、または文字列と列挙型を使用して、現在のものを決定するための値を追跡できます。 Type3VM.csは、Type3.XamlのDataContextに割り当てられるViewModelです。ただし、既存のアプリケーションフレームワークにデータコンテキストを割り当てようとする場合は、ここで同じメカニズムを使用します。使用中のアプリケーションフレームワークは、INotifyPropertyChangedに依存して、変更された値をWPFとそのバインディングgooに伝達します。依存関係プロパティがある場合は、コードを少し調整する必要があります。
この実装の欠点は、コンバーターとその長さは別として、コンテキストメニューが開かれるたびにガベージリストが作成されることです。シングルユーザーアプリケーションの場合、これはおそらく問題ありませんが、注意する必要があります。
アプリケーションは、Haacked Webサイトから簡単に利用できるRelayCommandの実装、または使用しているフレームワークで利用できる他のICommand互換ヘルパークラスを使用します。
public class Type3VM : INotifyPropertyChanged
{
private List<MenuData> menuData = new List<MenuData>(new[]
{
new MenuData("Zero", 0),
new MenuData("One", 1),
new MenuData("Two", 2),
new MenuData("Three", 3),
});
public IEnumerable<MenuData> MenuData { get { return menuData.ToList(); } }
private int selected;
public int Selected
{
get { return selected; }
set { selected = value; OnPropertyChanged(); }
}
private ICommand contextMenuClickedCommand;
public ICommand ContextMenuClickedCommand { get { return contextMenuClickedCommand; } }
private void ContextMenuClickedAction(object clicked)
{
var data = clicked as MenuData;
Selected = data.Item2;
OnPropertyChanged("MenuData");
}
public Type3VM()
{
contextMenuClickedCommand = new RelayCommand(ContextMenuClickedAction);
}
private void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class MenuData : Tuple<String, int>
{
public MenuData(String DisplayValue, int value) : base(DisplayValue, value) { }
}
<UserControl x:Class="SampleApp.Views.Type3"
xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.Microsoft.com/expression/blend/2008"
xmlns:Views="clr-namespace:SampleApp.Views"
xmlns:Converters="clr-namespace:SampleApp.Converters"
xmlns:ViewModels="clr-namespace:SampleApp.ViewModels"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
d:DataContext="{d:DesignInstance ViewModels:Type3VM}"
>
<UserControl.Resources>
<Converters:AllValuesEqualToBooleanConverter x:Key="IsCheckedVisibilityConverter" EqualValue="True" NotEqualValue="False" />
</UserControl.Resources>
<Grid>
<Grid.ContextMenu>
<ContextMenu ItemsSource="{Binding MenuData, Mode=OneWay}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem" >
<Setter Property="Header" Value="{Binding Item1}" />
<Setter Property="IsCheckable" Value="True" />
<Setter Property="IsChecked">
<Setter.Value>
<MultiBinding Converter="{StaticResource IsCheckedVisibilityConverter}" Mode="OneWay">
<Binding Path="DataContext.Selected" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Views:Type3}}" />
<Binding Path="Item2" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Command" Value="{Binding Path=DataContext.ContextMenuClickedCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Views:Type3}}}" />
<Setter Property="CommandParameter" Value="{Binding .}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Grid.ContextMenu>
<Grid.RowDefinitions><RowDefinition Height="*" /></Grid.RowDefinitions>
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /></Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" FontSize="30" Text="Right Click For Menu" />
</Grid>
</UserControl>
public class AreAllValuesEqualConverter<T> : IMultiValueConverter
{
public T EqualValue { get; set; }
public T NotEqualValue { get; set; }
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
T returnValue;
if (values.Length < 2)
{
returnValue = EqualValue;
}
// Need to use .Equals() instead of == so that string comparison works, but must check for null first.
else if (values[0] == null)
{
returnValue = (values.All(v => v == null)) ? EqualValue : NotEqualValue;
}
else
{
returnValue = (values.All(v => values[0].Equals(v))) ? EqualValue : NotEqualValue;
}
return returnValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
[ValueConversion(typeof(object), typeof(Boolean))]
public class AllValuesEqualToBooleanConverter : AreAllValuesEqualConverter<Boolean>
{ }
@Patrick回答への小さな追加。
@ MK10で述べたように、このソリューションでは、ユーザーはグループ内のすべてのアイテムの選択を解除できます。しかし、彼が提案した変更は今のところ私にはうまくいきません。たぶん、それ以来WPFモデルが変更されましたが、アイテムがチェックされていないときにChecked
イベントが発生しなくなりました。
これを回避するには、Unchecked
のMenuItem
イベントを処理することをお勧めします。
これらの手順を変更しました:
private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is MenuItem menuItem))
return;
var newGroupName = e.NewValue.ToString();
var oldGroupName = e.OldValue.ToString();
if (string.IsNullOrEmpty(newGroupName))
{
RemoveCheckboxFromGrouping(menuItem);
}
else
{
if (newGroupName != oldGroupName)
{
if (!string.IsNullOrEmpty(oldGroupName))
{
RemoveCheckboxFromGrouping(menuItem);
}
ElementToGroupNames.Add(menuItem, e.NewValue.ToString());
menuItem.Checked += MenuItemChecked;
menuItem.Unchecked += MenuItemUnchecked; // <-- ADDED
}
}
}
private static void RemoveCheckboxFromGrouping(MenuItem checkBox)
{
ElementToGroupNames.Remove(checkBox);
checkBox.Checked -= MenuItemChecked;
checkBox.Unchecked -= MenuItemUnchecked; // <-- ADDED
}
次のハンドラを追加しました:
private static void MenuItemUnchecked(object sender, RoutedEventArgs e)
{
if (!(e.OriginalSource is MenuItem menuItem))
return;
var isAnyItemChecked = ElementToGroupNames.Any(item => item.Value == GetGroupName(menuItem) && item.Key.IsChecked);
if (!isAnyItemChecked)
menuItem.IsChecked = true;
}
チェックされたアイテムは、ユーザーがもう一度クリックしたときにチェックされたままになります。
あなたはこのようなことをすることができます:
<Menu>
<MenuItem Header="File">
<ListBox BorderThickness="0" Background="Transparent">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<MenuItem IsCheckable="True" IsChecked="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" Header="{Binding Content, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.Items>
<ListBoxItem Content="Test" />
<ListBoxItem Content="Test2" />
</ListBox.Items>
</ListBox>
</MenuItem>
</Menu>
視覚的には奇妙な副作用がありますが(使用するとわかります)、それでも機能します
私が書いたキーワードでこの投稿を見てから数年後... wpfには簡単な解決策があると思いました...多分それは私ですが、小さなことのためにそのような巨大な武器を持っていることは少し特別だと思います受け入れられた解決策として。私は6likesを使用したソリューションについては話していません。このオプションを使用するためにクリックする場所がわかりません。
したがって、おそらくそれはまったくエレガントではありません...しかし、ここでは簡単な解決策を示します。それが行うことは簡単です..親に含まれるすべての要素へのループで、falseに設定します。ほとんどの場合、この部分を他の部分から分割しますが、もちろんこの場合にのみ正しくなります。
private void MenuItem_Click_1(object sender, RoutedEventArgs e)
{
MenuItem itemChecked = (MenuItem)sender;
MenuItem itemParent = (MenuItem)itemChecked.Parent;
foreach (MenuItem item in itemParent.Items)
{
if (item == itemChecked)continue;
item.IsChecked = false;
}
}
それはすべて簡単です。xamlは古典的なコードであり、特に何もありません。
<MenuItem Header="test">
<MenuItem Header="1" Click="MenuItem_Click_1" IsCheckable="True" StaysOpenOnClick="True"/>
<MenuItem Header="2" Click="MenuItem_Click_1" IsCheckable="True" StaysOpenOnClick="True"/>
</MenuItem>
もちろん、clickメソッドが必要な場合もありますが、問題ありません。オブジェクトの送信者を受け入れるメソッドを作成すると、各clickメソッドがこのメソッドを使用します。古くて醜いですが、しばらくの間は機能します。そして、ちょっとしたことで多くのコード行を想像するのにいくつかの問題があります.xamlに問題があるのはおそらく私ですが、メニュー項目を1つだけ選択するためにこれを実行する必要があるのは信じられないことのようです。