Подтвердить что ты не робот

Взаимоисключающие элементы меню?

С учетом следующего кода:

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

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

Любые идеи?

4b9b3361

Ответ 1

Возможно, это не то, что вы ищете, но вы можете написать расширение для класса MenuItem, которое позволяет использовать что-то вроде свойства GroupName класса RadioButton. Я слегка изменил этот удобный пример для аналогичного расширения элементов управления ToggleButton и немного переработал его для вашей ситуации и придумал следующее:

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.

Ответ 2

Вы также можете использовать Поведение. Как этот:

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

Ответ 3

Добавление этого внизу, поскольку у меня пока нет репутации...

Насколько полезен ответ Патрика, он не гарантирует, что элементы не могут быть сняты. Чтобы сделать это, обработчик 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 not possible for the user to deselect an item
    {
        menuItem.IsChecked = true;
    }
}

Ответ 4

Так как нет однозначного ответа, я размещаю свое решение здесь:

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

Таким образом, VS выдаст ошибку, если новичок попытается скомпилировать XAML следующим образом:

//обратите внимание, что это неправильное использование!

<local:RadioMenuItem Header="111" GroupName="G1" IsCheckable="True"/>

//обратите внимание, что это неправильное использование!

Ответ 5

Да, это можно сделать легко, сделав каждый MenuItem RadioButton. Это можно сделать с помощью Редактирования шаблона MenuItem.

  • Щелкните правой кнопкой мыши элемент MenuItem в левой панели документа → EditTemplate > EditCopy. Это добавит код для редактирования в Window.Resources.

  • Теперь вам нужно сделать только два изменения, которые очень просты.

    Взаимно эксклюзивные элементы меню a. Добавьте RadioButton с некоторыми ресурсами, чтобы скрыть свою часть круга.

    б. Измените BorderThickness = 0 для части пограничного элемента MenuItem.

    Эти изменения показаны ниже в качестве комментариев, остальные сгенерированные стили должны использоваться как есть:

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

Ответ 6

В XAML нет встроенного способа сделать это, вам нужно будет свернуть собственное решение или получить существующее решение, если оно доступно.

Ответ 7

Я просто подумал, что брошу свое решение, потому что ни один из ответов не отвечал моим потребностям. Мое полное решение здесь...

WPF MenuItem как RadioButton

Однако основная идея - использовать ItemContainerStyle.

<MenuItem.ItemContainerStyle>
    <Style TargetType="MenuItem">
        <Setter Property="Icon" Value="{DynamicResource RadioButtonResource}"/>
        <EventSetter Event="Click" Handler="MenuItemWithRadioButtons_Click" />
    </Style>
</MenuItem.ItemContainerStyle>

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

Ответ 8

Я достиг этого, используя пару строк кода:

Сначала объявите переменную:

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


                }

Ответ 9

Я нахожу, что я получаю взаимоисключающие пункты меню при привязке MenuItem.IsChecked к переменной.

Но у него есть одна причуда: если вы нажмете на выбранный пункт меню, он станет недействительным, показывая обычный красный прямоугольник. Я решил это, добавив обработчик для MenuItem.Click, который предотвращает unselecting, просто установив IsChecked обратно в true.

Код... Я привязываюсь к типу enum, поэтому я использую конвертер enum, который возвращает true, если свойство bound равно параметру, указанному в параметре. Вот 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;
        }
    }

Ответ 10

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

Что это!

Ответ 11

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

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

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

Ответ 12

Здесь другой подход, который использует 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
  }
}

Ответ 13

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

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

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

Простой класс, который представляет ваш выбор в меню, используется для иллюстрации. Контейнер образца использует свойства Tuple со строками и целочисленными значениями, благодаря которым довольно легко получить тесно связанный текст, читаемый человеком, в сочетании с машинно-ориентированным значением. Вы можете использовать только строки или 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>
{ }

Ответ 14

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

Ответ 15

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

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

Ответ 16

Здесь простое решение на основе MVVM, которое использует простой IValueConverter и CommandParameter для каждого MenuItem.

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

Привязать к свойству int (MenuSelection) в DataContext (ViewModel).

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

Удачи!