让 WPF 的 RadioButton 支持再次点击取消选中的功能

让 WPF 的 RadioButton 支持再次点击取消选中的功能

让 WPF 的 RadioButton 支持再次点击取消选中的功能

独立观察员 2022 年 01 月 16 日

 

零、前言

众所周知,RadioButton 是一种单选框,一般是放置好几个在同一面板中以组成一组;使用时,初始时可能一个都没被选中,或者是设置了一个默认选中项;然后,用户可以在这一组单选框中切换选择其中一个,不能多选,也不能取消选中(也就是不能重新回到一个都没选的状态)。

最近公司软件中有个界面,UI 给出的样式就是单选框的形式,所以就使用了一组 RadioButton 来实现,初始是一个都没选,之后用户可以在其中选择一项。可是后来需求说选中的项再次点击需要取消选中,摔!这个功能 RadioButton 是办不到的,CheckBox 是可以的,不过如果换成 CheckBox,一方面样式要改,另一方面,只能选择一项这个需求也要写代码实现(CheckBox 好像可以设置为单选?算了,不要在意这些细节),所以还是找找方法,看能不能让 RadioButton 支持取消选中吧。

 

一、方法一:后台直接处理

网上找到的方法就是在后台新增一个 bool 变量,用来记录上次(或者说点击前)RadioButton 是选中还是未选中,然后在点击事件中进行判断处理:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图

 

来看看效果吧(动图):

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图1

 

上面的动图先演示了 RadioButton 默认是不支持取消选中的;然后演示了通过上面代码实现的支持取消选中的 RadioButton。

 

这样确实是可以的,但是只适用于只有单个 RadioButton 的情况,因为如果有好几个 RadioButton,那么就要为每个 RadioButton 新建一个布尔变量以及一个点击事件方法,最多是把事件方法整合一下,总之是很奇怪的。

 

当然,这个战略(引入一个布尔变量来记录上次的选择情况)是没问题,只不过战术(直接在后台处理)有点问题。那么我们使用这个战略的话,还能形成什么战术呢?大致可以想到两种方法,接下来容我一一道来。

 

二、方法二:提取为自定义控件(用户控件

我们新建一个名为 RadioButtonUncheck 的用户控件(UserControl),将继承关系改为 RadioButton,并把上一节所示的处理逻辑添加进去:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图2

 

前台直接改为实例化一个 RadioButton 即可:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图3

 

然后在界面上使用这个用户控件:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图4

 

看看效果(动图):

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图5

 

很明显,有一些 Bug,这是为什么呢?原因就是,我们新建的那个用来记录上次选中状态的变量,在用户选中其它项,同时 WPF 框架自动取消选中本项时,没有进行记录。

 

所以我们需要在 Checked 和 Unchecked 这两个事件中分别对 _lastChecked 进行相应的赋值:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图6

 

然后,由于触发了 Click 事件后(也有可能是 PreviewMouseDown 后 Click 前的某个事件,比如 PreviewMouseUp),WPF 框架(或者说是 RadioButton 内部)就会把 IsChecked 设为 true(这就是前面的代码中需要另外新建变量来判断的原因),所以需要换为 PreviewMouseDown 事件,并在处理完成后调用 “e.Handled = true;” 阻止事件继续传递:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图7

 

现在,当 RadioButtonUncheck 控件通过点击由未选切换为选中时,事件执行顺序为 PreviewMouseDown--Checked:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图8

或:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图9

 

而由选中切换为未选时,事件执行顺序为 PreviewMouseDown--Unchecked:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图10

 

而如果没有 “e.Handled = true;”,则由未选切换为选中时,事件执行顺序如下:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图11

或:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图12

 

由选中切换为未选时(切换失败),事件执行顺序如下:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图13

 

至此,用户控件法圆满完成任务(动图):

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图14

 

完整代码:

using System;
using System.Windows;
using System.Windows.Controls;

namespace WPFPractice.UserControls
{
    /// <summary>
    /// 支持点击取消选中的 RadioButton;
    /// </summary>
    public partial class RadioButtonUncheck : RadioButton
    {
        /// <summary>
        /// 上次的选中状态
        /// </summary>
        private bool _lastChecked;

        /// <summary>
        /// 内容字符串
        /// </summary>
        private string ContentStr => Content + "";

        public RadioButtonUncheck()
        {
            InitializeComponent();

            Click += RadioButtonUncheck_Click; ;
            PreviewMouseDown += RadioButtonUncheck_PreviewMouseDown; ;
            Checked += RadioButtonUncheck_Checked;
            Unchecked += RadioButtonUncheck_Unchecked;
        }

        /// <summary>
        /// 点击事件处理方法
        /// </summary>
        private void RadioButtonUncheck_Click(object sender, RoutedEventArgs e)
        {
            Console.WriteLine($"[{ContentStr}]触发 Click 事件");
            //SwitchStatus();
        }

        /// <summary>
        /// 鼠标按下事件处理方法
        /// </summary>
        private void RadioButtonUncheck_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            Console.WriteLine($"[{ContentStr}]触发 PreviewMouseDown 事件");
            SwitchStatus();
            e.Handled = true;
        }

        /// <summary>
        /// 切换状态
        /// </summary>
        private void SwitchStatus()
        {
            if (_lastChecked)
            {
                IsChecked = false;
                //_lastChecked = false;
            }
            else
            {
                IsChecked = true;
                //_lastChecked = true;
            }
        }

        /// <summary>
        /// 选中事件 处理方法
        /// </summary>
        private void RadioButtonUncheck_Checked(object sender, RoutedEventArgs e)
        {
            Console.WriteLine($"[{ContentStr}]触发 Checked 事件");
            _lastChecked = true;
        }

        /// <summary>
        /// 取消选中事件 处理方法
        /// </summary>
        private void RadioButtonUncheck_Unchecked(object sender, RoutedEventArgs e)
        {
            Console.WriteLine($"[{ContentStr}]触发 Unchecked 事件");
            _lastChecked = false;
        }
    }
}

 

