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

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

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

Тип MarqueeControl реализует отображение текста, похожее на бегущую строку, с анимацией огней и мигающим текстом.

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

В этом пошаговом руководстве демонстрируется выполнение следующих задач.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Возможный порядок MarqueeControl

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

Примечание

Отображаемые диалоговые окна и команды меню могут отличаться от описанных в справке в зависимости от текущих настроек или выпуска.Чтобы изменить параметры, выберите в меню Сервис пункт Импорт и экспорт параметров.Дополнительные сведения см. в разделе Работа с параметрами.

Обязательные компоненты

Для выполнения этого пошагового руководства потребуется следующее.

  • разрешения, необходимые для создания и выполнения проектов приложений Windows Forms на компьютере, на котором установлена Visual Studio.

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

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

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

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

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

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

  1. Добавьте в решение проект "Библиотека элементов управления Windows Forms". Назовите проект "MarqueeControlLibrary".

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

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

  4. С помощью обозревателя решений создайте новую папку в проекте MarqueeControlLibrary. Дополнительные сведения см. в разделе Практическое руководство. Добавление в проект новых элементов. Назовите новую папку "Разработка".

  5. Щелкните правой кнопкой мыши папку Разработка и добавьте новый класс. Назовите новый исходный файл "MarqueeControlRootDesigner".

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

    Примечание

    Чтобы использовать сборку System.Design, в проекте должна использоваться полная версия .NET Framework, а не клиентский профиль .NET Framework.Чтобы изменить требуемую версию .NET Framework, обратитесь к разделу Практическое руководство. Указание конкретной версии или профиля платформы .NET Framework.

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

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

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

  • В проекте MarqueeControlTest добавьте ссылку на сборку MarqueeControlLibrary. Убедитесь, что используется вкладка Проекты в окне Добавить ссылку вместо создания прямой ссылки на сборку MarqueeControlLibrary.

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

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

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

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

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

  1. Откройте файл исходного кода MarqueeControl в Редакторе кода. В верхней части файла импортируйте следующие пространства имен:

    Imports System
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Drawing
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
  2. Добавьте DesignerAttribute в объявление класса MarqueeControl. При этом элемент управления связывается со своим конструктором.

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

    Imports System
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Drawing.Design
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Drawing.Design;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
  4. Измените объявление MarqueeControlRootDesigner для наследования класса DocumentDesigner. Примените ToolboxItemFilterAttribute для определения взаимодействия конструктора с панелью элементов.

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

    Namespace MarqueeControlLibrary.Design
    
        <ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _
        ToolboxItemFilterType.Require), _
        ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _
        ToolboxItemFilterType.Require)> _
        <System.Security.Permissions.PermissionSetAttribute(System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _
        Public Class MarqueeControlRootDesigner
            Inherits DocumentDesigner
    
    namespace MarqueeControlLibrary.Design
    {
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
        [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] 
        public class MarqueeControlRootDesigner : DocumentDesigner
        {
    
  5. Определите конструктор для класса MarqueeControlRootDesigner. Вставьте инструкцию WriteLine в текст конструктора. Она будет использована для отладки.

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

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

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

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

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

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

Imports MarqueeControlLibrary
using MarqueeControlLibrary;
  1. Измените объявление DemoMarqueeControl для наследования класса MarqueeControl.

  2. Выполните построение проекта.

  3. Откройте форму Form1 в конструкторе Windows Forms.

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

  5. Выполните построение проекта.

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

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

Для настройки проекта для отладки во время разработки выполните следующие действия.

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

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

  3. В разделе Действие при запуске выберите Запуск внешней программы. Отладка будет выполняться в отдельном экземпляре Visual Studio, поэтому нажмите кнопку с многоточием (Снимок экрана VisualStudioEllipsesButton) для выбора Visual Studio IDE. Имя исполняемого файла — devenv.exe, и его путь по умолчанию — "%ProgramFiles%\Microsoft Visual Studio 9.0\Common7\IDE\devenv.exe".

  4. Нажмите кнопку ОК, чтобы закрыть диалоговое окно.

  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 Sub Start()
        ' The MarqueeControl may contain any number of 
        ' controls that implement IMarqueeWidget, so 
        ' find each IMarqueeWidget child and call its
        ' StartMarquee method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StartMarquee()
            End If
        Next cntrl
    End Sub
    
    
    Public Sub [Stop]()
        ' The MarqueeControl may contain any number of 
        ' controls that implement IMarqueeWidget, so find
        ' each IMarqueeWidget child and call its StopMarquee
        ' method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StopMarquee()
            End If
        Next cntrl
    End Sub
    
    public void Start()
    {
        // The MarqueeControl may contain any number of 
        // controls that implement IMarqueeWidget, so 
        // find each IMarqueeWidget child and call its
        // StartMarquee method.
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StartMarquee();
            }
        }
    }
    
    public void Stop()
    {
        // The MarqueeControl may contain any number of 
        // controls that implement IMarqueeWidget, so find
        // each IMarqueeWidget child and call its StopMarquee
        // method.
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StopMarquee();
            }
        }
    }
    
  2. Переопределите метод OnLayout.

    Protected Overrides Sub OnLayout(ByVal levent As LayoutEventArgs)
        MyBase.OnLayout(levent)
    
        ' Repaint all IMarqueeWidget children if the layout 
        ' has changed.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                cntrl.PerformLayout()
            End If
        Next cntrl
    End Sub
    
    protected override void OnLayout(LayoutEventArgs levent)
    {
        base.OnLayout (levent);
    
        // Repaint all IMarqueeWidget children if the layout 
        // has changed.
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                Control control = cntrl as Control; 
    
                control.PerformLayout();
            }
        }
    }
    

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

В элементе 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.
       Sub StartMarquee()
    
       ' This method stops the animation. If the control can 
       ' contain other classes that implement IMarqueeWidget as
       ' children, the control should call StopMarquee on all
       ' its IMarqueeWidget child controls.
       Sub StopMarquee()
    
       ' This method specifies the refresh rate for the animation,
       ' in milliseconds.
       Property UpdatePeriod() As Integer
    
    End Interface
    
    // This interface defines the contract for any class that is to
    // be used in constructing a MarqueeControl.
    public interface IMarqueeWidget
    {
        // This method starts the animation. If the control can 
        // contain other classes that implement IMarqueeWidget as
        // children, the control should call StartMarquee on all
        // its IMarqueeWidget child controls.
        void StartMarquee();
    
        // This method stops the animation. If the control can 
        // contain other classes that implement IMarqueeWidget as
        // children, the control should call StopMarquee on all
        // its IMarqueeWidget child controls.
        void StopMarquee();
    
        // This method specifies the refresh rate for the animation,
        // in milliseconds.
        int UpdatePeriod
        {
            get;
            set;
        }
    }
    
  4. Добавьте новый элемент Пользовательский элемент управления в проект MarqueeControlLibrary. Назовите новый исходный файл "MarqueeText".

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

  6. В окне "Свойства" установите для компонента BackgroundWorker свойства WorkerReportsProgess и WorkerSupportsCancellation равными true. Эти параметры позволят компоненту BackgroundWorker периодически создавать событие ProgressChanged и отменять асинхронное обновление. Дополнительные сведения см. в разделе Компонент BackgroundWorker.

  7. Откройте файл исходного кода MarqueeText в Редакторе кода. В верхней части файла импортируйте следующие пространства имен:

    Imports System
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Drawing
    Imports System.Threading
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
    using System;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Drawing;
    using System.Threading;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
  8. Измените объявление MarqueeText для наследования Label и для реализации интерфейса IMarqueeWidget.

    <ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _
    ToolboxItemFilterType.Require)> _
    Partial Public Class MarqueeText
        Inherits Label
        Implements IMarqueeWidget
    
    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
    public partial class MarqueeText : Label, 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 isLit As Boolean = True
    
    ' These fields back the public properties.
    Private updatePeriodValue As Integer = 50
    Private lightColorValue As Color
    Private darkColorValue As Color
    
    ' These brushes are used to paint the light and dark
    ' colors of the text.
    Private lightBrush As Brush
    Private darkBrush As Brush
    
    ' This component updates the control asynchronously.
    Private WithEvents backgroundWorker1 As BackgroundWorker
    
    
    Public Sub New()
        ' This call is required by the Windows.Forms Form Designer.
        InitializeComponent()
    
        ' Initialize light and dark colors 
        ' to the control's default values.
        Me.lightColorValue = Me.ForeColor
        Me.darkColorValue = Me.BackColor
        Me.lightBrush = New SolidBrush(Me.lightColorValue)
        Me.darkBrush = New SolidBrush(Me.darkColorValue)
    End Sub 'New
    
    // When isLit is true, the text is painted in the light color;
    // When isLit is false, the text is painted in the dark color.
    // This value changes whenever the BackgroundWorker component
    // raises the ProgressChanged event.
    private bool isLit = true;
    
    // These fields back the public properties.
    private int updatePeriodValue = 50;
    private Color lightColorValue;
    private Color darkColorValue;
    
    // These brushes are used to paint the light and dark
    // colors of the text.
    private Brush lightBrush;
    private Brush darkBrush;
    
    // This component updates the control asynchronously.
    private BackgroundWorker backgroundWorker1;
    
    public MarqueeText()
    {
        // This call is required by the Windows.Forms Form Designer.
        InitializeComponent();
    
        // Initialize light and dark colors 
        // to the control's default values.
        this.lightColorValue = this.ForeColor;
        this.darkColorValue = this.BackColor;
        this.lightBrush = new SolidBrush(this.lightColorValue);
        this.darkBrush = new SolidBrush(this.darkColorValue);
    }
    
  10. Реализуйте интерфейс IMarqueeWidget.

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

    Атрибуты Category и Browsable применяются к свойству UpdatePeriod, поэтому оно отображается в пользовательском разделе окна "Свойства" под названием "Marquee".

    Public Overridable Sub StartMarquee() _
    Implements IMarqueeWidget.StartMarquee
        ' Start the updating thread and pass it the UpdatePeriod.
        Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod)
    End Sub
    
    Public Overridable Sub StopMarquee() _
    Implements IMarqueeWidget.StopMarquee
        ' Stop the updating thread.
        Me.backgroundWorker1.CancelAsync()
    End Sub
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property UpdatePeriod() As Integer _
    Implements IMarqueeWidget.UpdatePeriod
    
        Get
            Return Me.updatePeriodValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 Then
                Me.updatePeriodValue = Value
            Else
                Throw New ArgumentOutOfRangeException("UpdatePeriod", "must be > 0")
            End If
        End Set
    
    End Property
    
    public virtual void StartMarquee()
    {
        // Start the updating thread and pass it the UpdatePeriod.
        this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod);
    }
    
    public virtual void StopMarquee()
    {
        // Stop the updating thread.
        this.backgroundWorker1.CancelAsync();
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public int UpdatePeriod
    {
        get
        {
            return this.updatePeriodValue;
        }
    
        set
        {
            if (value > 0)
            {
                this.updatePeriodValue = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0");
            }
        }
    }
    
  11. Реализуйте методы доступа к свойству. Клиентам будут предоставлены два свойства: LightColor и DarkColor. Атрибуты Category и Browsable применяются к этим свойствам, поэтому они отображаются в пользовательском разделе окна "Свойства" под названием "Marquee".

    <Category("Marquee"), Browsable(True)> _
    Public Property LightColor() As Color
    
        Get
            Return Me.lightColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The LightColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then
                Me.lightColorValue = Value
                Me.lightBrush = New SolidBrush(Value)
            End If
        End Set
    
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property DarkColor() As Color
    
        Get
            Return Me.darkColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The DarkColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then
                Me.darkColorValue = Value
                Me.darkBrush = New SolidBrush(Value)
            End If
        End Set
    
    End Property
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color LightColor
    {
        get
        {
            return this.lightColorValue;
        }
        set
        {
            // The LightColor property is only changed if the 
            // client provides a different value. Comparing values 
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.lightColorValue.ToArgb() != value.ToArgb())
            {
                this.lightColorValue = value;
                this.lightBrush = new SolidBrush(value);
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color DarkColor
    {
        get
        {
            return this.darkColorValue;
        }
        set
        {
            // The DarkColor property is only changed if the 
            // client provides a different value. Comparing values 
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.darkColorValue.ToArgb() != value.ToArgb())
            {
                this.darkColorValue = value;
                this.darkBrush = new SolidBrush(value);
            }
        }
    }
    
  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 Sub backgroundWorker1_DoWork( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.DoWorkEventArgs) _
    Handles backgroundWorker1.DoWork
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
    
        ' This event handler will run until the client cancels
        ' the background task by calling CancelAsync.
        While Not worker.CancellationPending
            ' The Argument property of the DoWorkEventArgs
            ' object holds the value of UpdatePeriod, which 
            ' was passed as the argument to the RunWorkerAsync
            ' method. 
            Thread.Sleep(Fix(e.Argument))
    
            ' The DoWork eventhandler does not actually report
            ' progress; the ReportProgress event is used to 
            ' periodically alert the control to update its state.
            worker.ReportProgress(0)
        End While
    End Sub
    
    
    ' The ProgressChanged event is raised by the DoWork method.
    ' This event handler does work that is internal to the
    ' control. In this case, the text is toggled between its
    ' light and dark state, and the control is told to 
    ' repaint itself.
    Private Sub backgroundWorker1_ProgressChanged( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
    Handles backgroundWorker1.ProgressChanged
        Me.isLit = Not Me.isLit
        Me.Refresh()
    End Sub
    
            // This method is called in the worker thread's context, 
            // so it must not make any calls into the MarqueeText control.
            // Instead, it communicates to the control using the 
            // ProgressChanged event.
            //
            // The only work done in this event handler is
            // to sleep for the number of milliseconds specified 
            // by UpdatePeriod, then raise the ProgressChanged event.
            private void backgroundWorker1_DoWork(
                object sender,
                System.ComponentModel.DoWorkEventArgs e)
            {
                BackgroundWorker worker = sender as BackgroundWorker;
    
                // This event handler will run until the client cancels
                // the background task by calling CancelAsync.
                while (!worker.CancellationPending)
                {
                    // The Argument property of the DoWorkEventArgs
                    // object holds the value of UpdatePeriod, which 
                    // was passed as the argument to the RunWorkerAsync
                    // method. 
                    Thread.Sleep((int)e.Argument);
    
                    // The DoWork eventhandler does not actually report
                    // progress; the ReportProgress event is used to 
                    // periodically alert the control to update its state.
                    worker.ReportProgress(0);
                }
            }
    
            // The ProgressChanged event is raised by the DoWork method.
            // This event handler does work that is internal to the
            // control. In this case, the text is toggled between its
            // light and dark state, and the control is told to 
            // repaint itself.
            private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
            {
                this.isLit = !this.isLit;
                this.Refresh();
            }
    
    
  13. Переопределите метод OnPaint для включения анимации.

    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
        ' The text is painted in the light or dark color,
        ' depending on the current value of isLit.
        Me.ForeColor = IIf(Me.isLit, Me.lightColorValue, Me.darkColorValue)
    
        MyBase.OnPaint(e)
    End Sub
    
    protected override void OnPaint(PaintEventArgs e)
    {
        // The text is painted in the light or dark color,
        // depending on the current value of isLit.
        this.ForeColor =
            this.isLit ? this.lightColorValue : this.darkColorValue;
    
        base.OnPaint(e);
    }
    
  14. Нажмите клавишу F6 для построения решения.

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

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

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

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

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

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

  3. В окне "Свойства" установите для компонента BackgroundWorker свойства WorkerReportsProgess и WorkerSupportsCancellation равными true. Эти параметры позволят компоненту BackgroundWorker периодически создавать событие ProgressChanged и отменять асинхронное обновление. Дополнительные сведения см. в разделе Компонент BackgroundWorker.

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

  5. Откройте файл исходного кода MarqueeBorder в Редакторе кода. В верхней части файла импортируйте следующие пространства имен:

    Imports System
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Drawing
    Imports System.Drawing.Design
    Imports System.Threading
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
    using System;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Drawing;
    using System.Drawing.Design;
    using System.Threading;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
  6. Измените объявление MarqueeBorder для наследования Panel и для реализации интерфейса IMarqueeWidget.

    <Designer(GetType(MarqueeControlLibrary.Design.MarqueeBorderDesigner)), _
    ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _
    ToolboxItemFilterType.Require)> _
    Partial Public Class MarqueeBorder
        Inherits Panel
        Implements IMarqueeWidget
    
    [Designer(typeof(MarqueeControlLibrary.Design.MarqueeBorderDesigner ))]
    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
    public partial class MarqueeBorder : Panel, IMarqueeWidget
    {
    
  7. Объявите два перечисления для управлением состояния элемента управления MarqueeBorder: MarqueeSpinDirection, определяющее направление "движения" огней вдоль границы, и MarqueeLightShape, определяющее форму огней (квадратную или круглую). Поместите эти объявление перед объявлением класса MarqueeBorder.

    ' This defines the possible values for the MarqueeBorder
    ' control's SpinDirection property.
    Public Enum MarqueeSpinDirection
       CW
       CCW
    End Enum
    
    ' This defines the possible values for the MarqueeBorder
    ' control's LightShape property.
    Public Enum MarqueeLightShape
        Square
        Circle
    End Enum
    
    // This defines the possible values for the MarqueeBorder
    // control's SpinDirection property.
    public enum MarqueeSpinDirection
    {
        CW,
        CCW
    }
    
    // This defines the possible values for the MarqueeBorder
    // control's LightShape property.
    public enum MarqueeLightShape
    {
        Square,
        Circle
    }
    
  8. Объявите переменные экземпляра, соответствующие этим свойствам, и инициализируйте их в конструкторе.

    Public Shared MaxLightSize As Integer = 10
    
    ' These fields back the public properties.
    Private updatePeriodValue As Integer = 50
    Private lightSizeValue As Integer = 5
    Private lightPeriodValue As Integer = 3
    Private lightSpacingValue As Integer = 1
    Private lightColorValue As Color
    Private darkColorValue As Color
    Private spinDirectionValue As MarqueeSpinDirection = MarqueeSpinDirection.CW
    Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
    
    ' These brushes are used to paint the light and dark
    ' colors of the marquee lights.
    Private lightBrush As Brush
    Private darkBrush As Brush
    
    ' This field tracks the progress of the "first" light as it
    ' "travels" around the marquee border.
    Private currentOffset As Integer = 0
    
    ' This component updates the control asynchronously.
    Private WithEvents backgroundWorker1 As System.ComponentModel.BackgroundWorker
    
    
    Public Sub New()
        ' This call is required by the Windows.Forms Form Designer.
        InitializeComponent()
    
        ' Initialize light and dark colors 
        ' to the control's default values.
        Me.lightColorValue = Me.ForeColor
        Me.darkColorValue = Me.BackColor
        Me.lightBrush = New SolidBrush(Me.lightColorValue)
        Me.darkBrush = New SolidBrush(Me.darkColorValue)
    
        ' The MarqueeBorder control manages its own padding,
        ' because it requires that any contained controls do
        ' not overlap any of the marquee lights.
        Dim pad As Integer = 2 * (Me.lightSizeValue + Me.lightSpacingValue)
        Me.Padding = New Padding(pad, pad, pad, pad)
    
        SetStyle(ControlStyles.OptimizedDoubleBuffer, True)
    End Sub
    
    public static int MaxLightSize = 10;
    
    // These fields back the public properties.
    private int updatePeriodValue = 50;
    private int lightSizeValue = 5;
    private int lightPeriodValue = 3;
    private int lightSpacingValue = 1;
    private Color lightColorValue;
    private Color darkColorValue;
    private MarqueeSpinDirection spinDirectionValue = MarqueeSpinDirection.CW;
    private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
    
    // These brushes are used to paint the light and dark
    // colors of the marquee lights.
    private Brush lightBrush;
    private Brush darkBrush;
    
    // This field tracks the progress of the "first" light as it
    // "travels" around the marquee border.
    private int currentOffset = 0;
    
    // This component updates the control asynchronously.
    private System.ComponentModel.BackgroundWorker backgroundWorker1;
    
    public MarqueeBorder()
    {
        // This call is required by the Windows.Forms Form Designer.
        InitializeComponent();
    
        // Initialize light and dark colors 
        // to the control's default values.
        this.lightColorValue = this.ForeColor;
        this.darkColorValue = this.BackColor;
        this.lightBrush = new SolidBrush(this.lightColorValue);
        this.darkBrush = new SolidBrush(this.darkColorValue);
    
        // The MarqueeBorder control manages its own padding,
        // because it requires that any contained controls do
        // not overlap any of the marquee lights.
        int pad = 2 * (this.lightSizeValue + this.lightSpacingValue);
        this.Padding = new Padding(pad, pad, pad, pad);
    
        SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
    }
    
  9. Реализуйте интерфейс IMarqueeWidget.

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

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

    Public Overridable Sub StartMarquee() _
    Implements IMarqueeWidget.StartMarquee
        ' The MarqueeBorder control may contain any number of 
        ' controls that implement IMarqueeWidget, so find
        ' each IMarqueeWidget child and call its StartMarquee
        ' method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StartMarquee()
            End If
        Next cntrl
    
        ' Start the updating thread and pass it the UpdatePeriod.
        Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod)
    End Sub
    
    
    Public Overridable Sub StopMarquee() _
    Implements IMarqueeWidget.StopMarquee
        ' The MarqueeBorder control may contain any number of 
        ' controls that implement IMarqueeWidget, so find
        ' each IMarqueeWidget child and call its StopMarquee
        ' method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StopMarquee()
            End If
        Next cntrl
    
        ' Stop the updating thread.
        Me.backgroundWorker1.CancelAsync()
    End Sub
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Overridable Property UpdatePeriod() As Integer _
    Implements IMarqueeWidget.UpdatePeriod
    
        Get
            Return Me.updatePeriodValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 Then
                Me.updatePeriodValue = Value
            Else
                Throw New ArgumentOutOfRangeException("UpdatePeriod", _
                "must be > 0")
            End If
        End Set
    
    End Property
    
            public virtual void StartMarquee()
            {
                // The MarqueeBorder control may contain any number of 
                // controls that implement IMarqueeWidget, so find
                // each IMarqueeWidget child and call its StartMarquee
                // method.
                foreach (Control cntrl in this.Controls)
                {
                    if (cntrl is IMarqueeWidget)
                    {
                        IMarqueeWidget widget = cntrl as IMarqueeWidget;
                        widget.StartMarquee();
                    }
                }
    
                // Start the updating thread and pass it the UpdatePeriod.
                this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod);
            }
    
            public virtual void StopMarquee()
            {
                // The MarqueeBorder control may contain any number of 
                // controls that implement IMarqueeWidget, so find
                // each IMarqueeWidget child and call its StopMarquee
                // method.
                foreach (Control cntrl in this.Controls)
                {
                    if (cntrl is IMarqueeWidget)
                    {
                        IMarqueeWidget widget = cntrl as IMarqueeWidget;
                        widget.StopMarquee();
                    }
                }
    
                // Stop the updating thread.
                this.backgroundWorker1.CancelAsync();
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            public virtual int UpdatePeriod
            {
                get
                {
                    return this.updatePeriodValue;
                }
    
                set
                {
                    if (value > 0)
                    {
                        this.updatePeriodValue = value;
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0");
                    }
                }
            }
    
    
  10. Реализуйте методы доступа к свойству. Внешний вид элемента управления MarqueeBorder зависит от нескольких свойств.

    <Category("Marquee"), Browsable(True)> _
    Public Property LightSize() As Integer
        Get
            Return Me.lightSizeValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 AndAlso Value <= MaxLightSize Then
                Me.lightSizeValue = Value
                Me.DockPadding.All = 2 * Value
            Else
                Throw New ArgumentOutOfRangeException("LightSize", _
                "must be > 0 and < MaxLightSize")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightPeriod() As Integer
        Get
            Return Me.lightPeriodValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 Then
                Me.lightPeriodValue = Value
            Else
                Throw New ArgumentOutOfRangeException("LightPeriod", _
                "must be > 0 ")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightColor() As Color
        Get
            Return Me.lightColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The LightColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then
                Me.lightColorValue = Value
                Me.lightBrush = New SolidBrush(Value)
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property DarkColor() As Color
        Get
            Return Me.darkColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The DarkColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then
                Me.darkColorValue = Value
                Me.darkBrush = New SolidBrush(Value)
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightSpacing() As Integer
        Get
            Return Me.lightSpacingValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value >= 0 Then
                Me.lightSpacingValue = Value
            Else
                Throw New ArgumentOutOfRangeException("LightSpacing", _
                "must be >= 0")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True), _
    EditorAttribute(GetType(LightShapeEditor), _
    GetType(System.Drawing.Design.UITypeEditor))> _
    Public Property LightShape() As MarqueeLightShape
    
        Get
            Return Me.lightShapeValue
        End Get
    
        Set(ByVal Value As MarqueeLightShape)
            Me.lightShapeValue = Value
        End Set
    
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property SpinDirection() As MarqueeSpinDirection
    
        Get
            Return Me.spinDirectionValue
        End Get
    
        Set(ByVal Value As MarqueeSpinDirection)
            Me.spinDirectionValue = Value
        End Set
    
    End Property
    
            [Category("Marquee")]
            [Browsable(true)]
            public int LightSize
            {
                get
                {
                    return this.lightSizeValue;
                }
    
                set
                {
                    if (value > 0 && value <= MaxLightSize)
                    {
                        this.lightSizeValue = value;
                        this.DockPadding.All = 2 * value;
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("LightSize", "must be > 0 and < MaxLightSize");
                    }
                }
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            public int LightPeriod
            {
                get
                {
                    return this.lightPeriodValue;
                }
    
                set
                {
                    if (value > 0)
                    {
                        this.lightPeriodValue = value;
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("LightPeriod", "must be > 0 ");
                    }
                }
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            public Color LightColor
            {
                get
                {
                    return this.lightColorValue;
                }
    
                set
                {
                    // The LightColor property is only changed if the 
                    // client provides a different value. Comparing values 
                    // from the ToArgb method is the recommended test for
                    // equality between Color structs.
                    if (this.lightColorValue.ToArgb() != value.ToArgb())
                    {
                        this.lightColorValue = value;
                        this.lightBrush = new SolidBrush(value);
                    }
                }
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            public Color DarkColor
            {
                get
                {
                    return this.darkColorValue;
                }
    
                set
                {
                    // The DarkColor property is only changed if the 
                    // client provides a different value. Comparing values 
                    // from the ToArgb method is the recommended test for
                    // equality between Color structs.
                    if (this.darkColorValue.ToArgb() != value.ToArgb())
                    {
                        this.darkColorValue = value;
                        this.darkBrush = new SolidBrush(value);
                    }
                }
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            public int LightSpacing
            {
                get
                {
                    return this.lightSpacingValue;
                }
    
                set
                {
                    if (value >= 0)
                    {
                        this.lightSpacingValue = value;
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("LightSpacing", "must be >= 0");
                    }
                }
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            [EditorAttribute(typeof(LightShapeEditor), 
                 typeof(System.Drawing.Design.UITypeEditor))]
            public MarqueeLightShape LightShape
            {
                get
                {
                    return this.lightShapeValue;
                }
    
                set
                {
                    this.lightShapeValue = value;
                }
            }
    
            [Category("Marquee")]
            [Browsable(true)]
            public MarqueeSpinDirection SpinDirection
            {
                get
                {
                    return this.spinDirectionValue;
                }
    
                set
                {
                    this.spinDirectionValue = value;
                }
            }
    
    
  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 Sub backgroundWorker1_DoWork( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.DoWorkEventArgs) _
    Handles backgroundWorker1.DoWork
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
    
        ' This event handler will run until the client cancels
        ' the background task by calling CancelAsync.
        While Not worker.CancellationPending
            ' The Argument property of the DoWorkEventArgs
            ' object holds the value of UpdatePeriod, which 
            ' was passed as the argument to the RunWorkerAsync
            ' method. 
            Thread.Sleep(Fix(e.Argument))
    
            ' The DoWork eventhandler does not actually report
            ' progress; the ReportProgress event is used to 
            ' periodically alert the control to update its state.
            worker.ReportProgress(0)
        End While
    End Sub
    
    
    ' The ProgressChanged event is raised by the DoWork method.
    ' This event handler does work that is internal to the
    ' control. In this case, the currentOffset is incremented,
    ' and the control is told to repaint itself.
    Private Sub backgroundWorker1_ProgressChanged( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
    Handles backgroundWorker1.ProgressChanged
        Me.currentOffset += 1
        Me.Refresh()
    End Sub
    
    // This method is called in the worker thread's context, 
    // so it must not make any calls into the MarqueeBorder
    // control. Instead, it communicates to the control using 
    // the ProgressChanged event.
    //
    // The only work done in this event handler is
    // to sleep for the number of milliseconds specified 
    // by UpdatePeriod, then raise the ProgressChanged event.
    private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;
    
        // This event handler will run until the client cancels
        // the background task by calling CancelAsync.
        while (!worker.CancellationPending)
        {
            // The Argument property of the DoWorkEventArgs
            // object holds the value of UpdatePeriod, which 
            // was passed as the argument to the RunWorkerAsync
            // method. 
            Thread.Sleep((int)e.Argument);
    
            // The DoWork eventhandler does not actually report
            // progress; the ReportProgress event is used to 
            // periodically alert the control to update its state.
            worker.ReportProgress(0);
        }
    }
    
    // The ProgressChanged event is raised by the DoWork method.
    // This event handler does work that is internal to the
    // control. In this case, the currentOffset is incremented,
    // and the control is told to repaint itself.
    private void backgroundWorker1_ProgressChanged(
        object sender,
        System.ComponentModel.ProgressChangedEventArgs e)
    {
        this.currentOffset++;
        this.Refresh();
    }
    
  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 Overridable Function IsLit(ByVal lightIndex As Integer) As Boolean
        Dim directionFactor As Integer = _
        IIf(Me.spinDirectionValue = MarqueeSpinDirection.CW, -1, 1)
    
        Return (lightIndex + directionFactor * Me.currentOffset) Mod Me.lightPeriodValue = 0
    End Function
    
    
    Protected Overridable Sub DrawLight( _
    ByVal g As Graphics, _
    ByVal brush As Brush, _
    ByVal xPos As Integer, _
    ByVal yPos As Integer)
    
        Select Case Me.lightShapeValue
            Case MarqueeLightShape.Square
                g.FillRectangle( _
                brush, _
                xPos, _
                yPos, _
                Me.lightSizeValue, _
                Me.lightSizeValue)
                Exit Select
            Case MarqueeLightShape.Circle
                g.FillEllipse( _
                brush, _
                xPos, _
                yPos, _
                Me.lightSizeValue, _
                Me.lightSizeValue)
                Exit Select
            Case Else
                Trace.Assert(False, "Unknown value for light shape.")
                Exit Select
        End Select
    
    End Sub
    
    // This method determines if the marquee light at lightIndex
    // should be lit. The currentOffset field specifies where
    // the "first" light is located, and the "position" of the
    // light given by lightIndex is computed relative to this 
    // offset. If this position modulo lightPeriodValue is zero,
    // the light is considered to be on, and it will be painted
    // with the control's lightBrush. 
    protected virtual bool IsLit(int lightIndex)
    {
        int directionFactor =
            (this.spinDirectionValue == MarqueeSpinDirection.CW ? -1 : 1);
    
        return (
            (lightIndex + directionFactor * this.currentOffset) % this.lightPeriodValue == 0
            );
    }
    
    protected virtual void DrawLight(
        Graphics g,
        Brush brush,
        int xPos,
        int yPos)
    {
        switch (this.lightShapeValue)
        {
            case MarqueeLightShape.Square:
                {
                    g.FillRectangle(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue);
                    break;
                }
            case MarqueeLightShape.Circle:
                {
                    g.FillEllipse(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue);
                    break;
                }
            default:
                {
                    Trace.Assert(false, "Unknown value for light shape.");
                    break;
                }
        }
    }
    
  13. Переопределите методы OnLayout и OnPaint.

    Метод OnPaint рисует огни вдоль границ элементов управления MarqueeBorder.

    Метод OnPaint зависит от размеров элемента управления MarqueeBorder, поэтому этот метод нужно вызывать при каждом изменении макета. Для этого переопределите OnLayout и вызовите Refresh.

    Protected Overrides Sub OnLayout(ByVal levent As LayoutEventArgs)
        MyBase.OnLayout(levent)
    
        ' Repaint when the layout has changed.
        Me.Refresh()
    End Sub
    
    
    ' This method paints the lights around the border of the 
    ' control. It paints the top row first, followed by the
    ' right side, the bottom row, and the left side. The color
    ' of each light is determined by the IsLit method and
    ' depends on the light's position relative to the value
    ' of currentOffset.
    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
        Dim g As Graphics = e.Graphics
        g.Clear(Me.BackColor)
    
        MyBase.OnPaint(e)
    
        ' If the control is large enough, draw some lights.
        If Me.Width > MaxLightSize AndAlso Me.Height > MaxLightSize Then
            ' The position of the next light will be incremented 
            ' by this value, which is equal to the sum of the
            ' light size and the space between two lights.
            Dim increment As Integer = _
            Me.lightSizeValue + Me.lightSpacingValue
    
            ' Compute the number of lights to be drawn along the
            ' horizontal edges of the control.
            Dim horizontalLights As Integer = _
            (Me.Width - increment) / increment
    
            ' Compute the number of lights to be drawn along the
            ' vertical edges of the control.
            Dim verticalLights As Integer = _
            (Me.Height - increment) / increment
    
            ' These local variables will be used to position and
            ' paint each light.
            Dim xPos As Integer = 0
            Dim yPos As Integer = 0
            Dim lightCounter As Integer = 0
            Dim brush As Brush
    
            ' Draw the top row of lights.
            Dim i As Integer
            For i = 0 To horizontalLights - 1
                brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush)
    
                DrawLight(g, brush, xPos, yPos)
    
                xPos += increment
                lightCounter += 1
            Next i
    
            ' Draw the lights flush with the right edge of the control.
            xPos = Me.Width - Me.lightSizeValue
    
            ' Draw the right column of lights.
            'Dim i As Integer
            For i = 0 To verticalLights - 1
                brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush)
    
                DrawLight(g, brush, xPos, yPos)
    
                yPos += increment
                lightCounter += 1
            Next i
    
            ' Draw the lights flush with the bottom edge of the control.
            yPos = Me.Height - Me.lightSizeValue
    
            ' Draw the bottom row of lights.
            'Dim i As Integer
            For i = 0 To horizontalLights - 1
                brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush)
    
                DrawLight(g, brush, xPos, yPos)
    
                xPos -= increment
                lightCounter += 1
            Next i
    
            ' Draw the lights flush with the left edge of the control.
            xPos = 0
    
            ' Draw the left column of lights.
            'Dim i As Integer
            For i = 0 To verticalLights - 1
                brush = IIf(IsLit(lightCounter), Me.lightBrush, Me.darkBrush)
    
                DrawLight(g, brush, xPos, yPos)
    
                yPos -= increment
                lightCounter += 1
            Next i
        End If
    End Sub
    
    protected override void OnLayout(LayoutEventArgs levent)
    {
        base.OnLayout(levent);
    
        // Repaint when the layout has changed.
        this.Refresh();
    }
    
    // This method paints the lights around the border of the 
    // control. It paints the top row first, followed by the
    // right side, the bottom row, and the left side. The color
    // of each light is determined by the IsLit method and
    // depends on the light's position relative to the value
    // of currentOffset.
    protected override void OnPaint(PaintEventArgs e)
    {
        Graphics g = e.Graphics;
        g.Clear(this.BackColor);
    
        base.OnPaint(e);
    
        // If the control is large enough, draw some lights.
        if (this.Width > MaxLightSize &&
            this.Height > MaxLightSize)
        {
            // The position of the next light will be incremented 
            // by this value, which is equal to the sum of the
            // light size and the space between two lights.
            int increment =
                this.lightSizeValue + this.lightSpacingValue;
    
            // Compute the number of lights to be drawn along the
            // horizontal edges of the control.
            int horizontalLights =
                (this.Width - increment) / increment;
    
            // Compute the number of lights to be drawn along the
            // vertical edges of the control.
            int verticalLights =
                (this.Height - increment) / increment;
    
            // These local variables will be used to position and
            // paint each light.
            int xPos = 0;
            int yPos = 0;
            int lightCounter = 0;
            Brush brush;
    
            // Draw the top row of lights.
            for (int i = 0; i < horizontalLights; i++)
            {
                brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush;
    
                DrawLight(g, brush, xPos, yPos);
    
                xPos += increment;
                lightCounter++;
            }
    
            // Draw the lights flush with the right edge of the control.
            xPos = this.Width - this.lightSizeValue;
    
            // Draw the right column of lights.
            for (int i = 0; i < verticalLights; i++)
            {
                brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush;
    
                DrawLight(g, brush, xPos, yPos);
    
                yPos += increment;
                lightCounter++;
            }
    
            // Draw the lights flush with the bottom edge of the control.
            yPos = this.Height - this.lightSizeValue;
    
            // Draw the bottom row of lights.
            for (int i = 0; i < horizontalLights; i++)
            {
                brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush;
    
                DrawLight(g, brush, xPos, yPos);
    
                xPos -= increment;
                lightCounter++;
            }
    
            // Draw the lights flush with the left edge of the control.
            xPos = 0;
    
            // Draw the left column of lights.
            for (int i = 0; i < verticalLights; i++)
            {
                brush = IsLit(lightCounter) ? this.lightBrush : this.darkBrush;
    
                DrawLight(g, brush, xPos, yPos);
    
                yPos -= increment;
                lightCounter++;
            }
        }
    }
    

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Imports System
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
  3. Измените объявление MarqueeBorderDesigner для наследования ParentControlDesigner.

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

    Namespace MarqueeControlLibrary.Design
    
        <System.Security.Permissions.PermissionSetAttribute(System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _
        Public Class MarqueeBorderDesigner
            Inherits ParentControlDesigner
    
    namespace MarqueeControlLibrary.Design
    {
        [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] 
        public class MarqueeBorderDesigner : ParentControlDesigner
        {
    
  4. Замените базовую реализацию PreFilterProperties.

    Protected Overrides Sub PreFilterProperties( _
    ByVal properties As IDictionary)
    
        MyBase.PreFilterProperties(properties)
    
        If properties.Contains("Padding") Then
            properties.Remove("Padding")
        End If
    
        properties("Visible") = _
        TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _
        CType(properties("Visible"), PropertyDescriptor), _
        New Attribute(-1) {})
    
        properties("Enabled") = _
        TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _
        CType(properties("Enabled"), _
        PropertyDescriptor), _
        New Attribute(-1) {})
    
    End Sub
    
    protected override void PreFilterProperties(IDictionary properties)
    {
        base.PreFilterProperties(properties);
    
        if (properties.Contains("Padding"))
        {
            properties.Remove("Padding");
        }
    
        properties["Visible"] = TypeDescriptor.CreateProperty(
            typeof(MarqueeBorderDesigner),
            (PropertyDescriptor)properties["Visible"],
            new Attribute[0]);
    
        properties["Enabled"] = TypeDescriptor.CreateProperty(
            typeof(MarqueeBorderDesigner),
            (PropertyDescriptor)properties["Enabled"],
            new Attribute[0]);
    }
    
  5. Реализуйте свойства Enabled и Visible. Эти реализации затеняют свойства элемента управления.

    Public Property Visible() As Boolean
        Get
            Return CBool(ShadowProperties("Visible"))
        End Get
        Set(ByVal Value As Boolean)
            Me.ShadowProperties("Visible") = Value
        End Set
    End Property
    
    
    Public Property Enabled() As Boolean
        Get
            Return CBool(ShadowProperties("Enabled"))
        End Get
        Set(ByVal Value As Boolean)
            Me.ShadowProperties("Enabled") = Value
        End Set
    End Property
    
    public bool Visible
    {
        get
        {
            return (bool)ShadowProperties["Visible"];
        }
        set
        {
            this.ShadowProperties["Visible"] = value;
        }
    }
    
    public bool Enabled
    {
        get
        {
            return (bool)ShadowProperties["Enabled"];
        }
        set
        {
            this.ShadowProperties["Enabled"] = value;
        }
    }
    

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

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

По мере разработки пользователями своих экземпляров MarqueeControl корневой конструктор будет отслеживать изменения в MarqueeControl и дочерних элементах управления. В среде разработки предусмотрена служба IComponentChangeService для отслеживания изменений состояния компонентов.

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

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

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

  1. Откройте файл MarqueeControlRootDesigner в редакторе кода и замените метод Initialize. Вызовите базовую реализацию Initialize и запросите IComponentChangeService.

    MyBase.Initialize(component)
    
    Dim cs As IComponentChangeService = _
    CType(GetService(GetType(IComponentChangeService)), _
    IComponentChangeService)
    
    If (cs IsNot Nothing) Then
        AddHandler cs.ComponentChanged, AddressOf OnComponentChanged
    End If
    
    base.Initialize(component);
    
    IComponentChangeService cs =
        GetService(typeof(IComponentChangeService)) 
        as IComponentChangeService;
    
    if (cs != null)
    {
        cs.ComponentChanged +=
            new ComponentChangedEventHandler(OnComponentChanged);
    }
    
  2. Реализуйте обработчик событий OnComponentChanged. Проверьте тип компонента отправки, и если он находится в IMarqueeWidget, вызовите его метод 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
    
    private void OnComponentChanged(
        object sender,
        ComponentChangedEventArgs e)
    {
        if (e.Component is IMarqueeWidget)
        {
            this.Control.Refresh();
        }
    }
    

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

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

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

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

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

  1. В классе MarqueeControlRootDesigner добавьте обработчики событий OnVerbRunTest и OnVerbStopTest.

    Private Sub OnVerbRunTest( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Dim c As MarqueeControl = CType(Me.Control, MarqueeControl)
        c.Start()
    
    End Sub
    
    Private Sub OnVerbStopTest( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Dim c As MarqueeControl = CType(Me.Control, MarqueeControl)
        c.Stop()
    
    End Sub
    
    private void OnVerbRunTest(object sender, EventArgs e)
    {
        MarqueeControl c = this.Control as MarqueeControl;
    
        c.Start();
    }
    
    private void OnVerbStopTest(object sender, EventArgs e)
    {
        MarqueeControl c = this.Control as MarqueeControl;
    
        c.Stop();
    }
    
  2. Подключите эти обработчики событий к соответствующим командам конструктора. Класс MarqueeControlRootDesigner является производным от базового класса DesignerVerbCollection. Нужно будет создать два новых объекта DesignerVerb и добавить их в эту коллекцию в методе Initialize.

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

Создание пользовательского редактора 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.
    Friend Class LightShapeEditor
        Inherits UITypeEditor
    
    // This class demonstrates the use of a custom UITypeEditor. 
    // It allows the MarqueeBorder control's LightShape property
    // to be changed at design time using a customized UI element
    // that is invoked by the Properties window. The UI is provided
    // by the LightShapeSelectionControl class.
    internal class LightShapeEditor : UITypeEditor
    {
    
  3. Объявите переменную экземпляра IWindowsFormsEditorService под названием editorService.

    Private editorService As IWindowsFormsEditorService = Nothing
    
    private IWindowsFormsEditorService editorService = null;
    
  4. Переопределите метод GetEditStyle. Эта реализация возвращает DropDown, согласно которому среда разработки отображает LightShapeEditor.

            Public Overrides Function GetEditStyle( _
            ByVal context As System.ComponentModel.ITypeDescriptorContext) _
            As UITypeEditorEditStyle
                Return UITypeEditorEditStyle.DropDown
            End Function
    
    
    public override UITypeEditorEditStyle GetEditStyle(
    System.ComponentModel.ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.DropDown;
    }
    
  5. Переопределите метод EditValue. Эта реализация запрашивает в среде разработки объект IWindowsFormsEditorService. В случае успеха она создает LightShapeSelectionControl. Метод DropDownControl вызывается для запуска LightShapeEditor. При этом возвращаемое при этом вызове значение возвращается в среду разработки.

    Public Overrides Function EditValue( _
    ByVal context As ITypeDescriptorContext, _
    ByVal provider As IServiceProvider, _
    ByVal value As Object) As Object
        If (provider IsNot Nothing) Then
            editorService = _
            CType(provider.GetService(GetType(IWindowsFormsEditorService)), _
            IWindowsFormsEditorService)
        End If
    
        If (editorService IsNot Nothing) Then
            Dim selectionControl As _
            New LightShapeSelectionControl( _
            CType(value, MarqueeLightShape), _
            editorService)
    
            editorService.DropDownControl(selectionControl)
    
            value = selectionControl.LightShape
        End If
    
        Return value
    End Function
    
    public override object EditValue(
        ITypeDescriptorContext context,
        IServiceProvider provider,
        object value)
    {
        if (provider != null)
        {
            editorService =
                provider.GetService(
                typeof(IWindowsFormsEditorService))
                as IWindowsFormsEditorService;
        }
    
        if (editorService != null)
        {
            LightShapeSelectionControl selectionControl =
                new LightShapeSelectionControl(
                (MarqueeLightShape)value,
                editorService);
    
            editorService.DropDownControl(selectionControl);
    
            value = selectionControl.LightShape;
        }
    
        return value;
    }
    

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

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

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

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

  2. Из панели элементов перетащите в LightShapeSelectionControl два элемента управления Panel. Назовите их 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;
  1. Реализуйте обработчики событий Click для элементов управления squarePanel и circlePanel. Эти методы вызывают CloseDropDown для окончания сеанса редактирования UITypeEditor.

    Private Sub squarePanel_Click( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Me.lightShapeValue = MarqueeLightShape.Square
        Me.Invalidate(False)
        Me.editorService.CloseDropDown()
    
    End Sub
    
    
    Private Sub circlePanel_Click( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Me.lightShapeValue = MarqueeLightShape.Circle
        Me.Invalidate(False)
        Me.editorService.CloseDropDown()
    
    End Sub
    
            private void squarePanel_Click(object sender, EventArgs e)
            {
                this.lightShapeValue = MarqueeLightShape.Square;
    
                this.Invalidate( false );
    
                this.editorService.CloseDropDown();
            }
    
            private void circlePanel_Click(object sender, EventArgs e)
            {
                this.lightShapeValue = MarqueeLightShape.Circle;
    
                this.Invalidate( false );
    
                this.editorService.CloseDropDown();
            }
    
  2. Объявите переменную экземпляра IWindowsFormsEditorService под названием editorService.

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

    Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
    
    private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
    
  2. В конструкторе 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 Sub New( _
     ByVal lightShape As MarqueeLightShape, _
     ByVal editorService As IWindowsFormsEditorService)
         ' This call is required by the Windows.Forms Form Designer.
         InitializeComponent()
    
         ' Cache the light shape value provided by the 
         ' design-time environment.
         Me.lightShapeValue = lightShape
    
         ' Cache the reference to the editor service.
         Me.editorService = editorService
    
         ' Handle the Click event for the two panels. 
         AddHandler Me.squarePanel.Click, AddressOf squarePanel_Click
         AddHandler Me.circlePanel.Click, AddressOf circlePanel_Click
     End Sub
    
            // This constructor takes a MarqueeLightShape value from the
            // design-time environment, which will be used to display
            // the initial state.
            public LightShapeSelectionControl( 
                MarqueeLightShape lightShape,
                IWindowsFormsEditorService editorService )
            {
                // This call is required by the designer.
                InitializeComponent();
    
                // Cache the light shape value provided by the 
                // design-time environment.
                this.lightShapeValue = lightShape;
    
                // Cache the reference to the editor service.
                this.editorService = editorService;
    
                // Handle the Click event for the two panels. 
                this.squarePanel.Click += new EventHandler(squarePanel_Click);
                this.circlePanel.Click += new EventHandler(circlePanel_Click);
            }
    
  3. В методе Dispose отделите обработчики событий Click.

    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        If disposing Then
    
            ' Be sure to unhook event handlers
            ' to prevent "lapsed listener" leaks.
            RemoveHandler Me.squarePanel.Click, AddressOf squarePanel_Click
            RemoveHandler Me.circlePanel.Click, AddressOf circlePanel_Click
    
            If (components IsNot Nothing) Then
                components.Dispose()
            End If
    
        End If
        MyBase.Dispose(disposing)
    End Sub
    
            protected override void Dispose( bool disposing )
            {
                if( disposing )
                {
                    // Be sure to unhook event handlers
                    // to prevent "lapsed listener" leaks.
                    this.squarePanel.Click -= 
                        new EventHandler(squarePanel_Click);
                    this.circlePanel.Click -= 
                        new EventHandler(circlePanel_Click);
    
                    if(components != null)
                    {
                        components.Dispose();
                    }
                }
                base.Dispose( disposing );
            }
    
  4. Нажмите кнопку Показать все файлы в обозревателе решений. Откройте файл LightShapeSelectionControl.Designer.cs или LightShapeSelectionControl.Designer.vb и удалите определение по умолчанию метода Dispose.

  5. Реализуйте свойство LightShape.

    ' LightShape is the property for which this control provides
    ' a custom user interface in the Properties window.
    Public Property LightShape() As MarqueeLightShape
    
        Get
            Return Me.lightShapeValue
        End Get
    
        Set(ByVal Value As MarqueeLightShape)
            If Me.lightShapeValue <> Value Then
                Me.lightShapeValue = Value
            End If
        End Set
    
    End Property
    
            // LightShape is the property for which this control provides
            // a custom user interface in the Properties window.
            public MarqueeLightShape LightShape
            {
                get
                {
                    return this.lightShapeValue;
                }
    
                set
                {
                    if( this.lightShapeValue != value )
                    {
                        this.lightShapeValue = value;
                    }
                }
            }
    
  6. Переопределите метод OnPaint. Эта реализация отобразит квадрат с заливкой и круг. Также она выделит выбранное значение путем рисования границы вокруг одной или другой фигуры.

    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
        MyBase.OnPaint(e)
    
        Dim gCircle As Graphics = Me.circlePanel.CreateGraphics()
        Try
            Dim gSquare As Graphics = Me.squarePanel.CreateGraphics()
            Try
                ' Draw a filled square in the client area of
                ' the squarePanel control.
                gSquare.FillRectangle( _
                Brushes.Red, _
                0, _
                0, _
                Me.squarePanel.Width, _
                Me.squarePanel.Height)
    
                ' If the Square option has been selected, draw a 
                ' border inside the squarePanel.
                If Me.lightShapeValue = MarqueeLightShape.Square Then
                    gSquare.DrawRectangle( _
                    Pens.Black, _
                    0, _
                    0, _
                    Me.squarePanel.Width - 1, _
                    Me.squarePanel.Height - 1)
                End If
    
                ' Draw a filled circle in the client area of
                ' the circlePanel control.
                gCircle.Clear(Me.circlePanel.BackColor)
                gCircle.FillEllipse( _
                Brushes.Blue, _
                0, _
                0, _
                Me.circlePanel.Width, _
                Me.circlePanel.Height)
    
                ' If the Circle option has been selected, draw a 
                ' border inside the circlePanel.
                If Me.lightShapeValue = MarqueeLightShape.Circle Then
                    gCircle.DrawRectangle( _
                    Pens.Black, _
                    0, _
                    0, _
                    Me.circlePanel.Width - 1, _
                    Me.circlePanel.Height - 1)
                End If
            Finally
                gSquare.Dispose()
            End Try
        Finally
            gCircle.Dispose()
        End Try
    End Sub
    
            protected override void OnPaint(PaintEventArgs e)
            {
                base.OnPaint (e);
    
                using( 
                    Graphics gSquare = this.squarePanel.CreateGraphics(),
                    gCircle = this.circlePanel.CreateGraphics() )
                {   
                    // Draw a filled square in the client area of
                    // the squarePanel control.
                    gSquare.FillRectangle(
                        Brushes.Red, 
                        0,
                        0,
                        this.squarePanel.Width,
                        this.squarePanel.Height
                        );
    
                    // If the Square option has been selected, draw a 
                    // border inside the squarePanel.
                    if( this.lightShapeValue == MarqueeLightShape.Square )
                    {
                        gSquare.DrawRectangle( 
                            Pens.Black,
                            0,
                            0,
                            this.squarePanel.Width-1,
                            this.squarePanel.Height-1);
                    }
    
                    // Draw a filled circle in the client area of
                    // the circlePanel control.
                    gCircle.Clear( this.circlePanel.BackColor );
                    gCircle.FillEllipse( 
                        Brushes.Blue, 
                        0,
                        0,
                        this.circlePanel.Width, 
                        this.circlePanel.Height
                        );
    
                    // If the Circle option has been selected, draw a 
                    // border inside the circlePanel.
                    if( this.lightShapeValue == MarqueeLightShape.Circle )
                    {
                        gCircle.DrawRectangle( 
                            Pens.Black,
                            0,
                            0,
                            this.circlePanel.Width-1,
                            this.circlePanel.Height-1);
                    }
                }   
            }
    

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

Теперь можно построить проект 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 на Start и Stop соответственно.

  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();
}
  1. Установите проект MarqueeControlTest в качестве проекта для запуска и запустите его. Появится элемент форма с четырьмя элементами DemoMarqueeControl. Нажмите кнопку Пуск для запуска анимации. Текст начнет мигать, а огни двигаться вдоль границы.

Следующие действия

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

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

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

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

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

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

См. также

Задачи

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

Ссылки

UserControl

ParentControlDesigner

DocumentDesigner

IRootDesigner

DesignerVerb

UITypeEditor

BackgroundWorker

Другие ресурсы

Расширение поддержки времени разработки

Пользовательские конструкторы

.NET Shape Library: A Sample Designer

Журнал изменений

Дата

Журнал

Причина

Июль 2010

Добавлено примечание о клиентском профиле .NET Framework.

Обратная связь от клиента.