Overview

 
This is a simple Triangle shape Control:


 

Figure 1. The Control.
 


It has several custom properties:


 

Figure 2. Design time Properties.
 


In creating the Control, I retained the (Name), BackColor, ForeColor, Location, and the hidden Size property. I added several custom properties, which are the expandable Internal Angles property, the maxHeight and maxWidth property, and a Rotation property. The expandable property takes 3 Decimal angle values, and works exactly the same as any standard expandable Control property, allowing editing via the individual angle properties or directly through the Group Property.
 
This is a fully functioning Custom Control, which is shaped, rotated, and scaled as specified in the properties. The Vertices of the Triangle are calculated with the Drawing2D.Matrix Class, where Rotation, Scaling, and Translating (offsetting) are applied. The Control is reshaped by setting it's Region property, and a border is drawn using the ForeColor.

 

The Control

 

The Control is just an extended Panel, and it's a very simple example having few properties.
This is the code in the Triangle Class:



 
Imports System.ComponentModel
Imports System.Windows.Forms
Imports System.Drawing
  
''' <summary>
''' The Triangle Class
''' </summary>
''' <remarks>
''' Here I've hidden the Size property from the Properties Window.
''' I added 4 new properties: maxWidth, maxHeight, Rotation, and the expandable 'angles' property which has a display name: Internal Angles
''' It's in this class where the control re-shaping and border painting takes place
'''</remarks>
<DesignerAttribute(GetType(MyControlDesigner))> <System.Serializable()> _
Public Class Triangle
    Inherits Panel
  
    <Browsable(False), _
    DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)> _
    Public Overloads Property Size() As Size
        Get
            Return New Size(_maxWidth, _maxHeight)
        End Get
        Set(ByVal value As Size)
  
        End Set
    End Property
  
    Private _maxWidth As Integer = 100
    Public Property maxWidth() As Integer
        Get
            Return _maxWidth
        End Get
        Set(ByVal value As Integer)
            _maxWidth = value
            MyBase.Size = New Size(_maxWidth, _maxHeight)
            angles = _angles
        End Set
    End Property
  
    Private _maxHeight As Integer = 100
    Public Property maxHeight() As Integer
        Get
            Return _maxHeight
        End Get
        Set(ByVal value As Integer)
            _maxHeight = value
            MyBase.Size = New Size(_maxWidth, _maxHeight)
            angles = _angles
        End Set
    End Property
  
    Private _rotation As Integer = 0
    Public Property Rotation() As Integer
        Get
            Return _rotation
        End Get
        Set(ByVal value As Integer)
            If value >= 0 AndAlso value < 360 Then
                _rotation = value
                angles = _angles
            Else
                Throw New ArgumentOutOfRangeException("Rotation", "Rotation angle must be in range 0 to 359")
            End If
        End Set
    End Property
  
  
    Private _angles As New InternalAngles
    <TypeConverter(GetType(epConverter)), _
    DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
    DisplayName("Internal Angles")> _
    Public Property angles() As InternalAngles
        Get
            Return _angles
        End Get
        Set(ByVal value As InternalAngles)
            _angles = value
            reShapeTriangle
        End Set
    End Property
  
    Protected Overrides Sub OnCreateControl()
        reShapeTriangle()
        MyBase.OnCreateControl()
    End Sub
  
    Private Sub reShapeTriangle()
        If angles.A + angles.B + angles.C = 180 Then
            Dim tp As New TrianglePoints(100, angles.A, angles.B, angles.C, maxWidth, maxHeight, Rotation)
            Dim points() As Point = New Point() {tp.A.ToPoint, tp.B.ToPoint, tp.C.ToPoint}
            Dim gp As New Drawing2D.GraphicsPath
            gp.AddPolygon(points)
            Me.Region = New Region(gp)
            Me.Invalidate()
        End If
    End Sub
  
    Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
        If angles.A + angles.B + angles.C = 180 Then
            Dim tp As New TrianglePoints(100, angles.A, angles.B, angles.C, maxWidth, maxHeight, Rotation)
            Dim points() As Point = New Point() {tp.A.ToPoint, tp.B.ToPoint, tp.C.ToPoint}
            e.Graphics.DrawPolygon(New Pen(Me.ForeColor, 5), points)
        End If
        MyBase.OnPaint(e)
    End Sub
  
End Class

 
 

The Expandable Property

 

The expandable Property is the angles Property which has a DisplayName: Internal Angles. It's the TypeConverter specified in the Attributes that enables the expandable Property to work.



 
Private _angles As New InternalAngles
<TypeConverter(GetType(epConverter)), _
DesignerSerializationVisibility(DesignerSerializationVisibility.Content), _
DisplayName("Internal Angles")> _
Public Property angles() As InternalAngles
    Get
        Return _angles
    End Get
    Set(ByVal value As InternalAngles)
        _angles = value
        reShapeTriangle
    End Set
End Property

 


This is the reShapeTriangle Method:




Private Sub reShapeTriangle()
 
    If angles.A + angles.B + angles.C = 180 Then
        Dim tp As New TrianglePoints(100, angles.A, angles.B, angles.C, maxWidth, maxHeight, Rotation)
        Dim points() As Point = New Point() {tp.A.ToPoint, tp.B.ToPoint, tp.C.ToPoint}
        Dim gp As New Drawing2D.GraphicsPath
        gp.AddPolygon(points)
        Me.Region = New Region(gp)
        Me.Invalidate()
    End If
End Sub

 


The type of the angles Property is InternalAngles. This is the InternalAngles Class:



 
Imports System.ComponentModel
  
''' <summary>
''' InternalAngles Class
''' </summary>
''' <remarks>
''' This contains the Expanded A, B, and C properties.
''' It also contains 2 constructors and an Overridden ToString Function
'''</remarks>
Public Class InternalAngles
  
    Private _A As Decimal
    <DesignerSerializationVisibility(DesignerSerializationVisibility.Visible), _
    NotifyParentProperty(True)> _
    Public Property A() As Decimal
        Get
            Return _A
        End Get
        Set(ByVal value As Decimal)
            If value + _B + _C <= 180 Then
                _A = value
            ElseIf value + _B + _C > 180 Then
                Throw New ArgumentOutOfRangeException("Internal Angles", "sum of angles cannot exceed 180")
            End If
        End Set
    End Property
  
    Private _B As Decimal
    <DesignerSerializationVisibility(DesignerSerializationVisibility.Visible), _
    NotifyParentProperty(True)> _
    Public Property B() As Decimal
        Get
            Return _B
        End Get
        Set(ByVal value As Decimal)
            If _A + value + _C <= 180 Then
                _B = value
            ElseIf _A + value + _C > 180 Then
                Throw New ArgumentOutOfRangeException("Internal Angles", "sum of angles cannot exceed 180")
            End If
        End Set
    End Property
  
    Private _C As Decimal
    <DesignerSerializationVisibility(DesignerSerializationVisibility.Visible), _
    NotifyParentProperty(True)> _
    Public Property C() As Decimal
        Get
            Return _C
        End Get
        Set(ByVal value As Decimal)
            If _A + _B + value <= 180 Then
                _C = value
            ElseIf _A + _B + value > 180 Then
                Throw New ArgumentOutOfRangeException("Internal Angles", "sum of angles cannot exceed 180")
            End If
        End Set
    End Property
  
    Public Sub New(ByVal A As Decimal, ByVal B As Decimal, ByVal C As Decimal)
        Me.A = A
        Me.B = B
        Me.C = C
    End Sub
  
    Public Sub New()
    End Sub
  
    Public Overrides Function ToString() As String
        Return String.Format("{0}, {1}, {2}", Me.A, Me.B, Me.C)
    End Function
  
End Class

 

 

This has 3 sub-Properties: A, B, and C, and you can see these by expanding the Internal Angles Property (see Figure 2.).
On changing any of these Properties, the parent (Internal Angles) Property is notified.

 

Calculating Vertices

 

Given 3 correct angles, it's possible to scale a triangle to any size. This is the Class I used to calculate the vertices:
 


Imports System.Drawing
  
Public Class TrianglePoints
    Public A As PointF
    Public B As PointF
    Public C As PointF
  
    ''' <summary>
    '''
    ''' </summary>
    ''' <param name="sideC">The proposed initial side AB length. For ratio calculating purposes.</param>
    ''' <param name="angleA">The internal angle in Degrees at vertex A</param>
    ''' <param name="angleB">The internal angle in Degrees at vertex B</param>
    ''' <param name="angleC">The internal angle in Degrees at vertex C</param>
    ''' <remarks></remarks>
    Public Sub New(ByVal sideC As Decimal, ByVal angleA As Decimal, ByVal angleB As Decimal, ByVal angleC As Decimal, _
                                        ByVal maxWidth As Decimal, ByVal maxHeight As Decimal, ByVal Rotation As Integer)
  
        'Convert Degrees to Radians
        angleA = CDec(angleA * (Math.PI / 180))
        angleB = CDec(angleB * (Math.PI / 180))
        angleC = CDec(angleC * (Math.PI / 180))
  
        'Calculate point B
        Dim bX As Decimal = CDec(Math.Cos(angleA) * sideC)
        Dim bY As Decimal = maxHeight - CDec(Math.Sin(angleA) * sideC)
  
        'Calculate relative side B length
        Dim sideB As Decimal = CDec((sideC / Math.Sin(angleC)) * Math.Sin(angleB))
  
        'Assign values to PointF structures
        Me.A = New PointF(0, maxHeight)
        Me.C = New PointF(sideB, maxHeight)
        Me.B = New PointF(bX, bY)
  
        'Array to hold PointF members
        Dim pts() As PointF = New PointF() {Me.A, Me.B, Me.C}
  
        'Create a Matrix and Rotate pts() Array
        Dim m As New Drawing2D.Matrix
        m.RotateAt(Rotation, New PointF(0, maxHeight))
        m.TransformPoints(pts)
  
        'Calculate maximum scale factor
        Dim actualWidth As Decimal = CDec(pts.Max(Function(p) p.X) - pts.Min(Function(p) p.X))
        Dim actualHeight As Decimal = CDec(pts.Max(Function(p) p.Y) - pts.Min(Function(p) p.Y))
  
        Dim scaleFactor As Decimal = Math.Min(maxWidth / actualWidth, maxHeight / actualHeight)
  
        'Scale pts() Array
        m = New Drawing2D.Matrix
        m.Scale(scaleFactor, scaleFactor)
        m.TransformPoints(pts)
  
        'Calculate offsets
        actualHeight = CDec(pts.Max(Function(p) p.Y) - pts.Min(Function(p) p.Y))
  
        Dim ox As Single = -pts.Min(Function(p) p.X)
        Dim oy As Single = -(pts.Min(Function(p) p.Y) - (maxHeight - actualHeight))
  
        'Offset pts() Array
        m = New Drawing2D.Matrix
        m.Translate(ox, oy)
        m.TransformPoints(pts)
  
        'Assign transformed Array values back to PointF member variables
        Me.A = pts(0)
        Me.B = pts(1)
        Me.C = pts(2)
    End Sub
  
End Class

 



Other Resources