| File: System\Windows\Forms\Controls\Buttons\RadioButton.cs | Web Access |
| Project: src\winforms\src\System.Windows.Forms\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,{Assemblies.SystemDesign}")] [Designer($"System.Windows.Forms.Design.RadioButtonDesigner, {Assemblies.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; // UpdateOwnerDraw checks if OwnerDraw state changed and calls RecreateHandle if needed. UpdateOwnerDraw(); // If handle wasn't recreated (OwnerDraw state didn't change), refresh the appearance. 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); } } } private protected override bool OwnerDraw => // Order is key here - do NOT change! // We want NO owner draw ONLY when we're // * in Dark Mode // * when _then_ the Appearance is Button // * but then ONLY when we're rendering with FlatStyle.Standard // (because that would let us usually let us draw with the VisualStyleRenderers, // which cause HighDPI issues in Dark Mode). (!Application.IsDarkModeEnabled || Appearance != Appearance.Button || FlatStyle != FlatStyle.Standard) && base.OwnerDraw; /// <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 void ScaleConstants() { const int LogicalFlatSystemStylePaddingWidth = 24; const int LogicalFlatSystemStyleMinimumHeight = 13; _flatSystemStylePaddingWidth = LogicalToDeviceUnits(LogicalFlatSystemStylePaddingWidth); _flatSystemStyleMinimumHeight = LogicalToDeviceUnits(LogicalFlatSystemStyleMinimumHeight); } private protected override void InitializeControl() => ScaleConstants(); 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}"; }