You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WPF自定义MenuButton控件首次点击菜单项无响应问题求助

WPF自定义MenuButton控件首次点击菜单项无响应问题排查

自定义的WPF MenuButton控件外观如下:
MenuButton外观

控件功能说明:点击按钮显示菜单项,失去焦点时隐藏,菜单项点击逻辑正常,但首次点击菜单项无响应,第二次及以后点击才生效。以下是控件的原代码、问题分析和修复方案:

原代码

C#代码

public partial class MenuButton : Button, INotifyPropertyChanged
{
    private readonly ContextMenu Menu;
    private SolidColorBrush arrowColor = Brushes.Transparent;

    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register(nameof(Text), typeof(string), typeof(MenuButton), new(string.Empty, new(OnSomeTextPropertyChanged)));

    public static readonly DependencyProperty MenuPlacementProperty =
        DependencyProperty.Register(nameof(MenuPlacement), typeof(PlacementMode), typeof(MenuButton), new(PlacementMode.Top, new(OnSomeMenuPlacementPropertyChanged)));

    public static readonly DependencyProperty MenuItemsProperty =
        DependencyProperty.Register(nameof(MenuItems), typeof(ItemCollection), typeof(MenuButton), new(default(ItemCollection), new(OnSomeMenuItemsPropertyChanged)));

    public SolidColorBrush ArrowColor
    {
        get => arrowColor;
        private set
        {
            arrowColor = value;
            OnPropertyChanged(nameof(arrowColor));
        }
    }

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetCurrentValue(TextProperty, value);
    }

    public PlacementMode MenuPlacement
    {
        get => (PlacementMode)GetValue(MenuPlacementProperty);
        set => SetCurrentValue(MenuPlacementProperty, value);
    }

    public ItemCollection MenuItems
    {
        get => (ItemCollection)GetValue(MenuItemsProperty);
        set => SetCurrentValue(MenuItemsProperty, value);
    }

    public MenuButton()
    {
        Menu = new()
        {
            StaysOpen = true,
            HasDropShadow = false,
            PlacementTarget = this,
            Placement = MenuPlacement,
            ItemsSource = MenuItems,
        };

        SetCurrentValue(MenuItemsProperty, new DataGrid().Items);

        InitializeComponent();
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string prop = "")
    {
        PropertyChanged?.Invoke(this, new(prop));
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        base.OnMouseLeftButtonDown(e);

        Menu.Width = ActualWidth;
        Menu.IsOpen = true;
    }

    private void MenuButtonControl_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (sender is MenuButton button && bool.TryParse(e.NewValue?.ToString(), out bool enabled))
        {
            button.ArrowColor = enabled ? Brushes.Black : Brushes.DimGray;
        }
    }

    private static void OnSomeTextPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        if (target is MenuButton menuButton)
        {
            menuButton.ButtonText.Content = e.NewValue.ToString();
        }
    }

    private static void OnSomeMenuPlacementPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        if (target is MenuButton menuButton && menuButton.Menu is ContextMenu menu && Enum.TryParse(e.NewValue?.ToString(), true, out PlacementMode placementMode))
        {
            menu.Placement = placementMode;
        }
    }

    private static void OnSomeMenuItemsPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        if (target is MenuButton menuButton && menuButton.Menu is ContextMenu menu && e.NewValue is ItemCollection itemCollection)
        {
            menu.ItemsSource = itemCollection;
            menu.IsOpen = true;
            menu.IsOpen = false;
        }
    }
}

XAML代码

<Button x:Name="MenuButtonControl"
        x:Class="Controls.UserControls.MenuButton"
        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" 
        mc:Ignorable="d"
        d:DesignHeight="20"
        d:DesignWidth="100"
        HorizontalContentAlignment="Stretch"
        VerticalContentAlignment="Stretch"
        IsEnabledChanged="MenuButtonControl_IsEnabledChanged">
    <Button.Content>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"></RowDefinition>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="Auto"></ColumnDefinition>
            </Grid.ColumnDefinitions>

            <Label x:Name="ButtonText" Grid.Row="0" Grid.Column="0" Margin="0" Padding="0" Content="{Binding Text, ElementName=MenuButtonControl}" HorizontalAlignment="Center" VerticalAlignment="Stretch"></Label>
            <Path Grid.Row="0" Grid.Column="1" Margin="5,0" Fill="{Binding ArrowColor, ElementName=MenuButtonControl}" Data="M 0 0 L 4 5 L 8 0 Z" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Grid>
    </Button.Content>
</Button>

