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

StackOverflow https://stackoverflow.com/questions/16558267

Domanda

I would like to create a PictureBox class derived supporting the zoom, pan, scroll features and some selection tools (circle, rectangle, annulus).

I'm wondering which design patterns I should use (I just think about a command pattern for making a undo / redo) and some advices for starting properly design my control.

È stato utile?

Soluzione 2

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,
}

Altri suggerimenti

You don't mention the UI technology you are using, nor do you define what a "PictureBox" is so I'm going to assume you mean you are building an application that is a slim version of "Photoshop" and if you're doing a desktop application I would opt for WPF.

Controls are developed in Visual Studio using a UI Technology so saying it's 'in Visual Studio' doesn't really help as that could mean, WPF, Silverlight, ASP.NET, WebForms, Windows Forms, Javascript, Mono etc ...

Given those assumptions I would always opt for use MVVM as the main architecture for the application UI. It promotes loose-coupling and takes advantage of the benefits of WPFs data-binding technology.

That said if you mean Windows Forms then it would be worth looking at this.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top