Пошаговое руководство. Создание элемента управления, в котором используются преимущества функций, применяемых во время разработки

Чтобы оптимизировать взаимодействие с пользовательским элементом управления во время разработки, можно создать соответствующий пользовательский конструктор.

Внимание

Это содержимое было написано для платформа .NET Framework. Если вы используете .NET 6 или более позднюю версию, используйте это содержимое с осторожностью. Система конструктора изменилась для Windows Forms, и важно ознакомиться с изменениями конструктора после платформа .NET Framework статьи.

В этой статье описывается создание пользовательского конструктора для пользовательского элемента управления. Вы реализуете тип MarqueeControl и связанный с ним класс конструктора с именем MarqueeControlRootDesigner.

Тип MarqueeControl реализует экран, похожий на театральную афишу с анимированными фонарями и мигающим текстом.

Конструктор для этого элемента управления взаимодействует со средой разработки, позволяя реализовать пользовательский интерфейс времени разработки. С помощью пользовательского конструктора можно собрать пользовательскую реализацию MarqueeControl с анимированными фонарями и мигающим текстом в различных сочетаниях. Собранный элемент управления можно использовать в форме аналогично любому другому элементу управления Windows Forms.

По завершении работы с этим пошаговым руководством ваш пользовательский элемент управления будет выглядеть примерно так:

The app showing a marquee saying Text and a Start and Stop buttons.

Полный пример кода см. в статье Практическое руководство. Создание элемента управления Windows Forms, в котором используются преимущества функций, применяемых во время разработки.

Необходимые компоненты

Для выполнения действий, описанных в этом пошаговом руководстве, вам понадобится Visual Studio.

Создание проекта

Первым шагом является создание проекта приложения. В этом проекте выполняется сборка приложения, в котором размещается пользовательский элемент управления.

В Visual Studio создайте новый проект приложения Windows Forms и назовите его MarqueeControlTest.

Создание проекта библиотеки элементов управления

  1. Добавьте в решение проект библиотеки элементов управления Windows Forms. Присвойте проекту имя MarqueeControlLibrary.

  2. С помощью обозревателя решений удалите элемент управления проекта по умолчанию, удалив исходный файл с именем UserControl1.cs или UserControl1.vb в зависимости от выбранного языка.

  3. Добавьте новый элемент UserControl в проект MarqueeControlLibrary. Присвойте новому исходному файлу базовое имя MarqueeControl.

  4. С помощью обозревателя решений создайте новую папку в проекте MarqueeControlLibrary.

  5. Щелкните правой кнопкой мыши папку Design и добавьте новый класс. Присвойте ему имя MarqueeControlRootDesigner.

  6. Поскольку вы будете использовать типы из сборки System.Design, добавьте эту ссылку в проект MarqueeControlLibrary.

Ссылки на проект пользовательского элемента управления

Для тестирования пользовательского элемента управления будет использоваться проект MarqueeControlTest. Для работы с пользовательским элементом управления в тестовом проекте необходимо добавить ссылку на сборку MarqueeControlLibrary.

В проекте MarqueeControlTest добавьте ссылку на сборку MarqueeControlLibrary. Для этого необходимо использовать вкладку Проекты в диалоговом окне Добавление ссылки, а не задавать ссылку на сборку MarqueeControlLibrary напрямую.

Определение пользовательского элемента управления и его пользовательского конструктора

Пользовательский элемент управления будет производным от класса UserControl. Благодаря этому ваш элемент управления может содержать другие элементы управления и будет иметь более широкие функциональные возможности по умолчанию.

С пользовательским элементом управления будет связан пользовательский конструктор. Это позволит создать уникальный интерфейс разработки, адаптированный специально для вашего пользовательского элемента управления.

Для связывания элемента управления с конструктором используется класс DesignerAttribute. Поскольку вы полностью определяете поведение пользовательского элемента управления во время разработки, пользовательский конструктор будет реализовывать интерфейс IRootDesigner.

