让 WPF 的 RadioButton 支持再次点击取消选中的功能
零、前言
众所周知,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 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年2月26日 更新
评论区 @dovese 的简化版确实可以:
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 } }