ElektroStudios
ElektroStudios

Reputation: 20464

Sub-Classed Groupbox does not properly draw the contents

SCENARIO

In WinForms, I've sub-classed a GroupBox to change the border color of this control.

PROBLEM

At design mode (in the visual builder of VisualStudio), if I perform any kind of changes to the controls inside my Groupbox lets say click on each control to change the textfont then my Groupbox redraws the controls like this:

enter image description here

Note: After invalidating the control it redraws again properly.

QUESTION

This is a known issue when owner-drawing a container that stores a control collection like a GroupBox?

What I'm missing to do in the OnPaint method to fix this painting issue?

CODE

VB Version:

''' <summary>
''' Handles the <see cref="E:Paint"/> event.
''' </summary>
''' <param name="e">A <see cref="T:PaintEventArgs"/> that contains the event data.</param>
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)

    '  MyBase.OnPaint(e)
    Me.DrawBorder(e)

End Sub

''' <summary>
''' Draws a border on the control surface.
''' </summary>
Private Sub DrawBorder(ByVal e As PaintEventArgs)

    ' The groupbox header text size.
    Dim textSize As Size = TextRenderer.MeasureText(Me.Text, Me.Font)

    ' The width of the blankspace drawn at the right side of the text.
    Dim blankWidthSpace As Integer = 3

    ' The thex horizontal offset.
    Dim textOffset As Integer = 7

    ' The rectangle where to draw the border.
    Dim borderRect As Rectangle = e.ClipRectangle
    With borderRect
        .Y = .Y + (textSize.Height \ 2)
        .Height = .Height - (textSize.Height \ 2)
    End With

    ' The rectangle where to draw the header text.
    Dim textRect As Rectangle = e.ClipRectangle
    With textRect
        .X = .X + textOffset
        .Width = (textSize.Width - blankWidthSpace)
        .Height = textSize.Height
    End With

    ' Draw the border.
    ControlPaint.DrawBorder(e.Graphics, borderRect, Me.borderColor1, Me.borderStyle1)

    ' Fill the text rectangle.
    e.Graphics.FillRectangle(New SolidBrush(Me.BackColor), textRect)

    ' Draw the text on the text rectangle.
    textRect.Width = textSize.Width + (blankWidthSpace * 2) ' Fix the right side space.
    e.Graphics.DrawString(Me.Text, Me.Font, New SolidBrush(Me.ForeColor), textRect)

End Sub

C# version:

/// <summary>
/// Handles the <see cref="E:Paint"/> event.
/// </summary>
/// <param name="e">A <see cref="T:PaintEventArgs"/> that contains the event data.</param>

protected override void OnPaint(PaintEventArgs e)
{
    //  MyBase.OnPaint(e)
    this.DrawBorder(e);

/// <summary>
/// Draws a border on the control surface.
/// </summary>

private void DrawBorder(PaintEventArgs e)
{
    // The groupbox header text size.
    Size textSize = TextRenderer.MeasureText(this.Text, this.Font);

    // The width of the blankspace drawn at the right side of the text.
    int blankWidthSpace = 3;

    // The thex horizontal offset.
    int textOffset = 7;

    // The rectangle where to draw the border.
    Rectangle borderRect = e.ClipRectangle;
    var _with1 = borderRect;
    _with1.Y = _with1.Y + (textSize.Height / 2);
    _with1.Height = _with1.Height - (textSize.Height / 2);

    // The rectangle where to draw the header text.
    Rectangle textRect = e.ClipRectangle;
    var _with2 = textRect;
    _with2.X = _with2.X + textOffset;
    _with2.Width = (textSize.Width - blankWidthSpace);
    _with2.Height = textSize.Height;

    // Draw the border.
    ControlPaint.DrawBorder(e.Graphics, borderRect, this.borderColor1, this.borderStyle1);

    // Fill the text rectangle.
    e.Graphics.FillRectangle(new SolidBrush(this.BackColor), textRect);

    // Draw the text on the text rectangle.
    textRect.Width = textSize.Width + (blankWidthSpace * 2);
    // Fix the right side space.
    e.Graphics.DrawString(this.Text, this.Font, new SolidBrush(this.ForeColor), textRect);

}

//=======================================================
//Service provided by Telerik (www.telerik.com)
//=======================================================

Upvotes: 0

Views: 551

Answers (1)

There are actually several things wrong with your code and approach:

' The rectangle where to draw the border.
Dim borderRect As Rectangle = e.ClipRectangle
' and:
' The rectangle where to draw the header text.
Dim textRect As Rectangle = e.ClipRectangle

If you select a child control and move it just a bit, you will get the problem your image shows. The reason is that Windows does not ask the control to repaint its entire self for such a small change. Instead, it invalidates a small region around the child control(s) and passes it as the ClipRectangle. In some cases, it isnt actually correct.

So, locating the GroupBox border Rect based on e.ClipRectangle will draw parts of the border and Caption at the top of the area around the control just moved. Use Bounds adjusted for the various things


That should get rid of the problem mentioned, but it will uncover several others. For instance, if the theme or style calls for a 3D type border, 2 border lines are drawn and your calculations rely more on dead reckoning.

Next, I am not sure Control.DrawBorder is entirely appropriate if you are trying to override default behavior. This is going to respect certain things determined by themes/styles etc which are what you are trying to override.

I can also get the Caption to display better using TextRenderer.

To do what you want, you are probably going to have to take over everything which the default GroupBoxRenderer does. If you change your bordercolor to something like White or Fuschia, you'll see that the normal rendering is still taking place, your code is just trying to draw over the top of it.

Even if you got this working, some other combination of FlatStyle, Color, Theme or Style will fail because your code is taking none of that into consideration.

Here is what I used, but it just leads to a game of Whack-A-Mole with different issues arising:

Dim textRect As New Rectangle With {.X = 0,
                                    .Y = 2,
                                   .Height = textSize.Height,
                                    .Width = (textSize.Width - blankWidthSpace)
                                   }
Dim borderRect As New Rectangle With {.X = 1, .Y = (textSize.Height \ 2) + 1,
                                 .Width = Bounds.Width - 2,
                                 .Height = Bounds.Height - (textSize.Height \ 2) - 2}

TextRenderer.DrawText(e.Graphics, " " & MyBase.Text & " ", MyBase.Font,
                          textRect, MyBase.ForeColor, MyBase.BackColor)

This seems related to your other question about "ugly white borders". Trying to fix or override a Theme by subclassing a (bunch of) control(s) is not the best way to go about it.

I would look into providing an alternate Aero-like theme which fits your taste. Besides, who are you to say that all users will agree it is "ugly" (I think it makes the form look busy but not exactly "ugly").

Upvotes: 1

Related Questions