I'm using a semi-transparent Form to capture the mouse events such as LeftButtonDown, LeftButtonUp and MouseMove to be able to select an area on the screen to draw a rectangle on that area, the problem is that a new rectangle is drawn every time that I move the mouse producing an annoying result like this:
I just would like to update the drawn rectangle when I move the mouse to the new mouse location to expect a result like this else:
I've tried to dispose, clear, and re-instance the Graphics object without luck, also I've seen this S.O. question that talks about this.
This is the relevant part of the code that I'm using:
''' <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
And here is the full code if someone need to inspectionate it better:
NOTE: I've updated the code that I'm using now in my last question edit.
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
UPDATE 1
I've translated all the code to use it as a Form dialog to have more flexibility when selecting a region, I've replaced the entire code above to update my question, the code does not change too much just instead using a LL Hook to capture the mouse events I'm handling the mouse events of a semi-transparent maximized Form, I still drawing the rectangle on the Desktop Screen Graphics (not on the OnPaint Form event) that part of the code is the same as you can see in the code above:
Private ScreenGraphic As Graphics = Graphics.FromHwnd(IntPtr.Zero)
...'cause as I've said the Form is semi-transparent so if I draw a rectangle in the Form it will be semi-transparent too (or at least I don't know a way to avoid that).
Then I've discover a weird trick to solve the rectangle issue by changing the opacity of the Form before drawing the rectangle in the new coordinates:
Me.Opacity = 0.0R
Me.Opacity = 0.01R
Using pen As New Pen(Me.BorderColor, Me.BorderSize)
ScreenGraphic.DrawRectangle(pen, Me.SelectionRectangle)
End Using
The problem? ...Is not perfect, it generates a very annoying effect 'cause I get a lot of flickering when drawing the rectangle (and yes I have the Form doubleBuffered and also I'm using the CreateParams trick to try to avoid flickering, but nothing).
UPDATE 2
I've tried to use what @Plutonix pointed in his comment, the InvalidateRect function, with this API declaration:
<DllImport("user32.dll")>
Private Shared Function InvalidateRect(
ByVal hWnd As Integer,
ByRef lpRect As Rectangle,
ByVal bErase As Boolean) As Boolean
End Function
I've tried to use it with both False/True Flags.
The problem? the problem is the same as the one that I pointed in my first update:
'Is not perfect, it generates a very annoying effect 'cause I get a lot of flickering when drawing the rectangle (and yes I have the Form doubleBuffered and also I'm using the CreateParams trick to try to avoid flickering, but nothing).'
UPDATE 3
I'm trying to fix this issue using the RedrawWindow function which as I've seen in this SO answer it can be used to do the same as InvalidateRect function does but also with more flexibility and maybe without the annoying effect that I get using the InvalidateRect function, I just needed to try it.
The RedrawWindow function updates the specified rectangle or region in a window's client area.
This is the API declaration:
<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
I've tried to use the function with these parameters:
RedrawWindow(IntPtr.Zero, Me.SelectionRectangle, IntPtr.Zero, RedrawWindowFlags.Invalidate)
...Which I suppose that as the MSDN documentation describes, if the first parameter is NULL it means the desktop screen, the second parameter means the rectangle to update, the third parameter need to be null if I've specified a rectangle in the second parameter, and the last parameter means a flag that indicates the action(s) to perform (in this case invalidate the rectangle as @Plutonix said?)
I've tried to use that isntruction after drawing the rectangle and before drawing it, I mean in the OnMouseMove event, and else inside my DrawRectangle method in my code, but I don't see any difference in the screen, I still have the same problem that I shown in the images above when drawing the rectangle I mean that multiple rectangles are drawn when I move the mouse and any rectangle is erased by this function, maybe I'm using the wrong parameters?.
The solution is more simple and windows API isn't required. Just create a transparent from and draw red rectangle on it. Following code do that, you only need to replace in your semi transparent form. The flickering happens because we clean the graphics and then draw, the easiest way to avoid it is do painting at once, so if we paint the rectangle on a bitmap and then draw the bitmap, operation is done in one step and flickering doesn't happens.
Drawing will be done on OnPaintBackground of the drawing form so a drawing form with will be needed. This is the main class, where the events are captured:
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
As drawing will be done in OnPaintBackground, a second class is needed:
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
Just create two forms and paste... It will create the drawing form and draw the red rectangle creating a bitmap buffer so only one operation is done when drawing. This works very fine without flickering. Hope it helps!
Keith's answer is mostly correct, but lacking one key point:
Protected Overrides Sub OnPaint(ByVal e as PaintEventArgs)
MyBase.OnPaint(e)
If bClickHolding Then e.Graphics.DrawRectangle(pen:=Pen, rect:=Rect)
End Sub
You should do your drawing in the paint event, not the event handler.
This is why you're getting flickering, because the form paint event is being drawn in between the frames, causing the buffer to be cleared.
also, here's some additional 'hacks':
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)
you should probably draw it in a panel though. oh and don't put program logic in the OnPaint event, put it either in the handler, or in a separate thread.
if you want to draw it from another control/class, don't. instead, draw it in the main control's OnPaint event, and simply reference the object/boolean/size,location in the other control. (ie: If myBoundingbox.bClickHolding Then...)
some links that explain the issue (quote from MSDN):
When creating a new custom control or an inherited control with a different visual appearance, you must provide code to render the control by overriding the OnPaint method.
MSDN - Custom Control Painting and Rendering
hmm, after reading that part about transparency, i was going to suggest: (just set .TransparencyKey = Color.Black) but, that bypasses mouse events, would need some WndProc possibly to fix that: MSDN - Form.TransparencyKey Property - hmm yea, the problem with that is the window loses focus.
possibly something like this: MSDN - NativeWindow Class - but probably you will need to use a mouse hook, since you're not receiving messages for the window any longer with transparency.
also, this here is a sort of 'hack', that paint a rectangle in the background behind the cursor. the problem is, the effect lags behind the cursor, so it doesn't work if you move the mouse really fast. or maybe it would be better to put it on a timer instead. i'll leave it here for now. you can use either the OnMouseMove override or the WndProc method, but i can't see a performance difference. (edit: and nope, timer doesn't reduce the lag).
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
Hopefully this helps you.
Update 1: Reworked the code. Handles backwards selection rectangles, less checks, etc. Cleaned it up.
Update 2: Updated to reflect porkchop's correction.
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
来源:https://stackoverflow.com/questions/23674820/how-to-properly-clear-or-update-a-drawn-rectangle-on-the-screen