Click here to Skip to main content
Click here to Skip to main content

A Picture Viewer Class that can Scroll and Zoom using API

By , 10 Dec 2007
 
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Mohammed Abd Alla
Web Developer
Egypt Egypt
Member
currently studying engineering , and i had interest in programming for a while now.
i program in vb6 , vb.net , and C#.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionExcellentmemberMember 78627108 Feb '13 - 20:22 
Thank you.
Questionhow to reset the viewer to be emptymemberMohamed Abdullah 8228 Jul '12 - 3:04 
as in the form load cause i am not able to remove the image after loading it.
 
thanks in advance
GeneralMy vote of 5memberRedDK30 Sep '11 - 8:40 
v
GeneralRe: My vote of 5memberMohammed Abd Alla2 Mar '12 - 10:45 
Thanks for the 5 Smile | :)
QuestionA good one!memberRedDK30 Sep '11 - 8:30 
Love this VB stuff. Have no understanding of c# so those projects are guaranteed to be a mysteriously useful when they do compile for me.
 
VB, because of BASIC, naturally. Wouldn't forget it by choice but c# is forcing the issue.
 
Smile | :)
GeneralMy vote of 5memberherbert agosto8 May '11 - 21:21 
Excellent Article!
GeneralImage is not displayed completelymemberdaJunior9 Nov '10 - 19:51 
My picture has a lot of horizontal and vertical lines (every x pixels). Now I have the problem that not all lines are visible.
If I zoom into the picture, all lines are slowly becoming visible.
 
Is that the problem of "StretchBlt"?
Has anyone a solution for this problem?
GeneralYour work is Excellent. and i need a helpmemberhai23327 Nov '09 - 23:08 
how can i display the selected portion of image in center of the rectangle while zooming...
QuestionMemory leak?memberoverlander6 Aug '09 - 19:46 
I'm discovering that subsequent image loads seem to increase memory consumption until an exception is thrown. Something isn't being disposed of fully.
 
For instance, with the WindowsApplication9.exe in the /bin folder... If I keep loading images (jpgs from a digital camera at 10MP @ 3872 x 2592 pixels) into the app using the "Open Image" button, the memory runs away.
 
Any ideas? Otherwise, I really like the code. I'd love to be able to use it! Thanks.
AnswerRe: Memory leak?memberMohammed Abd Alla6 Sep '09 - 16:49 
thank you,
originally the program had a memory leak, but I fixed it,
try compiling the program instead of running the already compiled .exe
I wish I had more time to help but I'm very busy these days
tell me if it's urgent and I'll make time to recheck the code.
bye
AnswerRe: Memory leak?memberJohn Logan24 Feb '12 - 11:32 
Yes, there is a memory leak...
The following should correct it.
 
  
Public Function srcDispose() As Object
    If Not IsNothing(srcBitmap) Then
      'srcBitmap.Dispose() ' This is the source of the memory leak... Use DeleteObject instead.
      'Change the Declare in API.vb as follows (add as Integer)... 
      'else (PInvoke Restriction: Cannot return Variants).
      'Declare Function DeleteObject Lib "gdi32.dll" (ByVal hObject As IntPtr) As Integer
      'There may still be a leak, but it's very tiny in comparison to the above.
      DeleteObject(HBitmapSrc)
      srcBitmap = Nothing
      HBitmapSrc = Nothing
    End If

Generalwidth and height of the drawn picturemembernadya lobak6 Apr '09 - 19:56 
How can I get the width and height of the drawn picture? (after zoom in/out)
GeneralRe: width and height of the drawn picturememberMohammed Abd Alla11 Apr '09 - 7:47 
If I understood you correctly, then there is nothing called the width,height of a photo after zoom in or zoom out, because a photo have a certain width/height originally, but after the zoom in or out the dimensions of the photo depends on the container the photo resides in.
So what you really want to know is the dimenstions of the container not the photo.
QuestionHow can i draw running signal, please help me?membermoresbme15 Jun '08 - 21:52 
Assalamu'alaikum,
 
I'm Rezal, from Indonesia, Would u kind help me to break out my problems?
 
