A Picture Viewer Class that can Scroll and Zoom using API






4.86/5 (20 votes)
This is a simple class that can view scroll and zoom pictures

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 value13369376
)
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.
'first of all we load our image from a file into a Bitmap type
Dim srcImage As New Bitmap("MY Image Path")
'we get destination HDC and put it into a variable of type IntPtr
Dim Graph As Graphics = Me.CreateGraphics
Dim desHdc As IntPtr = Graph.GetHdc()
'For source HDC three steps
'create empty DC , get its HDC and put it into a variable of type IntPtr
Dim srcHdc As IntPtr = CreateCompatibleDC(IntPtr.Zero)
'get the handle for our source bitmap and put it into a variable of type IntPtr
Dim hBitmapSrc As IntPtr = srcImage.GetHbitmap()
'put the source Image into the source DC
SelectObject(srcHdc, hBitmapSrc)
'now the source DC is loaded with our image and we have its HDC
'we have destination HDC , we can BitBlt right away
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 'accelerated scrolling when right click drag ---------------- 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 theSub
will exit and nothing will happen. TheDownPress
is of Boolean type, it is set totrue
when theMouseDown
event fires and tofalse
whenMouseUp
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 aMouseMove
event. For this, we utilize (P) of type point which carries the current mouse position to be used when the nextMouseMove
event fires, we initializeP.X
andP.Y
in theMouseDown
event handler.In case of acceleration, a constant
CP.X
orCP.Y
is added to the X and Y values ofMRec
. Then theDrawPic
function is called with its parameters set to0 ' 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 theMouseMove
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 callsDrawPic
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 callsDrawPic
the same as above. But before changing the current zoom factor (Zfactor
), it makes a copy of it intooldZFactor
to be used byDrawPic
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 typeBitmap
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 isCOLORNOCOLOR
.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 byStretchBlt
. - 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 firstIF
statement has something to do with the X and Width values ofMRec
,BRec
,Host
,srcBitmap
. The nextIF
statement does the same thing but for the Y and Height ofMRec
,BRec
,Host
,srcBitmap
. Now we can explain oneIF
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 thenMRec
(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 otherIF '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 theBRec.X
andBRec.Width
to select a portion in the Host control which is centered to viewMRec
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
andMRec.Width
? Well..MRec.Width
will be equal toHost.Width
/ZFactor
which makes sense asMRec
gets scaled by theZFactor
, forMRec.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 aMouseMove
,Host_Paint
orHost_Rsize
but not by aMouseUp
. IfoldZFactor=ZFactor
then the right term will equalMRec.X
, as(Host.Width / oldZfactor - Host.Width / Zfactor)
will be equal to zero. If theoldZFactor
is not equal toZFactor
theMRec
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 ofMRec
are bigger than the image width or height, and if so it relocatesMRec
so its Right or Bottom values are equal to the image width or height. SinceMRec.Right
andMrec.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 ofMRec
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
andBRec
.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 'The end of DrawPic function
As we said earlier, the
StretchBlt
function needs 5 things. TheStretchBlt
function can't take the source or destination rectangle as a typeRectangle
, 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 anyGraphics
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 thedesHDC
gets released at the end of everyDrawPic
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