Voci di menu controllabili reciprocamente esclusivi?

Dato il seguente codice:

     

In XAML, c’è un modo per creare menuitem controllabili che si escludono a vicenda? Dove l’utente controlla l’articolo 2, gli articoli 1 e 3 vengono automaticamente deselezionati.

Posso farlo nel codice sottostante monitorando gli eventi click sul menu, determinando quale elemento è stato selezionato e deselezionando gli altri menu. Penso che ci sia un modo più semplice.

Qualche idea?

Questo potrebbe non essere quello che stai cercando, ma potresti scrivere un’estensione per la class MenuItem che ti permette di usare qualcosa come la proprietà GroupName della class RadioButton . Ho leggermente modificato questo pratico esempio per estendere in modo simile i controlli ToggleButton e rielaborato un po ‘per la tua situazione e ho trovato questo:

 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 ElementToGroupNames = new Dictionary(); 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; } } } } } 

Quindi, nello XAML, scriveresti:

       

È un po ‘un problema, ma offre il vantaggio di non costringerti a scrivere alcun codice procedurale addizionale (a parte la class di estensione, ovviamente) per implementarlo.

Il merito va a Brad Cunningham, autore della soluzione ToggleButton originale.

Puoi anche usare un comportamento. Come questo:

         public class MenuItemButtonGroupBehavior : Behavior { 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 GetCheckableSubMenuItems(ItemsControl menuItem) { var itemCollection = menuItem.Items; return itemCollection.OfType().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); } } 

Sì, questo può essere fatto facilmente rendendo ogni MenuItem un RadioButton. Questo può essere fatto modificando Template of MenuItem.

  1. Fare clic con il pulsante destro del mouse su MenuItem nel riquadro sinistro del documento-documento> Modifica modello> ModificaCopia. Questo aggiungerà il codice per la modifica in Window.Resources.

  2. Ora, devi fare solo due modifiche che sono molto semplici.

    MenuItem reciprocamente esclusivi un. Aggiungi il RadioButton con alcune risorse per hide la sua parte circolare.

    b. Cambia BorderThickness = 0 per la parte Border MenuItem.

    Queste modifiche sono mostrate sotto come commenti, il resto dello stile generato dovrebbe essere usato così com’è:

          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                               ...  
  3. Applica lo stile,

      

Aggiungendo questo in fondo poiché non ho ancora la reputazione …

Per quanto sia utile la risposta di Patrick, non garantisce che gli elementi non possano essere deselezionati. Per fare ciò, il gestore verificato deve essere modificato in un gestore Click e modificato come segue:

 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; } } 

Non esiste un modo integrato per farlo in XAML, sarà necessario eseguire il rollover della propria soluzione o ottenere una soluzione esistente, se disponibile.

Ho solo pensato di buttare la mia soluzione, poiché nessuna delle risposte ha soddisfatto i miei bisogni. La mia soluzione completa è qui …

WPF MenuItem come RadioButton

Tuttavia, l’idea di base è usare ItemContainerStyle.

    

E il seguente evento dovrebbe essere aggiunto in modo che il RadioButton sia selezionato quando si fa clic su MenuItem (altrimenti è necessario fare clic esattamente sul 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; } } } 

Poiché non c’è una risposta simile, inserisco qui la mia soluzione:

 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().FirstOrDefault(i => i.GroupName == GroupName && i.IsChecked); if (null != rmi) rmi.IsChecked = false; IsChecked = true; } base.OnClick(); } } 

In XAML basta usarlo come un normale MenuItem:

                

Abbastanza semplice e pulito. E naturalmente è ansible rendere GroupName una proprietà di dipendenza con alcuni codici aggiuntivi, che è lo stesso di altri.

A proposito, se non ti piace il segno di spunta, puoi cambiarlo in qualsiasi cosa tu voglia:

 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, eg a star? ;) p.Data = e.GetFlattenedPathGeometry(); } 

Se hai usato molto questo RadioMenuItem nel tuo programma, c’è un’altra versione più efficiente mostrata sotto. I dati letterali sono acquisiti da e.GetFlattenedPathGeometry().ToString() nel precedente snippet di codice.

 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; } 

Infine, se si prevede di includerlo per l’utilizzo nel progetto, è necessario hide la proprietà IsCheckable dalla class base, poiché il machenismo di controllo automatico della class MenuItem porterà il segno di spunta a un comportamento errato.

 private new bool IsCheckable { get; } 

Quindi VS darà un errore se un principiante tenta di compilare XAML in questo modo:

// nota che questo è un uso sbagliato!

// nota che questo è un uso sbagliato!

Ho raggiunto questo utilizzando un paio di righe di codice:

Prima dichiarare una variabile:

 MenuItem LastBrightnessMenuItem =null; 

