我正在使用半透明表单捕获鼠标事件,例如 LeftButtonDown , LeftButtonUp 和 MouseMove ,以便能够选择屏幕上的一个区域在该区域上绘制一个矩形,问题是每次移动鼠标时都会绘制一个新的矩形,产生令人讨厌的结果:
当我将鼠标移动到新的鼠标位置时,我只想更新绘制的矩形以期望得到这样的结果:
我试图在没有运气的情况下处理,清除和重新实例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
方法内部,但我不是看到屏幕上的任何差异,我仍然有同样的问题,我在绘制矩形时在上面的图像中显示我的意思是当我移动鼠标时绘制了多个矩形,任何矩形被此功能删除,也许我正在使用错误的参数?。
答案 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 - 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
只需创建两个表单并粘贴...它将创建绘图表单并绘制红色矩形,创建一个位图缓冲区,因此在绘制时只完成一个操作。这种工作非常精细,没有闪烁。希望它有所帮助!