Creating Custom Picturebox with Draggable and Resizable Selection Window

Creating Custom Picturebox with Draggable and Resizable Selection Window

You have different options:

  • You can draw a resizable frame on the picture box
  • You can create a resizable control and add it to picture box

In this answer, I've taken the second option to be able to use built-in sizing features of the controls. Here is a screen capture which shows how it looks like in action:

Sample Image

Example - Creating a Frame Control

As an example, I'll create a resizable control and will add it to the picture box.

using System;
using System.Drawing;
using System.Windows.Forms;
public class FrameControl : Control
{
public FrameControl()
{
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
DoubleBuffered = true;
ResizeRedraw = true;
BackColor = Color.Transparent;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using (var p = new Pen(Color.Black, 4))
{
p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
e.Graphics.DrawRectangle(p, 0, 0, Width - 1, Height - 1);
}
}
const int WM_NCHITTEST = 0x84;
const int WM_SETCURSOR = 0x20;
const int WM_NCLBUTTONDBLCLK = 0xA3;
protected override void WndProc(ref Message m)
{
int borderWidth = 10;
if (m.Msg == WM_SETCURSOR) /*Setting cursor to SizeAll*/
{
if ((m.LParam.ToInt32() & 0xffff) == 0x2 /*Move*/)
{
Cursor.Current = Cursors.SizeAll;
m.Result = (IntPtr)1;
return;
}
}
if ((m.Msg == WM_NCLBUTTONDBLCLK)) /*Disable Mazimiz on Double click*/
{
m.Result = (IntPtr)1;
return;
}
base.WndProc(ref m);
if (m.Msg == WM_NCHITTEST)
{
var pos = PointToClient(new Point(m.LParam.ToInt32() & 0xffff,
m.LParam.ToInt32() >> 16));
if (pos.X <= ClientRectangle.Left + borderWidth &&
pos.Y <= ClientRectangle.Top + borderWidth)
m.Result = new IntPtr(13); //TOPLEFT
else if (pos.X >= ClientRectangle.Right - borderWidth &&
pos.Y <= ClientRectangle.Top + borderWidth)
m.Result = new IntPtr(14); //TOPRIGHT
else if (pos.X <= ClientRectangle.Left + borderWidth &&
pos.Y >= ClientRectangle.Bottom - borderWidth)
m.Result = new IntPtr(16); //BOTTOMLEFT
else if (pos.X >= ClientRectangle.Right - borderWidth &&
pos.Y >= ClientRectangle.Bottom - borderWidth)
m.Result = new IntPtr(17); //BOTTOMRIGHT
else if (pos.X <= ClientRectangle.Left + borderWidth)
m.Result = new IntPtr(10); //LEFT
else if (pos.Y <= ClientRectangle.Top + borderWidth)
m.Result = new IntPtr(12); //TOP
else if (pos.X >= ClientRectangle.Right - borderWidth)
m.Result = new IntPtr(11); //RIGHT
else if (pos.Y >= ClientRectangle.Bottom - borderWidth)
m.Result = new IntPtr(15); //Bottom
else
m.Result = new IntPtr(2); //Move
}
}
}

Then add the control to the picture box:

var s = 100;
var c = new FrameControl();
c.Size = new Size(s, s);
c.Location = new Point((pictureBox1.Width - s) / 2, (pictureBox1.Height - s) / 2);
pictureBox1.Controls.Add(c);

To add a fancy effect of filling outside of the frame with semi-transparent color:

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.ExcludeClip(pictureBox1.Controls[0].Bounds);
using (var b = new SolidBrush(Color.FromArgb(100, Color.Black)))
e.Graphics.FillRectangle(b, pictureBox1.ClientRectangle);
}

As you can see in the paint event, you can find the FrameControl using pictureBox1.Controls[0]. So you can find its location and size.

You can encapsulate all the logic of the picture box in a derived picture box.

Note: Flicker-free rendering

If you experience flickering when moving the frame, use the following code in your form:

protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED
return cp;
}
}

C# - Most suitable design patterns to create a PictureBox supporting: panning / zooming / scrollable AND with selection tools (draggable rectangles)

MVVM could be use but when you are dealing with over-big pics it does not worth the cost of implementing it.

I implemented my own control, strongly inspired from an old code project: http://www.codeproject.com/Articles/15743/Pan-and-Zoom-Very-Large-Images.

One of the trick to take advantage of, is to make a copy of the picture given as an input, it would make the process, much more faster. It is not fully implemented cause I did not have that much time to work on it.

Here is below a implementation of mine

public class ARTImageControl : Control
{
public ARTImageControl()
{
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.DoubleBuffer, true);

this.DoubleBuffered = true;

this.InternalScrollbarHorizontal = new HScrollBar();
this.InternalScrollbarVertical = new VScrollBar();
this.ScrollBarsVisibility = ScrollBarVisibility.Auto;

this.ZoomToFit = false;
this.ZoomFactor = 1.0;
this.ZoomOnMouseWheel = true;

this.BorderColor = Color.Black;
this.BorderWidth = 1;
this.BorderStyle = ButtonBorderStyle.Solid;

this.PixelOffsetMode = PixelOffsetMode.Half;
this.SmoothingMode = SmoothingMode.None;
this.InterpolationMode = InterpolationMode.NearestNeighbor;

this.ActiveTool = ARTImageControlToolType.Pan;

Resize += ARTImageControl_Resize;
MouseMove += ARTImageControl_MouseMove;
MouseDown += ARTImageControl_MouseDown;
MouseWheel += ARTImageControl_MouseWheel;
}

protected void UpdateSizeDrawing()
{
this.SizeDrawing = new Size(Convert.ToInt32(this.Width / this.ZoomFactor), Convert.ToInt32(this.Height / this.ZoomFactor));
}

protected HScrollBar InternalScrollbarHorizontal { get; set; }
protected VScrollBar InternalScrollbarVertical { get; set; }

public ScrollBarVisibility ScrollBarsVisibility { get; set; }

#region Overrides

protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.Clear(this.BackColor);

if (this.Image != null)
{
e.Graphics.PixelOffsetMode = this.PixelOffsetMode;
e.Graphics.SmoothingMode = this.SmoothingMode;
e.Graphics.InterpolationMode = this.InterpolationMode;

if (this.ZoomToFit)
{
this.RectangleSource = new Rectangle(0, 0, this.Image.Width, this.Image.Height);
}
else
{
this.RectangleSource = new Rectangle(this.InternalPointDrawingOrigin, this.SizeDrawing);
}

Rectangle rectangleDestination = new Rectangle(this.BorderWidth, this.BorderWidth, this.RectangleDestination.Width - 2 * this.BorderWidth, this.RectangleDestination.Height - 2 * this.BorderWidth);

e.Graphics.DrawImage(this.Image, rectangleDestination, this.RectangleSource, GraphicsUnit.Pixel);
}

if ((this.BorderWidth > 0) && (this.BorderStyle != ButtonBorderStyle.None))
{
ControlPaint.DrawBorder(e.Graphics, this.RectangleDestination,
this.BorderColor, this.BorderWidth, this.BorderStyle,
this.BorderColor, this.BorderWidth, this.BorderStyle,
this.BorderColor, this.BorderWidth, this.BorderStyle,
this.BorderColor, this.BorderWidth, this.BorderStyle);
}

//base.OnPaint(e);
}
protected override void OnSizeChanged(EventArgs e)
{
this.RectangleDestination = new Rectangle(0, 0, ClientSize.Width, ClientSize.Height);
this.UpdateSizeDrawing();
base.OnSizeChanged(e);
this.Invalidate();
}

#endregion

#region Methods
public void ZoomFitToClient()
{
if (this.Image != null)
{
this.ZoomToFit = false;
this.ZoomFactor = Math.Min(ClientSize.Width / InternalImage.Width, ClientSize.Height / InternalImage.Height);
}
}
public void ZoomIn()
{
this.ZoomFactor *= 1.1f;
}
public void ZoomOut()
{
this.ZoomFactor *= 0.9f;
}

protected void CheckBoundaries()
{
if (this.Image != null)
{
Int32 boundaryX = this.Image.Width - Convert.ToInt32(this.ClientSize.Width / this.ZoomFactor);
Int32 boundaryY = this.Image.Height - Convert.ToInt32(this.ClientSize.Height / this.ZoomFactor);

if (InternalPointDrawingOrigin.X > boundaryX)
{
this.InternalPointDrawingOrigin = new Point(boundaryX, this.InternalPointDrawingOrigin.Y);
}
if (InternalPointDrawingOrigin.Y > boundaryY)
{
this.InternalPointDrawingOrigin = new Point(this.InternalPointDrawingOrigin.X, boundaryY);
}

if (InternalPointDrawingOrigin.X < 0)
{
this.InternalPointDrawingOrigin = new Point(0, this.InternalPointDrawingOrigin.Y);
}
if (InternalPointDrawingOrigin.Y < 0)
{
this.InternalPointDrawingOrigin = new Point(this.InternalPointDrawingOrigin.X, 0);
}
}
}
#endregion

#region Properties

protected Image InternalImage { get; set; }
public Image Image
{
get { return this.InternalImage; }
set
{
if (value != null)
{
if (this.InternalImage != null)
{
this.InternalImage.Dispose();
this.InternalPointDrawingOrigin = new Point(0, 0);
this.ZoomFactor = 1;
GC.Collect();
}

Rectangle rectangle = new Rectangle(0, 0, value.Width, value.Height);
this.InternalImage = new Bitmap(value).Clone(rectangle, PixelFormat.Format32bppPArgb);
this.Invalidate();
}
else
{
this.InternalImage = null;
this.Invalidate();
}
}
}

public PixelOffsetMode PixelOffsetMode { get; set; }
public SmoothingMode SmoothingMode { get; set; }
public InterpolationMode InterpolationMode { get; set; }

protected Double InternalZoomFactor { get; set; }
public Double ZoomFactor
{
get { return this.InternalZoomFactor; }
set
{
this.InternalZoomFactor = value;

if (this.InternalZoomFactor > 15)
{
this.InternalZoomFactor = 15;
}

if (this.InternalZoomFactor < 0.05)
{
this.InternalZoomFactor = 0.05;
}

if ((this.Image != null))
{
this.UpdateSizeDrawing();
this.CheckBoundaries();
}

this.Invalidate();
}
}

public Boolean InternalZoomToFit { get; set; }
public Boolean ZoomToFit
{
get { return this.InternalZoomToFit; }
set
{
this.InternalZoomToFit = value;
this.Invalidate();
}
}

public ButtonBorderStyle BorderStyle { get; set; }
public Byte BorderWidth { get; set; }
public Color BorderColor { get; set; }

public Boolean DoubleBuffering
{
get { return this.DoubleBuffered; }
set { this.DoubleBuffered = value; }
}

protected Point InternalPointMouseDownStart { get; set; }
protected Point InternalPointDrawingOrigin { get; set; }

protected Rectangle RectangleSource { get; set; }
protected Rectangle RectangleDestination { get; set; }

protected Size SizeDrawing { get; set; }

public Boolean ZoomOnMouseWheel { get; set; }

#endregion

#region Events
private void ARTImageControl_Resize(Object sender, EventArgs e)
{
this.UpdateSizeDrawing();

if (this.ZoomToFit)
{
this.Invalidate();
}
}
private void ARTImageControl_MouseDown(Object sender, MouseEventArgs e)
{
if (this.InternalImage != null)
{
this.InternalPointMouseDownStart = new Point(e.X, e.Y);
this.Focus();
}
}

private void ARTImageControl_MouseMove(Object sender, MouseEventArgs e)
{
if (this.Image != null)
{
if (e.Button == MouseButtons.Left)
{
if (this.ActiveTool == ARTImageControlToolType.Pan)
{
Int32 DeltaX = InternalPointMouseDownStart.X - e.X;
Int32 DeltaY = InternalPointMouseDownStart.Y - e.Y;

Int32 pointOriginX = Convert.ToInt32(InternalPointDrawingOrigin.X + (DeltaX / InternalZoomFactor));
Int32 pointOriginY = Convert.ToInt32(InternalPointDrawingOrigin.Y + (DeltaY / InternalZoomFactor));

this.InternalPointDrawingOrigin = new Point(pointOriginX, pointOriginY);

this.CheckBoundaries();

this.InternalPointMouseDownStart = new Point(e.X, e.Y);
}
else
{

}

this.Invalidate();
}
}
}
private void ARTImageControl_MouseWheel(Object sender, MouseEventArgs e)
{
if (ZoomOnMouseWheel)
{
if (e.Delta > 0)
{
this.ZoomIn();
}
else if (e.Delta < 0)
{
this.ZoomOut();
}
}
}
#endregion

public ARTImageControlToolType ActiveTool { get; set; }
}

public enum ARTImageControlToolType
{
Pan = 0,
Rectangle = 1,
}

public abstract class ARTShape
{
public String Name { get; protected set; }
public abstract Rectangle GetBoundingRectangle();
}

public class ARTRectangle : ARTShape
{
public ARTRectangle(UInt32 width, UInt32 height)
{
this.Width = width;
this.Height = height;
}

public UInt32 Width { get; protected set; }
public UInt32 Height { get; protected set; }

public override Rectangle GetBoundingRectangle()
{
throw new NotImplementedException();
}

public Color ColorInactive { get; protected set; }
public Color ColorActive { get; protected set; }
}

public enum ScrollBarVisibility
{
Auto = 0,
Always = 1,
Never = 2,
}

Creating Custom Picturebox with Draggable and Resizable Selection Window

You have different options:

  • You can draw a resizable frame on the picture box
  • You can create a resizable control and add it to picture box

In this answer, I've taken the second option to be able to use built-in sizing features of the controls. Here is a screen capture which shows how it looks like in action:

Sample Image

Example - Creating a Frame Control

As an example, I'll create a resizable control and will add it to the picture box.

using System;
using System.Drawing;
using System.Windows.Forms;
public class FrameControl : Control
{
public FrameControl()
{
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
DoubleBuffered = true;
ResizeRedraw = true;
BackColor = Color.Transparent;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using (var p = new Pen(Color.Black, 4))
{
p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
e.Graphics.DrawRectangle(p, 0, 0, Width - 1, Height - 1);
}
}
const int WM_NCHITTEST = 0x84;
const int WM_SETCURSOR = 0x20;
const int WM_NCLBUTTONDBLCLK = 0xA3;
protected override void WndProc(ref Message m)
{
int borderWidth = 10;
if (m.Msg == WM_SETCURSOR) /*Setting cursor to SizeAll*/
{
if ((m.LParam.ToInt32() & 0xffff) == 0x2 /*Move*/)
{
Cursor.Current = Cursors.SizeAll;
m.Result = (IntPtr)1;
return;
}
}
if ((m.Msg == WM_NCLBUTTONDBLCLK)) /*Disable Mazimiz on Double click*/
{
m.Result = (IntPtr)1;
return;
}
base.WndProc(ref m);
if (m.Msg == WM_NCHITTEST)
{
var pos = PointToClient(new Point(m.LParam.ToInt32() & 0xffff,
m.LParam.ToInt32() >> 16));
if (pos.X <= ClientRectangle.Left + borderWidth &&
pos.Y <= ClientRectangle.Top + borderWidth)
m.Result = new IntPtr(13); //TOPLEFT
else if (pos.X >= ClientRectangle.Right - borderWidth &&
pos.Y <= ClientRectangle.Top + borderWidth)
m.Result = new IntPtr(14); //TOPRIGHT
else if (pos.X <= ClientRectangle.Left + borderWidth &&
pos.Y >= ClientRectangle.Bottom - borderWidth)
m.Result = new IntPtr(16); //BOTTOMLEFT
else if (pos.X >= ClientRectangle.Right - borderWidth &&
pos.Y >= ClientRectangle.Bottom - borderWidth)
m.Result = new IntPtr(17); //BOTTOMRIGHT
else if (pos.X <= ClientRectangle.Left + borderWidth)
m.Result = new IntPtr(10); //LEFT
else if (pos.Y <= ClientRectangle.Top + borderWidth)
m.Result = new IntPtr(12); //TOP
else if (pos.X >= ClientRectangle.Right - borderWidth)
m.Result = new IntPtr(11); //RIGHT
else if (pos.Y >= ClientRectangle.Bottom - borderWidth)
m.Result = new IntPtr(15); //Bottom
else
m.Result = new IntPtr(2); //Move
}
}
}

Then add the control to the picture box:

var s = 100;
var c = new FrameControl();
c.Size = new Size(s, s);
c.Location = new Point((pictureBox1.Width - s) / 2, (pictureBox1.Height - s) / 2);
pictureBox1.Controls.Add(c);

To add a fancy effect of filling outside of the frame with semi-transparent color:

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.ExcludeClip(pictureBox1.Controls[0].Bounds);
using (var b = new SolidBrush(Color.FromArgb(100, Color.Black)))
e.Graphics.FillRectangle(b, pictureBox1.ClientRectangle);
}

As you can see in the paint event, you can find the FrameControl using pictureBox1.Controls[0]. So you can find its location and size.

You can encapsulate all the logic of the picture box in a derived picture box.

Note: Flicker-free rendering

If you experience flickering when moving the frame, use the following code in your form:

protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED
return cp;
}
}

OnResize sub is not fully working in VB.net

I found a way to achieve what I wanted. But, it sounds a bit complicated, then if you find something better, do not hesitate to tell me.

The idea is to find out where the left-up corner of my moving/resizing red rectangle is in the PictureBox rectangle ABCD. Is it in the rectangle ABC (impacting on the right side) or in the rectangle ADC (impacting on the bottom side). Then, adjust the right coding depending of the situation.

Follow this LINK for more explanation on the math's side.

    Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
' To redraw the whole image based on Glass window
Try
' Minimum limits of Glass window
If Me.Width < 40 Then Me.Width = CInt(40 * Form1.dRatioImageWH)
If Me.Height < 40 Then Me.Height = CInt(40 / Form1.dRatioImageWH)
' ------------------------------------------------------------
' Adjust the right coding after checking if the Point P is within the appropriate triangle
' How do I check whether a given point lies inside a triangle whose coordinates are given?
' If Area of ABC == Area of (PAB + PBC + PAC), then Point P is inside the triangle ABC
' ------------------------------------------------------------
Dim A As New Point
A.X = 0
A.Y = 0
Dim B As New Point
B.X = 400
B.Y = 0
Dim C As New Point
C.X = 400
C.Y = 266
Dim P As New Point
P.X = Me.Location.X
P.Y = Me.Location.Y
' Area of Triangle ABC (upper half of the PictureBox)
Dim areaABC As Integer
areaABC = (A.X * (B.Y - C.Y) + B.X * (C.Y - A.Y) + C.X * (A.Y - B.Y)) / 2
' Area of 3 Triangles inside the Triangle ABC based on Point P
Dim areaPAB As Integer
areaPAB = (P.X * (A.Y - B.Y) + A.X * (B.Y - P.Y) + B.X * (P.Y - A.Y)) / 2
Dim areaPBC As Integer
areaPBC = (P.X * (B.Y - C.Y) + B.X * (C.Y - P.Y) + C.X * (P.Y - B.Y)) / 2
Dim areaPAC As Integer
areaPAC = (P.X * (A.Y - C.Y) + A.X * (C.Y - P.Y) + C.X * (P.Y - A.Y)) / 2
' Target: keep the ratio Width/Height when resizing
If (areaABC > areaPAB + areaPBC + areaPAC) = True Then
' Point P in Triangle ABC (upper half of the PictureBox)
If Me.Height > Form1.PictureBox1.Height - Me.Location.Y Then
Me.Height = Form1.PictureBox1.Height - Me.Location.Y
Else
Me.Height = CInt(Me.Width / Form1.dRatioImageWH)
End If
Else
' Point P in Triangle ADC (lower half of the PictureBox)
If Me.Width > Form1.PictureBox1.Width - Me.Location.X Then
Me.Width = Form1.PictureBox1.Width - Me.Location.X
Else
Me.Width = CInt(Me.Height * Form1.dRatioImageWH)
End If
End If
' Control to be redrawn
Me.Invalidate()
' Raise the Resize event
MyBase.OnResize(e)
Catch ex As Exception
MsgBox(ex.Message)
End Try
End Sub

Translate Rectangle Position in Zoom Mode Picturebox

You can translate selected rectangle on the picture box to the rectangle on image this way:

public RectangleF GetRectangeOnImage(PictureBox p, Rectangle selectionRect)
{
var method = typeof(PictureBox).GetMethod("ImageRectangleFromSizeMode",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var imageRect = (Rectangle)method.Invoke(p, new object[] { p.SizeMode });
if (p.Image == null)
return selectionRect;
var cx = (float)p.Image.Width / (float)imageRect.Width;
var cy = (float)p.Image.Height / (float)imageRect.Height;
var r2 = Rectangle.Intersect(imageRect, selectionRect);
r2.Offset(-imageRect.X, -imageRect.Y);
return new RectangleF(r2.X * cx, r2.Y * cy, r2.Width * cx, r2.Height * cy);
}

Note: You can find ImageRectangleFromSizeMode method source code here and use it as write such method as part of your application code.

Example - Crop Image of PictureBox having SizeMode = Zoom

As an example, the following code will crop the given rectangle of the picture box 1 and will set the result as image of picture box 2:

var selectedRectangle = new Rectangle(7, 30, 50, 40);
var result = GetRectangeOnImage(pictureBox1, selectedRectangle);
using (var bm = new Bitmap((int)result.Width, (int)result.Height))
{
using (var g = Graphics.FromImage(bm))
g.DrawImage(pictureBox1.Image, 0, 0, result, GraphicsUnit.Pixel);
pictureBox2.Image = (Image)bm.Clone();
}

Here is the input image:

Sample Image

And this is the result:

Sample Image



Related Topics



Leave a reply



Submit