Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Picture Viewer Class that can Scroll and Zoom using API

0.00/5 (No votes)
10 Dec 2007 1  
This is a simple class that can view scroll and zoom pictures
Screenshot - KPicView1.jpg

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:

  1. Destination DC handle (HDC), identifies the DC that we want to paste into
  2. Destination rectangle defines where to paste the copied data in the destination DC
  3. Source DC handle (HDC) , identifies which DC we want to copy from
  4. Source rectangle which is the area of the source DC that will be copied
  5. 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 BitBlt.

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:

Screenshot - Mrec.jpg

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.

  1. 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 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.

  2. 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.

  3. The DrawPic function is the main function in this class. It basically does it all. This function is divided into four sections as follows:
    1. 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.

    2. 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.

    3. 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).

    4. 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 'The end of DrawPic 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here