Jose C
Jose C

Reputation: 317

"Glitch" on Winforms Panel when overriding OnPaint

I'm trying to put a customized title on a Panel, it is displaying but when I scroll using scroll bar the title is painted but it's not always erased, so it displays a lot of titles as I'm scrolling up again.

Any ideas on what I'm doing wrong?

The code is something like:

Public Class MyPanel
    Inherits Windows.Forms.Panel
Protected Overrides Sub OnPaint(ByVal myPEV As PaintEventArgs)
        Dim myRectF As RectangleF = New RectangleF(0, 0, Me.Width, Me.Height)
        Dim mySF As New StringFormat
        MyBase.OnPaint(myPEV)
        Dim myhDC As IntPtr = GetWindowDC(Me.Handle) 'from user32.dll
        Dim myGraphs As Graphics = Graphics.FromHdc(myhDC)
        If Me.Text IsNot Nothing Then
            mySF.Alignment = StringAlignment.Center
            mySF.LineAlignment = StringAlignment.Near
            myGraphs.DrawString(Me.Text, Me.Font, New SolidBrush(Me.ForeColor), myRectF, mySF)
            myGraphs.Dispose()
            ReleaseDC(Handle, myhDC) 'from user32.dll
        End If
    End Sub
End Class

Upvotes: 0

Views: 1788

Answers (4)

J...
J...

Reputation: 31403

The problem is caused by the fact that your text is being drawn in isolation - it is not associated with a component and so the Panel can't do anything intelligent with the region occupied by the text. A component does not always redraw everything OnPaint, it will often translate what it can and will only redraw what is necessary. One hack would be to force a repaint of the entire control when scrolling :

Protected Overrides Sub OnScroll(se As System.Windows.Forms.ScrollEventArgs)
    Me.Invalidate(true)  'set to "true" to also repaint child controls
    MyBase.OnScroll(se)
End Sub

More elegant, perhaps, would be to leverage the already built-in layout and drawing power of the existing controls. There's no need to perform surgery with an axe when you have a scalpel in your toolbox :

Public Class MyPanel
    Inherits Windows.Forms.Panel
    Private headLbl As New Label

    <Browsable(True)> _
    <Description("The text to appear in the panel header.")> _
    Public Overrides Property Text As String
        Get
            Return headLbl.Text
        End Get
        Set(value As String)
            headLbl.Text = value
        End Set
    End Property

    Protected Overrides Sub OnSizeChanged(e As System.EventArgs)
        MyBase.OnSizeChanged(e)
        headLbl.Width = Me.Width
    End Sub

    Public Sub New()
        headLbl.TextAlign = ContentAlignment.MiddleCenter
        headLbl.AutoSize = False
        headLbl.Location = New Point(0, 0)
        headLbl.Width = Me.Width
        Me.Controls.Add(headLbl)
    End Sub
End Class

Edit :

If you want the label to stay top center (rather than scrolling with the contents) you can add :

Protected Overrides Sub OnScroll(se As System.Windows.Forms.ScrollEventArgs)
    MyBase.OnScroll(se)
    headLbl.Location = New Point(0, 0)
End Sub

Upvotes: 1

Hans Passant
Hans Passant

Reputation: 941545

I'll do the fish and explain why you are having this problem. You are doing battle with a system option that's turned on for all modern Windows versions, named "Show window contents while dragging". That enables an optimization that minimizes the amount of painting that needs to be done when the user scrolls a window.

The basic way it works is that Windows itself scrolls the majority of the window content by the scroll amount by copying the pixels in the window. A fast bitblt that avoids having to repaint those pixels. The underlying winapi function is ScrollWindowEx(). And then invalidates the part of the window for which it doesn't have pixels to copy, just the bottom of the window if you scroll down, the top sliver if you scroll up. Accordingly, the app has to do much less work in its paint event handler, it only needs to draw the part of the window that was revealed by the scroll.

This plays havoc on the text that you draw in your OnPaint() method. It gets scrolled as well by the bitblt. But you don't scroll it yourself, you draw it in the same position regardless of the scrollbar position. The net effect is that the text "smears", the pixels of the text got moved but they are not being redrawn.

What you should do is scroll the text as well. Easy to do, your OnPaint() method override should look like this:

Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
    MyBase.OnPaint(e)
    If Me.Text IsNot Nothing Then
        e.Graphics.TranslateTransform(Me.AutoScrollPosition.X, Me.AutoScrollPosition.Y)
        // etc, be sure to use e.Graphics
        //...
    End If
End Sub

The Graphics.TranslateTransform() call ensures that the text is offset properly and scrolls along with the rest of the pixels in the window. And the optimized bitblt will no longer cause the smearing.

This is what you should do but probably not what you want to do. Lars showed you a workaround, you have to intentionally defeat the optimization and force the entire window to be repainted, not just the optimized sliver that was revealed by the scroll.

That works, but it certainly does produce a visible artifact. For a brief moment, you'll see the scrolled text pixels, just before they get overpainted by OnPaintBackground(). The effect is, erm, interesting, you see it doing the "pogo", jumping up and down as you scroll.

A real fix requires disabling the bitblt optimization. User Tom showed how to do that in this answer. Works well on a Panel control as well, I'll reproduce his code in VB.NET syntax. Paste this code into your class:

Protected Overrides Sub WndProc(ByRef m As Message)
    '' Trap scroll notifications, send WM_SETREDRAW
    If m.Msg = 276 Or m.Msg = 277 Then
        SendMessageW(Me.Handle, 11, IntPtr.Zero, IntPtr.Zero)
        MyBase.WndProc(m)
        SendMessageW(Me.Handle, 11, New IntPtr(1), IntPtr.Zero)
        Me.Invalidate()
        Return
    End If
    MyBase.WndProc(m)
End Sub

Private Declare Function SendMessageW Lib "User32.dll" (ByVal hWnd As IntPtr, ByVal Msg As Integer, ByVal WParam As IntPtr, ByVal LParam As IntPtr) As IntPtr

Upvotes: 2

LarsTech
LarsTech

Reputation: 81620

I would try it this way:

Public Class MyPanel
  Inherits Windows.Forms.Panel

  Public Sub New()
    Me.Text = "Test Header"
    Me.DoubleBuffered = True
    Me.ResizeRedraw = True
  End Sub

  Protected Overrides Sub OnPaint(ByVal myPEV As PaintEventArgs)
    myPEV.Graphics.Clear(Me.BackColor)
    MyBase.OnPaint(myPEV)
    TextRenderer.DrawText(myPEV.Graphics, Me.Text, Me.Font, Me.ClientRectangle, _
                          Me.ForeColor, Color.Empty, _
                          TextFormatFlags.HorizontalCenter)
  End Sub

  Protected Overrides Sub OnScroll(se As ScrollEventArgs)
    Me.Invalidate()
    MyBase.OnScroll(se)
  End Sub    
End Class

It will still experience some tearing, which would be solved by actually not doing this and putting a Label centered above the panel.

Upvotes: 1

simon at rcl
simon at rcl

Reputation: 7344

OnPaint will be called when the panel scrolls, so you're painting it multiple times. It's a long time since I've done any of this, but you need to check out the Client area and make sure your Rect is not the visible rect of the panel, but but an absolute position on the panel.

Upvotes: 1

Related Questions