Quando consideriamo un gruppo di menu, esiste una probabilità di utilizzare un singolo gestore di eventi. In questo caso possiamo usare questa logica:

  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 } 

Trovo che ottengo le voci di menu che si escludono a vicenda quando lego MenuItem.IsChecked a una variabile.

Ma ha una stranezza: se fai clic sulla voce di menu selezionata, essa diventa non valida, mostrata dal solito rettangolo rosso. L’ho risolto aggiungendo un gestore per MenuItem.Click che impedisce la deselezione semplicemente impostando IsChecked su true.

Il codice … Sono vincolato a un tipo enum, quindi utilizzo un convertitore enum che restituisce true se la proprietà associata è uguale al parametro fornito. Ecco lo XAML:

    

Ed ecco il codice dietro:

  private void MenuItem_OnClickDisallowUnselect(object sender, RoutedEventArgs e) { var menuItem = e.OriginalSource as MenuItem; if (menuItem == null) return; if (! menuItem.IsChecked) { menuItem.IsChecked = true; } } 

Basta creare un Template per MenuItem che conterrà un RadioButton con un GroupName impostato su un valore. Puoi anche cambiare il modello dei RadioButton in modo che assomigli al glifo di controllo predefinito di MenuItem (che può essere facilmente estratto con Expression Blend).

Questo è tutto!

Potresti fare qualcosa del genere:

                    

Ha alcuni effetti collaterali strani visivamente (vedrai quando lo usi), ma funziona comunque

Ecco un altro approccio che utilizza RoutedUICommands, una proprietà enum pubblica e DataTriggers. Questa è una soluzione piuttosto prolissa. Io sfortunatamente non vedo alcun modo di rendere Style.Triggers più piccolo, perché non so come dire che il valore di Binding è l’unica cosa diversa? (BTW, per MVVMers questo è un esempio terribile. Ho messo tutto nella class MainWindow solo per mantenere le cose semplici.)

MainWindow.xaml:

                                    

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 } } 

Qui è ancora un altro modo – non facile da qualsiasi tratto, ma è MVVM compatibile, collegabile e altamente testabile dell’unità. Se hai la libertà di aggiungere un convertitore al tuo progetto e non ti dispiace un po ‘di spazzatura sotto forma di un nuovo elenco di elementi ogni volta che si apre il menu di scelta rapida, questo funziona davvero bene. Risponde alla domanda originale su come fornire un insieme mutuamente esclusivo di elementi selezionati in un menu di scelta rapida.

Penso che se si desidera estrarre tutto questo in un controllo utente, è ansible trasformarlo in un componente della libreria riutilizzabile da riutilizzare nell’applicazione. I componenti utilizzati sono Type3.Xaml con una griglia semplice, un blocco di testo e il menu di scelta rapida. Fai clic con il pulsante destro del mouse su qualsiasi punto della griglia per visualizzare il menu.

Un convertitore di valori denominato AllValuesEqualToBooleanConverter viene utilizzato per confrontare il valore di ogni voce di menu con il valore corrente del gruppo e mostrare il segno di spunta accanto alla voce di menu attualmente selezionata.

Una semplice class che rappresenta le tue scelte di menu viene utilizzata per l’illustrazione. Il contenitore di esempio usa le proprietà Tuple with String e Integer che rendono abbastanza facile avere uno snippet di testo leggibile strettamente accoppiato con un valore di tipo machine-friendly. Puoi usare stringhe da solo o String e un Enum per tenere traccia del Valore per prendere decisioni su ciò che è corrente. Type3VM.cs è il ViewModel assegnato a DataContext per Type3.Xaml. Tuttavia, è ansible assegnare il contesto dei dati nel framework dell’applicazione esistente, utilizzare lo stesso meccanismo qui. Il framework dell’applicazione in uso si basa su INotifyPropertyChanged per comunicare i valori modificati a WPF e al relativo goo di binding. Se si hanno proprietà di dipendenza, potrebbe essere necessario modificare leggermente il codice.

Lo svantaggio di questa implementazione, a parte il convertitore e la sua lunghezza, è che viene creato un elenco dei rifiuti ogni volta che si apre il menu di scelta rapida. Per le applicazioni per utente singolo, probabilmente è ok ma dovresti esserne consapevole.

L’applicazione utilizza un’implementazione di RelayCommand che è prontamente disponibile dal sito Web di Haacked o da qualsiasi altra class di helper compatibile con ICommand disponibile in qualsiasi framework in uso.

public class Type3VM: INotifyPropertyChanged {private List menuData = new List (new [] {new MenuData (“Zero”, 0), nuovi MenuData (“One”, 1), nuovi MenuData (“Two”, 2), new MenuData ( “Tre”, 3),});

  public IEnumerable 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 { public MenuData(String DisplayValue, int value) : base(DisplayValue, value) { } } 

 public class AreAllValuesEqualConverter : 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 { }