如何正确清除或更新屏幕上绘制的矩形

时间:2014-05-15 09:52:48

标签: .net vb.net graphics gdi+ rectangles

我正在使用半透明表单捕获鼠标事件,例如 LeftButtonDown LeftButtonUp MouseMove ,以便能够选择屏幕上的一个区域在该区域上绘制一个矩形,问题是每次移动鼠标时都会绘制一个新的矩形,产生令人讨厌的结果:

enter image description here

当我将鼠标移动到新的鼠标位置时,我只想更新绘制的矩形以期望得到这样的结果:

enter image description here

我试图在没有运气的情况下处理,清除和重新实例Graphics对象,我也见过this S.O.谈论这个的问题。

这是我正在使用的代码的相关部分:

''' <summary>
''' The Graphics object to draw on the screen.
''' </summary>
Dim ScreenGraphic As Graphics = Graphics.FromHwnd(IntPtr.Zero)

Private Sub MouseEvents_MouseMove(ByVal MouseLocation As Point) Handles MouseEvents.MouseMove

    ' If left mouse button is hold then set the rectangle area...
    If IsMouseLeftDown Then

        ' ... blah blah blah
        ' ... more code here

        ' Draw the rectangle area.
        Me.DrawRectangle()

    End If

''' <summary>
''' Draws the rectangle on the selected area.
''' </summary>
Private Sub DrawRectangle()

    ' Call the "EraseRectanglehere" method here before re-drawing ?
    ' Me.EraseRectangle

    Using pen As New Pen(Me.BorderColor, Me.BorderSize)
        ScreenGraphic.DrawRectangle(pen, SelectionRectangle)
    End Using

End Sub

''' <summary>
''' Erases the last drawn rectangle.
''' </summary>
Private Sub EraseRectangle()

End Sub

如果有人需要更好地检查它,这里是完整的代码:

  

注意:我已经在上一个问题编辑中更新了我现在使用的代码。

Imports System.Runtime.InteropServices

Public Class RangeSelector : Inherits Form

#Region " Properties "

    ''' <summary>
    ''' Gets or sets the border size of the range selector.
    ''' </summary>
    ''' <value>The size of the border.</value>
    Public Property BorderSize As Integer = 2

    ''' <summary>
    ''' Gets or sets the border color of the range selector.
    ''' </summary>
    ''' <value>The color of the border.</value>
    Public Property BorderColor As Color = Color.Red

#End Region

#Region " Objects "

    ''' <summary>
    ''' Indicates the initial location when the mouse left button is clicked.
    ''' </summary>
    Private InitialLocation As Point = Point.Empty

    ''' <summary>
    ''' Indicates the rectangle that contains the selected area.
    ''' </summary>
    Private SelectionRectangle As Rectangle = Rectangle.Empty

    ''' <summary>
    ''' The Graphics object to draw on the screen.
    ''' </summary>
    Private ScreenGraphic As Graphics = Graphics.FromHwnd(IntPtr.Zero)

#End Region

#Region " Constructors "

    ''' <summary>
    ''' Initializes a new instance of the <see cref="RangeSelector"/> class.
    ''' </summary>
    Public Sub New()

        InitializeComponent()

    End Sub

    ''' <summary>
    ''' Initializes a new instance of the <see cref="RangeSelector" /> class.
    ''' </summary>
    ''' <param name="BorderSize">Indicates the border size of the range selector.</param>
    ''' <param name="BorderColor">Indicates the border color of the range selector.</param>
    Public Sub New(ByVal BorderSize As Integer, ByVal BorderColor As Color)

        Me.BorderSize = BorderSize
        Me.BorderColor = BorderColor

        InitializeComponent()

    End Sub

#End Region