Suppose that i want to draw running signal, just like if we press ctrl+alt+del in windows then we'll see performance graph. And it should be there too the axis and ordinat label that able to automatically changing based on the data. for data, i have previously floating point number data in text (89.348,94.883,...).
 
I use vb.net 3.5 for programming.
 
Please. Thank u for ur help.
QuestionHow to draw next image at end of the first image [modified]memberTitto Sebastian20 Feb '08 - 22:03 
Your work is Excellent.
Can you please share to me following
How to draw next image at end of the first image while scrolling.
I mean when we reached end of the first image, i need to draw next image or same image again with out any time delay.That is it must view as Continues action.
Please help me
 
Titto Sebastian
 
modified on Thursday, February 21, 2008 4:41 AM

GeneralRe: How to draw next image at end of the first imagememberMohammed Abd Alla21 Feb '08 - 8:52 
Hi titto,
 
Thank you for your comment and,
 
It is possible to draw another image at the end of the first image.
BUT there are important factors:
 
1-Are the next image (or images) already loaded into memory ?
2-How big the next images (or images) are ?
 
If the all images are loaded into memory there will be no time delay at all,
but they will take alot of memory according to how big these images are.
 
for further discussion plz reply.
GeneralRe: How to draw next image at end of the first imagememberTitto Sebastian2 Mar '08 - 19:04 
Thanks for your great replay.
 
My aim is to stitch images.For example we taken different pictures in a mounten,
We need to stitch the images as into one panoramic image.
 
with regards
Titto Sebastian.
GeneralGreat ArticlememberOldGrandad29 Nov '07 - 4:47 
Thanks for you excellent article.
Worked first time in VB.net 2005
Smile | :)
GeneralMouseWheel ZoommemberDarkfyre08 Nov '07 - 2:09 
I am trying to experiment with modifying your code to zoom images with the mousewheel.
I have set the mousewheel event on the main form.
I passed the main form control to the NetPicView class
 
The following even was added.
AddHandler focusControl.MouseWheel, AddressOf form_MouseWheel
 
focuscontrol is the form control.
 

Here is the event code:
 
Private Sub Form_MouseWheel(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs)
 
If IsNothing(srcBitmap) Then Exit Sub
 
Host.Cursor = Cursors.Arrow
 
If e.Delta >= 0 Then
If Zfactor > MaxZ Then Exit Sub
oldZfactor = Zfactor
Zfactor = Zfactor * 1.3
DrawPic(e.X, e.Y)
ElseIf e.Delta < 0 Then
If Zfactor < MinZ Then Exit Sub
oldZfactor = Zfactor
Zfactor = Zfactor / 1.3
DrawPic(e.X, e.Y)
End If
RaiseEvent ZoomChanged(Zfactor)
 
End Sub
 
The experiment worked...almost.
When I zoom with the mouse wheel it does not always stay on the same pixel on the image. It seems to have an offset in the
x axis only. This does not happen if you zoom with the mouse click. I wonder if there is a way to fix this.
 
Thanks for the work. It is awesome. Wink | ;)
GeneralSpot on...memberMartinABooker15 Oct '07 - 13:18 
Thankyou for a brilliant control
GeneralVery usefulmemberPaul Talbot31 Jul '07 - 3:39 
I've been looking for an example on doing this for a while, very helpful. Thank you.
GeneralManaged versionmembereisernWolf6 May '07 - 10:06 
Hi! Thank you for your source. Helped me a lot. But there is one picularity in all this. If the image is changed (for example, rotated or changed in any other way) the viewer works improperly. I rewrote your class using only managed code (it solves the problem and does not hurt performance):
 
