Create a 3D Cube Structure for GDI+

Conspicuously absent from the .NET Framework and GDI+ are three-dimensional (3D) shapes. Sure, you can draw lines, ellipses, rectangles, arcs, Bezier curves, and polygons, but things as obvious as spheres and cubes are absent. A sphere is a circle with volume, and a cube is a rectangle with a z-dimension or a depth. Unfortunately, 3D primitive shapes just don't exist.

Recently, when working at a logistics company, I was asked if we could render loads as 3D images, showing the load as various stacks of bins and pallets. Such an implementation would permit suppliers, carriers, dockworkers, and plants to visualize what was in a particular shipment. With all of the shippers in the world, this seems like it would be a common enough problem. Unfortunately, to do this you have to write your own primitives or leap up to something more advanced like DirectX 9. What I decided to do was experiment with GDI+ and implement a 3D cube primitive to determine how much work was involved in capturing the graphics aspect of this problem. This article more or less describes the results.

Defining the Cube Structure

A 3D cube, for the purposes of this example, has height, width, and depth values. When each of the six sides of the primitive has the same dimension as the others, you have a cube. Additionally, the graphics primitive needs to know about its Cartesian location, and the ability to divine the center of the cube also seems useful. The result is six rectangles connected along their respective edges.

An additional objective was that the cube be "renderable" using GDI+. This meant that the class or structure had to work with one of the GDI+ draw methods. Graphics.DrawPolygon turned out to be an option. However, imitating the implementation of the Rectangle structure, defining the 3D Cube as a structure, using the Rectangle structure as a pattern, and emulating the style of properties and fields of the Rectangle structure in my Cube structure seemed like a practical choice.

Defining Cube Fields and Properties

Following the Rectangle structure defined in the System.Drawing namespace, I added location, height, width, depth, center, path, and rotateX and rotateY fields. Location and center were defined as point structures; a point is a single X,Y Cartesian pair. Location represents the upper-left corner of the cube. Height, depth, and width are self-explanatory; these were defined as integers. Path was defined as a GraphicsPath, which is more or less an array of points. RotateX and rotateY are enumerated types representing the aspect ratio. These values are used to create a horizontal left, right, or center, and vertical up, down, or center viewpoint perspective. Collectively, rotateX and rotateY help give the illusion of viewing the cube from different angles.

You can define the project as a Class Library and add a reference to the System.Drawing.dll assembly. If you include the necessary Imports statement and the members mentioned thus far, your structure could be implemented as shown in Listing 1.

Listing 1: Defining the enumerations and fields for the Cube structure

Imports System
Imports System.Drawing
Imports System.Drawing.Drawing2D

Public Enum RotateHorizontal
    Left = -1
    Center = 0
    Right = 1
End Enum

Public Enum RotateVertical
    Up = -1
    Center = 0
    Down = 1
End Enum

Public Structure Cube
    Private FLocation As Point
    Private FHeight As Integer
    Private FWidth As Integer
    Private FDepth As Integer
    Private FCenter As Point
    Private FPath As GraphicsPath
    Private FRotateX As RotateHorizontal
    Private FRotateY As RotateVertical


End Structure

To avoid getting lost, note that you will be adding code to the Cube structure, thereby growing the structure and the code in each successive listing until you have all of the code in the final listing.

Note: Using camel-cased names for fields and Pascal-cased names for properties (for example, location and Location) won't work in VB.NET because it is case insensitive. I prefer to use an F-prefix to mean field because it is easy and not as atrocious as m_, mvar_, or str_. Also, even though a language like C# is case sensitive, using case to distinguish members is not CLS compliant. This means that cross-language use of such classes may cause problems, and tools such as FxCop (http://www.gotdotnet.com/team/fxcop/) will report an error in your code due to such usage. Whichever convention you use, try to be consistent.

The next thing you need to do is add the symmetric properties for your fields. Convention fields are always private, and properties are the public means by which you permit consumers to change a field's value in a constrained way. Listing 2 shows the Cube Structure with the associated properties and a few extra convenience properties whose values are derived from the previously defined fields.

