File: System\Windows\Forms\Controls\GroupBox\GroupBox.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.Drawing.Text;
using System.Windows.Forms.Layout;
using System.Windows.Forms.VisualStyles;
 
namespace System.Windows.Forms;
 
/// <summary>
///  Encapsulates a standard Windows group box.
/// </summary>
[DefaultEvent(nameof(Enter))]
[DefaultProperty(nameof(Text))]
[Designer($"System.Windows.Forms.Design.GroupBoxDesigner, {AssemblyRef.SystemDesign}")]
[SRDescription(nameof(SR.DescriptionGroupBox))]
public partial class GroupBox : Control
{
    private int _fontHeight = -1;
    private Font? _cachedFont;
    private FlatStyle _flatStyle = FlatStyle.Standard;
 
    /// <summary>
    ///  Initializes a new instance of the <see cref="GroupBox"/> class.
    /// </summary>
    public GroupBox() : base()
    {
        // This class overrides GetPreferredSizeCore, let Control automatically cache the result
        SetExtendedState(ExtendedStates.UserPreferredSizeCache, true);
 
        SetStyle(ControlStyles.ContainerControl, true);
        SetStyle(ControlStyles.SupportsTransparentBackColor | ControlStyles.UserPaint |
            ControlStyles.ResizeRedraw, OwnerDraw);
 
        SetStyle(ControlStyles.Selectable, false);
        TabStop = false;
    }
 
    /// <summary>
    ///  Gets or sets a value indicating whether the control will allow drag and
    ///  drop operations and events to be used.
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public override bool AllowDrop
    {
        get => base.AllowDrop;
        set => base.AllowDrop = value;
    }
 
