File: System\Windows\Forms\Controls\Buttons\RadioButton.cs
Web Access
Project: src\src\System.Windows.Forms\src\System.Windows.Forms.csproj (System.Windows.Forms)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms.ButtonInternal;
using System.Windows.Forms.Layout;
using Windows.Win32.System.Variant;
using Windows.Win32.UI.Accessibility;
 
namespace System.Windows.Forms;
 
/// <summary>
///  Encapsulates a standard Windows radio button (option button).
/// </summary>
[DefaultProperty(nameof(Checked))]
[DefaultEvent(nameof(CheckedChanged))]
[DefaultBindingProperty(nameof(Checked))]
[ToolboxItem($"System.Windows.Forms.Design.AutoSizeToolboxItem,{AssemblyRef.SystemDesign}")]
[Designer($"System.Windows.Forms.Design.RadioButtonDesigner, {AssemblyRef.SystemDesign}")]
[SRDescription(nameof(SR.DescriptionRadioButton))]
public partial class RadioButton : ButtonBase
{
    private static readonly object s_checkedChangedEvent = new();
    private static readonly object s_appearanceChangeEvent = new();
 
    private const ContentAlignment AnyRight = ContentAlignment.TopRight | ContentAlignment.MiddleRight | ContentAlignment.BottomRight;
 
    // Used to see if we need to iterate through the auto checked items and modify their tab stops.
    private bool _firstFocus = true;
    private bool _isChecked;
    private bool _autoCheck = true;
    private ContentAlignment _checkAlign = ContentAlignment.MiddleLeft;
    private Appearance _appearance = Appearance.Normal;
    private int _flatSystemStylePaddingWidth;
    private int _flatSystemStyleMinimumHeight;
 
    /// <summary>
    ///  Initializes a new instance of the <see cref="RadioButton"/> class.
    /// </summary>
    public RadioButton() : base()
    {
        ScaleConstants();
 
        // Radio buttons shouldn't respond to right clicks, so we need to do all our own click logic
        SetStyle(ControlStyles.StandardClick, false);
 
        TextAlign = ContentAlignment.MiddleLeft;
        TabStop = false;
        SetAutoSizeMode(AutoSizeMode.GrowAndShrink);
    }
 