#Region " Event Handlers "

    Protected Overrides Sub OnMouseDown(e As MouseEventArgs)

        ' MyBase.OnMouseDown(e)
        InitialLocation = e.Location
        SelectionRectangle = New Rectangle(InitialLocation.X, InitialLocation.Y, 0, 0)

    End Sub

    Protected Overrides Sub OnMouseUp(e As MouseEventArgs)

        ' Make the Form transparent to take the region screenshot.
        Me.Opacity = 0.0R

        ' ToDo:
        ' take the screenshot.
        ' Return the selected rectangle area and save it.

        Me.Close()

    End Sub

    Protected Overrides Sub OnMouseMove(e As MouseEventArgs)

        ' If left mouse button is hold then set the rectangle area...
        If e.Button = MouseButtons.Left Then

            If (e.Location.X < Me.InitialLocation.X) _
            AndAlso (e.Location.Y < Me.InitialLocation.Y) Then ' Top-Left

                Me.SelectionRectangle = New Rectangle(e.Location.X,
                                                      e.Location.Y,
                                                      Me.InitialLocation.X - e.Location.X,
                                                      Me.InitialLocation.Y - e.Location.Y)

            ElseIf (e.Location.X > Me.InitialLocation.X) _
            AndAlso (e.Location.Y < Me.InitialLocation.Y) Then ' Top-Right

                Me.SelectionRectangle = New Rectangle(Me.InitialLocation.X,
                                                      e.Location.Y,
                                                      e.Location.X - Me.InitialLocation.X,
                                                      Me.InitialLocation.Y - e.Location.Y)

            ElseIf (e.Location.X < Me.InitialLocation.X) _
            AndAlso (e.Location.Y > Me.InitialLocation.Y) Then ' Bottom-Left

                Me.SelectionRectangle = New Rectangle(e.Location.X,
                                                      Me.InitialLocation.Y,
                                                      Me.InitialLocation.X - e.Location.X,
                                                      e.Location.Y - Me.InitialLocation.Y)

            ElseIf (e.Location.X > Me.InitialLocation.X) _
            AndAlso (e.Location.Y > Me.InitialLocation.Y) Then ' Bottom-Right

                Me.SelectionRectangle = New Rectangle(Me.InitialLocation.X,
                                                      Me.InitialLocation.Y,
                                                      e.Location.X - Me.InitialLocation.X,
                                                      e.Location.Y - Me.InitialLocation.Y)
            End If

            ' Draw the rectangle area.
            Me.DrawRectangle()

        End If

    End Sub

#End Region

