File: System\Windows\Forms\Controls\GroupBox\GroupBox.Modern.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.Collections.Concurrent;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
using System.Windows.Forms.Layout;
using System.Windows.Forms.Rendering.Button;
 
namespace System.Windows.Forms;
 
public partial class GroupBox
{
    private const string MissingSemiBoldFamily = "";
 
    private static readonly ConcurrentDictionary<string, string> s_semiBoldFamilyNames =
        new(StringComparer.OrdinalIgnoreCase);
 
    private Font? _modernCaptionFont;
    private Font? _modernCaptionSourceFont;
    private FlatStyle _modernCaptionFlatStyle;
    private float _modernCaptionTextScale;
    private int _modernCaptionDpi;
 
    private bool UsesModernRenderer
        => OwnerDraw
            && EffectiveVisualStylesMode >= VisualStylesMode.Net11;
 
    private Font ModernCaptionFont
    {
        get
        {
            float textScale = Math.Clamp(
                Application.SystemVisualSettings.TextScaleFactor,
                1f,
                2.25f);
 
            if (_modernCaptionFont is null
                || !ReferenceEquals(_modernCaptionSourceFont, Font)
                || _modernCaptionFlatStyle != FlatStyle
                || _modernCaptionTextScale != textScale
                || _modernCaptionDpi != DeviceDpiInternal)
            {
                InvalidateModernCaptionFont();
 
                _modernCaptionFont = CreateModernCaptionFont(textScale);
                _modernCaptionSourceFont = Font;
                _modernCaptionFlatStyle = FlatStyle;
                _modernCaptionTextScale = textScale;
                _modernCaptionDpi = DeviceDpiInternal;
            }
 
            return _modernCaptionFont;
        }
    }
 
    private Rectangle ModernDisplayRectangle
    {
        get
        {
            Padding decoration = GetModernDecorationPadding();
            Size size = ClientSize;
 
            return new Rectangle(
                decoration.Left,
                decoration.Top,
                Math.Max(size.Width - decoration.Horizontal, 0),
                Math.Max(size.Height - decoration.Vertical, 0));
        }
    }
 
    private Padding GetModernDecorationPadding()
    {
        int captionHeight = ModernCaptionFont.Height;
 
        switch (FlatStyle)
        {
            case FlatStyle.Standard:
            {
                // Card: reserve the caption band plus its visual gap to the card. No internal
                // horizontal or bottom inset, so a docked child with Padding = 0 fills the card.
                int gap = ScaleModernMetric(ModernControlVisualStyles.GroupBoxCaptionGap);
 
                return new Padding(
                    left: Padding.Left,
                    top: Padding.Top + captionHeight + gap,
                    right: Padding.Right,
                    bottom: Padding.Bottom);
            }
 
            case FlatStyle.Flat:
            {
                // Outline: the top border line runs along the caption baseline, so content clears
                // the descenders that hang below it (descent + 1px leeway). Left/right/bottom sit
                // just inside the border.
                (int ascent, int descent) = GetModernCaptionMetrics();
                int leeway = ScaleModernMetric(
                    ModernControlVisualStyles.GroupBoxFlatBaselineLeeway);
                int borderInset = GetModernBorderThickness() + leeway;
 
                return new Padding(
                    left: Padding.Left + borderInset,
                    top: Padding.Top + ascent + descent + leeway,
                    right: Padding.Right + borderInset,
                    bottom: Padding.Bottom + borderInset);
            }
 
            case FlatStyle.Popup:
            {
                // Accent header: content is flush to the bottom of the filled header rectangle
                // (0 top gap) with a 2px inset on the remaining sides.
                int verticalPadding = ScaleModernMetric(
                    ModernControlVisualStyles.GroupBoxHeaderVerticalPadding);
                int headerHeight = captionHeight + (2 * verticalPadding);
                int inset = ScaleModernMetric(
                    ModernControlVisualStyles.GroupBoxPopupContentInset);
 
                return new Padding(
                    left: Padding.Left + inset,
                    top: Padding.Top + headerHeight,
                    right: Padding.Right + inset,
                    bottom: Padding.Bottom + inset);
            }
 
            default:
            {
                int gap = ScaleModernMetric(ModernControlVisualStyles.GroupBoxCaptionGap);
 
                return new Padding(
                    left: Padding.Left,
                    top: Padding.Top + captionHeight + gap,
                    right: Padding.Right,
                    bottom: Padding.Bottom);
            }
        }
    }
 
    /// <summary>
    ///  Returns the modern caption font's ascent and descent in device pixels.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   The values are derived from the font family's design metrics (cell ascent/descent scaled
    ///   against the line spacing) applied to the caption font's device-pixel line height, so they
    ///   track the current DPI and text-scale factor.
    ///  </para>
    /// </remarks>
    private (int Ascent, int Descent) GetModernCaptionMetrics()
    {
        Font font = ModernCaptionFont;
        FontFamily family = font.FontFamily;
        FontStyle style = font.Style;
 
        int lineSpacingDesignUnits = family.GetLineSpacing(style);
        if (lineSpacingDesignUnits <= 0)
        {
            return (font.Height, 0);
        }
 
        float lineHeightPixels = font.GetHeight(DeviceDpiInternal);
        int ascent = (int)Math.Ceiling(
            lineHeightPixels * family.GetCellAscent(style) / lineSpacingDesignUnits);
        int descent = (int)Math.Ceiling(
            lineHeightPixels * family.GetCellDescent(style) / lineSpacingDesignUnits);
 
        return (ascent, descent);
    }
 
    private void DrawModernGroupBox(PaintEventArgs e)
    {
        Rectangle clientBounds = ClientRectangle;
        if (clientBounds.Width <= 1 || clientBounds.Height <= 1)
        {
            return;
        }
 
        Rectangle strokeBounds = clientBounds;
        strokeBounds.Width--;
        strokeBounds.Height--;
 
        using GraphicsStateScope state = new(e.Graphics);
        if (FlatStyle != FlatStyle.Standard)
        {
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
        }
 
        switch (FlatStyle)
        {
            case FlatStyle.Standard:
                DrawModernCard(e, clientBounds);
                break;
            case FlatStyle.Flat:
                DrawModernOutline(e, strokeBounds);
                break;
            case FlatStyle.Popup:
                DrawModernPopup(e, strokeBounds);
                break;
        }
    }
 
    private void DrawModernCard(PaintEventArgs e, Rectangle bounds)
    {
        int captionHeight = ModernCaptionFont.Height;
        int captionGap = ScaleModernMetric(
            ModernControlVisualStyles.GroupBoxCaptionGap);
        Rectangle frameBounds = new(
            bounds.Left,
            bounds.Top + captionHeight + captionGap,
            bounds.Width,
            Math.Max(0, bounds.Height - captionHeight - captionGap));
        Rectangle captionBounds = GetStandardCaptionBounds(
            bounds,
            captionHeight);
 
        Color effectiveBackColor = DisabledColor;
        Color surfaceColor = PopupButtonColorMath.TowardsContrast(
            effectiveBackColor,
            0.035f);
        if (!Enabled)
        {
            surfaceColor = PopupButtonColorMath.Mute(surfaceColor, 0.55f);
        }
 
        using var surfaceBrush = surfaceColor.GetCachedSolidBrushScope();
        e.Graphics.FillRectangle(surfaceBrush, frameBounds);
 
        // The modern card fills an opaque surface over the whole body, which would hide a
        // BackgroundImage painted by OnPaintBackground. Composite the image back over the surface
        // so it remains visible (issue #14779).
        PaintModernBackgroundImage(e, frameBounds);
 
        DrawModernCaption(
            e.Graphics,
            captionBounds,
            GetCaptionColor(effectiveBackColor));
    }
 
    /// <summary>
    ///  Composites the control's <see cref="Control.BackgroundImage"/> over an already-painted
    ///  modern surface, clipped to <paramref name="clipBounds"/>.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   The modern GroupBox renderers fill an opaque body surface that would otherwise hide the
    ///   BackgroundImage that <see cref="Control.OnPaintBackground"/> painted. Using a transparent
    ///   back color keeps the surface visible around a non-tiling image layout while restoring the
    ///   image itself.
    ///  </para>
    /// </remarks>
    private void PaintModernBackgroundImage(PaintEventArgs e, Rectangle clipBounds)
    {
        if (BackgroundImage is null || clipBounds.Width <= 0 || clipBounds.Height <= 0)
        {
            return;
        }
 
        ControlPaint.DrawBackgroundImage(
            e.Graphics,
            BackgroundImage,
            Color.Transparent,
            BackgroundImageLayout,
            ClientRectangle,
            clipBounds,
            DisplayRectangle.Location,
            RightToLeft);
    }
 
    private void DrawModernOutline(PaintEventArgs e, Rectangle bounds)
    {
        int captionHeight = ModernCaptionFont.Height;
        (int ascent, _) = GetModernCaptionMetrics();
        int cornerRadius = ScaleModernMetric(
            ModernControlVisualStyles.GroupBoxCornerRadius);
 
        // The top border line runs along the caption text baseline.
        int baseline = bounds.Top + ascent;
        Rectangle frameBounds = new(
            bounds.Left,
            baseline,
            bounds.Width,
            Math.Max(0, bounds.Bottom - baseline));
 
        Rectangle captionBounds = GetFlatCaptionBounds(
            bounds,
            captionHeight,
            cornerRadius);
 
        Color effectiveBackColor = DisabledColor;
        Color borderColor = Application.SystemVisualSettings.AccentColor;
        if (!Enabled)
        {
            borderColor = PopupButtonColorMath.Mute(borderColor, 0.55f);
        }
 
        DrawRoundedFrame(e.Graphics, frameBounds, borderColor);
 
        Rectangle captionBackground = GetFlatCaptionBackgroundBounds(
            e.Graphics,
            captionBounds,
            frameBounds);
        if (!captionBackground.IsEmpty)
        {
            PaintBackground(e, captionBackground);
        }
 
        DrawModernCaption(
            e.Graphics,
            captionBounds,
            GetCaptionColor(effectiveBackColor));
    }
 
    private void DrawModernPopup(PaintEventArgs e, Rectangle bounds)
    {
        int verticalPadding = ScaleModernMetric(
            ModernControlVisualStyles.GroupBoxHeaderVerticalPadding);
        int horizontalPadding = ScaleModernMetric(
            ModernControlVisualStyles.GroupBoxHeaderHorizontalPadding);
        int headerHeight = ModernCaptionFont.Height + (2 * verticalPadding);
        Rectangle headerBounds = new(
            bounds.Left,
            bounds.Top,
            bounds.Width,
            Math.Min(bounds.Height, headerHeight));
 
        Rectangle captionBounds = GetPopupCaptionBounds(
            bounds,
            ModernCaptionFont.Height,
            horizontalPadding,
            verticalPadding);
 
        Color bodyColor = BackColor.A == 0
            ? Color.Transparent
            : BackColor;
        Color headerColor = Application.SystemVisualSettings.AccentColor;
        Color borderColor = PopupButtonColorMath.TowardsContrast(
            headerColor,
            0.2f);
        if (!Enabled)
        {
            bodyColor = PopupButtonColorMath.Mute(bodyColor, 0.55f);
            headerColor = PopupButtonColorMath.Mute(headerColor, 0.55f);
            borderColor = PopupButtonColorMath.Mute(borderColor, 0.55f);
        }
 
        using GraphicsPath path = CreateModernFramePath(bounds);
        using (var bodyBrush = bodyColor.GetCachedSolidBrushScope())
        {
            e.Graphics.FillPath(bodyBrush, path);
        }
 
        // Composite the BackgroundImage over the opaque body fill, clipped to the rounded frame,
        // so it stays visible under the modern Popup surface (issue #14779).
        if (BackgroundImage is not null)
        {
            using GraphicsStateScope imageState = new(e.Graphics);
            e.Graphics.SetClip(path);
            PaintModernBackgroundImage(e, bounds);
        }
 
        using (GraphicsStateScope state = new(e.Graphics))
        {
            e.Graphics.SetClip(headerBounds);
            using var headerBrush = headerColor.GetCachedSolidBrushScope();
            e.Graphics.FillPath(headerBrush, path);
        }
 
        using var borderPen = borderColor.GetCachedPenScope(
            GetModernBorderThickness());
        e.Graphics.DrawPath(borderPen, path);
 
        // The rounded frame is clipped with a non-antialiased region; blend the resulting corner
        // artifacts into the parent by tracing the parent color just outside the border.
        int frameRadius = GetModernFrameCornerRadius(bounds);
        ParentBackgroundRenderer.PaintRoundedBorderRegionMitigation(
            e.Graphics,
            bounds,
            new Size(frameRadius, frameRadius),
            GetModernBorderThickness(),
            ParentInternal?.BackColor ?? BackColor);
 
        Color captionColor = Enabled
            ? PopupButtonColorMath.GetReadableForeColor(headerColor)
            : ModernControlColorMath.GetDisabledTextColor(
                PopupButtonColorMath.GetReadableForeColor(headerColor),
                headerColor);
        DrawModernCaption(e.Graphics, captionBounds, captionColor);
    }
 