Listing 2: The Cube structure with properties

Imports System
Imports System.Drawing
Imports System.Drawing.Drawing2D

Public Enum RotateHorizontal
    Left = -1
    Center = 0
    Right = 1
End Enum

Public Enum RotateVertical
    Up = -1
    Center = 0
    Down = 1
End Enum

Public Enum CubeSides
    Left
    Right
    Top
    Bottom
    Front
    Back
End Enum


Public Structure Cube
    Private FLocation As Point
    Private FHeight As Integer
    Private FWidth As Integer
    Private FDepth As Integer
    Private FCenter As Point
    Private FPath As GraphicsPath
    Private FRotateX As RotateHorizontal
    Private FRotateY As RotateVertical

    Public Property Location() As Point
        Get
            Return FLocation
        End Get
        Set(ByVal Value As Point)
            FLocation = Value
        End Set
    End Property

    Public Property Height() As Integer
        Get
            Return FHeight
        End Get
        Set(ByVal Value As Integer)
            FHeight = Value
        End Set
    End Property

    Public Property Width() As Integer
        Get
            Return FWidth
        End Get
        Set(ByVal Value As Integer)
            FWidth = Value
        End Set
    End Property

    Public Property Depth() As Integer
        Get
            Return FDepth
        End Get
        Set(ByVal Value As Integer)
            FDepth = Value
        End Set
    End Property

    Public Property Center() As Point
        Get
            Return FCenter
        End Get
        Set(ByVal Value As Point)
            FCenter = Value
        End Set
    End Property

    Public ReadOnly Property Path() As GraphicsPath
        Get
            Return FPath
        End Get
    End Property

    Public Property RotateX() As RotateHorizontal
        Get
            Return FRotateX
        End Get
        Set(ByVal Value As RotateHorizontal)
            FRotateX = Value
        End Set
    End Property

    Public Property RotateY() As RotateVertical
        Get
            Return FRotateY
        End Get
        Set(ByVal Value As RotateVertical)
            FRotateY = Value
        End Set
    End Property

    Public ReadOnly Property X() As Integer
        Get
            Return FLocation.X
        End Get
    End Property

    Public ReadOnly Property Y() As Integer
        Get
            Return FLocation.Y
        End Get
    End Property

    'Return the rectangle that bounds the entire polygon
    'representing the cube
    Public ReadOnly Property BoundsRect() As Rectangle
        Get
            If (FPath Is Nothing) Then
                Return New Rectangle(0, 0, 0, 0)
            Else
                Dim r As RectangleF = Path.GetBounds()
                ' Implicit conversion from single to integer,
                ' really only available in VB
                Return New Rectangle(r.X, r.Y, r.Width, r.Height)
            End If
        End Get
    End Property

    Public Property Size() As CubeSize
        Get
            Return New CubeSize(FWidth, FHeight, FDepth)
        End Get
        Set(ByVal Value As CubeSize)
            FWidth = Value.Width
            FHeight = Value.Height
            FDepth = Value.Depth
        End Set
    End Property

    Public ReadOnly Property Item(ByVal index As CubeSides) _
           As Point()
        Get
            Select Case index
                Case CubeSides.Back
                    Return Back
                Case CubeSides.Front
                    Return Front
                Case CubeSides.Left
                    Return Left
                Case CubeSides.Right
                    Return Right
                Case CubeSides.Top
                    Return Top
                Case CubeSides.Bottom
                    Return Bottom
                Case Else
                    Return Front
            End Select
        End Get
    End Property

    Public ReadOnly Property Top() As Point()
        Get
            ' TODO: Need to implement behavior here
            Return New Point() {}
            ' Return GetTop(location, height, width, depth,
            ' rotateX, rotateY)
        End Get
    End Property

    Public ReadOnly Property Bottom() As Point()
        Get
            ' TODO: Need to implement behavior here
            Return New Point() {}
            ' Return GetBottom(location, height, width, depth,
            ' rotateX, rotateY)
        End Get
    End Property

    Public ReadOnly Property Left() As Point()
        Get
            ' TODO: Need to implement behavior here
            Return New Point() {}
            ' Return GetLeft(location, height, width, depth,
            ' rotateX, rotateY)
        End Get
    End Property

    Public ReadOnly Property Right() As Point()
        Get
            ' TODO: Need to implement behavior here
            Return New Point() {}
            ' Return GetRight(location, height, width, depth,
            ' rotateX, rotateY)
        End Get
    End Property

    Public ReadOnly Property Front() As Point()
        Get
            ' TODO: Need to implement behavior here
            Return New Point() {}
            ' Return GetFront(location, height, width, depth,
            ' rotateX, rotateY)
        End Get
    End Property

    Public ReadOnly Property Back() As Point()
        Get
            ' TODO: Need to implement behavior here
            Return New Point() {}
            ' Return GetBack(location, height, width, depth,
            ' rotateX, rotateY)
        End Get
    End Property
