Tutorial: Crear un control que aproveche las características en tiempo de diseño

La experiencia en tiempo de diseño de un control personalizado puede mejorarse con la creación de un diseñador personalizado que se asocie a este.

Precaución

Este contenido se ha escrito para .NET Framework. Si usa .NET 6 o una versión posterior, use este contenido con precaución. El sistema de diseñador ha cambiado para Windows Forms y es importante que revise el artículo Cambios del diseñador desde .NET Framework.

En este artículo se muestra cómo crear un diseñador personalizado para un control personalizado. Implementará un tipo MarqueeControl y una clase de diseñador asociada denominada MarqueeControlRootDesigner.

El tipo MarqueeControl implementa una pantalla similar a una marquesina de teatro con luces animadas y texto parpadeante.

El diseñador de este control interactúa con el entorno de diseño para proporcionar una experiencia personalizada en tiempo de diseño. Con el diseñador personalizado, puede ensamblar una implementación MarqueeControl personalizada con diversas combinaciones de luces animadas y texto parpadeante. Puede usar el control ensamblado en un formulario como cualquier otro control de Windows Forms.

Cuando haya terminado con este tutorial, el control personalizado tendrá un aspecto similar al siguiente:

La aplicación muestra una marca que dice Texto y los botones Iniciar y Detener.

Para ver la lista de códigos completa, consulte Cómo: Crear un control de Windows Forms que aproveche las características en tiempo de diseño.

Prerrequisitos

Para completar este tutorial, necesitará Visual Studio.

Crear el proyecto

El primer paso es crear el proyecto de la aplicación. Este proyecto se usará para compilar la aplicación que hospeda el control personalizado.

En Visual Studio, cree un proyecto de aplicación de Windows Forms y asígnele el nombre MarqueeControlTest.

Creación del proyecto de biblioteca de controles

  1. Agregue a la solución un proyecto de biblioteca de controles de Windows Forms y asígnele el nombre MarqueeControlLibrary.

  2. Elimine el control predeterminado del proyecto mediante el Explorador de soluciones; para ello, elimine el archivo de código fuente denominado "UserControl1.cs" o "UserControl1.vb", en función del lenguaje de su elección.

  3. Agregue un elemento UserControl nuevo al proyecto MarqueeControlLibrary. Asigne al archivo de código fuente nuevo el nombre base MarqueeControl.

  4. Con el Explorador de soluciones, cree una carpeta en el proyecto MarqueeControlLibrary.

  5. Haga clic con el botón derecho en la carpeta Design y agregue una clase nueva. Asígnele el nombre MarqueeControlRootDesigner.

  6. Deberá usar tipos del ensamblado System.Design, por lo que debe agregar esta referencia al proyecto MarqueeControlLibrary.

Referencia al proyecto del control personalizado

El proyecto MarqueeControlTest se va a usar para probar el control personalizado. El proyecto de prueba reconocerá el control personalizado al agregar una referencia de proyecto al ensamblado MarqueeControlLibrary.

Agregue una referencia de proyecto al ensamblado MarqueeControlLibrary en el proyecto MarqueeControlTest. Asegúrese de usar la pestaña Proyectos del cuadro de diálogo Agregar referencia en lugar de hacer referencia al ensamblado MarqueeControlLibrary directamente.

Definición de un control personalizado y de su diseñador personalizado

El control personalizado se derivará de la clase UserControl. Esto permite que el control contenga otros controles y proporciona a este numerosas funcionalidades predeterminadas.

El control personalizado tendrá un diseñador personalizado asociado. Esto le permite crear una experiencia de diseño única adaptada a su control personalizado de forma específica.

El control se asocia a su diseñador mediante la clase DesignerAttribute. Puesto que está desarrollando el comportamiento en tiempo de diseño del control personalizado al completo, el diseñador personalizado implementará la interfaz IRootDesigner.

