让 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;”,则由未选切换为选中时,事件执行顺序如下:
或:
由选中切换为未选时(切换失败),事件执行顺序如下:
至此,用户控件法圆满完成任务(动图):
完整代码:
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 的值:
完整代码:
using System.Windows;
using System.Windows.Controls;namespace WPFTemplateLib.Attached;/// <summary>
/// RadioButton 附加属性类
/// </summary>
public class RadioButtonAttached : DependencyObject
{#region IsCanUncheckpublic 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
WPF
WPF DataGrid 如何将被选中行带到视野中
WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题
WPF DataGrid 通过自定义表头模拟首行固定
WPF ComboBox 使用 ResourceBinding 动态绑定资源键并支持语言切换
【翻译】WPF 中附加行为的介绍 Introduction to Attached Behaviors in WPF
WPF 使用 Expression Design 画图导出及使用 Path 画图
WPF MVVM 弹框之等待框
解决 WPF 绑定集合后数据变动界面却不更新的问题(使用 ObservableCollection)
WPF 消息框 TextBox 绑定新数据时让光标和滚动条跳到最下面
真・WPF 按钮拖动和调整大小
WPF MVVM 模式下的弹窗
WPF 让一组 Button 实现 RadioButton 的当前样式效果
WPF 原生绑定和命令功能使用指南
WPF 用户控件的自定义依赖属性在 MVVM 模式下的使用备忘
在WPF的MVVM模式中使用OCX组件