Определение пользовательского элемента управления и его пользовательского конструктора

  1. Откройте исходный файл MarqueeControl в редакторе кода. В верхней части файла импортируйте указанные ниже пространства имен:

    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. Добавьте DesignerAttribute в объявление класса MarqueeControl. Это позволит связать пользовательский элемент управления с его конструктором.

    [Designer( typeof( MarqueeControlLibrary.Design.MarqueeControlRootDesigner ), typeof( IRootDesigner ) )]
    public class MarqueeControl : UserControl
    {
    
    <Designer(GetType(MarqueeControlLibrary.Design.MarqueeControlRootDesigner), _
     GetType(IRootDesigner))> _
    Public Class MarqueeControl
        Inherits UserControl
    
  3. Откройте исходный файл MarqueeControlRootDesigner в редакторе кода. В верхней части файла импортируйте указанные ниже пространства имен:

    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. Измените объявление MarqueeControlRootDesigner таким образом, чтобы задать наследование от класса DocumentDesigner. Примените ToolboxItemFilterAttribute, чтобы задать взаимодействие конструктора с панелью элементов.

    Примечание.

    Определение класса MarqueeControlRootDesigner заключено в пространстве имен с именем MarqueeControlLibrary.Design. Это объявление помещает конструктор в специальное пространство имен, зарезервированное для типов, связанных с разработкой.

    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. Определите конструктор для класса MarqueeControlRootDesigner. Вставьте инструкцию WriteLine в текст конструктора. Это будет использоваться для отладки.

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

Создание экземпляра пользовательского элемента управления

  1. Добавьте новый элемент UserControl в проект MarqueeControlTest. Присвойте новому исходному файлу базовое имя DemoMarqueeControl.

  2. Откройте файл DemoMarqueeControl в редакторе кода. В верхней части исходного файла импортируйте пространство имен MarqueeControlLibrary:

    Imports MarqueeControlLibrary
    
    using MarqueeControlLibrary;
    
  3. Измените объявление DemoMarqueeControl таким образом, чтобы задать наследование от класса MarqueeControl.

  4. Выполните сборку проекта.

  5. Откройте приложение Form1 в конструкторе Windows Forms.

  6. Найдите вкладку Компоненты MarqueeControlTest на панели элементов и откройте ее. Перетащите DemoMarqueeControl с панели элементов в форму.

  7. Выполните сборку проекта.

Настройка проекта для отладки во время разработки

При разработке пользовательского интерфейса времени разработки потребуется выполнять отладку элементов управления и компонентов. Настроить проект для отладки во время разработки очень просто. Дополнительные сведения см. в статье Пошаговое руководство. Отладка пользовательских элементов управления Windows Forms во время разработки.

  1. Щелкните проект MarqueeControlLibrary правой кнопкой мыши и выберите пункт Свойства.

  2. В диалоговом окне Страницы свойств MarqueeControlLibrary выберите страницу Отладка.

  3. В разделе Действие при запуске выберите Запуск внешней программы. Вы будете отлаживать отдельный экземпляр Visual Studio, поэтому нажмите кнопку с многоточием (The Ellipsis button (...) in the Properties window of Visual Studio), чтобы перейти к интегрированной среде разработки Visual Studio. Имя исполняемого файла — devenv.exe. Если вы установили его в расположение по умолчанию, путь к нему — %ProgramFiles(x86)%\Microsoft Visual Studio\2019\<выпуск>\Common7\IDE\devenv.exe.

  4. Выберите OK, чтобы закрыть диалоговое окно.

  5. Щелкните правой кнопкой мыши проект MarqueeControlLibrary и выберите пункт Назначить запускаемым проектом, чтобы включить эту конфигурацию отладки.

Контрольная точка

Теперь все готово к отладке поведения пользовательского элемента управления во время разработки. Проверьте, корректно ли настроена среда отладки, после чего проверьте связь между пользовательским элементом управления и пользовательским конструктором.

Проверка связи между средой отладки и конструктором

  1. Откройте исходный файл MarqueeControlRootDesigner в редакторе кода и поместите точку останова в инструкцию WriteLine.

  2. Нажмите F5 для запуска сеанса отладки.

    Создается новый экземпляр Visual Studio.

  3. В новом экземпляре Visual Studio откройте решение MarqueeControlTest. Чтобы найти решение, выберите Последние проекты в меню Файл. Файл решения MarqueeControlTest.sln будет указан в качестве последнего использованного файла.

  4. Откройте файл DemoMarqueeControl в конструкторе.

    Фокус передается в экземпляр отладки Visual Studio, а выполнение останавливается в точке останова. Нажмите клавишу F5, чтобы продолжить сеанс отладки.

Теперь все готово для разработки и отладки пользовательского элемента управления и связанного с ним пользовательского конструктора. Оставшаяся часть этой статьи посвящена деталям реализации функциональных возможностей элемента управления и конструктора.

Реализация пользовательского элемента управления

Элемент MarqueeControl представляет собой немного измененный элемент UserControl. Он предоставляет два метода, Start и Stop, которые соответственно запускают и останавливают анимацию афиши. Поскольку MarqueeControl содержит дочерние элементы управления, реализующие интерфейс IMarqueeWidget, в методах Start и Stop перечисляются все дочерние элементы управления и вызываются соответственно методы StartMarquee и StopMarquee для каждого дочернего элемента управления, реализующего интерфейс IMarqueeWidget.

Внешний вид элементов управления MarqueeBorder и MarqueeText зависит от макета, поэтому MarqueeControl переопределяет метод OnLayout и вызывает метод PerformLayout для дочерних элементов управления этого типа.

Это все изменения в MarqueeControl. Функциональные возможности времени выполнения реализуются элементами управления MarqueeBorder и MarqueeText, а функции времени разработки — классами MarqueeBorderDesigner и MarqueeControlRootDesigner.

Реализация пользовательского элемента управления

  1. Откройте исходный файл MarqueeControl в редакторе кода. Реализуйте методы Start и 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. Переопределите метод 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
    

Создание дочернего элемента управления для пользовательского элемента управления

Элемент MarqueeControl будет содержать дочерние элементы управления двух видов: MarqueeBorder и MarqueeText.

  • MarqueeBorder — рисует границу из фонарей по краям элемента. Эти фонари последовательно мигают, благодаря чему создается эффект их движения по краям элемента. Частота мигания фонарей определяется свойством UpdatePeriod. Другие аспекты внешнего вида элемента управления задаются с помощью ряда других настраиваемых свойств. Запуск и остановка анимации осуществляются с помощью методов StartMarquee и StopMarquee соответственно.

  • MarqueeText — рисует мигающую строку. Как и в случае с элементом управления MarqueeBorder, частота мигания текста определяется свойством UpdatePeriod. Элемент управления MarqueeText также имеет методы StartMarquee и StopMarquee, аналогичные соответствующим методам элемента управления MarqueeBorder.

Во время разработки MarqueeControlRootDesigner позволяет добавлять эти два типа элементов управления в элемент MarqueeControl в любом сочетании.

Общие функциональные возможности этих двух элементов управления факторизованы в интерфейсе IMarqueeWidget. Благодаря этому MarqueeControl может обнаруживать любые дочерние элементы управления, связанные с афишей, и соответствующим образом работать с ними.

Чтобы реализовать функцию периодической анимации, вы будете использовать объекты BackgroundWorker из пространства имен System.ComponentModel. Вы можете использовать объекты Timer, но при наличии большого количества объектов IMarqueeWidget одного потока пользовательского интерфейса может быть недостаточно для обработки анимации.

Создание дочернего элемента управления для пользовательского элемента управления

  1. Добавьте в проект MarqueeControlLibrary новый элемент класса. Присвойте новому исходному файлу базовое имя IMarqueeWidget.

  2. Откройте исходный файл IMarqueeWidget в редакторе кода и измените объявление с class на 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. Добавьте в интерфейс IMarqueeWidget следующий код, чтобы предоставить два метода и свойство, которые управляют анимацией афиши:

    // 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. Добавьте новый элемент Пользовательский элемент управления в проект MarqueeControlLibrary. Присвойте новому исходному файлу базовое имя MarqueeText.

  5. Перетащите компонент BackgroundWorker из панели элементов в свой элемент управления MarqueeText. Этот компонент будет обеспечивать асинхронное обновление элемента управления MarqueeText.

  6. В окне Свойства присвойте свойствам WorkerReportsProgress и WorkerSupportsCancellation компонента BackgroundWorker значение true. Благодаря этим настройкам компонент BackgroundWorker сможет периодически вызывать событие ProgressChanged и отменять асинхронные обновления.

    Дополнительные сведения см. в разделе Компонент BackgroundWorker.

  7. Откройте исходный файл MarqueeText в редакторе кода. В верхней части файла импортируйте указанные ниже пространства имен:

    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. Измените объявление MarqueeText таким образом, чтобы задать наследование от Label и реализовать интерфейс 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. Объявите переменные экземпляра, соответствующие предоставленным свойствам, и инициализируйте их в конструкторе. Поле isLit определяет, должен ли текст окрашиваться в цвет, заданный свойством 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. Реализуйте интерфейс IMarqueeWidget.

    Методы StartMarquee и StopMarquee вызывают методы RunWorkerAsync и CancelAsync компонента BackgroundWorker для запуска и остановки анимации.

    Атрибуты Category и Browsable применяются к свойству UpdatePeriod, поэтому оно отображается в пользовательском разделе окна свойств с именем 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. Реализация методов доступа к свойства. Клиентам предоставляются два свойства: LightColor и DarkColor. Атрибуты Category и Browsable применяются к этим свойствам, поэтому эти свойства отображаются в пользовательском разделе окна свойств с именем 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. Реализуйте обработчики для событий DoWork и ProgressChanged компонента BackgroundWorker.

    Обработчик события DoWork находится в спящем режиме на протяжении заданного свойством UpdatePeriod периода времени в миллисекундах, после чего вызывает событие ProgressChanged, пока анимация не будет остановлена в коде посредством вызова метода CancelAsync.

    Обработчик события ProgressChanged последовательно изменяет цвет текста со светлого на темный и наоборот, создавая эффект мигания.

    // 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. Чтобы включить анимацию, переопределите метод OnPaint.

    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. Нажмите клавишу F6 , чтобы создать решение.

Создание дочернего элемента управления MarqueeBorder

Элемент управления MarqueeBorder реализован немного сложнее, чем элемент управления MarqueeText. Он содержит больше свойств и более активно использует анимацию в методе OnPaint. В целом, он во многом похож на элемент управления MarqueeText.

Поскольку элемент управления MarqueeBorder может иметь дочерние элементы управления, в нем должна быть реализована обработка событий Layout.

Создание дочернего элемента управления MarqueeBorder

  1. Добавьте новый элемент Пользовательский элемент управления в проект MarqueeControlLibrary. Присвойте новому исходному файлу базовое имя MarqueeBorder.

  2. Перетащите компонент BackgroundWorker из панели элементов в свой элемент управления MarqueeBorder. Этот компонент будет обеспечивать асинхронное обновление элемента управления MarqueeBorder.

  3. В окне Свойства присвойте свойствам WorkerReportsProgress и WorkerSupportsCancellation компонента BackgroundWorker значение true. Благодаря этим настройкам компонент BackgroundWorker сможет периодически вызывать событие ProgressChanged и отменять асинхронные обновления. Дополнительные сведения см. в разделе Компонент BackgroundWorker.

  4. В окне Свойства нажмите кнопку События. Присоедините обработчики для событий DoWork и ProgressChanged.

  5. Откройте исходный файл MarqueeBorder в редакторе кода. В верхней части файла импортируйте указанные ниже пространства имен:

    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. Измените объявление MarqueeBorder таким образом, чтобы задать наследование от Panel и реализовать интерфейс 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. Объявите два перечисления для управления состоянием элемента управления MarqueeBorder: MarqueeSpinDirection (определяет направление кругового движения фонарей по краям элемента) и MarqueeLightShape (определяет квадратную или круглую форму фонарей). Поместите эти объявления перед объявлением класса 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. Объявите переменные экземпляра, соответствующие предоставленным свойствам, и инициализируйте их в конструкторе.

    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. Реализуйте интерфейс IMarqueeWidget.

    Методы StartMarquee и StopMarquee вызывают методы RunWorkerAsync и CancelAsync компонента BackgroundWorker для запуска и остановки анимации.

    Поскольку элемент управления MarqueeBorder может содержать дочерние элементы управления, в методе StartMarquee перечисляются все дочерние элементы управления и вызывается метод StartMarquee для тех из них, которые реализуют интерфейс IMarqueeWidget. Метод StopMarquee имеет аналогичную реализацию.

    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. Реализация методов доступа к свойства. Элемент управления MarqueeBorder содержит несколько свойств, которые используются для управления его внешним видом.

    [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. Реализуйте обработчики для событий DoWork и ProgressChanged компонента BackgroundWorker.

    Обработчик события DoWork находится в спящем режиме на протяжении заданного свойством UpdatePeriod периода времени в миллисекундах, после чего вызывает событие ProgressChanged, пока анимация не будет остановлена в коде посредством вызова метода CancelAsync.

    Обработчик события ProgressChanged увеличивает позицию базового фонаря, на основании которого определяется светлый или темный цвет остальных фонарей, и вызывает метод 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 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. Реализуйте вспомогательные методы IsLit и DrawLight.

    Метод IsLit определяет цвет фонаря в заданной позиции. Горящие фонари отрисовываются цветом, который задается свойством LightColor. Цвет не горящих (темных) фонарей задается свойством DarkColor.

    Метод DrawLight отрисовывает фонарь соответствующих цвета и формы в заданной позиции.

    // 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. Переопределите методы OnLayout и OnPaint.

    Метод OnPaint отрисовывает фонари по краям элемента управления MarqueeBorder.

    Поскольку метод OnPaint зависит от размеров элемента управления MarqueeBorder, его необходимо вызывать при каждом изменении макета. Для этого следует переопределить метод OnLayout и вызвать метод 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
    

Создание пользовательского конструктора для скрытия и фильтрации свойств

Класс MarqueeControlRootDesigner предоставляет реализацию корневого конструктора. Помимо этого конструктора, который работает с элементом управления MarqueeControl, вам потребуется пользовательский конструктор, связанный с элементом управления MarqueeBorder. Этот конструктор реализует пользовательское поведение, соответствующее контексту пользовательского корневого конструктора.

В частности, MarqueeBorderDesigner будет скрывать и фильтровать определенные свойства элемента управления MarqueeBorder, изменяя их взаимодействие со средой разработки.

Перехват вызовов метода доступа к свойству компонента называется "скрытием". Это позволяет конструктору отслеживать заданное пользователем значение и при необходимости передавать его в разрабатываемый компонент.

В этом примере MarqueeBorderDesigner будет скрывать свойства Visible и Enabled, в результате чего пользователь не сможет скрыть или отключить элемент управления MarqueeBorder во время разработки.

Конструкторы также могут добавлять и удалять свойства. В этом примере свойство Padding будет удалено во время разработки, поскольку элемент управления MarqueeBorder программно задает заполнение на основании размера фонарей, задаваемого свойством LightSize.

Базовым для MarqueeBorderDesigner является класс ComponentDesigner, содержащий методы, которые могут изменять предоставляемые элементом управления атрибуты, свойства и события во время разработки:

При изменении общедоступного интерфейса компонента с использованием этих методов соблюдайте следующие правила:

  • Добавляйте или удаляйте элементы только в методах PreFilter.

  • Изменяйте существующие элементы только в методах PostFilter.

  • Всегда вызывайте базовую реализацию в начале в методах PreFilter.

  • Всегда вызывайте базовую реализацию в конце в методах PostFilter.

Соблюдение этих правил гарантирует, что все конструкторы в среде разработки будут иметь согласованное представление всех разрабатываемых компонентов.

Класс ComponentDesigner предоставляет словарь для управления значениями скрытых свойств, что избавляет от необходимости создавать определенные переменные экземпляра.

Создание пользовательского конструктора для скрытия и фильтрации свойств

  1. Щелкните правой кнопкой мыши папку Design и добавьте новый класс. Присвойте новому исходному файлу базовое имя MarqueeBorderDesigner.

  2. Откройте исходный файл MarqueeBorderDesigner в редакторе кода. В верхней части файла импортируйте указанные ниже пространства имен:

    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. Измените объявление MarqueeBorderDesigner таким образом, чтобы задать наследование от ParentControlDesigner.

    Поскольку элемент управления MarqueeBorder может содержать дочерние элементы управления, MarqueeBorderDesigner наследуется от класса ParentControlDesigner, который обрабатывает взаимодействие между родительскими и дочерними элементами.

    namespace MarqueeControlLibrary.Design
    {
        public class MarqueeBorderDesigner : ParentControlDesigner
        {
    
    Namespace MarqueeControlLibrary.Design
    
        Public Class MarqueeBorderDesigner
            Inherits ParentControlDesigner
    
  4. Переопределите базовую реализацию метода 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. Реализуйте свойства Enabled и Visible. Эти реализации скрывают свойства элемента управления.

    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
    

Обработка изменений компонентов

Класс MarqueeControlRootDesigner предоставляет пользовательский интерфейс времени разработки для экземпляров MarqueeControl. Большая часть функциональных возможностей времени разработки наследуется из класса DocumentDesigner. В вашем коде реализуются две конкретные настройки: обработка изменений компонентов и добавление команд конструктора.

Когда пользователи будут разрабатывать собственные экземпляры MarqueeControl, корневой конструктор будет отслеживать изменения в дочерних элементах управления MarqueeControl. Среда времени разработки предлагает удобную службу IComponentChangeService для отслеживания изменений состояния компонента.

Чтобы получить ссылку на эту службу, необходимо выполнить запрос к среде с помощью метода GetService. В случае успешного выполнения запроса конструктор может присоединить обработчик события ComponentChanged и выполнить все задачи, необходимые для поддержания согласованного состояния во время разработки.

Для класса MarqueeControlRootDesigner следует вызывать метод Refresh для каждого объекта IMarqueeWidget, содержащегося в MarqueeControl. При этом объект IMarqueeWidget соответствующим образом перерисовывается при изменении таких свойств, как свойство Size его родительского элемента.

Обработка изменений компонентов

  1. Откройте исходный файл MarqueeControlRootDesigner в редакторе кода и переопределите метод Initialize. Вызовите базовую реализацию Initialize и выполните запрос к 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. Реализуйте обработчик события OnComponentChanged. Проверьте тип отправляющего компонента и, если он является интерфейсом IMarqueeWidget, вызовите его метод 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
    

Добавление команд конструктора в пользовательский конструктор

Команда конструктора ― это команда меню, связанная с обработчиком событий. Команды конструктора добавляются в контекстное меню компонента во время разработки. Дополнительные сведения см. в разделе DesignerVerb.

Здесь вы добавите в свои конструкторы две команды: Запустить тест и Остановить тест. С помощью этих команд во время разработки можно просматривать поведение MarqueeControl во время выполнения. Эти команды будут добавлены в MarqueeControlRootDesigner.

При вызове команды Запустить тест обработчик событий команды вызовет метод StartMarquee для MarqueeControl. При вызове команды Остановить тест обработчик событий команды вызовет метод StopMarquee для MarqueeControl. В реализациях методов StartMarquee и StopMarquee эти методы вызываются для содержащихся элементов управления, которые реализуют IMarqueeWidget. Поэтому в тесте будут использоваться все содержащиеся элементы управления IMarqueeWidget.

Добавление команд конструктора в пользовательские конструкторы

  1. Добавьте в класс MarqueeControlRootDesigner обработчики событий с именами OnVerbRunTest и 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. Присоедините эти обработчики событий к соответствующим командам конструктора. MarqueeControlRootDesigner наследует DesignerVerbCollection из базового класса. Вы создадите два новых объекта DesignerVerb и добавите их в эту коллекцию в методе 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)))
    

Создание пользовательского элемента UITypeEditor

При создании собственного интерфейса времени разработки для пользователей часто требуется реализовать пользовательское взаимодействие с окном свойств. Чтобы добиться этого, можно создать элемент UITypeEditor.

В окне свойств элемента управления MarqueeBorder предоставляется несколько свойств. Два из них (MarqueeSpinDirection и MarqueeLightShape) представлены перечислениями. Чтобы продемонстрировать применение редактора типов пользовательского интерфейса, свойство MarqueeLightShape будет иметь связанный класс UITypeEditor.

Создание собственного редактора типов пользовательского интерфейса

  1. Откройте исходный файл MarqueeBorder в редакторе кода.

  2. В определении класса MarqueeBorder объявите класс с именем 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.
    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. Объявите переменную экземпляра IWindowsFormsEditorService с именем editorService.

    private IWindowsFormsEditorService editorService = null;
    
    Private editorService As IWindowsFormsEditorService = Nothing
    
  4. Переопределите метод GetEditStyle. Эта реализация возвращает DropDown, на основании чего среда разработки определяет, как отобразить 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. Переопределите метод EditValue. Эта реализация запрашивает объект IWindowsFormsEditorService в среде разработки. В случае успешного выполнения запроса создается LightShapeSelectionControl. Метод DropDownControl вызывается для запуска LightShapeEditor. Возвращаемое этим вызовом значение передается в среду разработки.

    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
    

Создание элемента управления представлением для собственного элемента UITypeEditor

Свойство MarqueeLightShape поддерживает две формы фонарей: Square и Circle. Вы создадите пользовательский элемент управления, который используется исключительно для графического отображения этих значений в окно свойств. Этот пользовательский элемент управления будет обеспечивать взаимодействие UITypeEditor с окном свойств.

Создание элемента управления представлением для собственного редактора типов пользовательского интерфейса

  1. Добавьте новый элемент UserControl в проект MarqueeControlLibrary. Присвойте новому исходному файлу базовое имя LightShapeSelectionControl.

  2. Перетащите два элемента управления Panel с панели элементов на элемент LightShapeSelectionControl. Присвойте им имена squarePanel и circlePanel. Расположите их рядом друг с другом. Присвойте свойству Size обоих элементов управления Panel значение (60, 60). Присвойте свойству Location элемента управления squarePanel значение (8, 10). Присвойте свойству Location элемента управления circlePanel значение (80, 10). Наконец, присвойте свойству Size объекта LightShapeSelectionControl значение (150, 80).

  3. Откройте исходный файл LightShapeSelectionControl в редакторе кода. В верхней части исходного файла импортируйте пространство имен System.Windows.Forms.Design:

    Imports System.Windows.Forms.Design
    
    using System.Windows.Forms.Design;
    
  4. Реализуйте обработчики события Click для элементов управления squarePanel и circlePanel. Эти методы вызывают метод CloseDropDown для завершения сеанса редактирования собственного элемента 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();
    }
    
    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. Объявите переменную экземпляра IWindowsFormsEditorService с именем editorService.

    Private editorService As IWindowsFormsEditorService
    
    private IWindowsFormsEditorService editorService;
    
  6. Объявите переменную экземпляра MarqueeLightShape с именем lightShapeValue.

    private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
    
    Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
    
  7. В конструкторе LightShapeSelectionControl присоедините обработчики события Click к событиям Click элементов управления squarePanel и circlePanel. Также определите перегрузку конструктора, которая присваивает значение MarqueeLightShape из среды разработки полю 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. В методе Dispose отсоедините обработчики события 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. В обозревателе решений нажмите кнопку Показать все файлы. Откройте файл LightShapeSelectionControl.Designer.cs или LightShapeSelectionControl.Designer.vb и удалите заданное по умолчанию определение метода Dispose.

  10. Реализуйте свойство 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. Переопределите метод OnPaint. Эта реализация будет отрисовывать закрашенный квадрат и круг. Она также будет выделять выбранное значение путем отрисовки границы вокруг одной из этих фигур.

    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
    

Тестирование пользовательского элемента управления в конструкторе

На этом этапе можно выполнить сборку проекта MarqueeControlLibrary. Чтобы протестировать реализацию, создайте элемент управления, наследуемый от класса MarqueeControl, и используйте его в форме.

Создание пользовательской реализации элемента управления MarqueeControl

  1. Откройте DemoMarqueeControl в конструкторе Windows Forms. При этом будет создан экземпляр типа DemoMarqueeControl, который будет отображаться в экземпляре типа MarqueeControlRootDesigner.

  2. На панели элементов откройте вкладку Компоненты MarqueeControlLibrary. Обратите внимание на доступные для выбора элементы управления MarqueeBorder и MarqueeText.

  3. Перетащите экземпляр элемента управления MarqueeBorder в область конструктора DemoMarqueeControl. Закрепите этот элемент управления MarqueeBorder в родительском элементе управления.

  4. Перетащите экземпляр элемента управления MarqueeText в область конструктора DemoMarqueeControl.

  5. Постройте решение.

  6. Щелкните правой кнопкой мыши элемент DemoMarqueeControl, а затем в контекстном меню выберите команду Запустить тест, чтобы запустить анимацию. Выберите команду Остановить тест, чтобы остановить анимацию.

  7. Откройте форму Form1 в конструкторе.

  8. Поместите в форму два элемента управления Button. Присвойте им имена startButton и stopButton, а затем измените значения свойств Text на Запустить и Остановить соответственно.

  9. Реализуйте обработчики события Click для обоих элементов управления Button.

  10. На панели элементов откройте вкладку Компоненты MarqueeControlTest. Обратите внимание на доступный для выбора элемент управления DemoMarqueeControl.

  11. Перетащите экземпляр DemoMarqueeControl в область конструктора Form1.

  12. В обработчиках события Click вызовите методы Start и Stop для 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. Настройте проект MarqueeControlTest как запускаемый проект и запустите его. Появится форма, в которой отображается ваш элемент DemoMarqueeControl. Нажмите кнопку Запустить, чтобы запустить анимацию. Текст начнет мигать, а по краям рамки будут двигаться фонари.

Следующие шаги

На примере MarqueeControlLibrary демонстрируется простая реализация пользовательских элементов управления и связанных конструкторов. Этот пример можно усложнить несколькими способами:

  • Измените значения свойств элемента управления DemoMarqueeControl в конструкторе. Добавьте дополнительные элементы управления MarqueBorder и закрепите их в родительских экземплярах, чтобы создать эффект вложенного размещения. Поэкспериментируйте с различными значениями UpdatePeriod и связанных с фонарями свойств.

  • Создайте собственные реализации IMarqueeWidget. Например, можно создать мигающую "неоновую вывеску" или анимированную вывеску с несколькими изображениями.

  • Выполните дополнительную настройку интерфейса времени разработки. Попробуйте скрыть другие свойства в дополнение к Enabled и Visible, а также добавить новые свойства. Добавьте новые команды конструктора, чтобы упростить выполнение распространенных задач, таких как закрепление дочерних элементов управления.

  • Лицензируйте элемент MarqueeControl.

  • Реализуйте управление сериализацией элементов управления и созданием кода для них. Дополнительные сведения см. в разделе Динамическое создание и компиляция исходного кода.

См. также