Para definir un control personalizado y su diseñador personalizado

  1. Abra el archivo de código fuente MarqueeControl en el Editor de código. En la parte superior del archivo, importe los espacios de nombres siguientes:

    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Drawing
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
  2. Agregue el atributo DesignerAttribute a la declaración de clase MarqueeControl. Esto asocia el control personalizado a su diseñador.

    [Designer( typeof( MarqueeControlLibrary.Design.MarqueeControlRootDesigner ), typeof( IRootDesigner ) )]
    public class MarqueeControl : UserControl
    {
    
    <Designer(GetType(MarqueeControlLibrary.Design.MarqueeControlRootDesigner), _
     GetType(IRootDesigner))> _
    Public Class MarqueeControl
        Inherits UserControl
    
  3. Abra el archivo de código fuente MarqueeControlRootDesigner en el Editor de código. En la parte superior del archivo, importe los espacios de nombres siguientes:

    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;
    
    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
    
  4. Cambie la declaración de MarqueeControlRootDesigner para heredar de la clase DocumentDesigner. Aplique el atributo ToolboxItemFilterAttribute para especificar la interacción del diseñador con el Cuadro de herramientas.

    Nota

    La definición de la clase MarqueeControlRootDesigner se ha incluido en un espacio de nombres denominado MarqueeControlLibrary.Design. Esta declaración coloca el diseñador en un espacio de nombres especial reservado para tipos relacionados con el diseño.

    namespace MarqueeControlLibrary.Design
    {
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
        public class MarqueeControlRootDesigner : DocumentDesigner
        {
    
    Namespace MarqueeControlLibrary.Design
    
        <ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _
        ToolboxItemFilterType.Require), _
        ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _
        ToolboxItemFilterType.Require)> _
        Public Class MarqueeControlRootDesigner
            Inherits DocumentDesigner
    
  5. Defina el constructor de la clase MarqueeControlRootDesigner. Inserte una instrucción WriteLine en el cuerpo del constructor. Esto resultará útil para la depuración.

    public MarqueeControlRootDesigner()
    {
        Trace.WriteLine("MarqueeControlRootDesigner ctor");
    }
    
    Public Sub New()
        Trace.WriteLine("MarqueeControlRootDesigner ctor")
    End Sub
    

Creación de una instancia del control personalizado

  1. Agregue un elemento UserControl nuevo al proyecto MarqueeControlTest. Asigne al archivo de código fuente nuevo el nombre base DemoMarqueeControl.

  2. Abra el archivo DemoMarqueeControl en el Editor de código. En la parte superior del archivo, importe el espacio de nombres MarqueeControlLibrary:

    Imports MarqueeControlLibrary
    
    using MarqueeControlLibrary;
    
  3. Cambie la declaración de DemoMarqueeControl para heredar de la clase MarqueeControl.

  4. Compile el proyecto.

  5. Abra Form1 en el Diseñador de Windows Forms.

  6. Busque la pestaña de Componentes MarqueeControlTest en el Cuadro de herramientas y ábrala. Arrastre un elemento DemoMarqueeControl del Cuadro de herramientas al formulario.

  7. Compile el proyecto.

Configuración del proyecto para la depuración en tiempo de diseño

Al desarrollar una experiencia personalizada en tiempo de diseño, deberá depurar los controles y los componentes. Existe una forma sencilla de configurar el proyecto para permitir la depuración en tiempo de diseño. Para obtener más información, consulte Tutorial: Depuración de controles personalizados de Windows Forms en tiempo de diseño.

  1. Haga clic con el botón derecho en el proyecto MarqueeControlLibrary y seleccione Propiedades.

  2. En el cuadro de diálogo Páginas de propiedades de MarqueeControlLibrary, seleccione la página Depurar.

  3. En la sección Acción de inicio, seleccione Iniciar programa externo. Va a depurar una instancia independiente de Visual Studio, por lo que debe hacer clic en el botón de puntos suspensivos (Botón de puntos suspensivos (...) en la ventana Propiedades de Visual Studio) para buscar el IDE de Visual Studio. El nombre del archivo ejecutable es devenv.exe y, si ha realizado la instalación en la ubicación predeterminada, su ruta de acceso es %ProgramFiles(x86)%\Microsoft Visual Studio\2019\<edición>\Common7\IDE\devenv.exe.

  4. Seleccione Aceptar para cerrar el cuadro de diálogo.

  5. Haga clic con el botón derecho en el proyecto MarqueeControlLibrary y seleccione Establecer como proyecto de inicio para habilitar esta configuración de depuración.

Punto de control

Ahora está listo para depurar el comportamiento en tiempo de diseño del control personalizado. Una vez que haya determinado que el entorno de depuración se ha configurado correctamente, probará la asociación entre el control personalizado y el diseñador personalizado.

Para probar el entorno de depuración y la asociación del diseñador

  1. Abra el archivo de código fuente MarqueeControlRootDesigner en el Editor de código y coloque un punto de interrupción en la instrucción WriteLine.

  2. Presione F5 para iniciar la sesión de depuración.

    Se crea una instancia de Visual Studio.

  3. En esa nueva instancia de Visual Studio, abra la solución MarqueeControlTest. Si quiere encontrar la solución de forma fácil, seleccione Proyectos recientes en el menú Archivo. El archivo de solución MarqueeControlTest.sln se mostrará como el archivo usado más recientemente.

  4. Abra DemoMarqueeControl en el diseñador.

    La instancia de depuración de Visual Studio obtiene el foco y la ejecución se detiene en el punto de interrupción. Presione F5 para continuar con la sesión de depuración.

En este punto, todo está listo para desarrollar y depurar el control personalizado y su diseñador personalizado asociado. El resto del artículo se centra en los detalles de las características de implementación del control y el diseñador.

Implementación del control personalizado

MarqueeControl es una clase UserControl ligeramente personalizada. Expone dos métodos: Start, que inicia la animación de la marquesina, y Stop, que la detiene. Dado que MarqueeControl contiene controles secundarios que implementan la interfaz IMarqueeWidget, Start y Stop enumeran cada uno de los controles secundarios y llaman a los métodos StartMarquee y StopMarquee, respectivamente, en cada control secundario que implementa IMarqueeWidget.

La apariencia de los controles MarqueeBorder y MarqueeText depende del diseño, por lo que MarqueeControl invalida el método OnLayout y llama a PerformLayout en los controles secundarios de este tipo.

Esta es la extensión de las personalizaciones MarqueeControl. Los controles MarqueeBorder y MarqueeText implementan las características en tiempo de ejecución y las clases MarqueeBorderDesigner y MarqueeControlRootDesigner implementan las características en tiempo de diseño.

Para implementar el control personalizado

  1. Abra el archivo de código fuente MarqueeControl en el Editor de código. Implemente los métodos Start y Stop.

    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();
            }
        }
    }
    
    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
    
  2. Invalide el método OnLayout.

    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();
            }
        }
    }
    
    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
    