问题分析

  1. ContextMenu状态异常:OnSomeMenuItemsPropertyChanged中强制切换IsOpen状态(先设为true再设为false),干扰了ContextMenu内部的事件处理逻辑,导致首次点击菜单项时事件无法正常触发。
  2. MenuItems初始化错误:用new DataGrid().Items初始化菜单数据源,DataGrid的Items集合是控件内部绑定集合,不适合作为ContextMenu的数据源,还会触发上述错误的状态切换。
  3. PropertyChanged通知错误:ArrowColor属性的通知参数用了字段名arrowColor而非属性名ArrowColor,导致箭头颜色绑定无法更新。
  4. 焦点冲突:OnMouseLeftButtonDown中调用base方法触发Button的默认点击状态,与ContextMenu的焦点获取逻辑冲突,影响菜单项点击响应。

修复方案

修改后的C#代码

public partial class MenuButton : Button, INotifyPropertyChanged
{
    private readonly ContextMenu Menu;
    private SolidColorBrush arrowColor = Brushes.Transparent;

    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register(nameof(Text), typeof(string), typeof(MenuButton), new(string.Empty, new(OnSomeTextPropertyChanged)));

    public static readonly DependencyProperty MenuPlacementProperty =
        DependencyProperty.Register(nameof(MenuPlacement), typeof(PlacementMode), typeof(MenuButton), new(PlacementMode.Top, new(OnSomeMenuPlacementPropertyChanged)));

    // 改用ObservableCollection作为绑定源,更适合动态数据场景
    public static readonly DependencyProperty MenuItemsProperty =
        DependencyProperty.Register(nameof(MenuItems), typeof(ObservableCollection<object>), typeof(MenuButton), new(new ObservableCollection<object>(), new(OnSomeMenuItemsPropertyChanged)));

    public SolidColorBrush ArrowColor
    {
        get => arrowColor;
        private set
        {
            arrowColor = value;
            // 修复PropertyChanged通知的属性名
            OnPropertyChanged(nameof(ArrowColor));
        }
    }

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetCurrentValue(TextProperty, value);
    }

    public PlacementMode MenuPlacement
    {
        get => (PlacementMode)GetValue(MenuPlacementProperty);
        set => SetCurrentValue(MenuPlacementProperty, value);
    }

    public ObservableCollection<object> MenuItems
    {
        get => (ObservableCollection<object>)GetValue(MenuItemsProperty);
        set => SetCurrentValue(MenuItemsProperty, value);
    }

    public MenuButton()
    {
        Menu = new()
        {
            // 恢复默认关闭行为,点击菜单项或外部区域自动关闭
            StaysOpen = false,
            HasDropShadow = false,
            PlacementTarget = this,
            Placement = MenuPlacement,
            ItemsSource = MenuItems,
        };

        // 初始化空的ObservableCollection,替换原错误的DataGrid.Items
        SetCurrentValue(MenuItemsProperty, new ObservableCollection<object>());

        InitializeComponent();
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string prop = "")
    {
        PropertyChanged?.Invoke(this, new(prop));
    }

    protected override void OnClick()
    {
        // 改用OnClick事件打开菜单,避免MouseLeftButtonDown的焦点冲突
        Menu.Width = ActualWidth;
        Menu.IsOpen = true;
    }

    private void MenuButtonControl_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (sender is MenuButton button)
        {
            button.ArrowColor = (bool)e.NewValue ? Brushes.Black : Brushes.DimGray;
        }
    }

    private static void OnSomeTextPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        if (target is MenuButton menuButton)
        {
            menuButton.ButtonText.Content = e.NewValue.ToString();
        }
    }

    private static void OnSomeMenuPlacementPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        if (target is MenuButton menuButton && menuButton.Menu is ContextMenu menu && e.NewValue is PlacementMode placementMode)
        {
            menu.Placement = placementMode;
        }
    }

    private static void OnSomeMenuItemsPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        if (target is MenuButton menuButton && menuButton.Menu is ContextMenu menu && e.NewValue is ObservableCollection<object> itemCollection)
        {
            menu.ItemsSource = itemCollection;
            // 移除错误的IsOpen开关逻辑
        }
    }
}

关键修改点

  • 将MenuItemsProperty类型改为ObservableCollection<object>,更适合作为数据绑定源。
  • 移除OnSomeMenuItemsPropertyChanged中错误的IsOpen状态切换代码。
  • 修复ArrowColor的PropertyChanged通知参数,使用正确的属性名。
  • 将菜单打开逻辑移至OnClick方法,避免MouseLeftButtonDown的焦点冲突。
  • 设置Menu.StaysOpen = false,恢复ContextMenu默认关闭行为。
  • 修正MenuItems初始化方式,使用空的ObservableCollection<object>。

内容的提问来源于stack exchange,提问作者Vlad i Slav

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 03:15:57