internal sealed class Viewer : IDisposable
{
public float CurrentZoom
{
get
{
return _zoomFactor;
}
set
{
_zoomFactor = value;
this.DrawImage(_host.Width / 2, _host.Height / 2);
}
}
 
public Image Image
{
get
{
return _image;
}
set
{
if (_image != value)
{
_image = value;
_host.Invalidate();
}
}
}
 
public float MaxZoom
{
get
{
return _maxZ;
}
set
{
_maxZ = value;
}
}
 
public float MinZoom
{
get
{
return _minZ;
}
set
{
_minZ = value;
}
}
 
private void DrawImage(float zoomX, float zoomY)
{
if (_image == null)
{
return;
}
 
_xOut = false;
_yOut = false;
 
if (_host.Width > _image.Width * _zoomFactor)
{
_mrec.X = 0;
_mrec.Width = _image.Width;
_brec.X = (_host.Width - _image.Width * _zoomFactor) / 2;
_brec.Width = _image.Width * _zoomFactor;
}
else
{
_mrec.X = _mrec.X + ((_host.Width / _oldZFactor - _host.Width / _zoomFactor) / ((_host.Width + 0.001f) / zoomX));
_mrec.Width = _host.Width / _zoomFactor;
_brec.X = 0;
_brec.Width = _host.Width;
}
 
if (_host.Height > _image.Height * _zoomFactor)
{
_mrec.Y = 0;
_mrec.Height = _image.Height;
_brec.Y = (_host.Height - _image.Height * _zoomFactor) / 2;
_brec.Height = _image.Height * _zoomFactor;
}
else
{
_mrec.Y = _mrec.Y + ((_host.Height / _oldZFactor - _host.Height / _zoomFactor) / ((_host.Height + 0.001f) / zoomY));
_mrec.Height = _host.Height / _zoomFactor;
_brec.Y = 0;
_brec.Height = _host.Height;
}
 
_oldZFactor = _zoomFactor;
 
if (_mrec.Right > _image.Width)
{
_xOut = true;
_mrec.X = _image.Width - _mrec.Width;
}
 
if (_mrec.X < 0)
{
_xOut = true;
_mrec.X = 0;
}
 
if (_mrec.Bottom > _image.Height)
{
_yOut = true;
_mrec.Y = _image.Height - _mrec.Height;
}
 
if (_mrec.Y < 0)
{
_yOut = true;
_mrec.Y = 0;
}
 
using (Graphics g = _host.CreateGraphics())
{
int brecX = (int)_brec.X;
int brecY = (int)_brec.Y;
int brecWidth = (int)_brec.Width;
int brecHeight = (int)_brec.Height;
 
int mrecX = (int)_mrec.X;
int mrecY = (int)_mrec.Y;
int mrecWidth = (int)_mrec.Width;
int mrecHeight = (int)_mrec.Height;
 
g.DrawImage(
_image
, new Rectangle(brecX, brecY, brecWidth, brecHeight)
, new Rectangle(mrecX, mrecY, mrecWidth, mrecHeight)
, GraphicsUnit.Pixel
);
 
int leftRectWidth = brecX - _host.Left;
int topRectHeight = brecY - _host.Top;
int topRectWidth = _host.Width - leftRectWidth;
int rightRectLeft = _host.Left + leftRectWidth + brecWidth;
 
g.FillRectangle(Brushes.Black, new Rectangle(0, 0, leftRectWidth, _host.Height));
g.FillRectangle(Brushes.Black, new Rectangle(leftRectWidth, 0, topRectWidth, topRectHeight));
g.FillRectangle(Brushes.Black, Rectangle.FromLTRB(rightRectLeft, topRectHeight, _host.Right, _host.Bottom));
g.FillRectangle(Brushes.Black, Rectangle.FromLTRB(leftRectWidth, _host.Top + topRectHeight + brecHeight, rightRectLeft, _host.Bottom));
}
}
 
private void _host_MouseDown(object sender, MouseEventArgs e)
{
if (_image != null)
{
_p.X = e.X;
_p.Y = e.Y;
_cp.X = 0;
_cp.Y = 0;
_cs.X = e.X;
_cs.Y = e.Y;
_downPress = true;
}
}
 
private void _host_MouseMove(object sender, MouseEventArgs e)
{
if (_image != null)
{
if (_downPress)
{
_host.Cursor = Cursors.NoMove2D;
 
// Accelerated scrolling when right click drag.
if (e.Button == MouseButtons.Right)
{
_cp.X = (_p.X - e.X) * (_image.Width / 2000);
_cp.Y = (_p.Y - e.Y) * (_image.Height / 2000);
}
 
_mrec.X = ((_p.X - e.X) / _zoomFactor) + _mrec.X + _cp.X;
_mrec.Y = ((_p.Y - e.Y) / _zoomFactor) + _mrec.Y + _cp.Y;
this.DrawImage(0, 0);
 
if (!_xOut)
{
_p.X = e.X;
}
 
if (!_yOut)
{
_p.X = e.Y;
}
}
}
}
 
private void _host_MouseUp(object sender, MouseEventArgs e)
{
if (_image != null)
{
_downPress = false;
_host.Cursor = Cursors.Arrow;
 
if (_cs.X == e.X && _cs.Y == e.Y)
{
if (e.Button == MouseButtons.Left)
{
if (_zoomFactor > _maxZ)
{
return;
}
 
_oldZFactor = _zoomFactor;
_zoomFactor = _zoomFactor * 1.3f;
}
else if (e.Button == MouseButtons.Right)
{
if (_zoomFactor < _minZ)
{
return;
}
 
_oldZFactor = _zoomFactor;
_zoomFactor = _zoomFactor / 1.3f;
}
}
 
_host.Invalidate();
}
}
 
private void _host_Paint(object sender, PaintEventArgs e)
{
this.DrawImage(0, 0);
}
 
private void _host_Resize(object sender, EventArgs e)
{
if (_hostLoadComplete)
{
this.DrawImage(0, 0);
}
}
 
private Control _host;
private Image _image;
private PointF _p;
private PointF _cp;
private PointF _cs;
private RectangleF _mrec; // Main rectangle.
private RectangleF _brec; // Boundary rectangle.
private float _zoomFactor = 1; // Current zoom.
private float _minZ = 0.05f; // Minimum zoom.
private float _maxZ = 20.0f; // Maximum zoom.
private float _oldZFactor = 1; // Previous zoom (bigger means zoom in).
private bool _xOut;
private bool _yOut;
private bool _hostLoadComplete;
private bool _downPress;
 
///
/// Initializes a new instance of the class.
///

///
/// is .
///

public Viewer(Control hostControl)
{
if (hostControl == null)
{
throw new ArgumentNullException("hostControl");
}
 
_host = hostControl;
 
_host.MouseDown += _host_MouseDown;
_host.MouseMove += _host_MouseMove;
_host.MouseUp += _host_MouseUp;
_host.Paint += _host_Paint;
_host.Resize += _host_Resize;
 
_hostLoadComplete = true;
}
 
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///

public void Dispose()
{
if (_host != null)
{
_host.MouseDown -= _host_MouseDown;
_host.MouseMove -= _host_MouseMove;
_host.MouseUp -= _host_MouseUp;
_host.Paint -= _host_Paint;
_host.Resize -= _host_Resize;
}
}
}
GeneralRe: Managed versionmemberMohammed Abd Alla7 May '07 - 12:13 
thnx eisernwolf , i orginally wrote this class in managed code ,
but i wanted more scroll speed especially for large images.
 