    private Rectangle GetFlatCaptionBounds(
        Rectangle bounds,
        int captionHeight,
        int cornerRadius)
    {
        // The caption is indented by the corner radius so it clears the rounded corner. Padding
        // does not move the Flat caption. In RTL the indent is applied on the right.
        bool rightToLeft = RightToLeft == RightToLeft.Yes;
        int captionX = rightToLeft
            ? bounds.Left
            : bounds.Left + cornerRadius;
        int captionOffset = ScaleModernMetric(
            ModernControlVisualStyles.BorderThickness);
 
        return new Rectangle(
            captionX,
            bounds.Top + captionOffset,
            Math.Max(0, bounds.Width - cornerRadius),
            captionHeight);
    }
 
    private Rectangle GetFlatCaptionBackgroundBounds(
        Graphics graphics,
        Rectangle captionBounds,
        Rectangle frameBounds)
    {
        Rectangle textBounds = GetCaptionTextBounds(
            graphics,
            captionBounds);
        if (textBounds.IsEmpty)
        {
            return Rectangle.Empty;
        }
 
        int gap = ScaleModernMetric(
            ModernControlVisualStyles.GroupBoxCaptionGap);
        int left = Math.Max(
            frameBounds.Left,
            textBounds.Left - gap);
        int right = Math.Min(
            frameBounds.Right,
            textBounds.Right + gap);
 
        return right <= left
            ? Rectangle.Empty
            : new Rectangle(
                left,
                captionBounds.Top,
                right - left,
                captionBounds.Height);
    }
 
    private Rectangle GetPopupCaptionBounds(
        Rectangle bounds,
        int captionHeight,
        int horizontalPadding,
        int verticalPadding)
    {
        Rectangle captionBounds = GetStandardCaptionBounds(
            bounds,
            captionHeight);
 
        return new Rectangle(
            captionBounds.Left + horizontalPadding,
            captionBounds.Top + verticalPadding,
            Math.Max(
                0,
                captionBounds.Width - (2 * horizontalPadding)),
            captionHeight);
    }
 
    private void DrawRoundedFrame(
        Graphics graphics,
        Rectangle bounds,
        Color borderColor)
    {
        if (bounds.Width <= 0 || bounds.Height <= 0)
        {
            return;
        }
 
        using GraphicsPath path = CreateModernFramePath(bounds);
        using var pen = borderColor.GetCachedPenScope(
            GetModernBorderThickness());
        graphics.DrawPath(pen, path);
 
        // The rounded frame is clipped with a non-antialiased region; blend the resulting corner
        // artifacts into the parent by tracing the parent color just outside the border.
        int frameRadius = GetModernFrameCornerRadius(bounds);
        ParentBackgroundRenderer.PaintRoundedBorderRegionMitigation(
            graphics,
            bounds,
            new Size(frameRadius, frameRadius),
            GetModernBorderThickness(),
            ParentInternal?.BackColor ?? BackColor);
    }
 
    private GraphicsPath CreateModernFramePath(Rectangle bounds)
    {
        GraphicsPath path = new();
        int radius = GetModernFrameCornerRadius(bounds);
        path.AddRoundedRectangle(bounds, new Size(radius, radius));
 
        return path;
    }
 
    private int GetModernFrameCornerRadius(Rectangle bounds)
        => Math.Clamp(
            ScaleModernMetric(ModernControlVisualStyles.GroupBoxCornerRadius),
            1,
            Math.Max(1, Math.Min(bounds.Width, bounds.Height)));
 
    private Color GetCaptionColor(Color backgroundColor)
        => Enabled
            ? ForeColor
            : ModernControlColorMath.GetDisabledTextColor(
                ForeColor,
                backgroundColor);
 
    private Rectangle GetStandardCaptionBounds(
        Rectangle bounds,
        int captionHeight)
    {
        // Padding only shifts the caption horizontally: right by Padding.Left in LTR, left by
        // Padding.Right in RTL. It never moves the caption vertically.
        bool rightToLeft = RightToLeft == RightToLeft.Yes;
        int leftOffset = rightToLeft ? 0 : Padding.Left;
        int rightOffset = rightToLeft ? Padding.Right : 0;
 
        return new Rectangle(
            bounds.Left + leftOffset,
            bounds.Top,
            Math.Max(0, bounds.Width - leftOffset - rightOffset),
            captionHeight);
    }
 
