【问题标题】:Show ContextMenu on Left Click using only XAML仅使用 XAML 在左键单击时显示 ContextMenu
【发布时间】:2009-02-17 01:39:18
【问题描述】:

WPF ContextMenu 的默认行为是在用户右键单击时显示它。我希望 ContextMenu 在用户左键单击时显示。看起来这应该是ContextMenu 上的一个简单属性,但事实并非如此。

我对其进行了操纵,以便在代码隐藏中处理LeftMouseButtonDown 事件,然后显示上下文菜单。

我在我的项目中使用 MVVM,这意味着我将 DataTemplates 用于具有上下文菜单的项目。摆脱代码隐藏并找到一种使用 XAML 中的触发器或属性来显示上下文菜单的方法会更加优雅。

对这个问题有什么想法或解决方案吗?

【问题讨论】:

  • 不幸的是,我认为没有针对此问题的唯一 XAML 解决方案。上下文菜单行为被嵌入到 FrameworkElement 中。
  • 这与 Windows 中的标准背道而驰,您有充分的理由这样做吗?
  • 这很好,也许我应该使用 ContextMenu 以外的东西来完成这项工作。它基本上是一个下拉菜单,当您单击该项目时出现,不是按钮,而是一种按钮。 ContextMenu 似乎是一个显而易见的选择,但也许这是错误的。
  • 在此处查看我使用 Expression Blend 触发器的答案:stackoverflow.com/a/4917707/87912

标签: .net wpf xaml mvvm contextmenu


【解决方案1】:

我刚刚根据 HK1 的回答编写并测试了这个(您也可以阅读Attached Properties Overview) 中的附加属性:

public static class ContextMenuLeftClickBehavior
{
    public static bool GetIsLeftClickEnabled(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsLeftClickEnabledProperty);
    }

    public static void SetIsLeftClickEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsLeftClickEnabledProperty, value);
    }

    public static readonly DependencyProperty IsLeftClickEnabledProperty = DependencyProperty.RegisterAttached(
        "IsLeftClickEnabled", 
        typeof(bool), 
        typeof(ContextMenuLeftClickBehavior), 
        new UIPropertyMetadata(false, OnIsLeftClickEnabledChanged));

    private static void OnIsLeftClickEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        var uiElement = sender as UIElement;

        if(uiElement != null) 
        {
            bool IsEnabled = e.NewValue is bool && (bool) e.NewValue;

            if(IsEnabled)
            {
                if(uiElement is ButtonBase)
                    ((ButtonBase)uiElement).Click += OnMouseLeftButtonUp;
                else
                    uiElement.MouseLeftButtonUp += OnMouseLeftButtonUp;
            }
            else
            {
                if(uiElement is ButtonBase)
                    ((ButtonBase)uiElement).Click -= OnMouseLeftButtonUp;
                else
                    uiElement.MouseLeftButtonUp -= OnMouseLeftButtonUp;
            }
        }
    }

    private static void OnMouseLeftButtonUp(object sender, RoutedEventArgs e)
    {
        Debug.Print("OnMouseLeftButtonUp");
        var fe = sender as FrameworkElement;
        if(fe != null)
        {
            // if we use binding in our context menu, then it's DataContext won't be set when we show the menu on left click
            // (it seems setting DataContext for ContextMenu is hardcoded in WPF when user right clicks on a control, although I'm not sure)
            // so we have to set up ContextMenu.DataContext manually here
            if (fe.ContextMenu.DataContext == null)
            {
                fe.ContextMenu.SetBinding(FrameworkElement.DataContextProperty, new Binding { Source = fe.DataContext });
            }

            fe.ContextMenu.IsOpen = true;
        }
    }

}

...

<Button Content="Do All" local:ContextMenuLeftClickBehavior.IsLeftClickEnabled="True" >
    <Button.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Make everything awesome" />
            <MenuItem Header="Control the World" />
        </ContextMenu>
    </Button.ContextMenu>
</Button>

(注意 OnMouseLeftButtonUp() 方法中的注释)