#Region " Private Methods "

    Private Sub InitializeComponent()

        Me.SuspendLayout()
        Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None
        Me.BackColor = System.Drawing.Color.Black
        Me.BackgroundImageLayout = System.Windows.Forms.ImageLayout.None
        Me.CausesValidation = False
        Me.ClientSize = New System.Drawing.Size(100, 100)
        Me.ControlBox = False
        Me.Cursor = System.Windows.Forms.Cursors.Cross
        Me.DoubleBuffered = True
        Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None
        Me.MaximizeBox = False
        Me.MinimizeBox = False
        Me.Name = "RangeSelector"
        Me.Opacity = 0.01R
        Me.ShowIcon = False
        Me.ShowInTaskbar = False
        Me.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide
        Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen
        Me.TopMost = True
        Me.WindowState = System.Windows.Forms.FormWindowState.Maximized
        Me.ResumeLayout(False)

    End Sub

    ''' <summary>
    ''' Draws the rectangle on the selected area.
    ''' </summary>
    Private Sub DrawRectangle()

        ' Just a weird trick to refresh the painting.
        ' Me.Opacity = 0.0R
        ' Me.Opacity = 0.01R

        ' Using g As Graphics = Graphics.FromHwnd(IntPtr.Zero)

        Using pen As New Pen(Me.BorderColor, Me.BorderSize)
            ScreenGraphic.DrawRectangle(pen, Me.SelectionRectangle)
        End Using

        ' End Using

    End Sub

#End Region

End Class
  

更新1

我已经翻译了所有代码以将其用作Form对话框,以便在选择区域时具有更大的灵活性,我已经替换了上面的整个代码来更新我的问题,代码不会改变太多只是使用LL Hook捕获鼠标事件我正在处理半透明最大化Form的鼠标事件,我仍然在桌面屏幕图形上绘制矩形(而不是{{1表单事件)代码的一部分与您在上面的代码中看到的相同:

OnPaint

...'因为我已经说过Form是半透明的,所以如果我在Form中绘制一个矩形,它也将是半透明的(或者至少我不知道如何避免这种情况)

然后我发现了一个奇怪的技巧,通过在新坐标中绘制矩形之前更改Form的不透明度来解决矩形问题:

Private ScreenGraphic As Graphics = Graphics.FromHwnd(IntPtr.Zero)

问题? ...不完美,它会产生一种非常恼人的效果,因为我在绘制矩形时会出现大量闪烁(是的,我有Form doubleBuffered,我也使用 Me.Opacity = 0.0R Me.Opacity = 0.01R Using pen As New Pen(Me.BorderColor, Me.BorderSize) ScreenGraphic.DrawRectangle(pen, Me.SelectionRectangle) End Using 技巧来避免闪烁,但没有。)

  

更新2

我尝试使用@ Plutonix 在他的评论中指出InvalidateRect函数,并使用此API声明:

CreateParams

我已尝试将其与<DllImport("user32.dll")> Private Shared Function InvalidateRect( ByVal hWnd As Integer, ByRef lpRect As Rectangle, ByVal bErase As Boolean) As Boolean End Function / False标志一起使用。

问题?问题与我在第一次更新中指出的问题相同:

'不完美,它会产生非常恼人的效果'因为我在绘制矩形时会出现很多闪烁(是的,我有Form doubleBuffered,而且我正在使用True技巧尝试避免闪烁,但没有。)

  

更新3

我正在尝试使用RedrawWindow函数解决此问题,正如我在this SO answer中看到的那样,它可用于执行与InvalidateRect函数相同的功能,但也可以更灵活,也许没有使用InvalidateRect函数产生的恼人效果,我只需要尝试一下。

  

RedrawWindow函数更新指定的矩形或区域   窗口的客户区。

这是API声明:

CreateParams

我尝试使用带有这些参数的函数:

<DllImport("user32.dll")>
Private Shared Function RedrawWindow(
        ByVal hWnd As IntPtr,
        <[In]> ByRef lprcUpdate As Rectangle,
        ByVal hrgnUpdate As IntPtr,
        ByVal flags As RedrawWindowFlags) As Boolean
End Function

<Flags()>
Private Enum RedrawWindowFlags As UInteger
    ''' <summary>
    ''' Invalidates the rectangle or region that you specify in lprcUpdate or hrgnUpdate.
    ''' You can set only one of these parameters to a non-NULL value. If both are NULL, RDW_INVALIDATE invalidates the entire window.
    ''' </summary>
    Invalidate = &H1

    ''' <summary>Causes the OS to post a WM_PAINT message to the window regardless of whether a portion of the window is invalid.</summary>
    InternalPaint = &H2

    ''' <summary>
    ''' Causes the window to receive a WM_ERASEBKGND message when the window is repainted.
    ''' Specify this value in combination with the RDW_INVALIDATE value; otherwise, RDW_ERASE has no effect.
    ''' </summary>
    [Erase] = &H4

    ''' <summary>
    ''' Validates the rectangle or region that you specify in lprcUpdate or hrgnUpdate.
    ''' You can set only one of these parameters to a non-NULL value. If both are NULL, RDW_VALIDATE validates the entire window.
    ''' This value does not affect internal WM_PAINT messages.
    ''' </summary>
    Validate = &H8

    NoInternalPaint = &H10

    ''' <summary>Suppresses any pending WM_ERASEBKGND messages.</summary>
    NoErase = &H20

    ''' <summary>Excludes child windows, if any, from the repainting operation.</summary>
    NoChildren = &H40

    ''' <summary>Includes child windows, if any, in the repainting operation.</summary>
    AllChildren = &H80

    ''' <summary>Causes the affected windows, which you specify by setting the RDW_ALLCHILDREN and RDW_NOCHILDREN values, to receive WM_ERASEBKGND and WM_PAINT messages before the RedrawWindow returns, if necessary.</summary>
    UpdateNow = &H100

    ''' <summary>
    ''' Causes the affected windows, which you specify by setting the RDW_ALLCHILDREN and RDW_NOCHILDREN values, to receive WM_ERASEBKGND messages before RedrawWindow returns, if necessary.
    ''' The affected windows receive WM_PAINT messages at the ordinary time.
    ''' </summary>
    EraseNow = &H200

    Frame = &H400

    NoFrame = &H800
End Enum

...我认为,正如MSDN文档所描述的那样,如果第一个参数为NULL则表示桌面屏幕,第二个参数表示要更新的矩形,如果我指定了第三个参数,则需要为null第二个参数中的矩形,最后一个参数表示一个标志,指示要执行的操作(在这种情况下使矩形无效为@ Plutonix 表示?)

我在绘制矩形后尝试使用该isntruction,在绘制之前,我的意思是在RedrawWindow(IntPtr.Zero, Me.SelectionRectangle, IntPtr.Zero, RedrawWindowFlags.Invalidate) 事件中,而在我的代码中的OnMouseMove方法内部,但我不是看到屏幕上的任何差异,我仍然有同样的问题,我在绘制矩形时在上面的图像中显示我的意思是当我移动鼠标时绘制了多个矩形,任何矩形被此功能删除,也许我正在使用错误的参数?。

3 个答案:

答案 0 :(得分:4)

基思的回答大多是正确的,但缺少一个关键点:

Protected Overrides Sub OnPaint(ByVal e as PaintEventArgs)
    MyBase.OnPaint(e)
    If bClickHolding Then e.Graphics.DrawRectangle(pen:=Pen, rect:=Rect)
End Sub

你应该在paint事件中进行绘制,而不是事件处理程序。

这就是为什么你会出现闪烁的原因,因为在帧之间绘制了表单绘制事件,导致缓冲区被清除。

另外,还有其他一些黑客&#39;

Protected Overrides Sub OnPaintBackground(ByVal e as PaintEventArgs)
    Return ' will skip painting the background
    MyBase.OnPaintBackground(e)
End Sub

SetStyle(ControlStyles.ResizeRedraw, True)
SetStyle(ControlStyles.DoubleBuffer, True)
SetStyle(ControlStyles.AllPaintingInWmPaint, True)

你应该把它画在一个小组中。哦,不要把程序逻辑放在OnPaint事件中,把它放在处理程序中,或者放在一个单独的线程中。

如果你想从另一个控制/班级中抽取它,请不要。相反,在主控件的OnPaint事件中绘制它,并简单地引用另一个控件中的object / boolean / size,location。 (即:如果myBoundingbox.bClickHolding那么......)

解释此问题的一些链接(来自MSDN):

  

使用a创建新的自定义控件或继承的控件时   不同的视觉外观,你必须提供代码来渲染   通过覆盖OnPaint方法进行控制。

MSDN - Control.Paint Event

MSDN - Control.OnPaint Method

MSDN - Custom Control Painting and Rendering

嗯,在阅读关于透明度的部分后,我打算建议:(只需设置.TransparencyKey = Color.Black)但是,绕过鼠标事件,可能需要一些WndProc来修复它:MSDN - Form.TransparencyKey Property - 嗯,问题就是窗户失去了焦点。

可能是这样的:MSDN - NativeWindow Class - 但是你可能需要使用鼠标钩子,因为你不再使用透明度接收窗口的消息。

此外,这是一种“黑客”,它在光标后面的背景中绘制一个矩形。问题是,效果滞后于光标,因此如果你快速移动鼠标它就不会起作用。或者将它放在计时器上会更好。我暂时把它留在这里。您可以使用OnMouseMove覆盖或WndProc方法,但我看不到性能差异。 (编辑:和nope,计时器不会减少延迟)。

Private Shared mouseNotify() As Int32 = {&H200, &H201, &H204, &H207} ' WM_MOUSEMOVE, WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN

Friend Shared Function isOverControl(ByRef theControl As Control) As Boolean
    Return theControl.ClientRectangle.Contains(theControl.PointToClient(Cursor.Position))
End Function

    Protected Overrides Sub OnMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs)
        'Invalidate()
        MyBase.OnMouseMove(e)
    End Sub

    Protected Overrides Sub OnPaintBackground(ByVal e As System.Windows.Forms.PaintEventArgs)
        MyBase.OnPaintBackground(e)
        Dim x As Integer = PointToClient(Cursor.Position).X - 5
        Dim y As Integer = PointToClient(Cursor.Position).Y - 5
        e.Graphics.DrawRectangle(New Pen(Brushes.Aqua, 1), 0, 0, ClientRectangle.Width - 1, ClientRectangle.Height - 1)
        e.Graphics.FillRectangle(Brushes.Aqua, x, y, 10, 10)
    End Sub

    Protected Overrides Sub WndProc(ByRef m As Message)
        If mouseNotify.Contains(CInt(m.Msg)) Then
            If isOverControl(Me) Then Invalidate()
        End If
        MyBase.WndProc(m)
    End Sub

答案 1 :(得分:3)

希望这可以帮到你。


更新1:重新编写代码。处理向后选择矩形,减少检查等。清理它。

更新2:更新以反映porkchop的更正。


Public Class SelectionRectTesting

    Private pCurrent As Point
    Private pStart As Point
    Private pStop As Point

    Private Rect As Rectangle
    Private Graphics As Graphics
    Private Pen As New Pen(Color.Red, 1)

    Private bClickHolding = False

    Private Sub SelectionRectTestingLoad(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        SetStyle(ControlStyles.ResizeRedraw, True)
        SetStyle(ControlStyles.DoubleBuffer, True)
        SetStyle(ControlStyles.AllPaintingInWmPaint, True)
    End Sub

    Private Sub HandleMouseDown(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseDown
        bClickHolding = True
        pStart.X = e.X
        pStart.Y = e.Y
    End Sub

    Private Sub HandleMouseMove(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseMove
        If bClickHolding = True Then
            pCurrent.X = e.X
            pCurrent.Y = e.Y

            If pCurrent.X < pStart.X Then
                Rect.X = pCurrent.X
                Rect.Width = pStart.X - pCurrent.X
            Else
                Rect.X = pStart.X
                Rect.Width = pCurrent.X - pStart.X
            End If

            If pCurrent.Y < pStart.Y Then
                Rect.Y = pCurrent.Y
                Rect.Height = pStart.Y - pCurrent.Y
            Else
                Rect.Y = pStart.Y
                Rect.Height = pCurrent.Y - pStart.Y
            End If

            Invalidate()
        End If
    End Sub

    Private Sub HandleMouseUp(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseUp
        bClickHolding = False
        Invalidate()
    End Sub

    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
        MyBase.OnPaint(e)
        If bClickHolding Then
            e.Graphics.DrawRectangle(pen:=Pen, rect:=Rect)
        End If
    End Sub

    Protected Overrides Sub OnPaintBackground(ByVal e As PaintEventArgs)
        Return ' will skip painting the background
        MyBase.OnPaintBackground(e)
    End Sub

End Class

答案 2 :(得分:3)

解决方案更简单,并且不需要Windows API。只需创建一个透明的,并在其上绘制红色矩形。下面的代码执行此操作,您只需要以半透明的形式替换。闪烁发生是因为我们清理图形然后绘制,避免它的最简单方法是立即进行绘制,因此如果我们在位图上绘制矩形然后绘制位图,则操作只需一步完成,闪烁就不会出现。发生了。

绘图将在绘图表单的OnPaintBackground上完成,因此需要一个绘图表单。这是捕获事件的主要类:

Public Class YourFormClass

    Dim Start As Point
    Dim DrawSize As Size
    Public DrawRect As Rectangle
    Public Drawing As Boolean = False
    Dim Info As Label
    Dim DrawForm As Form

    Private Sub YourFormClass_Load(sender As Object, e As EventArgs) Handles Me.Load
        ' Add any initialization after the InitializeComponent() call.
        ControlBox = False
        WindowState = FormWindowState.Maximized
        FormBorderStyle = Windows.Forms.FormBorderStyle.None
        BackColor = Color.Gray
        Opacity = 0.2

        DrawForm = New DrawingFormClass(Me)
        With DrawForm
            .BackColor = Color.Tomato
            .TopLevel = True
            .TransparencyKey = Color.Tomato
            .TopMost = True
            .FormBorderStyle = Windows.Forms.FormBorderStyle.None
            .ControlBox = False
            .WindowState = FormWindowState.Maximized
        End With

        Info = New Label
        With Info
            .Top = 16
            .Left = 16
            .ForeColor = Color.White
            .AutoSize = True
            DrawForm.Controls.Add(Info)
        End With

        Me.AddOwnedForm(DrawForm)
        DrawForm.Show()
    End Sub

    Private Sub Form1_MouseDown(sender As Object, e As MouseEventArgs) Handles Me.MouseDown
        Drawing = True
        Start = e.Location
    End Sub

    Private Sub Form1_MouseMove(sender As Object, e As MouseEventArgs) Handles Me.MouseMove
        If Drawing Then
            DrawSize = New Size(e.X - Start.X, e.Y - Start.Y)
            DrawRect = New Rectangle(Start, DrawSize)

            If DrawRect.Height < 0 Then
                DrawRect.Height = Math.Abs(DrawRect.Height)
                DrawRect.Y -= DrawRect.Height
            End If

            If DrawRect.Width < 0 Then
                DrawRect.Width = Math.Abs(DrawRect.Width)
                DrawRect.X -= DrawRect.Width
            End If

            Info.Text = DrawRect.ToString
            DrawForm.Invalidate()
        End If
    End Sub

    Private Sub Form1_MouseUp(sender As Object, e As MouseEventArgs) Handles Me.MouseUp
        Drawing = False
    End Sub

End Class

由于绘图将在OnPaintBackground中完成,因此需要第二个类:

Public Class DrawingFormClass

    Private DrawParent As YourFormClass

    Public Sub New(Parent As YourFormClass)

        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.
        Me.DrawParent = YourFormClass
    End Sub

    Protected Overrides Sub OnPaintBackground(e As PaintEventArgs)
        Dim Bg As Bitmap
        Dim Canvas As Graphics


        If DrawParent.Drawing Then
            Bg = New Bitmap(Width, Height)
            Canvas = Graphics.FromImage(Bg)
            Canvas.Clear(Color.Tomato)
            Canvas.DrawRectangle(Pens.Red, DrawParent.DrawRect)
            Canvas.Dispose()
            e.Graphics.DrawImage(Bg, 0, 0, Width, Height)

            Bg.Dispose()
        Else
            MyBase.OnPaintBackground(e)
        End If

    End Sub

End Class

只需创建两个表单并粘贴...它将创建绘图表单并绘制红色矩形,创建一个位图缓冲区,因此在绘制时只完成一个操作。这种工作非常精细,没有闪烁。希望它有所帮助!