    private Font CreateModernCaptionFont(float textScale)
    {
        float styleScale = FlatStyle == FlatStyle.Flat
            ? 1f
            : ModernControlVisualStyles.GroupBoxCaptionFontScale;
        string semiBoldFamilyName = Font.Style == FontStyle.Regular
            ? s_semiBoldFamilyNames.GetOrAdd(
                Font.FontFamily.Name,
                FindSemiBoldFamilyName)
            : MissingSemiBoldFamily;
 
        if (semiBoldFamilyName.Length == 0)
        {
            return new Font(
                Font.FontFamily,
                Font.Size * styleScale * textScale,
                Font.Style,
                Font.Unit,
                Font.GdiCharSet,
                Font.GdiVerticalFont);
        }
 
        using FontFamily semiBoldFamily = new(semiBoldFamilyName);
        return new Font(
            semiBoldFamily,
            Font.Size * styleScale * textScale,
            FontStyle.Regular,
            Font.Unit,
            Font.GdiCharSet,
            Font.GdiVerticalFont);
    }
 
    internal static string FindSemiBoldFamilyName(string sourceFamilyName)
    {
        string baseFamilyName = sourceFamilyName.EndsWith(
            " Regular",
            StringComparison.OrdinalIgnoreCase)
            ? sourceFamilyName[..^" Regular".Length]
            : sourceFamilyName;
        string expectedName = IsSemiBoldFamilyName(sourceFamilyName)
            ? sourceFamilyName
            : $"{baseFamilyName} Semibold";
 
        using InstalledFontCollection installedFonts = new();
        foreach (FontFamily family in installedFonts.Families)
        {
            if (family.Name.Equals(
                expectedName,
                StringComparison.OrdinalIgnoreCase))
            {
                return family.Name;
            }
        }
 
        return MissingSemiBoldFamily;
    }
 
    internal static bool IsSemiBoldFamilyName(string familyName)
        => familyName.Contains(
            "Semibold",
            StringComparison.OrdinalIgnoreCase)
            || familyName.Contains(
                "Semi Bold",
                StringComparison.OrdinalIgnoreCase)
            || familyName.Contains(
                "Demibold",
                StringComparison.OrdinalIgnoreCase)
            || familyName.Contains(
                "Demi Bold",
                StringComparison.OrdinalIgnoreCase);
 
    private int GetModernBorderThickness()
    {
        SystemVisualSettings settings = Application.SystemVisualSettings;
        Size borderMetrics = ModernControlVisualStyles.GetFocusBorderMetrics(
            settings.FocusBorderMetrics,
            settings.TextScaleFactor,
            DeviceDpiInternal);
 
        return Math.Max(borderMetrics.Width, borderMetrics.Height);
    }
 
    private Rectangle GetCaptionTextBounds(
        Graphics graphics,
        Rectangle availableBounds)
    {
        if (string.IsNullOrEmpty(Text)
            || availableBounds.Width <= 0
            || availableBounds.Height <= 0)
        {
            return Rectangle.Empty;
        }
 
        Size measuredSize = UseCompatibleTextRendering
            ? Size.Ceiling(
                MeasureModernCaption(
                    graphics,
                    availableBounds.Size))
            : TextRenderer.MeasureText(
                graphics,
                Text,
                ModernCaptionFont,
                availableBounds.Size,
                GetModernCaptionTextFormatFlags());
        int width = Math.Min(measuredSize.Width, availableBounds.Width);
        int x = RightToLeft == RightToLeft.Yes
            ? availableBounds.Right - width
            : availableBounds.Left;
 
        return new Rectangle(
            x,
            availableBounds.Top,
            width,
            availableBounds.Height);
    }
 
    private void DrawModernCaption(
        Graphics graphics,
        Rectangle bounds,
        Color color)
    {
        if (string.IsNullOrEmpty(Text) || bounds.Width <= 0 || bounds.Height <= 0)
        {
            return;
        }
 
        if (UseCompatibleTextRendering)
        {
            using var brush = color.GetCachedSolidBrushScope();
            using StringFormat format = CreateModernCaptionStringFormat();
 
            graphics.DrawString(Text, ModernCaptionFont, brush, bounds, format);
            return;
        }
 
        TextRenderer.DrawText(
            graphics,
            Text,
            ModernCaptionFont,
            bounds,
            color,
            GetModernCaptionTextFormatFlags());
    }
 