三、方法三:附加行为

关于附加行为,是通过附加属性来实现的,可以参考我之前的翻译文章《【翻译】WPF 中附加行为的介绍 Introduction to Attached Behaviors in WPF》:

在一个元素上设置一个附加属性,那么你就可以从暴露这个附加属性的类中获得该元素的访问。一旦那个类有权限访问那个元素,它就能在其上挂钩事件,响应这些事件的触发,使该元素做出它本来不会做的事情。

 

下面直接进入正题,首先在一个新建类 RadioButtonAttached 中添加一个 bool 类型的附加属性 IsCanUncheck,当其被设置为 true 时,会给设置的元素附加 PreviewMouseDown、Checked、Unchecked 三个事件,和上一节一样:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图15

 

注意,附加属性还需要两个包装方法:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图16

 

由于附加属性的变动处理方法要求是静态方法:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图17

 

所以导致三个事件的处理方法也要是静态方法,不然就会报错:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图18

 

进而导致之前引入成员变量 _lastChecked 的方式行不通了:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图19

 

所以这个状态存储的地方需要另外寻找。对于这种情况,我经常使用的是元素的 Tag 属性,这次也是这样干的,也就是说使用单选框的 Tag 来存储上次的选中与否状态。

Checked 和 Unchecked 中还是换汤不换药:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图20

 

主要是 PreviewMouseDown 事件处理方法中,当第一次点击,Tag 中还没有存储时,bool 会转换失败,所以 Tag 中应该存储 true 供下次使用;而转换成功则将转换出的值(存在 lastChecked 变量中)取反存入 Tag 中供下次使用。(这样看来两种情况好像都可以直接使用 rb.Tag = !lastChecked; 哈哈,懒得改了)。之后就是依据 lastChecked 来决定(取反)IsChecked 的值:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图21

 

完整代码:

using System.Windows;
using System.Windows.Controls;

namespace WPFTemplateLib.Attached;

/// <summary>
/// RadioButton 附加属性类
/// </summary>
public class RadioButtonAttached : DependencyObject
{
    #region IsCanUncheck

    public static bool GetIsCanUncheck(FrameworkElement item)
    {
        return (bool)item.GetValue(IsCanUncheckProperty);
    }

    public static void SetIsCanUncheck(FrameworkElement item, bool value)
    {
        item.SetValue(IsCanUncheckProperty, value);
    }

    /// <summary>
    /// 是否能取消选中(启用此功能会占用 Tag 属性)
    /// </summary>
    public static readonly DependencyProperty IsCanUncheckProperty =
        DependencyProperty.RegisterAttached(
            "IsCanUncheck",
            typeof(bool),
            typeof(RadioButtonAttached),
            new UIPropertyMetadata(false, OnIsCanUncheckChanged));

