| File: System\Windows\Forms\Controls\Buttons\CheckBox.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> /// 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> [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}"; }