Related
Problem - I'm writing a program that draws graphics, and zooming is one of the features. Currently, a picturebox is placed on a panel, and the picturebox has vertical and horizontal scroll bars on the right and bottom. How to combine scrollbar with mouse wheel zooming? And I'm not sure if I should use paint to draw the graphics or set a bitmap to draw the graphics onto it?
Expected - When the mouse wheel is scrolled, the entire canvas(picturebox) include drawn graphics are scaled according to the current mouse position as the center (the horizontal and vertical scroll bars change according to the zoom center). When the mouse wheel is pressed and moved, the canvas can be dragged freely.
Expected as follows:
The initial code
private List<Point> _points;
private int _pointRadius = 50;
private float _scale = 1f;
private float _offsetX = 0f;
private float _offsetY = 0f;
private void picturebox_MouseDown(object sender, MouseEventArgs e)
{
_points.Add(e.Location);
}
private void picturebox_MouseWheel(object sender, MouseEvnetArgs e)
{
if(e.Delta < 0)
{
_scale += 0.1f;
_offsetX = e.X * (1f - _scale);
_offsetY = e.X * (1f - _scale);
}
else
{
_scale -= 0.1f;
_offsetX = e.X * (1f - _scale);
_offsetY = e.X * (1f - _scale);
}
picturebox.Invalidate();
}
private void picturebox_Paint(object sender, PaintEventArgs e)
{
e.Graphics.TranslateTransform(_offsetX, _offsetY);
e.Graphics.ScaleTransform(_scaleX, _scaleY);
foreach (Point p in _points)
{
e.Graphics.FillEllipse(Brushes.Black, p.X, - _pointRadius, p.Y - _pointRadius, 2 * _pointRadius, 2 * _pointRadius);
}
}
Hope the answer is modified based on the initial code.
Thanks in advance to everyone who helped me.
Would it be easier if I drew the graphics on a Bitmap?
Considering the nature of your task and the already implemented solutions in my ImageViewer I created a solution that draws the result in a Metafile, which is both elegant, consumes minimal memory and allows zooming without quality issues.
Here is the stripped version of my ImageViewer:
public class MetafileViewer : Control
{
private HScrollBar sbHorizontal = new HScrollBar { Visible = false };
private VScrollBar sbVertical = new VScrollBar { Visible = false };
private Metafile? image;
private Size imageSize;
private Rectangle targetRectangle;
private Rectangle clientRectangle;
private float zoom = 1;
private bool sbHorizontalVisible;
private bool sbVerticalVisible;
private int scrollFractionVertical;
public MetafileViewer()
{
Controls.AddRange(new Control[] { sbHorizontal, sbVertical });
sbHorizontal.ValueChanged += ScrollbarValueChanged;
sbVertical.ValueChanged += ScrollbarValueChanged;
}
void ScrollbarValueChanged(object? sender, EventArgs e) => Invalidate();
public Metafile? Image
{
get => image;
set
{
image = value;
imageSize = image?.Size ?? default;
InvalidateLayout();
}
}
public bool TryTranslate(Point mouseCoord, out PointF canvasCoord)
{
canvasCoord = default;
if (!targetRectangle.Contains(mouseCoord))
return false;
canvasCoord = new PointF((mouseCoord.X - targetRectangle.X) / zoom, (mouseCoord.Y - targetRectangle.Y) / zoom);
if (sbHorizontalVisible)
canvasCoord.X += sbHorizontal.Value / zoom;
if (sbVerticalVisible)
canvasCoord.Y += sbVertical.Value / zoom;
return true;
}
private void InvalidateLayout()
{
Invalidate();
if (imageSize.IsEmpty)
{
sbHorizontal.Visible = sbVertical.Visible = sbHorizontalVisible = sbVerticalVisible = false;
targetRectangle = Rectangle.Empty;
return;
}
Size clientSize = ClientSize;
if (clientSize.Width < 1 || clientSize.Height < 1)
{
targetRectangle = Rectangle.Empty;
return;
}
Size scaledSize = imageSize.Scale(zoom);
// scrollbars visibility
sbHorizontalVisible = scaledSize.Width > clientSize.Width
|| scaledSize.Width > clientSize.Width - SystemInformation.VerticalScrollBarWidth && scaledSize.Height > clientSize.Height;
sbVerticalVisible = scaledSize.Height > clientSize.Height
|| scaledSize.Height > clientSize.Height - SystemInformation.HorizontalScrollBarHeight && scaledSize.Width > clientSize.Width;
if (sbHorizontalVisible)
clientSize.Height -= SystemInformation.HorizontalScrollBarHeight;
if (sbVerticalVisible)
clientSize.Width -= SystemInformation.VerticalScrollBarWidth;
if (clientSize.Width < 1 || clientSize.Height < 1)
{
targetRectangle = Rectangle.Empty;
return;
}
Point clientLocation = Point.Empty;
var targetLocation = new Point((clientSize.Width >> 1) - (scaledSize.Width >> 1),
(clientSize.Height >> 1) - (scaledSize.Height >> 1));
// both scrollbars
if (sbHorizontalVisible && sbVerticalVisible)
{
sbHorizontal.Dock = sbVertical.Dock = DockStyle.None;
sbHorizontal.Width = clientSize.Width;
sbHorizontal.Top = clientSize.Height;
sbHorizontal.Left = 0;
sbVertical.Height = clientSize.Height;
sbVertical.Left = clientSize.Width;
}
// horizontal scrollbar
else if (sbHorizontalVisible)
sbHorizontal.Dock = DockStyle.Bottom;
// vertical scrollbar
else if (sbVerticalVisible)
sbVertical.Dock = DockStyle.Right;
// adjust scrollbar values
if (sbHorizontalVisible)
{
sbHorizontal.Minimum = targetLocation.X;
sbHorizontal.Maximum = targetLocation.X + scaledSize.Width;
sbHorizontal.LargeChange = clientSize.Width;
sbHorizontal.SmallChange = 32;
sbHorizontal.Value = Math.Min(sbHorizontal.Value, sbHorizontal.Maximum - sbHorizontal.LargeChange);
}
if (sbVerticalVisible)
{
sbVertical.Minimum = targetLocation.Y;
sbVertical.Maximum = targetLocation.Y + scaledSize.Height;
sbVertical.LargeChange = clientSize.Height;
sbVertical.SmallChange = 32;
sbVertical.Value = Math.Min(sbVertical.Value, sbVertical.Maximum - sbVertical.LargeChange);
}
sbHorizontal.Visible = sbHorizontalVisible;
sbVertical.Visible = sbVerticalVisible;
clientRectangle = new Rectangle(clientLocation, clientSize);
targetRectangle = new Rectangle(targetLocation, scaledSize);
if (sbVerticalVisible)
clientRectangle.X = SystemInformation.VerticalScrollBarWidth;
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
InvalidateLayout();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
if (image == null || e.ClipRectangle.Width <= 0 || e.ClipRectangle.Height <= 0)
return;
if (targetRectangle.IsEmpty)
InvalidateLayout();
if (targetRectangle.IsEmpty)
return;
Graphics g = e.Graphics;
g.IntersectClip(clientRectangle);
Rectangle dest = targetRectangle;
if (sbHorizontalVisible)
dest.X -= sbHorizontal.Value;
if (sbVerticalVisible)
dest.Y -= sbVertical.Value;
g.DrawImage(image, dest);
g.DrawRectangle(SystemPens.ControlText, Rectangle.Inflate(targetRectangle, 1, 1));
}
protected override void OnMouseWheel(MouseEventArgs e)
{
base.OnMouseWheel(e);
switch (ModifierKeys)
{
// zoom
case Keys.Control:
float delta = (float)e.Delta / SystemInformation.MouseWheelScrollDelta / 5;
if (delta.Equals(0f))
return;
delta += 1;
SetZoom(zoom * delta);
break;
// vertical scroll
case Keys.None:
VerticalScroll(e.Delta);
break;
}
}
private void VerticalScroll(int delta)
{
// When scrolling by mouse, delta is always +-120 so this will be a small change on the scrollbar.
// But we collect the fractional changes caused by the touchpad scrolling so it will not be lost either.
int totalDelta = scrollFractionVertical + delta * sbVertical.SmallChange;
scrollFractionVertical = totalDelta % SystemInformation.MouseWheelScrollDelta;
int newValue = sbVertical.Value - totalDelta / SystemInformation.MouseWheelScrollDelta;
SetValueSafe(sbVertical, newValue);
}
internal static void SetValueSafe(ScrollBar scrollBar, int value)
{
if (value < scrollBar.Minimum)
value = scrollBar.Minimum;
else if (value > scrollBar.Maximum - scrollBar.LargeChange + 1)
value = scrollBar.Maximum - scrollBar.LargeChange + 1;
scrollBar.Value = value;
}
private void SetZoom(float value)
{
const float maxZoom = 10f;
float minZoom = image == null ? 1f : 1f / Math.Min(imageSize.Width, imageSize.Height);
if (value < minZoom)
value = minZoom;
if (value > maxZoom)
value = maxZoom;
if (zoom.Equals(value))
return;
zoom = value;
InvalidateLayout();
}
}
And then the updated version of your initial code (add a new point by right click, zoom by Ctrl + mouse scroll):
public partial class RenderMetafileForm : Form
{
private static Size canvasSize = new Size(300, 200);
private List<PointF> points = new List<PointF>();
private const float pointRadius = 5;
public RenderMetafileForm()
{
InitializeComponent();
metafileViewer.MouseClick += MetafileViewer_MouseClick;
UpdateMetafile();
}
private void MetafileViewer_MouseClick(object? sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right && metafileViewer.TryTranslate(e.Location, out var coord))
{
points.Add(coord);
UpdateMetafile();
}
}
private void UpdateMetafile()
{
Graphics refGraph = Graphics.FromHwnd(IntPtr.Zero);
IntPtr hdc = refGraph.GetHdc();
Metafile result;
try
{
result = new Metafile(hdc, new Rectangle(Point.Empty, canvasSize), MetafileFrameUnit.Pixel, EmfType.EmfOnly, "Canvas");
using (var g = Graphics.FromImage(result))
{
foreach (PointF point in points)
g.FillEllipse(Brushes.Navy, point.X - pointRadius, point.Y - pointRadius, pointRadius * 2, pointRadius * 2);
}
}
finally
{
refGraph.ReleaseHdc(hdc);
refGraph.Dispose();
}
Metafile? previous = metafileViewer.Image;
metafileViewer.Image = result;
previous?.Dispose();
}
}
Result:
⚠️ Note: I did not add panning by keyboard or by grabbing the image but you can extract those from the original ImageViewer. Also, I removed DPI-aware scaling but see the ScaleSize extensions in the linked project.
Issue: Attempting to zoom (scale) an Image from (or at the) mouse location using transforms in the Paint event to translate bitmap origin to mouse location, then scale the Image and translate its origin back.
The Image jumps and fails to scale from the relocated origin when translating the mouse location.
Rotate, scale, and pan function correctly without translating to the the mouse location.
Running on .Net 4.7.2, using Visual Studio in Windows 10 1909
v18363.778
The relevant code blocks:
private void trackBar1_Scroll(object sender, EventArgs e)
{
// Get rotation angle
ang = trackBar1.Value;
pnl1.Invalidate();
}
private void pnl1_MouseWheel(object sender, MouseEventArgs e)
{
// Get mouse location
mouse = e.location;
// Get new scale (zoom) factor
zoom = (float)(e.Delta > 0 ? zoom * 1.05 : zoom / 1.05);
pnl1.Invalidate();
}
private void pnl1_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
pan = true;
mouX = e.X;
mouY = e.Y;
oldX = imgX;
oldY = imgY;
}
private void pnl1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left || !pan) return;
// Coordinates of panned image
imgX = oldX + e.X - mouX;
imgY = oldY + e.Y - mouY;
pnl1.Invalidate();
}
private void pnl1_MouseUp(object sender, MouseEventArgs e)
{
pan = false;
}
private void pnl1_Paint(object sender, PaintEventArgs e)
{
// Apply rotation angle # center of bitmap
e.Graphics.TranslateTransform(img.Width / 2, img.Height / 2);
e.Graphics.RotateTransform(ang);
e.Graphics.TranslateTransform(-img.Width / 2, -img.Height / 2);
// Apply scaling factor - focused # mouse location
e.Graphics.TranslateTransform(mouse.X, mouse.Y, MatrixOrder.Append);
e.Graphics.ScaleTransform(zoom, zoom, MatrixOrder.Append);
e.Graphics.TranslateTransform(-mouse.X, -mouse.Y, MatrixOrder.Append);
// Apply drag (pan) location
e.Graphics.TranslateTransform(imgX, imgY, MatrixOrder.Append);
// Draw "bmp" # location
e.Graphics.DrawImage(img, 0, 0);
}
A few suggestions and a couple of tricks.
Not exactly tricks, just some methods to speed up the calculations when more than one graphic transformation is in place.
Divide and conquer: split the different graphics effects and transformations in different, specialized, methods that do one thing. Then design in a way that makes it possible for these methods to work together when needed.
Keep it simple: when Graphics objects need to accumulate more than a couple of transformations, the order in which Matrices are stacked can cause misunderstandings. It's simpler (and less prone to generate weird outcomes) to calculate some generic transformations (translate and scale, mostly) beforehand, then let GDI+ render already pre-cooked objects and shapes.
Here, only Matrix.RotateAt and Matrix.Multiply are used.
Some notes about Matrix transformations here: Flip the GraphicsPath
Use the right tools: for example, a Panel used as canvas is not exactly the best choice. This Control is not double-buffered; this feature can be enabled, but the Panel class is not meant for drawing, while a PictureBox (or a non-System flat Label) supports it on its own.
Some more notes here: How to apply a fade transition effect to Images
The sample code shows 4 zoom methods, plus generates rotation transformations (which work side-by-side, don't accumulate).
The Zoom modes are selected using an enumerator (private enum ZoomMode):
Zoom modes:
ImageLocation: Image scaling is performed in-place, keeping the current Location on the canvas in a fixed position.
CenterCanvas: while the Image is scaled, it remains centered on the Canvas.
CenterMouse: the Image is scaled and translated to center itself on the current Mouse location on the Canvas.
MouseOffset: the Image is scaled and translated to maintain a relative position determined by the initial location of the Mouse pointer on the Image itself.
You can notice that the code simplifies all the calculations, applying translations exclusively relative to the Rectangle that defines the current Image bounds and only in relation to the Location of this shape.
The Rectangle is only scaled when the calculation needs to preemptively determine what the Image size will be after the Mouse Wheel has generated the next Zoom factor.
Visual sample of the implemented functionalities:
Sample code:
canvas is the Custom Control, derived from PictureBox (you can find its definition at the bottom). This control is added to the Form in code, here. Modify as needed.
trkRotationAngle is the TrackBar used to define the current rotation of the Image. Add this control to the Form in the designer.
radZoom_CheckedChanged is the event handler of all the RadioButtons used to set the current Zoom Mode. The value these Controls set is assigned in their Tag property. Add these controls to the Form in the designer.
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Windows.Forms;
public partial class frmZoomPaint : Form
{
private float rotationAngle = 0.0f;
private float zoomFactor = 1.0f;
private float zoomStep = .05f;
private RectangleF imageRect = RectangleF.Empty;
private PointF imageLocation = PointF.Empty;
private PointF mouseLocation = PointF.Empty;
private Bitmap drawingImage = null;
private PictureBoxEx canvas = null;
private ZoomMode zoomMode = ZoomMode.ImageLocation;
private enum ZoomMode
{
ImageLocation,
CenterCanvas,
CenterMouse,
MouseOffset
}
public frmZoomPaint()
{
InitializeComponent();
string imagePath = [Path of the Image];
drawingImage = (Bitmap)Image.FromStream(new MemoryStream(File.ReadAllBytes(imagePath)));
imageRect = new RectangleF(Point.Empty, drawingImage.Size);
canvas = new PictureBoxEx(new Size(555, 300));
canvas.Location = new Point(10, 10);
canvas.MouseWheel += canvas_MouseWheel;
canvas.MouseMove += canvas_MouseMove;
canvas.MouseDown += canvas_MouseDown;
canvas.MouseUp += canvas_MouseUp;
canvas.Paint += canvas_Paint;
Controls.Add(canvas);
}
private void canvas_MouseWheel(object sender, MouseEventArgs e)
{
mouseLocation = e.Location;
float zoomCurrent = zoomFactor;
zoomFactor += e.Delta > 0 ? zoomStep : -zoomStep;
if (zoomFactor < .10f) zoomStep = .01f;
if (zoomFactor >= .10f) zoomStep = .05f;
if (zoomFactor < .0f) zoomFactor = zoomStep;
switch (zoomMode) {
case ZoomMode.CenterCanvas:
imageRect = CenterScaledRectangleOnCanvas(imageRect, canvas.ClientRectangle);
break;
case ZoomMode.CenterMouse:
imageRect = CenterScaledRectangleOnMousePosition(imageRect, e.Location);
break;
case ZoomMode.MouseOffset:
imageRect = OffsetScaledRectangleOnMousePosition(imageRect, zoomCurrent, e.Location);
break;
default:
break;
}
canvas.Invalidate();
}
private void canvas_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
mouseLocation = e.Location;
imageLocation = imageRect.Location;
canvas.Cursor = Cursors.NoMove2D;
}
private void canvas_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
imageRect.Location =
new PointF(imageLocation.X + (e.Location.X - mouseLocation.X),
imageLocation.Y + (e.Location.Y - mouseLocation.Y));
canvas.Invalidate();
}
private void canvas_MouseUp(object sender, MouseEventArgs e) =>
canvas.Cursor = Cursors.Default;
private void canvas_Paint(object sender, PaintEventArgs e)
{
var drawingRect = GetDrawingImageRect(imageRect);
using (var mxRotation = new Matrix())
using (var mxTransform = new Matrix()) {
e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
e.Graphics.PixelOffsetMode = PixelOffsetMode.Half;
mxRotation.RotateAt(rotationAngle, GetDrawingImageCenterPoint(drawingRect));
mxTransform.Multiply(mxRotation);
e.Graphics.Transform = mxTransform;
e.Graphics.DrawImage(drawingImage, drawingRect);
}
}
private void trkRotationAngle_ValueChanged(object sender, EventArgs e)
{
rotationAngle = trkAngle.Value;
canvas.Invalidate();
canvas.Focus();
}
private void radZoom_CheckedChanged(object sender, EventArgs e)
{
var rad = sender as RadioButton;
if (rad.Checked) {
zoomMode = (ZoomMode)int.Parse(rad.Tag.ToString());
}
canvas.Focus();
}
#region Drawing Methods
public RectangleF GetScaledRect(RectangleF rect, float scaleFactor) =>
new RectangleF(rect.Location,
new SizeF(rect.Width * scaleFactor, rect.Height * scaleFactor));
public RectangleF GetDrawingImageRect(RectangleF rect) =>
GetScaledRect(rect, zoomFactor);
public PointF GetDrawingImageCenterPoint(RectangleF rect) =>
new PointF(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
public RectangleF CenterScaledRectangleOnCanvas(RectangleF rect, RectangleF canvas)
{
var scaled = GetScaledRect(rect, zoomFactor);
rect.Location = new PointF((canvas.Width - scaled.Width) / 2,
(canvas.Height - scaled.Height) / 2);
return rect;
}
public RectangleF CenterScaledRectangleOnMousePosition(RectangleF rect, PointF mousePosition)
{
var scaled = GetScaledRect(rect, zoomFactor);
rect.Location = new PointF(mousePosition.X - (scaled.Width / 2),
mousePosition.Y - (scaled.Height / 2));
return rect;
}
public RectangleF OffsetScaledRectangleOnMousePosition(RectangleF rect, float currentZoom, PointF mousePosition)
{
var currentRect = GetScaledRect(imageRect, currentZoom);
if (!currentRect.Contains(mousePosition)) return rect;
float scaleRatio = currentRect.Width / GetScaledRect(rect, zoomFactor).Width;
PointF mouseOffset = new PointF(mousePosition.X - rect.X, mousePosition.Y - rect.Y);
PointF scaledOffset = new PointF(mouseOffset.X / scaleRatio, mouseOffset.Y / scaleRatio);
PointF position = new PointF(rect.X - (scaledOffset.X - mouseOffset.X),
rect.Y - (scaledOffset.Y - mouseOffset.Y));
rect.Location = position;
return rect;
}
#endregion
}
The simple PictureBoxEx custom control (modify and extend as needed):
This PictureBox is selectable, so it can be focused, with a Mouse click
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
[DesignerCategory("Code")]
public class PictureBoxEx : PictureBox
{
public PictureBoxEx() : this (new Size(200, 200)){ }
public PictureBoxEx(Size size) {
SetStyle(ControlStyles.Selectable | ControlStyles.UserMouse, true);
BorderStyle = BorderStyle.FixedSingle;
Size = size;
}
}
#Jimi: Thank you for the detailed information - very useful for visualizing the concepts involved in the graphics manipulations. I had arrived at a functioning solution (see code below) however, your code utilizes steps with greater efficiency. Admittedly, my code is developed with more of an intent to learn the mechanics of image manipulation - as I am still at the early part of the learning curve. Nonetheless, your illustration of the mechanics and techniques is extremely helpful.
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace ZoomImage
{
public partial class Form1 : Form
{
Image img;
Bitmap bmp;
float ang = 0;
float zoom = 1;
bool pan;
bool? ctr = false;
Point mcurrent;
PointF mouse;
PointF image;
PointF _image;
PointF rotate;
public Form1()
{
InitializeComponent();
MouseWheel += mouseWheel;
img = Image.FromFile(#"C:\testimage.jpg");
bmp = new Bitmap(img);
// Set initial scale to fit canvas window
float wRatio = (float)pbx.Width / (float)img.Width;
float hRatio = (float)pbx.Height / (float)img.Height;
zoom = Math.Min(wRatio, hRatio);
image.X = (pbx.Width - zoom * img.Width) / 2;
image.Y = (pbx.Height - zoom * img.Height) / 2;
}
private void label()
{
string _imgX = string.Format("{0:000}", image.X);
string _imgY = string.Format("{0:000}", image.Y);
lbl1.Text = "Location: " + _imgX + ", " + _imgY + "\r\nRotation: " + ang + "\r\nZoom: " + zoom + "\r\nMouse: " + mcurrent.X + ", " + mcurrent.Y;
}
private void btnRotate_Click(object sender, EventArgs e)
{
if (ModifierKeys == Keys.Control)
{
string msg = "Set center of rotation point:\r\n\nMove mouse to desired center ";
msg += "of rotation then hold \"Alt\" and left-click.\r\n\n";
msg += "To restore center of rotation to center of image:\r\n\nHold \"Shift\" and";
msg += " click \"Rotate\".";
MessageBox.Show(msg,"Change center of rotation");
ctr = null;
pbx.Focus();
return;
}
else if (ModifierKeys == Keys.Shift)
{
ctr = false;
return;
}
ang = ang == 270 ? 0 : ang += 90;
if (ang > 360) ang -= 360;
trackBar1.Value = (int)ang;
ctr = ctr == null ? false : ctr;
if (ctr == false) rotate = new PointF(img.Width / 2, img.Height / 2);
pbx.Invalidate();
}
private void trackBar1_Scroll(object sender, EventArgs e)
{
ang = trackBar1.Value;
if (ctr == false) rotate = new PointF(img.Width / 2, img.Height / 2);
pbx.Invalidate();
}
private void mouseWheel(object sender, MouseEventArgs e)
{
mouse = new PointF(e.X - image.X, e.Y - image.Y);
float zinc = 0.05f;
float zfac = 1 + zinc;
zoom = (float)(e.Delta > 0 ? zoom * (zfac) : zoom / (zfac));
// Adjust "img" (bitmap) orgin to maintain fixed focus # mouse location
if (e.Delta > 0)
{
image.X -= zinc * mouse.X;
image.Y -= zinc * mouse.Y;
}
else
{
image.X += (1 - 1 / (zfac)) * mouse.X;
image.Y += (1 - 1 / (zfac)) * mouse.Y;
}
image = new PointF(image.X, image.Y);
pbx.Invalidate();
}
private void mouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
if (ModifierKeys == Keys.Alt && ctr == null)
{
ctr = true;
rotate = new PointF((e.X - image.X) / zoom, (e.Y - image.Y) / zoom);
return;
}
pan = true;
mouse = e.Location;
_image = image;
}
private void mouseMove(object sender, MouseEventArgs e)
{
mcurrent = e.Location;
label();
if (e.Button != MouseButtons.Left || !pan) return;
image.X = _image.X + e.X - mouse.X;
image.Y = _image.Y + e.Y - mouse.Y;
image = new PointF(image.X, image.Y);
pbx.Invalidate();
}
private void mouseUp(object sender, MouseEventArgs e)
{
pan = false;
}
private void pbx_Paint(object sender, PaintEventArgs e)
{
label();
// Generate bitmap "bmp" - this can be saved as drawn...if deisred
bmp = new Bitmap(img.Width, img.Height);
using (Graphics g = Graphics.FromImage(bmp))
{
Matrix transform = new Matrix();
transform.Scale(zoom, zoom, MatrixOrder.Append);
transform.RotateAt(ang, rotate);
transform.Translate(image.X, image.Y, MatrixOrder.Append);
g.Transform = transform;
g.DrawImage(img, 0, 0);
}
e.Graphics.DrawImage(bmp, 0, 0);
}
}
}
How can I make a free-form selection (like in paint or photoshop) in a picture box and then crop that selection and save it to a folder?
I already did a rectangle crop but I want that free-form selection..
Here is my rectangle crop:
Image img;
bool mouseClicked;
Point startPoint = new Point();
Point endPoint = new Point();
Rectangle rectCropArea;
private void Button1_Click(System.Object sender, System.EventArgs e)
{
}
private void OnLoad(System.Object sender, System.EventArgs e)
{
loadPrimaryImage();
}
private void loadPrimaryImage()
{
img = Image.FromFile("..\\..\\images.jpg");
PictureBox1.Image = img;
}
private void PicBox_MouseUp(System.Object sender, System.Windows.Forms.MouseEventArgs e)
{
mouseClicked = false;
if ((endPoint.X != -1)) {
Point currentPoint = new Point(e.X, e.Y);
Y1.Text = e.X.ToString();
Y2.Text = e.Y.ToString();
}
endPoint.X = -1;
endPoint.Y = -1;
startPoint.X = -1;
startPoint.Y = -1;
}
private void PicBox_MouseDown(System.Object sender, System.Windows.Forms.MouseEventArgs e)
{
mouseClicked = true;
startPoint.X = e.X;
startPoint.Y = e.Y;
//Display coordinates
X1.Text = startPoint.X.ToString();
Y1.Text = startPoint.Y.ToString();
endPoint.X = -1;
endPoint.Y = -1;
rectCropArea = new Rectangle(new Point(e.X, e.Y), new Size());
}
private void PicBox_MouseMove(System.Object sender, System.Windows.Forms.MouseEventArgs e)
{
Point ptCurrent = new Point(e.X, e.Y);
if ((mouseClicked)) {
if ((endPoint.X != -1)) {
//Display Coordinates
X1.Text = startPoint.X.ToString();
Y1.Text = startPoint.Y.ToString();
X2.Text = e.X.ToString();
Y2.Text = e.Y.ToString();
}
endPoint = ptCurrent;
if ((e.X > startPoint.X & e.Y > startPoint.Y)) {
rectCropArea.Width = e.X - startPoint.X;
rectCropArea.Height = e.Y - startPoint.Y;
} else if ((e.X < startPoint.X & e.Y > startPoint.Y)) {
rectCropArea.Width = startPoint.X - e.X;
rectCropArea.Height = e.Y - startPoint.Y;
rectCropArea.X = e.X;
rectCropArea.Y = startPoint.Y;
} else if ((e.X > startPoint.X & e.Y < startPoint.Y)) {
rectCropArea.Width = e.X - startPoint.X;
rectCropArea.Height = startPoint.Y - e.Y;
rectCropArea.X = startPoint.X;
rectCropArea.Y = e.Y;
} else {
rectCropArea.Width = startPoint.X - e.X;
rectCropArea.Height = startPoint.Y - e.Y;
rectCropArea.X = e.X;
rectCropArea.Y = e.Y;
}
PictureBox1.Refresh();
}
}
private void PicBox_Paint(System.Object sender, System.Windows.Forms.PaintEventArgs e)
{
Pen drawLine = new Pen(Color.Red);
drawLine.DashStyle = DashStyle.Dash;
e.Graphics.DrawRectangle(drawLine, rectCropArea);
}
private void btnCrop_Click(System.Object sender, System.EventArgs e)
{
PictureBox2.Refresh();
Bitmap sourceBitmap = new Bitmap(PictureBox1.Image, PictureBox1.Width, PictureBox1.Height);
Graphics g = PictureBox2.CreateGraphics();
if (!(CheckBox1.Checked)) {
g.DrawImage(sourceBitmap, new Rectangle(0, 0, PictureBox2.Width, PictureBox2.Height), rectCropArea, GraphicsUnit.Pixel);
sourceBitmap.Dispose();
} else {
int x1 = 0;
int x2 = 0;
int y1 = 0;
int y2 = 0;
try {
x1 = Convert.ToInt32(CX1.Text);
x2 = Convert.ToInt32(CX2.Text);
y1 = Convert.ToInt32(CY1.Text);
y2 = Convert.ToInt32(CY2.Text);
} catch (Exception ex) {
MessageBox.Show("Enter valid Coordinates (only Integer values)");
}
if (((x1 < x2 & y1 < y2))) {
rectCropArea = new Rectangle(x1, y1, x2 - x1, y2 - y1);
} else if ((x2 < x1 & y2 > y1)) {
rectCropArea = new Rectangle(x2, y1, x1 - x2, y2 - y1);
} else if ((x2 > x1 & y2 < y1)) {
rectCropArea = new Rectangle(x1, y2, x2 - x1, y1 - y2);
} else {
rectCropArea = new Rectangle(x2, y2, x1 - x2, y1 - y2);
}
PictureBox1.Refresh();
//This repositions the dashed box to new location as per coordinates entered.
g.DrawImage(sourceBitmap, new Rectangle(0, 0, PictureBox2.Width, PictureBox2.Height), rectCropArea, GraphicsUnit.Pixel);
sourceBitmap.Dispose();
}
}
private void pictureBox1_MouseClick(System.Object sender, System.Windows.Forms.MouseEventArgs e)
{
PictureBox1.Refresh();
}
private void CheckBox1_CheckedChanged(System.Object sender, System.EventArgs e)
{
if ((CheckBox1.Checked)) {
CX1.Visible = true;
Label10.Visible = true;
CY1.Visible = true;
Label9.Visible = true;
CX2.Visible = true;
Label8.Visible = true;
CY2.Visible = true;
Label7.Visible = true;
X1.Text = "0";
X2.Text = "0";
Y1.Text = "0";
Y2.Text = "0";
} else {
CX1.Visible = false;
Label10.Visible = false;
CY1.Visible = false;
Label9.Visible = false;
CX2.Visible = false;
Label8.Visible = false;
CY2.Visible = false;
Label7.Visible = false;
}
}
public Form1()
{
Load += OnLoad;
}
}
To copy a free-form selection you need to work with polygons.
Here is a complete example. Just paste this into a new solution and try it out (just change the path to the images).
It will create 2 pictureboxes and load an image into the first one and also create an image. Then you can click on the first image and when you have clicked 2 times it will start to show a selection, when you are finished just press the button and it will copy the selection to the other pictureBox and then save it as an png image.
What it does is to create a brush from the first image and then paint the polygon onto another image and set the other pixels in the rectangle to a background color of your choice, in this case the color: Color.Transparent.
Example:
public partial class Form1 : Form {
private List<Point> _points = new List<Point>();
private PictureBox _pictureBox1;
private PictureBox _pictureBox2;
private Button _button1;
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
Size = new Size(1366, 675);
_pictureBox1 = new PictureBox {
Location = new Point(12, 51),
Size = new Size(651, 474),
BorderStyle = BorderStyle.FixedSingle
};
_pictureBox2 = new PictureBox
{
Location = new Point(669, 51),
Size = new Size(651, 474),
BorderStyle = BorderStyle.FixedSingle
};
_button1 = new Button {
Text = #"Copy selected area",
Location = new Point(13, 13),
Size = new Size(175, 23)
};
Controls.AddRange(new Control[] { _pictureBox1, _pictureBox2, _button1 });
_pictureBox1.Image = Image.FromFile(#"d:\temp\Hopetoun_falls.jpg");
_points = new List<Point>();
_pictureBox1.MouseDown += delegate(object o, MouseEventArgs args) { _points.Add(args.Location); _pictureBox1.Refresh(); };
_pictureBox1.Paint += pictureBox1_Paint;
_button1.Click += button_Click;
}
private void pictureBox1_Paint(object sender, PaintEventArgs e) {
if (_points.Count < 2) {
return;
}
var max = _points.Count;
for (int i = 1; i < max; i++) {
e.Graphics.DrawLine(Pens.Red, _points[i-1].X, _points[i-1].Y, _points[i].X, _points[i].Y);
}
e.Graphics.DrawLine(Pens.Red, _points[max - 1].X, _points[max - 1].Y, _points[0].X, _points[0].Y);
}
private static Bitmap GetSelectedArea(Image source, Color bgColor, List<Point> points) {
var bigBm = new Bitmap(source);
using (var gr = Graphics.FromImage(bigBm)) {
// Set the background color.
gr.Clear(bgColor);
// Make a brush out of the original image.
using (var br = new TextureBrush(source)) {
// Fill the selected area with the brush.
gr.FillPolygon(br, points.ToArray());
// Find the bounds of the selected area.
var sourceRect = GetPointListBounds(points);
// Make a bitmap that only holds the selected area.
var result = new Bitmap(sourceRect.Width, sourceRect.Height);
// Copy the selected area to the result bitmap.
using (var resultGr = Graphics.FromImage(result)) {
var destRect = new Rectangle(0, 0, sourceRect.Width, sourceRect.Height);
resultGr.DrawImage(bigBm, destRect, sourceRect, GraphicsUnit.Pixel);
}
// Return the result.
return result;
}
}
}
private static Rectangle GetPointListBounds(List<Point> points) {
int xmin = points[0].X;
int xmax = xmin;
int ymin = points[0].Y;
int ymax = ymin;
for (int i = 1; i < points.Count; i++) {
if (xmin > points[i].X) xmin = points[i].X;
if (xmax < points[i].X) xmax = points[i].X;
if (ymin > points[i].Y) ymin = points[i].Y;
if (ymax < points[i].Y) ymax = points[i].Y;
}
return new Rectangle(xmin, ymin, xmax - xmin, ymax - ymin);
}
private void button_Click(object sender, EventArgs e) {
if (_points.Count < 3) {
return;
}
var img = GetSelectedArea(_pictureBox1.Image, Color.Transparent, _points);
_pictureBox2.Image = img;
_pictureBox2.Image.Save(#"d:\temp\sample.png", ImageFormat.Png);
}
}
This is the code I have so far:
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
int maxX, minX, maxY, minY;
minX = pictureBox2.Location.X;
minY = pictureBox2.Location.Y;
maxX = (pictureBox2.Size.Width) + minX;
maxY = (pictureBox2.Size.Height) + minY;
Point PictureLocation = new Point(e.X, e.Y);
if ( (PictureLocation.X <= maxX)
&& (PictureLocation.X >= minX)
&& (PictureLocation.Y <= maxY)
&& (PictureLocation.Y >= minY) )
{
MessageBox.Show("The drag is working");
//Rest of the program is fine
}
else
{
MessageBox.Show("Please re-drag the item into the area");
}
}
At the moment all it comes up with is the else statement and I can't quite figure out why.
ADDITIONAL:
If it helps, I am also using a faint form during the drag in this way which could effect the position as I've found that the ABOVE CODE HAS A CORRECT ZONE to the right and below of where I need it:
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
mdown = e.Location;
form2.BackgroundImageLayout = ImageLayout.Zoom;
form2.BackgroundImage = pictureBox1.Image;
form2.Opacity = 0.5f;
form2.MaximizeBox = false;
form2.ControlBox = false;
form2.Text = "";
form2.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
form2.Size = new Size(150, 150);
form2.Show();
Point pt = pictureBox1.PointToScreen(pictureBox1.Location);
form2.Location = pt;
}
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
Point pt = pictureBox1.PointToScreen(new Point(-(form2.Width / 2) + e.X, -(form2.Height / 2) + e.Y));
form2.Location = pt;
}
}
The point you are given by e.X and e.Y is relative to the control that raised the event (the picturebox).
So you actually want:
minX = 0;
minY = 0;
maxX = pictureBox2.Size.Width;
maxY = pictureBox2.Size.Height;
as the first picture box was taking a datum at it's own location 0,0, so by using that, and the fact that my pictureboxes are on the same y location starting you get this:
minX = (pictureBox2.Location.X)-(pictureBox1.Location.X);
minY = 0;
maxX = ((pictureBox2.Location.X) - (pictureBox1.Location.X)) + (pictureBox2.Width);
maxY = 0 + (pictureBox2.Height);
Thanks for everyone's help!
I have the below which draws a rectangle on mouse drag and also a grid drawing script which draws a 32x32 grid on the picture box what I'm trying to do is snap the rectangle to the grid then screen shot inside the rectangle.
I've got the screen shot bit and the drawing of the rectangle just not the snapping to grid bit working.
private bool _selecting;
private Rectangle _selection;
private void picCanvas_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
_selecting = true;
_selection = new Rectangle(new Point(e.X, e.Y), new Size());
}
}
private void picCanvas_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
if (_selecting)
{
_selection.Width = e.X - _selection.X;
_selection.Height = e.Y - _selection.Y;
pictureBox1.Refresh();
}
}
public Image Crop(Image image, Rectangle selection)
{
Bitmap bmp = image as Bitmap;
// Check if it is a bitmap:
if (bmp == null)
throw new ArgumentException("No valid bitmap");
// Crop the image:
Bitmap cropBmp = bmp.Clone(selection, bmp.PixelFormat);
// Release the resources:
image.Dispose();
return cropBmp;
}
private void picCanvas_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
if (e.Button == MouseButtons.Left &&
_selecting &&
_selection.Size != new Size())
{
// Create cropped image:
//Image img = Crop(pictureBox1.Image, _selection);
// Fit image to the picturebox:
//pictureBox1.Image = img;
_selecting = false;
}
else
_selecting = false;
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (_selecting)
{
// Draw a rectangle displaying the current selection
Pen pen = Pens.GreenYellow;
e.Graphics.DrawRectangle(pen, _selection);
}
Graphics g = e.Graphics;
int numOfCells = amount;
Pen p = new Pen(Color.LightGray);
for (int y = 0; y < numOfCells; ++y)
{
g.DrawLine(p, 0, y * ysize, numOfCells * ysize, y * ysize);
}
for (int x = 0; x < numOfCells; ++x)
{
g.DrawLine(p, x * xsize, 0, x * xsize, numOfCells * xsize);
}
}
First I would declare a snapping method
private Point SnapToGrid(Point p)
{
double x = Math.Round((double)p.X / xsize) * xsize;
double y = Math.Round((double)p.Y / ysize) * ysize;
return new Point((int)x, (int)y);
}
Then you can initialize the selection like this in MouseDown:
_selection = new Rectangle(SnapToGrid(e.Location), new Size());
And you can adjust the width in MouseMove like this:
Point dest = SnapToGrid(e.Location);
_selection.Width = dest.X - _selection.X;
_selection.Height = dest.Y - _selection.Y;