Creación de un control secundario para el control personalizado

El elemento MarqueeControl hospedará dos tipos de controles secundarios: los controles MarqueeBorder y MarqueeText.

  • MarqueeBorder: este control dibuja un borde de "luces" alrededor del perímetro. Las luces parpadean en secuencia, por lo que parecen moverse alrededor del borde. La velocidad a la que parpadean las luces se controla mediante una propiedad llamada UpdatePeriod. Otros aspectos de la apariencia del control se determinan mediante otras propiedades personalizadas. Dos métodos, denominados StartMarquee y StopMarquee, controlan cuándo se inicia y se detiene la animación.

  • MarqueeText: este control dibuja una cadena parpadeante. Al igual que en el control MarqueeBorder, la velocidad a la que el texto parpadea se controla mediante la propiedad UpdatePeriod. El control MarqueeText también tiene los métodos StartMarquee y StopMarquee en común con el control MarqueeBorder.

En tiempo de diseño, MarqueeControlRootDesigner permite que estos dos tipos de control se agreguen a MarqueeControl combinados de cualquier forma.

Las características comunes de ambos controles se factorizan en una interfaz denominada IMarqueeWidget. Esto permite que MarqueeControl detecte cualquier control secundario relacionado con Marquee y le aplique un tratamiento especial.

Para implementar la característica de animación periódica, se usarán los objetos BackgroundWorker del espacio de nombres System.ComponentModel. Puede usar objetos Timer, pero si hay muchos objetos IMarqueeWidget presentes, es posible que el subproceso de la interfaz de usuario simple no pueda hacer frente a la animación.

Para crear un control secundario del control personalizado

  1. Agregue un nuevo elemento de clase al proyecto MarqueeControlLibrary. Asigne al nuevo archivo de código fuente el nombre base "IMarqueeWidget".

  2. Abra el archivo de código fuente IMarqueeWidget en el Editor de código y cambie la declaración de class a 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
    
  3. Agregue el código siguiente a la interfaz IMarqueeWidget para exponer dos métodos y una propiedad que manipule la animación de la marquesina:

    // 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;
        }
    }
    
    ' 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
    
  4. Agregue un elemento Control personalizado nuevo al proyecto MarqueeControlLibrary. Asigne al archivo de código fuente nuevo el nombre base "MarqueeText".

  5. Arrastre un componente BackgroundWorker del Cuadro de herramientas al control MarqueeText. Este componente permitirá que el control MarqueeText se actualice automáticamente de forma asincrónica.

  6. En la ventana Propiedades, establezca las propiedades WorkerReportsProgress y WorkerSupportsCancellation del componente BackgroundWorker en true. Esta configuración permite que el componente BackgroundWorker genere el evento ProgressChanged de forma periódica y cancele las actualizaciones asincrónicas.

    Para obtener más información, vea BackgroundWorker (Componente).

  7. Abra el archivo de código fuente MarqueeText en el Editor de código. En la parte superior del archivo, importe los espacios de nombres siguientes:

    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;
    
    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
    
  8. Cambie la declaración de MarqueeText para heredar de Label y para implementar la interfaz IMarqueeWidget:

    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
    public partial class MarqueeText : Label, IMarqueeWidget
    {
    
    <ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _
    ToolboxItemFilterType.Require)> _
    Partial Public Class MarqueeText
        Inherits Label
        Implements IMarqueeWidget
    
  9. Declare las variables de instancia que corresponden a las propiedades expuestas e inicialícelas en el constructor. El campo isLit determina si el texto se va a pintar del color indicado por la propiedad LightColor.

    // 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);
    }
    
    ' 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
    
  10. Implemente la interfaz IMarqueeWidget.

    Los métodos StartMarquee y StopMarquee invocan a los métodos RunWorkerAsync y CancelAsync del componente BackgroundWorker para iniciar y detener la animación.

    Los atributos Category y Browsable se aplican a la propiedad UpdatePeriod para que aparezca en una sección personalizada de la ventana Propiedades denominada "Marquee".

    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");
            }
        }
    }
    
    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
    
  11. Implemente los descriptores de acceso de propiedad. Expondrá dos propiedades a los clientes: LightColor y DarkColor. Los atributos Category y Browsable se aplican a estas propiedades, de forma que estas aparecen en una sección personalizada de la ventana Propiedades denominada "Marquee".

    [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 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
    
  12. Implemente los controladores para los eventos DoWork y ProgressChanged del componente BackgroundWorker.

    El controlador de eventos DoWork está en suspensión durante el número de milisegundos que especifique UpdatePeriod y, a continuación, genera el evento ProgressChanged, hasta que el código detiene la animación mediante la llamada a CancelAsync.

    El controlador de eventos ProgressChanged alterna el texto entre los estados claro y oscuro para dar la apariencia de parpadeo.

    // 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();
    }
    
    
    ' 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
    
  13. Invalide el método OnPaint para habilitar la animación.

    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);
    }
    
    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
    
  14. Presione F6 para compilar la solución.

Creación del control secundario MarqueeBorder

El control MarqueeBorder es algo más sofisticado que el control MarqueeText. Tiene más propiedades y la animación del método OnPaint es más compleja. En principio, es bastante similar al control MarqueeText.

Dado que el control MarqueeBorder puede tener controles secundarios, debe tener en cuenta los eventos Layout.

Para crear el control MarqueeBorder

  1. Agregue un elemento Control personalizado nuevo al proyecto MarqueeControlLibrary. Asigne al archivo de código fuente nuevo el nombre base "MarqueeBorder".

  2. Arrastre un componente BackgroundWorker del Cuadro de herramientas al control MarqueeBorder. Este componente permitirá que el control MarqueeBorder se actualice automáticamente de forma asincrónica.

  3. En la ventana Propiedades, establezca las propiedades WorkerReportsProgress y WorkerSupportsCancellation del componente BackgroundWorker en true. Esta configuración permite que el componente BackgroundWorker genere el evento ProgressChanged de forma periódica y cancele las actualizaciones asincrónicas. Para obtener más información, vea BackgroundWorker (Componente).

  4. En la ventana Propiedades, seleccione el botón Eventos. Asocie controladores para los eventos DoWork y ProgressChanged.

  5. Abra el archivo de código fuente MarqueeBorder en el Editor de código. En la parte superior del archivo, importe los espacios de nombres siguientes:

    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;
    
    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
    
  6. Cambie la declaración de MarqueeBorder para heredar de Panel y para implementar la interfaz IMarqueeWidget.

    [Designer(typeof(MarqueeControlLibrary.Design.MarqueeBorderDesigner ))]
    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
    public partial class MarqueeBorder : Panel, IMarqueeWidget
    {
    
    <Designer(GetType(MarqueeControlLibrary.Design.MarqueeBorderDesigner)), _
    ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _
    ToolboxItemFilterType.Require)> _
    Partial Public Class MarqueeBorder
        Inherits Panel
        Implements IMarqueeWidget
    
  7. Declare dos enumeraciones para administrar el estado del control MarqueeBorder: MarqueeSpinDirection, que determina la dirección en la que las luces "giran" alrededor del borde y MarqueeLightShape, que determina la forma de las luces (cuadradas o circulares). Coloque estas declaraciones delante de la declaración de clase MarqueeBorder.

    // 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
    }
    
    ' 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
    
  8. Declare las variables de instancia que corresponden a las propiedades expuestas e inicialícelas en el constructor.

    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);
    }
    
    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
    
  9. Implemente la interfaz IMarqueeWidget.

    Los métodos StartMarquee y StopMarquee invocan a los métodos RunWorkerAsync y CancelAsync del componente BackgroundWorker para iniciar y detener la animación.

    Dado que el control MarqueeBorder puede contener controles secundarios, el método StartMarquee enumera todos los controles secundarios y llama a StartMarquee en los que implementan IMarqueeWidget. El método StopMarquee tiene una implementación similar.

    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");
            }
        }
    }
    
    
    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
    
  10. Implemente los descriptores de acceso de propiedad. El control MarqueeBorder tiene varias propiedades para controlar su apariencia.

    [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;
        }
    }
    
    
    <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
    
  11. Implemente los controladores para los eventos DoWork y ProgressChanged del componente BackgroundWorker.

    El controlador de eventos DoWork está en suspensión durante el número de milisegundos que especifique UpdatePeriod y, a continuación, genera el evento ProgressChanged, hasta que el código detiene la animación mediante la llamada a CancelAsync.

    El controlador de eventos ProgressChanged incrementa la posición de la luz "base", desde la que se determina el estado claro u oscuro del resto de luces, y llama al método Refresh para que el control vuelva a dibujarse.

    // 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();
    }
    
    ' 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
    
  12. Implemente los métodos auxiliares IsLit y DrawLight.

    El método IsLit determina el color de una luz en una posición determinada. Las luces "encendidas" se dibujan en el color que indica la propiedad LightColor y las que son "oscuras" se dibujan en el color que indica la propiedad DarkColor.

    El método DrawLight dibuja una luz con el color, la forma y la posición adecuados.

    // 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;
                }
        }
    }
    
    ' 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
    
  13. Invalide los métodos OnLayout y OnPaint.

    El método OnPaint dibuja las luces a lo largo de los bordes del control MarqueeBorder.

    Dado que el método OnPaint depende de las dimensiones del control MarqueeBorder, debe llamarlo cada vez que cambie el diseño. Para ello, invalide OnLayout y llame a Refresh.

    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++;
            }
        }
    }
    
    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
    

Creación de un diseñador personalizado para sombrear y filtrar propiedades

La clase MarqueeControlRootDesigner proporciona la implementación del diseñador raíz. Además de este diseñador, que funciona en MarqueeControl, necesitará un diseñador personalizado que esté asociado específicamente con el control MarqueeBorder. Este diseñador proporciona un comportamiento personalizado que resulta adecuado en el contexto del diseñador raíz personalizado.

En concreto, MarqueeBorderDesigner "sombreará" y filtrará ciertas propiedades en el control MarqueeBorder, lo que cambiará su interacción con el entorno de diseño.

La intercepción de llamadas al descriptor de acceso de propiedades de un componente se conoce como "sombreado". Permite a un diseñador realizar un seguimiento del valor establecido por el usuario y, opcionalmente, pasar ese valor al componente que se está diseñando.

En este ejemplo, MarqueeBorderDesigner sombreará las propiedades Visible y Enabled, lo que impide que el usuario pueda hacer que el control MarqueeBorder sea invisible o se deshabilite durante el tiempo de diseño.

Los diseñadores también pueden agregar y quitar propiedades. En este ejemplo, la propiedad Padding se quitará en tiempo de diseño, ya que el control MarqueeBorder establece el relleno mediante programación, en función del tamaño de las luces que especifica la propiedad LightSize.

La clase base para MarqueeBorderDesigner es ComponentDesigner, que tiene métodos para cambiar los atributos, las propiedades y los eventos que expone un control en tiempo de diseño:

Al cambiar la interfaz pública de un componente mediante estos métodos, siga estas reglas:

  • Agregar o quitar elementos solamente en los métodos PreFilter

  • Modificar los elementos existentes solamente en los métodos PostFilter

  • Llamar siempre primero a la implementación base en los métodos PreFilter

  • Llamar siempre en último lugar a la implementación base en los métodos PostFilter

Cumplir estas reglas garantiza que todos los diseñadores del entorno en tiempo de diseño tengan una visión coherente de todos los componentes que se están diseñando.

La clase ComponentDesigner proporciona un diccionario para administrar los valores de las propiedades sombreadas, lo que elimina la necesidad de crear variables de instancias específicas.

