【问题标题】:"Glitch" on Winforms Panel when overriding OnPaint覆盖 OnPaint 时 Winforms 面板上的“故障”
【发布时间】:2014-01-24 11:06:17
【问题描述】:

我正在尝试在面板上放置自定义标题,它正在显示,但是当我使用滚动条滚动时,标题被绘制但并不总是被删除,因此当我再次向上滚动时它会显示很多标题.

关于我做错了什么有什么想法吗?

代码是这样的:

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

【问题讨论】:

  • 我无法重现这个。您是否可能在程序的其他地方弄乱了 Windows 消息?什么操作系统?哪个版本的 .NET/VisualStudio?
  • W7x64 中的 VS2012 和 2005 都编译为 x86。面板有超出面板边界的按钮,并且 Autoscroll 设置为 True,因此滚动条显示
  • 知道了 - 我滚动的是面板的容器,而不是面板本身。我现在看到了你的问题。
  • 顺便说一句,您的 myPEV 参数具有 Graphics 属性,因此您不必创建 Graphics 对象

标签: .net vb.net winforms controls


【解决方案1】:

我会做鱼并解释你为什么会遇到这个问题。您正在使用针对所有现代 Windows 版本都已打开的系统选项进行战斗,该选项名为“拖动时显示窗口内容”。这样可以进行优化,最大限度地减少用户滚动窗口时需要完成的绘制量。

它的基本工作方式是,Windows 本身通过复制窗口中的像素来按滚动量滚动大部分窗口内容。避免重新绘制这些像素的快速 bitblt。底层的winapi函数是ScrollWindowEx()。然后使窗口中没有像素要复制的部分无效,如果向下滚动,则仅在窗口底部,如果向上滚动,则使顶部条无效。因此,应用程序在其绘制事件处理程序中要做的工作要少得多,它只需要绘制由滚动显示的窗口部分。

这会对您在 OnPaint() 方法中绘制的文本造成严重破坏。它也被 bitblt 滚动。但是您不会自己滚动它,无论滚动条的位置如何,您都将它绘制在相同的位置。最终效果是文本“涂抹”,文本的像素被移动但它们没有被重绘。

应该做的就是滚动文本。很容易做到,你的 OnPaint() 方法覆盖应该是这样的:

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

Graphics.TranslateTransform() 调用可确保文本正确偏移并与窗口中的其余像素一起滚动。并且优化后的bitblt不会再造成拖影。

这是你应该做的,但可能不是你想要做的。 Lars 向您展示了一种解决方法,您必须故意破坏优化并强制重新绘制整个窗口,而不仅仅是滚动显示的优化条。

这行得通,但它确实会产生可见的工件。片刻之后,您将看到滚动的文本像素,就在它们被 OnPaintBackground() 覆盖之前。效果是,嗯,很有趣,你会看到它在做“pogo”,在你滚动时上下跳跃。

真正的修复需要禁用 bitblt 优化。 User Tomthis answer 中展示了如何做到这一点。在 Panel 控件上也能很好地工作,我将用 VB.NET 语法重现他的代码。将此代码粘贴到您的课程中:

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

【讨论】:

    【解决方案2】:

    问题是由于您的文本是单独绘制的 - 它与组件无关,因此面板无法对文本占用的区域执行任何智能操作。组件并不总是重绘所有内容OnPaint,它通常会翻译它可以翻译的内容,并且只会重绘必要的内容。一种技巧是在滚动时强制重新绘制整个控件:

    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
    

    也许更优雅的是利用现有控件已经内置的布局和绘图功能。当您的工具箱中有手术刀时,无需使用斧头进行手术:

    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
    

    编辑:

    如果您希望标签保持顶部居中(而不是随着内容滚动),您可以添加:

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

    【讨论】:

    • 我尝试了第一个,它运行良好......直到另一个控件在文本顶部出现,然后人工制品又回来了。我稍后会尝试将标签添加到控件中,看看它是如何工作的。谢谢!
    • @JoséL.Cardós - 您也可以使用Invalidate(true) 来强制子控件也重新绘制。我已经更新了我的答案。
    【解决方案3】:

    OnPaint 将在面板滚动时被调用,因此您需要多次绘制它。我已经很久没有这样做了,但是您需要检查客户区域并确保您的 Rect 不是面板的可见矩形,而是面板上的绝对位置。

    【讨论】:

    • 我不知道如何检查,在哪里看有什么线索吗?谢谢!
    • 看看“客户区滚动绘画”之类的东西。恐怕我是在 10 多年前才开始接触这个的,而我已经忘记了。对不起!
    【解决方案4】:

    我会这样尝试:

    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
    

    它仍然会出现一些撕裂,这可以通过实际上不这样做并在面板上方居中放置一个标签来解决。

    【讨论】:

    • 相隔 11 秒...很好。
    • @J... 看到你要发帖了,赶紧上去。 :-)
    • wtf - 这是 10k 的超级大国还是什么?看到拐角处?!
    猜你喜欢
    • 2010-10-18
    • 1970-01-01
    • 1970-01-01
    • 2021-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多