【讨论】:

  • 优秀的解决方案,要解决您的 cmets 中引用的绑定问题,您可以设置放置目标:fe.ContextMenu.PlacementTarget = fe 然后 DataContext="{Binding Path=PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}"&gt; 然后您可以使用 ContextMenuService 属性,如 Placement 和 Horizo​​ntal/VerticalOffset 来定位它。
  • 我不想在已经显示上下文菜单的情况下单击按钮时再次打开它。所以在 IsOpen=true 下面我添加了这两行:fe.ContextMenu.Closed += ((s, a) =&gt; { fe.IsEnabled = true; }); fe.IsEnabled = false;
  • @user1552075 建议在xaml中添加ContextMenuService.IsEnabled="false",避免IsOpen=True时需要额外点击切换操作。
【解决方案2】:

我建议做的是创建一个带有附加 DependencyProperty 的新静态类。调用类 LeftClickContextMenu 和属性 Enabled(只是想法)。当您注册 DependencyProperty 时,添加一个 on changed 回调。然后在属性更改回调中,如果 Enabled 设置为 true,然后将处理程序添加到 LeftMouseButtonDown 事件并在那里执行您的操作。如果 Enabled 设置为 false,则删除处理程序。这允许您通过简单地在 xaml 中使用以下内容来将其设置为任何属性。

<Border namespace:LeftClickContextMenu.Enabled="True" />

这种技术称为附加行为,您可以在此代码项目文章中了解更多信息:http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx

【讨论】:

  • 我认为这个解决方案更简单:uxpassion.com/blog/old-blog/…
  • 这绝对是一种更简单的方法,如果您只需要它一个地方,我建议您将代码放在后面的代码中。但是,如果您在某些地方需要这种行为,我建议的附加行为方法会更好。
  • 哦,如果您想要一个更简单的按钮解决方案,您可以使用 ToggleButton 并将其 IsChecked 属性绑定到上下文菜单的 IsOpen 属性。
【解决方案3】:

虽然 Caleb 的回答是正确的,但它不包括工作代码。我使用 VB.NET 设置了一个示例(对不起),所以我在这里发布。

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:AttachedBehaviorTest.AttachedBehaviorTest"
    Title="MainWindow" Height="350" Width="525">
    <Grid>
        <StackPanel>
            <TextBlock local:ContextMenuLeftClickBehavior.IsLeftClickEnabled="True">Some Text Goes Here
                <TextBlock.ContextMenu>
                    <ContextMenu>
                        <MenuItem Header="Test1" />
                    </ContextMenu>
                </TextBlock.ContextMenu>            
            </TextBlock>

        </StackPanel>
    </Grid>
</Window>
Namespace AttachedBehaviorTest

    Public NotInheritable Class ContextMenuLeftClickBehavior

        Private Sub New()
        End Sub

        Public Shared Function GetIsLeftClickEnabled(obj As DependencyObject) As Boolean
            Return CBool(obj.GetValue(IsLeftClickEnabled))
        End Function

        Public Shared Sub SetIsLeftClickEnabled(obj As DependencyObject, value As Boolean)
            obj.SetValue(IsLeftClickEnabled, value)
        End Sub

        Public Shared ReadOnly IsLeftClickEnabled As DependencyProperty = _
            DependencyProperty.RegisterAttached("IsLeftClickEnabled", GetType(Boolean), GetType(ContextMenuLeftClickBehavior), New UIPropertyMetadata(False, AddressOf OnIsLeftClickEnabled))

        Private Shared Sub OnIsLeftClickEnabled(sender As Object, e As DependencyPropertyChangedEventArgs)
            Dim fe As FrameworkElement = TryCast(sender, FrameworkElement)
            If fe IsNot Nothing Then
                Dim IsEnabled As Boolean = CBool(e.NewValue)
                If IsEnabled = True Then
                    AddHandler fe.MouseLeftButtonUp, AddressOf OnMouseLeftButtonUp
                    Debug.Print("Added Handlers")
                Else
                    RemoveHandler fe.MouseLeftButtonUp, AddressOf OnMouseLeftButtonUp
                    Debug.Print("RemovedHandlers")
                End If 
            End If
        End Sub

        Private Shared Sub OnMouseLeftButtonUp(sender As Object, e As RoutedEventArgs)
            Debug.Print("OnMouseLeftButtonUp")
            Dim fe As FrameworkElement = TryCast(sender, FrameworkElement)
            If fe IsNot Nothing Then
                'Next Line is Needed if Context Menu items are Data Bound
                'fe.ContextMenu.DataContext = fe.DataContext
                fe.ContextMenu.IsOpen = True
            End If
        End Sub

    End Class

End Namespace

