Create a Shaped 3D Control with GDI+

My previous VB Today column demonstrated how to define a custom three-dimensional shape primitive, a 3D cube structure, with .NET and GDI+. This article continues the discussion of GDI+ and demonstrates how you can convert such primitives into shaped controls, as you might desire to create non-rectilinear controls for custom Windows software development. (A couple of interesting general books on this subject are About Face and The Inmates are Running the Asylum, both by Alan Cooper of Visual Basic fame. While these books won't tell you how to create shaped controls in VB.NET, they are well written and enlightening on the subject of GUI design.)

This demonstration builds on the 3D cube structure from the first part of the previous VB Today column and layers a 3D Windows control on top of that structure. Along the way, you will learn about custom control design, surfacing constituent properties, using control attributes, and shaping controls. Both the new code and the original structure code are provided for convenience.

The goal is to convert a shaped structure into a shaped control. This means that the bounding region of the control matches the actual control. In contrast, most Windows controls are two-dimensional and rectangular. The example here is cubic, but the shape could be any polygon. The process is pretty much the same for any shaped control.

Defining the Control

The first step is to create a project that contains or references the assembly or source containing the 3D structure. Next, you need to surface constituent properties that you'd like consumers to be able to modify at design time and then use attributes to conceal public properties that are not suitable for modifying in the VS.NET Properties window.

Implementing Control Fields

Start by implementing control fields that you need and methods you'd like to have. The basic control already contains a significant number of events and fields that every custom control inherits. The ability to respond to click events or set the background color is inherent in every control, so you need to add only elements that are unique to your control. For your purposes, you need a field to capture the underlying cubic structure and an additional pen to indicate the drawing color of the shape's outline.

To begin your custom control and a test application, create a new Windows application solution. You'll use the default form for testing the control. Add a class library project to the solution with File|New|Project and select the Class Library project from the New Project dialog. You can delete the default class1.vb file generated from the template for class libraries. In the Solution Explorer (View|Solution Explorer), select the new class library project. Next, select Project|Add Inherited Control. This step opens the Add New Item dialog (see Figure 1). Change the default selection Inherited User Control (in the list of templates) to Custom Control, and change the Name input field to Cube3d. Click Open.

Figure 1: Add a custom control to the class library that will contain the 3D cube control.

You certainly could use an empty class and code all of the elements by hand, but using project templates is a lot easier. (Refer to my book Advanced Visual Basic .NET Power Coding [Addison-Wesley, 2003] for more on project templates.)

At this point, the source code for the 3D cube control should look like the code in Listing 1.

Listing 1: An empty custom control created from VS.NET's project item templates.

Public Class Cube3d
   Inherits System.Windows.Forms.Control

#Region " Component Designer generated code "

   Public Sub New()
      MyBase.New()

      ' This call is required by the Component Designer.
      InitializeComponent()

      'Add any initialization after the InitializeComponent() call

   End Sub

   'Control overrides dispose to clean up the component list.
   Protected Overloads Overrides Sub Dispose(ByVal disposing _
                                             As Boolean)
      If disposing Then
         If Not (components Is Nothing) Then components.Dispose()
         End If
      End If
      MyBase.Dispose(disposing)
   End Sub

   'Required by the Control Designer
   Private components As System.ComponentModel.IContainer

   ' NOTE: The following procedure is required by the Component
   ' Designer
   ' It can be modified using the Component Designer.  Do not
   ' modify it using the code editor.
   <System.Diagnostics.DebuggerStepThrough()> _
      Private Sub InitializeComponent()
      components = New System.ComponentModel.Container()
   End Sub

#End Region

    Protected Overrides Sub OnPaint(ByVal pe As System.Windows._
                                    Forms.PaintEventArgs)
      MyBase.OnPaint(pe)

      'Add your custom paint code here
    End Sub

End Class

For now, you can ignore everything between the #Region directive. (Unless I am referring to a modification to generated code between the #Region directive, this code will be elided in future listings to save space.) However, note that this is a sizeable chunk that you didn't have to write because you used the template.

The next step is to add Import statements for namespaces you will be using, and incorporate the fields discussed at the beginning of this section. Listing 2 shows the modified code with the collapsed #Region code.

Listing 2: Add the imports statement and fields.

Imports System
Imports System.Collections
Imports System.ComponentModel
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Data
Imports System.Windows.Forms

Public Class Cube3d
   Inherits System.Windows.Forms.Control

[ Component Designer generated code ]
   Private FPen As pen = New pen(Color.Black)
   Private FCube As Cube

   Protected Overrides Sub OnPaint(ByVal pe As System.Windows. _
                                   Forms.PaintEventArgs)
      MyBase.OnPaint(pe)

      'Add your custom paint code here
   End Sub

End Class

All of the Imports statements and the two private fields, FPen and FCube, were added (shown in bold). F is a prefix convention that I use. F indicates a field, and the F-prefix is dropped to contrive the property name. VB.NET is strongly typed, so you don't benefit from using type prefix notations like int, str, or the like. In fact, Microsoft no longer encourages the use of prefixes. (Some people like to use m_ to indicate that a field is a member of a class, but membership is implied. All you have to do is distinguish fields from properties, and consistency is most important.)

The Pen will be the color of the cube's outline, and the FCube field refers to the 3D cube defined and referred to previously. (The complete Listing for the Cube structure is provided at the end of this article in the section Primitive 3D Cube Code Reference.) Next, you need to define some behaviors.

Create a Shaped 3D Control with GDI+

Implementing Control Methods

Listing 3: Custom control methods.

Imports System
Imports System.Collections
Imports System.ComponentModel
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Data
Imports System.Windows.Forms

Public Class Cube3d
   Inherits System.Windows.Forms.Control

#Region " Component Designer generated code "

   Public Sub New()
      MyBase.New()

      SetStyle(ControlStyles.SupportsTransparentBackColor Or _
            ControlStyles.ResizeRedraw, True)

      InitializeComponent()

      FCube = New Cube(CubeX, CubeY, CubeHeight, CubeWidth, _
                       CubeDepth)

   End Sub
#End Region

   Private FPen As pen = New pen(Color.Black)
   Private FCube As Cube

   Protected Overrides Sub OnPaint(ByVal e _
      As System.Windows.Forms.PaintEventArgs)
      MyBase.OnPaint(e)
      Dim path As GraphicsPath = FCube.GetCube()
      e.Graphics.DrawPath(FPen, path)
      Me.Region = EnclosingRegion()
   End Sub

   Public Function EnclosingRegion() As Region
      Dim copy As Cube3D = Clone()
      copy.ScaleCube(1, 1, 1)
      Dim path As GraphicsPath = copy.Cube.GetCube()
      path.FillMode = FillMode.Winding
      Return New Region(path)
   End Function

   Public Function Clone() As Cube3d
      Dim copy As Cube3D = New Cube3d
      copy.SetCubeBounds(CubeX, CubeY, CubeWidth, CubeHeight, _
                         CubeDepth)
      Return copy
   End Function

   Public Sub ScaleCube(ByVal x As Integer, ByVal y As Integer, _
                        ByVal z As Integer)
      CubeLocation = New Point(CubeX - x, CubeY - y)
      CubeWidth += 2 * x
      CubeHeight += 2 * y
      CubeDepth += 2 * z
   End Sub

   Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
      MyBase.OnResize(e)
      ResizeCubeStructure()
   End Sub

   Public Sub SetCubeBounds(ByVal x As Integer, _
      ByVal y As Integer, _ ByVal width As Integer, _
      ByVal height As Integer, ByVal depth As Integer)
      FCube.Location = New Point(x, y)
      FCube.Width = width
      FCube.Height = height
      FCube.Depth = depth
      Invalidate()
   End Sub

   Private Sub ResizeCubeStructure()
      FCube.FillRectangle(Bounds)
      Invalidate()
   End Sub
' elided intentionally!

The Listing 3 code modifies the constructor (Sub New) and OnPaint, and adds EnclosingRegion, Clone, ScaleCube, OnResize, SetCubeBounds, and ResizeCubeStructure. In the constructor, it adds a call to SetStyle. Passing an Or'd list of enumerated values permits you to customize the control style. For this demonstration, I elected to permit background transparency and enable automatic redrawing when the control is resized. The former permits see-through controls, and the latter ensures that the underlying cube representation adjusts when the control is resized.

For custom controls, the only method you really need to override is the OnPaint method. The OnPaint method is where you give a custom control its unique visual character. Your OnPaint method obtains the GraphicsPath—think array of points—that represents the cube, draws the cube, and then changes the control's region to match that of the cube shape rather than the rectangular control. (Refer to Shaping the Control for more information on using the Region property.)

The EnclosingRegion method plays a little game. Essentially, you obtain a copy of the cube and expand it three-dimensionally by one pixel, providing enough room for the cube outline. You use a copy because you don't want to change the cube itself; you just want a little more room for the clip region—visible area—of the cube. The Clone method creates a new instance, a copy, of the cube itself.

ScaleCube accepts three points and adjusts the location, width, height, and depth. (Multiplying width and height by 2 accounts for the offsetting change to x and y.) As well as permitting the cube to shrink and swell, you use this method to permit enough room to ensure that the cube control's outline is visible along the edges. (Scaling the cube to make room for the visual image is a bit dodgy, as my friend Tony Cowan would say, and could stand some improvement.)

The OnResize method is overridden to ensure that when the control is resized the underlying cube is resized relatively. Without this method, the actual dimensions of the cube could be greater or less than the size of the control, breaking the illusion of the control conforming to the primitive's dimensions.

Finally, SetCubeBounds accounts for the depth dimension and invalidates the control, so that it is redrawn, and ResizeCubeStructure shrinks or grows the cube to fit the control's bounds and again causes the control to be redrawn.

Surfacing Constituent Properties

The 3D cube control is defined by a location and three Cartesian points representing height, width, and depth. While it makes sense to permit the consumer to modify the location and dimensions, permitting design-time modification of other properties probably makes no sense and may be error prone. Listing 4 contains only the properties that you will permit consumers to modify at design time. Simply add the code in Listing 4 to the Cube3D's class code. (The complete Listing is provided in Control Code Listing.)

Listing 4: The public, design-time modifiable properties of the 3D Cube control.

   Public Property CubeWidth() As Integer
      Get
         Return FCube.Width
      End Get
        Set(ByVal Value As Integer)
         FCube.Width = Value
         Invalidate()
      End Set
   End Property
   Public Property CubeHeight() As Integer
      Get
         Return FCube.Height
      End Get
      Set(ByVal Value As Integer)
         FCube.Height = Value
         Invalidate()
      End Set
   End Property
   Public Property CubeDepth() As Integer
      Get
         Return FCube.Depth
      End Get
      Set(ByVal Value As Integer)
         FCube.Depth = Value
         Invalidate()
      End Set
   End Property
   Public Property CubeCenter() As Point
      Get
         Return FCube.Center
      End Get
      Set(ByVal Value As Point)
         FCube.Center = Value
         Invalidate()
      End Set
   End Property
   Public Property CubeLocation() As Point
      Get
         Return FCube.Location
      End Get
      Set(ByVal Value As Point)
         FCube.Location = Value
         Invalidate()
      End Set
   End Property

If you want to change all of the cube's dimensions, call SetCubeBounds. Each of the cube's location and dimensional properties individually causes the cube to be redrawn. These properties, modifiable at design time, are CubeWidth, CubeHeight, CubeDepth, CubeCenter, and CubeLocation. CubeLocation moves the upper-left corner of the cube, and CubeCenter relocates the cube relative to its three-dimensional center. Each of these properties is implemented in terms of the underlying 3D cube structure.

Create a Shaped 3D Control with GDI+

Preventing Properties from Appearing in the Properties Window

Other properties were removed from the designer either arbitrarily or because they cause design-time errors. For example, no property editor exists for Pen objects as yet (see my book Visual Basic .NET Power Coding [Addison-Wesley, 2003] for more on custom property editors); thus, the designer would not be able to adequately permit modifying a Pen at design time. (You probably could write a custom property editor in terms of a Pen color, but that's another article.)

Public properties are visible in the Properties window by default. As Listing 5 shows, to make a property invisible, you tag the property with the BrowsableAttribute, passing False to the attribute. Remember that the Attribute suffix is dropped from attributes by convention.

Listing 5: Use the BrowsableAttribute initialized with False to remove properties from the design-time environment.

   <Browsable(False)> _
   Public Property CubeSize() As CubeSize
      Get
            Return FCube.Size
      End Get
         Set(ByVal Value As CubeSize)
            FCube.Size = Value
            Invalidate()
      End Set
   End Property
   <Browsable(False)> _
   Public ReadOnly Property CubeX() As Integer
      Get
         Return FCube.X
      End Get
   End Property
   <Browsable(False)> _
   Public ReadOnly Property CubeY() As Integer
      Get
         Return FCube.Y
      End Get
   End Property
   <Browsable(False)> _
      Public Property CubePen() As Pen
      Get
         Return FPen
      End Get
      Set(ByVal Value As Pen)
         FPen = Value
      End Set
   End Property
   <Browsable(False)> _
   Public ReadOnly Property Cube() As Cube
      Get
         Return FCube
      End Get
   End Property

The non-browsable properties in Listing 5 complete the implementation of the 3D Cube control.

Shaping the Control

.NET supports shaped controls directly. In earlier versions of VB, one had to have a pretty comprehensive understanding of device contexts and the Windows API to create shaped controls. While you'll find some pretty good articles on shaped VB6 Forms with the API, the conspicuous absence of information about non-rectilinear forms suggests that this topic is fairly obscure.

Controls in .NET have a Region property, which is an instance of the Region class. This class supports Transforms, which in turn are an instance of the Matrix class. The Region class permits shaping and the Transform property of the Region class makes it easier to twist, turn, and rotate regions. In this example, all you had to do was get the bounding region of the cube, assign that to the containing control, and violà! You have a shaped control. Because Forms are controls, you can use the same technique to create shaped Windows forms.

Defining a Toolbar Bitmap

VS.NET contains several designers and editors. One such designer is icon and bitmap designer. By adding a bitmap item (select Project|Add New Item|Bitmap File) to your project, you can add a bitmap and draw the bitmap for your control's Toolbar representation or add an existing bitmap to the project.

If you add a new bitmap file to your project, VS.NET opens the designer and makes a new Image menu available. The Image menu contains menu items and links to toolbars that permit drawing a bitmap from scratch. Figure 2 shows a bitmap that represents an approximation of the control's visual representation. The preview window (see Figure 2, middle) shows you how the bitmap will appear with its actual size, and the drawing window (see Figure 2, right) is a zoomed-in view that makes it easier to draw the bitmap.

Figure 2: Drawing a custom bitmap for your control's Toolbar appearance

To associate this bitmap with your custom control, you need to use the ToolboxBitmapAttribute and the path to the bitmap file or a resource type. (This demonstration uses the latter approach.) The bitmap can be a 16x16- or 32x32-pixel image. Listing 6 shows the ToolboxBitmapAttribute applied to your control class header.

Listing 6: Use the ToolboxBitmapAttribute to add a graphic to the toolbox along with your control.

Imports System
Imports System.Collections
Imports System.ComponentModel
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Data
Imports System.Windows.Forms

<ToolboxBitmap(GetType(Cube3d))> _
Public Class Cube3d
   Inherits System.Windows.Forms.Control

From my previously mentioned book, the following summary of instructions incorporates a toolbox graphic for your control:

  1. Select the class library from the solution explorer.
  2. Click File|Add New Item from VS.NET's menu.
  3. In the Add New Item dialog, pick the Bitmap File template, change the name to Cube3d.bmp, and click Open.
  4. Open the bitmap item (if it is not already open). In the Properties window, change the Height and Width to 16.
  5. Draw the bitmap.
  6. Use bright green for your background color (by selecting the Image|Show Colors Window and picking the neon green color).
  7. Select the Cube3d.bmp file in the Solution explorer, press F4 to display the Properties window, and change the Build Action to Embedded Resource (see Figure 3).
  8. Finally, specify the resource type (GetType(Cube3d) in the ToolboxBitmapAttribute for your control class.

Figure 3: Embedding a bitmap is an easy way to drag it along with your application.

(The IDE seems to be a bit particular about the image details, so much so that I did not gratuitously mention my book. Rather, I mentioned the book because I had to go back and check when the image did not show up in the toolbox the first time.)

The final steps include copying the cube3d.bmp to the folder containing the assembly (or including the full path in the ToolboxBitmapAttribute) or embedding the bitmap as a resource, compiling the library containing your custom control, and adding the library to the toolbox.

Create a Shaped 3D Control with GDI+

Adding the Control to the Toolbar

After you have compiled the class library containing the control, add it to the Toolbar in VS.NET. You can optionally add a new toolbar tab by selecting the Toolbox (View|Toolbox) and selecting Add Tab from the context menu (right-click over the Toolbox to display the context menu). I added a tab named Controls. To add the custom control, select the new (or an existing) tab, again displaying the toolbox context menu. This time, select Add/Remove Items from the context menu, displaying the Customize Toolbox dialog. On the .NET Framework Components tab, click Browse and navigate to the class library assembly containing the 3D Cube control.

Figure 4 shows the design-time view of the cube with the background color changed to blue.

Figure 4: The control shown in the toolbox and painted on a form at design time

Testing the Control

The final step is to drag and drop the control from the toolbox onto a form or user control. To test that the bounding region is limited to the cube shape, add a Click event handler. The event should fire only when you click within the boundaries of the control, as opposed to the rectangular boundary shown when the control is selected at design time.

Before you send me some e-mail about errors in the code, albeit minor ones, I'm warning you now: The control code contains errors. The cube bound and control boundary should be synonymous. This implies that setting the bounds of the cube and the control are identical. Additionally, adjusting the center of the cube should reposition the control. Currently, changing properties such as the CubeCenter makes the cube appear offset from the control, and the control does not resize or snap-to-fit automatically at design time. Another flaw is that the depth value is zero by default, so the cube looks like a rectangle when it is first dropped on a form. If you play with the RotateX and RotateY properties of the underlying cube, you will also notice that the cube does not render correctly in some configurations. These tweaks were intentionally left in place for you to play with if you'd like.

Hint: A minimalist approach to improving design flaws would lead one to remove all of the cube properties and accomplish everything through the outer control, specifically the resize event. A maximalist approach would include adding a Z or depth property, surfacing rotation properties, and incorporating the Z or Depth property into a three-dimensional size property. The latter would require a custom property editor for a three-dimensional size and the ScaleCube method would need to be modified to encompass the rotation factors.

The two remaining sections contain the complete code listing for the 3D Cube control and the 3D Cube structure, respectively. While I encourage you to review the previous VB Today column, I know you're busy so I included the complete listing here.

Control Code Listing

Listing 7 is the complete listing for the 3D Cube control.

Listing 7: The complete Listing for the 3D Cube Control

Imports System
Imports System.Collections
Imports System.ComponentModel
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Data
Imports System.Windows.Forms

<ToolboxBitmap("Cube3d.bmp")> _
Public Class Cube3d
   Inherits System.Windows.Forms.Control

#Region " Component Designer generated code "

   Public Sub New(ByVal Container _
                  As System.ComponentModel.IContainer)
      MyClass.New()

      'Required for Windows.Forms Class Composition Designer
      'support
      Container.Add(Me)
   End Sub

   Public Sub New()
      MyBase.New()

      SetStyle(ControlStyles.SupportsTransparentBackColor Or _
               ControlStyles.ResizeRedraw, True)

      InitializeComponent()

      FCube = New Cube(CubeX, CubeY, CubeHeight, CubeWidth, 
      CubeDepth)

   End Sub

   'Component overrides dispose to clean up the component list.
   Protected Overloads Overrides Sub Dispose( _
   ByVal disposing As Boolean)
      If disposing Then
         If Not (components Is Nothing) Then
            components.Dispose()
         End If
      End If
      MyBase.Dispose(disposing)
   End Sub

   'Required by the Component Designer
   Private components As System.ComponentModel.IContainer

   'NOTE: The following procedure is required by the
   'Component Designer
   'It can be modified using the Component Designer.
   'Do not modify it using the code editor.
   <System.Diagnostics.DebuggerStepThrough()> _
   Private Sub InitializeComponent()
        components = New System.ComponentModel.Container
   End Sub

#End Region

   Private FPen As Pen = New Pen(Color.Black)
   Private FCube As Cube

   Protected Overrides Sub OnPaint( _
   ByVal e As System.Windows.Forms.PaintEventArgs)
      MyBase.OnPaint(e)
      Dim path As GraphicsPath = FCube.GetCube()
      e.Graphics.DrawPath(FPen, path)
      Me.Region = EnclosingRegion()
   End Sub

   Public Function EnclosingRegion() As Region
      Dim copy As Cube3D = Clone()
      copy.ScaleCube(1, 1, 1)
      Dim path As GraphicsPath = copy.Cube.GetCube()
      path.FillMode = FillMode.Winding
      Return New Region(path)
   End Function

   Public Function Clone() As Cube3d
      Dim copy As Cube3D = New Cube3d
      copy.SetCubeBounds(CubeX, CubeY, CubeWidth, CubeHeight, 
      CubeDepth)
      Return copy
   End Function

   Public Sub ScaleCube(ByVal x As Integer, ByVal y As Integer, 
   ByVal z As Integer)
      CubeLocation = New Point(CubeX - x, CubeY - y)
      CubeWidth += 2 * x
      CubeHeight += 2 * y
      CubeDepth += 2 * z
   End Sub

   Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
      MyBase.OnResize(e)
      ResizeCubeStructure()
   End Sub

   Public Sub SetCubeBounds(ByVal x As Integer, _
      ByVal y As Integer, _
      ByVal width As Integer, ByVal height As Integer, _
      ByVal depth As Integer)
      FCube.Location = New Point(x, y)
      FCube.Width = width
      FCube.Height = height
      FCube.Depth = depth
      Invalidate()
   End Sub

   Private Sub ResizeCubeStructure()
      FCube.FillRectangle(Bounds)
      Invalidate()
   End Sub


   Public Property CubeWidth() As Integer
      Get
         Return FCube.Width
      End Get
      Set(ByVal Value As Integer)
         FCube.Width = Value
         Invalidate()
      End Set
   End Property
   Public Property CubeHeight() As Integer
      Get
         Return FCube.Height
      End Get
      Set(ByVal Value As Integer)
         FCube.Height = Value
         Invalidate()
      End Set
   End Property
   Public Property CubeDepth() As Integer
      Get
         Return FCube.Depth
      End Get
      Set(ByVal Value As Integer)
         FCube.Depth = Value
         Invalidate()
      End Set
   End Property
   Public Property CubeCenter() As Point
      Get
         Return FCube.Center
      End Get
      Set(ByVal Value As Point)
         FCube.Center = Value
         Invalidate()
      End Set
   End Property
   Public Property CubeLocation() As Point
      Get
         Return FCube.Location
      End Get
      Set(ByVal Value As Point)
         FCube.Location = Value
         Invalidate()
      End Set
   End Property

   <Browsable(False)> _
   Public Property CubeSize() As CubeSize
      Get
         Return FCube.Size
      End Get
      Set(ByVal Value As CubeSize)
         FCube.Size = Value
         Invalidate()
      End Set
   End Property
   <Browsable(False)> _
   Public ReadOnly Property CubeX() As Integer
      Get
         Return FCube.X
      End Get
   End Property
   <Browsable(False)> _
   Public ReadOnly Property CubeY() As Integer
      Get
         Return FCube.Y
      End Get
   End Property
   <Browsable(False)> _
      Public Property CubePen() As Pen
      Get
         Return FPen
      End Get
      Set(ByVal Value As Pen)
         FPen = Value
      End Set
   End Property
   <Browsable(False)> _
   Public ReadOnly Property Cube() As Cube
      Get
         Return FCube
      End Get
   End Property

End Class

Create a Shaped 3D Control with GDI+

Primitive 3D Cube Code Reference

Listing 8 is the underlying cube primitive and helper types.

Listing 8: The complete code Listing for the 3D Cube Structure and helper definitions

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 FillRectangle(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 depth As Integer, _
      ByVal width 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

Public Structure CubeSize
   Private FHeight As Integer
   Private FWidth As Integer
   Private FDepth As Integer

   Public Sub New(ByVal width As Integer, ByVal height As Integer, _
                  ByVal depth As Integer)
      FWidth = width
      FHeight = height
      FDepth = depth
   End Sub

   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

End Structure

Create a Shaped 3D Control with GDI+

Create Shaped Controls and Forms

GDI+ permits you to extend the .NET Framework to develop some interesting graphics and controls. One still has to do some legwork, but creating shaped controls and shaped forms is significantly easier. In the future, I expect to see many more interesting applications that deviate from the standard rectilinear style.

In this article, you learned about surfacing constituent properties, using control attributes, creating custom controls and shaped controls, and how to associate a bitmap with your custom controls using the ToolboxBitmap attribute. (You may have also learned that everyone writes a few bugs, so testing is critical.) I hope you enjoy creating shaped controls and forms.

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 kimmel@softconcepts.com if you need assistance developing software 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

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

  • On-demand Event Event Date: October 23, 2014 Despite the current "virtualize everything" mentality, there are advantages to utilizing physical hardware for certain tasks. This is especially true for backups. In many cases, it is clearly in an organization's best interest to make use of physical, purpose-built backup appliances rather than relying on virtual backup software (VBA - Virtual Backup Appliances). Join us for this webcast to learn why physical appliances are preferable to virtual backup appliances, …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds