// 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>
/// Represents a Windows check box.
/// </summary>
[DefaultProperty(nameof(Checked))]
[DefaultEvent(nameof(CheckedChanged))]
[DefaultBindingProperty(nameof(CheckState))]
[ToolboxItem($"System.Windows.Forms.Design.AutoSizeToolboxItem,{Assemblies.SystemDesign}")]
[SRDescription(nameof(SR.DescriptionCheckBox))]
public partial class CheckBox : ButtonBase
{
private static readonly object s_checkedChangedEvent = new();
private static readonly object s_checkStateChangedEvent = new();
private static readonly object s_appearanceChangedEvent = new();
private const ContentAlignment AnyRight = ContentAlignment.TopRight | ContentAlignment.MiddleRight | ContentAlignment.BottomRight;
private ContentAlignment _checkAlign = ContentAlignment.MiddleLeft;
private CheckState _checkState;
private Appearance _appearance;
private bool _threeState;
private Rendering.CheckBox.AnimatedCheckGlyphRenderer? _checkGlyphRenderer;
private Rendering.CheckBox.AnimatedToggleSwitchRenderer? _toggleSwitchRenderer;
private int _flatSystemStylePaddingWidth;
private int _flatSystemStyleMinimumHeight;
// A flag indicating if UIA StateChanged event needs to be triggered,
// to avoid double-triggering when Checked value changes.
private bool _notifyAccessibilityStateChangedNeeded;
/// <summary>
/// Initializes a new instance of the <see cref="CheckBox"/> class.
/// </summary>
public CheckBox() : base()
{
// Checkboxes shouldn't respond to right clicks, so we need to do all our own click logic
SetStyle(ControlStyles.StandardClick | ControlStyles.StandardDoubleClick, false);
SetAutoSizeMode(AutoSizeMode.GrowAndShrink);
AutoCheck = true;
TextAlign = ContentAlignment.MiddleLeft;
}
private protected override void InitializeControl() => ScaleConstants();
private bool AccObjDoDefaultAction { get; set; }
public partial Appearance Appearance
{
get => _appearance;
set
{
SourceGenerated.EnumValidator.Validate(value);
if (_appearance == value)
{
return;
}
using (LayoutTransaction.CreateTransactionIf(AutoSize, ParentInternal, this, PropertyNames.Appearance))
{
_appearance = value;
ResetAdapter();
// UpdateOwnerDraw synchronizes control styles with the OwnerDraw state and recreates
// the handle if they differ. Since we hijack FlatStyle.Standard for DarkMode, the transition
// between Normal and Button appearance is critical for updating the OwnerDraw flag.
UpdateOwnerDraw();
UpdateToggleSwitchStyles();
// If handle wasn't recreated (OwnerDraw state didn't change), refresh the appearance.
if (OwnerDraw)
{
Refresh();
}
else
{
UpdateStyles();
}
OnAppearanceChanged(EventArgs.Empty);
}
}
}
private protected override bool OwnerDraw =>
// Appearance.ToggleSwitch intentionally wins over FlatStyle.System: a toggle has no
// native BS_GROUPBOX rendering and must remain owner-drawn so the requested appearance
// is never ignored and mouse-state geometry is always available.
IsToggleSwitchAppearance
||
// 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);
/// <summary>
/// Gets a value indicating whether the check box should render as the modern, animated toggle switch.
/// </summary>
private bool IsToggleSwitchAppearance
{
get
{
return Appearance == Appearance.ToggleSwitch
&& EffectiveVisualStylesMode >= VisualStylesMode.Net11
&& !ThreeState;
}
}
private Rendering.CheckBox.AnimatedToggleSwitchRenderer ToggleSwitchRenderer =>
_toggleSwitchRenderer ??= new(this, Rendering.CheckBox.ModernCheckBoxStyle.Rounded);
internal Rendering.CheckBox.AnimatedCheckGlyphRenderer CheckGlyphRenderer
=> _checkGlyphRenderer ??= new(this);
private void UpdateToggleSwitchStyles()
{
if (IsToggleSwitchAppearance)
{
// Owner-paint with WinForms double buffering for a flicker-free, fluent animation.
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
_toggleSwitchRenderer?.SynchronizeState();
}
else
{
_toggleSwitchRenderer?.StopAnimation();
}
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.CheckBoxOnAppearanceChangedDescr))]
public event EventHandler? AppearanceChanged
{
add => Events.AddHandler(s_appearanceChangedEvent, value);
remove => Events.RemoveHandler(s_appearanceChangedEvent, value);
}
/// <summary>
/// Gets or sets a value indicating whether the <see cref="Checked"/> or <see cref="CheckState"/>
/// value and the check box's appearance are automatically changed when it is clicked.
/// </summary>
[DefaultValue(true)]
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.CheckBoxAutoCheckDescr))]
public bool AutoCheck { get; set; }
/// <summary>
/// Gets or sets the horizontal and vertical alignment of a check box on a check box control.
/// </summary>
/// <remarks>
/// <para>
/// When <see cref="Appearance"/> is <see cref="Appearance.ToggleSwitch"/> and the control renders as a toggle
/// switch in an effective .NET 11-or-later mode,
/// <see cref="ContentAlignment.TopLeft"/>, <see cref="ContentAlignment.MiddleLeft"/>,
/// <see cref="ContentAlignment.BottomLeft"/>, <see cref="ContentAlignment.TopCenter"/>,
/// <see cref="ContentAlignment.MiddleCenter"/>, and <see cref="ContentAlignment.BottomCenter"/> are treated
/// like left-aligned values, and
/// <see cref="ContentAlignment.TopRight"/>, <see cref="ContentAlignment.MiddleRight"/>, and
/// <see cref="ContentAlignment.BottomRight"/> are treated like right-aligned values.
/// </para>
/// </remarks>
[Bindable(true)]
[Localizable(true)]
[SRCategory(nameof(SR.CatAppearance))]
[DefaultValue(ContentAlignment.MiddleLeft)]
[SRDescription(nameof(SR.CheckBoxCheckAlignDescr))]
public ContentAlignment CheckAlign
{
get => _checkAlign;
set
{
SourceGenerated.EnumValidator.Validate(value);
if (_checkAlign == value)
{
return;
}
_checkAlign = value;
LayoutTransaction.DoLayoutIf(AutoSize, ParentInternal, this, PropertyNames.CheckAlign);
if (OwnerDraw)
{
Invalidate();
}
else
{
UpdateStyles();
}
}
}
/// <summary>
/// Gets or sets a value indicating whether the check box is checked.
/// </summary>
[Bindable(true),
SettingsBindable(true)]
[DefaultValue(false)]
[SRCategory(nameof(SR.CatAppearance))]
[RefreshProperties(RefreshProperties.All)]
[SRDescription(nameof(SR.CheckBoxCheckedDescr))]
public bool Checked
{
get => _checkState != CheckState.Unchecked;
set
{
if (value != Checked)
{
CheckState = value ? CheckState.Checked : CheckState.Unchecked;
}
}
}
/// <summary>
/// Gets or sets a value indicating whether the check box is checked.
/// </summary>
[Bindable(true)]
[SRCategory(nameof(SR.CatAppearance))]
[DefaultValue(CheckState.Unchecked)]
[RefreshProperties(RefreshProperties.All)]
[SRDescription(nameof(SR.CheckBoxCheckStateDescr))]
public CheckState CheckState
{
get => _checkState;
set
{
SourceGenerated.EnumValidator.Validate(value);
if (_checkState == value)
{
return;
}
// For the animated toggle switch, stop any in-flight animation (leaving the thumb at its current
// position) before the state changes, then start a fresh animation toward the new position.
bool animateToggleSwitch = IsToggleSwitchAppearance && IsHandleCreated;
if (animateToggleSwitch)
{
ToggleSwitchRenderer.PrepareStateChange();
}
bool oldChecked = Checked;
_checkState = value;
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvoke.BM_SETCHECK, (WPARAM)(int)_checkState);
}
bool checkedChanged = oldChecked != Checked;
if (checkedChanged)
{
OnCheckedChanged(EventArgs.Empty);
}
_notifyAccessibilityStateChangedNeeded = !checkedChanged;
OnCheckStateChanged(EventArgs.Empty);
_notifyAccessibilityStateChangedNeeded = false;
if (animateToggleSwitch)
{
ToggleSwitchRenderer.StartAnimation();
}
}
}
/// <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_3STATE;
if (Appearance == Appearance.Button)
{
cp.Style |= PInvoke.BS_PUSHLIKE;
}
// Determine the alignment of the check box
ContentAlignment align = RtlTranslateContent(CheckAlign);
if ((align & AnyRight) != 0)
{
cp.Style |= PInvoke.BS_RIGHTBUTTON;
}
}
return cp;
}
}
protected override Size DefaultSize => new(104, 24);
protected override void RescaleConstantsForDpi(int deviceDpiOld, int deviceDpiNew)
{
base.RescaleConstantsForDpi(deviceDpiOld, deviceDpiNew);
ScaleConstants();
}
private void ScaleConstants()
{
const int LogicalFlatSystemStylePaddingWidth = 25;
const int LogicalFlatSystemStyleMinimumHeight = 13;
_flatSystemStylePaddingWidth = LogicalToDeviceUnits(LogicalFlatSystemStylePaddingWidth);
_flatSystemStyleMinimumHeight = LogicalToDeviceUnits(LogicalFlatSystemStyleMinimumHeight);
}
internal override Size GetPreferredSizeCore(Size proposedConstraints)
{
if (IsToggleSwitchAppearance)
{
return Rendering.CheckBox.ToggleSwitchMetrics.Create(this).GetPreferredSize(this);
}
if (Appearance == Appearance.Button && FlatStyle == FlatStyle.Popup)
{
return DarkModeAdapterFactory.CreatePopupAdapter(this).GetPreferredSizeCore(proposedConstraints)
+ Padding.Size;
}
if (Appearance == Appearance.Button)
{
if (EffectiveVisualStylesModeInternal >= VisualStylesMode.Net11
&& FlatStyle is FlatStyle.Standard or FlatStyle.Flat)
{
ButtonBaseAdapter modernAdapter = FlatStyle == FlatStyle.Flat
? DarkModeAdapterFactory.CreateFlatAdapter(this)
: DarkModeAdapterFactory.CreateStandardAdapter(this);
return modernAdapter.GetPreferredSizeCore(proposedConstraints) + Padding.Size;
}
ButtonStandardAdapter adapter = new(this);
return adapter.GetPreferredSizeCore(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 check-box or text
size.Height = Math.Max(size.Height + 5, _flatSystemStyleMinimumHeight);
return size + Padding.Size;
}
internal override Rectangle OverChangeRectangle
{
get
{
if (IsToggleSwitchAppearance
|| Appearance == Appearance.Button
|| !OwnerDraw)
{
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
{
// Popup mouseover rectangle is actually bigger than GetCheckmarkRectangle
return Adapter.CommonLayout().Layout().CheckBounds;
}
}
}
internal override Rectangle DownChangeRectangle
{
get
{
if (IsToggleSwitchAppearance
|| Appearance == Appearance.Button
|| !OwnerDraw)
{
return base.DownChangeRectangle;
}
else
{
// Popup mouseover rectangle is actually bigger than GetCheckmarkRectangle()
return Adapter.CommonLayout().Layout().CheckBounds;
}
}
}
internal override bool SupportsUiaProviders => true;
/// <summary>
/// Gets or sets a value indicating the alignment of the text on the checkbox control.
/// </summary>
[Localizable(true)]
[DefaultValue(ContentAlignment.MiddleLeft)]
public override ContentAlignment TextAlign
{
get => base.TextAlign;
set => base.TextAlign = value;
}
/// <summary>
/// Gets or sets a value indicating whether the check box will allow three check states rather than two.
/// </summary>
[DefaultValue(false)]
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.CheckBoxThreeStateDescr))]
public bool ThreeState
{
get => _threeState;
set
{
if (_threeState == value)
{
return;
}
_toggleSwitchRenderer?.StopAnimation();
_threeState = value;
ResetAdapter();
UpdateOwnerDraw();
UpdateToggleSwitchStyles();
LayoutTransaction.DoLayoutIf(AutoSize, ParentInternal, this, nameof(ThreeState));
if (IsHandleCreated)
{
Invalidate();
}
}
}
/// <summary>
/// Occurs when the value of the <see cref="Checked"/> property changes.
/// </summary>
[SRDescription(nameof(SR.CheckBoxOnCheckedChangedDescr))]
public event EventHandler? CheckedChanged
{
add => Events.AddHandler(s_checkedChangedEvent, value);
remove => Events.RemoveHandler(s_checkedChangedEvent, value);
}
/// <summary>
/// Occurs when the value of the <see cref="CheckState"/> property changes.
/// </summary>
[SRDescription(nameof(SR.CheckBoxOnCheckStateChangedDescr))]
public event EventHandler? CheckStateChanged
{
add => Events.AddHandler(s_checkStateChangedEvent, value);
remove => Events.RemoveHandler(s_checkStateChangedEvent, value);
}
protected override AccessibleObject CreateAccessibilityInstance() => new CheckBoxAccessibleObject(this);
protected virtual void OnAppearanceChanged(EventArgs e)
{
if (Events[s_appearanceChangedEvent] is EventHandler eh)
{
eh(this, e);
}
}
/// <summary>
/// Raises the <see cref="CheckedChanged"/> event.
/// </summary>
protected virtual void OnCheckedChanged(EventArgs e)
{
NotifyAccessibilityStateChanged();
((EventHandler?)Events[s_checkedChangedEvent])?.Invoke(this, e);
}
/// <summary>
/// Raises the <see cref="CheckStateChanged"/> event.
/// </summary>
protected virtual void OnCheckStateChanged(EventArgs e)
{
if (OwnerDraw)
{
Refresh();
}
if (_notifyAccessibilityStateChangedNeeded)
{
NotifyAccessibilityStateChanged();
}
((EventHandler?)Events[s_checkStateChangedEvent])?.Invoke(this, e);
}
private void NotifyAccessibilityStateChanged()
{
if (FlatStyle == FlatStyle.System)
{
AccessibilityNotifyClients(AccessibleEvents.SystemCaptureStart, -1);
}
// 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);
}
if (FlatStyle == FlatStyle.System)
{
AccessibilityNotifyClients(AccessibleEvents.SystemCaptureEnd, -1);
}
}
protected override void OnClick(EventArgs e)
{
if (AutoCheck)
{
switch (CheckState)
{
case CheckState.Unchecked:
CheckState = CheckState.Checked;
break;
case CheckState.Checked:
if (ThreeState)
{
CheckState = CheckState.Indeterminate;
// If the check box is clicked as a result of AccObj::DoDefaultAction then the native check box
// does not fire OBJ_STATE_CHANGE event when going to Indeterminate state and we need to.
if (AccObjDoDefaultAction)
{
AccessibilityNotifyClients(AccessibleEvents.StateChange, -1);
}
}
else
{
CheckState = CheckState.Unchecked;
}
break;
default:
CheckState = CheckState.Unchecked;
break;
}
}
base.OnClick(e);
}
/// <summary>
/// We override this to ensure that the control's click values are set up correctly.
/// </summary>
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvoke.BM_SETCHECK, (WPARAM)(int)_checkState);
}
UpdateToggleSwitchStyles();
}
/// <inheritdoc/>
protected override void OnPaint(PaintEventArgs pevent)
{
if (IsToggleSwitchAppearance)
{
using GraphicsStateScope scope = new(pevent.Graphics);
ToggleSwitchRenderer.RenderControl(pevent.Graphics);
return;
}
base.OnPaint(pevent);
}
/// <inheritdoc/>
protected override void OnVisualStylesModeChanged(EventArgs e)
{
base.OnVisualStylesModeChanged(e);
// Entering or leaving the modern toggle-switch appearance changes whether the control is owner-drawn.
UpdateOwnerDraw();
UpdateToggleSwitchStyles();
if (IsHandleCreated)
{
Invalidate();
}
}
/// <inheritdoc/>
protected override void OnSystemColorsChanged(EventArgs e)
{
base.OnSystemColorsChanged(e);
_checkGlyphRenderer?.InvalidateAccentColor();
_toggleSwitchRenderer?.InvalidateAccentColor();
UpdateOwnerDraw();
UpdateToggleSwitchStyles();
}
internal ContentAlignment RtlTranslatedCheckAlign
=> RtlTranslateContent(CheckAlign);
internal bool ShowFocusCuesInternal => ShowFocusCues;
protected override void Dispose(bool disposing)
{
if (disposing)
{
_checkGlyphRenderer?.Dispose();
_checkGlyphRenderer = null;
_toggleSwitchRenderer?.Dispose();
_toggleSwitchRenderer = null;
}
base.Dispose(disposing);
}
/// <summary>
/// Raises the <see cref="ButtonBase.OnMouseUp"/> event.
/// </summary>
protected override void OnMouseUp(MouseEventArgs mevent)
{
// It's best not to have the mouse captured while running Click events
if (mevent.Button == MouseButtons.Left
&& MouseIsPressed
&& MouseIsDown
&& PInvoke.WindowFromPoint(PointToScreen(mevent.Location)) == HWND)
{
// Paint in raised state.
ResetFlagsandPaint();
if (!ValidationCancelled)
{
if (Capture)
{
OnClick(mevent);
}
OnMouseClick(mevent);
}
}
base.OnMouseUp(mevent);
}
internal override ButtonBaseAdapter CreateFlatAdapter()
=> UseModernGlyphRenderer
? new CheckBoxModernAdapter(this, FlatStyle.Flat)
: new CheckBoxFlatAdapter(this);
internal override ButtonBaseAdapter CreatePopupAdapter()
=> UseModernGlyphRenderer
? new CheckBoxModernAdapter(this, FlatStyle.Popup)
: new CheckBoxPopupAdapter(this);
internal override ButtonBaseAdapter CreateStandardAdapter()
=> UseModernGlyphRenderer
? new CheckBoxModernAdapter(this, FlatStyle.Standard)
: new CheckBoxStandardAdapter(this);
private bool UseModernGlyphRenderer
=> Appearance == Appearance.Normal
&& EffectiveVisualStylesMode >= VisualStylesMode.Net11;
/// <summary>
/// Overridden to handle mnemonics properly.
/// </summary>
protected internal override bool ProcessMnemonic(char charCode)
{
if (UseMnemonic && IsMnemonic(charCode, Text) && CanSelect)
{
if (Focus())
{
// Paint in raised state...
ResetFlagsandPaint();
if (!ValidationCancelled)
{
OnClick(EventArgs.Empty);
}
}
return true;
}
return false;
}
public override string ToString() => $"{base.ToString()}, CheckState: {(int)CheckState}";
}