MSH Logo – A GUI Disaster

Wed, Feb 8, 2006 4-minute read

Ok, so now that we’ve talked about our grand design for MSH Logo, our next task is to simply integrate this into a GUI.  You can download the Visual Studio 2005 project from here.

The most interesting class, by far, is our Turtle class:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;

namespace Monad_Hosting
{
    /// <summary>
    /// A turtle class that implements some of the logo primitives.
    /// It stores a reference to the canvas upon which it draws, and
    /// is responsible for drawing on that canvas.
    /// </summary>
    class Turtle
    {
        Graphics canvas;
        Pen drawingPen = new Pen(Color.LightGreen);

        // Although the canvas can only represent integer
        // positions, we store our state in double precision.
        // Otherwise, most interesting graphics (that tend to
        // involve recursion and small numbers) look terribly
        // broken.
        double currentX, currentY;
        bool penDown = true;
        bool showTurtle = true;
        int direction = 90;

        public Turtle(Graphics canvas)
        {
            this.canvas = canvas;

            Initialize();
        }

        public void PenUp()
        {
            penDown = false;
        }

        public void PenDown()
        {
            penDown = true;
        }

        public void Forward(double steps)
        {
            int oldX = (int) currentX;
            int oldY = (int) currentY;

            // In essense, the turtle draws the hypotenuse
            // of a triangle as it moves.  Since the user provides
            // the length of the hypotenuse, we use standard
            // trigonometry to determine the X and Y components
            // of the movement independently.
            currentX += steps * Math.Cos(DegToRad(direction));
            currentY -= steps * Math.Sin(DegToRad(direction));

            if(penDown)
            {
                canvas.DrawLine(drawingPen, oldX, oldY, 
                    (int) currentX, (int) currentY);
            }
        }

        public void Backward(double steps)
        {
            Forward(-1 * steps);
        }

        public void Left(int degrees)
        {
            direction = (direction + degrees) % 360;
        }

        public void Right(int degrees)
        {
            direction = (direction - degrees + 360) % 360;
        }

        public void Hide()
        {
            showTurtle = false;
        }

        public void Show()
        {
            showTurtle = true;
        }

        public void Draw()
        {
            if (showTurtle)
            {
                // We leverage the 2d transformations of the .Net
                // Graphics class here to save us from doing the 
                // math for rotation contortions ourselves.
                // Rather than draw a rotated turtle, we instead rotate
                // (and reposition) the canvas, then draw a straight
                // turtle.  When we restore the canvas again, the
                // turtle now appears rotated.

                System.Drawing.Drawing2D.GraphicsState canvasState = 
                    canvas.Save();
                canvas.TranslateTransform((float) currentX, (float) currentY);
                canvas.RotateTransform(90 - direction);

                canvas.DrawLine(drawingPen, -4, 4, 0, -8);
                canvas.DrawLine(drawingPen, 0, -8, 4, 4);
                canvas.DrawLine(drawingPen, -4, 4, 4, 4);

                canvas.Restore(canvasState);
            }
        }

        public void Reset()
        {
            Initialize();
            canvas.Clear(Color.DarkGreen);
        }

        private void Initialize()
        {
            currentX = canvas.VisibleClipBounds.Width / 2.0;
            currentY = canvas.VisibleClipBounds.Height / 2.0;

            penDown = true;
            showTurtle = true;
            direction = 90;
        }

        // The user specifies their angles in degrees, but
        // the .Net math classes prefer radians.
        private double DegToRad(int degrees)
        {
            return (Math.PI * (double) degrees / 180.0);
        }
    }
}

Our GUI application mainly interacts with the Turtle object:

public partial class MonadHost : Form 
{
        // The drawing surface is the canvas on which the
        // turtle draws.
        private Graphics drawingSurface = null;
        private Turtle turtle = null;

        // We save the image of the turtle's tracks just
        // before we draw the turtle icon.  That way, when
        // the turtle moves, we don't have to worry about erasing
        // the icon.
        Image savedImage = null;

        public MonadHost()
        {
            InitializeComponent();
            InitializeCustom();
        }

        private void InitializeCustom()
        {
            InitializeCanvas();

            this.tabControl.Focus();
        }

        // Make our form look a little more presentable when
        // we resize it.
        private void MonadHost_ResizeEnd(object sender, EventArgs e)
        {
            InitializeCanvas();
        }

        // This brings our application back to a clean state.
        // We create a fresh new canvas the same size as the current
        // form, create a new turtle to reference that canvas,
        // and draw the turtle.
        private void InitializeCanvas()
        {
            this.pictureBox.Image = 
                new Bitmap(pictureBox.Width, pictureBox.Height);
            drawingSurface = Graphics.FromImage(this.pictureBox.Image);
            drawingSurface.SmoothingMode = 
                System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            turtle = new Turtle(drawingSurface);
            turtle.Reset();
            
            savedImage = new Bitmap(this.pictureBox.Image);
            turtle.Draw();
        }

        // The following methods are pretty similar.  We
        // first draw our saved image to the canvas (the one without
        // the turtle icon,) have the turtle draw (or do) whatever
        // it was told to do, save the resulting image, draw
        // the turtle icon, and finally refresh the view of
        // the canvas.
        private void penUp_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            drawingSurface.DrawImage(savedImage, 0, 0);

            turtle.PenUp();

            savedImage = new Bitmap(this.pictureBox.Image);
            turtle.Draw();
            this.pictureBox.Refresh();
        }

        (...)

        private void backward10_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            drawingSurface.DrawImage(savedImage, 0, 0);

            turtle.Backward(10);

            savedImage = new Bitmap(this.pictureBox.Image);
            turtle.Draw();
            this.pictureBox.Refresh();
        }
    }
}

When you run the program, its user interface may feel a little barbaric.  Or (more probably,) a lot barbaric.  The feature set is closed, and offers no extensibility.  In fact, you may already be contemplating a lawsuit against me for an acute case of carpal tunnel syndrome.

Next time, we’ll look at a way to resolve this issue.

[Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]