    /// <summary>
    ///  Gets or sets a value indicating whether the <see cref="Checked"/> value and the appearance of
    ///  the control automatically change when the control is clicked.
    /// </summary>
    [DefaultValue(true)]
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.RadioButtonAutoCheckDescr))]
    public bool AutoCheck
    {
        get => _autoCheck;
        set
        {
            if (_autoCheck != value)
            {
                _autoCheck = value;
                PerformAutoUpdates(false);
            }
        }
    }
 
    /// <summary>
    ///  Gets or sets the appearance of the radio button control is drawn.
    /// </summary>
    [DefaultValue(Appearance.Normal)]
    [SRCategory(nameof(SR.CatAppearance))]
    [Localizable(true)]
    [SRDescription(nameof(SR.RadioButtonAppearanceDescr))]
    public Appearance Appearance
    {
        get => _appearance;
        set
        {
            if (_appearance != value)
            {
                SourceGenerated.EnumValidator.Validate(value);
 
                using (LayoutTransaction.CreateTransactionIf(AutoSize, ParentInternal, this, PropertyNames.Appearance))
                {
                    _appearance = value;
                    if (OwnerDraw)
                    {
                        Refresh();
                    }
                    else
                    {
                        UpdateStyles();
                    }
 
                    OnAppearanceChanged(EventArgs.Empty);
                }
            }
        }
    }
 
    [SRCategory(nameof(SR.CatPropertyChanged))]
    [SRDescription(nameof(SR.RadioButtonOnAppearanceChangedDescr))]
    public event EventHandler? AppearanceChanged
    {
        add => Events.AddHandler(s_appearanceChangeEvent, value);
        remove => Events.RemoveHandler(s_appearanceChangeEvent, value);
    }
 
    /// <summary>
    ///  Gets or sets the location of the check box portion of the radio button control.
    /// </summary>
    [Localizable(true)]
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue(ContentAlignment.MiddleLeft)]
    [SRDescription(nameof(SR.RadioButtonCheckAlignDescr))]
    public ContentAlignment CheckAlign
    {
        get => _checkAlign;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);
 
            _checkAlign = value;
            if (OwnerDraw)
            {
                Invalidate();
            }
            else
            {
                UpdateStyles();
            }
        }
    }
 
    /// <summary>
    ///  Gets or sets a value indicating whether the control is checked or not.
    /// </summary>
    [Bindable(true),
    SettingsBindable(true)]
    [DefaultValue(false)]
    [SRCategory(nameof(SR.CatAppearance))]
    [SRDescription(nameof(SR.RadioButtonCheckedDescr))]
    public bool Checked
    {
        get => _isChecked;
        set
        {
            if (_isChecked != value)
            {
                _isChecked = value;
 
                if (IsHandleCreated)
                {
                    PInvokeCore.SendMessage(this, PInvoke.BM_SETCHECK, (WPARAM)(BOOL)value);
                }
 
                Invalidate();
                Update();
                PerformAutoUpdates(tabbedInto: false);
                OnCheckedChanged(EventArgs.Empty);
            }
        }
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event EventHandler? DoubleClick
    {
        add => base.DoubleClick += value;
        remove => base.DoubleClick -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event MouseEventHandler? MouseDoubleClick
    {
        add => base.MouseDoubleClick += value;
        remove => base.MouseDoubleClick -= value;
    }
 
    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.ClassName = PInvoke.WC_BUTTON;
            if (OwnerDraw)
            {
                cp.Style |= PInvoke.BS_OWNERDRAW;
            }
            else
            {
                cp.Style |= PInvoke.BS_RADIOBUTTON;
                if (Appearance == Appearance.Button)
                {
                    cp.Style |= PInvoke.BS_PUSHLIKE;
                }
 
                // Determine the alignment of the radio button
                ContentAlignment align = RtlTranslateContent(CheckAlign);
                if ((align & AnyRight) != 0)
                {
                    cp.Style |= PInvoke.BS_RIGHTBUTTON;
                }
            }
 
            return cp;
        }
    }
 
    protected override Size DefaultSize => new(104, 24);
 
    /// <summary>
    ///  When overridden in a derived class, handles rescaling of any magic numbers used in control painting.
    ///  For RadioButton controls, scale the width of the system-style padding and height of the radio button image.
    ///  Must call the base class method to get the current DPI values. This method is invoked only when
    ///  Application opts-in into the Per-monitor V2 support, targets .NETFX 4.7 and has
    ///  EnableDpiChangedMessageHandling and EnableDpiChangedHighDpiImprovements config switches turned on.
    /// </summary>
    /// <param name="deviceDpiOld">Old DPI value</param>
    /// <param name="deviceDpiNew">New DPI value</param>
    protected override void RescaleConstantsForDpi(int deviceDpiOld, int deviceDpiNew)
    {
        base.RescaleConstantsForDpi(deviceDpiOld, deviceDpiNew);
        ScaleConstants();
    }
 
    private protected override void InitializeConstantsForInitialDpi(int initialDpi)
        => ScaleConstants();
 
    private void ScaleConstants()
    {
        const int LogicalFlatSystemStylePaddingWidth = 24;
        const int LogicalFlatSystemStyleMinimumHeight = 13;
        _flatSystemStylePaddingWidth = LogicalToDeviceUnits(LogicalFlatSystemStylePaddingWidth);
        _flatSystemStyleMinimumHeight = LogicalToDeviceUnits(LogicalFlatSystemStyleMinimumHeight);
    }
 
    internal override Size GetPreferredSizeCore(Size proposedConstraints)
    {
        if (FlatStyle != FlatStyle.System)
        {
            return base.GetPreferredSizeCore(proposedConstraints);
        }
 
        Size textSize = TextRenderer.MeasureText(Text, Font);
        Size size = SizeFromClientSize(textSize);
        size.Width += _flatSystemStylePaddingWidth;
 
        // Ensure minimum height to avoid truncation of RadioButton circle or text.
        size.Height = ScaleHelper.IsScalingRequirementMet
            ? Math.Max(size.Height + 5, _flatSystemStyleMinimumHeight)
            : size.Height + 5;
 
        return size;
    }
 
    internal override Rectangle OverChangeRectangle
    {
        get
        {
            if (Appearance == Appearance.Button)
            {
                return base.OverChangeRectangle;
            }
            else if (FlatStyle == FlatStyle.Standard)
            {
                // Return an out of bounds rectangle to avoid invalidation.
                return new Rectangle(-1, -1, 1, 1);
            }
            else
            {
                return Adapter.CommonLayout().Layout().CheckBounds;
            }
        }
    }
 
    internal override Rectangle DownChangeRectangle =>
        Appearance == Appearance.Button || FlatStyle == FlatStyle.System
                ? base.DownChangeRectangle
                : Adapter.CommonLayout().Layout().CheckBounds;
 
    internal override bool SupportsUiaProviders => true;
 
    [DefaultValue(false)]
    public new bool TabStop
    {
        get => base.TabStop;
        set => base.TabStop = value;
    }
 
    /// <summary>
    ///  Gets or sets the value indicating whether the user can give the focus to this
    ///  control using the TAB key.
    /// </summary>
    [Localizable(true)]
    [DefaultValue(ContentAlignment.MiddleLeft)]
    public override ContentAlignment TextAlign
    {
        get => base.TextAlign;
        set => base.TextAlign = value;
    }
 
    /// <summary>
    ///  Occurs when the
    ///  value of the <see cref="Checked"/>
    ///  property changes.
    /// </summary>
    [SRDescription(nameof(SR.RadioButtonOnCheckedChangedDescr))]
    public event EventHandler? CheckedChanged
    {
        add => Events.AddHandler(s_checkedChangedEvent, value);
        remove => Events.RemoveHandler(s_checkedChangedEvent, value);
    }
 
    protected override AccessibleObject CreateAccessibilityInstance() => new RadioButtonAccessibleObject(this);
 
    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
 
        if (IsHandleCreated)
        {
            PInvokeCore.SendMessage(this, PInvoke.BM_SETCHECK, (WPARAM)(BOOL)_isChecked);
        }
    }
 
    /// <summary>
    ///  Raises the <see cref="CheckBox.CheckedChanged"/> event.
    /// </summary>
    protected virtual void OnCheckedChanged(EventArgs e)
    {
        // MSAA events:
        AccessibilityNotifyClients(AccessibleEvents.StateChange, -1);
        AccessibilityNotifyClients(AccessibleEvents.NameChange, -1);
 
        // UIA events:
        if (IsAccessibilityObjectCreated)
        {
            using var nameVariant = (VARIANT)Name;
            AccessibilityObject.RaiseAutomationPropertyChangedEvent(UIA_PROPERTY_ID.UIA_NamePropertyId, nameVariant, nameVariant);
            AccessibilityObject.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationPropertyChangedEventId);
        }
 
        ((EventHandler?)Events[s_checkedChangedEvent])?.Invoke(this, e);
    }
 
    protected override void OnClick(EventArgs e)
    {
        if (_autoCheck)
        {
            Checked = true;
        }
 
        base.OnClick(e);
    }
 
    protected internal override void OnEnter(EventArgs e)
    {
        // Just like the Win32 RadioButton, fire a click if the user arrows onto the control.
        if (MouseButtons == MouseButtons.None)
        {
            if (PInvoke.GetKeyState((int)Keys.Tab) >= 0)
            {
                // Entered the radio button by using arrow keys.
                // Paint in raised state.
                ResetFlagsandPaint();
                if (!ValidationCancelled)
                {
                    OnClick(e);
                }
            }
            else
            {
                // Entered the radio button by pressing Tab
                PerformAutoUpdates(true);
 
                // Reset the TabStop so we can come back later. PerformAutoUpdates will set TabStop to false.
                TabStop = true;
            }
        }
 
        base.OnEnter(e);
    }
 
    private void PerformAutoUpdates(bool tabbedInto)
    {
        if (!_autoCheck)
        {
            return;
        }
 
        if (_firstFocus)
        {
            WipeTabStops(tabbedInto);
        }
 
        TabStop = _isChecked;
        if (!_isChecked || ParentInternal is not { } parent)
        {
            return;
        }
 
        ControlCollection children = parent.Controls;
        for (int i = 0; i < children.Count; i++)
        {
            if (children[i] is RadioButton radioButton
                && radioButton != this
                && radioButton.AutoCheck
                && radioButton.Checked)
            {
                radioButton.Checked = false;
            }
        }
    }
 
    /// <summary>
    ///  Removes tab stops from all radio buttons, other than the one that currently has the focus.
    /// </summary>
    private void WipeTabStops(bool tabbedInto)
    {
        if (ParentInternal is not { } parent)
        {
            return;
        }
 
        ControlCollection children = parent.Controls;
        for (int i = 0; i < children.Count; i++)
        {
            if (children[i] is RadioButton button)
            {
                if (!tabbedInto)
                {
                    button._firstFocus = false;
                }
 
                if (button._autoCheck)
                {
                    button.TabStop = false;
                }
            }
        }
    }
 
    internal override ButtonBaseAdapter CreateFlatAdapter() => new RadioButtonFlatAdapter(this);
 
    internal override ButtonBaseAdapter CreatePopupAdapter() => new RadioButtonPopupAdapter(this);
 
    internal override ButtonBaseAdapter CreateStandardAdapter() => new RadioButtonStandardAdapter(this);
 
    private void OnAppearanceChanged(EventArgs e)
    {
        if (Events[s_appearanceChangeEvent] is EventHandler eh)
        {
            eh(this, e);
        }
    }
 
    protected override void OnMouseUp(MouseEventArgs mevent)
    {
        if (mevent.Button == MouseButtons.Left
            && GetStyle(ControlStyles.UserPaint)
            && MouseIsDown
            && PInvoke.WindowFromPoint(PointToScreen(mevent.Location)) == HWND)
        {
            // Paint in raised state.
            ResetFlagsandPaint();
            if (!ValidationCancelled)
            {
                OnClick(mevent);
                OnMouseClick(mevent);
            }
        }
 
        base.OnMouseUp(mevent);
    }
 
    /// <summary>
    ///  Generates a <see cref="Control.Click"/> event for the button, simulating a click by a user.
    /// </summary>
    public void PerformClick()
    {
        if (CanSelect)
        {
            // Paint in raised state.
            ResetFlagsandPaint();
            if (!ValidationCancelled)
            {
                OnClick(EventArgs.Empty);
            }
        }
    }
 
    protected internal override bool ProcessMnemonic(char charCode)
    {
        if (UseMnemonic && IsMnemonic(charCode, Text) && CanSelect)
        {
            if (!Focused)
            {
                // This will cause an OnEnter event, which in turn will fire the click event.
                Focus();
            }
            else
            {
                // Generate a click if already focused.
                PerformClick();
            }
 
            return true;
        }
 
        return false;
    }
 
    public override string ToString() => $"{base.ToString()}, Checked: {Checked}";
}