Walkthrough: Creating a Windows Forms Control That Takes Advantage of Visual Studio Design-Time Features
The design-time experience for a custom control can be enhanced by authoring an associated custom designer.
This walkthrough illustrates how to create a custom designer for a custom control. You will implement a MarqueeControl type and an associated designer class, called MarqueeControlRootDesigner.
The MarqueeControl type implements a display similar to a theater marquee, with animated lights and flashing text.
The designer for this control interacts with the design environment to provide a custom design-time experience. With the custom designer, you can assemble a custom MarqueeControl implementation with animated lights and flashing text in many combinations. You can use the assembled control on a form like any other Windows Forms control.
Tasks illustrated in this walkthrough include:
Creating the Project
Creating a Control Library Project
Referencing the Custom Control Project
Defining a Custom Control and Its Custom Designer
Creating an Instance of Your Custom Control
Setting Up the Project for Design-Time Debugging
Implementing Your Custom Control
Creating a Child Control for Your Custom Control
Create the MarqueeBorder Child Control
Creating a Custom Designer to Shadow and Filter Properties
Handling Component Changes
Adding Designer Verbs to your Custom Designer
Creating a Custom UITypeEditor
Testing your Custom Control in the Designer
When you are finished, your custom control will look something like the following:
For the complete code listing, see How to: Create a Windows Forms Control That Takes Advantage of Design-Time Features.
Note
The dialog boxes and menu commands you see might differ from those described in Help depending on your active settings or edition. To change your settings, choose Import and Export Settings on the Tools menu. For more information, see Working with Settings.
Prerequisites
In order to complete this walkthrough, you will need:
- Sufficient permissions to be able to create and run Windows Forms application projects on the computer where Visual Studio is installed.
Creating the Project
The first step is to create the application project. You will use this project to build the application that hosts the custom control.
To create the project
- Create a Windows Forms Application project called "MarqueeControlTest." For more information, see How to: Create a New Windows Forms Application Project.
Creating a Control Library Project
The next step is to create the control library project. You will create a new custom control and its corresponding custom designer.
To create the control library project
Add a Windows Forms Control Library project to the solution. Name the project "MarqueeControlLibrary."
Using Solution Explorer, delete the project's default control by deleting the source file named "UserControl1.cs" or "UserControl1.vb", depending on your language of choice. For more information, see How to: Remove, Delete, and Exclude Items.
Add a new UserControl item to the MarqueeControlLibrary project. Give the new source file a base name of "MarqueeControl."
Using Solution Explorer, create a new folder in the MarqueeControlLibrary project. For more information, see How to: Add New Project Items. Name the new folder "Design."
Right-click the Design folder and add a new class. Give the source file a base name of "MarqueeControlRootDesigner."
You will need to use types from the System.Design assembly, so add this reference to the MarqueeControlLibrary project.
Note
To use the System.Design assembly, your project must target the full version of the .NET Framework, not the .NET Framework Client Profile. To change the target framework, see How to: Target a Specific .NET Framework Version or Profile.
Referencing the Custom Control Project
You will use the MarqueeControlTest project to test the custom control. The test project will become aware of the custom control when you add a project reference to the MarqueeControlLibrary assembly.
To reference the custom control project
- In the MarqueeControlTest project, add a project reference to the MarqueeControlLibrary assembly. Be sure to use the Projects tab in the Add Reference dialog box instead of referencing the MarqueeControlLibrary assembly directly.
Defining a Custom Control and Its Custom Designer
Your custom control will derive from the UserControl class. This allows your control to contain other controls, and it gives your control a great deal of default functionality.
Your custom control will have an associated custom designer. This allows you to create a unique design experience tailored specifically for your custom control.
You associate the control with its designer by using the DesignerAttribute class. Because you are developing the entire design-time behavior of your custom control, the custom designer will implement the IRootDesigner interface.
To define a custom control and its custom designer
Open the MarqueeControl source file in the Code Editor. At the top of the file, import the following namespaces:
Imports System Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Drawing Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Drawing; using System.Windows.Forms; using System.Windows.Forms.Design;
Add the DesignerAttribute to the MarqueeControl class declaration. This associates the custom control with its designer.
<Designer(GetType(MarqueeControlLibrary.Design.MarqueeControlRootDesigner), _ GetType(IRootDesigner))> _ Public Class MarqueeControl Inherits UserControl
[Designer( typeof( MarqueeControlLibrary.Design.MarqueeControlRootDesigner ), typeof( IRootDesigner ) )] public class MarqueeControl : UserControl {
Open the MarqueeControlRootDesigner source file in the Code Editor. At the top of the file, import the following namespaces:
Imports System Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing.Design Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing.Design; using System.Windows.Forms; using System.Windows.Forms.Design;
Change the declaration of MarqueeControlRootDesigner to inherit from the DocumentDesigner class. Apply the ToolboxItemFilterAttribute to specify the designer interaction with the Toolbox.
Note The definition for the MarqueeControlRootDesigner class has been enclosed in a namespace called "MarqueeControlLibrary.Design." This declaration places the designer in a special namespace reserved for design-related types.
Namespace MarqueeControlLibrary.Design <ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _ ToolboxItemFilterType.Require), _ ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _ ToolboxItemFilterType.Require)> _ <System.Security.Permissions.PermissionSetAttribute(System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _ Public Class MarqueeControlRootDesigner Inherits DocumentDesigner
namespace MarqueeControlLibrary.Design { [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)] [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)] [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] public class MarqueeControlRootDesigner : DocumentDesigner {
Define the constructor for the MarqueeControlRootDesigner class. Insert a WriteLine statement in the constructor body. This will be useful for debugging purposes.
Public Sub New() Trace.WriteLine("MarqueeControlRootDesigner ctor") End Sub
public MarqueeControlRootDesigner() { Trace.WriteLine("MarqueeControlRootDesigner ctor"); }
Creating an Instance of Your Custom Control
To observe the custom design-time behavior of your control, you will place an instance of your control on the form in MarqueeControlTest project.
To create an instance of your custom control
Add a new UserControl item to the MarqueeControlTest project. Give the new source file a base name of "DemoMarqueeControl."
Open the DemoMarqueeControl file in the Code Editor. At the top of the file, import the MarqueeControlLibrary namespace:
Imports MarqueeControlLibrary
using MarqueeControlLibrary;
Change the declaration of DemoMarqueeControl to inherit from the MarqueeControl class.
Build the project.
Open Form1 in the Windows Forms Designer.
Find the MarqueeControlTest Components tab in the Toolbox and open it. Drag a DemoMarqueeControl from the Toolbox onto your form.
Build the project.
Setting Up the Project for Design-Time Debugging
When you are developing a custom design-time experience, it will be necessary to debug your controls and components. There is a simple way to set up your project to allow debugging at design time. For more information, see Walkthrough: Debugging Custom Windows Forms Controls at Design Time.
To set up the project for design-time debugging
Right-click the MarqueeControlLibrary project and select Properties.
In the "MarqueeControlLibrary Property Pages" dialog box, select the Debug page.
In the Start Action section, select Start External Program. You will be debugging a separate instance of Visual Studio, so click the ellipsis () button to browse for the Visual Studio IDE. The name of the executable file is devenv.exe, and if you installed to the default location, its path is %programfiles%\Microsoft Visual Studio 9.0\Common7\IDE\devenv.exe.
Click OK to close the dialog box.
Right-click the MarqueeControlLibrary project and select "Set as StartUp Project" to enable this debugging configuration.
Checkpoint
You are now ready to debug the design-time behavior of your custom control. Once you have determined that the debugging environment is set up correctly, you will test the association between the custom control and the custom designer.
To test the debugging environment and the designer association
Open the MarqueeControlRootDesigner source file in the Code Editor and place a breakpoint on the WriteLine statement.
Press F5 to start the debugging session. Note that a new instance of Visual Studio is created.
In the new instance of Visual Studio, open the "MarqueeControlTest" solution. You can easily find the solution by selecting Recent Projects from the File menu. The "MarqueeControlTest.sln" solution file will be listed as the most recently used file.
Open the DemoMarqueeControl in the designer. Note that the debugging instance of Visual Studio acquires focus and execution stops at your breakpoint. Press F5 to continue the debugging session.
At this point, everything is in place for you to develop and debug your custom control and its associated custom designer. The remainder of this walkthrough will concentrate on the details of implementing features of the control and the designer.
Implementing Your Custom Control
The MarqueeControl is a UserControl with a little bit of customization. It exposes two methods: Start, which starts the marquee animation, and Stop, which stops the animation. Because the MarqueeControl contains child controls that implement the IMarqueeWidget interface, Start and Stop enumerate each child control and call the StartMarquee and StopMarquee methods, respectively, on each child control that implements IMarqueeWidget.
The appearance of the MarqueeBorder and MarqueeText controls is dependent on the layout, so MarqueeControl overrides the OnLayout method and calls PerformLayout on child controls of this type.
This is the extent of the MarqueeControl customizations. The run-time features are implemented by the MarqueeBorder and MarqueeText controls, and the design-time features are implemented by the MarqueeBorderDesigner and MarqueeControlRootDesigner classes.
To implement your custom control
Open the MarqueeControl source file in the Code Editor. Implement the Start and Stop methods.
Public Sub Start() ' The MarqueeControl may contain any number of ' controls that implement IMarqueeWidget, so ' find each IMarqueeWidget child and call its ' StartMarquee method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StartMarquee() End If Next cntrl End Sub Public Sub [Stop]() ' The MarqueeControl may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StopMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StopMarquee() End If Next cntrl End Sub
public void Start() { // The MarqueeControl may contain any number of // controls that implement IMarqueeWidget, so // find each IMarqueeWidget child and call its // StartMarquee method. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StartMarquee(); } } } public void Stop() { // The MarqueeControl may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StopMarquee // method. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StopMarquee(); } } }
Override the OnLayout method.
Protected Overrides Sub OnLayout(ByVal levent As LayoutEventArgs) MyBase.OnLayout(levent) ' Repaint all IMarqueeWidget children if the layout ' has changed. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) cntrl.PerformLayout() End If Next cntrl End Sub
protected override void OnLayout(LayoutEventArgs levent) { base.OnLayout (levent); // Repaint all IMarqueeWidget children if the layout // has changed. foreach( Control cntrl in this.Controls ) { if( cntrl is IMarqueeWidget ) { Control control = cntrl as Control; control.PerformLayout(); } } }
Creating a Child Control for Your Custom Control
The MarqueeControl will host two kinds of child control: the MarqueeBorder control and the MarqueeText control.
MarqueeBorder: This control paints a border of "lights" around its edges. The lights flash in sequence, so they appear to be moving around the border. The speed at which the lights flash is controlled by a property called UpdatePeriod. Several other custom properties determine other aspects of the control's appearance. Two methods, called StartMarquee and StopMarquee, control when the animation starts and stops.
MarqueeText: This control paints a flashing string. Like the MarqueeBorder control, the speed at which the text flashes is controlled by the UpdatePeriod property. The MarqueeText control also has the StartMarquee and StopMarquee methods in common with the MarqueeBorder control.
At design time, the MarqueeControlRootDesigner allows these two control types to be added to a MarqueeControl in any combination.
Common features of the two controls are factored into an interface called IMarqueeWidget. This allows the MarqueeControl to discover any Marquee-related child controls and give them special treatment.
To implement the periodic animation feature, you will use BackgroundWorker objects from the System.ComponentModel namespace. You could use Timer objects, but when many IMarqueeWidget objects are present, the single UI thread may be unable to keep up with the animation.
To create a child control for your custom control
Add a new class item to the MarqueeControlLibrary project. Give the new source file a base name of "IMarqueeWidget."
Open the IMarqueeWidget source file in the Code Editor and change the declaration from class to interface:
' This interface defines the contract for any class that is to ' be used in constructing a MarqueeControl. Public Interface IMarqueeWidget
// This interface defines the contract for any class that is to // be used in constructing a MarqueeControl. public interface IMarqueeWidget {
Add the following code to the IMarqueeWidget interface to expose two methods and a property that manipulate the marquee animation:
' This interface defines the contract for any class that is to ' be used in constructing a MarqueeControl. Public Interface IMarqueeWidget ' This method starts the animation. If the control can ' contain other classes that implement IMarqueeWidget as ' children, the control should call StartMarquee on all ' its IMarqueeWidget child controls. Sub StartMarquee() ' This method stops the animation. If the control can ' contain other classes that implement IMarqueeWidget as ' children, the control should call StopMarquee on all ' its IMarqueeWidget child controls. Sub StopMarquee() ' This method specifies the refresh rate for the animation, ' in milliseconds. Property UpdatePeriod() As Integer End Interface
// This interface defines the contract for any class that is to // be used in constructing a MarqueeControl. public interface IMarqueeWidget { // This method starts the animation. If the control can // contain other classes that implement IMarqueeWidget as // children, the control should call StartMarquee on all // its IMarqueeWidget child controls. void StartMarquee(); // This method stops the animation. If the control can // contain other classes that implement IMarqueeWidget as // children, the control should call StopMarquee on all // its IMarqueeWidget child controls. void StopMarquee(); // This method specifies the refresh rate for the animation, // in milliseconds. int UpdatePeriod { get; set; } }
Add a new Custom Control item to the MarqueeControlLibrary project. Give the new source file a base name of "MarqueeText."
Drag a BackgroundWorker component from the Toolbox onto your MarqueeText control. This component will allow the MarqueeText control to update itself asynchronously.
In the Properties window, set the BackgroundWorker component's WorkerReportsProgess and WorkerSupportsCancellation properties to true. These settings allow the BackgroundWorker component to periodically raise the ProgressChanged event and to cancel asynchronous updates. For more information, see BackgroundWorker Component.
Open the MarqueeText source file in the Code Editor. At the top of the file, import the following namespaces:
Imports System Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing Imports System.Threading Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing; using System.Threading; using System.Windows.Forms; using System.Windows.Forms.Design;
Change the declaration of MarqueeText to inherit from Label and to implement the IMarqueeWidget interface:
<ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _ ToolboxItemFilterType.Require)> _ Partial Public Class MarqueeText Inherits Label Implements IMarqueeWidget
[ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)] public partial class MarqueeText : Label, IMarqueeWidget {
Declare the instance variables that correspond to the exposed properties, and initialize them in the constructor. The isLit field determines if the text is to be painted in the color given by the LightColor property.
' When isLit is true, the text is painted in the light color; ' When isLit is false, the text is painted in the dark color. ' This value changes whenever the BackgroundWorker component ' raises the ProgressChanged event. Private isLit As Boolean = True ' These fields back the public properties. Private updatePeriodValue As Integer = 50 Private lightColorValue As Color Private darkColorValue As Color ' These brushes are used to paint the light and dark ' colors of the text. Private lightBrush As Brush Private darkBrush As Brush ' This component updates the control asynchronously. Private WithEvents backgroundWorker1 As BackgroundWorker Public Sub New() ' This call is required by the Windows.Forms Form Designer. InitializeComponent() ' Initialize light and dark colors ' to the control's default values. Me.lightColorValue = Me.ForeColor Me.darkColorValue = Me.BackColor Me.lightBrush = New SolidBrush(Me.lightColorValue) Me.darkBrush = New SolidBrush(Me.darkColorValue) End Sub 'New
// When isLit is true, the text is painted in the light color; // When isLit is false, the text is painted in the dark color. // This value changes whenever the BackgroundWorker component // raises the ProgressChanged event. private bool isLit = true; // These fields back the public properties. private int updatePeriodValue = 50; private Color lightColorValue; private Color darkColorValue; // These brushes are used to paint the light and dark // colors of the text. private Brush lightBrush; private Brush darkBrush; // This component updates the control asynchronously. private BackgroundWorker backgroundWorker1; public MarqueeText() { // This call is required by the Windows.Forms Form Designer. InitializeComponent(); // Initialize light and dark colors // to the control's default values. this.lightColorValue = this.ForeColor; this.darkColorValue = this.BackColor; this.lightBrush = new SolidBrush(this.lightColorValue); this.darkBrush = new SolidBrush(this.darkColorValue); }
Implement the IMarqueeWidget interface.
The StartMarquee and StopMarquee methods invoke the BackgroundWorker component's RunWorkerAsync and CancelAsync methods to start and stop the animation.
The Category and Browsable attributes are applied to the UpdatePeriod property so it appears in a custom section of the Properties window called "Marquee."
Public Overridable Sub StartMarquee() _ Implements IMarqueeWidget.StartMarquee ' Start the updating thread and pass it the UpdatePeriod. Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod) End Sub Public Overridable Sub StopMarquee() _ Implements IMarqueeWidget.StopMarquee ' Stop the updating thread. Me.backgroundWorker1.CancelAsync() End Sub <Category("Marquee"), Browsable(True)> _ Public Property UpdatePeriod() As Integer _ Implements IMarqueeWidget.UpdatePeriod Get Return Me.updatePeriodValue End Get Set(ByVal Value As Integer) If Value > 0 Then Me.updatePeriodValue = Value Else Throw New ArgumentOutOfRangeException("UpdatePeriod", "must be > 0") End If End Set End Property
public virtual void StartMarquee() { // Start the updating thread and pass it the UpdatePeriod. this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod); } public virtual void StopMarquee() { // Stop the updating thread. this.backgroundWorker1.CancelAsync(); } [Category("Marquee")] [Browsable(true)] public int UpdatePeriod { get { return this.updatePeriodValue; } set { if (value > 0) { this.updatePeriodValue = value; } else { throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0"); } } }
Implement the property accessors. You will expose two properties to clients: LightColor and DarkColor. The Category and Browsable attributes are applied to these properties, so the properties appear in a custom section of the Properties window called "Marquee."
<Category("Marquee"), Browsable(True)> _ Public Property LightColor() As Color Get Return Me.lightColorValue End Get Set(ByVal Value As Color) ' The LightColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then Me.lightColorValue = Value Me.lightBrush = New SolidBrush(Value) End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property DarkColor() As Color Get Return Me.darkColorValue End Get Set(ByVal Value As Color) ' The DarkColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then Me.darkColorValue = Value Me.darkBrush = New SolidBrush(Value) End If End Set End Property
[Category("Marquee")] [Browsable(true)] public Color LightColor { get { return this.lightColorValue; } set { // The LightColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.lightColorValue.ToArgb() != value.ToArgb()) { this.lightColorValue = value; this.lightBrush = new SolidBrush(value); } } } [Category("Marquee")] [Browsable(true)] public Color DarkColor { get { return this.darkColorValue; } set { // The DarkColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.darkColorValue.ToArgb() != value.ToArgb()) { this.darkColorValue = value; this.darkBrush = new SolidBrush(value); } } }
Implement the handlers for the BackgroundWorker component's DoWork and ProgressChanged events.
The DoWork event handler sleeps for the number of milliseconds specified by UpdatePeriod then raises the ProgressChanged event, until your code stops the animation by calling CancelAsync.
The ProgressChanged event handler toggles the text between its light and dark state to give the appearance of flashing.
' This method is called in the worker thread's context, ' so it must not make any calls into the MarqueeText control. ' Instead, it communicates to the control using the ' ProgressChanged event. ' ' The only work done in this event handler is ' to sleep for the number of milliseconds specified ' by UpdatePeriod, then raise the ProgressChanged event. Private Sub backgroundWorker1_DoWork( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles backgroundWorker1.DoWork Dim worker As BackgroundWorker = CType(sender, BackgroundWorker) ' This event handler will run until the client cancels ' the background task by calling CancelAsync. While Not worker.CancellationPending ' The Argument property of the DoWorkEventArgs ' object holds the value of UpdatePeriod, which ' was passed as the argument to the RunWorkerAsync ' method. Thread.Sleep(Fix(e.Argument)) ' The DoWork eventhandler does not actually report ' progress; the ReportProgress event is used to ' periodically alert the control to update its state. worker.ReportProgress(0) End While End Sub ' The ProgressChanged event is raised by the DoWork method. ' This event handler does work that is internal to the ' control. In this case, the text is toggled between its ' light and dark state, and the control is told to ' repaint itself. Private Sub backgroundWorker1_ProgressChanged( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.ProgressChangedEventArgs) _ Handles backgroundWorker1.ProgressChanged Me.isLit = Not Me.isLit Me.Refresh() End Sub
// This method is called in the worker thread's context, // so it must not make any calls into the MarqueeText control. // Instead, it communicates to the control using the // ProgressChanged event. // // The only work done in this event handler is // to sleep for the number of milliseconds specified // by UpdatePeriod, then raise the ProgressChanged event. private void backgroundWorker1_DoWork( object sender, System.ComponentModel.DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; // This event handler will run until the client cancels // the background task by calling CancelAsync. while (!worker.CancellationPending) { // The Argument property of the DoWorkEventArgs // object holds the value of UpdatePeriod, which // was passed as the argument to the RunWorkerAsync // method. Thread.Sleep((int)e.Argument); // The DoWork eventhandler does not actually report // progress; the ReportProgress event is used to // periodically alert the control to update its state. worker.ReportProgress(0); } } // The ProgressChanged event is raised by the DoWork method. // This event handler does work that is internal to the // control. In this case, the text is toggled between its // light and dark state, and the control is told to // repaint itself. private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e) { this.isLit = !this.isLit; this.Refresh(); }
Override the OnPaint method to enable the animation.
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) ' The text is painted in the light or dark color, ' depending on the current value of isLit. Me.ForeColor = IIf(Me.isLit, Me.lightColorValue, Me.darkColorValue) MyBase.OnPaint(e) End Sub
protected override void OnPaint(PaintEventArgs e) { // The text is painted in the light or dark color, // depending on the current value of isLit. this.ForeColor = this.isLit ? this.lightColorValue : this.darkColorValue; base.OnPaint(e); }
Press F6 to build the solution.
Create the MarqueeBorder Child Control
The MarqueeBorder control is slightly more sophisticated than the MarqueeText control. It has more properties and the animation in the OnPaint method is more involved. In principle, it is quite similar to the MarqueeText control.
Because the MarqueeBorder control can have child controls, it needs to be aware of Layout events.
To create the MarqueeBorder control
Add a new Custom Control item to the MarqueeControlLibrary project. Give the new source file a base name of "MarqueeBorder."
Drag a BackgroundWorker component from the Toolbox onto your MarqueeBorder control. This component will allow the MarqueeBorder control to update itself asynchronously.
In the Properties window, set the BackgroundWorker component's WorkerReportsProgess and WorkerSupportsCancellation properties to true. These settings allow the BackgroundWorker component to periodically raise the ProgressChanged event and to cancel asynchronous updates. For more information, see BackgroundWorker Component.
In the Properties window, click the Events button. Attach handlers for the DoWork and ProgressChanged events.
Open the MarqueeBorder source file in the Code Editor. At the top of the file, import the following namespaces:
Imports System Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Drawing Imports System.Drawing.Design Imports System.Threading Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Drawing; using System.Drawing.Design; using System.Threading; using System.Windows.Forms; using System.Windows.Forms.Design;
Change the declaration of MarqueeBorder to inherit from Panel and to implement the IMarqueeWidget interface.
<Designer(GetType(MarqueeControlLibrary.Design.MarqueeBorderDesigner)), _ ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _ ToolboxItemFilterType.Require)> _ Partial Public Class MarqueeBorder Inherits Panel Implements IMarqueeWidget
[Designer(typeof(MarqueeControlLibrary.Design.MarqueeBorderDesigner ))] [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)] public partial class MarqueeBorder : Panel, IMarqueeWidget {
Declare two enumerations for managing the MarqueeBorder control's state: MarqueeSpinDirection, which determines the direction in which the lights "spin" around the border, and MarqueeLightShape, which determines the shape of the lights (square or circular). Place these declarations before the MarqueeBorder class declaration.
' This defines the possible values for the MarqueeBorder ' control's SpinDirection property. Public Enum MarqueeSpinDirection CW CCW End Enum ' This defines the possible values for the MarqueeBorder ' control's LightShape property. Public Enum MarqueeLightShape Square Circle End Enum
// This defines the possible values for the MarqueeBorder // control's SpinDirection property. public enum MarqueeSpinDirection { CW, CCW } // This defines the possible values for the MarqueeBorder // control's LightShape property. public enum MarqueeLightShape { Square, Circle }
Declare the instance variables that correspond to the exposed properties, and initialize them in the constructor.
Public Shared MaxLightSize As Integer = 10 ' These fields back the public properties. Private updatePeriodValue As Integer = 50 Private lightSizeValue As Integer = 5 Private lightPeriodValue As Integer = 3 Private lightSpacingValue As Integer = 1 Private lightColorValue As Color Private darkColorValue As Color Private spinDirectionValue As MarqueeSpinDirection = MarqueeSpinDirection.CW Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square ' These brushes are used to paint the light and dark ' colors of the marquee lights. Private lightBrush As Brush Private darkBrush As Brush ' This field tracks the progress of the "first" light as it ' "travels" around the marquee border. Private currentOffset As Integer = 0 ' This component updates the control asynchronously. Private WithEvents backgroundWorker1 As System.ComponentModel.BackgroundWorker Public Sub New() ' This call is required by the Windows.Forms Form Designer. InitializeComponent() ' Initialize light and dark colors ' to the control's default values. Me.lightColorValue = Me.ForeColor Me.darkColorValue = Me.BackColor Me.lightBrush = New SolidBrush(Me.lightColorValue) Me.darkBrush = New SolidBrush(Me.darkColorValue) ' The MarqueeBorder control manages its own padding, ' because it requires that any contained controls do ' not overlap any of the marquee lights. Dim pad As Integer = 2 * (Me.lightSizeValue + Me.lightSpacingValue) Me.Padding = New Padding(pad, pad, pad, pad) SetStyle(ControlStyles.OptimizedDoubleBuffer, True) End Sub
public static int MaxLightSize = 10; // These fields back the public properties. private int updatePeriodValue = 50; private int lightSizeValue = 5; private int lightPeriodValue = 3; private int lightSpacingValue = 1; private Color lightColorValue; private Color darkColorValue; private MarqueeSpinDirection spinDirectionValue = MarqueeSpinDirection.CW; private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square; // These brushes are used to paint the light and dark // colors of the marquee lights. private Brush lightBrush; private Brush darkBrush; // This field tracks the progress of the "first" light as it // "travels" around the marquee border. private int currentOffset = 0; // This component updates the control asynchronously. private System.ComponentModel.BackgroundWorker backgroundWorker1; public MarqueeBorder() { // This call is required by the Windows.Forms Form Designer. InitializeComponent(); // Initialize light and dark colors // to the control's default values. this.lightColorValue = this.ForeColor; this.darkColorValue = this.BackColor; this.lightBrush = new SolidBrush(this.lightColorValue); this.darkBrush = new SolidBrush(this.darkColorValue); // The MarqueeBorder control manages its own padding, // because it requires that any contained controls do // not overlap any of the marquee lights. int pad = 2 * (this.lightSizeValue + this.lightSpacingValue); this.Padding = new Padding(pad, pad, pad, pad); SetStyle(ControlStyles.OptimizedDoubleBuffer, true); }
Implement the IMarqueeWidget interface.
The StartMarquee and StopMarquee methods invoke the BackgroundWorker component's RunWorkerAsync and CancelAsync methods to start and stop the animation.
Because the MarqueeBorder control can contain child controls, the StartMarquee method enumerates all child controls and calls StartMarquee on those that implement IMarqueeWidget. The StopMarquee method has a similar implementation.
Public Overridable Sub StartMarquee() _ Implements IMarqueeWidget.StartMarquee ' The MarqueeBorder control may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StartMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StartMarquee() End If Next cntrl ' Start the updating thread and pass it the UpdatePeriod. Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod) End Sub Public Overridable Sub StopMarquee() _ Implements IMarqueeWidget.StopMarquee ' The MarqueeBorder control may contain any number of ' controls that implement IMarqueeWidget, so find ' each IMarqueeWidget child and call its StopMarquee ' method. Dim cntrl As Control For Each cntrl In Me.Controls If TypeOf cntrl Is IMarqueeWidget Then Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget) widget.StopMarquee() End If Next cntrl ' Stop the updating thread. Me.backgroundWorker1.CancelAsync() End Sub <Category("Marquee"), Browsable(True)> _ Public Overridable Property UpdatePeriod() As Integer _ Implements IMarqueeWidget.UpdatePeriod Get Return Me.updatePeriodValue End Get Set(ByVal Value As Integer) If Value > 0 Then Me.updatePeriodValue = Value Else Throw New ArgumentOutOfRangeException("UpdatePeriod", _ "must be > 0") End If End Set End Property
public virtual void StartMarquee() { // The MarqueeBorder control may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StartMarquee // method. foreach (Control cntrl in this.Controls) { if (cntrl is IMarqueeWidget) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StartMarquee(); } } // Start the updating thread and pass it the UpdatePeriod. this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod); } public virtual void StopMarquee() { // The MarqueeBorder control may contain any number of // controls that implement IMarqueeWidget, so find // each IMarqueeWidget child and call its StopMarquee // method. foreach (Control cntrl in this.Controls) { if (cntrl is IMarqueeWidget) { IMarqueeWidget widget = cntrl as IMarqueeWidget; widget.StopMarquee(); } } // Stop the updating thread. this.backgroundWorker1.CancelAsync(); } [Category("Marquee")] [Browsable(true)] public virtual int UpdatePeriod { get { return this.updatePeriodValue; } set { if (value > 0) { this.updatePeriodValue = value; } else { throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0"); } } }
Implement the property accessors. The MarqueeBorder control has several properties for controlling its appearance.
<Category("Marquee"), Browsable(True)> _ Public Property LightSize() As Integer Get Return Me.lightSizeValue End Get Set(ByVal Value As Integer) If Value > 0 AndAlso Value <= MaxLightSize Then Me.lightSizeValue = Value Me.DockPadding.All = 2 * Value Else Throw New ArgumentOutOfRangeException("LightSize", _ "must be > 0 and < MaxLightSize") End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightPeriod() As Integer Get Return Me.lightPeriodValue End Get Set(ByVal Value As Integer) If Value > 0 Then Me.lightPeriodValue = Value Else Throw New ArgumentOutOfRangeException("LightPeriod", _ "must be > 0 ") End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightColor() As Color Get Return Me.lightColorValue End Get Set(ByVal Value As Color) ' The LightColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then Me.lightColorValue = Value Me.lightBrush = New SolidBrush(Value) End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property DarkColor() As Color Get Return Me.darkColorValue End Get Set(ByVal Value As Color) ' The DarkColor property is only changed if the ' client provides a different value. Comparing values ' from the ToArgb method is the recommended test for ' equality between Color structs. If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then Me.darkColorValue = Value Me.darkBrush = New SolidBrush(Value) End If End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property LightSpacing() As Integer Get Return Me.lightSpacingValue End Get Set(ByVal Value As Integer) If Value >= 0 Then Me.lightSpacingValue = Value Else Throw New ArgumentOutOfRangeException("LightSpacing", _ "must be >= 0") End If End Set End Property <Category("Marquee"), Browsable(True), _ EditorAttribute(GetType(LightShapeEditor), _ GetType(System.Drawing.Design.UITypeEditor))> _ Public Property LightShape() As MarqueeLightShape Get Return Me.lightShapeValue End Get Set(ByVal Value As MarqueeLightShape) Me.lightShapeValue = Value End Set End Property <Category("Marquee"), Browsable(True)> _ Public Property SpinDirection() As MarqueeSpinDirection Get Return Me.spinDirectionValue End Get Set(ByVal Value As MarqueeSpinDirection) Me.spinDirectionValue = Value End Set End Property
[Category("Marquee")] [Browsable(true)] public int LightSize { get { return this.lightSizeValue; } set { if (value > 0 && value <= MaxLightSize) { this.lightSizeValue = value; this.DockPadding.All = 2 * value; } else { throw new ArgumentOutOfRangeException("LightSize", "must be > 0 and < MaxLightSize"); } } } [Category("Marquee")] [Browsable(true)] public int LightPeriod { get { return this.lightPeriodValue; } set { if (value > 0) { this.lightPeriodValue = value; } else { throw new ArgumentOutOfRangeException("LightPeriod", "must be > 0 "); } } } [Category("Marquee")] [Browsable(true)] public Color LightColor { get { return this.lightColorValue; } set { // The LightColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.lightColorValue.ToArgb() != value.ToArgb()) { this.lightColorValue = value; this.lightBrush = new SolidBrush(value); } } } [Category("Marquee")] [Browsable(true)] public Color DarkColor { get { return this.darkColorValue; } set { // The DarkColor property is only changed if the // client provides a different value. Comparing values // from the ToArgb method is the recommended test for // equality between Color structs. if (this.darkColorValue.ToArgb() != value.ToArgb()) { this.darkColorValue = value; this.darkBrush = new SolidBrush(value); } } } [Category("Marquee")] [Browsable(true)] public int LightSpacing { get { return this.lightSpacingValue; } set { if (value >= 0) { this.lightSpacingValue = value; } else { throw new ArgumentOutOfRangeException("LightSpacing", "must be >= 0"); } } } [Category("Marquee")] [Browsable(true)] [EditorAttribute(typeof(LightShapeEditor), typeof(System.Drawing.Design.UITypeEditor))] public MarqueeLightShape LightShape { get { return this.lightShapeValue; } set { this.lightShapeValue = value; } } [Category("Marquee")] [Browsable(true)] public MarqueeSpinDirection SpinDirection { get { return this.spinDirectionValue; } set { this.spinDirectionValue = value; } }
Implement the handlers for the BackgroundWorker component's DoWork and ProgressChanged events.
The DoWork event handler sleeps for the number of milliseconds specified by UpdatePeriod then raises the ProgressChanged event, until your code stops the animation by calling CancelAsync.
The ProgressChanged event handler increments the position of the "base" light, from which the light/dark state of the other lights is determined, and calls the Refresh method to cause the control to repaint itself.
' This method is called in the worker thread's context, ' so it must not make any calls into the MarqueeBorder ' control. Instead, it communicates to the control using ' the ProgressChanged event. ' ' The only work done in this event handler is ' to sleep for the number of milliseconds specified ' by UpdatePeriod, then raise the ProgressChanged event. Private Sub backgroundWorker1_DoWork( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles backgroundWorker1.DoWork Dim worker As BackgroundWorker = CType(sender, BackgroundWorker) ' This event handler will run until the client cancels ' the background task by calling CancelAsync. While Not worker.CancellationPending ' The Argument property of the DoWorkEventArgs ' object holds the value of UpdatePeriod, which ' was passed as the argument to the RunWorkerAsync ' method. Thread.Sleep(Fix(e.Argument)) ' The DoWork eventhandler does not actually report ' progress; the ReportProgress event is used to ' periodically alert the control to update its state. worker.ReportProgress(0) End While End Sub ' The ProgressChanged event is raised by the DoWork method. ' This event handler does work that is internal to the ' control. In this case, the currentOffset is incremented, ' and the control is told to repaint itself. Private Sub backgroundWorker1_ProgressChanged( _ ByVal sender As Object, _ ByVal e As System.ComponentModel.ProgressChangedEventArgs) _ Handles backgroundWorker1.ProgressChanged Me.currentOffset += 1 Me.Refresh() End Sub
// This method is called in the worker thread's context, // so it must not make any calls into the MarqueeBorder // control. Instead, it communicates to the control using // the ProgressChanged event. // // The only work done in this event handler is // to sleep for the number of milliseconds specified // by UpdatePeriod, then raise the ProgressChanged event. private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; // This event handler will run until the client cancels // the background task by calling CancelAsync. while (!worker.CancellationPending) { // The Argument property of the DoWorkEventArgs // object holds the value of UpdatePeriod, which // was passed as the argument to the RunWorkerAsync // method. Thread.Sleep((int)e.Argument); // The DoWork eventhandler does not actually report // progress; the ReportProgress event is used to // periodically alert the control to update its state. worker.ReportProgress(0); } } // The ProgressChanged event is raised by the DoWork method. // This event handler does work that is internal to the // control. In this case, the currentOffset is incremented, // and the control is told to repaint itself. private void backgroundWorker1_ProgressChanged( object sender, System.ComponentModel.ProgressChangedEventArgs e) { this.currentOffset++; this.Refresh(); }
Implement the helper methods, IsLit and DrawLight.
The IsLit method determines the color of a light at a given position. Lights that are "lit" are drawn in the color given by the LightColor property, and those that are "dark" are drawn in the color given by the DarkColor property.
The DrawLight method draws a light using the appropriate color, shape, and position.
' This method determines if the marquee light at lightIndex ' should be lit. The currentOffset field specifies where ' the "first" light is located, and the "position" of the ' light given by lightIndex is computed relative to this ' offset. If this position modulo lightPeriodValue is zero, ' the light is considered to be on, and it will be painted ' with the control's lightBrush. Protected Overridable Function IsLit(ByVal lightIndex As Integer) As Boolean Dim directionFactor As Integer = _ IIf(Me.spinDirectionValue = MarqueeSpinDirection.CW, -1, 1) Return (lightIndex + directionFactor * Me.currentOffset) Mod Me.lightPeriodValue = 0 End Function Protected Overridable Sub DrawLight( _ ByVal g As Graphics, _ ByVal brush As Brush, _ ByVal xPos As Integer, _ ByVal yPos As Integer) Select Case Me.lightShapeValue Case MarqueeLightShape.Square g.FillRectangle( _ brush, _ xPos, _ yPos, _ Me.lightSizeValue, _ Me.lightSizeValue) Exit Select Case MarqueeLightShape.Circle g.FillEllipse( _ brush, _ xPos, _ yPos, _ Me.lightSizeValue, _ Me.lightSizeValue) Exit Select Case Else Trace.Assert(False, "Unknown value for light shape.") Exit Select End Select End Sub
// This method determines if the marquee light at lightIndex // should be lit. The currentOffset field specifies where // the "first" light is located, and the "position" of the // light given by lightIndex is computed relative to this // offset. If this position modulo lightPeriodValue is zero, // the light is considered to be on, and it will be painted // with the control's lightBrush. protected virtual bool IsLit(int lightIndex) { int directionFactor = (this.spinDirectionValue == MarqueeSpinDirection.CW ? -1 : 1); return ( (lightIndex + directionFactor * this.currentOffset) % this.lightPeriodValue == 0 ); } protected virtual void DrawLight( Graphics g, Brush brush, int xPos, int yPos) { switch (this.lightShapeValue) { case MarqueeLightShape.Square: { g.FillRectangle(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue); break; } case MarqueeLightShape.Circle: { g.FillEllipse(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue); break; } default: { Trace.Assert(false, "Unknown value for light shape."); break; } } }
Override the OnLayout and OnPaint methods.
The OnPaint method draws the lights along the edges of the MarqueeBorder control.
Because the OnPaint method depends on the dimensions of the MarqueeBorder control, you need to call it whenever the layout changes. To achieve this, override OnLayout and call Refresh.
Protected Overrides Sub OnLayout(ByVal levent As LayoutEventArgs) MyBase.OnLayout(levent) ' Repaint when the layout has changed. Me.Refresh() End Sub ' This method paints the lights around the border of the ' control. It paints the top row first, followed by the ' right side, the bottom row, and the left side. The color ' of each light is determined by the IsLit method and ' depends on the light's position relative to the value ' of currentOffset. Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) Dim g As Graphics = e.Graphics g.Clear(Me.BackColor) MyBase.OnPaint(e) ' If the control is large enough, draw some lights. If Me.Width > MaxLightSize AndAlso Me.Height > MaxLightSize Then ' The position of the next light will be incremented ' by this value, which is equal to the sum of the ' light size and the space between two lights. Dim increment As Integer = _ Me.lightSizeValue + Me.lightSpacingValue ' Compute the number of lights to be drawn along the ' horizontal edges of the control. Dim horizontalLights As Integer = _ (Me.Width - increment) / increment ' Compute the number of lights to be drawn along the ' vertical edges of the control. Dim verticalLights As Integer = _ (Me.Height - increment) / increment ' These local variables will be used to position and ' paint each light. Dim xPos As Integer = 0 Dim yPos As Integer = 0 Dim lightCounter As Integer = 0 Dim brush As Brush ' Draw the top row of lights. Dim i As Integer For i = 0 To horizontalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) xPos += increment lightCounter += 1 Next i ' Draw the lights flush with the right edge of the control. xPos = Me.Width - Me.lightSizeValue ' Draw the right column of lights. 'Dim i As Integer For i = 0 To verticalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) yPos += increment lightCounter += 1 Next i ' Draw the lights flush with the bottom edge of the control. yPos = Me.Height - Me.lightSizeValue ' Draw the bottom row of lights. 'Dim i As Integer For i = 0 To horizontalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) xPos -= increment lightCounter += 1 Next i ' Draw the lights flush with the left edge of the control. xPos = 0 ' Draw the left column of lights. 'Dim i As Integer For i = 0 To verticalLights - 1 brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush) DrawLight(g, brush, xPos, yPos) yPos -= increment lightCounter += 1 Next i End If End Sub
protected override void OnLayout(LayoutEventArgs levent) { base.OnLayout(levent); // Repaint when the layout has changed. this.Refresh(); } // This method paints the lights around the border of the // control. It paints the top row first, followed by the // right side, the bottom row, and the left side. The color // of each light is determined by the IsLit method and // depends on the light's position relative to the value // of currentOffset. protected override void OnPaint(PaintEventArgs e) { Graphics g = e.Graphics; g.Clear(this.BackColor); base.OnPaint(e); // If the control is large enough, draw some lights. if (this.Width > MaxLightSize && this.Height > MaxLightSize) { // The position of the next light will be incremented // by this value, which is equal to the sum of the // light size and the space between two lights. int increment = this.lightSizeValue + this.lightSpacingValue; // Compute the number of lights to be drawn along the // horizontal edges of the control. int horizontalLights = (this.Width - increment) / increment; // Compute the number of lights to be drawn along the // vertical edges of the control. int verticalLights = (this.Height - increment) / increment; // These local variables will be used to position and // paint each light. int xPos = 0; int yPos = 0; int lightCounter = 0; Brush brush; // Draw the top row of lights. for (int i = 0; i < horizontalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); xPos += increment; lightCounter++; } // Draw the lights flush with the right edge of the control. xPos = this.Width - this.lightSizeValue; // Draw the right column of lights. for (int i = 0; i < verticalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); yPos += increment; lightCounter++; } // Draw the lights flush with the bottom edge of the control. yPos = this.Height - this.lightSizeValue; // Draw the bottom row of lights. for (int i = 0; i < horizontalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); xPos -= increment; lightCounter++; } // Draw the lights flush with the left edge of the control. xPos = 0; // Draw the left column of lights. for (int i = 0; i < verticalLights; i++) { brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush; DrawLight(g, brush, xPos, yPos); yPos -= increment; lightCounter++; } } }
Creating a Custom Designer to Shadow and Filter Properties
The MarqueeControlRootDesigner class provides the implementation for the root designer. In addition to this designer, which operates on the MarqueeControl, you will need a custom designer that is specifically associated with the MarqueeBorder control. This designer provides custom behavior that is appropriate in the context of the custom root designer.
Specifically, the MarqueeBorderDesigner will "shadow" and filter certain properties on the MarqueeBorder control, changing their interaction with the design environment.
Intercepting calls to a component's property accessor is known as "shadowing." It allows a designer to track the value set by the user and optionally pass that value to the component being designed.
For this example, the Visible and Enabled properties will be shadowed by the MarqueeBorderDesigner, which prevents the user from making the MarqueeBorder control invisible or disabled during design time.
Designers can also add and remove properties. For this example, the Padding property will be removed at design time, because the MarqueeBorder control programmatically sets the padding based on the size of the lights specified by the LightSize property.
The base class for MarqueeBorderDesigner is ComponentDesigner, which has methods that can change the attributes, properties, and events exposed by a control at design time:
When changing the public interface of a component using these methods, you must follow these rules:
Add or remove items in the PreFilter methods only
Modify existing items in the PostFilter methods only
Always call the base implementation first in the PreFilter methods
Always call the base implementation last in the PostFilter methods
Adhering to these rules ensures that all designers in the design-time environment have a consistent view of all components being designed.
The ComponentDesigner class provides a dictionary for managing the values of shadowed properties, which relieves you of the need to create specific instance variables.
To create a custom designer to shadow and filter properties
Right-click the Design folder and add a new class. Give the source file a base name of "MarqueeBorderDesigner."
Open the MarqueeBorderDesigner source file in the Code Editor. At the top of the file, import the following namespaces:
Imports System Imports System.Collections Imports System.ComponentModel Imports System.ComponentModel.Design Imports System.Diagnostics Imports System.Windows.Forms Imports System.Windows.Forms.Design
using System; using System.Collections; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics; using System.Windows.Forms; using System.Windows.Forms.Design;
Change the declaration of MarqueeBorderDesigner to inherit from ParentControlDesigner.
Because the MarqueeBorder control can contain child controls, MarqueeBorderDesigner inherits from ParentControlDesigner, which handles the parent-child interaction.
Namespace MarqueeControlLibrary.Design <System.Security.Permissions.PermissionSetAttribute(System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _ Public Class MarqueeBorderDesigner Inherits ParentControlDesigner
namespace MarqueeControlLibrary.Design { [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] public class MarqueeBorderDesigner : ParentControlDesigner {
Override the base implementation of PreFilterProperties.
Protected Overrides Sub PreFilterProperties( _ ByVal properties As IDictionary) MyBase.PreFilterProperties(properties) If properties.Contains("Padding") Then properties.Remove("Padding") End If properties("Visible") = _ TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _ CType(properties("Visible"), PropertyDescriptor), _ New Attribute(-1) {}) properties("Enabled") = _ TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _ CType(properties("Enabled"), _ PropertyDescriptor), _ New Attribute(-1) {}) End Sub
protected override void PreFilterProperties(IDictionary properties) { base.PreFilterProperties(properties); if (properties.Contains("Padding")) { properties.Remove("Padding"); } properties["Visible"] = TypeDescriptor.CreateProperty( typeof(MarqueeBorderDesigner), (PropertyDescriptor)properties["Visible"], new Attribute[0]); properties["Enabled"] = TypeDescriptor.CreateProperty( typeof(MarqueeBorderDesigner), (PropertyDescriptor)properties["Enabled"], new Attribute[0]); }
Implement the Enabled and Visible properties. These implementations shadow the control's properties.
Public Property Visible() As Boolean Get Return CBool(ShadowProperties("Visible")) End Get Set(ByVal Value As Boolean) Me.ShadowProperties("Visible") = Value End Set End Property Public Property Enabled() As Boolean Get Return CBool(ShadowProperties("Enabled")) End Get Set(ByVal Value As Boolean) Me.ShadowProperties("Enabled") = Value End Set End Property
public bool Visible { get { return (bool)ShadowProperties["Visible"]; } set { this.ShadowProperties["Visible"] = value; } } public bool Enabled { get { return (bool)ShadowProperties["Enabled"]; } set { this.ShadowProperties["Enabled"] = value; } }
Handling Component Changes
The MarqueeControlRootDesigner class provides the custom design-time experience for your MarqueeControl instances. Most of the design-time functionality is inherited from the DocumentDesigner class; your code will implement two specific customizations: handling component changes, and adding designer verbs.
As users design their MarqueeControl instances, your root designer will track changes to the MarqueeControl and its child controls. The design-time environment offers a convenient service, IComponentChangeService, for tracking changes to component state.
You acquire a reference to this service by querying the environment with the GetService method. If the query is successful, your designer can attach a handler for the ComponentChanged event and perform whatever tasks are required to maintain a consistent state at design time.
In the case of the MarqueeControlRootDesigner class, you will call the Refresh method on each IMarqueeWidget object contained by the MarqueeControl. This will cause the IMarqueeWidget object to repaint itself appropriately when properties like its parent's Size are changed.
To handle component changes
Open the MarqueeControlRootDesigner source file in the Code Editor and override the Initialize method. Call the base implementation of Initialize and query for the IComponentChangeService.
MyBase.Initialize(component) Dim cs As IComponentChangeService = _ CType(GetService(GetType(IComponentChangeService)), _ IComponentChangeService) If (cs IsNot Nothing) Then AddHandler cs.ComponentChanged, AddressOf OnComponentChanged End If
base.Initialize(component); IComponentChangeService cs = GetService(typeof(IComponentChangeService)) as IComponentChangeService; if (cs != null) { cs.ComponentChanged += new ComponentChangedEventHandler(OnComponentChanged); }
Implement the OnComponentChanged event handler. Test the sending component's type, and if it is an IMarqueeWidget, call its Refresh method.
Private Sub OnComponentChanged( _ ByVal sender As Object, _ ByVal e As ComponentChangedEventArgs) If TypeOf e.Component Is IMarqueeWidget Then Me.Control.Refresh() End If End Sub
private void OnComponentChanged( object sender, ComponentChangedEventArgs e) { if (e.Component is IMarqueeWidget) { this.Control.Refresh(); } }
Adding Designer Verbs to your Custom Designer
A designer verb is a menu command linked to an event handler. Designer verbs are added to a component's shortcut menu at design time. For more information, see DesignerVerb.
You will add two designer verbs to your designers: Run Test and Stop Test. These verbs will allow you to view the run-time behavior of the MarqueeControl at design time. These verbs will be added to the MarqueeControlRootDesigner.
When Run Test is invoked, the verb event handler will call the StartMarquee method on the MarqueeControl. When Stop Test is invoked, the verb event handler will call the StopMarquee method on the MarqueeControl. The implementation of the StartMarquee and StopMarquee methods call these methods on contained controls that implement IMarqueeWidget, so any contained IMarqueeWidget controls will also participate in the test.
To add designer verbs to your custom designers
In the MarqueeControlRootDesigner class, add event handlers named OnVerbRunTest and OnVerbStopTest.
Private Sub OnVerbRunTest( _ ByVal sender As Object, _ ByVal e As EventArgs) Dim c As MarqueeControl = CType(Me.Control, MarqueeControl) c.Start() End Sub Private Sub OnVerbStopTest( _ ByVal sender As Object, _ ByVal e As EventArgs) Dim c As MarqueeControl = CType(Me.Control, MarqueeControl) c.Stop() End Sub
private void OnVerbRunTest(object sender, EventArgs e) { MarqueeControl c = this.Control as MarqueeControl; c.Start(); } private void OnVerbStopTest(object sender, EventArgs e) { MarqueeControl c = this.Control as MarqueeControl; c.Stop(); }
Connect these event handlers to their corresponding designer verbs. MarqueeControlRootDesigner inherits a DesignerVerbCollection from its base class. You will create two new DesignerVerb objects and add them to this collection in the Initialize method.
Me.Verbs.Add(New DesignerVerb("Run Test", _ New EventHandler(AddressOf OnVerbRunTest))) Me.Verbs.Add(New DesignerVerb("Stop Test", _ New EventHandler(AddressOf OnVerbStopTest)))
this.Verbs.Add( new DesignerVerb("Run Test", new EventHandler(OnVerbRunTest)) ); this.Verbs.Add( new DesignerVerb("Stop Test", new EventHandler(OnVerbStopTest)) );
Creating a Custom UITypeEditor
When you create a custom design-time experience for users, it is often desirable to create a custom interaction with the Properties window. You can accomplish this by creating a UITypeEditor. For more information, see How to: Create a UI Type Editor.
The MarqueeBorder control exposes several properties in the Properties window. Two of these properties, MarqueeSpinDirection and MarqueeLightShape are represented by enumerations. To illustrate the use of a UI type editor, the MarqueeLightShape property will have an associated UITypeEditor class.
To create a custom UI type editor
Open the MarqueeBorder source file in the Code Editor.
In the definition of the MarqueeBorder class, declare a class called LightShapeEditor that derives from UITypeEditor.
' This class demonstrates the use of a custom UITypeEditor. ' It allows the MarqueeBorder control's LightShape property ' to be changed at design time using a customized UI element ' that is invoked by the Properties window. The UI is provided ' by the LightShapeSelectionControl class. Friend Class LightShapeEditor Inherits UITypeEditor
// This class demonstrates the use of a custom UITypeEditor. // It allows the MarqueeBorder control's LightShape property // to be changed at design time using a customized UI element // that is invoked by the Properties window. The UI is provided // by the LightShapeSelectionControl class. internal class LightShapeEditor : UITypeEditor {
Declare an IWindowsFormsEditorService instance variable called editorService.
Private editorService As IWindowsFormsEditorService = Nothing
private IWindowsFormsEditorService editorService = null;
Override the GetEditStyle method. This implementation returns DropDown, which tells the design environment how to display the LightShapeEditor.
Public Overrides Function GetEditStyle( _ ByVal context As System.ComponentModel.ITypeDescriptorContext) _ As UITypeEditorEditStyle Return UITypeEditorEditStyle.DropDown End Function
public override UITypeEditorEditStyle GetEditStyle( System.ComponentModel.ITypeDescriptorContext context) { return UITypeEditorEditStyle.DropDown; }
Override the EditValue method. This implementation queries the design environment for an IWindowsFormsEditorService object. If successful, it creates a LightShapeSelectionControl. The DropDownControl method is invoked to start the LightShapeEditor. The return value from this invocation is returned to the design environment.
Public Overrides Function EditValue( _ ByVal context As ITypeDescriptorContext, _ ByVal provider As IServiceProvider, _ ByVal value As Object) As Object If (provider IsNot Nothing) Then editorService = _ CType(provider.GetService(GetType(IWindowsFormsEditorService)), _ IWindowsFormsEditorService) End If If (editorService IsNot Nothing) Then Dim selectionControl As _ New LightShapeSelectionControl( _ CType(value, MarqueeLightShape), _ editorService) editorService.DropDownControl(selectionControl) value = selectionControl.LightShape End If Return value End Function
public override object EditValue( ITypeDescriptorContext context, IServiceProvider provider, object value) { if (provider != null) { editorService = provider.GetService( typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; } if (editorService != null) { LightShapeSelectionControl selectionControl = new LightShapeSelectionControl( (MarqueeLightShape)value, editorService); editorService.DropDownControl(selectionControl); value = selectionControl.LightShape; } return value; }
Creating a View Control for your Custom UITypeEditor
- The MarqueeLightShape property supports two types of light shapes: Square and Circle. You will create a custom control used solely for the purpose of graphically displaying these values in the Properties window. This custom control will be used by your UITypeEditor to interact with the Properties window.
To create a view control for your custom UI type editor
Add a new UserControl item to the MarqueeControlLibrary project. Give the new source file a base name of "LightShapeSelectionControl."
Drag two Panel controls from the Toolbox onto the LightShapeSelectionControl. Name them squarePanel and circlePanel. Arrange them side by side. Set the Size property of both Panel controls to (60, 60). Set the Location property of the squarePanel control to (8, 10). Set the Location property of the circlePanel control to (80, 10). Finally, set the Size property of the LightShapeSelectionControl to (150, 80).
Open the LightShapeSelectionControl source file in the Code Editor. At the top of the file, import the System.Windows.Forms.Design namespace:
Imports System.Windows.Forms.Design
using System.Windows.Forms.Design;
Implement Click event handlers for the squarePanel and circlePanel controls. These methods invoke CloseDropDown to end the custom UITypeEditor editing session.
Private Sub squarePanel_Click( _ ByVal sender As Object, _ ByVal e As EventArgs) Me.lightShapeValue = MarqueeLightShape.Square Me.Invalidate(False) Me.editorService.CloseDropDown() End Sub Private Sub circlePanel_Click( _ ByVal sender As Object, _ ByVal e As EventArgs) Me.lightShapeValue = MarqueeLightShape.Circle Me.Invalidate(False) Me.editorService.CloseDropDown() End Sub
private void squarePanel_Click(object sender, EventArgs e) { this.lightShapeValue = MarqueeLightShape.Square; this.Invalidate( false ); this.editorService.CloseDropDown(); } private void circlePanel_Click(object sender, EventArgs e) { this.lightShapeValue = MarqueeLightShape.Circle; this.Invalidate( false ); this.editorService.CloseDropDown(); }
Declare an IWindowsFormsEditorService instance variable called editorService.
Private editorService As IWindowsFormsEditorService
private IWindowsFormsEditorService editorService;
Declare a MarqueeLightShape instance variable called lightShapeValue.
Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
In the LightShapeSelectionControl constructor, attach the Click event handlers to the squarePanel and circlePanel controls' Click events. Also, define a constructor overload that assigns the MarqueeLightShape value from the design environment to the lightShapeValue field.
' This constructor takes a MarqueeLightShape value from the ' design-time environment, which will be used to display ' the initial state. Public Sub New( _ ByVal lightShape As MarqueeLightShape, _ ByVal editorService As IWindowsFormsEditorService) ' This call is required by the Windows.Forms Form Designer. InitializeComponent() ' Cache the light shape value provided by the ' design-time environment. Me.lightShapeValue = lightShape ' Cache the reference to the editor service. Me.editorService = editorService ' Handle the Click event for the two panels. AddHandler Me.squarePanel.Click, AddressOf squarePanel_Click AddHandler Me.circlePanel.Click, AddressOf circlePanel_Click End Sub
// This constructor takes a MarqueeLightShape value from the // design-time environment, which will be used to display // the initial state. public LightShapeSelectionControl( MarqueeLightShape lightShape, IWindowsFormsEditorService editorService ) { // This call is required by the designer. InitializeComponent(); // Cache the light shape value provided by the // design-time environment. this.lightShapeValue = lightShape; // Cache the reference to the editor service. this.editorService = editorService; // Handle the Click event for the two panels. this.squarePanel.Click += new EventHandler(squarePanel_Click); this.circlePanel.Click += new EventHandler(circlePanel_Click); }
In the Dispose method, detach the Click event handlers.
Protected Overrides Sub Dispose(ByVal disposing As Boolean) If disposing Then ' Be sure to unhook event handlers ' to prevent "lapsed listener" leaks. RemoveHandler Me.squarePanel.Click, AddressOf squarePanel_Click RemoveHandler Me.circlePanel.Click, AddressOf circlePanel_Click If (components IsNot Nothing) Then components.Dispose() End If End If MyBase.Dispose(disposing) End Sub
protected override void Dispose( bool disposing ) { if( disposing ) { // Be sure to unhook event handlers // to prevent "lapsed listener" leaks. this.squarePanel.Click -= new EventHandler(squarePanel_Click); this.circlePanel.Click -= new EventHandler(circlePanel_Click); if(components != null) { components.Dispose(); } } base.Dispose( disposing ); }
In Solution Explorer, click the Show All Files button. Open the LightShapeSelectionControl.Designer.cs or LightShapeSelectionControl.Designer.vb file, and remove the default definition of the Dispose method.
Implement the LightShape property.
' LightShape is the property for which this control provides ' a custom user interface in the Properties window. Public Property LightShape() As MarqueeLightShape Get Return Me.lightShapeValue End Get Set(ByVal Value As MarqueeLightShape) If Me.lightShapeValue <> Value Then Me.lightShapeValue = Value End If End Set End Property
// LightShape is the property for which this control provides // a custom user interface in the Properties window. public MarqueeLightShape LightShape { get { return this.lightShapeValue; } set { if( this.lightShapeValue != value ) { this.lightShapeValue = value; } } }
Override the OnPaint method. This implementation will draw a filled square and circle. It will also highlight the selected value by drawing a border around one shape or the other.
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) MyBase.OnPaint(e) Dim gCircle As Graphics = Me.circlePanel.CreateGraphics() Try Dim gSquare As Graphics = Me.squarePanel.CreateGraphics() Try ' Draw a filled square in the client area of ' the squarePanel control. gSquare.FillRectangle( _ Brushes.Red, _ 0, _ 0, _ Me.squarePanel.Width, _ Me.squarePanel.Height) ' If the Square option has been selected, draw a ' border inside the squarePanel. If Me.lightShapeValue = MarqueeLightShape.Square Then gSquare.DrawRectangle( _ Pens.Black, _ 0, _ 0, _ Me.squarePanel.Width - 1, _ Me.squarePanel.Height - 1) End If ' Draw a filled circle in the client area of ' the circlePanel control. gCircle.Clear(Me.circlePanel.BackColor) gCircle.FillEllipse( _ Brushes.Blue, _ 0, _ 0, _ Me.circlePanel.Width, _ Me.circlePanel.Height) ' If the Circle option has been selected, draw a ' border inside the circlePanel. If Me.lightShapeValue = MarqueeLightShape.Circle Then gCircle.DrawRectangle( _ Pens.Black, _ 0, _ 0, _ Me.circlePanel.Width - 1, _ Me.circlePanel.Height - 1) End If Finally gSquare.Dispose() End Try Finally gCircle.Dispose() End Try End Sub
protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); using( Graphics gSquare = this.squarePanel.CreateGraphics(), gCircle = this.circlePanel.CreateGraphics() ) { // Draw a filled square in the client area of // the squarePanel control. gSquare.FillRectangle( Brushes.Red, 0, 0, this.squarePanel.Width, this.squarePanel.Height ); // If the Square option has been selected, draw a // border inside the squarePanel. if( this.lightShapeValue == MarqueeLightShape.Square ) { gSquare.DrawRectangle( Pens.Black, 0, 0, this.squarePanel.Width-1, this.squarePanel.Height-1); } // Draw a filled circle in the client area of // the circlePanel control. gCircle.Clear( this.circlePanel.BackColor ); gCircle.FillEllipse( Brushes.Blue, 0, 0, this.circlePanel.Width, this.circlePanel.Height ); // If the Circle option has been selected, draw a // border inside the circlePanel. if( this.lightShapeValue == MarqueeLightShape.Circle ) { gCircle.DrawRectangle( Pens.Black, 0, 0, this.circlePanel.Width-1, this.circlePanel.Height-1); } } }
Testing your Custom Control in the Designer
At this point, you can build the MarqueeControlLibrary project. Test your implementation by creating a control that inherits from the MarqueeControl class and using it on a form.
To create a custom MarqueeControl implementation
Open DemoMarqueeControl in the Windows Forms Designer. This creates an instance of the DemoMarqueeControl type and displays it in an instance of the MarqueeControlRootDesigner type.
In the Toolbox, open the MarqueeControlLibrary Components tab. You will see the MarqueeBorder and MarqueeText controls available for selection.
Drag an instance of the MarqueeBorder control onto the DemoMarqueeControl design surface. Dock this MarqueeBorder control to the parent control.
Drag an instance of the MarqueeText control onto the DemoMarqueeControl design surface.
Build the solution.
Right-click the DemoMarqueeControl and from the shortcut menu select the Run Test option to start the animation. Click Stop Test to stop the animation.
Open Form1 in Design view.
Place two Button controls on the form. Name them startButton and stopButton, and change the Text property values to Start and Stop, respectively.
In the Toolbox, open the MarqueeControlTest Components tab. You will see the DemoMarqueeControl available for selection.
Drag an instance of DemoMarqueeControl onto the Form1 design surface.
In the Click event handlers, invoke the Start and Stop methods on the DemoMarqueeControl.
Private Sub startButton_Click(sender As Object, e As System.EventArgs)
Me.demoMarqueeControl1.Start()
End Sub 'startButton_Click
Private Sub stopButton_Click(sender As Object, e As System.EventArgs)
Me.demoMarqueeControl1.Stop()
End Sub 'stopButton_Click
private void startButton_Click(object sender, System.EventArgs e)
{
this.demoMarqueeControl1.Start();
}
private void stopButton_Click(object sender, System.EventArgs e)
{
this.demoMarqueeControl1.Stop();
}
- Set the MarqueeControlTest project as the startup project and run it. You will see the form displaying your DemoMarqueeControl. Click the Start button to start the animation. You should see the text flashing and the lights moving around the border.
Next Steps
The MarqueeControlLibrary demonstrates a simple implementation of custom controls and associated designers. You can make this sample more sophisticated in several ways:
Change the property values for the DemoMarqueeControl in the designer. Add more MarqueBorder controls and dock them within their parent instances to create a nested effect. Experiment with different settings for the UpdatePeriod and the light-related properties.
Author your own implementations of IMarqueeWidget. You could, for example, create a flashing "neon sign" or an animated sign with multiple images.
Further customize the design-time experience. You could try shadowing more properties than Enabled and Visible, and you could add new properties. Add new designer verbs to simplify common tasks like docking child controls.
License the MarqueeControl. For more information, see How to: License Components and Controls.
Control how your controls are serialized and how code is generated for them. For more information, see Dynamic Source Code Generation and Compilation.
See Also
Tasks
How to: Create a Windows Forms Control That Takes Advantage of Design-Time Features