MSH Logo – A GUI Disaster
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.]