    private StringFormat CreateModernCaptionStringFormat()
    {
        StringFormat format = new()
        {
            Alignment = StringAlignment.Near,
            LineAlignment = StringAlignment.Center,
            HotkeyPrefix = ShowKeyboardCues
                ? HotkeyPrefix.Show
                : HotkeyPrefix.Hide,
            Trimming = StringTrimming.EllipsisCharacter
        };
 
        if (RightToLeft == RightToLeft.Yes)
        {
            format.FormatFlags |= StringFormatFlags.DirectionRightToLeft;
        }
 
        return format;
    }
 
    private TextFormatFlags GetModernCaptionTextFormatFlags()
    {
        TextFormatFlags flags = TextFormatFlags.SingleLine
            | TextFormatFlags.VerticalCenter
            | TextFormatFlags.EndEllipsis
            | TextFormatFlags.PreserveGraphicsClipping
            | TextFormatFlags.PreserveGraphicsTranslateTransform;
        if (!ShowKeyboardCues)
        {
            flags |= TextFormatFlags.HidePrefix;
        }
 
        if (RightToLeft == RightToLeft.Yes)
        {
            flags |= TextFormatFlags.Right | TextFormatFlags.RightToLeft;
        }
 
        return flags;
    }
 
    private SizeF MeasureModernCaption(
        Graphics graphics,
        Size availableSize)
    {
        using StringFormat format = CreateModernCaptionStringFormat();
 
        return graphics.MeasureString(
            Text,
            ModernCaptionFont,
            availableSize,
            format);
    }
 
    private int ScaleModernMetric(int value)
        => ScaleHelper.ScaleToDpi(value, DeviceDpiInternal);
 
    private void InvalidateModernCaptionFont()
    {
        _modernCaptionFont?.Dispose();
        _modernCaptionFont = null;
        _modernCaptionSourceFont = null;
        _modernCaptionFlatStyle = default;
        _modernCaptionTextScale = 0f;
        _modernCaptionDpi = 0;
    }
 
    /// <inheritdoc/>
    protected override void OnSystemVisualSettingsChanged(
        SystemVisualSettingsChangedEventArgs e)
    {
        base.OnSystemVisualSettingsChanged(e);
 
        if (!UsesModernRenderer)
        {
            return;
        }
 
        if ((e.Changed & SystemVisualSettingsCategories.TextScale) != 0)
        {
            InvalidateModernCaptionFont();
            CommonProperties.xClearPreferredSizeCache(this);
            LayoutTransaction.DoLayout(
                this,
                this,
                PropertyNames.SystemVisualSettings);
            if (ParentInternal is { } parent)
            {
                LayoutTransaction.DoLayout(
                    parent,
                    this,
                    PropertyNames.SystemVisualSettings);
            }
        }
 
        if ((e.Changed
            & (SystemVisualSettingsCategories.TextScale
                | SystemVisualSettingsCategories.AccentColor
                | SystemVisualSettingsCategories.FocusMetrics)) != 0)
        {
            Invalidate();
        }
    }
 
    /// <inheritdoc/>
    /// <remarks>
    ///  <para>
    ///   Net11 and later share GroupBox metrics. Crossing the classic or disabled boundary
    ///   changes the caption and content geometry, while modern-to-modern transitions repaint.
    ///  </para>
    /// </remarks>
    protected override VisualStylesModeChangeImpact GetVisualStylesModeChangeImpact(
        VisualStylesMode oldMode,
        VisualStylesMode newMode)
    {
        if (FlatStyle == FlatStyle.System)
        {
            return VisualStylesModeChangeImpact.None;
        }
 
        bool oldUsesModernMetrics = oldMode >= VisualStylesMode.Net11;
        bool newUsesModernMetrics = newMode >= VisualStylesMode.Net11;
 
        return oldUsesModernMetrics != newUsesModernMetrics
            ? VisualStylesModeChangeImpact.Metrics
            : VisualStylesModeChangeImpact.Repaint;
    }
 
    /// <inheritdoc/>
    protected override void RescaleConstantsForDpi(
        int deviceDpiOld,
        int deviceDpiNew)
    {
        InvalidateModernCaptionFont();
        base.RescaleConstantsForDpi(deviceDpiOld, deviceDpiNew);
    }
 
    /// <inheritdoc/>
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            InvalidateModernCaptionFont();
        }
 
        base.Dispose(disposing);
    }
}