Para crear un diseñador personalizado a fin de sombrear y filtrar propiedades

  1. Haga clic con el botón derecho en la carpeta Design y agregue una clase nueva. Asigne al archivo de código fuente el nombre base MarqueeBorderDesigner.

  2. Abra el archivo de código fuente MarqueeBorderDesigner en el Editor de código. En la parte superior del archivo, importe los espacios de nombres siguientes:

    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
  3. Cambie la declaración de MarqueeBorderDesigner para heredar de ParentControlDesigner.

    Dado que el control MarqueeBorder puede contener controles secundarios, MarqueeBorderDesigner hereda de ParentControlDesigner, lo que controla la interacción de elementos primarios y secundarios.

    namespace MarqueeControlLibrary.Design
    {
        public class MarqueeBorderDesigner : ParentControlDesigner
        {
    
    Namespace MarqueeControlLibrary.Design
    
        Public Class MarqueeBorderDesigner
            Inherits ParentControlDesigner
    
  4. Reemplace la implementación base de PreFilterProperties.

    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]);
    }
    
    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
    
  5. Implemente las propiedades Enabled y Visible. Estas implementaciones sombrean las propiedades del control.

    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;
        }
    }
    
    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
    

Control de los cambios de componentes

La clase MarqueeControlRootDesigner proporciona una experiencia personalizada en tiempo de diseño para las instancias de MarqueeControl. La mayor parte de la funcionalidad en tiempo de diseño se hereda de la clase DocumentDesigner. El código implementará dos personalizaciones específicas: el control de los cambios de componentes y la adición de verbos del diseñador.

Cuando los usuarios diseñan sus instancias de MarqueeControl, el diseñador raíz realizará un seguimiento de los cambios realizados en MarqueeControl y en sus controles secundarios. El entorno en tiempo de diseño ofrece un servicio práctico, IComponentChangeService, para realizar un seguimiento de los cambios en el estado del componente.

Para obtener una referencia a este servicio, consulte el entorno con el método GetService. Si la consulta se realiza correctamente, el diseñador puede asociar un controlador para el evento ComponentChanged y realizar las tareas necesarias para mantener un estado coherente en tiempo de diseño.

En el caso de la clase MarqueeControlRootDesigner, llamará al método Refresh en cada objeto IMarqueeWidget que MarqueeControl contenga. Esto hará que el objeto IMarqueeWidget se vuelva a dibujar correctamente de forma automática cuando se cambien propiedades como Size en el elemento primario.

Para controlar los cambios de los componentes

  1. Abra el archivo de código fuente MarqueeControlRootDesigner en el Editor de código y reemplace el método Initialize. Llame a la implementación base de Initialize y consulte IComponentChangeService.

    base.Initialize(component);
    
    IComponentChangeService cs =
        GetService(typeof(IComponentChangeService))
        as IComponentChangeService;
    
    if (cs != null)
    {
        cs.ComponentChanged +=
            new ComponentChangedEventHandler(OnComponentChanged);
    }
    
    MyBase.Initialize(component)
    
    Dim cs As IComponentChangeService = _
    CType(GetService(GetType(IComponentChangeService)), _
    IComponentChangeService)
    
    If (cs IsNot Nothing) Then
        AddHandler cs.ComponentChanged, AddressOf OnComponentChanged
    End If
    
  2. Implemente el controlador de eventos OnComponentChanged. Pruebe el tipo del componente de envío y, si es un elemento IMarqueeWidget, llame a su método Refresh.

    private void OnComponentChanged(
        object sender,
        ComponentChangedEventArgs e)
    {
        if (e.Component is IMarqueeWidget)
        {
            this.Control.Refresh();
        }
    }
    
    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
    

Adición de verbos de diseñador al diseñador personalizado

Un verbo de diseñador es un comando de menú vinculado a un controlador de eventos. Los verbos de diseñador se agregan al menú contextual de un componente en tiempo de diseño. Para obtener más información, vea DesignerVerb.

Va a agregar dos verbos de este tipo a sus diseñadores: Ejecutar prueba y Detener prueba. Estos verbos le permitirán ver el comportamiento en tiempo de ejecución de MarqueeControl en tiempo de diseño y se agregarán a MarqueeControlRootDesigner.

Cuando se invoca Ejecutar prueba, el controlador de eventos de verbo llamará al método StartMarquee en MarqueeControl. Cuando se invoca Detener prueba, el controlador de eventos de verbo llamará al método StopMarquee en MarqueeControl. La implementación de los métodos StartMarquee y StopMarquee llama a estos métodos en los controles contenidos que implementan IMarqueeWidget, por lo que cualquier control IMarqueeWidget contenido también participará en la prueba.

Para agregar verbos de diseñador a los diseñadores personalizados

  1. En la clase MarqueeControlRootDesigner, agregue controladores de eventos denominados OnVerbRunTest y OnVerbStopTest.

    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();
    }
    
    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
    
  2. Conecte estos controladores de eventos con sus verbos de diseñador correspondientes. MarqueeControlRootDesigner hereda un elemento DesignerVerbCollection de su clase base. Va a crear dos objetos DesignerVerb nuevos y a agregarlos a esta colección en el método Initialize.

    this.Verbs.Add(
        new DesignerVerb("Run Test",
        new EventHandler(OnVerbRunTest))
        );
    
    this.Verbs.Add(
        new DesignerVerb("Stop Test",
        new EventHandler(OnVerbStopTest))
        );
    
    Me.Verbs.Add(New DesignerVerb("Run Test", _
    New EventHandler(AddressOf OnVerbRunTest)))
    
    Me.Verbs.Add(New DesignerVerb("Stop Test", _
    New EventHandler(AddressOf OnVerbStopTest)))
    

Creación de un elemento UITypeEditor personalizado

Al crear una experiencia personalizada en tiempo de diseño para los usuarios, a menudo es conveniente crear una interacción personalizada con la ventana Propiedades. Para lograrlo, cree un objeto UITypeEditor.

El control MarqueeBorder expone varias propiedades en la ventana Propiedades. Dos de estas propiedades, MarqueeSpinDirection y MarqueeLightShape, se representan mediante enumeraciones. Para ilustrar el uso de un editor de tipos de interfaz de usuario, se asociará una clase UITypeEditor a la propiedad MarqueeLightShape.

Para crear un editor de tipos de interfaz de usuario personalizado

  1. Abra el archivo de código fuente MarqueeBorder en el Editor de código.

  2. En la definición de la clase MarqueeBorder, declare una clase denominada LightShapeEditor que se derive de 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
    {
    
    ' 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
    
  3. Declare una variable de instancia de IWindowsFormsEditorService llamada editorService.

    private IWindowsFormsEditorService editorService = null;
    
    Private editorService As IWindowsFormsEditorService = Nothing
    
  4. Invalide el método GetEditStyle. Esta implementación devuelve DropDown, que indica al entorno de diseño cómo mostrar el editor LightShapeEditor.

    public override UITypeEditorEditStyle GetEditStyle(
    System.ComponentModel.ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.DropDown;
    }
    
    Public Overrides Function GetEditStyle( _
    ByVal context As System.ComponentModel.ITypeDescriptorContext) _
    As UITypeEditorEditStyle
        Return UITypeEditorEditStyle.DropDown
    End Function
    
    
  5. Invalide el método EditValue. Esta implementación consulta el entorno de diseño para un objeto IWindowsFormsEditorService. Si se realiza correctamente, crea un objeto LightShapeSelectionControl. El método DropDownControl se invoca para iniciar LightShapeEditor. El valor devuelto de esta invocación se devuelve al entorno de diseño.

    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;
    }
    
    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
    

Creación de un control de vista para el objeto UITypeEditor personalizado

La propiedad MarqueeLightShape admite dos tipos de formas de luz: Square y Circle. Va a crear un control personalizado que se use exclusivamente para mostrar estos valores de forma gráfica en la ventana Propiedades. El objeto UITypeEditor usará este control personalizado para interactuar con la ventana Propiedades.

Para crear un control de vista para el editor de tipos de interfaz de usuario personalizado

  1. Agregue un elemento UserControl nuevo al proyecto MarqueeControlLibrary. Asigne al archivo de código fuente nuevo el nombre base LightShapeSelectionControl.

  2. Arrastre dos controles Panel desde el Cuadro de herramientas a LightShapeSelectionControl. Asígneles los nombres squarePanel y circlePanel. Organícelos uno al lado del otro. Establezca la propiedad Size de ambos controles Panel en (60, 60). Establezca la propiedad Location del control squarePanel en (8, 10). Establezca la propiedad Location del control circlePanel en (80, 10). Por último, establezca la propiedad Size de LightShapeSelectionControl en (150, 80).

  3. Abra el archivo de código fuente LightShapeSelectionControl en el Editor de código. En la parte superior del archivo, importe el espacio de nombres System.Windows.Forms.Design:

    Imports System.Windows.Forms.Design
    
    using System.Windows.Forms.Design;
    
  4. Implemente controladores de eventos Click para los controles squarePanel y circlePanel. Estos métodos invocan a CloseDropDown para finalizar la sesión de edición de UITypeEditor personalizada.

    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();
    }
    
    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
    
  5. Declare una variable de instancia de IWindowsFormsEditorService llamada editorService.

    Private editorService As IWindowsFormsEditorService
    
    private IWindowsFormsEditorService editorService;
    
  6. Declare una variable de instancia de MarqueeLightShape llamada lightShapeValue.

    private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
    
    Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
    
  7. En el constructor LightShapeSelectionControl, asocie los controladores de eventos Click a los eventos Click de los controles squarePanel y circlePanel. Asimismo, defina una sobrecarga de constructor que asigne el valor MarqueeLightShape del entorno de diseño al campo lightShapeValue.

    // 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);
    }
    
    ' 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
    
  8. En el método Dispose, desasocie los controladores de eventos Click.

    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 );
    }
    
    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
    
  9. En el Explorador de soluciones, haga clic en el botón Mostrar todos los archivos. Abra el archivo LightShapeSelectionControl.Designer.cs o LightShapeSelectionControl.Designer.vb y quite la definición predeterminada del método Dispose.

  10. Implemente la propiedad LightShape.

    // 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;
            }
        }
    }
    
    ' 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
    
  11. Invalide el método OnPaint. Esta implementación dibujará un cuadrado y un círculo rellenos. También dibujará un borde alrededor de una forma o la otra para resaltar el valor seleccionado.

    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);
            }
        }	
    }
    
    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
    

Prueba del control personalizado en el diseñador

En este punto, puede compilar el proyecto MarqueeControlLibrary. Cree un control que herede de la clase MarqueeControl y úselo en un formulario para probar su implementación.

Para crear una implementación personalizada de MarqueeControl

  1. Abra DemoMarqueeControl en el Diseñador de Windows Forms. Al hacerlo, se crea una instancia de tipo DemoMarqueeControl y se muestra en una instancia de tipo MarqueeControlRootDesigner.

  2. En el Cuadro de herramientas, abra la pestaña Componentes MarqueeControlLibrary. Verá los controles MarqueeBorder y MarqueeText disponibles para su selección.

  3. Arrastre una instancia del control MarqueeBorder a la superficie de diseño de DemoMarqueeControl. Acople este control MarqueeBorder al control primario.

  4. Arrastre una instancia del control MarqueeText a la superficie de diseño de DemoMarqueeControl.

  5. Compile la solución.

  6. Haga clic con el botón derecho en DemoMarqueeControl y, en el menú contextual, seleccione la opción Ejecutar prueba para iniciar la animación. Haga clic en Detener prueba para detener la animación.

  7. Abra Form1 en la vista Diseño.

  8. Coloque dos controles Button en el formulario. Asígneles los nombres startButton y stopButton y cambie los valores de la propiedad Text a Iniciar y Detener, respectivamente.

  9. Implemente controladores de eventos Click para ambos controles Button.

  10. En el Cuadro de herramientas, abra la pestaña de Componentes MarqueeControlTest. Verá DemoMarqueeControl disponible para su selección.

  11. Arrastre una instancia de DemoMarqueeControl a la superficie de diseño de Form1.

  12. En los controladores de eventos Click, invoque los métodos Start y Stop en 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();
    }
    
  13. Establezca el proyecto MarqueeControlTest como proyecto de inicio y ejecútelo. Verá que el formulario muestra su DemoMarqueeControl. Seleccione el botón Iniciar para iniciar la animación. El texto debería aparecer parpadeando y las luces deberían moverse alrededor del borde.

Pasos siguientes

La biblioteca MarqueeControlLibrary muestra una implementación sencilla de los controles personalizados y los diseñadores asociados. Existen diversas formas de hacer que este ejemplo sea más sofisticado:

  • Cambie los valores de las propiedades de DemoMarqueeControl en el diseñador. Agregue más controles MarqueBorder y acóplelos dentro de sus instancias primarias para crear un efecto anidado. Experimente con distintas configuraciones para UpdatePeriod y las propiedades relacionadas con la luz.

  • Cree sus propias implementaciones de IMarqueeWidget. Por ejemplo, puede crear un "letrero de neón" parpadeante o un letrero animado con varias imágenes.

  • Personalice aún más la experiencia en tiempo de diseño. Puede probar a sombrear más propiedades que Enabled y Visible y también agregar nuevas propiedades. Agregue nuevos verbos de diseñador para simplificar tareas comunes, como acoplar los controles secundarios.

  • Obtenga una licencia de MarqueeControl.

  • Controle cómo se serializan los controles y cómo se genera el código para ellos. Para obtener más información, consulte Generación y compilación dinámicas de código fuente.

Consulte también