讓 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
原創文章,轉載請註明: 轉載自 獨立觀察員