1. 程式人生 > 其它 >讓 WPF 的 RadioButton 支援再次點選取消選中的功能

讓 WPF 的 RadioButton 支援再次點選取消選中的功能

讓 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 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

原創文章,轉載請註明: 轉載自 獨立觀察員

本文連結地址: 讓 WPF 的 RadioButton 支援再次點選取消選中的功能 [http://dlgcy.com/wpf-radiobutton-support-click-again-to-unchecked/]