    /// <summary>
    ///  Override to re-expose AutoSize.
    /// </summary>
    [Browsable(true)]
    [EditorBrowsable(EditorBrowsableState.Always)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public override bool AutoSize
    {
        get => base.AutoSize;
        set => base.AutoSize = value;
    }
 
    [SRCategory(nameof(SR.CatPropertyChanged))]
    [SRDescription(nameof(SR.ControlOnAutoSizeChangedDescr))]
    [Browsable(true)]
    [EditorBrowsable(EditorBrowsableState.Always)]
    public new event EventHandler? AutoSizeChanged
    {
        add => base.AutoSizeChanged += value;
        remove => base.AutoSizeChanged -= value;
    }
 
    /// <summary>
    ///  Allows the control to optionally shrink when AutoSize is true.
    /// </summary>
    [SRDescription(nameof(SR.ControlAutoSizeModeDescr))]
    [SRCategory(nameof(SR.CatLayout))]
    [Browsable(true)]
    [DefaultValue(AutoSizeMode.GrowOnly)]
    [Localizable(true)]
    public AutoSizeMode AutoSizeMode
    {
        get => GetAutoSizeMode();
        set
        {
            SourceGenerated.EnumValidator.Validate(value);
 
            if (GetAutoSizeMode() == value)
            {
                return;
            }
 
            SetAutoSizeMode(value);
            if (ParentInternal is not null)
            {
                // DefaultLayout does not keep anchor information until it needs to. When
                // AutoSize became a common property, we could no longer blindly call into
                // DefaultLayout, so now we do a special InitLayout just for DefaultLayout.
                if (ParentInternal.LayoutEngine == DefaultLayout.Instance)
                {
                    ParentInternal.LayoutEngine.InitLayout(this, BoundsSpecified.Size);
                }
 
                LayoutTransaction.DoLayout(ParentInternal, this, PropertyNames.AutoSize);
            }
        }
    }
 
    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            if (!OwnerDraw)
            {
                cp.ClassName = PInvoke.WC_BUTTON;
                cp.Style |= PInvoke.BS_GROUPBOX;
            }
            else
            {
                // If we swap back to a different flat style we need to reset these.
                cp.ClassName = null;
                cp.Style &= ~PInvoke.BS_GROUPBOX;
            }
 
            cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_CONTROLPARENT;
 
            return cp;
        }
    }
 
    // Set the default Padding to 3 so that it is consistent with Everett
    protected override Padding DefaultPadding => new(3);
 
    protected override Size DefaultSize => new(200, 100);
 
    /// <summary>
    ///  Gets a rectangle that represents the dimensions of the <see cref="GroupBox"/>
    /// </summary>
    public override Rectangle DisplayRectangle
    {
        get
        {
            Size size = ClientSize;
 
            if (_fontHeight == -1)
            {
                _fontHeight = Font.Height;
                _cachedFont = Font;
            }
            else if (!ReferenceEquals(_cachedFont, Font))
            {
                // Must also cache font identity here because we need to provide an accurate DisplayRectangle
                // picture even before the OnFontChanged event bubbles through.
                _fontHeight = Font.Height;
                _cachedFont = Font;
            }
 
            // For efficiency, so that we don't need to read property store four times
            Padding padding = Padding;
            return new Rectangle(
                padding.Left,
                _fontHeight + padding.Top,
                Math.Max(size.Width - padding.Horizontal, 0),
                Math.Max(size.Height - _fontHeight - padding.Vertical, 0));
        }
    }
 
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue(FlatStyle.Standard)]
    [SRDescription(nameof(SR.ButtonFlatStyleDescr))]
    public FlatStyle FlatStyle
    {
        get
        {
            return _flatStyle;
        }
        set
        {
            // valid values are 0x0 to 0x3
            SourceGenerated.EnumValidator.Validate(value);
 
            if (_flatStyle == value)
            {
                return;
            }
 
            bool originalOwnerDraw = OwnerDraw;
            _flatStyle = value;
 
            // In CreateParams, we pick our class style based on OwnerDraw
            // if this has changed we need to recreate
            bool needRecreate = (OwnerDraw != originalOwnerDraw);
 
            SetStyle(ControlStyles.ContainerControl, true);
 
            SetStyle(ControlStyles.SupportsTransparentBackColor |
                     ControlStyles.UserPaint |
                     ControlStyles.ResizeRedraw |
                     ControlStyles.UserMouse, OwnerDraw);
 
            if (needRecreate)
            {
                RecreateHandle();
            }
            else
            {
                Refresh();
            }
        }
    }
 
    private bool OwnerDraw => FlatStyle != FlatStyle.System;
 
    /// <summary>
    ///  Gets or sets a value indicating whether the user may press the TAB key to give the focus to the
    ///  <see cref="GroupBox"/>.
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new bool TabStop
    {
        get => base.TabStop;
        set => base.TabStop = value;
    }
 
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event EventHandler? TabStopChanged
    {
        add => base.TabStopChanged += value;
        remove => base.TabStopChanged -= value;
    }
 
    [Localizable(true)]
    [AllowNull]
    public override string Text
    {
        get => base.Text;
        set
        {
            // the GroupBox controls immediately draws when the WM_SETTEXT comes through, but
            // does so in the wrong font, so we suspend that behavior, and then
            // invalidate.
            bool suspendRedraw = Visible;
            try
            {
                if (suspendRedraw && IsHandleCreated)
                {
                    PInvokeCore.SendMessage(this, PInvokeCore.WM_SETREDRAW, (WPARAM)(BOOL)false);
                }
 
                base.Text = value;
            }
            finally
            {
                if (suspendRedraw && IsHandleCreated)
                {
                    PInvokeCore.SendMessage(this, PInvokeCore.WM_SETREDRAW, (WPARAM)(BOOL)true);
                }
            }
 
            Invalidate(true);
        }
    }
 
    /// <summary>
    ///  Determines whether to use compatible text rendering engine (GDI+) or not (GDI).
    /// </summary>
    [DefaultValue(false)]
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.UseCompatibleTextRenderingDescr))]
    public bool UseCompatibleTextRendering
    {
        get => UseCompatibleTextRenderingInternal;
        set => UseCompatibleTextRenderingInternal = value;
    }
 
    /// <summary>
    ///  Determines whether the control supports rendering text using GDI+ and GDI. This is provided for container
    ///  controls to iterate through its children to set UseCompatibleTextRendering to the same value if the child
    ///  control supports it.
    /// </summary>
    internal override bool SupportsUseCompatibleTextRendering => true;
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event EventHandler? Click
    {
        add => base.Click += value;
        remove => base.Click -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event MouseEventHandler? MouseClick
    {
        add => base.MouseClick += value;
        remove => base.MouseClick -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event EventHandler? DoubleClick
    {
        add => base.DoubleClick += value;
        remove => base.DoubleClick -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event MouseEventHandler? MouseDoubleClick
    {
        add => base.MouseDoubleClick += value;
        remove => base.MouseDoubleClick -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event KeyEventHandler? KeyUp
    {
        add => base.KeyUp += value;
        remove => base.KeyUp -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event KeyEventHandler? KeyDown
    {
        add => base.KeyDown += value;
        remove => base.KeyDown -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event KeyPressEventHandler? KeyPress
    {
        add => base.KeyPress += value;
        remove => base.KeyPress -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event MouseEventHandler? MouseDown
    {
        add => base.MouseDown += value;
        remove => base.MouseDown -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event MouseEventHandler? MouseUp
    {
        add => base.MouseUp += value;
        remove => base.MouseUp -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event MouseEventHandler? MouseMove
    {
        add => base.MouseMove += value;
        remove => base.MouseMove -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event EventHandler? MouseEnter
    {
        add => base.MouseEnter += value;
        remove => base.MouseEnter -= value;
    }
 
    /// <hideinheritance/>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public new event EventHandler? MouseLeave
    {
        add => base.MouseLeave += value;
        remove => base.MouseLeave -= value;
    }
 
    protected override void OnPaint(PaintEventArgs e)
    {
        // BACKCOMPAT requirement:
        //
        // Why the Height/Width < 10 check? This is because uxtheme doesn't seem to handle those cases similar to
        // what we do for the non-themed case, so if someone is using the groupbox as a separator, their app will
        // look weird in .NET Framework 2.0. We render the old way in these cases.
 
        if (!Application.RenderWithVisualStyles || Width < 10 || Height < 10)
        {
            DrawGroupBox(e);
        }
        else
        {
            GroupBoxState gbState = Enabled ? GroupBoxState.Normal : GroupBoxState.Disabled;
            TextFormatFlags textFlags = TextFormatFlags.TextBoxControl | TextFormatFlags.WordBreak
                | TextFormatFlags.PreserveGraphicsTranslateTransform | TextFormatFlags.PreserveGraphicsClipping;
 
            if (!ShowKeyboardCues)
            {
                textFlags |= TextFormatFlags.HidePrefix;
            }
 
            if (RightToLeft == RightToLeft.Yes)
            {
                textFlags |= (TextFormatFlags.Right | TextFormatFlags.RightToLeft);
            }
 
            // We only pass in the text color if it is explicitly set, else we let the renderer use the color
            // specified by the theme. This is a temporary workaround till we find a good solution for the
            // "default theme color" issue.
#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
            if (ShouldSerializeForeColor() || Application.IsDarkModeEnabled || !Enabled)
            {
                Color textColor = Enabled ? ForeColor : TextRenderer.DisabledTextColor(BackColor);
 
                GroupBoxRenderer.DrawGroupBox(
                    e,
                    new Rectangle(0, 0, Width, Height),
                    Text,
                    Font,
                    textColor,
                    textFlags,
                    gbState);
            }
            else
            {
                GroupBoxRenderer.DrawGroupBox(
                    e,
                    new Rectangle(0, 0, Width, Height),
                    Text,
                    Font,
                    textFlags,
                    gbState);
            }
#pragma warning restore WFO5001
        }
 
        base.OnPaint(e); // raise paint event
    }
 
    private void DrawGroupBox(PaintEventArgs e)
    {
        // Offset from the left bound.
        const int TextOffset = 8;
 
        // Max text bounding box passed to drawing methods to support RTL.
        Rectangle textRectangle = ClientRectangle;
        textRectangle.X += TextOffset;
        textRectangle.Width -= 2 * TextOffset;
 
        Color backColor = DisabledColor;
        Size textSize;
 
        if (UseCompatibleTextRendering)
        {
            Graphics graphics = e.GraphicsInternal;
            using var textBrush = ForeColor.GetCachedSolidBrushScope();
            using StringFormat format = new StringFormat
            {
                HotkeyPrefix = ShowKeyboardCues ? HotkeyPrefix.Show : HotkeyPrefix.Hide
            };
 
            // Adjust string format for Rtl controls
 
            if (RightToLeft == RightToLeft.Yes)
            {
                format.FormatFlags |= StringFormatFlags.DirectionRightToLeft;
            }
 
            textSize = Size.Ceiling(graphics.MeasureString(Text, Font, textRectangle.Width, format));
 
            if (Enabled)
            {
                graphics.DrawString(Text, Font, textBrush, textRectangle, format);
            }
            else
            {
                ControlPaint.DrawStringDisabled(graphics, Text, Font, backColor, textRectangle, format);
            }
        }
        else
        {
            using DeviceContextHdcScope hdc = new(e);
 
            DRAW_TEXT_FORMAT flags = DRAW_TEXT_FORMAT.DT_WORDBREAK | DRAW_TEXT_FORMAT.DT_EDITCONTROL;
 
            if (!ShowKeyboardCues)
            {
                flags |= DRAW_TEXT_FORMAT.DT_HIDEPREFIX;
            }
 
            if (RightToLeft == RightToLeft.Yes)
            {
                flags |= DRAW_TEXT_FORMAT.DT_RTLREADING;
                flags |= DRAW_TEXT_FORMAT.DT_RIGHT;
            }
 
            using var hfont = GdiCache.GetHFONTScope(Font);
            textSize = hdc.HDC.MeasureText(Text, hfont, new Size(textRectangle.Width, int.MaxValue), (TextFormatFlags)flags);
 
            if (Enabled)
            {
                hdc.HDC.DrawText(Text, hfont, textRectangle, ForeColor, (TextFormatFlags)flags);
            }
            else
            {
                ControlPaint.DrawStringDisabled(
                    hdc,
                    Text,
                    Font,
                    backColor,
                    textRectangle,
                    (TextFormatFlags)flags);
            }
        }
 
        int textLeft = TextOffset;    // Left side of binding box (independent on RTL).
 
        if (RightToLeft == RightToLeft.Yes)
        {
            textLeft += textRectangle.Width - textSize.Width;
        }
 
        // Math.Min to assure we paint at least a small line.
        int textRight = Math.Min(textLeft + textSize.Width, Width - 6);
 
        int boxTop = FontHeight / 2;
 
        if (SystemInformation.HighContrast)
        {
            Color boxColor = Enabled ? ForeColor : SystemColors.GrayText;
 
            ReadOnlySpan<int> lines =
            [
                0, boxTop, 0, Height,                       // Left
                0, Height - 1, Width, Height - 1,           // Bottom
                0, boxTop, textLeft, boxTop,                // Top-left
                textRight, boxTop, Width - 1, boxTop,       // Top-right
                Width - 1, boxTop, Width - 1, Height - 1    // Right
            ];
 
            if (boxColor.HasTransparency())
            {
                Graphics graphics = e.GraphicsInternal;
                using var boxPen = boxColor.GetCachedPenScope();
                graphics.DrawLines(boxPen, lines);
            }
            else
            {
                using DeviceContextHdcScope hdc = new(e);
                using CreatePenScope hpen = new(boxColor);
                hdc.DrawLines(hpen, lines);
            }
        }
        else
        {
            ReadOnlySpan<int> lightLines =
            [
                1, boxTop, 1, Height - 1,                       // Left
                0, Height - 1, Width, Height - 1,               // Bottom
                1, boxTop, textLeft, boxTop,                    // Top-left
                textRight, boxTop, Width - 1, boxTop,           // Top-right
                Width - 1, boxTop - 1, Width - 1, Height - 1    // Right
            ];
 
            ReadOnlySpan<int> darkLines =
            [
                0, boxTop, 0, Height - 2,                       // Left
                0, Height - 2, Width - 1, Height - 2,           // Bottom
                0, boxTop - 1, textLeft, boxTop - 1,            // Top-left
                textRight, boxTop - 1, Width - 2, boxTop - 1,   // Top-right
                Width - 2, boxTop - 1, Width - 2, Height - 2    // Right
            ];
 
            using DeviceContextHdcScope hdc = new(e);
            using CreatePenScope hpenLight = new(ControlPaint.Light(backColor, 1.0f));
            hdc.DrawLines(hpenLight, lightLines);
            using CreatePenScope hpenDark = new(ControlPaint.Dark(backColor, 0f));
            hdc.DrawLines(hpenDark, darkLines);
        }
    }
 
    internal override Size GetPreferredSizeCore(Size proposedSize)
    {
        // Translating 0,0 from ClientSize to actual Size tells us how much space is required for the borders.
        Size borderSize = SizeFromClientSize(Size.Empty);
        Size totalPadding = borderSize + new Size(0, _fontHeight) + Padding.Size;
 
        Size prefSize = LayoutEngine.GetPreferredSize(this, proposedSize - totalPadding);
        return prefSize + totalPadding;
    }
 
    protected override void OnFontChanged(EventArgs e)
    {
        _fontHeight = -1;
        _cachedFont = null;
        Invalidate();
        base.OnFontChanged(e);
    }
 
    /// <summary>
    ///  We use this to process mnemonics and send them on to the first child
    ///  control.
    /// </summary>
    protected internal override bool ProcessMnemonic(char charCode)
    {
        if (IsMnemonic(charCode, Text) && CanProcessMnemonic())
        {
            SelectNextControl(null, true, true, true, false);
            return true;
        }
 
        return false;
    }
 
    protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
    {
        if (factor.Width != 1F && factor.Height != 1F)
        {
            // Make sure when we're scaling by non-unity to clear the font cache
            // as the font has likely changed, but we don't know it yet as OnFontChanged has yet to
            // be called on us by our parent.
            _fontHeight = -1;
            _cachedFont = null;
        }
 
        base.ScaleControl(factor, specified);
    }
 
    internal override bool SupportsUiaProviders => true;
 
    /// <summary>
    ///  Returns a string representation for this control.
    /// </summary>
    public override string ToString() => $"{base.ToString()}, Text: {Text}";
 
    /// <summary>
    ///  The Windows group box doesn't erase the background so we do it ourselves here.
    /// </summary>
    private void WmEraseBkgnd(ref Message m)
    {
        if (m.WParamInternal == 0u)
        {
            return;
        }
 
        PInvokeCore.GetClientRect(this, out RECT rect);
        Color backColor = BackColor;
 
        if (backColor.HasTransparency())
        {
            using Graphics graphics = Graphics.FromHdcInternal((HDC)m.WParamInternal);
            using var brush = backColor.GetCachedSolidBrushScope();
            graphics.FillRectangle(brush, rect);
        }
        else
        {
            using var hbrush = new CreateBrushScope(backColor);
            PInvoke.FillRect((HDC)m.WParamInternal, rect, hbrush);
        }
 
        m.ResultInternal = (LRESULT)1;
    }
 
    protected override void WndProc(ref Message m)
    {
        if (OwnerDraw)
        {
            base.WndProc(ref m);
            return;
        }
 
        switch (m.MsgInternal)
        {
            case PInvokeCore.WM_ERASEBKGND:
            case PInvokeCore.WM_PRINTCLIENT:
                WmEraseBkgnd(ref m);
                break;
            case PInvokeCore.WM_GETOBJECT:
                base.WndProc(ref m);
 
                // Force MSAA to always treat a group box as a custom window. This ensures its child controls
                // will always be exposed through MSAA. Reason: When FlatStyle=System, we map down to the Win32
                // "Button" window class to get OS group box rendering; but the OS does not expose the children
                // of buttons to MSAA (because it assumes buttons won't have children).
                if (m.LParamInternal == (int)OBJECT_IDENTIFIER.OBJID_QUERYCLASSNAMEIDX)
                {
                    m.ResultInternal = (LRESULT)0;
                }
 
                break;
            default:
                base.WndProc(ref m);
                break;
        }
    }
 
    protected override AccessibleObject CreateAccessibilityInstance() => new GroupBoxAccessibleObject(this);
}