Étant donné le code suivant:
<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>
En XAML, existe-t-il un moyen de créer des menus vérifiables qui s’excluent mutuellement? Où est l'utilisateur vérifie l'élément2, les éléments 1 et 3 sont automatiquement désélectionnés.
Je peux accomplir cela dans le code en surveillant les événements de clic dans le menu, en déterminant quel élément a été coché et en décochant les autres paramètres Je pense qu'il existe un moyen plus facile.
Des idées?
Ce n'est peut-être pas ce que vous recherchez, mais vous pouvez écrire une extension pour la classe MenuItem
qui vous permet d'utiliser quelque chose comme la propriété GroupName
de la classe RadioButton
. J'ai légèrement modifié this exemple pratique pour étendre de la même façon les contrôles ToggleButton
et je l'ai retravaillé un peu pour votre situation et j'ai trouvé ceci:
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;
}
}
}
}
}
Ensuite, dans le XAML, vous écririez:
<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>
C'est un peu pénible, mais cela offre l'avantage de ne pas vous obliger à écrire un code de procédure supplémentaire (à part la classe d'extension, bien sûr) pour l'implémenter.
Le mérite revient à Brad Cunningham, auteur de la solution ToggleButton originale.
Vous pouvez également utiliser un comportement. Comme celui-ci:
<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);
}
}
Ajoutant ceci au bas puisque je n'ai pas encore la réputation ...
Aussi utile que puisse être la réponse de Patrick, cela ne garantit pas que les éléments ne peuvent pas être décochés. Pour ce faire, le gestionnaire Checked doit être remplacé par un gestionnaire Click et par ce qui suit:
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;
}
}
Oui, cela peut être fait facilement en faisant de chaque MenuItem un RadioButton. Cela peut être fait en modifiant le modèle de MenuItem.
Cliquez avec le bouton droit sur MenuItem dans le volet gauche de Document-Outline> EditTemplate> EditCopy. Cela ajoutera le code pour l'édition sous Window.Resources.
Maintenant, vous devez faire seulement deux changements qui sont très simples.
une. Ajoutez le RadioButton avec des ressources pour masquer sa partie circulaire.
b. Change BorderThickness = 0 pour la partie MenuItem Border.
Ces modifications sont présentées ci-dessous sous forme de commentaires, le reste du style généré doit être utilisé tel quel:
<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>
Appliquer le style,
<MenuItem IsCheckable="True" Header="Open" Style="{DynamicResource MenuItemStyle1}"
Il n’existe pas de méthode intégrée à cet effet dans XAML, vous devez créer votre propre solution ou obtenir une solution existante, le cas échéant.
Puisqu'il n'y a pas de réponse samilar, je poste ma solution ici:
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();
}
}
En XAML, utilisez-le simplement comme un élément de menu habituel:
<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>
Assez simple et propre. Et bien sûr, vous pouvez faire de la GroupName
une propriété de dépendance par quelques codes supplémentaires, ce qui est identique aux autres.
BTW, si vous n'aimez pas la coche, vous pouvez le changer comme vous le souhaitez:
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();
}
Si vous avez utilisé beaucoup de cette RadioMenuItem
dans votre programme, une autre version plus efficace est présentée ci-dessous. Les données littérales sont acquises à partir de e.GetFlattenedPathGeometry().ToString()
dans le fragment de code précédent.
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;
}
Et enfin, si vous prévoyez de l'envelopper pour l'utiliser dans votre projet, vous devez masquer la propriété IsCheckable
de la classe de base, car le machenisme de contrôle automatique de la classe MenuItem
entraînera le comportement incorrect de l'état de contrôle radio.
private new bool IsCheckable { get; }
Ainsi, VS générera une erreur si un débutant tente de compiler XAML de la manière suivante:
// notez qu'il s'agit d'un mauvais usage!
<local:RadioMenuItem Header="111" GroupName="G1" IsCheckable="True"/>
// notez qu'il s'agit d'un mauvais usage!
Je pensais juste que jetterais ma solution, car aucune des réponses ne répondait à mes besoins. Ma solution complète est ici ...
WPF MenuItem en tant que RadioButton
Cependant, l'idée de base est d'utiliser ItemContainerStyle.
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Icon" Value="{DynamicResource RadioButtonResource}"/>
<EventSetter Event="Click" Handler="MenuItemWithRadioButtons_Click" />
</Style>
</MenuItem.ItemContainerStyle>
Et l'événement suivant doit être ajouté pour que le bouton RadioBase soit coché lorsque l'utilisateur clique sur MenuItem (sinon, vous devez cliquer exactement sur le bouton 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;
}
}
}
J'ai réalisé ceci en utilisant quelques lignes de code:
Commençons par déclarer une variable:
MenuItem LastBrightnessMenuItem =null;
Lorsque nous considérons un groupe de règles, il existe une probabilité d'utiliser un seul gestionnaire d'événements. Dans ce cas, nous pouvons utiliser cette logique:
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
}
Je constate que des éléments de menu qui s’excluent mutuellement s’excluent lorsque MenuItem est lié à une variable.
Mais il y a un problème: si vous cliquez sur l'élément de menu sélectionné, il devient invalide, comme l'indique le rectangle rouge habituel. Je l'ai résolu en ajoutant un gestionnaire pour MenuItem.Click qui empêche la désélection en définissant simplement IsChecked à true.
Le code ... Je lie à un type enum, j'utilise donc un convertisseur enum qui renvoie true si la propriété bound est égale au paramètre fourni. Voici le 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"/>
Et voici le code derrière:
private void MenuItem_OnClickDisallowUnselect(object sender, RoutedEventArgs e)
{
var menuItem = e.OriginalSource as MenuItem;
if (menuItem == null) return;
if (! menuItem.IsChecked)
{
menuItem.IsChecked = true;
}
}
Voici une autre approche qui utilise RoutedUICommands, une propriété d’énumération publique et DataTriggers. C'est une jolie solution verbeuse. Malheureusement, je ne vois aucun moyen de rendre le Style.Triggers plus petit, parce que je ne sais pas comment dire simplement que la valeur de liaison est la seule chose différente (BTW, pour les MVVMers, c’est un exemple terrible. J'ai tout mis dans la classe MainWindow juste pour que les choses restent simples.)
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
}
}
Créez simplement un modèle pour MenuItem qui contiendra un bouton radio avec un nom de groupe défini sur une valeur . Vous pouvez également modifier le modèle pour que les boutons radio ressemblent au glyphe de contrôle par défaut de MenuItem (pouvant être facilement extrait avec Expression Blend).
C'est tout!
Voici encore un autre moyen - difficile à atteindre, mais compatible avec MVVM, pouvant être lié et soumis à des tests extrêmement poussés. Si vous êtes libre d’ajouter un convertisseur à votre projet et que vous ne vous occupez pas de la corbeille sous la forme d’une nouvelle liste d’éléments à chaque fois que le menu contextuel s’ouvre, cela fonctionne vraiment bien. Cela répond à la question initiale de savoir comment fournir un ensemble d'éléments cochés mutuellement exclusifs dans un menu contextuel.
Je pense que si vous voulez extraire tout cela dans un contrôle utilisateur, vous pouvez en faire un composant de bibliothèque réutilisable à réutiliser dans votre application . Les composants utilisés sont Type3.Xaml avec une simple grille, un bloc de texte et le menu contextuel. . Cliquez avec le bouton droit n'importe où dans la grille pour faire apparaître le menu.
Un convertisseur de valeur nommé AllValuesEqualToBooleanConverter permet de comparer la valeur de chaque élément de menu à la valeur actuelle du groupe et d’afficher la coche en regard de l’élément de menu actuellement sélectionné.
Une classe simple représentant vos choix de menus est utilisée à titre d’illustration. Le conteneur d'échantillons utilise les propriétés Tuple with String et Integer, ce qui rend assez facile la création d'un extrait de texte lisible par l'homme, étroitement couplé, associé à une valeur conviviale pour l'ordinateur. Vous pouvez utiliser des chaînes seules ou String et un Enum pour garder une trace de la Valeur afin de prendre des décisions sur ce qui est en cours . Type3VM.cs est le ViewModel affecté au DataContext pour Type3.Xaml. Quoi que vous fassiez pour assigner votre contexte de données dans votre infrastructure d’application existante, utilisez le même mécanisme ici. Le cadre d'application utilisé s'appuie sur INotifyPropertyChanged pour communiquer les valeurs modifiées à WPF et à sa liaison obligatoire. Si vous avez des propriétés de dépendance, vous devrez peut-être modifier légèrement le code.
L'inconvénient de cette implémentation, à l'exception du convertisseur et de sa longueur, est qu'une liste de déchets est créée chaque fois que le menu contextuel est ouvert. Cela convient probablement pour les applications mono-utilisateur, mais vous devez en être conscient.
L'application utilise une implémentation de RelayCommand qui est facilement disponible sur le site Web Haacked ou de toute autre classe d'assistance compatible avec ICommand disponible dans n'importe quel framework que vous utilisez.
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>
{ }
Un petit ajout à la réponse de @Patrick.
Comme @ MK10 l'a mentionné, cette solution permet à l'utilisateur de désélectionner tous les éléments d'un groupe. Mais les changements qu'il a suggérés ne fonctionnent pas pour moi maintenant. Peut-être que le modèle WPF a été modifié depuis cette époque, mais maintenant, l'événement Checked
ne se déclenche pas lorsqu'un élément est décoché.
Pour l'éviter, je suggérerais de traiter l'événement Unchecked
pour MenuItem
.
J'ai changé ces procédures:
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
}
et a ajouté le gestionnaire suivant:
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;
}
Maintenant, l'élément coché reste coché lorsque l'utilisateur clique dessus une deuxième fois.
Vous pouvez faire quelque chose comme ça:
<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>
Il a des effets secondaires étranges visuellement (vous verrez quand vous l'utilisez), mais cela fonctionne néanmoins
Plusieurs années après avoir lu cet article avec les mots-clés que j'ai écrits ... je pensais qu'il y avait une solution facile, en wpf ... C'est peut-être moi, mais je pense que c'est un peu spécial d'avoir un arsenal aussi gigantesque pour une si petite chose comme solution acceptée. Je ne parle même pas de la solution avec 6likes je n'ai pas compris où cliquer pour avoir cette option.
Alors peut-être que ce n'est vraiment pas élégant du tout ... Mais voici une solution simple. Ce qu’il fait est simple .. une boucle à tous les éléments contenus par le parent, pour le mettre à false. La plupart du temps, les gens séparent cette partie des autres, bien sûr, ce n'est que correct dans ce cas.
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;
}
}
c'est tout et facile, xaml est un code classique avec absolument rien de particulier
<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>
Bien sûr, vous pourriez avoir besoin de la méthode click, ce n'est pas un problème, vous pouvez créer une méthode qui accepte un expéditeur d'objet et chacune de vos méthodes click utilisera cette méthode. C’est vieux, c’est moche, mais pour le moment ça marche ..__ Et j’ai du mal à imaginer autant de lignes de code pour une si petite chose, c’est probablement moi qui ai un problème avec xaml, mais cela semble incroyable de devoir faire ceci pour n'avoir qu'un seul menuitem sélectionné.