Взаимоисключающие проверяемые пункты меню? - PullRequest
43 голосов
/ 06 сентября 2010

Учитывая следующий код:

<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 автоматически не проверяются.

Я могу сделать это в коде, отслеживая события щелчка в меню, определяя, какой элемент был проверен, и снимая отметки с других элементов меню.Я думаю, что есть более простой способ.

Есть идеи?

Ответы [ 16 ]

0 голосов
/ 18 января 2019

Небольшое дополнение к ответу @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;
    }

Теперь отмеченный элемент остается отмеченным, когда пользователь щелкает его во второй раз.

0 голосов
/ 20 сентября 2018

Спустя несколько лет после того, как я вижу этот пост с ключевыми словами, которые я написал ... я думал, что было бы простое решение, в wpf ... Возможно, это я, но я думаю, что это немного особенный, иметь такой огромный арсенал для такая мелочь, как принятое решение. Я даже не говорю о решении с 6likes, я не понимал, где нажимать, чтобы иметь эту опцию.

Так что, может быть, это совсем не элегантно ... Но здесь простое решение. То, что он делает, это просто ... цикл для всех элементов, содержащихся в родительском элементе, чтобы поставить его в ложь. Большую часть времени люди отделяют эту часть от других частей, конечно, это правильно только в этом случае.

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, но кажется невероятным, чтобы сделать это, чтобы получить только один выбранный пункт меню.

0 голосов
/ 01 ноября 2013

Вот еще один способ - не простой, но он MVVM-совместимый, привязываемый и легко тестируемый. Если у вас есть свобода добавления Конвертера в ваш проект и вы не возражаете против небольшого мусора в виде нового списка элементов каждый раз, когда открывается контекстное меню, это работает очень хорошо. Он отвечает первоначальному вопросу о том, как предоставить взаимоисключающий набор проверенных элементов в контекстном меню.

Я думаю, что если вы хотите извлечь все это в пользовательский элемент управления, вы можете превратить его в повторно используемый библиотечный компонент для повторного использования в вашем приложении. Используемые компоненты: Type3.Xaml с простой сеткой, одним текстовым блоком и контекстным меню. Щелкните правой кнопкой мыши в любом месте сетки, чтобы отобразить меню.

Преобразователь значений с именем AllValuesEqualToBooleanConverter используется для сравнения значения каждого элемента меню с текущим значением группы и отображения галочки рядом с выбранным в данный момент элементом меню.

Простой класс, который представляет ваш выбор в меню, используется для иллюстрации. Контейнер с примерами использует свойства Tuple со свойствами String и Integer, благодаря которым довольно легко получить тесно связанный текст, читаемый человеком, в паре с машинно-ориентированным значением. Вы можете использовать только строки или String и Enum, чтобы отслеживать значение для принятия решения о том, что является текущим. Type3VM.cs - это ViewModel, который назначен DataContext для Type3.Xaml. Как бы вы ни пытались назначить свой контекст данных в существующей структуре приложения, используйте тот же механизм здесь. Используемая инфраструктура приложения полагается на INotifyPropertyChanged для передачи измененных значений в WPF и его привязку goo. Если у вас есть свойства зависимостей, вам может понадобиться немного изменить код.

Недостатком этой реализации, помимо преобразователя и его длины, является то, что список мусора создается каждый раз, когда открывается контекстное меню. Для однопользовательских приложений это, вероятно, нормально, но вы должны знать об этом.

Приложение использует реализацию RelayCommand, которая легко доступна с веб-сайта Haacked или любого другого совместимого с 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>
{ }
0 голосов
/ 13 июля 2013

Вот еще один подход, который использует RoutedUICommands, открытое свойство enum и DataTriggers. Это довольно многословное решение. К сожалению, я не вижу способа уменьшить Style.Triggers, потому что я не знаю, как просто сказать, что значение привязки - это единственное, что отличается? (Кстати, для 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
  }
}
0 голосов
/ 08 сентября 2010

Вы можете сделать что-то вроде этого:

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

У него есть какой-то странный побочный эффект визуально (вы увидите, когда его используете), но, тем не менее, он работает

0 голосов
/ 07 сентября 2010

Просто создайте шаблон для MenuItem, который будет содержать RadioButton с установленным значением GroupName.Вы также можете изменить шаблон для RadioButtons так, чтобы он выглядел как контрольный глиф по умолчанию для MenuItem (который можно легко извлечь с помощью Expression Blend).

Вот и все!

...