but u r right , rotating the image doesn't update the sourcehdc which leads to
incorrect results.
GeneralRe: Managed versionmemberTomPaq25 Sep '09 - 15:19 
Thanks for your article and code. They saved me a lot of time. I want to send back to you a version that has the following improvements:
 
1. Changed your control to inherit UserControl so we get all the bells and whistles that go along with that - such as:
a. Dispose using an overloads
b. Capability to drop it into a Form from the VS ToolBox.
c. Use of UserControl's OnPaint, OnMouseWheel, etc
2. Rotation using the Image.RotateFlip method and resetting SourceHDC.
3. MouseWheel for Zoom. I needed right/left click for other reasons so the MouseWheel zoom was necessary. I think it is a superior UI for zooming.
4. I didn't see any memory leaks, but I would like to hear from anyone who finds any.
5. I tested eisernwolf's version of managed code for Drawing (ported his C# version to VB) - it is in the code below as the DrawImageWithoutAPI method). It is not as fast/smooth as your original version, so I won't use it unless someone can figure out how to make it as fast.
 
Again, Mohammed, many thanks for your code!
Tom
 
-------------------------------------------------
 
Imports System.Drawing
 
Public Class ImageViewer
Inherits UserControl
 

#Region " Properties & Declarations "
 
Private Gr As Graphics
 
Public Property Image() As Bitmap
Get
Return _Image
End Get
Set(ByVal Value As Bitmap)
If _Image IsNot Value Then
_Image = Value
_ZoomFactor = 1
_OldZoomFactor = 1
Me.Invalidate()
End If
End Set
End Property
Private _Image As Bitmap
 
' Current, minimum, maximum, previous zoom (bigger means zoom in)
Public Property CurrentZoom() As Single
Get
Return _ZoomFactor
End Get
Set(ByVal Value As Single)
_ZoomFactor = Value
DrawImage(Me.Width / 2, Me.Height / 2)
End Set
End Property
Private _ZoomFactor As Single = 1
Private _OldZoomFactor As Single = 1
 
Public Property MinZoom() As Single
Get
Return _MinZoom
End Get
Set(ByVal Value As Single)
_MinZoom = Value
End Set
End Property
Private _MinZoom As Single = 0.05
 
Public Property MaxZoom() As Single
Get
Return _MaxZoom
End Get
Set(ByVal Value As Single)
_MaxZoom = Value
End Set
End Property
Private _MaxZoom As Single = 20
 
' Handles
Private _SourceHDC As IntPtr = Nothing
Private _DestinationHDC As IntPtr = Nothing
 
' Saved Points
Private _MouseDown As PointF
Private _SavedMouseDown As PointF
 
' Main rectangle , Boundary rectangle
Private _MainRectF As RectangleF
Private _BoundaryRectF As RectangleF
 
' Boolean
Private _XOut As Boolean = False
Private _YOut As Boolean = False
Private _DownPress As Boolean = False
 
#End Region
 

#Region " Dispose Overloads "
 
Public Overloads Sub Dispose()
 
If Not IsNothing(_Image) Then
_Image.Dispose()
_Image = Nothing
End If
 
If Not _SourceHDC.Equals(IntPtr.Zero) Then
DeleteDC(_SourceHDC)
_SourceHDC = Nothing
End If
 
If Not IsNothing(Gr) Then
Gr.Dispose()
Gr = Nothing
End If
 
GC.Collect()
 
End Sub
 
#End Region
 

#Region " Overriden Events "
 
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
 
MyBase.OnPaint(e)
 
DrawImage(0, 0)
 
End Sub
 

Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
 
MyBase.OnResize(e)
 
If _Image Is Nothing Then Exit Sub
 
DrawImage(0, 0)
 
End Sub
 

Protected Overrides Sub OnMouseUp(ByVal e As System.Windows.Forms.MouseEventArgs)
 
MyBase.OnMouseUp(e)
 
If _Image Is Nothing Then Exit Sub
 
_DownPress = False
Me.Cursor = Cursors.Arrow
 
If _SavedMouseDown.X = e.X And _SavedMouseDown.Y = e.Y Then
 
If e.Button = MouseButtons.Left Then
If _ZoomFactor > _MaxZoom Then Exit Sub
_OldZoomFactor = _ZoomFactor
_ZoomFactor = _ZoomFactor * 1.3
ElseIf e.Button = MouseButtons.Right Then
If _ZoomFactor < _MinZoom Then Exit Sub
_OldZoomFactor = _ZoomFactor
_ZoomFactor = _ZoomFactor / 1.3
End If
 
DrawImage(e.X, e.Y)
RaiseEvent ZoomChanged(_ZoomFactor)
 
End If
 
End Sub
 

Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)
 
MyBase.OnMouseDown(e)
 
If _Image Is Nothing Then Exit Sub
 
_MouseDown.X = e.X
_MouseDown.Y = e.Y
_SavedMouseDown.X = e.X
_SavedMouseDown.Y = e.Y
 
_DownPress = True
 
End Sub
 

Protected Overrides Sub OnMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs)
 
MyBase.OnMouseMove(e)
 
If _Image Is Nothing Then Exit Sub
 
If _DownPress = True Then
Me.Cursor = Cursors.NoMove2D
 
' Accelerated scrolling when right click drag
Dim cp As New PointF(0, 0)
If e.Button = MouseButtons.Right Then
cp.X = (_MouseDown.X - e.X) * (_Image.Width / 2000)
cp.Y = (_MouseDown.Y - e.Y) * (_Image.Height / 2000)
End If
 
_MainRectF.X = ((_MouseDown.X - e.X) / _ZoomFactor) + _MainRectF.X + cp.X
_MainRectF.Y = ((_MouseDown.Y - e.Y) / _ZoomFactor) + _MainRectF.Y + cp.Y
DrawImage(0, 0)
 
If _XOut = False Then _MouseDown.X = e.X
 
If _YOut = False Then _MouseDown.Y = e.Y
 
End If
 
RaiseEvent MoveOver((e.X - _BoundaryRectF.X) / _ZoomFactor + _MainRectF.X, (e.Y - _BoundaryRectF.Y) / _ZoomFactor + _MainRectF.Y)
 
End Sub
 

Protected Overrides Sub OnMouseWheel(ByVal e As System.Windows.Forms.MouseEventArgs)
 
MyBase.OnMouseWheel(e)
 
If _Image Is Nothing Then Exit Sub
 
Me.Cursor = Cursors.Arrow
 
If e.Delta >= 0 Then
If _ZoomFactor > _MaxZoom Then Exit Sub
_OldZoomFactor = _ZoomFactor
_ZoomFactor = _ZoomFactor * 1.3
ElseIf e.Delta < 0 Then
If _ZoomFactor < _MinZoom Then Exit Sub
_OldZoomFactor = _ZoomFactor
_ZoomFactor = _ZoomFactor / 1.3
End If
 
DrawImage(e.X, e.Y)
RaiseEvent ZoomChanged(_ZoomFactor)
 
End Sub
 
#End Region
 

#Region " Draw Image "
 
Private Sub DrawImage(ByVal zoomX As Single, ByVal zoomY As Single)
 
If _Image Is Nothing Then Exit Sub
 
If _SourceHDC.Equals(IntPtr.Zero) Then
_SourceHDC = CreateCompatibleDC(IntPtr.Zero)
SelectObject(_SourceHDC, _Image.GetHbitmap())
End If
 
If _DestinationHDC.Equals(IntPtr.Zero) Then
If IsNothing(Gr) Then
Gr = Me.CreateGraphics
End If
_DestinationHDC = Gr.GetHdc()
SetStretchBltMode(_DestinationHDC, 3)
End If
 
_XOut = False
_YOut = False
 
If Me.Width > _Image.Width * _ZoomFactor Then
_MainRectF.X = 0
_MainRectF.Width = _Image.Width
_BoundaryRectF.X = (Me.Width - _Image.Width * _ZoomFactor) / 2
_BoundaryRectF.Width = _Image.Width * _ZoomFactor
 
BitBlt(_DestinationHDC, 0, 0, _BoundaryRectF.X, Me.Height, _SourceHDC, 0, 0, TernaryRasterOperations.BLACKNESS)
BitBlt(_DestinationHDC, _BoundaryRectF.Right, 0, _BoundaryRectF.X, Me.Height, _SourceHDC, 0, 0, TernaryRasterOperations.BLACKNESS)
Else
_MainRectF.X = _MainRectF.X + ((Me.Width / _OldZoomFactor - Me.Width / _ZoomFactor) / ((Me.Width + 0.001) / zoomX))
_MainRectF.Width = Me.Width / _ZoomFactor
_BoundaryRectF.X = 0
_BoundaryRectF.Width = Me.Width
End If
 
If Me.Height > _Image.Height * _ZoomFactor Then
_MainRectF.Y = 0
_MainRectF.Height = _Image.Height
_BoundaryRectF.Y = (Me.Height - _Image.Height * _ZoomFactor) / 2
_BoundaryRectF.Height = _Image.Height * _ZoomFactor
 
BitBlt(_DestinationHDC, 0, 0, Me.Width, _BoundaryRectF.Y, _SourceHDC, 0, 0, TernaryRasterOperations.BLACKNESS)
BitBlt(_DestinationHDC, 0, _BoundaryRectF.Bottom, Me.Width, _BoundaryRectF.Y, _SourceHDC, 0, 0, TernaryRasterOperations.BLACKNESS)
Else
_MainRectF.Y = _MainRectF.Y + ((Me.Height / _OldZoomFactor - Me.Height / _ZoomFactor) / ((Me.Height + 0.001) / zoomY))
_MainRectF.Height = Me.Height / _ZoomFactor
_BoundaryRectF.Y = 0
_BoundaryRectF.Height = Me.Height
End If
 
_OldZoomFactor = _ZoomFactor
 
If _MainRectF.Right > _Image.Width Then
_XOut = True
_MainRectF.X = (_Image.Width - _MainRectF.Width)
End If
 
If _MainRectF.X < 0 Then
_XOut = True
_MainRectF.X = 0
End If
 
If _MainRectF.Bottom > _Image.Height Then
_YOut = True
_MainRectF.Y = (_Image.Height - _MainRectF.Height)
End If
 
If _MainRectF.Y < 0 Then
_YOut = True
_MainRectF.Y = 0
End If
 
StretchBlt(_DestinationHDC, _BoundaryRectF.X, _BoundaryRectF.Y, _BoundaryRectF.Width, _BoundaryRectF.Height, _
_SourceHDC, _MainRectF.X, _MainRectF.Y, _MainRectF.Width, _MainRectF.Height, _
TernaryRasterOperations.SRCCOPY)
 
Gr.ReleaseHdc(_DestinationHDC)
_DestinationHDC = Nothing
 
End Sub
 
' Without the API: Noticeably less smooth
Private Sub DrawImageWithoutAPI(ByVal zoomX As Single, ByVal zoomY As Single)
 
If _Image Is Nothing Then Exit Sub
 
_XOut = False
_YOut = False
 
If Me.Width > (_Image.Width * _ZoomFactor) Then
 
_MainRectF.X = 0
_MainRectF.Width = _Image.Width
_BoundaryRectF.X = (Me.Width - _Image.Width * _ZoomFactor) / 2
_BoundaryRectF.Width = _Image.Width * _ZoomFactor
 
Else
 
_MainRectF.X = _MainRectF.X + ((Me.Width / _OldZoomFactor - Me.Width / _ZoomFactor) / ((Me.Width + 0.001F) / zoomX))
_MainRectF.Width = Me.Width / _ZoomFactor
_BoundaryRectF.X = 0
_BoundaryRectF.Width = Me.Width
 
End If
 
If (Me.Height > _Image.Height * _ZoomFactor) Then
 
_MainRectF.Y = 0
_MainRectF.Height = _Image.Height
_BoundaryRectF.Y = (Me.Height - _Image.Height * _ZoomFactor) / 2
_BoundaryRectF.Height = _Image.Height * _ZoomFactor
 
Else
 
_MainRectF.Y = _MainRectF.Y + ((Me.Height / _OldZoomFactor - Me.Height / _ZoomFactor) / ((Me.Height + 0.001F) / zoomY))
_MainRectF.Height = Me.Height / _ZoomFactor
_BoundaryRectF.Y = 0
_BoundaryRectF.Height = Me.Height
 
End If
 
_OldZoomFactor = _ZoomFactor
 
If (_MainRectF.Right > _Image.Width) Then
_XOut = True
_MainRectF.X = _Image.Width - _MainRectF.Width
End If
 
If (_MainRectF.X < 0) Then
_XOut = True
_MainRectF.X = 0
End If
 
If (_MainRectF.Bottom > _Image.Height) Then
_YOut = True
_MainRectF.Y = _Image.Height - _MainRectF.Height
End If
 
If (_MainRectF.Y < 0) Then
_YOut = True
_MainRectF.Y = 0
End If
 
Using g As Graphics = Me.CreateGraphics()
 
Dim bRecX As Integer = CInt(_BoundaryRectF.X)
Dim bRecY As Integer = CInt(_BoundaryRectF.Y)
Dim bRecWidth As Integer = CInt(_BoundaryRectF.Width)
Dim bRecHeight As Integer = CInt(_BoundaryRectF.Height)
 
Dim mRecX As Integer = CInt(_MainRectF.X)
Dim mRecY As Integer = CInt(_MainRectF.Y)
Dim mRecWidth As Integer = CInt(_MainRectF.Width)
Dim mRecHeight As Integer = CInt(_MainRectF.Height)
 
g.DrawImage(_Image, New Rectangle(bRecX, bRecY, bRecWidth, bRecHeight), New Rectangle(mRecX, mRecY, mRecWidth, mRecHeight), GraphicsUnit.Pixel)
 
Dim leftRectWidth As Integer = bRecX - Me.Left
Dim topRectHeight As Integer = bRecY - Me.Top
Dim topRectWidth As Integer = Me.Width - leftRectWidth
Dim rightRectLeft As Integer = Me.Left + leftRectWidth + bRecWidth
 
g.FillRectangle(Brushes.Black, New Rectangle(0, 0, leftRectWidth, Me.Height))
g.FillRectangle(Brushes.Black, New Rectangle(leftRectWidth, 0, topRectWidth, topRectHeight))
g.FillRectangle(Brushes.Black, Rectangle.FromLTRB(rightRectLeft, topRectHeight, Me.Right, Me.Bottom))
g.FillRectangle(Brushes.Black, Rectangle.FromLTRB(leftRectWidth, Me.Top + topRectHeight + bRecHeight, rightRectLeft, Me.Bottom))
 
End Using
 
End Sub
 

#End Region
 

#Region " Rotate Image & Save Image "
 
' Parameter: Rotation can be 90°, 180° or 270°
Public Sub RotateImage(ByVal rotate As String)
 
If _Image Is Nothing Then Exit Sub
 
Dim rt As RotateFlipType
If rotate = "90°" Then
rt = RotateFlipType.Rotate90FlipXY
ElseIf rotate = "180°" Then
rt = RotateFlipType.Rotate180FlipXY
ElseIf rotate = "270°" Then
rt = RotateFlipType.Rotate270FlipXY
Else
Exit Sub
End If
 
_Image.RotateFlip(rt)
 
_SourceHDC = Nothing
_DestinationHDC = Nothing
 
Me.Invalidate()
 
End Sub
 
Public Sub SaveImage(ByVal img As Bitmap)
 
' Displays a SaveFileDialog so the user can save the Image
Dim saveFileDialog1 As New SaveFileDialog()
saveFileDialog1.Filter = "JPeg Image|*.jpg|Bitmap Image|*.bmp|Gif Image|*.gif"
saveFileDialog1.Title = "Save Image File"
saveFileDialog1.ShowDialog()
 
' If the file name is not an empty string open it for saving.
If saveFileDialog1.FileName <> "" Then
' Saves the Image via a FileStream created by the OpenFile method.
Dim fs As System.IO.FileStream = CType _
(saveFileDialog1.OpenFile(), System.IO.FileStream)
' Saves the Image in the appropriate ImageFormat based upon the file type selected in the dialog box.
' NOTE that the FilterIndex property is one-based.
Select Case saveFileDialog1.FilterIndex
Case 1
img.Save(fs, System.Drawing.Imaging.ImageFormat.Jpeg)
Case 2
img.Save(fs, System.Drawing.Imaging.ImageFormat.Bmp)
Case 3
img.Save(fs, System.Drawing.Imaging.ImageFormat.Gif)
End Select
fs.Close()
fs.Dispose()
End If
 
End Sub
 
#End Region
 

#Region " Raised Events "
 
Public Event MoveOver(ByVal Px As Single, ByVal Py As Single)
Public Event ZoomChanged(ByVal CurZoom As Single)
 
#End Region
 

End Class
GeneralExcellent!memberNatreen11 Apr '07 - 8:15 
Very well done, and many thanks for sharing.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130516.1 | Last Updated 10 Dec 2007
Article Copyright 2007 by Mohammed Abd Alla
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid