Creating Custom Controls from Scratch

Sooner or later, the Betty Crocker cookies just aren't exactly what you had in mind, and it is time to bake up a new control from scratch. In the previous examples, you derived first from an existing control type (e.g., Button) and then from UserControl. In the next example, you will derive from the base control type: Control.

Creating your control from scratch lets you manage the look and feel of your new control with precision. However, it also requires that you implement every aspect of the control, including painting it—that is, you get power, but at the cost of greater responsibility, a lesson to us all.

You'll create a control that provides the analog clock functionality you implemented in Chapter 10, but makes that functionality available to any program as a control — as easy to use as a button.

Begin by creating a new Windows Control Library project in Visual Studio .NET in your language of choice. Call it ClockFaceControl. When you first create the project, you'll be put into the design mode. Right-click on the form and choose View Code. Change the name of the source file from UserControl1.cs or .vb to ClockFaceControl.cs or .vb

Next, change the name of the class from UserControl1 to ClockFaceCtrl, and change the base class from UserControl to Control.

image with no caption

public class ClockFaceCtrl : System.Windows.Forms.Control

image with no caption

Public Class ClockFaceCtrl
    Inherits System.Windows.Forms.Control

After you make this change to the base class, go back to the designer. You'll find that there is no longer a visible form. The Control class does not provide the form functionality that UserControl provides, but this is fine; you'll add the UI explicitly in the OnPaint method.

The code for this control is nearly identical to that shown in Chapter 10. Here are the changes:

  • The ClockFaceControl derives from Control rather than Form.

  • None of the form code (e.g., Dispose or InitializeComponent) is needed.

  • Remove the mouse-down event handler (the control will not respond to this event). Thus, there is no need for the xCenter and yCenter member variables, and the center is set to the width and height divided by 2 and then left alone.

  • Expose TwentyFourHours as a Boolean property so the client can set a 24- or 12- hour format.

You can create the ClockFaceControl by lifting the code from Chapter 10 and dropping it into a new Control class. The complete source for the control is shown in Example 17-5 and Example 17-6.

Example 17-5. The ClockFace control (C#)

image with no caption

using System;
using System.Collections;
using System.Drawing;
using System.Drawing.Drawing2D;   // for LineCap enumerations
               using System.Timers;              // for onTimer event
using System.Windows.Forms;
   
namespace ClockFace
{
   
   public class ClockFaceCtrl : System.Windows.Forms.Control
   {
      private int FaceRadius = 700;
      private bool b24Hours = false;
      private DateTime currentTime;
      private static int DateRadius = 900;
      private static int offset = 0;
      Font font = new Font("Arial", 80);
      private StringDraw sdToday;
   
      public ClockFaceCtrl(  )
      {
         
         BackColor = SystemColors.Window;
         ForeColor = SystemColors.WindowText;
   
         string today = System.DateTime.Now.ToLongDateString(  );
         today = " " + today.Replace(",","");
         sdToday = new StringDraw(today,this);
   
   
         currentTime = DateTime.Now;
   
         System.Timers.Timer timer = new System.Timers.Timer(  );
         timer.Elapsed += 
                    new System.Timers.ElapsedEventHandler(OnTimer);
         timer.Interval = 50;
         timer.Enabled = true;
         
      }
   
      public bool TwentyFourHours
               {
               get { return b24Hours; }
               set { b24Hours = value; }
               }
                 
   
      public void OnTimer(Object source, ElapsedEventArgs e)
      {
   
         Graphics g = this.CreateGraphics(  );
         SetScale(g);
         DrawFace(g);
         DrawTime(g,false);
         DrawDate(g);
         g.Dispose(  );
      }
   
      protected override void OnPaint ( PaintEventArgs e )
      {
         base.OnPaint(e);
         Graphics g = e.Graphics;
         SetScale(g);
         DrawFace(g);
         DrawTime(g,true);
      }
   
      private void SetScale(Graphics g)
      {
         if ( Width =  = 0 || Height =  = 0 )
            return;
   
         // set the origin at the center
         g.TranslateTransform(Width / 2, Height / 2);
         // set inches to the minimum of the 
         // width or height divided
         // by the dots per inch  
         float inches = Math.Min(Width / g.DpiX, Height / g.DpiX);
   
         g.ScaleTransform(inches * g.DpiX / 2000, 
                       inches * g.DpiY / 2000);
      }
   
      
      private static float GetSin(float degAngle)
      {
         return (float) Math.Sin(Math.PI * degAngle / 180f);
      }
   
      private static float GetCos(float degAngle)
      {
         return (float) Math.Cos(Math.PI * degAngle / 180f);
      }
   
      private void DrawFace(Graphics g)
      {
   
         Brush brush = new SolidBrush(ForeColor);
         Brush greenBrush = new SolidBrush(Color.Green);
         
         float x, y;
   
         int numHours = b24Hours ? 24 : 12;
         int deg = 360 / numHours;
   
         for (int i = 1; i <= numHours; i++)
         {
            SizeF stringSize = g.MeasureString(
                             i.ToString(  ),font);
            x = GetCos(i*deg + 90) * FaceRadius;
            y = GetSin(i*deg + 90) * FaceRadius;
   
            StringFormat format = new StringFormat(  );
            format.Alignment = StringAlignment.Center;
            format.LineAlignment = StringAlignment.Center;
   
            if ( currentTime.Second  =  = i * 5)
               g.DrawString(i.ToString(  ), font, 
                                 greenBrush, -x, -y,format);
            else
               g.DrawString(i.ToString(  ), font, brush, -x,
                                 -y,format);
         }
      }
   
   
      private void DrawTime(Graphics g, bool forceDraw)
      {
   
         float hourLength = FaceRadius * 0.5f;
         float minuteLength = FaceRadius * 0.7f;
         float secondLength = FaceRadius * 0.9f;
   
         Pen hourPen = new Pen(BackColor);
         Pen minutePen = new Pen(BackColor);
         Pen secondPen = new Pen(BackColor);
   
         hourPen.EndCap = LineCap.ArrowAnchor;
         minutePen.EndCap = LineCap.ArrowAnchor;
   
         hourPen.Width = 30;
         minutePen.Width = 20;
   
         Brush secondBrush = new SolidBrush(Color.Green);
         Brush blankBrush = new SolidBrush(BackColor);
         
   
         float rotation;
         GraphicsState state;
   
         DateTime newTime = DateTime.Now;
         bool newMin = false;
   
         if ( newTime.Minute != currentTime.Minute )
            newMin = true;
   
         rotation = GetSecondRotation(  );
         state = g.Save(  );
         g.RotateTransform(rotation);
         g.FillEllipse(blankBrush,-25,-secondLength,50,50);
         g.Restore(state);
   
         if ( newMin  || forceDraw )
         {
            rotation = GetMinuteRotation(  );
            state = g.Save(  );
            g.RotateTransform(rotation);
            g.DrawLine(minutePen,0,0,0,-minuteLength);
            g.Restore(state);
   
            rotation = GetHourRotation(  );
            state = g.Save(  );
            g.RotateTransform(rotation);
            g.DrawLine(hourPen,0,0,0,-hourLength);
            g.Restore(state);
         }
   
   
         currentTime = newTime;
   
         hourPen.Color = Color.Red;
         minutePen.Color = Color.Blue;
         secondPen.Color = Color.Green;
   
         state = g.Save(  );
         rotation = GetSecondRotation(  );
         g.RotateTransform(rotation);
         g.FillEllipse(secondBrush,-25,-secondLength,50,50);
         g.Restore(state);
   
         if ( newMin || forceDraw )
         {
   
            state = g.Save(  );
            rotation = GetMinuteRotation(  );
            g.RotateTransform(rotation);
            g.DrawLine(minutePen,0,0,0,-minuteLength);
            g.Restore(state);
   
            state = g.Save(  );
            rotation = GetHourRotation(  );
            g.RotateTransform(rotation);
            g.DrawLine(hourPen,0,0,0,-hourLength);
            g.Restore(state);
         }
      }
   
   
      // determine the rotation to draw the hour hand
      private float GetHourRotation(  )
      {
         // degrees depend on 24 vs. 12 hour clock
         float deg = b24Hours ? 15 : 30;
         float numHours = b24Hours ? 24 : 12;
         return( 360f * currentTime.Hour / numHours +
            deg * currentTime.Minute / 60f);
      }
   
      private float GetMinuteRotation(  )
      {
         return( 360f * currentTime.Minute / 60f ); //+
         // 6f * currentTime.Second / 60f);
      }
   
      private float GetSecondRotation(  )
      {
         return(360f * currentTime.Second / 60f);
   
      }
   
      private class LtrDraw
      {
         char myChar;
         float x;
         float y;
         float oldx;
         float oldy;
         
   
         public LtrDraw(char c)
         {
            myChar = c;
         }
   
         public float X
         {
            get { return x; }
            set { oldx = x; x = value; }
         }
   
         public float Y
         {
            get { return y; }
            set { oldy = y; y = value; }
         }
   
         public float GetWidth(Graphics g, Font font)
         {
            SizeF stringSize = 
               g.MeasureString(myChar.ToString(  ),font);
            return stringSize.Width;
         }
   
         public float GetHeight(Graphics g, Font font)
         {
            SizeF stringSize = 
              g.MeasureString(myChar.ToString(  ),font);
            return stringSize.Height;
         }
   
         public void DrawString(
                     Graphics g, Brush brush, ClockFaceCtrl ctrl)
         {
            // Font font = new Font("Arial", 40); 
            Font font = ctrl.font;
            Brush blankBrush = 
               new SolidBrush(ctrl.BackColor);
            g.DrawString(
                myChar.ToString(  ),font,blankBrush,oldx,oldy);
            g.DrawString(myChar.ToString(  ),font,brush,x,y);
         }
      }   //  close for nested class LtrDraw
   
      private class StringDraw
      {
         ArrayList theString = new ArrayList(  );
         LtrDraw l;
         ClockFaceCtrl theControl;
   
         public StringDraw(string s, ClockFaceCtrl theControl)
         {
            this.theControl = theControl;
            foreach (char c in s)
            {
               l = new LtrDraw(c);
               theString.Add(l);
            }
         }
   
         public void DrawString(Graphics g, Brush brush)
         {
            int angle = 360 / theString.Count;
            int counter = 0;
   
            foreach (LtrDraw theLtr in theString)
            {
               float newX = GetCos(angle  * counter + 90 -
                 ClockFaceCtrl.offset) * ClockFaceCtrl.DateRadius ;
               float newY = GetSin(angle * counter + 90 -
                 ClockFaceCtrl.offset) * ClockFaceCtrl.DateRadius ;
               theLtr.X = newX - (theLtr.GetWidth(g,theControl.font) / 2);
               theLtr.Y = newY - (theLtr.GetHeight(g,theControl.font) / 2);
               counter++;
               theLtr.DrawString(g,brush,theControl);
            }
            ClockFaceCtrl.offset += 1;
         }
      }   //  close for nested class StringDraw
   
      private void DrawDate(Graphics g)
      {
         Brush brush = new SolidBrush(ForeColor);
         sdToday.DrawString(g,brush);
      }
   
   }
}

Example 17-6. The ClockFace control (VB.NET)

image with no caption

Imports System
Imports System.Collections
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Timers
Imports System.Windows.Forms
   
   
Namespace ClockFace
   
    Public Class ClockFaceCtrl
        Inherits System.Windows.Forms.Control
        Private FaceRadius As Integer = 700
        Private b24Hours As Boolean = False
        Private currentTime As DateTime
        Private Shared DateRadius As Integer = 900
        Private Shared offset As Integer = 0
        Private font As New font("Arial", 80)
        Private sdToday As StringDraw
   
        Public Sub New(  )
   
            BackColor = SystemColors.Window
            ForeColor = SystemColors.WindowText
   
            Dim today As String = System.DateTime.Now.ToLongDateString(  )
            today = " " + today.Replace(",", "")
            sdToday = New StringDraw(today, Me)
   
   
            currentTime = DateTime.Now
            Dim timer As New System.Timers.Timer(  )
            AddHandler timer.Elapsed, AddressOf OnTimer
            timer.Interval = 50
            timer.Enabled = True
        End Sub 'New 
   
   
        Public Property TwentyFourHours(  ) As Boolean
               Get
                Return b24Hours
               End Get
               Set(ByVal Value As Boolean)
                b24Hours = Value
               End Set
               End Property
   
        Public Sub OnTimer( _
          ByVal [source] As [Object], _
          ByVal e As ElapsedEventArgs)
   
            Dim g As Graphics = Me.CreateGraphics(  )
            'Brush brush = new SolidBrush(ForeColor);
            SetScale(g)
            DrawFace(g)
            DrawTime(g, False)
            DrawDate(g)
            g.Dispose(  )
        End Sub 'OnTimer
        ' DrawDate(g,brush); 
   
        Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
            MyBase.OnPaint(e)
            Dim g As Graphics = e.Graphics
            SetScale(g)
            DrawFace(g)
            DrawTime(g, True)
        End Sub 'OnPaint
   
   
        Private Sub SetScale(ByVal g As Graphics)
            If Width = 0 Or Height = 0 Then
                Return
            End If
            ' set the origin at the center
            g.TranslateTransform(Width / 2, Height / 2)
   
            Dim inches As Single = _
               Math.Min(Width / g.DpiX, Height / g.DpiX)
   
            g.ScaleTransform( _
               inches * g.DpiX / 2000, inches * g.DpiY / 2000)
        End Sub 'SetScale
   
   
        Private Shared Function _
           GetSin(ByVal degAngle As Single) As Single
            Return CSng(Math.Sin((Math.PI * degAngle / 180.0F)))
        End Function 'GetSin
   
   
        Private Shared Function _
           GetCos(ByVal degAngle As Single) As Single
            Return CSng(Math.Cos((Math.PI * degAngle / 180.0F)))
        End Function 'GetCos
   
   
        Private Sub DrawFace(ByVal g As Graphics)
   
            Dim brush = New SolidBrush(ForeColor)
            Dim greenBrush = New SolidBrush(Color.Green)
   
            Dim x, y As Single
   
            Dim numHours As Integer
            If b24Hours Then
                numHours = 24
            Else
                numHours = 12
            End If
   
            Dim deg As Integer = 360 / numHours
   
            Dim i As Integer
            For i = 1 To numHours
                Dim stringSize As SizeF = _
                  g.MeasureString(i.ToString(  ), font)
                x = GetCos((i * deg + 90)) * FaceRadius
                y = GetSin((i * deg + 90)) * FaceRadius
   
                Dim format As New StringFormat(  )
                format.Alignment = StringAlignment.Center
                format.LineAlignment = StringAlignment.Center
   
                If currentTime.Second = i * 5 Then
                    g.DrawString(i.ToString(  ), font, _
                      greenBrush, -x, -y, format)
                Else
                    g.DrawString(i.ToString(  ), font, _
                      brush, -x, -y, format)
                End If
            Next i
        End Sub 'DrawFace
   
        Private Sub DrawTime( _
          ByVal g As Graphics, ByVal forceDraw As Boolean)
   
            Dim hourLength As Single = FaceRadius * 0.5F
            Dim minuteLength As Single = FaceRadius * 0.7F
            Dim secondLength As Single = FaceRadius * 0.9F
   
            Dim hourPen As New Pen(BackColor)
            Dim minutePen As New Pen(BackColor)
            Dim secondPen As New Pen(BackColor)
   
            hourPen.EndCap = LineCap.ArrowAnchor
            minutePen.EndCap = LineCap.ArrowAnchor
   
            hourPen.Width = 30
            minutePen.Width = 20
   
            Dim secondBrush = New SolidBrush(Color.Green)
            Dim blankBrush = New SolidBrush(BackColor)
   
   
            Dim rotation As Single
            Dim state As GraphicsState
   
            Dim newTime As DateTime = DateTime.Now
            Dim newMin As Boolean = False
   
            If newTime.Minute <> currentTime.Minute Then
                newMin = True
            End If
   
            rotation = GetSecondRotation(  )
            state = g.Save(  )
            g.RotateTransform(rotation)
            g.FillEllipse(blankBrush, -25, -secondLength, 50, 50)
            g.Restore(state)
   
            If newMin Or forceDraw Then
   
                rotation = GetMinuteRotation(  )
                state = g.Save(  )
                g.RotateTransform(rotation)
                g.DrawLine(minutePen, 0, 0, 0, -minuteLength)
                g.Restore(state)
   
                rotation = GetHourRotation(  )
                state = g.Save(  )
                g.RotateTransform(rotation)
                g.DrawLine(hourPen, 0, 0, 0, -hourLength)
                g.Restore(state)
            End If
   
            currentTime = newTime
   
            hourPen.Color = Color.Red
            minutePen.Color = Color.Blue
            secondPen.Color = Color.Green
   
            state = g.Save(  )
            rotation = GetSecondRotation(  )
            g.RotateTransform(rotation)
            g.FillEllipse(secondBrush, -25, -secondLength, 50, 50)
            g.Restore(state)
   
            If newMin Or forceDraw Then
   
                state = g.Save(  )
                rotation = GetMinuteRotation(  )
                g.RotateTransform(rotation)
                g.DrawLine(minutePen, 0, 0, 0, -minuteLength)
                g.Restore(state)
   
                state = g.Save(  )
                rotation = GetHourRotation(  )
                g.RotateTransform(rotation)
                g.DrawLine(hourPen, 0, 0, 0, -hourLength)
                g.Restore(state)
            End If
        End Sub 'DrawTime
   
   
        ' determine the rotation to draw the hour hand
        Private Function GetHourRotation(  ) As Single
            ' degrees depend on 24 vs. 12 hour clock
            Dim deg As Single
            Dim numHours As Single
            If b24Hours Then
                deg = 15
                numHours = 24
            Else
                deg = 30
                numHours = 12
            End If
   
            Return 360.0F * currentTime.Hour / numHours + deg * _
              currentTime.Minute / 60.0F
        End Function 'GetHourRotation
   
   
        Private Function GetMinuteRotation(  ) As Single
            Return 360.0F * currentTime.Minute / 60.0F '+
        End Function 'GetMinuteRotation
        ' 6f * currentTime.Second / 60f);
   
        Private Function GetSecondRotation(  ) As Single
            Return 360.0F * currentTime.Second / 60.0F
        End Function 'GetSecondRotation
       _ 
   
        Private Class LtrDraw
            Private myChar As Char
            Private _x As Single
            Private _y As Single
            Private oldx As Single
            Private oldy As Single
   
   
            Public Sub New(ByVal c As Char)
                myChar = c
            End Sub 'New
   
            Public Property X(  ) As Single
                Get
                    Return _x
                End Get
                Set(ByVal Value As Single)
                    oldx = _x
                    _x = Value
                End Set
            End Property
   
            Public Property Y(  ) As Single
                Get
                    Return _y
                End Get
                Set(ByVal Value As Single)
                    oldy = _y
                    _y = Value
                End Set
            End Property
   
            Public Function GetWidth( _
              ByVal g As Graphics, ByVal font As font) _
              As Single
                Dim stringSize As SizeF = _
                  g.MeasureString(myChar.ToString(  ), font)
                Return stringSize.Width
            End Function 'GetWidth
   
   
            Public Function GetHeight( _
              ByVal g As Graphics, ByVal font As font) _
              As Single
                Dim stringSize As SizeF = _
                  g.MeasureString(myChar.ToString(  ), font)
                Return stringSize.Height
            End Function 'GetHeight
   
   
            Public Sub DrawString( _
               ByVal g As Graphics, ByVal brush As Brush, _
               ByVal ctrl As ClockFaceCtrl)
                ' Font font = new Font("Arial", 40); 
                Dim font As Font = ctrl.font
                Dim blankBrush = New SolidBrush(ctrl.BackColor)
                g.DrawString(myChar.ToString(  ), font, _
                  blankBrush, oldx, oldy)
                g.DrawString(myChar.ToString(  ), _
                  font, brush, X, Y)
            End Sub 'DrawString
        End Class 'LtrDraw
       _ 
   
        Private Class StringDraw
            Private theString As New ArrayList(  )
            Private l As LtrDraw
            Private theControl As ClockFaceCtrl
   
   
            Public Sub New(ByVal s As String, _
              ByVal theControl As ClockFaceCtrl)
                Me.theControl = theControl
                Dim c As Char
                For Each c In s
                    l = New LtrDraw(c)
                    theString.Add(l)
                Next c
            End Sub 'New
   
   
            Public Sub DrawString( _
              ByVal g As Graphics, ByVal brush As Brush)
                Dim angle As Integer = 360 / theString.Count
                Dim counter As Integer = 0
   
                Dim theLtr As LtrDraw
                For Each theLtr In theString
                    Dim newX As Single = _
                      GetCos((angle * counter + 90 - _
                      ClockFaceCtrl.offset)) * _
                      ClockFaceCtrl.DateRadius
   
                    Dim newY As Single = _
                      GetSin((angle * counter + 90 - _
                      ClockFaceCtrl.offset)) * _
                      ClockFaceCtrl.DateRadius
                    theLtr.X = newX - theLtr.GetWidth( _
                       g, theControl.font) / 2
                    theLtr.Y = newY - theLtr.GetHeight( _
                       g, theControl.font) / 2
                    counter += 1
                    theLtr.DrawString(g, brush, theControl)
                Next theLtr
                ClockFaceCtrl.offset += 1
            End Sub 'DrawString
        End Class 'StringDraw
   
   
        Private Sub DrawDate(ByVal g As Graphics)
            Dim brush = New SolidBrush(ForeColor)
            sdToday.DrawString(g, brush)
        End Sub 'DrawDate
    End Class 'ClockFaceCtrl 
End Namespace 'ClockFace

Testing the custom control

As you did with the UserControl, you'll test the custom control by creating a testing project in the same solution as the control. Call the new test project ClockFaceTester. Add a reference to your control, and then update the toolbar to include the control (you may need to browse to the dll for the control). Drag the control onto the form, name it clockFace, and then resize the form to fit.

Finally, drag a button onto the form, set its text to 24 Hours, and set its name to btn24. Your form should now look like Figure 17-14.

Clock control test form

Figure 17-14. Clock control test form

You can see already that the clock control is working, but you'll want to set the 24-hour property programmatically. Double-click on the button to create its event handler:

image with no caption

private void btn24_Click(object sender, System.EventArgs e)
{
   if ( clockFace.TwentyFourHours )
   {
      btn24.Text = "24 Hours";
      clockFace.TwentyFourHours = false;
      clockFace.Invalidate(  );
   }
   else
   {
      btn24.Text = "12 Hours";
      clockFace.TwentyFourHours = true;
      clockFace.Invalidate(  );
   }
}
Private Sub btn24_Click( _
   ByVal sender As System.Object, ByVal e As System.EventArgs) _
   Handles btn24.Click
     If clockFace.TwentyFourHours Then
         btn24.Text = "24 Hours"
         clockFace.TwentyFourHours = False
         clockFace.Invalidate(  )
     Else
         btn24.Text = "12 Hours"
         clockFace.TwentyFourHours = True
         clockFace.Invalidate(  )
     End If
 End Sub

That's it! Set the tester program as the startup project and run it—you'll see the control executing on the form much as it did in Chapter 10, but this time it is a custom control you can drop on any form.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset