前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >让 WPF 的 RadioButton 支持再次点击取消选中的功能

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

作者头像
独立观察员
发布2022-12-06 19:09:38
2.1K0
发布2022-12-06 19:09:38
举报
文章被收录于专栏:独立观察员博客

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

目录

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

零、前言

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

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

三、方法三:附加行为法

独立观察员 2022 年 01 月 16 日

零、前言

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

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

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

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

来看看效果吧(动图):

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

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

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

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

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

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

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

看看效果(动图):

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

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

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

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

或:

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

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

或:

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

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

完整代码:

代码语言:javascript
复制
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 三个事件,和上一节一样:

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

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

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

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

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

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

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

完整代码:

代码语言:javascript
复制
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 即可:

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-01-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 独立观察员博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 让 WPF 的 RadioButton 支持再次点击取消选中的功能
    • 零、前言
      • 一、方法一:后台直接处理
        • 二、方法二:提取为自定义控件(用户控件)
          • 三、方法三:附加行为法
          相关产品与服务
          对象存储
          对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档