Have C# forms application with PictureBox embedded in Panel, to take advantage of Panel AutoScroll as suggested in other posts when image and thus PictureBox need to be scrolled horizontally or vertically. Want to zoom the image and recompute AutoScrollPosition to keep same Point visible after zooming. Can double size of PictureBox, then recopy source image, accomplishing zoom. But AutoScrollPosition remains unchanged, thus what was visible before zoom has moved off screen.
How to recompute AutoScrollPosition to keep image focus after zoom?
There are three typical types of zooming:
zoom into the center, triggered by zoom buttons
zoom into the mouse position, triggered by clicking or scroll-wheeling
zoom into a rectangle, by drawing a rectangle
I assume the typical setup: A PictureBox set to SizeMode=Zoom nested in a Panel with AutoScroll=true and zooming that takes care to keep the aspect ratios of Image and PictureBox equal.
Let's start by introducing terminology:
There is an Image we call bitmap and
it is displayed by a PictureBox; let's call it canvas..
.. which is nested in a Panel we call frame
User-friendly zooming needs a fixed point, that is a point that shall stay put.
For 1) it is the center of the frame, for 2) it is the mouse location and for 3) it is the center of the rectangle.
Before zooming we calculate the old zoom ratio, the fixed point in the frame, the fixed point in the canvas and finally the fixed point in the bitmap.
After zoming we calculate the new zoom ratio and the new fixed point in the canvas. Finally we use it to move the canvas to bring the fixed canvas point to the fixed frame point.
Here is an example for zooming into the (current) center; it is a common click event for two buttons and it only doubles and halves the zoom ratio.
Much finer grained factors are of course simple to implement; even better is a fixed list of zoom levels, like Photoshop has!
private void zoom_Click(object sender, EventArgs e)
{
PictureBox canvas = pictureBox1;
Panel frame = panel1;
// Set new zoom level, depending on the button
float zoom = sender == btn_ZoomIn ? 2f : 0.5f;
// calculate old ratio:
float ratio = 1f * canvas.ClientSize.Width / canvas.Image.Width;
// calculate frame fixed pixel:
Point fFix = new Point( frame.Width / 2, frame.Height / 2);
// calculate the canvas fixed pixel:
Point cFix = new Point(-canvas.Left + fFix.X, -canvas.Top + fFix.Y );
// calculate the bitmap fixed pixel:
Point iFix = new Point((int)(cFix.X / ratio),(int)( cFix.Y / ratio));
// do the zoom
canvas.Size = new Size( (int)(canvas.Width * zoom), (int)(canvas.Height * zoom) );
// calculate new ratio:
float ratio2 = 1f * canvas.ClientSize.Width / canvas.Image.Width;
// calculate the new canvas fixed pixel:
Point cFix2 = new Point((int)(iFix.X * ratio2),(int)( iFix.Y * ratio2));
// move the canvas:
canvas.Location = new Point(-cFix2.X + fFix.X, -cFix2.Y + fFix.Y);
}
Note that while one can try to restore the relative AutoScrollValues this is not only hard, because their values are a little quirky but it is also won't be adaptable to the other zoom types.
Related
Can anyone help me on this?
I have a picture box with a image and this image have some coordinates.
My X starts at 60 and end at 135
My Y stats at 75 and end at 120
Because i have only the first and the last point, i want to calculate and see the coordinates when i mouse over my image.
I started with solving my first problem: i have to delimitate my start and my end.
So i tried a trackbar.
Im trying first get the current X position:
Set my picturebox at position x = 0;
Set my trackbar at position x = -10, so my first pin will be at position 0;
Set my tracbar size.x = picturebox.x + 20, so my last pin will be at end of picture box.
My trackbar have the current properties:
Minimum = 60, Maximum = 135;
Set a mouse move event in my picturebox:
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
double dblValue;
dblValue = ((double)e.X/ (double)trackBar1.Width) * (trackBar1.Maximum - trackBar1.Minimum);
dblValue = dblValue + 60;
trackBar1.Value = Convert.ToInt32(dblValue);
lblX.Text = dblValue.ToString();
}
It's almost working, but still not very accurate.
Anyone have some idea that may work?
I'm not exactly sure what it is you are trying to do, but if you are trying to get to the coordinates within the picturebox, there is a function on the pictureBox class called PointToClient(Point) that computes the location of the specified screen point into client coordinates. You can use the X and Y coordinates from the MouseEventArgs to create the Point object to pass to the function.
To clarify:
The X and Y properties of the MouseEventArgs in the MouseMove event are the screen coordinates (0,0) being the upper left corner of the screen.
Many controls like the PictureBox control include a PointToClient method that will convert the screen coordinates to the local control's coordinates, where (0,0) will be the upper left corner of the control.
So, for example, if your control is placed on the screen at location (60, 75) and has a bottom right coordinate of (135, 120). If your mouse is over the control, and is 10 pixels from the left and 20 pixels from the top, then the X and Y properties of the MouseEventArgs in the MouseMove event would be: X = 70 and Y = 95. If you convert these to the internal coordinates of the picturebox control using PointToClient, it will indicate that X = 10 and Y = 20.
Now, if you were wanting to have a TrackBar that displays a relative indication of where the X coordinate of the mouse is over some control, you would calculate it as follows:
// Set the minimum and maximum of the trackbar to 0 and 100 for a simple percentage.
trackBar1.Minimum = 0;
trackBar1.Maximum = 100;
// In the pictureBox1_MouseMove event have the following code:
trackBar1.Value = pictureBox1.PointToClient(new Point(e.X, e.Y)).X * 100 / pictureBox1.Width;
If you wanted to have the trackbar use screen coordinates to track the relative position of the X coordinate of the mouse over some control, you would calculate it as follows:
// Set the minimum and maximum values of the track bar to the screen coordinates of the
// control we want to track.
trackBar1.Minimum = pictureBox1.PointToScreen(0,0).X;
trackBar1.Maximum = pictureBox1.PointToScreen(pictureBox1.Width, 0).X;
// In the pictureBox1_MouseMove event have the following code:
trackBar1.Value = e.X;
If you wanted to have the trackbar use the internal coordinates of some control to track the internal position of the X coordinate of the mouse over that control, you would calculate it as follows:
// Set the minimum and maximum values of the track bar to zero and the width of the
// control we want to track.
trackBar1.Minimum = 0;
trackBar1.Maximum = pictureBox1.Width;
// In the pictureBox1_MouseMove event have the following code:
trackBar1.Value = pictureBox1.PointToClient(new Point(e.X, e.Y)).X;
// or - not recommended - read below.
trackBar1.Value = e.X - pictureBox1.Left;
Now, there is one caveat, and that is if you put controls inside other controls, like a panel inside a panel inside a panel, etc. then the 'world' coordinates of a control inside of another control are based upon their location within the parent control. That is why it is a good idea to use the internal coordinates of the control via PointToClient and screen coordinates from internal coordinates via PointToScreen because otherwise you would have to work your way up through all of the containers until you reached the screen, keeping track of Top and Left coordinates all the way.
I hope this answers your question.
I have a PictureBox inside a Panel in order to zoom and pan. I created the possibility to select 4 points with the mouse click and draw a rectangle on the PictureBox. Once the rectangle is over my picture I pass the coordinates of the rectangle to the method "cropRectangle". This method crops the rectangle and replace the old image with the cropped one. This works very well:
(OriginalImage is the bitmap of the actual image in the pictureBox)
private void cropRectangle(Rectangle rect){
double left = (rect.X) * originalImage.Width / pictureBox.Width,
top = (rect.Y) * originalImage.Width / pictureBox.Height,
right = (rect.Width) * originalImage.Width / pictureBox.Width,
bottom = (rect.Height) * originalImage.Height / pictureBox.Height;
rect = new Rectangle (Convert.ToInt32(left), Convert.ToInt32(top), Convert.ToInt32(right), Convert.ToInt32(bottom));
Bitmap bitmap = orignalImage.Clone(rect, originalImage.PixelFormat);
pictureBox.Image = (Image)bitmap;
centerPictureBox();
// fit image into pictureBox with respect to the ratio
float ratio = orignalImage.Width / orignalImage.Height;
pictureBox.Width = panel.Width;
pictureBox.Height = Convert.ToInt32(pictureBox.Width * ratio);
centerPictureBox();
}
What I am trying to do now is to zoom the selected area instead to crop it. The rectangle of the picturebox has to match with the panel.
How can I show only the selected area (rectangle) of the picturebox through the panel without cropping the image?
You should stick with modifying the existing Bitmap using the Graphics object instead of changing the size of the PictureBox. You don't want to be tied to a UI control when the desired functionality is already available elsewhere.
Here are rough steps to achieve that:
Create a temporary Bitmap object that will store the zoomed image. Bitmap tBitmap = new Bitmap(zoomX, zoomY, PixelFormat.Format24bppRgb);
Calculate the zoom factors and stuff like you already do (I didn't check if the code is correct but I assume that it is) when you want to zoom.
Create a new Graphics object from the temporary bitmap. Graphics graphics = Graphics.FromImage(tBitmap);
Set the InterpolationMode so that the image is scaled with a good quality. graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
Draw the zoomed image using the DrawImage method (using the original image from the PictureBox). graphics.DrawImage(pictureBox.Image, new Rectangle(0, 0, pictureBox.Width, pictureBox.Height), new Rectangle(/*The crop rectangle you draw already*/), GraphicsUnit.Pixel);
Set the newly drawn bitmap as the Image in your PictureBox. pictureBox.Image = tBitmap;
Remember to dispose of the graphics object we used for drawing. graphics.Dispose();
You might need to refresh the PictureBox to force it to redraw itself. pictureBox.Refresh();
Those are the basic steps to follow. I didn't have time to go through your existing code that deeply so you might need to change some additional things to make it work.
Here's also an MSDN article that covers the same stuff: Cropping and Scaling Images in GDI+
You might be interested in this control (ZoomPicBox) which allows for zooming and panning a picture box.
All credit for this code is Bob Powell and it was taken from his site (which seems to be down now and has been for a long time now.).
I copied the code from archive.org at this link:
https://web.archive.org/web/20080313161349/http://www.bobpowell.net/zoompicbox.htm
That link has additional information and is worth a read. The code is available in VB.Net as well.
I don't know why Bob Powell's site is down, but it was a great site for Windows Graphics information.
I felt this code was worth repeating. This control can be dragged onto the form.
namespace bobpowell.net
{
/// <summary>
/// ZoomPicBox does what it says on the wrapper.
/// </summary>
/// <remarks>
/// PictureBox doesn't lend itself well to overriding. Why not start with something basic and do the job properly?
/// </remarks>
public class ZoomPicBox : ScrollableControl
{
Image _image;
[
Category("Appearance"),
Description("The image to be displayed")
]
public Image Image
{
get{return _image;}
set
{
_image=value;
UpdateScaleFactor();
Invalidate();
}
}
float _zoom=1.0f;
[
Category("Appearance"),
Description("The zoom factor. Less than 1 to reduce. More than 1 to magnify.")
]
public float Zoom
{
get{return _zoom;}
set
{
if(value<0 || value<0.00001)
value=0.00001f;
_zoom=value;
UpdateScaleFactor();
Invalidate();
}
}
/// <summary>
/// Calculates the effective size of the image
///after zooming and updates the AutoScrollSize accordingly
/// </summary>
private void UpdateScaleFactor()
{
if(_image==null)
this.AutoScrollMinSize=this.Size;
else
{
this.AutoScrollMinSize=new Size(
(int)(this._image.Width*_zoom+0.5f),
(int)(this._image.Height*_zoom+0.5f)
);
}
}
InterpolationMode _interpolationMode=InterpolationMode.High;
[
Category("Appearance"),
Description("The interpolation mode used to smooth the drawing")
]
public InterpolationMode InterpolationMode
{
get{return _interpolationMode;}
set{_interpolationMode=value;}
}
protected override void OnPaintBackground(PaintEventArgs pevent)
{
// do nothing.
}
protected override void OnPaint(PaintEventArgs e)
{
//if no image, don't bother
if(_image==null)
{
base.OnPaintBackground(e);
return;
}
//Set up a zoom matrix
Matrix mx=new Matrix(_zoom,0,0,_zoom,0,0);
//now translate the matrix into position for the scrollbars
mx.Translate(this.AutoScrollPosition.X / _zoom, this.AutoScrollPosition.Y / _zoom);
//use the transform
e.Graphics.Transform=mx;
//and the desired interpolation mode
e.Graphics.InterpolationMode=_interpolationMode;
//Draw the image ignoring the images resolution settings.
e.Graphics.DrawImage(_image,new Rectangle(0,0,this._image.Width,this._image.Height),0,0,_image.Width, _image.Height,GraphicsUnit.Pixel);
base.OnPaint (e);
}
public ZoomPicBox()
{
//Double buffer the control
this.SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.ResizeRedraw |
ControlStyles.UserPaint |
ControlStyles.DoubleBuffer, true);
this.AutoScroll=true;
}
}
}
I have found a elegant solution to my problem:
private void zoomInsideRectangle(Rectangle rect){
float zoomFactor = ((float)panel.Width / rect.Width) - 1;
pictureBox.Width = pictureBox.Width + convertToIntPerfect(pictureBox.Width * zoomFactor);
pictureBox.Height = pictureBox.Height + convertToIntPerfect(pictureBox.Height * zoomFactor);
rect.X = rect.X + convertToIntPerfect(rect.X * zoomFactor);
rect.Y = rect.Y + convertToIntPerfect(rect.Y * zoomFactor);
pictureBox.Left = convertToIntPerfect(-rect.X);
pictureBox.Top = convertToIntPerfect(-rect.Y);
}
Since I know the length of the panel where I can see the picturebox. I take the ratio of the panel and the width of my rectangle that I want to zoom in. This ratio is my zoomratio.
I multiply the size of the picturebox with the ratio that I calculated.
I anchor the picturebox by the left and top with the coordinates of my rectangle. But right before doing that I have to multiply my coordinates of my rectangle with the zoomratio since I changed the size of the picturebox.
I didn't implemented the Y transformation since the original ratio of the image will be damaged.
I am using the WriteableBitmapEx extension method to rotate a WriteableBitmap. bi in the code is a WritableBitmap. The RotateFree method rotates the bitmap in any degree and returns a new rotated WritableBitmap. My code:
private void rotate()
{
degree += 1;
var rotate = bi.RotateFree(degree);
ImageControl.Source = rotate;
}
My problem is since the ImageControl size is fixed, it causes the rotated bitmap to be clipped. So what is the best way to prevent this? I guess I am looking for a way to resize the ImageControl during rotation to prevent clipping. Any suggestions?
UPDATE
Based on this useful info Calculate rotated rectangle size from known bounding box coordinates I think I managed to calculate the bounding box width (bx) and height(by) and resize it accordingly during the rotation
double radian = (degree / 180.0) * Math.PI;
double bx = rotate.PixelWidth * Math.Cos(radian) + rotate.PixelHeight * Math.Sin(radian);
double by = rotate.PixelWidth * Math.Sin(radian) + rotate.PixelHeight * Math.Cos(radian);
While it appears that the ImageControl width and height increases/decreases during rotation, the image is still being clipped.
UPDATE 2
Based on #Rene suggestion, I managed to prevent the clipping. Combined with the ImageControl Width/Height calculation, the image size is retained during rotation by also setting its stretch property to NONE.
The issue now is to make sure the ImageControl resize from its center so that it does not appear moving. I can include a sample project if anyone interested
UPDATE 3
For those who might be interested the final solution. This is how I do it. The result is, the image is rotated without clipping and its size is retained during rotation. In addition, the rotation appears to originate from the center.
To adjust the ImageControl position as it's resizing so that the rotation appears to originated from center, I use this code.
var translationDelta = new Point((ImageControl.ActualWidth - bx) / 2.0, (ImageControl.ActualHeight - by) / 2.0);
UpdateImagePosition(translationDelta);
ApplyPosition();
// This code update the ImageControl position on the canvas
public void UpdateImagePosition(Point delta)
{
var newPosition = new Point(ImagePosition.X + delta.X, ImagePosition.Y + delta.Y);
ImagePosition = newPosition;
}
//This apply the new position to make the ImageControl rotate from center
public void ApplyPosition()
{
ObjComposite.TranslateX = ImagePosition.X;
ObjComposite.TranslateY = ImagePosition.Y;
}
Use RotateFree with the crop parameter set to false: RotateFree(degree, false). Also set the ImageControl Stretch property to Uniform: Stretch="Uniform".
rene
I'm working on plotting program in WPF using the canvas element. What I want to achieve is a scrollbar with draggable endpoints. Example of these kinds of scrollbars are in the After Effects video editing software by Adobe.
Basic functionality of such a scrollbar is that it is able to scroll trough content that is bigger then it's container, but both the left and right endpoint can be dragged to dynamically change the scale of the content.
I have implemented a similar scrollbar in the plotting program; users should be able to drag around the in and outpoint (Rectangles in a canvas), and the plot canvas should respond to this by scaling to the desired range.
Information I need for this:
Width of the total plot (amount of plotpoints)
Width of the container (static, 600px)
Percentage of the in and out points relative to the total width of the scrollbar canvas
Link to current screenshot
With this information I have created a MatrixTransform, using the ScaleAt() method to scale the plot canvas inside the container so that it matches the in and outpoints in the scrollbar below. For this I used the following code. resetTransform gets called FPS times a second to keep up with the incoming data and XMAX and YMAX are updated elsewhere to reflect this.
public void resetTransform(Boolean useSlider = false)
{
//Add transformgroup to plot
double yscale = plot.Height / view.YMAX; //YMAX is maximum plot value received
double xscale = plot.Width / view.XMAX; //XMAX is total ammount of plotted points
Matrix m = new Matrix(1, 0, 0, 1, 0, 0);
if (useSlider)
{
double maxVal = zoomBar.ActualWidth - outPoint.Width;
double outP = Canvas.GetLeft(outPoint); //points position relative to the scrollbar
double inP = Canvas.GetLeft(inPoint);
double center = (((outP + inP) / 2) / maxVal) * plot.ActualWidth;
double delta = (outP-inP);
double factor = (maxVal/delta) * xscale;
double mappedinP = (inP / maxVal) * view.XMAX;
double anchorOut = (outP / maxVal) * view.XMAX;
double anchorIn = (inP / maxVal) * view.XMAX;
m.ScaleAt(factor, -yscale,center,0); //scale around the center point,
m.Translate(0, plot.Height); //to compensate the flipped graph, move it back down
}
scale = new ScaleTransform(m.M11, m.M22, 0, 0); //save scale factors in a scaletransform for reference
signals.scaleSignalStrokes(scale); //Scale the plotlines to compensate for canvas scaling
MatrixTransform matrixTrans = new MatrixTransform(m); //Create matrixtransform
plot.RenderTransform = matrixTrans; //Apply to canvas
}
Expectation: Everything should work and the plotted graph would scale nicely when the amount of plotpoints grows over time. Reality: The graph scales when moving the points around, but it is not representative; Moreover, the more plot points are added, the more the whole canvas shifts to the right and the less control I seem to have over the transformation. The algorithm as it is now is probably to wrong approach to get the result I need, but I have spent quite some time thinking how to do this right.
Update
I have uploaded a video to give a clearer picture on the interaction. In the video you can clearly see the canvas shifting to the right.
Screencapture video
How should I scale the canvas (plot) to fit within two boundaries?
So, after some struggling, I found the right algorithm to solve this problem. I will post the adjusted version of the resetTransform function below:
//Reset graph transform
public void resetTransform(Boolean useSlider = false)
{
double yscale = plot.Height / view.YMAX; //YMAX is maximum plot value received
double xscale = plot.Width / view.XMAX; //XMAX is total ammount of plotted points
Matrix m = new Matrix(1, 0, 0, 1, 0, 0);
if (useSlider)
{
double maxVal = zoomBar.ActualWidth - outPoint.Width;
double outP = Canvas.GetLeft(outPoint); //points position relative to the scrollbar
double inP = Canvas.GetLeft(inPoint);
double delta = (outP-inP);
double factor = (maxVal/delta) * xscale;
anchorOut = (outP / maxVal) * view.XMAX; //Define anchorpoint coordinates
anchorIn = (inP / maxVal) * view.XMAX;
double center = (anchorOut +anchorIn)/2; //Define centerpoint
m.Translate(-anchorIn, 0); //Move graph to inpoint
m.ScaleAt(factor, -yscale,0,0); //scale around the inpoint, with a factor so that outpoint is plot.Height(=600px) further away
m.Translate(0, plot.Height); //to compensate the flipped graph, move it back down
}
scale = new ScaleTransform(m.M11, m.M22, 0, 0); //save scale factors in a scaletransform for reference
signals.scaleSignalStrokes(scale); //Scale the plotlines to compensate for canvas scaling
MatrixTransform matrixTrans = new MatrixTransform(m); //Create matrixtransform
plot.RenderTransform = matrixTrans; //Apply to canvas
}
So rather than scaling around the centre point, I should first translate the image and then scale around the origin of the canvas with a factor. This means the other side of the canvas is exactly plot.Height pixels away (with some added scaling)
Everything seems to work fine now, but because I am using custom controls (draggable Rectangles in a canvas) I notice these rectangles do not always fire the mousse events.
Since it is out of the scope of this question, I've described the issue further in this post
I'm working on some image processing and for debug I'm overlaying colours on the original bitmap.
The problem is the image is rendered in a picture box with SizeMode set to Zoom and invalidating every time I update a pixel is Really slow and just gets slower the larger picturebox is (for the same size image)
What I think might help is if I only invalidate the pixel(s) I've changed but I don't know how convert the co-ordinates of the pixel I've changed into a rectangle rendered on the control. Obviously if the image is being drawn larger than the original image then the rectangle I'm invalidating is going to be more than one pixel
Added a method to get the zoom and padding of the picture pox
private void CalculateZoomAndPadding()
{
Double imageAspect = (Double)pictureBox1.Image.Width / (Double)pictureBox1.Image.Height;
Double pbAspect = (Double)pictureBox1.Width / (Double)pictureBox1.Height;
Boolean heightRestricted = imageAspect < pbAspect;
hPadding = 0;
vPadding = 0;
if (heightRestricted)
{
zoom = (Double)pictureBox1.Height / (Double)pictureBox1.Image.Height;
Double imageWidth = (Double)pictureBox1.Image.Width * zoom;
hPadding = (Double)(pictureBox1.Width - imageWidth) / 2d;
}
else
{
zoom = (Double)pictureBox1.Width / (Double)pictureBox1.Image.Width;
Double imageHeight = (Double)pictureBox1.Image.Height * zoom;
vPadding = (Double)(pictureBox1.Height - imageHeight) / 2d;
}
}
then to invalidate a pixel called invalidate like this:
pictureBox1.Invalidate(new Rectangle(Convert.ToInt32(Math.Floor(x * zoom)) + Convert.ToInt32(hPadding) -1, Convert.ToInt32(Math.Floor(y * zoom)) + Convert.ToInt32(vPadding) -1, PixelSize, PixelSize));
when I first did this I only invalidated the are directly covered by the pixel but found that this was subject to rounding errors so expanded it to include a few extra.
Can you change all the pixels and then just invalidate the image once?
I'd just add a timer that fires 30 or 60 times per second that invalidates the whole control. While there might be a slight delay in updating you shouldn't be able to notice it due to your monitor's refresh rate most likely being 60 Hz only anyway.