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

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

Осторожность

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

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

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

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

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

Приложение с надписью

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

Необходимые условия

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Заметка

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

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

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

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

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

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

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

  4. Создайте проект.

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

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

  7. Создайте проект.

Настройка проекта для отладки Design-Time

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

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

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

  3. В разделе начальное действие выберите запуск внешней программы. Вы будете отлаживать отдельный экземпляр Visual Studio, поэтому нажмите кнопку с многоточием (кнопку с многоточием (...) в окне свойств Visual Studio) для поиска интегрированной среды разработки Visual Studio. Имя исполняемого файла — devenv.exe, и если вы установили в расположение по умолчанию, то его путь — %ProgramFiles(x86)%\Microsoft Visual Studio\2019\<издания>\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 void Start()
    {
        // The MarqueeControl may contain any number of
        // controls that implement IMarqueeWidget, so
        // find each IMarqueeWidget child and call its
        // StartMarquee method.
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StartMarquee();
            }
        }
    }
    
    public void Stop()
    {
        // The MarqueeControl may contain any number of
        // controls that implement IMarqueeWidget, so find
        // each IMarqueeWidget child and call its StopMarquee
        // method.
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StopMarquee();
            }
        }
    }
    
    Public Sub Start()
        ' The MarqueeControl may contain any number of 
        ' controls that implement IMarqueeWidget, so 
        ' find each IMarqueeWidget child and call its
        ' StartMarquee method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StartMarquee()
            End If
        Next cntrl
    End Sub
    
    
    Public Sub [Stop]()
        ' The MarqueeControl may contain any number of 
        ' controls that implement IMarqueeWidget, so find
        ' each IMarqueeWidget child and call its StopMarquee
        ' method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StopMarquee()
            End If
        Next cntrl
    End Sub
    
  2. Переопределите метод OnLayout.

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

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

