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.
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.
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 16×16- or 32×32-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:
- Select the class library from the solution explorer.
- Click File|Add New Item from VS.NET’s menu.
- In the Add New Item dialog, pick the Bitmap File template, change the name to Cube3d.bmp, and click Open.
- Open the bitmap item (if it is not already open). In the Properties window, change the Height and Width to 16.
- Draw the bitmap.
- Use bright green for your background color (by selecting the Image|Show Colors Window and picking the neon green color).
- 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).
- 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.
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: 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
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 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 pkimmel@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.