Introduction
This class has some good abilities. It can view a picture, zoom, and scroll. Well it does that, but it does it with style :-).
- It uses API functions so it is fast.
- It scrolls and zooms at the same time using two mouse buttons.
- It does normal scrolling when you drag with left mouse button (the point you started dragging from will always be under the mouse cursor).
- It also does accelerated scrolling when you drag using right mouse button (good for big pictures, or when the picture is zoomed in).
- The zoom is mouse position dependent, so it zooms to the point you are clicking.
- The picture will always be in the middle of the host control if the current size of the image is smaller than the Host control.
- When you hit the boundary of the image while scrolling, then you move the mouse the other way the picture will not start scrolling again until the mouse goes past the point which it stopped scrolling from. In other words, the point you started scrolling from will always be under the mouse cursor.
This long article may give the impression that the code is long and complicated, but actually the code is very simple and small for what it does. May be when viewed in the VS editor, it will look more contained and easy to follow.
Background
Before I talk about how these functions work, I will talk first about the API functions used, away from scrolling and zooming.
Dealing with API can sometimes cause headache but dealing with graphics API will surely cause headache, nevertheless its results are pretty amazing.
I use the API functions 'CreateCompatibleDc
', 'SelectObject
', 'DeleteDC
', 'BitBlt
', 'StretchBlt
'.
We utilize what is called the device context or DC, which you can think of as a place to draw on or take drawings from.
As BitBlt
or StretchBlt
are used to transfer image data from one DC to another DC, they need some parameters:
- Destination DC handle (HDC), identifies the DC that we want to paste into
- Destination rectangle defines where to paste the copied data in the destination DC
- Source DC handle (HDC) , identifies which DC we want to copy from
- Source rectangle which is the area of the source DC that will be copied
- A parameter that controls the appearance of the transferred image data (for our uses, this parameter is fixed which is
SRCCOPY
and has a value 13369376
)
You must know the difference between the DC and the HDC. Basically a DC is a place in memory, and the HDC is a value which enables us to identify that place uniquely, so every DC has an HDC.
For the destination DC we already have it, it is the DC of a Form
, a PictureBox
, a Label
or whatever control we want to draw into, what is left is to get its HDC, and now we have the destination HDC.
For the destination rectangle, we get the boundary of that Form
or PictureBox
defined by a rectangle which has a starting point (x,y) and area(width,height).
For source DC, well we don't have a source DC yet, so we create an EMPTY one using API function 'CreateCompatibleDC
' which returns its HDC.
We can now BitBlt
from source HDC to destination HDC, but the source DC is empty, it has blackness in it so all we get on the Form
is blackness.
First we have to put an image in the source DC. For this, we use API function 'SelectObject(sourceHDC,HandleBitmap)
' which means put the image whose handle is 'HandleBitmap
' into the DC whose HDC is 'sourceHDC
'.
The bitmap handle to a bitmap is like the HDC to a DC. We get the bitmap handle by calling 'Bitmap.GetHbitmap()
' method which returns the Bitmap
handle.
Finally for source rectangle, we will use the dimensions of the image we put inside the source DC.
Now and only now, we can StretchBlt
or B
itBlt
.
This sample code illustrates the process. Put this code in a button_click
handler and supply the path of your image and the image will be viewed on the main form. But first, don't forget to declare these API functions. You can find it in the attached file. It takes a lot of space so I didn't put it here.
Dim srcImage As New Bitmap("MY Image Path")
Dim Graph As Graphics = Me.CreateGraphics
Dim desHdc As IntPtr = Graph.GetHdc()
Dim srcHdc As IntPtr = CreateCompatibleDC(IntPtr.Zero)
Dim hBitmapSrc As IntPtr = srcImage.GetHbitmap()
SelectObject(srcHdc, hBitmapSrc)
BitBlt(desHdc, 0, 0, srcImage.Width, srcImage.Height, srcHdc, 0, 0, 13369376)
A pretty simple code, we can use StretchBlt
instead of BitBlt
without any modification of the code, but notice that StretchBlt
takes the source and destination rectangles as parameters, where BitBlt
takes the destination rectangle. Writing this code one time from memory will help you visualize its operation.
You can instead use the 'Graphics.DrawImage
' method to accomplish the same result but there is a big difference in speed especially when scrolling. I was making this control for a friend and decided to use API when I knew he intended to view a 27MB , 9000*3000, .jpg image in it.
Using the Code
Now back to our class...
Our class operation is simple. I use a Rectangle (MRec
) that stores at all times which portion of the image will be copied to the Host Control, and a Rectangle (BRec
) which stores at all times where to put the copied portion in the Host control, and a zoom factor ZFactor
which tells if (MRec
) should be scaled up or down before it is copied to the host control.
This class has a main function which is DrawPic
and some event handlers MouseDown
, MouseMove
, MouseUp
, Host_Paint
and Host_Resize
. Every event handler (when the corresponding event fires) does some changes to the rectangle (MRec
) or to the (ZFactor
) then calls DrawPic
. The following illustration explains what I said:
If the host is painted or resized, nothing happens to the location or the size of MRec
, and DrawPic
is called right away which draws the unchanged current MRec
into the Host.
I use MouseDown
, MouseUp
and MouseMove
to see if the user is currently dragging or clicking and with which mouse button.
If the user is dragging the X and Y values of the rectangle MRec
are changed, in another words the dragging changes the location of MRec
but not its size, and the DrawPic
function is continuously called.
If the user is clicking, the zoom factor (ZFactor
) is changed up or down depending on which mouse button she/he clicked, DrawPic
is called and the current X,Y location of the click are passed to it.
I will discuss three things, the MouseMove
event handler, the MouseUp
event handler and the DrawPic
function, the rest is easy to figure out.
- The
Host_MouseMove
event handler:
Private Sub Host_MouseMove(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs)
If IsNothing(srcBitmap) Then Exit Sub
If DownPress = True Then
Host.Cursor = Cursors.NoMove2D
If e.Button = MouseButtons.Right Then
CP.X = (P.X - e.X) * (srcBitmap.Width / 2000)
CP.Y = (P.Y - e.Y) * (srcBitmap.Height / 2000)
End If
Mrec.X = ((P.X - e.X) / Zfactor) + Mrec.X + CP.X
Mrec.Y = ((P.Y - e.Y) / Zfactor) + Mrec.Y + CP.Y
DrawPic(0, 0)
If Xout = False Then
P.X = e.X
End If
If Yout = False Then
P.Y = e.Y
End If
End If
End Sub
First it checks to see if we have some image in srcBitmap
, if not the Sub
will exit and nothing will happen. The DownPress
is of Boolean type, it is set to true
when the MouseDown
event fires and to false
when MouseUp
event fires. After that comes the acceleration of right mouse button drag, this acceleration is dependent on the size of the image.
Now to the real part, the X and Y values of MRec
are increased or decreased according to where the mouse is now and where it was the last time it fired a MouseMove
event. For this, we utilize (P) of type point which carries the current mouse position to be used when the next MouseMove
event fires, we initialize P.X
and P.Y
in the MouseDown
event handler.
In case of acceleration, a constant CP.X
or CP.Y
is added to the X and Y values of MRec
. Then the DrawPic
function is called with its parameters set to 0 ' DrawPic(0,0) '
, Now we set (P.X =e.X)
and (P.Y=e.Y)
to know from it how much we move the next time the MouseMove
event happens.
- The
Host_MouseUp
event handler:
Private Sub Host_MouseUp(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs)
If IsNothing(srcBitmap) Then Exit Sub
DownPress = False
Host.Cursor = Cursors.Arrow
If CS.X = e.X And CS.Y = e.Y Then
If e.Button = MouseButtons.Left Then
If Zfactor > MaxZ Then Exit Sub
oldZfactor = Zfactor
Zfactor = Zfactor * 1.3
DrawPic(e.X, e.Y)
ElseIf e.Button = MouseButtons.Right Then
If Zfactor < MinZ Then Exit Sub
oldZfactor = Zfactor
Zfactor = Zfactor / 1.3
DrawPic(e.X, e.Y)
End If
RaiseEvent ZoomChanged(Zfactor)
End If
End Sub
If the released mouse button is the left button, it will multiply the current zoom factor (ZFactor
) by 1.3 (zoom in), then it calls DrawPic
passing the current X and Y coordinates of the mouse. If the right mouse button is released, it will divide the current zoom factor by 1.3 (zoom out) and then calls DrawPic
the same as above. But before changing the current zoom factor (Zfactor
), it makes a copy of it into oldZFactor
to be used by DrawPic
later.
- The
DrawPic
function is the main function in this class. It basically does it all. This function is divided into four sections as follows:
- It checks to see if the variables are declared and if not, it will declare and initialize them (this is a one time only operation).
Private Function DrawPic(ByVal ZoomX As Single, _
ByVal ZoomY As Single) As Boolean
If IsNothing(srcBitmap) Then Exit Function
If srcHDC.Equals(IntPtr.Zero) Then
srcHDC = CreateCompatibleDC(IntPtr.Zero)
HBitmapSrc = srcBitmap.GetHbitmap()
SelectObject(srcHDC, HBitmapSrc)
End If
If desHDC.Equals(IntPtr.Zero) Then
If IsNothing(Gr) Then
Gr = Host.CreateGraphics
End If
desHDC = Gr.GetHdc()
SetStretchBltMode(desHDC, 3)
End If
First we declare the function (DrawPic
), it takes two variables (ZoomX
, ZoomY
) these variables are passed to the function only when a zoom action (click) occurs, else they are zeros.
We check to see if our source bitmap (srcBitmap
) of type Bitmap
currently has a picture in it, if not, we exit this function and do nothing.
We then check if there exists a source DC (by checking if srcHDC=0
), if not we create one by calling 'CreateCompatibleDC
' which is now just an empty DC that has nothing in it. After that we have to put a picture in it. To be able to put a picture in it, we must first get the handle of that picture by using 'srcBitmap.GetHbitmap()
', then we put the picture using its handle in the source DC using its HDC by calling 'Selectobject (srcHDC,HBitmapSrc)
'.
After that, we check if we have a destination DC, if not we get the DC of the Host by first creating a Graphics
object from the host (if it is not yet created) and then calling the 'Graphics.GetHdc()
' method which returns the HDC.
Then we call the 'SetStretchBltMode
' function to set the mode for our destination DC to 3 which is COLORNOCOLOR
.
Now you should know from the previous discussion that we did the above steps to get just two variables, our very important two variables, the Destination DC handle (desHDC
), and the source DC handle (srcHDC
) which will be used later in this function by StretchBlt
.
- This part of the
DrawPic
function is completely separate from the previous part. It is the part where the math comes in.
Xout = False
Yout = False
If Host.Width > srcBitmap.Width * Zfactor Then
Mrec.X = 0
Mrec.Width = srcBitmap.Width
Brec.X = (Host.Width - srcBitmap.Width * Zfactor) / 2
Brec.Width = srcBitmap.Width * Zfactor
BitBlt(desHDC, 0, 0, Brec.X, Host.Height, srcHDC, _
0, 0, TernaryRasterOperations.BLACKNESS)
BitBlt(desHDC, Brec.Right, 0, Brec.X, Host.Height, _
srcHDC, 0, 0, TernaryRasterOperations.BLACKNESS)
Else
Mrec.X = Mrec.X + ((Host.Width / oldZfactor - _
Host.Width / Zfactor) / ((Host.Width + 0.001) / ZoomX))
Mrec.Width = Host.Width / Zfactor
Brec.X = 0
Brec.Width = Host.Width
End If
If Host.Height > srcBitmap.Height * Zfactor Then
Mrec.Y = 0
Mrec.Height = srcBitmap.Height
Brec.Y = (Host.Height - srcBitmap.Height * Zfactor) / 2
Brec.Height = srcBitmap.Height * Zfactor
BitBlt(desHDC, 0, 0, Host.Width, Brec.Y, srcHDC, 0, _
0, TernaryRasterOperations.BLACKNESS)
BitBlt(desHDC, 0, Brec.Bottom, Host.Width, Brec.Y, _
srcHDC, 0, 0, TernaryRasterOperations.BLACKNESS)
Else
Mrec.Y = Mrec.Y + ((Host.Height / oldZfactor - _
Host.Height / Zfactor) / ((Host.Height + 0.001) / ZoomY))
Mrec.Height = Host.Height / Zfactor
Brec.Y = 0
Brec.Height = Host.Height
End If
oldZfactor = Zfactor
You can see that this part is divided equally into two IF
statements, the first IF
statement has something to do with the X and Width values of MRec
, BRec
, Host
, srcBitmap
. The next IF
statement does the same thing but for the Y and Height of MRec
, BRec
, Host
, srcBitmap
. Now we can explain one IF
statement and the other will be the same.
In the first IF
statement, it checks if the Host width will be bigger than the source image multiplied by the zoom factor. It tries to know if the whole image will be contained in the host control or not, because if so, it will be able to see the whole image and if we will be able to see the whole image then MRec
(which is the portion copied from source to destination) must be set to contain the whole image, So we make 'MRec.X=0
' and 'MRec.Width=srcBitmap.Width
', and for the other IF 'MRec.Y=0'
and 'MRec.Width=srcBitmap.Width'
.
What is BRec
used for? The program can function without it 'meaning that you can delete it completely from the program' BUT as you zoom out more on your picture the small picture will not appear in the center of the host, but it will align to a side of the Host. So we set the BRec.X
and BRec.Width
to select a portion in the Host control which is centered to view MRec
in it.
The two BitBlt
functions are used to draw blackness around the picture when the picture is smaller that the Host. You can remove them and see what happens.
Now all this talk will happen if the image or the zoomed image is smaller then the Host control, but what happens if this is not the case.
In this case we make 'BRec.X=0
' and 'BRec.Width=Host.Width
' which is logical, meaning that the viewed image will take up all Host area.
But what about MRec.X
and MRec.Width
? Well.. MRec.Width
will be equal to Host.Width
/ZFactor
which makes sense as MRec
gets scaled by the ZFactor
, for MRec.X
you should notice that if the 'oldZFactor
' equals 'ZFactor
', this means that no zooming will occur meaning that this function is being called by a MouseMove
, Host_Paint
or Host_Rsize
but not by a MouseUp
. If oldZFactor=ZFactor
then the right term will equal MRec.X
, as (Host.Width / oldZfactor - Host.Width / Zfactor)
will be equal to zero. If the oldZFactor
is not equal to ZFactor
the MRec
will be positioned to approach the point clicked for zooming.
- This part checks to see if
MRec
is within the image boundary or not. If not, it will relocate it so it is in the image boundary.
If Mrec.X < 0 Then
Xout = True
Mrec.X = 0
End If
If Mrec.Y < 0 Then
Yout = True
Mrec.Y = 0
End If
If Mrec.Right > srcBitmap.Width Then
Xout = True
Mrec.X = (srcBitmap.Width - Mrec.Width)
End If
If Mrec.Bottom > srcBitmap.Height Then
Yout = True
Mrec.Y = (srcBitmap.Height - Mrec.Height)
End If
This part is straight forward. It checks if the X or Y values of MRec
are less that zero, and if so it makes them zero, and then it checks if the Right and Bottom values of MRec
are bigger than the image width or height, and if so it relocates MRec
so its Right or Bottom values are equal to the image width or height. Since MRec.Right
and Mrec.Buttom
are read only, we cannot modify them, so we calculate the new X or Y location with a simple subtraction. (notice that this part manipulates the location of MRec
only but not its Width or Height, it doesn't change its size).
- This part does the actual drawing of the image in the Host depending on values of
MRec
and BRec
.
StretchBlt(desHDC, Brec.X, Brec.Y, Brec.Width, Brec.Height, _
srcHDC, Mrec.X, Mrec.Y, Mrec.Width, Mrec.Height, _
TernaryRasterOperations.SRCCOPY)
Gr.ReleaseHdc(desHDC)
desHDC = Nothing
End Function
As we said earlier, the StretchBlt
function needs 5 things. The StretchBlt
function can't take the source or destination rectangle as a type Rectangle
, it separates the (X,Y,Width,Height) each has its own parameter for both source and destination, so actually it needs 11 parameters.
Finally we release the Host DC by calling 'Graphics.ReleaseHdc(HDC)
' method, and this is a very important step because suppose you want to use any Graphics
function say 'Gr.DrwaEllipse
' if you put your code before you call 'ReleaseHdc
', the program will return an error, you should place your code after the HDC has been released. So the desHDC
gets released at the end of every DrawPic
call.
Points of Interest
Hope you get something out of that. This class can also be used "after making necessary changes" to be part of a graphics editing program.
In the above code, we saw that what applies to X applies to Y, and what applies to Width applies to Height, which basically means that the code can be cut in half if we lived in a two dimensional world. :)
That's it for now, enjoy.
History
- Updated on May 1st, 2007: Fixed program memory leak, now it's leak free woohoo