Tutorial: Crear un control de formularios Windows Forms que aproveche las características en tiempo de diseño de Visual Studio
Se puede mejorar la experiencia en tiempo de diseño para un control personalizado creando un diseñador personalizado asociado.
En este tutorial se muestra cómo crear a 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 presentación similar a una marquesina de teatro, con luces animadas y el texto parpadeante.
El diseñador para este control interactúa con el entorno de diseño para proporcionar una experiencia en tiempo de diseño personalizada. Con el diseñador personalizado, se puede ensamblar una implementación de MarqueeControl personalizada con luces animadas y texto parpadeante en muchas combinaciones. Se puede utilizar el control ensamblado en un formulario como cualquier otro control de formularios Windows Forms.
Las tareas ilustradas en este tutorial incluyen:
-
Crear el proyecto
-
Crear un proyecto de Biblioteca de controles
-
Hacer referencia al proyecto de Biblioteca de controles
-
Definir un control personalizado y su diseñador personalizado
-
Crear una instancia del control personalizado
-
Establecer el proyecto para depuración en tiempo de diseño
-
Implementar el control personalizado
-
Crear un control secundario para el control personalizado
-
Crear el control MarqueeBorder secundario
-
Crear un diseñador personalizado para sombrear y filtrar propiedades
-
Controlar los cambios de componente
-
Agregar verbos del diseñador al diseñador personalizado
-
Crear un UITypeEditor personalizado
-
Probar el control personalizado en el diseñador
Cuando termine, el control personalizado tendrá el siguiente aspecto:
Para obtener la lista de código completa, vea Cómo: Crear un control de formularios Windows Forms que aproveche las características en tiempo de diseño.
Nota |
|---|
| Los cuadros de diálogo y comandos de menú que se ven pueden diferir de los descritos en la Ayuda, en función de la configuración activa o la edición. Para cambiar la configuración, elija la opción Importar y exportar configuraciones en el menú Herramientas. Para obtener más información, vea Valores de configuración de Visual Studio. |
El primer paso es crear el proyecto de aplicación. Este proyecto se utilizará para generar la aplicación que aloja el control personalizado.
Para crear el proyecto
-
Cree un proyecto de aplicación para Windows llamado a "MarqueeControlTest." Para obtener más información, vea Cómo: Crear un proyecto de aplicación para Windows.
El paso siguiente es crear el proyecto de Biblioteca de controles. Creará a un nuevo control personalizado y su diseñador personalizado correspondiente.
Para crear el proyecto de Biblioteca de controles
-
Agregue un proyecto de Biblioteca de controles para Windows a la solución. Para obtener más información, vea Agregar nuevo proyecto (Cuadro de diálogo). Dé al proyecto el nombre de "MarqueeControlLibrary".
-
Con el Explorador de soluciones, elimine el control predeterminado del proyecto eliminando el archivo de código fuente denominado "UserControl1.cs" o "UserControl1.vb", dependiendo del lenguaje elegido. Para obtener más información, vea Cómo: Quitar, eliminar y excluir elementos.
-
Agregue un nuevo elemento UserControl al proyecto MarqueeControlLibrary. Dé el nombre base de "MarqueeControl" al nuevo archivo de código fuente.
-
Con el Explorador de soluciones, cree una nueva carpeta en el proyecto MarqueeControlLibrary. Para obtener más información, vea Cómo: Agregar nuevos elementos de proyecto. Denomine "Diseño" a la nueva carpeta.
-
Haga clic con el botón secundario del mouse (ratón) en la carpeta Diseño y agregue una nueva clase. Dé el nombre base de "MarqueeControlRootDesigner" al archivo de código fuente.
-
Necesitará utilizar los tipos del ensamblado System.Design, así que agregue esta referencia al proyecto MarqueeControlTest. Para obtener más información, vea Cómo: Agregar y quitar referencias en Visual Studio (C#, J#).
Utilizará el proyecto MarqueeControlTest para probar el control personalizado. El proyecto de prueba se dará cuenta del control personalizado cuando agregue una referencia de proyecto al ensamblado MarqueeControlLibrary.
Para hacer referencia al proyecto de control personalizado
-
En el proyecto MarqueeControlTest, agregue una referencia de proyecto al ensamblado MarqueeControlLibrary. Asegúrese de utilizar la ficha Proyectos en el cuadro de diálogo Agregar referencia en lugar de hacer referencia directamente al ensamblado MarqueeControlLibrary.
Su control personalizado derivará de la clase UserControl. Esta opción permite al control contener otros controles y da una gran funcionalidad predeterminada al control.
El control personalizado tendrá un diseñador personalizado asociado. Esto le permite crear una experiencia del diseño única, personalizada específicamente para su control personalizado.
Asocie el control a su diseñador utilizando la clase DesignerAttribute. Como está desarrollando el comportamiento en tiempo de diseño completo del control personalizado, el diseñador personalizado implementará la interfaz IRootDesigner.
Para definir un control personalizado y su diseñador personalizado
-
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:
-
Agregue DesignerAttribute a la declaración de clase MarqueeControl. Esta opción asocia el control personalizado a su diseñador.
-
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:
-
Escriba la definición de la clase MarqueeControlRootDesigner en un espacio de nombres denominado "MarqueeControlLibrary.Design". Esta declaración coloca al diseñador en un espacio de nombres especial reservado para los tipos relacionados con el diseño.
Además, cambie la declaración de MarqueeControlRootDesigner para heredar de la clase DocumentDesigner.
namespace MarqueeControlLibrary.Design { [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)] [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)] [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] public class MarqueeControlRootDesigner : DocumentDesigner {
-
Defina el constructor para la clase MarqueeControlRootDesigner. Inserte una instrucción WriteLine en el cuerpo del constructor. Esta opción es útil con fines de depuración.
Para observar el comportamiento en tiempo de diseño personalizado del control, colocará una instancia de su control en el formulario en el proyecto MarqueeControlTest.
Para crear una instancia de su control personalizado
-
Agregue un nuevo elemento UserControl al proyecto MarqueeControlTest. Dé el nombre base de "DemoMarqueeControl" al nuevo archivo de código fuente.
-
En la parte superior del archivo, importe el espacio de nombres MarqueeControlLibrary siguiente:
-
Cambie la declaración de DemoMarqueeControl para heredar de la clase MarqueeControl.
-
Genere el proyecto.
-
Abra Form1 en el Diseñador de Windows Forms.
-
Busque la ficha "Mi controles de usuario" en el Cuadro de herramientas y ábrala. Arrastre un DemoMarqueeControl desde el Cuadro de herramientas hasta el formulario.
-
Genere el proyecto.
Cuando está desarrollando una experiencia en tiempo de diseño personalizada, será necesario depurar los controles y componentes. Hay un modo muy sencillo de configurar el proyecto para permitir la depuración en tiempo de diseño. Para obtener más información, vea Tutorial: Depurar controles personalizados de formularios Windows Forms en tiempo de diseño.
Para configurar el proyecto para depuración en tiempo de diseño
-
Haga clic con el botón secundario del mouse en el proyecto MarqueeControlLibrary y seleccione Propiedades.
-
En el cuadro de diálogo "Páginas de propiedades de MarqueeControlLibrary", seleccione la página Propiedades de configuración.
-
En la sección Acción de inicio, seleccione Programa externo de inicio. Como está depurando una instancia independiente de Visual Studio, haga clic en el botón de puntos suspensivos (
) para buscar el IDE de Visual Studio. El nombre del archivo ejecutable es devenv.exe, y si lo ha instalado en la ubicación predeterminada, la ruta de acceso será "C:\Archivos de programa\Microsoft Visual Studio .NET\Common7\IDE\devenv.exe." -
Haga clic en Aceptar para cerrar el cuadro de diálogo.
-
Haga clic con el botón secundario del mouse en el proyecto MarqueeControlLibrary y seleccione "Establecer como proyecto de inicio" para habilitar esta configuración de depuración.
Ahora está listo para depurar el comportamiento en tiempo de diseño de su control personalizado. Una vez determinado que el entorno de depuración está 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
-
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.
-
Presione F5 para iniciar la depuración. Observe que se crea una nueva instancia de Visual Studio.
-
En la nueva instancia de Visual Studio, abra la solución "MarqueeControlTest". Para encontrar con facilidad la solución, seleccione Proyectos recientes en el menú Archivo. El archivo de solución "MarqueeControlTest.sln" se mostrará como el último archivo utilizado.
-
Abra DemoMarqueeControl en el diseñador. Observe que la instancia de depuración de Visual Studio adquiere el foco y la ejecución se detiene en su punto de interrupción. Presione F5 para continuar 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 de este tutorial se concentrará en los detalles de implementación de las características del control y el diseñador.
MarqueeControl es un UserControl con algo de personalización. Expone dos métodos: Start, que inicia la animación de la marquesina, y Stop, que detiene la animación. Como MarqueeControl contiene controles secundarios que implementan la interfaz IMarqueeWidget, Start y Stop enumeran cada control secundario 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 tanto MarqueeControl reemplaza el método OnLayout y llama al método PerformLayout en los controles secundarios de este tipo.
Ésta es la extensión de las personalizaciones de 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
-
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(); } } }
-
Reemplace 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(); } } }
MarqueeControl alojará dos tipos de control secundario: el control MarqueeBorder y el control MarqueeText.
-
MarqueeBorder: Este control representa un borde de "luces" alrededor de los bordes. Las luces parpadean en forma secuencial, por lo que parece que se mueven alrededor del borde. La velocidad de parpadeo de las luces está controlada por una propiedad llamada UpdatePeriod. Varias propiedades personalizadas determinan otros aspectos de la apariencia del control. Dos métodos, llamados StartMarquee y StopMarquee, controlan cuándo se inicia o se detiene la animación.
-
MarqueeText: Este control representa una cadena parpadeante. Al igual que el control MarqueeBorder, la velocidad en la que el texto parpadea está controlada por la propiedad UpdatePeriod. El control MarqueeText también tiene en común los métodos StartMarquee y StopMarquee con el control MarqueeBorder.
En tiempo de diseño, MarqueeControlRootDesigner permite agregar estos dos tipos de control a MarqueeControl en cualquier combinación.
Las características comunes de los dos controles se factorizan en una interfaz llamada IMarqueeWidget. Esto permite a MarqueeControl descubrir los controles secundarios relacionados con la marquesina y darles un tratamiento especial.
Para implementar la función de animación periódica, utilizará los objetos BackgroundWorker del espacio de nombres System.ComponentModel. Podría utilizar los objetos Timer, pero cuando hay presentes varios objetos IMarqueeWidget, es posible que el único subproceso de la interfaz de usuario no pueda mantener la animación.
Para crear un control secundario para su control personalizado
-
Agregue un nuevo elemento de clase al proyecto MarqueeControlLibrary. Dé el nombre base de "IMarqueeWidget" al nuevo archivo de código fuente.
-
Abra el archivo de código fuente IMarqueeWidget en el Editor de código y cambie la declaración de class a interface:
-
Agregue el código siguiente a la interfaz IMarqueeWidget para exponer dos métodos y una propiedad que manipulan 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; } }
-
Agregue un nuevo elemento UserControl al proyecto MarqueeControlLibrary. Dé el nombre base de "MarqueeText" al nuevo archivo de código fuente.
-
Arrastre un componente BackgroundWorker desde el Cuadro de herramientas hasta el control MarqueeText. Este componente permitirá al control MarqueeText actualizarse de forma asincrónica.
-
En la ventana Propiedades, establezca las propiedades WorkerReportsProgess y WorkerSupportsCancellation del componente BackgroundWorker en true. Esta configuración permite al componente BackgroundWorker provocar periódicamente el evento ProgressChanged y cancelar las actualizaciones asincrónicas. Para obtener más información, vea BackgroundWorker (Componente).
-
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:
-
Cambie la declaración de MarqueeText para heredar de Label e implementar la interfaz IMarqueeWidget:
-
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 representa en el color proporcionado 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); }
-
Implemente la interfaz IMarqueeWidget.
Los métodos StartMarquee y StopMarquee invocan los métodos RunWorkerAsync y CancelAsync del componente BackgroundWorker para iniciar y detener la aplicación.
Los atributos Category y Browsable se aplican a la propiedad UpdatePeriod, por lo que aparece en una sección personalizada de la ventana Propiedades llamada "Marquesina".
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"); } } }
-
Implemente los descriptores de acceso de propiedad. Expone dos propiedades a los clientes: LightColor y DarkColor. Los atributos Category y Browsable se aplican a estas propiedades, por lo que aparece en una sección personalizada de la ventana Propiedades llamada "Marquesina".
[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); } } }
-
Implemente los controladores para los eventos DoWork y ProgressChanged del componente BackgroundWorker.
El controlador de eventos DoWork permanece desactivado durante la cantidad de milisegundos especificada en UpdatePeriod y luego provoca el evento ProgressChanged, hasta que el código detiene la aplicación llamando al método CancelAsync.
El controlador de eventos ProgressChanged alterna el estado de iluminación y apagado del texto para proporcionar 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(); }
-
Reemplace el método OnPaint para habilitar la animación.
El control MarqueeBorder es ligeramente más complejo más que el control MarqueeText. Tiene más propiedades y el método OnPaint presenta más animación. En principio, es bastante similar al control MarqueeBorder.
Dado que el control MarqueeBorder puede tener controles secundarios, necesita darse cuenta de los eventos Layout.
Para crear el control MarqueeBorder
-
Agregue un nuevo elemento UserControl al proyecto MarqueeControlLibrary. Dé el nombre base de "MarqueeControl" al nuevo archivo de código fuente.
-
Arrastre un componente BackgroundWorker desde el Cuadro de herramientas al control MarqueeBorder. Este componente permitirá al control MarqueeBorder actualizarse de forma asincrónica.
-
En la ventana Propiedades, establezca las propiedades WorkerReportsProgess y WorkerSupportsCancellation del componente BackgroundWorker en true. Esta configuración permite al componente BackgroundWorker provocar periódicamente el evento ProgressChanged y cancelar las actualizaciones asincrónicas. Para obtener más información, vea BackgroundWorker (Componente).
-
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:
-
Cambie la declaración de MarqueeBorder para heredar de Panel e implementar la interfaz IMarqueeWidget:
-
Declare dos enumeraciones para administrar el estado del control MarqueeBorder: MarqueeSpinDirection, que determina la dirección en la que la luz "gira" alrededor del borde, y MarqueeLightShape, que determina la forma de las luces (cuadradas o circulares). Coloque estas declaraciones antes de la declaración de clase MarqueeBorder.
-
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); }
-
Implemente la interfaz IMarqueeWidget.
Los métodos StartMarquee y StopMarquee invocan los métodos RunWorkerAsync y CancelAsync del componente BackgroundWorker para iniciar y detener la aplicación.
Como el control MarqueeBorder puede contener controles secundarios, el método StartMarquee enumera todos los controles secundarios y llama a StartMarquee en aquellos que implementa 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"); } } }
Implemente los descriptores de acceso de propiedad. El control MarqueeBorder dispone de 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; } }
-
Implemente los controladores para los eventos DoWork y ProgressChanged del componente BackgroundWorker.
El controlador de eventos DoWork permanece desactivado durante la cantidad de milisegundos especificada en UpdatePeriod y luego provoca el evento ProgressChanged, hasta que el código detiene la aplicación llamando al método CancelAsync.
El controlador de eventos ProgressChanged aumenta la posición de la luz "base", desde la cual se determina el estado iluminado/oscuro de las otras luces, y llama al método Refresh para hacer que el control se pinte a sí mismo.
// 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(); }
-
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 que se "encienden" se representan en el color proporcionado por la propiedad LightColor, y los que son "oscuros" se representan en el color proporcionado por la propiedad DarkColor.
El método DrawLight dibuja una luz utilizando 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; } } }
-
Utilice 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, necesita llamarlo cada vez que el diseño cambia. Para ello, reemplace las llamadas a los métodos OnLayout y 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++; } } }
La clase MarqueeControlRootDesigner proporciona la implementación para el diseñador raíz. Además de este diseñador, que funciona en MarqueeControl, necesitará a un diseñador personalizado que se asocia específicamente al control MarqueeBorder. Este diseñador proporciona comportamiento personalizado que es adecuado en el contexto del diseñador raíz personalizado.
Específicamente, MarqueeBorderDesigner sombreará y filtrará ciertas propiedades en el control MarqueeBorder, cambiando su interacción con el entorno de diseño.
Las llamadas que interceptan al descriptor de acceso de propiedad de un componente se conocen como "sombreado". Permite a un diseñador realizar el seguimiento del valor establecido por el usuario y opcionalmente pasa ese valor al componente que se está diseñando.
En este ejemplo, las propiedades Visible y Enabled se sombrearán por MarqueeBorderDesigner, que impide al usuario hacer invisible o desactivar el control MarqueeBorder durante el tiempo de diseño.
Los diseñadores también pueden agregar y quitar propiedades. En este ejemplo, se quitará la propiedad DockPadding en tiempo de diseño, porque el control MarqueeBorder establece mediante programación el relleno en función del tamaño de las luces especificado en la propiedad LightSize.
La clase base para MarqueeBorderDesigner es ComponentDesigner, que tiene métodos que pueden cambiar los atributos, propiedades y eventos expuestos por un control en tiempo de diseño:
Al cambiar la interfaz pública de un componente utilizando estos métodos, debe seguir las reglas siguientes:
-
Agregue o quite elementos únicamente en los métodos PreFilter
-
Modifique los elementos existentes únicamente en los métodos PostFilter
-
Siempre llame primero a la implementación base de los métodos PreFilter
-
Siempre llame primero a la implementación base de los métodos PostFilter
El cumplimiento de estas reglas garantiza que todos los diseñadores en entorno en tiempo de diseño poseen una vista coherente de todos los componentes que se están diseñando.
La clase ComponentDesigner proporciona un diccionario para administrar los valores de propiedades sombreadas, que libera de la necesidad de crear variables de instancia específicas.
Para crear a un diseñador personalizado para sombrear y filtrar propiedades
-
Haga clic con el botón secundario del mouse (ratón) en la carpeta Diseño y agregue una nueva clase. Dé el nombre base de "MarqueeBorderDesigner" al archivo de código fuente.
-
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:
-
Cambie la declaración de MarqueeBorderDesigner para heredar de ParentControlDesigner y agregue la definición para la clase MarqueeBorderDesigner en el espacio de nombres MarqueeControlLibrary.Design.
Dado que el control MarqueeBorder puede contener controles secundarios, MarqueeBorderDesigner hereda de ParentControlDesigner, que controla la interacción primaria-secundaria.
-
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]); }
-
Implemente las propiedades Enabled y Visible. Estas implementaciones sombrean las propiedades del control.
La clase MarqueeControlRootDesigner proporciona experiencia en tiempo de diseño personalizada 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 determinadas: la administración de los cambios de componentes y la adición de verbos del diseñador.
Cuando los usuarios diseñan las instancias de MarqueeControl, el diseñador raíz realizará el seguimiento de los cambios en MarqueeControl y sus controles secundarios. El entorno en tiempo de diseño ofrece un práctico servicio, IComponentChangeService, para realizar el seguimiento de los cambios al estado de componente.
Adquiere una referencia a este servicio consultando el entorno con el método GetService. Si la consulta es satisfactoria, el diseñador puede adjuntar un controlador al evento ComponentChanged y realizar las tareas necesarias para conservar un estado coherente en tiempo de diseño.
En el caso de la clase MarqueeControlRootDesigner, llame al método Refresh en cada objeto IMarqueeWidget contenido por MarqueeControl. Esto hará que el objeto IMarqueeWidget se vuelva a representar correctamente cuando se cambien propiedades como la propiedad Size del elemento primario.
Para controlar los cambios de componente
-
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 del método Initialize y consulte IComponentChangeService.
-
Implemente el controlador de eventos OnComponentChanged. Pruebe el tipo del componente enviado y si es IMarqueeWidget, llame al método Refresh.
Un verbo del diseñador es un comando de menú vinculado a un controlador de eventos. Los verbos del diseñador se agregan en tiempo de diseño al menú contextual de un componente. Para obtener más información, vea DesignerVerb.
Agregará dos verbos del diseñador a los diseñadores: Ejecutar prueba y Detener prueba. Estos verbos le permitirán ver en tiempo de diseño el comportamiento en tiempo de ejecución de MarqueeControl. Estos verbos se agregarán a MarqueeControlRootDesigner.
Cuando se invoca Ejecutar prueba, el controlador de eventos del verbo llamará al método StartMarquee en MarqueeControl. Cuando se invoca Detener prueba, el controlador de eventos del verbo llamará al método StopMarquee en MarqueeControl. La implementación de los métodos StartMarquee y StopMarquee llama a estos métodos en controles contenidos que implementan IMarqueeWidget, por tanto también participará en la prueba cualquier control IMarqueeWidget contenido.
Para agregar verbos del diseñador a los diseñadores personalizados
-
En la clase MarqueeControlRootDesigner, agregue los controladores de eventos denominados OnVerbRunTest y OnVerbStopTest.
-
Conecte estos controladores de eventos a sus verbos del diseñador correspondientes. MarqueeControlRootDesigner hereda un DesignerVerbCollection de su clase base. Creará dos nuevos objetos DesignerVerb y los agregará a esta colección en el método Initialize.
Cuando crea una experiencia en tiempo de diseño personalizada para los usuarios, se aconseja crear una interacción personalizada con la ventana Propiedades. Puede llevarlo a cabo creando un UITypeEditor. Para obtener más información, vea Cómo: Crear un editor de tipos de interfaz de usuario.
El control MarqueeBorder expone varias propiedades en la ventana Propiedades. Dos de estas propiedades, MarqueeSpinDirection y MarqueeLightShape, se representan por enumeraciones. Para explicar el uso de un editor de tipos de la interfaz de usuario, la propiedad MarqueeLightShape tendrá una clase UITypeEditor asociada.
Para crear un editor de tipos de la interfaz de usuario personalizada
-
Abra el archivo de código fuente MarqueeBorder en el Editor de código.
-
En la definición de la clase MarqueeBorder, declare una clase llamada LightShapeEditor que deriva 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 {
-
Declare una variable de instancia IWindowsFormsEditorService denominada editorService.
-
Reemplace el método GetEditStyle. Esta implementación devuelve DropDown, que indica al entorno de diseño cómo mostrar LightShapeEditor.
-
Reemplace el método EditValue. Esta implementación consulta en el entorno de diseño un objeto IWindowsFormsEditorService. Si es correcto, crea un 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; }
-
La propiedad MarqueeLightShape admite dos tipos de formas para la iluminación: Square y Circle. Creará un control personalizado que se utilizará únicamente para mostrar gráficamente estos valores en la ventana Propiedades. UITypeEditor utilizará este control personalizado para interactuar con la ventana Propiedades.
Para crear un control de vista para el editor de tipos de la interfaz de usuario personalizado
-
Agregue un nuevo elemento UserControl al proyecto MarqueeControlLibrary. Dé el nombre base de "LightShapeSelectionControl" al nuevo archivo de código fuente.
-
Arrastre dos controles Panel desde el Cuadro de herramientas a LightShapeSelectionControl. Denomínelos 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). Finalmente, establezca la propiedad Size de LightShapeSelectionControl en (150, 80).
-
Abra el archivo de código fuente LightShapeSelectionControl en el Editor de código.
-
Implemente los controladores de eventos Click para los controles squarePanel y circlePanel. Estos métodos invocan CloseDropDown para finalizar la sesión de edición personalizada de UITypeEditor.
private void squarePanel_Click(object sender, EventArgs e) { this.lightShapeValue = MarqueeLightShape.Square; this.Invalidate( false ); this.editorService.CloseDropDown(); } private void circlePanel_Click(object sender, EventArgs e) { this.lightShapeValue = MarqueeLightShape.Circle; this.Invalidate( false ); this.editorService.CloseDropDown(); }
-
Declare una variable de instancia MarqueeLightShape denominada lightShapeValue.
-
En el constructor LightShapeSelectionControl, adjunte los controladores de eventos Click a los eventos Click de los controles squarePanel y circlePanel. También, 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); }
-
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 ); }
-
Implemente la propiedad LightShape.
-
Reemplace el método OnPaint. Esta implementación dibujará un cuadrado y círculo rellenados. También resaltará el valor seleccionado dibujando un borde alrededor de cualquiera de las dos formas.
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); } } }
Llegado a este punto, puede generar el proyecto MarqueeControlLibrary. Puede crear un control que hereda de la clase MarqueeControl y lo utiliza en un formulario.
Para crear una implementación de MarqueeControl personalizada
-
Abra DemoMarqueeControl en la vista de diseño. Esto creará una instancia del tipo DemoMarqueeControl y la mostrará en una instancia del tipo MarqueeControlRootDesigner.
-
En el Cuadro de herramientas, abra la ficha Componentes de MarqueeControlLibrary. Verá los controles MarqueeBorder y MarqueeText disponibles para la selección.
-
Arrastre una instancia del control MarqueeBorder a la superficie de diseño DemoMarqueeControl. Acople este control MarqueeBorder al control principal.
-
Arrastre una instancia del control MarqueeText a la superficie de diseño DemoMarqueeControl.
-
Genere la solución.
-
Seleccione DemoMarqueeControl y haga clic en el glifo de la etiqueta inteligente. Seleccione la opción Ejecutar prueba para iniciar la animación. Haga clic en Detener prueba para detener la animación.
-
Abra Form1 en la vista de diseño.
-
Coloque dos controles Button en el formulario. Denomínelos startButton y stopButton y cambie los valores de la propiedad Text a "Iniciar" y "Detener", respectivamente.
-
Implemente los controladores de eventos Click para ambos controles Button.
-
En el Cuadro de herramientas, abra la ficha Componentes de MarqueeControlTest. Verá el DemoMarqueeControl disponible para la selección.
-
Arrastre una instancia de DemoMarqueeControl a la superficie de diseño de Form1.
-
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(); }
-
Establezca el proyecto MarqueeControlTest como proyecto de inicio y ejecútelo. Verá el formulario que muestra el DemoMarqueeControl. Haga clic en el botón Iniciar para iniciar la aplicación. Debería ver el texto parpadeante y las luces desplazándose alrededor del borde.
MarqueeControlLibrary muestra una implementación simple de controles personalizados y de los diseñadores asociados. Hay varias formas de realizar este ejemplo más complejo:
-
Cambie los valores de la propiedad para 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 iluminación.
-
Cree sus propias implementaciones de IMarqueeWidget. Por ejemplo, podría crear un "signo de neón" parpadeante o un signo animado con varias imágenes.
-
Siga personalizando la experiencia en tiempo de diseño. Otra posibilidad es sombrear otras propiedades que no sean Enabled y Visible y agregar las nuevas propiedades. Agregue nuevos verbos del diseñador para simplificar las tareas comunes como el acoplamiento de controles secundarios.
-
Otorgue licencias MarqueeControl. Para obtener más información, vea Cómo: Obtener licencia para componentes y controles.
-
Controle cómo se serializan los controles y cómo se genera el código para ellos. Para obtener más información, vea Generación y compilación dinámicas de código fuente.
Tareas
Cómo: Crear un control de formularios Windows Forms que aproveche las características en tiempo de diseñoReferencia
UserControlParentControlDesigner
DocumentDesigner
IRootDesigner
DesignerVerb
UITypeEditor
BackgroundWorker
Otros recursos
Ampliar compatibilidad en tiempo de diseñoDiseñadores personalizados
.NET Shape Library: A Sample Designer
Nota