Page 1 of 1

Drawing Shapes and Strings

#1 andrewsw  Icon User is offline

  • I'm not here to twist your niblets
  • member icon

Reputation: 4453
  • View blog
  • Posts: 16,302
  • Joined: 12-December 12

Posted 31 March 2015 - 05:58 PM

The purpose of this tutorial is to demonstrate drawing simple shapes (and strings) on a WinForm. You could also extend the application to explore other, more detailed, shapes and effects.

Included shapes:
Line, Ellipse, Filled Ellipse, Rectangle, Filled Rectangle, String

(I know, you might not consider a String a Shape!)

One thing in particular that I want to emphasize is the use of the Paint event to perform all the drawing/painting. Frequently people will just create a new Graphics object (CreateGraphics) to draw shapes on, for example, clicking a Button. The Microsoft documentation itself uses this approach (for example). When you create your own Graphics object it needs to be properly disposed of, along with other resources (Pens and Brushes). Causing your artwork to be redrawn when the form is resized, or minimized, etc., could also be problematic.

It is my understanding that the Paint event should be used to perform any drawing, and redrawing. It is what it is designed to do and will handle the resizing of the form. It also supplies us with a Graphics object that will, automatically, be correctly disposed of.

Disclaimer: I don't do a great deal of drawing so this tutorial is introductory. I am relying upon, and encouraging, drawing-gurus to criticise my code or approach, if necessary. (I will correct parts of the tutorial if it proves necessary.)

Pre-Requisites: You need to be comfortable with WinForms, Controls and their events. You might read-up on the Stack<T> Class beforehand as well. Basically, it is like a stack of plates: the last plate added is the first plate removed (last-in-first-out, LIFO).

Graphics and Drawing in Windows Forms :MSDN

Attached Image

x1, y1 and x2, y2 are the top-left and bottom-right corners of the drawn object. The top left of the panel is at (0, 0) and x runs horizontally, and y vertically.

The Approach

The Shapes are drawn inside a Panel. (Shapes can be drawn on the Form itself, we have a little more control if we use a Panel.)

When the Draw button is pressed a new (custom) Shape object is created, based on the user's choices from the ComboBoxes and other controls.

The Panel's Paint event is triggered by this.pnlDrawing.Invalidate(). N.B. we don't call the Paint event directly, Invalidate() calls it for us.

The Paint event draws our shape. Nothing other than drawing occurs, and should occur, in this event. No application logic occurs in this event. Specifically, it is the Draw-Button's Click event which determines what needs to be drawn.

Rather than drawing (and replacing) a single shape each time, a Stack is used to remember previously drawn shapes. The Paint event loops through the Stack drawing all its shapes.

The Remove button simply removes (Pops) the last drawn shape from the Stack and invalidates the panel again (causing it to be redrawn, but without the most recent shape).

The Clear button empties (Clears) the Stack and triggers the Paint event. Because the Stack is empty nothing is drawn, and the Panel is cleared of shapes.

Get Our Paint On

Create a new WinForm application named DrawingStuff, or another more sensible name.

The main Panel is named pnlDrawing. I set its Size to 600, 200, anchored it Bottom, Left, Right and gave it a (FixedSingle) border.

I use prefixes for the Controls: cbo (ComboBox), btn (Button), lbl (Label), nud (NumericUpDown), txt (TextBox). (The remaining parts of their names should, hopefully, be obvious from looking through the code.)

The full code is posted at the end of the tutorial.

The Shape class:
    class Shape {
        public string ShapeName { get; set; }
        public string PenColor { get; set; }
        public string FillColor { get; set; }
        public string Text { get; set; }
        public string Font { get; set; }
        public int Size { get; set; }
        public int X1 { get; set; }
        public int Y1 { get; set; }
        public int X2 { get; set; }
        public int Y2 { get; set; }
    }


This is enough to account for the simple shapes that this tutorial demonstrates, including Strings (DrawString). To extend the application to include more complex shapes and/or effects will require a little more planning. You might have a simple Shape class and then perhaps two or three subclasses of this. To keep things simple we, ideally, want a single Stack instance that we can loop through to draw all of our shapes.

Be careful though. You don't want to end up replicating the entire Drawing namespace! Initially, you could just add a few more properties to our current Shape definition, to allow you to explore a few more shapes or features. At our introductory level it won't matter too much that a number of the properties are rarely used.

Note that the X2 and Y2 properties are dual-purpose. For some shapes they will represent their width and height.

using statements (nothing exciting to see here):

Spoiler

namespace DrawingStuff {
    public partial class frmDrawing : Form {
        private List<string> _colors = new List<string>();
        private List<string> _fonts = new List<string>();
        private Stack<Shape> _shapes = new Stack<Shape>();

        public frmDrawing() {
            InitializeComponent();
        }


We will be able to obtain lists of all the available colours and fonts, populating the ComboBoxes from these.

I am using a Stack to store all of our drawn shapes, so that we can easily remove (Pop()) the last-drawn shape.
        private void frmDrawing_Load(object sender, EventArgs e) {

            foreach (System.Reflection.PropertyInfo prop in typeof(Color).GetProperties()) {
                if (prop.PropertyType.FullName == "System.Drawing.Color")
                    _colors.Add(prop.Name);
            }
            cboPenColor.Items.AddRange(_colors.ToArray());
            cboPenColor.SelectedIndex = -1;
            cboFillColor.Items.AddRange(_colors.ToArray());
            cboFillColor.SelectedIndex = -1;

            cboShape.Items.AddRange(new[] { "Line", "Ellipse", "Filled Ellipse", "Rectangle",
                "Filled Rectangle", "String" });

            foreach (FontFamily font in System.Drawing.FontFamily.Families) {
                _fonts.Add(font.Name);
            }
            cboFont.Items.AddRange(_fonts.ToArray());
            this.ResizeRedraw = true;       // control redraws itself when resized
        }


This code just populates the ComboBoxes (and causes the form to be redrawn when resized).

The Draw Button
        private void btnDraw_Click(object sender, EventArgs e) {
            if (cboShape.SelectedIndex == -1) {
                MessageBox.Show("Select a Shape.", "Select Shape",
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
                cboShape.Focus();
                return;
            }
            if (cboShape.SelectedItem.ToString() == "String" && String.IsNullOrWhiteSpace(txtText.Text)) {
                MessageBox.Show("Enter some text.", "Enter Text",
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
                txtText.Focus();
                return;
            }


Simple Validation. The user must select a type of shape to draw, and there is no point in drawing a string if it doesn't contain any text.

I don't use any other validation before creating a Shape because in the following code I will set the PenColor, FillColor and Font to default values if the user hasn't specified these.
            Shape newShape = new Shape();
            newShape.ShapeName = cboShape.SelectedItem.ToString();

            if (cboPenColor.SelectedIndex == -1) {
                cboPenColor.SelectedItem = "Blue";
            }
            newShape.PenColor = cboPenColor.SelectedItem.ToString();
            if (cboFillColor.SelectedIndex == -1) {
                cboFillColor.SelectedItem = "Transparent";
            }
            newShape.FillColor = cboFillColor.SelectedItem.ToString();
            newShape.Text = txtText.Text;
            if (cboFont.SelectedIndex == -1) {
                cboFont.SelectedItem = "Arial";
            }
            newShape.Font = cboFont.SelectedItem.ToString();


Get the NumericUpDown values:
            newShape.Size = Convert.ToInt32(this.nudSize.Value);
            newShape.X1 = Convert.ToInt32(this.nudX1.Value);
            newShape.Y1 = Convert.ToInt32(this.nudY1.Value);
            newShape.X2 = Convert.ToInt32(this.nudX2.Value);
            newShape.Y2 = Convert.ToInt32(this.nudY2.Value);


I have omitted validation of these values.



In a real application we should use data-binding to bind UI-element properties to properties of an object (our Shape, or a Shapes collection). This way, we wouldn't have to go through all the controls manually, checking and validating each one.

With data-binding it would also be much easier to store proper Color and Font objects rather than manually converting to and from string values ("Blue" to Color.Blue, etc.).



            _shapes.Push(newShape);
            this.pnlDrawing.Invalidate();
        }


This is the beauty of our approach: we only have to push our new Shape onto the Stack and cause the Panel to be redrawn.

Remove and Clear

I'll cover these two Buttons here as the code for them is straightforward.
        private void btnRemove_Click(object sender, EventArgs e) {
            if (_shapes.Count > 0) {
                _shapes.Pop();
                this.pnlDrawing.Invalidate();
            } else {
                MessageBox.Show("Nothing to remove.", "No Shapes",
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }
        }

        private void btnClear_Click(object sender, EventArgs e) {
            if (_shapes.Count > 0) {
                _shapes.Clear();
                this.pnlDrawing.Invalidate();
            } else {
                MessageBox.Show("Nothing to remove.", "No Shapes",
                    MessageBoxButtons.OK, MessageBoxIcon.Warning);
                return;
            }
        }


All we need to do is adjust the Stack (Pop() or Clear()) and cause the re-painting (Invalidate()), having first checked that there are some shapes on the Stack to be removed or cleared.

The Paint Event
        private void pnlDrawing_Paint(object sender, PaintEventArgs e) {
            using (Pen myPen = new Pen(Color.FromName("Blue")))
            using (SolidBrush myBrush = new SolidBrush(Color.FromName("Transparent"))) {


using statements are extremely useful - use them as much as possible. They ensure that the resources will be correctly disposed of.

The colours of the Pen and Brush will immediately be changed according to the values stored for each Shape on the Stack. We need to give then some initial value though (Blue and Transparent) because neither have a constructor accepting 0 arguments. That is, we cannot use new Pen().
                foreach (Shape shp in _shapes) {
                    myPen.Color = Color.FromName(shp.PenColor);
                    myBrush.Color = Color.FromName(shp.FillColor);


We loop through our stack of shapes, changing the Pen and Brush colours.
                    switch (shp.ShapeName) {
                        case "Line":
                            e.Graphics.DrawLine(myPen, shp.X1, shp.Y1, shp.X2, shp.Y2);
                            break;
                        case "Ellipse":
                            e.Graphics.DrawEllipse(myPen, shp.X1, shp.Y1, shp.X2, shp.Y2);
                            break;
                        case "Filled Ellipse":
                            e.Graphics.FillEllipse(myBrush, shp.X1, shp.Y1, shp.X2, shp.Y2);
                            break;
                        case "Rectangle":
                            e.Graphics.DrawRectangle(myPen, shp.X1, shp.Y1, shp.X2, shp.Y2);
                            break;
                        case "Filled Rectangle":
                            e.Graphics.FillRectangle(myBrush, shp.X1, shp.Y1, shp.X2, shp.Y2);
                            break;


We draw the appropriate shape. Each of these shapes has a number of different (overloaded) methods to draw them. I am using just one method, and the method that is most consistent across our variety of shapes. This helped to keep our Shape definition as straightforward as possible.
                        case "String":
                            FontConverter fc = new FontConverter();
                            Font font = fc.ConvertFromString(shp.Font) as Font;
                            font = new Font(font.Name, shp.Size, font.Style);

                            e.Graphics.DrawString(shp.Text, font, myBrush, shp.X1, shp.Y1);
                            break;
                        default:
                            break;
                    }
                }
            }
        }


Drawing a string requires a little more work, needing a Font instance.

I left all the closing braces in the above code to emphasize that there is nothing else in the Paint event. It simply iterates the Stack of shapes, using the available Graphics instance to draw the appropriate shape.

Notice also that we don't need to dispose of the supplied Graphics instance. The rule is, if we create a Graphics instance (CreateGraphics()) then we are responsible for ensuring that it is correctly disposed of. if the system creates it then the system will dispose of it.



In a real application we should make efforts to avoid hard-coding these string (magic) values, "Line", etc., and using a large switch statement.



The Full Monty:

Spoiler


Attached File  DrawingStuff.zip (13.43K)
Number of downloads: 80

This post has been edited by andrewsw: 02 April 2015 - 06:53 AM


Is This A Good Question/Topic? 1
  • +

Replies To: Drawing Shapes and Strings

#2 andrewsw  Icon User is offline

  • I'm not here to twist your niblets
  • member icon

Reputation: 4453
  • View blog
  • Posts: 16,302
  • Joined: 12-December 12

Posted 01 April 2015 - 05:07 AM

Here is an important amendment that should be considered, and added:
        private void pnlDrawing_Paint(object sender, PaintEventArgs e) {
            if (_shapes.Count == 0)
                return;
            using (Pen myPen = new Pen(Color.FromName("Blue")))

If there are no shapes on the stack, return immediately, before creating Pens or Brushes. Pens and Brushes, etc., are expensive, and we want the Paint event to be lean and mean. It should do as little work as possible, and not create resources that it doesn't need.
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1