End Structure

I added an enumeration (CubeSides) so I could refer to the sides of a cube by name. I defined a property indexer to return an array of points representing a specific size, added a property that returns the enclosing bounds of the cube by getting the bounds of a GraphicsPath object, and defined read-only properties representing each side. I stubbed out these properties in terms of as yet unimplemented methods.

Create a 3D Cube Structure for GDI+

Implementing Behaviors

At this point, the bulk of the remaining work is the implementation of constructors and methods for calculating the points needed to describe the sides of a cube. For the most part, I used the constructors to initialize fields, and the side-drawing code is very similar for each of the sides. The most difficult part is the meticulous arithmetic necessary to ensure that all of the edges match. Listing 3 contains the remaining code (including the code already provided) for the Cube structure.

Listing 3: The complete Cube structure listing

Imports System
Imports System.Drawing
Imports System.Drawing.Drawing2D

Public Enum RotateHorizontal
    Left = -1
    Center = 0
    Right = 1
End Enum

Public Enum RotateVertical
    Up = -1
    Center = 0
    Down = 1
End Enum

Public Enum CubeSides
    Left
    Right
    Top
    Bottom
    Front
    Back
End Enum


Public Structure Cube
    Private FLocation As Point
    Private FHeight As Integer
    Private FWidth As Integer
    Private FDepth As Integer
    Private FCenter As Point
    Private FPath As GraphicsPath
    Private FRotateX As RotateHorizontal
    Private FRotateY As RotateVertical

    Public Property Location() As Point
        Get
            Return FLocation
        End Get
        Set(ByVal Value As Point)
            FLocation = Value
            Changed()
        End Set
    End Property

    Public Property Height() As Integer
        Get
            Return FHeight
        End Get
        Set(ByVal Value As Integer)
            FHeight = Value
            Changed()
        End Set
    End Property

    Public Property Width() As Integer
        Get
            Return FWidth
        End Get
        Set(ByVal Value As Integer)
            FWidth = Value
            Changed()
        End Set
    End Property

    Public Property Depth() As Integer
        Get
            Return FDepth
        End Get
        Set(ByVal Value As Integer)
            FDepth = Value
            Changed()
        End Set
    End Property

    Public Property Center() As Point
        Get
            Return FCenter
        End Get
        Set(ByVal Value As Point)
            FCenter = Value
            Changed()
        End Set
    End Property

    Public ReadOnly Property Path() As GraphicsPath
        Get
            Return FPath
        End Get
    End Property

    Public Property RotateX() As RotateHorizontal
        Get
            Return FRotateX
        End Get
        Set(ByVal Value As RotateHorizontal)
            FRotateX = Value
            Changed()
        End Set
    End Property

    Public Property RotateY() As RotateVertical
        Get
            Return FRotateY
        End Get
        Set(ByVal Value As RotateVertical)
            FRotateY = Value
            Changed()
        End Set
    End Property

    Public ReadOnly Property X() As Integer
        Get
            Return FLocation.X
        End Get
    End Property

    Public ReadOnly Property Y() As Integer
        Get
            Return FLocation.Y
        End Get
    End Property

    'Return the rectangle that bounds the entire polygon
    'representing the cube
    Public ReadOnly Property BoundsRect() As Rectangle
        Get
            If (FPath Is Nothing) Then
                Return New Rectangle(0, 0, 0, 0)
            Else
                Dim r As RectangleF = Path.GetBounds()
                ' Implicit conversion from single to integer,
                ' really only available in VB
                Return New Rectangle(r.X, r.Y, r.Width, r.Height)
            End If
        End Get
    End Property

    Public Property Size() As CubeSize
        Get
            Return New CubeSize(FWidth, FHeight, FDepth)
        End Get
        Set(ByVal Value As CubeSize)
            FWidth = Value.Width
            FHeight = Value.Height
            FDepth = Value.Depth
            Changed()
        End Set
    End Property

    Public ReadOnly Property Item(ByVal index As CubeSides) -
           As Point()
        Get
            Select Case index
                Case CubeSides.Back
                    Return Back
                Case CubeSides.Front
                    Return Front
                Case CubeSides.Left
                    Return Left
                Case CubeSides.Right
                    Return Right
                Case CubeSides.Top
                    Return Top
                Case CubeSides.Bottom
                    Return Bottom
                Case Else
                    Return Front
            End Select
        End Get
    End Property

    Public ReadOnly Property Top() As Point()
        Get
            Return GetTop(Location, Height, Width, Depth, _
                          RotateX, RotateY)
        End Get
    End Property

    Public ReadOnly Property Bottom() As Point()
        Get
            Return GetBottom(Location, Height, Width, Depth, _
                             RotateX, RotateY)
        End Get
    End Property

    Public ReadOnly Property Left() As Point()
        Get
            Return GetLeft(Location, Height, Width, Depth, _
                           RotateX, RotateY)
        End Get
    End Property

    Public ReadOnly Property Right() As Point()
        Get
            Return GetRight(Location, Height, Width, Depth, _
                            RotateX, RotateY)
        End Get
    End Property

    Public ReadOnly Property Front() As Point()
        Get
            Return GetFront(Location, Height, Width, Depth, _
                            RotateX, RotateY)
        End Get
    End Property

    Public ReadOnly Property Back() As Point()
        Get
            Return GetBack(Location, Height, Width, Depth, _
                           RotateX, RotateY)
        End Get
    End Property

    Public Sub New(ByVal x As Integer, ByVal Y As Integer, _
        ByVal height As Integer, ByVal width As Integer, _
        ByVal depth As Integer, ByVal rotateX As RotateHorizontal, _
        ByVal rotateY As RotateVertical)

        FPath = New GraphicsPath
        FLocation = New Point(x, Y)
        FHeight = height
        FWidth = width
        FDepth = depth
        FRotateX = rotateX
        FRotateY = rotateY
        FCenter = New Point(Location.X + (width + depth / 2 * _
                                          rotateX) / 2, _
            Location.Y + (height + depth / 2 * rotateY) / 2)

        ConstructPath()

    End Sub

    Public Sub New(ByVal x As Integer, ByVal Y As Integer, _
            ByVal height As Integer, ByVal width As Integer, _
            ByVal depth As Integer)

        FPath = New GraphicsPath
        FLocation = New Point(x, Y)
        FHeight = height
        FWidth = width
        FDepth = depth
        FRotateX = RotateHorizontal.Right
        FRotateY = RotateVertical.Up
        FCenter = New Point(Location.X + (width + depth / 2 * _
                                          rotateX) / 2, _
            Location.Y + (height + depth / 2 * rotateY) / 2)

        ConstructPath()

    End Sub

    Public Sub New(ByVal point As Point, _
        ByVal height As Integer, ByVal width As Integer, _
        ByVal depth As Integer)

        FPath = New GraphicsPath
        FLocation = point
        FHeight = Height
        FWidth = Width
        FDepth = Depth
        FRotateX = RotateHorizontal.Right
        FRotateY = RotateVertical.Up
        FCenter = New Point(Location.X + (Width + Depth / 2 * _
                                          RotateX) / 2, _
            Location.Y + (Height + Depth / 2 * RotateY) / 2)

        ConstructPath()

    End Sub

    Public Sub New(ByVal point As Point, ByVal size As CubeSize)

        FPath = New GraphicsPath
        FLocation = point
        FHeight = size.Height
        FWidth = size.Width
        FDepth = size.Depth
        FRotateX = RotateHorizontal.Right
        FRotateY = RotateVertical.Up
        FCenter = New Point(Location.X + (width + depth / 2 * _
                            RotateX) / 2, _
            Location.Y + (height + depth / 2 * RotateY) / 2)

        ConstructPath()

    End Sub

    Private Sub Changed()
        ConstructPath()
    End Sub

    Private Sub ConstructPath()
        FPath = New GraphicsPath
        Path.AddLines(GetBack(Location, Height, Width, Depth, _
                      RotateX, RotateY))
        Path.AddLines(GetFront(Location, Height, Width, Depth, _
                      RotateX, RotateY))
        Path.AddLines(GetTop(Location, Height, Width, Depth, _
                      RotateX, RotateY))
        Path.AddLines(GetBottom(Location, Height, Width, Depth, _
                      RotateX, RotateY))
        Path.AddLines(GetLeft(Location, Height, Width, Depth, _
                      RotateX, RotateY))
        Path.AddLines(GetRight(Location, Height, Width, Depth, _
                      RotateX, RotateY))
    End Sub

    Private Function GetXFromCenter(ByVal newCenter As Point) _
            As Integer
        Return newCenter.X - (FWidth + FDepth / 2 * FRotateX) / 2
    End Function

    Private Function GetYFromCenter(ByVal newCenter As Point) _
            As Integer
        Return newCenter.Y - (FHeight + FDepth / 2 * FRotateY) / 2
    End Function

    Public Sub FilleRectangle(ByVal boundingRect As Rectangle)
        Dim x As Integer
        If (FRotateX = RotateHorizontal.Right) Then
            x = 0
        Else
            x = Math.Abs(Depth / 2 * FRotateX)
        End If

        Dim y As Integer
        If (FRotateY = RotateVertical.Down) Then
            y = 0
        Else
            y = Math.Abs(Depth / 2 * RotateY)
        End If

        FLocation = New Point(x, y)
        FWidth = boundingRect.Width - Depth / 2 - 1
        FHeight = boundingRect.Height - Depth / 2 - 1
        ConstructPath()
    End Sub

    Public Function GetCube() As GraphicsPath
        Return FPath
    End Function

    Public Function GetSides(ByVal theseSides As CubeSides()) _
           As GraphicsPath
        Dim newPath As GraphicsPath = New GraphicsPath
        Dim I As Integer
        For I = 0 To theseSides.Length - 1
            newPath.AddLines(Item(theseSides(I)))
        Next I

        Return newPath
    End Function

    Public Shared Function GetXOffset(ByVal depth As Integer, _
        ByVal rotateX As RotateHorizontal) As Integer

        Return depth / 2 * rotateX
    End Function

    Public Shared Function GetYOffset(ByVal depth As Integer, _
        ByVal rotateY As RotateVertical) As Integer

        Return depth / 2 * rotateY
    End Function

    Public Shared Function GetTop(ByVal point As Point, _
                                  ByVal height As Integer, _
                                  ByVal width As Integer, _
                                  ByVal depth As Integer, _
                                  ByVal rotateX As RotateHorizontal, _
                                  ByVal rotateY As RotateVertical) _
                                        As Point()

        Return New Point() { _
            New Point(point.X, point.Y), _
New Point(point.X + GetXOffset(depth, rotateX), point.Y + _
GetYOffset(depth, rotateY)), _
            New Point(point.X + width + GetXOffset(depth, rotateX), _
point.Y + GetYOffset(depth, rotateY)), _
            New Point(point.X + width, point.Y), _
            New Point(point.X, point.Y)}
    End Function

    Public Shared Function GetLeft(ByVal point As Point, _
           ByVal height As Integer, _
    ByVal width As Integer, ByVal depth As Integer, _
          ByVal rotateX As RotateHorizontal, _
    ByVal rotateY As RotateVertical)

        Return New Point() {New Point(point.X, point.Y), _
            New Point(point.X + GetXOffset(depth, rotateX), _
                point.Y + GetYOffset(depth, rotateY)), _
            New Point(point.X + GetXOffset(depth, rotateX), _
                point.Y + GetYOffset(depth, rotateY) + height), _
            New Point(point.X, point.Y + height), _
            New Point(point.X, point.Y)}
    End Function

    Public Shared Function GetRight(ByVal point As Point, _
           ByVal height As Integer, _
        ByVal width As Integer, ByVal depth As Integer, _
        ByVal rotateX As RotateHorizontal, ByVal rotateY _
              As RotateVertical)

        Return New Point() {New Point(point.X + width, point.Y), _
            New Point(point.X + width + GetXOffset(depth, rotateX), _
                      point.Y + GetYOffset(depth, rotateY)), _
            New Point(point.X + width + GetXOffset(depth, rotateX), _
                      point.Y + GetYOffset(depth, rotateY) + height), _
            New Point(point.X + width, point.Y + height), _
            New Point(point.X + width, point.Y)}
    End Function

    Public Shared Function GetBottom(ByVal point As Point, _
           ByVal height As Integer, _
           ByVal width As Integer, ByVal depth As Integer, _
           ByVal rotateX As RotateHorizontal, _
           ByVal rotateY As RotateVertical) As Point()


        Return New Point() {New Point(point.X, point.Y + height), _
            New Point(point.X + GetXOffset(depth, rotateX), _
                point.Y + GetYOffset(depth, rotateY) + height), _
            New Point(point.X + width + GetXOffset(depth, rotateX), _
                point.Y + GetYOffset(depth, rotateY) + height), _
            New Point(point.X + width, point.Y + height), _
            New Point(point.X, point.Y + height)}
    End Function

    Public Shared Function GetFront(ByVal point As Point, _
           ByVal height As Integer, _
           ByVal width As Integer, ByVal depth As Integer, _
           ByVal rotateX As RotateHorizontal, ByVal rotateY _
           As RotateVertical) As Point()

        Return New Point() {point, New Point(point.X + width, point.Y), _
            New Point(point.X + width, point.Y + height), _
            New Point(point.X, point.Y + height), point}

    End Function

    Public Shared Function GetBack(ByVal point As Point, _
           ByVal height As Integer, _
           ByVal width As Integer, ByVal depth As Integer, _
           ByVal rotateX As RotateHorizontal, _
           ByVal rotateY As RotateVertical) As Point()

        Dim topLeft As Point = New Point(point.X + _
            GetXOffset(depth, rotateX), _
            point.Y + GetYOffset(depth, rotateY))

        Dim topRight As Point = New Point(point.X + width + _
            GetXOffset(depth, rotateX), point.Y + _
            GetYOffset(depth, rotateY))

        Dim bottomRight As Point = New Point(point.X + width + _
            GetXOffset(depth, rotateX), point.Y + height + _
            GetYOffset(depth, rotateY))

        Dim bottomLeft As Point = New Point(point.X + _
            GetXOffset(depth, rotateX), point.Y + height + _
            GetYOffset(depth, rotateY))

        Return New Point() {topLeft, topRight, bottomRight, _
                            bottomLeft, topLeft}

    End Function

End Structure

Create a 3D Cube Structure for GDI+

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.

Biography

Paul Kimmel is the VB Today columnist, has written several books on .NET programming, and is a software architect. You may contact him at pkimmel@softconcepts.com 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.



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Download the Information Governance Survey Benchmark Report to gain insights that can help you further establish business value in your Records and Information Management (RIM) program and across your entire organization. Discover how your peers in the industry are dealing with this evolving information lifecycle management environment and uncover key insights such as: 87% of organizations surveyed have a RIM program in place 8% measure compliance 64% cannot get employees to "let go" of information for …

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds