|
// 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,{AssemblyRef.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 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 bool AccObjDoDefaultAction { get; set; }
/// <summary>
/// Gets or sets the value that determines the appearance of a check box control.
/// </summary>
[DefaultValue(Appearance.Normal)]
[Localizable(true)]
[SRCategory(nameof(SR.CatAppearance))]
[SRDescription(nameof(SR.CheckBoxAppearanceDescr))]
public Appearance Appearance
{
get => _appearance;
set
{
SourceGenerated.EnumValidator.Validate(value);
if (_appearance == value)
{
return;
}
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.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;
}
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;
}
}
/// <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 protected override void InitializeConstantsForInitialDpi(int initialDpi) => 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 (Appearance == Appearance.Button)
{
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 (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
{
// Popup mouseover rectangle is actually bigger than GetCheckmarkRectangle
return Adapter.CommonLayout().Layout().CheckBounds;
}
}
}
internal override Rectangle DownChangeRectangle
{
get
{
if (Appearance == Appearance.Button || FlatStyle == FlatStyle.System)
{
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; set; }
/// <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);
}
}
/// <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() => new CheckBoxFlatAdapter(this);
internal override ButtonBaseAdapter CreatePopupAdapter() => new CheckBoxPopupAdapter(this);
internal override ButtonBaseAdapter CreateStandardAdapter() => new CheckBoxStandardAdapter(this);
/// <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}";
}
|