【讨论】:

    【解决方案4】:

    此答案与@nightcoder 的答案完全相同(感谢您的启发!)。它使用 Blend 风格的行为,与附加属性相比,这是一种更现代的方法。

    using System;
    using System.Windows;
    using System.Windows.Controls.Primitives;
    using System.Windows.Data;
    using System.Windows.Interactivity;
    
    /// <summary>
    /// Add this to any button menu allow a left click to open the context menu as well as the right.
    /// </summary>
    public class ContextMenuLeftClickBehavior : Behavior<ButtonBase>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
    
            this.AssociatedObject.Loaded += this.OnWindowLoaded;
            this.AssociatedObject.Unloaded += this.OnWindowUnloaded;
        }
    
        private void OnWindowLoaded(object sender, RoutedEventArgs e)
        {
            this.AssociatedObject.Click += OnMouseLeftButtonUp;
        }
    
        private void OnWindowUnloaded(object sender, RoutedEventArgs e)
        {
            this.AssociatedObject.Click -= OnMouseLeftButtonUp; // Cannot override OnDetached(), as this is not called on Dispose. Known issue in WPF.
        }
    
        private static void OnMouseLeftButtonUp(object sender, RoutedEventArgs e)
        {
            if (sender is ButtonBase fe && fe.ContextMenu != null)
            {
                if (fe.ContextMenu != null)
                {
                    // If we use binding in our context menu, then it's DataContext won't be set when we show the menu on left click. It
                    // seems setting DataContext for ContextMenu is hardcoded in WPF when user right clicks on a control? So we have to set
                    // up ContextMenu.DataContext manually here.
                    if (fe.ContextMenu?.DataContext == null)
                    {
                        fe.ContextMenu?.SetBinding(FrameworkElement.DataContextProperty, new Binding { Source = fe.DataContext });
                    }
    
                    fe.ContextMenu.IsOpen = true;
                }
            }
        }
    }
    

    然后将行为添加到按钮:

    <Button>
        <i:Interaction.Behaviors>
            <attachedProperties:ContextMenuLeftClickBehavior/>
        </i:Interaction.Behaviors>                                                
    <Button>
    

    Ellipse 或 Rectangle 等元素没有 OnClick 事件,这意味着对于任何交互的东西来说,没有什么能很好地发挥作用。因此,将所有内容包装在一个按钮中以获取 OnClick 事件。也可以通过将鼠标光标更改为鼠标悬停时的手来暗示该区域是可点击的。

    <Button.Style>
        <Style TargetType="{x:Type Button}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <Ellipse Fill="{TemplateBinding Background}" Width="16" Height="16"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <!--  Bind to custom color in ViewModel -->
                    <Setter Property="Background" Value="{Binding CustomBrush}"/>
                    <Setter Property="Cursor" Value="Hand"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Button.Style>
    

    【讨论】:

    • 这对我有用,但自发布此答案以来,有些事情发生了变化。 System.Windows.Interactivity 似乎不再存在,Behavior 现在是 NuGet 包Microsoft.Xaml.Behaviors.WPF 中Microsoft.Xaml.Behaviors 命名空间的一部分(还有其他框架的其他包)。另外值得注意的是attachedProperties命名空间是一个clr-namespace指向包含行为子类的命名空间。在我的例子中,我的 XAML 中已经有一个本地命名空间,所以它只是 &lt;local:ContextMenuLeftClickBehavior/&gt;。
    【解决方案5】:

    忘记“唯一的 xaml”的东西。当您将其包装到附加行为中时,可以很好地解决这个问题。

    这是一种在左键单击时显示上下文菜单的方法:

    在Border 元素上创建一个新的左键处理程序:

    <Border x:Name="Win"
            Width="40"
            Height="40"
            Background="Purple"
            MouseLeftButtonUp="UIElement_OnMouseLeftButtonUp">
    

    然后添加:

    private void UIElement_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        e.Handled = true;
    
        var mouseDownEvent =
            new MouseButtonEventArgs(Mouse.PrimaryDevice,
                Environment.TickCount,
                MouseButton.Right)
            {
                RoutedEvent = Mouse.MouseUpEvent,
                Source = Win,
            };
    
    
        InputManager.Current.ProcessInput(mouseDownEvent);
    }
    

    它的作用,它基本上将左键单击映射到右键单击。为了可重用性,您可以将其包装到附加的行为中。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-03-19
      • 2011-04-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-15
      相关资源
      最近更新 更多