MarqueeControl будет размещать два типа дочернего элемента управления: элемент управления MarqueeBorder и элемент управления MarqueeText.

  • MarqueeBorder: этот элемент управления создает рамку из «огоньков» по своим краям. Фонари мигают поочередно, создавая впечатление, что они движутся по периметру. Скорость, с которой мигает свет, управляется свойством, называемым UpdatePeriod. Несколько других настраиваемых свойств определяют другие аспекты внешнего вида элемента управления. Два метода, называемые StartMarquee и StopMarquee, управляют при запуске и остановке анимации.

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

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

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

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

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

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

  2. Откройте исходный файл в редакторе кода и измените объявление с на :

    // This interface defines the contract for any class that is to
    // be used in constructing a MarqueeControl.
    public interface IMarqueeWidget
    {
    
    ' This interface defines the contract for any class that is to
    ' be used in constructing a MarqueeControl.
    Public Interface IMarqueeWidget
    
  3. Добавьте следующий код в интерфейс IMarqueeWidget, чтобы предоставить два метода и свойство, которое управляет анимацией маркирования:

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

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

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

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

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

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

    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
    public partial class MarqueeText : Label, IMarqueeWidget
    {
    
    <ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _
    ToolboxItemFilterType.Require)> _
    Partial Public Class MarqueeText
        Inherits Label
        Implements IMarqueeWidget
    
  9. Объявите переменные экземпляра, соответствующие открытым свойствам, и инициализируйте их в конструкторе. Поле isLit определяет, должен ли текст быть окрашен в цвет, заданный свойством LightColor.

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

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

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

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

    [Category("Marquee")]
    [Browsable(true)]
    public Color LightColor
    {
        get
        {
            return this.lightColorValue;
        }
        set
        {
            // The LightColor property is only changed if the
            // client provides a different value. Comparing values
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.lightColorValue.ToArgb() != value.ToArgb())
            {
                this.lightColorValue = value;
                this.lightBrush = new SolidBrush(value);
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color DarkColor
    {
        get
        {
            return this.darkColorValue;
        }
        set
        {
            // The DarkColor property is only changed if the
            // client provides a different value. Comparing values
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.darkColorValue.ToArgb() != value.ToArgb())
            {
                this.darkColorValue = value;
                this.darkBrush = new SolidBrush(value);
            }
        }
    }
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightColor() As Color
    
        Get
            Return Me.lightColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The LightColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then
                Me.lightColorValue = Value
                Me.lightBrush = New SolidBrush(Value)
            End If
        End Set
    
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property DarkColor() As Color
    
        Get
            Return Me.darkColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The DarkColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then
                Me.darkColorValue = Value
                Me.darkBrush = New SolidBrush(Value)
            End If
        End Set
    
    End Property
    
  12. Реализуйте обработчики для событий BackgroundWorker компонента DoWork и ProgressChanged.

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

    Обработчик событий ProgressChanged переключает текст между его светлым и темным состоянием, чтобы обеспечить внешний вид мигания.

    // This method is called in the worker thread's context,
    // so it must not make any calls into the MarqueeText control.
    // Instead, it communicates to the control using the
    // ProgressChanged event.
    //
    // The only work done in this event handler is
    // to sleep for the number of milliseconds specified
    // by UpdatePeriod, then raise the ProgressChanged event.
    private void backgroundWorker1_DoWork(
        object sender,
        System.ComponentModel.DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;
    
        // This event handler will run until the client cancels
        // the background task by calling CancelAsync.
        while (!worker.CancellationPending)
        {
            // The Argument property of the DoWorkEventArgs
            // object holds the value of UpdatePeriod, which
            // was passed as the argument to the RunWorkerAsync
            // method.
            Thread.Sleep((int)e.Argument);
    
            // The DoWork eventhandler does not actually report
            // progress; the ReportProgress event is used to
            // periodically alert the control to update its state.
            worker.ReportProgress(0);
        }
    }
    
    // The ProgressChanged event is raised by the DoWork method.
    // This event handler does work that is internal to the
    // control. In this case, the text is toggled between its
    // light and dark state, and the control is told to
    // repaint itself.
    private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
    {
        this.isLit = !this.isLit;
        this.Refresh();
    }
    
    
    ' This method is called in the worker thread's context, 
    ' so it must not make any calls into the MarqueeText control.
    ' Instead, it communicates to the control using the 
    ' ProgressChanged event.
    '
    ' The only work done in this event handler is
    ' to sleep for the number of milliseconds specified 
    ' by UpdatePeriod, then raise the ProgressChanged event.
    Private Sub backgroundWorker1_DoWork( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.DoWorkEventArgs) _
    Handles backgroundWorker1.DoWork
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
    
        ' This event handler will run until the client cancels
        ' the background task by calling CancelAsync.
        While Not worker.CancellationPending
            ' The Argument property of the DoWorkEventArgs
            ' object holds the value of UpdatePeriod, which 
            ' was passed as the argument to the RunWorkerAsync
            ' method. 
            Thread.Sleep(Fix(e.Argument))
    
            ' The DoWork eventhandler does not actually report
            ' progress; the ReportProgress event is used to 
            ' periodically alert the control to update its state.
            worker.ReportProgress(0)
        End While
    End Sub
    
    
    ' The ProgressChanged event is raised by the DoWork method.
    ' This event handler does work that is internal to the
    ' control. In this case, the text is toggled between its
    ' light and dark state, and the control is told to 
    ' repaint itself.
    Private Sub backgroundWorker1_ProgressChanged( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
    Handles backgroundWorker1.ProgressChanged
        Me.isLit = Not Me.isLit
        Me.Refresh()
    End Sub
    
  13. Переопределите метод OnPaint, чтобы включить анимацию.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    [Category("Marquee")]
    [Browsable(true)]
    public int LightSize
    {
        get
        {
            return this.lightSizeValue;
        }
    
        set
        {
            if (value > 0 && value <= MaxLightSize)
            {
                this.lightSizeValue = value;
                this.DockPadding.All = 2 * value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("LightSize", "must be > 0 and < MaxLightSize");
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public int LightPeriod
    {
        get
        {
            return this.lightPeriodValue;
        }
    
        set
        {
            if (value > 0)
            {
                this.lightPeriodValue = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("LightPeriod", "must be > 0 ");
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color LightColor
    {
        get
        {
            return this.lightColorValue;
        }
    
        set
        {
            // The LightColor property is only changed if the
            // client provides a different value. Comparing values
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.lightColorValue.ToArgb() != value.ToArgb())
            {
                this.lightColorValue = value;
                this.lightBrush = new SolidBrush(value);
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color DarkColor
    {
        get
        {
            return this.darkColorValue;
        }
    
        set
        {
            // The DarkColor property is only changed if the
            // client provides a different value. Comparing values
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.darkColorValue.ToArgb() != value.ToArgb())
            {
                this.darkColorValue = value;
                this.darkBrush = new SolidBrush(value);
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public int LightSpacing
    {
        get
        {
            return this.lightSpacingValue;
        }
    
        set
        {
            if (value >= 0)
            {
                this.lightSpacingValue = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("LightSpacing", "must be >= 0");
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    [EditorAttribute(typeof(LightShapeEditor),
         typeof(System.Drawing.Design.UITypeEditor))]
    public MarqueeLightShape LightShape
    {
        get
        {
            return this.lightShapeValue;
        }
    
        set
        {
            this.lightShapeValue = value;
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public MarqueeSpinDirection SpinDirection
    {
        get
        {
            return this.spinDirectionValue;
        }
    
        set
        {
            this.spinDirectionValue = value;
        }
    }
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightSize() As Integer
        Get
            Return Me.lightSizeValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 AndAlso Value <= MaxLightSize Then
                Me.lightSizeValue = Value
                Me.DockPadding.All = 2 * Value
            Else
                Throw New ArgumentOutOfRangeException("LightSize", _
                "must be > 0 and < MaxLightSize")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightPeriod() As Integer
        Get
            Return Me.lightPeriodValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 Then
                Me.lightPeriodValue = Value
            Else
                Throw New ArgumentOutOfRangeException("LightPeriod", _
                "must be > 0 ")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightColor() As Color
        Get
            Return Me.lightColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The LightColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then
                Me.lightColorValue = Value
                Me.lightBrush = New SolidBrush(Value)
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property DarkColor() As Color
        Get
            Return Me.darkColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The DarkColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then
                Me.darkColorValue = Value
                Me.darkBrush = New SolidBrush(Value)
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightSpacing() As Integer
        Get
            Return Me.lightSpacingValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value >= 0 Then
                Me.lightSpacingValue = Value
            Else
                Throw New ArgumentOutOfRangeException("LightSpacing", _
                "must be >= 0")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True), _
    EditorAttribute(GetType(LightShapeEditor), _
    GetType(System.Drawing.Design.UITypeEditor))> _
    Public Property LightShape() As MarqueeLightShape
    
        Get
            Return Me.lightShapeValue
        End Get
    
        Set(ByVal Value As MarqueeLightShape)
            Me.lightShapeValue = Value
        End Set
    
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property SpinDirection() As MarqueeSpinDirection
    
        Get
            Return Me.spinDirectionValue
        End Get
    
        Set(ByVal Value As MarqueeSpinDirection)
            Me.spinDirectionValue = Value
        End Set
    
    End Property
    
  11. Реализуйте обработчики событий DoWork и ProgressChanged для компонента BackgroundWorker.

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

    Обработчик событий ProgressChanged увеличивает позицию "базового" света, от которой определяется светло-тёмное состояние остальных светов, и вызывает метод Refresh для перерисовки элемента управления.

    // This method is called in the worker thread's context,
    // so it must not make any calls into the MarqueeBorder
    // control. Instead, it communicates to the control using
    // the ProgressChanged event.
    //
    // The only work done in this event handler is
    // to sleep for the number of milliseconds specified
    // by UpdatePeriod, then raise the ProgressChanged event.
    private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;
    
        // This event handler will run until the client cancels
        // the background task by calling CancelAsync.
        while (!worker.CancellationPending)
        {
            // The Argument property of the DoWorkEventArgs
            // object holds the value of UpdatePeriod, which
            // was passed as the argument to the RunWorkerAsync
            // method.
            Thread.Sleep((int)e.Argument);
    
            // The DoWork eventhandler does not actually report
            // progress; the ReportProgress event is used to
            // periodically alert the control to update its state.
            worker.ReportProgress(0);
        }
    }
    
    // The ProgressChanged event is raised by the DoWork method.
    // This event handler does work that is internal to the
    // control. In this case, the currentOffset is incremented,
    // and the control is told to repaint itself.
    private void backgroundWorker1_ProgressChanged(
        object sender,
        System.ComponentModel.ProgressChangedEventArgs e)
    {
        this.currentOffset++;
        this.Refresh();
    }
    
    ' This method is called in the worker thread's context, 
    ' so it must not make any calls into the MarqueeBorder
    ' control. Instead, it communicates to the control using 
    ' the ProgressChanged event.
    '
    ' The only work done in this event handler is
    ' to sleep for the number of milliseconds specified 
    ' by UpdatePeriod, then raise the ProgressChanged event.
    Private Sub backgroundWorker1_DoWork( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.DoWorkEventArgs) _
    Handles backgroundWorker1.DoWork
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
    
        ' This event handler will run until the client cancels
        ' the background task by calling CancelAsync.
        While Not worker.CancellationPending
            ' The Argument property of the DoWorkEventArgs
            ' object holds the value of UpdatePeriod, which 
            ' was passed as the argument to the RunWorkerAsync
            ' method. 
            Thread.Sleep(Fix(e.Argument))
    
            ' The DoWork eventhandler does not actually report
            ' progress; the ReportProgress event is used to 
            ' periodically alert the control to update its state.
            worker.ReportProgress(0)
        End While
    End Sub
    
    
    ' The ProgressChanged event is raised by the DoWork method.
    ' This event handler does work that is internal to the
    ' control. In this case, the currentOffset is incremented,
    ' and the control is told to repaint itself.
    Private Sub backgroundWorker1_ProgressChanged( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
    Handles backgroundWorker1.ProgressChanged
        Me.currentOffset += 1
        Me.Refresh()
    End Sub
    
  12. Реализуйте вспомогательные методы, IsLit и DrawLight.

    Метод IsLit определяет цвет света в заданной позиции. Огоньки, которые "светятся," отображаются в цвете, заданном свойством LightColor, а "темные" отображаются в цвете, заданном свойством DarkColor.

    Метод DrawLight рисует свет с помощью соответствующего цвета, фигуры и положения.

    // This method determines if the marquee light at lightIndex
    // should be lit. The currentOffset field specifies where
    // the "first" light is located, and the "position" of the
    // light given by lightIndex is computed relative to this
    // offset. If this position modulo lightPeriodValue is zero,
    // the light is considered to be on, and it will be painted
    // with the control's lightBrush.
    protected virtual bool IsLit(int lightIndex)
    {
        int directionFactor =
            (this.spinDirectionValue == MarqueeSpinDirection.CW ? -1 : 1);
    
        return (
            (lightIndex + directionFactor * this.currentOffset) % this.lightPeriodValue == 0
            );
    }
    
    protected virtual void DrawLight(
        Graphics g,
        Brush brush,
        int xPos,
        int yPos)
    {
        switch (this.lightShapeValue)
        {
            case MarqueeLightShape.Square:
                {
                    g.FillRectangle(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue);
                    break;
                }
            case MarqueeLightShape.Circle:
                {
                    g.FillEllipse(brush, xPos, yPos, this.lightSizeValue, this.lightSizeValue);
                    break;
                }
            default:
                {
                    Trace.Assert(false, "Unknown value for light shape.");
                    break;
                }
        }
    }
    
    ' This method determines if the marquee light at lightIndex
    ' should be lit. The currentOffset field specifies where
    ' the "first" light is located, and the "position" of the
    ' light given by lightIndex is computed relative to this 
    ' offset. If this position modulo lightPeriodValue is zero,
    ' the light is considered to be on, and it will be painted
    ' with the control's lightBrush. 
    Protected Overridable Function IsLit(ByVal lightIndex As Integer) As Boolean
        Dim directionFactor As Integer = _
        IIf(Me.spinDirectionValue = MarqueeSpinDirection.CW, -1, 1)
    
        Return (lightIndex + directionFactor * Me.currentOffset) Mod Me.lightPeriodValue = 0
    End Function
    
    
    Protected Overridable Sub DrawLight( _
    ByVal g As Graphics, _
    ByVal brush As Brush, _
    ByVal xPos As Integer, _
    ByVal yPos As Integer)
    
        Select Case Me.lightShapeValue
            Case MarqueeLightShape.Square
                g.FillRectangle( _
                brush, _
                xPos, _
                yPos, _
                Me.lightSizeValue, _
                Me.lightSizeValue)
                Exit Select
            Case MarqueeLightShape.Circle
                g.FillEllipse( _
                brush, _
                xPos, _
                yPos, _
                Me.lightSizeValue, _
                Me.lightSizeValue)
                Exit Select
            Case Else
                Trace.Assert(False, "Unknown value for light shape.")
                Exit Select
        End Select
    
    End Sub
    
  13. Переопределите методы OnLayout и OnPaint.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    namespace MarqueeControlLibrary.Design
    {
        public class MarqueeBorderDesigner : ParentControlDesigner
        {
    
    Namespace MarqueeControlLibrary.Design
    
        Public Class MarqueeBorderDesigner
            Inherits ParentControlDesigner
    
  4. Переопределите базовую реализацию PreFilterProperties.

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

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

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

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

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

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

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

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

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

    base.Initialize(component);
    
    IComponentChangeService cs =
        GetService(typeof(IComponentChangeService))
        as IComponentChangeService;
    
    if (cs != null)
    {
        cs.ComponentChanged +=
            new ComponentChangedEventHandler(OnComponentChanged);
    }
    
    MyBase.Initialize(component)
    
    Dim cs As IComponentChangeService = _
    CType(GetService(GetType(IComponentChangeService)), _
    IComponentChangeService)
    
    If (cs IsNot Nothing) Then
        AddHandler cs.ComponentChanged, AddressOf OnComponentChanged
    End If
    
  2. Реализуйте обработчик событий OnComponentChanged. Проверьте тип компонента отправки и, если это IMarqueeWidget, вызовите его метод Refresh.

    private void OnComponentChanged(
        object sender,
        ComponentChangedEventArgs e)
    {
        if (e.Component is IMarqueeWidget)
        {
            this.Control.Refresh();
        }
    }
    
    Private Sub OnComponentChanged( _
    ByVal sender As Object, _
    ByVal e As ComponentChangedEventArgs)
        If TypeOf e.Component Is IMarqueeWidget Then
            Me.Control.Refresh()
        End If
    End Sub
    

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

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

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

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

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

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

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

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

Создание настраиваемого UITypeEditor

При создании пользовательского интерфейса во время разработки для пользователей часто желательно создать пользовательское взаимодействие с окном "Свойства". Это можно сделать, создав UITypeEditor.

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

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

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

  2. В определении класса MarqueeBorder объявите класс с именем LightShapeEditor, производный от UITypeEditor.

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

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

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

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

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

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

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

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

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

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

    Imports System.Windows.Forms.Design
    
    using System.Windows.Forms.Design;
    
  4. Реализуйте обработчики событий Click для элементов управления squarePanel и circlePanel. Эти методы вызывают CloseDropDown для завершения пользовательского сеанса редактирования UITypeEditor.

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

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

    private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
    
    Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
    
  7. В конструкторе LightShapeSelectionControl подключите обработчики событий Click к событиям squarePanel и circlePanel элементов управления Click. Кроме того, определите перегрузку конструктора, которая назначает значение MarqueeLightShape из среды конструктора полю lightShapeValue.

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

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

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

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

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

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

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

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

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

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

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

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

  5. Создайте решение.

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

  7. Откройте Form1 в представлении конструктора.

  8. Поместите два элемента управления Button в форму. Назовите их startButton и stopButtonи измените значения свойств Text на 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();
    }
    
  13. Задайте проект MarqueeControlTest в качестве запускаемого проекта и запустите его. Вы увидите форму, которая отображает ваш DemoMarqueeControl. Нажмите кнопку Пуск, чтобы запустить анимацию. Вы увидите мигающий текст и свет, перемещающийся вокруг рамки.

Дальнейшие действия

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

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

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

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

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

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

См. также