Introduction
Finally moving from VB6 to VB.NET, here is one of my first steps you might find interesting. Working on another project, I needed a magnifying glass to inspect parts
of the screen more closely. Instead of downloading any of those available on the internet, I made one myself, since that way I would surely learn some new things.
How it works
The main code
The application starts with a frmStart
which does only a few things: determine the size of the glass and the magnification factor and switch on the glass.
Therefore there isn't much code to talk about.
Public Class frmStart
Private Sub cmdStart_Click(sender As System.Object, _
e As System.EventArgs) Handles cmdStart.Click
Me.Hide()
Dim MagnSize As Integer = Choose(lstMagnSize.SelectedIndex + 1, 48, 64, 96, 128)
frmScreenCopy.MagnSize = MagnSize
frmScreenCopy.OriSize = MagnSize \ Choose(lstMagn.SelectedIndex + 1, 2, 4, 8, 16)
frmScreenCopy.Show()
End Sub
Private Sub frmStart_Load(sender As Object, e As System.EventArgs) Handles Me.Load
lstMagnSize.SelectedIndex = 1
lstMagn.SelectedIndex = 1
End Sub
End Class
The magnification factor is the size of the glass divided by the original size (that is the portion that actually gets magnified).
There is some more code in frmScreenCopy
, the second and last form of the project. Since the MouseMove
event is fired very frequently,
the code in there should be kept to a minimum. Therefore at first all declarations are done on form level.
Two Public
variables are set by frmStart
(see above).
Imports System.Drawing.Drawing2D
Public Class frmScreenCopy
Public OriSize As Integer = 16 Public MagnSize As Integer = 64 Dim bmpOriCopy As Bitmap Dim bmpgrOriCopy As Graphics Dim rctOri As Rectangle Dim rctOriCopy As Rectangle Dim rctMagn As Rectangle Dim Desktop As Image Dim picgr As Graphics
Dim gpath As GraphicsPath Dim rgn As Region
Dim pn As Pen = New Pen(Color.Silver, 4)
Each time the form is activated, the objects have to be (re-)set in case the size of the glass or original size has changed.
The screenshot capturing has to take place in the same event. Before you put the screenshot into the PictureBox
, you have to adjust its bounds.
Otherwise the glass would only work in the size the PictureBox
has at design time. To see what I mean, just switch line 028 and 029.
Private Sub frmScreenCopy_Activated(sender As Object, e As System.EventArgs) Handles Me.Activated
bmpOriCopy = New Bitmap(OriSize, OriSize)
bmpgrOriCopy = Graphics.FromImage(bmpOriCopy)
rctOriCopy = New Rectangle(0, 0, OriSize, OriSize)
rctOri = New Rectangle(0, 0, OriSize, OriSize) rctMagn = New Rectangle(0, 0, MagnSize, MagnSize)
Dim SC As New ScreenShot.ScreenCapture
pic.SetBounds(0, 0, Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height)
pic.Image = SC.CaptureScreen
Desktop = pic.Image.Clone
picgr = pic.CreateGraphics
Cursor.Hide() Cursor.Tag = "off"
End Sub
When the user stops working with the glass using the Escape-key, you make sure the cursor is back and visible.
Private Sub frmScreenCopy_Deactivate(sender As Object, e As System.EventArgs) Handles Me.Deactivate
Cursor.Show()
Cursor.Tag = "on"
End Sub
I know to avoid memory leaks, one should carefully dispose off all objects made. But I do not know how far you should go in this. For instance, in this case, when the form
is disposed off, aren't all objects too? To be on the safe side, I did dispose them all one after the other here.
Private Sub frmScreenCopy_Disposed(sender As Object, e As System.EventArgs) Handles Me.Disposed
rgn.Dispose()
gpath.Dispose()
bmpgrOriCopy.Dispose()
bmpOriCopy.Dispose()
Desktop.Dispose()
picgr.Dispose()
End Sub
In the KeyDown
event, there is more code than shown here, but it is simple and obvious code that doesn't need any explanation.
Private Sub frmScreenCopy_KeyDown(sender As Object, _
e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyDown
Select e.KeyCode
Case Keys.Escape
Me.Close()
frmStart.Show()
Case ...
End Select
End Sub
Finally, here you have the code that makes the magnifying glass. First, the rctOri
rectangle is set to the position of the mouse.
The rectangle is positioned in the middle of the mouse. Then a copy is made of that portion of the screen in the buffering bitmap bmpOriCopy
. Why I explain later.
Private Sub pic_MouseMove(sender As Object, e As System.Windows.Forms.MouseEventArgs) Handles pic.MouseMove
rctOri.X = e.X - OriSize / 2
rctOri.Y = e.Y - OriSize / 2
bmpgrOriCopy.DrawImage(pic.Image, rctOriCopy, rctOri, GraphicsUnit.Pixel)
Before you can draw a new magnifying glass, you have to remove the previous one from the screen. That's the function of the Desktop
image object: it is buffering the screen for restoration purposes.
picgr.DrawImage(Desktop, rctMagn, rctMagn, GraphicsUnit.Pixel)
Now we finally are ready to draw a new magnification glass.
068 069 rctMagn.X = e.X - MagnSize / 2
070 rctMagn.Y = e.Y - MagnSize / 2
071 gpath = New GraphicsPath
072 gpath.AddEllipse(rctMagn)
073 rgn = New Region(gpath)
074 picgr.Clip = rgn
075
076 picgr.DrawImage(bmpOriCopy, rctMagn, rctOriCopy, GraphicsUnit.Pixel)
077 picgr.DrawEllipse(pn, rctMagn)
078
079 End Sub
080 End Class
Comment on the code
The shortest summary of what is done sounds like this:
- The screen is captured and put in a screen-sized
PictureBox
.
- In the
MouseMove
event of the PictureBox
, a portion under the mouse button is enlarged in a circular formed shape.
Screen capturing
Capturing the whole desktop screen in VB.NET is not so different from VB6: there are only some special API functions one should know, such as:
User32.GetDesktopWindow();
User32.GetWindowDC();
GDI32.CreateCompatibleDC();
GDI32.CreateCompatibleBitmap()
...
But for the screen capturing part of the project, I thankfully borrowed from gigemboy on vbforums.com,
his ScreenCapture class and I left it unchanged
(some functions haven't been used in my Magnifying Glass project and could be deleted). Using that class (line 27-29), capturing the screen is very easy.
Drawing a transparent circle shape
After putting a copy of the desktop into a PictureBox
, you can detect the position of the mouse (line 59)
and then project an enlarged version of the portion of the screen underneath the mouse.
Basically the enlargement is done using rectangle shapes, with DrawImage
. But a magnifying glass should be circular shaped.
In VB6, I would have made the circular shape using the API functions bitblt
or
stretchblt
and a mask (black and white) picture. With those API functions, it would go something like that:
bitblt picResult.hdc, x, y, W, H, picMask.hdc,0,0,SRCINVERT
bitblt picResult.hdc, x, y, W, H, picGrad.hdc,0,0,SRCAND
bitblt picResult.hdc, x, y, W, H, picMask.hdc,0,0,SRCINVERT
In VB.NET, I could do the same. But then I would learn nothing. Still, I was not sure if I would find another way, a way implemented in .NET
that would be as fast as the APIs bitblt
, stretchblt
. But I gave it a chance, and found out there are indeed ways in VB.NET, and they work fast enough.
You can first draw something into a GraphicsPath
(lines 71-72), put this path into a Region
object (line 73), and then use this region to clip (line 74) the following
graphic actions (lines 76-77). A clipping region works as a mask. Paths can be anything you like. This way you can produce all kinds of transparent irregular shapes.
In my project, I draw a circle (~ellipse) (line 72) I use to clip the enlarged rectangle (line 76).
Probably, behind the scenes, this clipping with a path in a region is still using the raster operations mentioned above. I think they first draw in white on a black surface
to create a mask, and then use the mask to draw transparently.
It has to work smoothly
The most difficult part of the project was to make things work as smoothly as possible. There are many different ways to implement a working magnifying glass,
but they do not always work that smoothly. The code in the MouseMove
event gets repeated very fast. The less code there, the better. It is for instance very
important to place Dim
s and New
s outside if possible. Here, Path
and Region
have to be renewed each time the mouse moves.
But all the rest is declared outside.
There are also things I cannot explain. Often less is more. But not in this case. In the alternative code beneath, I use no buffer for the original size rectangle image
and I draw from the Desktop-image directly instead. I also don't use a PictureBox
to work with. Instead I use Me.BackgroundImage
. The result is less code,
and less actions to do for the computer. But in this version, there is a certain flickering. Why is it not clear? It should work better.
000 Imports System.Drawing.Drawing2D
001
002 Public Class frmScreenCopy
003
004 Public OriSize As Integer = 16 005 Public MagnSize As Integer = 64 006 Dim rctOri As Rectangle 007 Dim rctMagn As Rectangle 008 Dim Desktop As Image
009 Dim picgr As Graphics
010
011 Dim gpath As GraphicsPath 012 Dim rgn As Region
013 Dim pn As Pen = New Pen(Color.Silver, 4)
014
015 Private Sub frmScreenCopy_Activated(sender As Object, e _
As System.EventArgs) Handles Me.Activated
016 017 rctOri = New Rectangle(0, 0, OriSize, OriSize) 018 rctMagn = New Rectangle(0, 0, MagnSize, MagnSize)
019
020 Dim SC As New ScreenShot.ScreenCapture
021 Me.SetBounds(0, 0, Screen.PrimaryScreen.Bounds.Width, _
Screen.PrimaryScreen.Bounds.Height)
022 Me.BackgroundImage = SC.CaptureScreen
023 Desktop = Me.BackgroundImage.Clone
024 picgr = Me.CreateGraphics
025
026 Cursor.Hide()
027 Cursor.Tag = "off"
028 End Sub
029
030 Private Sub frmScreenCopy_Deactivate(sender As Object, _
e As System.EventArgs) Handles Me.Deactivate
031 Cursor.Show()
032 Cursor.Tag = "on"
033 End Sub
034
035 Private Sub frmScreenCopy_Disposed(sender As Object, _
e As System.EventArgs) Handles Me.Disposed
036 rgn.Dispose()
037 gpath.Dispose()
038 Desktop.Dispose()
039 picgr.Dispose()
040 End Sub
041
042 Private Sub frmScreenCopy_KeyDown(sender As Object, _
e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyDown
043 Select e.KeyCode
044 Case Keys.Escape
...
047 Case Keys.F2
...
061 Case Keys.F4
070 End Select
071 End Sub
072
073 Private Sub frmScreenCopy_MouseMove(sender As Object, _
e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseMove
074 075 picgr.DrawImage(Desktop, rctMagn, rctMagn, GraphicsUnit.Pixel)
076
077 078 rctOri.X = e.X - OriSize / 2
079 rctOri.Y = e.Y - OriSize / 2
080 rctMagn.X = e.X - MagnSize / 2
081 rctMagn.Y = e.Y - MagnSize / 2
082
083 gpath = New GraphicsPath
084 gpath.AddEllipse(rctMagn)
085 rgn = New Region(gpath)
086 picgr.Clip = rgn
087
088 picgr.DrawImage(Desktop, rctMagn, rctOri, GraphicsUnit.Pixel)
089 picgr.DrawEllipse(pn, rctMagn)
090 End Sub
091 End Class
If you want to see what I mean by flickering, you'll have try out this code. The flickering is subtle but real and not nice.
History
This is a working first draft. I probably will code new versions of it with more functions. For instance I would like to be able to easily
select parts of the screen and save them. But then maybe it won't be simple anymore and would be better separated in another article?
Update: January 23, 2012
I've had a very useful comment from trembru who suggested to replace the capture class that uses API functions, with the following code:
Private Function CaptureScreen() As Image
Dim Img As New Bitmap(Screen.PrimaryScreen.WorkingArea.Width, _
Screen.PrimaryScreen.WorkingArea.Height)
Dim g As Graphics = Graphics.FromImage(Img)
g.CopyFromScreen(0, 0, 0, 0, Img.Size)
g.Dispose()
Return Img
End Function
Of course, I changed my code since it makes it better, shorter, and simpler again. I also changed the application icon, because the previous one looked terrible.
The result is here: