Introduction
Some time ago, I needed to have two picturebox controls on one form. One larger picturebox shows an image, while the other zooms a portion of the same image, depending on the position of the cursor. I could not find a ready-made solution or example source code for it, so I struggled a bit and got it to work properly, without flickering or lagging. I hope some of you can use this or find it helpful.
The demo project
The downloadable demo project contains all the source code needed, and I suggest you download it first. This article is not a step-by-step instruction, but rather focuses on the core parts to achieve the goal.
Definition of the problems
There were a couple of problems I had to solve to get to the solution.
- How to load an image into a fixed size
PictureBox
control, without changing the proportions of the original image.
- How to center this resized image in the
PictureBox
control.
- How to copy a part of the image to the second
PictureBox
control and zoom it in using a variable zoom factor.
Solutions
The first problem I encountered was to load an image of any size into the PictureBox
control, without changing the dimensions of the PictureBox
control and without changing the proportions of the image.
This might seem straightforward at first sight, by setting the SizeMode
property of the main PictureBox
control to SizeMode.CenterImage
, but that creates a more complex new problem. The image will be displayed the way I would like it to, but that also means I have to calculate the exact position of the mouse cursor relative to the image, each time the mouse moves over the image. I wanted the mouse position to match the image position, even if the mouse is outside of the edge of the image.
To solve this, I decided to copy the loaded image to a temporary bitmap which is the same size as the PictureBox
control, filling any area that is not covered by the image with a given color. The ResizeAndDisplayImage()
method below shows you how I achieved it.
private void ResizeAndDisplayImage()
{
picImage.BackColor = _BackColor;
picZoom.BackColor = _BackColor;
if (_OriginalImage == null)
return;
int sourceWidth = _OriginalImage.Width;
int sourceHeight = _OriginalImage.Height;
int targetWidth;
int targetHeight;
double ratio;
if (sourceWidth > sourceHeight)
{
targetWidth = picImage.Width;
ratio = (double)targetWidth / sourceWidth;
targetHeight = (int)(ratio * sourceHeight);
}
else if (sourceWidth < sourceHeight)
{
targetHeight = picImage.Height;
ratio = (double)targetHeight / sourceHeight;
targetWidth = (int)(ratio * sourceWidth);
}
else
{
targetHeight = picImage.Height;
targetWidth = picImage.Width;
}
int targetTop = (picImage.Height - targetHeight) / 2;
int targetLeft = (picImage.Width - targetWidth) / 2;
Bitmap tempBitmap = new Bitmap(picImage.Width, picImage.Height,
PixelFormat.Format24bppRgb);
tempBitmap.SetResolution(_OriginalImage.HorizontalResolution,
_OriginalImage.VerticalResolution);
Graphics bmGraphics = Graphics.FromImage(tempBitmap);
bmGraphics.Clear(_BackColor);
bmGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
bmGraphics.DrawImage(_OriginalImage,
new Rectangle(targetLeft, targetTop, targetWidth, targetHeight),
new Rectangle(0, 0, sourceWidth, sourceHeight),
GraphicsUnit.Pixel);
bmGraphics.Dispose();
picImage.Image = tempBitmap;
}
This method is called after the user selects an image to load from file. It first resizes the image using the maximum size that can be fit inside the PictureBox
control, and fills the areas that are not covered by the image with the selected background color.
The method then calculates a top and left position to center the image, and copies it to a temporary bitmap, which is used as the image in the PictureBox
control. The image is now shown in the PictureBox
, centered, and resized to fit.
The next problem was to capture a part of the image when the mouse moves over it, and zoom it to display it in the other smaller PictureBox
.
The method UpdateZoomedImage(MouseEventArgs e)
, which is called when the mouse moves over the larger PictureBox
control, handles this problem.
private void UpdateZoomedImage(MouseEventArgs e)
{
int zoomWidth = picZoom.Width / _ZoomFactor;
int zoomHeight = picZoom.Height / _ZoomFactor;
int halfWidth = zoomWidth / 2;
int halfHeight = zoomHeight / 2;
Bitmap tempBitmap = new Bitmap(zoomWidth, zoomHeight,
PixelFormat.Format24bppRgb);
Graphics bmGraphics = Graphics.FromImage(tempBitmap);
bmGraphics.Clear(_BackColor);
bmGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
bmGraphics.DrawImage(picImage.Image,
new Rectangle(0, 0, zoomWidth, zoomHeight),
new Rectangle(e.X - halfWidth, e.Y - halfHeight,
zoomWidth, zoomHeight), GraphicsUnit.Pixel);
picZoom.Image = tempBitmap;
bmGraphics.DrawLine(Pens.Black, halfWidth + 1,
halfHeight - 4, halfWidth + 1, halfHeight - 1);
bmGraphics.DrawLine(Pens.Black, halfWidth + 1, halfHeight + 6,
halfWidth + 1, halfHeight + 3);
bmGraphics.DrawLine(Pens.Black, halfWidth - 4, halfHeight + 1,
halfWidth - 1, halfHeight + 1);
bmGraphics.DrawLine(Pens.Black, halfWidth + 6, halfHeight + 1,
halfWidth + 3, halfHeight + 1);
bmGraphics.Dispose();
picZoom.Refresh();
}
This method first calculates the size of the portion of the image we want to capture, depending on the current zoom factor. It then creates a temporary bitmap, and copies the portion of the image to it. The SizeMode
property of the smaller PictureBox
is set to SizeMode.StretchImage
to simulate zooming. The larger the zoom factor, the smaller the portion copied, and therefore, the more zoomed in a stretched image will look.
Finally, a crosshair is drawn on the zoomed image to copy the mouse cursor position in the original image.
Remarks
For better functionality, I had to fine tune some settings and properties. To remove some flickering in the zoomed image, I set the DoubleBuffered
property of the form to true
.
Both of the PictureBox
controls are square, i.e., they have the same width and height. If you want to change this, you will have to adjust some of the code, particularly the methods shown above. Also, I chose a width and height of 120 for the smaller PictureBox
control for a reason: 120 is divisible by 2, 3, 4, 5, and 6. This happens to be the possible values of the zoom factor in my demo project. This ensures correct positioning of the crosshair cursor, which is based on a calculation that uses division. A division with round-off errors might position the crosshair just off the correct position.