Create a 3D Cube Structure for GDI+, Page 3
The complete listing is just over 400 lines of code. Rather than cover every single line, I will leave it to you to explore. However, some small changes and points of interest—as well as some interesting GDI+ elements—are worth showcasing. Let's start with the changes:
- Most of the property setters have a call to the new method, Changed. Changed reconstructs the GraphicsPath object by calling ConstructPath.
- A couple of the constructors (Sub New methods) provide default values for the FRotateX and FRotateY fields. In a class, you could use field initializers like Private FRotateX As RotateHorizontal = RotateHorizontal.Right, but initializers are permitted only on constants in structures.
Now, let's examine the GDI+ features you use to implement the cube:
- Using Point: The Point structure contains two integers representing a two-dimensional location in a Cartesian plane, most notably the real estate represented by your monitor's screen. In conjunction with using Point, you defined CubeSize as a structure with a height, width, and depth. When combined with a Point, you can visualize a cube shape.
- Using Rectangle: Rectangle is a GDI+ structure that represents a two-dimensional rectangle with four corners. If you take six equally sized rectangles and join them at the edges, you can generate a cube.
Both Point and Rectangle have integer fields and properties. GDI+ defines PointF and RectangleF, which use floating-point numbers. If you desired to be as consistent with GDI+ as possible, you could define a structure (CubeF) that uses floating-point numbers for X, Y, Width, Height, and Depth, as well.
- Using GraphicsPath: A GraphicsPath is an open or closed path of points. Sort of like the children's game Connect the Dots, the GraphicsPath represents the dots and GDI+ knows how to connect the dots. Because GDI+ doesn't currently define a Cube primitive, you can define your cube as a collection of points expressed as a GraphicsPath and use GDI+ to connect those dots.
- Calculating the Center: Some of the features I found it useful to define include the ability to calculate the 3D center. In two dimensions, the horizontal center is X + Width/2 and the vertical center is Y + Height/2. For a cube, you need to account for the Z axis, or depth.
To approximate the center, each of the constructors adds half the depth multiplied by the number that represents that vertex's aspect ratio. Another means is to request the bounding rectangle for the GraphicsPath and then calculate the center in the usual way. Experiment with my implementation or try using the bounding rectangle to see which means of calculating the center yields the best result.
- Constructing the GraphicsPath: A direct way of defining the GraphicsPath was to visualize each face of the cube as a rectangle comprised of lines. All of these lines were then added to a single instance of a GraphicsPath object. (Perhaps a professional game programmer could devise a better algorithm, but this one seems practical.) In ConstructPath, GetBack, GetFront, GetTop, GetBottom, GetRight, and GetLeft are each called and the results are added to an instance of the GraphicsPath object, which is in turn assigned to the field FPath.
As mentioned previously, no method in the Graphics class is named DrawCube, but one is named DrawPath. While consistent with the Cube's progenitor Rectangle, your Cube does not draw itself. Consumers of the Cube can use Graphics.DrawPath, getting the job done with the GraphicsPath object returned by GetCube.
Rendering the Cube with GDI+
Rendering the cube is easy. All you need to do is construct an instance of a Cube structure, pass in the constructor's required arguments, obtain a Graphics object, and call DrawPath, passing the GraphicsPath that represents the Cube. If you want the cube to be present and updated each time a form was repainted, for example, place the call to DrawPath in the Form's OnPaint event handler. Listing 4 demonstrates.
Listing 4: Rendering the Cube
Imports CubeLibrary_VB Imports System.Drawing.Drawing2D Public Class Form1 Inherits System.Windows.Forms.Form [ Windows Form Designer generated code ] Private cube As cube = New cube(100, 100, 100, 200, 50) Private Sub Form1_Paint(ByVal sender As Object, _ ByVal e As System.Windows.Forms.PaintEventArgs) _ Handles MyBase.Paint e.Graphics.DrawPath(Pens.Red, cube.GetCube()) End Sub End Class
The Pens class is a predefined class with shared instances of Pens. (The Pens class has some great colors. One of mine is AliceBlue.) Figure 1 shows the output from Listing 4.
Figure 1: Output from the Cube Primitive
With a little bit of effort and ingenuity, you can make a simple game sort of like the children's Spyrograph from the 60s. Give it a try. (Hint: Assign a matrix randomly rotated between 0° and 360° to the Graphics object's Transform property, rotating the cube at its center.) Figure 2 shows a sample output from my solution.
Figure 2: Rotate the Cube at Its Center Using a Matrix Object, the Matrix.RotateAt Method, and the Transform Property of the Graphics Class
Graphics Are Easy with GDI+
Graphics are just plain fun. Without graphics we wouldn't have cool software like video games. While it may be very challenging to write today's advanced games from Graphics primitives like the Cube example, this is how such games got started. Modern video games use tools like the DirectX 9 SDK and pre-existing libraries that work with advanced primitives like sprites and wire-framing.
However, you can write visually powerful business solutions using graphics primitives and GDI+. GDI+ makes graphics programming easier than ever. Unfortunately for beginners, some things are easy to do and others require that you to write a couple of hundred lines of code.
Paul Kimmel is the VB Today columnist, has written several books on .NET programming, and is a software architect. You may contact him at firstname.lastname@example.org if you need assistance or are interested in joining the Lansing Area .NET Users Group (glugnet.org).
Copyright © 2004 by Paul Kimmel. All Rights Reserved.
Page 3 of 3