    static void OnIsCanUncheckChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement item = depObj as FrameworkElement;

        if (item == null)
            return;

        switch (depObj)
        {
            case RadioButton radioButton:
            {
                if ((bool) e.NewValue)
                {
                    radioButton.PreviewMouseDown += RadioButton_PreviewMouseDown;
                    radioButton.Checked += RadioButton_Checked;
                    radioButton.Unchecked += RadioButton_Unchecked;
                }
                else
                {
                    radioButton.PreviewMouseDown -= RadioButton_PreviewMouseDown;
                    radioButton.Checked -= RadioButton_Checked;
                    radioButton.Unchecked -= RadioButton_Unchecked;
                }

                break;
            }
            default:
                break;
        }
    }

    private static void RadioButton_Unchecked(object sender, RoutedEventArgs e)
    {
        var rb = sender as RadioButton;
        if (rb == null)
        {
            return;
        }

        rb.Tag = false;
    }

    private static void RadioButton_Checked(object sender, RoutedEventArgs e)
    {
        var rb = sender as RadioButton;
        if (rb == null)
        {
            return;
        }
        
        rb.Tag = true;
    }

    private static void RadioButton_PreviewMouseDown(object sender, RoutedEventArgs e)
    {
        var rb = sender as RadioButton;
        if (rb == null)
        {
            return;
        }

        //使用 RadioButton 的 Tag 来存储上次选中的状态,之后可以从中获取来进行判断;
        bool parseSuccess = bool.TryParse(rb.Tag + "", out bool lastChecked);
        if (!parseSuccess)
        {
            //转换失败,说明是第一次点击,也就是本次本勾选了,所以应该把 true 存起来;
            rb.Tag = true;
        }
        else
        {
            rb.Tag = !lastChecked;
        }

        if (lastChecked)
        {
            rb.IsChecked = false;
            //lastChecked = false;
        }
        else
        {
            rb.IsChecked = true;
            //lastChecked = true;
        }

        e.Handled = true;
    }

    #endregion
}

 

使用时只需要在普通 RadioButton 元素上加上这个附加属性并将值置为 True 即可:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图22

 

效果和上一节的一样(实际上方法三是先写成的),就不再演示了,来个全家福吧:

让 WPF 的 RadioButton 支持再次点击取消选中的功能插图23

 

最后是源码地址:https://gitee.com/dlgcy/DLGCY_WPFPractice/tree/Blog20220116 

 

2022 年 2 月 26 日 更新

博客园评论区的 @dovese 修改了一个简化版,亲测可行,无需占用  Tag 属性,只针对取消选中操作进行拦截:

using System.Windows;
using System.Windows.Controls;

namespace WPFTemplateLib.Attached
{
    /// <summary>
    /// RadioButton 附加属性类
    /// </summary>
    public class RadioButtonAttached : DependencyObject
    {
        #region IsCanUncheck

        public static bool GetIsCanUncheck(FrameworkElement item)
        {
            return (bool)item.GetValue(IsCanUncheckProperty);
        }

        public static void SetIsCanUncheck(FrameworkElement item, bool value)
        {
            item.SetValue(IsCanUncheckProperty, value);
        }

        /// <summary>
        /// 是否能取消选中
        /// </summary>
        public static readonly DependencyProperty IsCanUncheckProperty =
            DependencyProperty.RegisterAttached(
                "IsCanUncheck",
                typeof(bool),
                typeof(RadioButtonAttached),
                new UIPropertyMetadata(false, OnIsCanUncheckChanged));

        static void OnIsCanUncheckChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
            FrameworkElement item = depObj as FrameworkElement;

            if (item == null)
                return;

            switch (depObj)
            {
                case RadioButton radioButton:
                {
                    if ((bool)e.NewValue)
                    {
                        radioButton.PreviewMouseDown += RadioButton_PreviewMouseDown;
                    }
                    else
                    {
                        radioButton.PreviewMouseDown -= RadioButton_PreviewMouseDown;
                    }
                    break;
                }
                default:
                    break;
            }
        }

        private static void RadioButton_PreviewMouseDown(object sender, RoutedEventArgs e)
        {
            var rb = sender as RadioButton;
            if (rb == null)
            {
                return;
            }

            if (rb.IsChecked == true)
            {
                rb.IsChecked = false;
                e.Handled = true;
            }
        }

        #endregion
    }
}
 
 

Leave a Reply