Exemplarische Vorgehensweise: Erstellen eines Steuerelements, das Entwurfszeitfeatures nutzt

Die Entwurfszeitfunktionalität für ein benutzerdefiniertes Steuerelement kann durch die Erstellung eines zugehörigen benutzerdefinierten Designers erweitert werden.

Achtung

Dieser Inhalt wurde für .NET Framework geschrieben. Wenn Sie .NET 6 oder eine höhere Version verwenden, verwenden Sie diesen Inhalt mit Vorsicht. Das Designersystem hat sich für Windows Forms geändert, und es ist wichtig, dass Sie die Designeränderungen seit .NET Framework -Artikel überprüfen.

Dieser Artikel veranschaulicht, wie Sie einen benutzerdefinierten Designer für ein benutzerdefiniertes Steuerelement erstellen. Sie implementieren einen MarqueeControl-Typ und eine zugeordnete Designerklasse namens MarqueeControlRootDesigner.

Der MarqueeControl Typ implementiert eine Anzeige ähnlich einer Lichtwerbung an der Außenfassade eines Kinos, mit animierten Lichtern und blinkendem Text.

Der Designer für dieses Steuerelement interagiert mit der Entwurfsumgebung, um eine benutzerdefinierte Entwurfszeitfunktionalität bereitzustellen. Mit dem benutzerdefinierten Designer können Sie eine benutzerdefinierte MarqueeControl-Implementierung mit animierten Lichtern und blinkendem Text in vielen Kombinationen zusammenstellen. Sie können das erstellte Steuerelement anschließend wie jedes andere Windows Forms-Steuerelement in einem Formular verwenden.

Nach Abschluss dieser exemplarischen Vorgehensweise wird Ihr benutzerdefiniertes Steuerelement in etwa wie das folgende aussehen:

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

Den vollständigen Code finden Sie unter Vorgehensweise: Erstellen eines Windows Forms-Steuerelements, das Entwurfszeitfeatures nutzt.

Voraussetzungen

Sie benötigen Visual Studio, um diese exemplarische Vorgehensweise bearbeiten zu können.

Erstellen eines Projekts

Im ersten Schritt erstellen Sie das Anwendungsprojekt. Sie verwenden dieses Projekt, um die Anwendung zu erstellen, die das benutzerdefinierte Steuerelement enthält.

Erstellen Sie in Visual Studio ein neues Windows Forms-Anwendungsprojekt, und geben Sie ihm den Namen MarqueeControlTest.

Erstellen des Steuerelementbibliotheksprojekts

  1. Fügen Sie der Projektmappe ein Projekt des Typs „Windows Forms-Steuerelementbibliothek“ hinzu. Geben Sie dem Projekt den Namen MarqueeControlLibrary.

  2. Löschen Sie mit dem Projektmappen-Explorer das Standardsteuerelement des Projekts, indem Sie die Quelldatei mit dem Namen „UserControl1.cs“ oder „UserControl1.vb“ löschen, je nach der von Ihnen gewählten Sprache.

  3. Fügen Sie dem MarqueeControlLibrary-Projekt ein neues UserControl-Element hinzu. Geben Sie der neuen Quelldatei den Basisnamen MarqueeControl.

  4. Erstellen Sie über den Projektmappen-Explorer einen neuen Ordner im MarqueeControlLibrary-Projekt.

  5. Klicken Sie mit der rechten Maustaste auf den Ordner Design, und fügen Sie eine neue Klasse hinzu. Nennen Sie sie MarqueeControlRootDesigner.

  6. Sie müssen Typen aus der Assembly „System.Design“ verwenden, also fügen Sie diesen Verweis zum Projekt MarqueeControlLibrary hinzu.

Verweisen auf das benutzerdefinierte Steuerelement

Sie werden das benutzerdefinierte Steuerelement mithilfe des Projekts MarqueeControlTest testen. Das Testprojekt erhält Kenntnis über das benutzerdefinierte Steuerelement, wenn Sie einen Projektverweis auf die Assembly MarqueeControlLibrary hinzufügen.

Fügen Sie dem Projekt MarqueeControlTest einen Projektverweis auf die Assembly MarqueeControlLibrary hinzu. Achten Sie darauf, die Registerkarte Projekte im Dialogfeld Verweis hinzufügen zu verwenden, anstatt direkt auf die MarqueeControlLibrary-Assembly zu verweisen.

Definieren eines benutzerdefinierten Steuerelements und des zugehörigen benutzerdefinierten Designers

Ihr benutzerdefiniertes Steuerelement wird von der Klasse UserControl abgeleitet. Auf diese Weise kann Ihr Steuerelement andere Steuerelemente enthalten, und Ihr Steuerelement erhält zahlreiche Standardfunktionen.

Ihr benutzerdefiniertes Steuerelement verfügt über einen zugeordneten benutzerdefinierten Designer. So können Sie eine individuelle Designfunktionalität erstellen, die speziell auf Ihr benutzerdefiniertes Steuerelement zugeschnitten ist.

Sie ordnen das Steuerelement mithilfe der DesignerAttribute-Klasse dem zugehörigen Designer zu. Da Sie das gesamte Entwurfszeitverhalten Ihres benutzerdefinierten Steuerelements entwickeln, implementiert der benutzerdefinierte Designer die Schnittstelle IRootDesigner.

So definieren Sie ein benutzerdefiniertes Steuerelement und den zugehörigen benutzerdefinierten Designer

  1. Öffnen Sie die Quelldatei MarqueeControl im Code-Editor. Importieren Sie am Anfang der Datei die folgenden Namespaces:

    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. Fügen Sie das DesignerAttribute der MarqueeControl-Klassendeklaration hinzu. Dadurch wird das benutzerdefinierte Steuerelement dem entsprechenden Designer zugeordnet.

    [Designer( typeof( MarqueeControlLibrary.Design.MarqueeControlRootDesigner ), typeof( IRootDesigner ) )]
    public class MarqueeControl : UserControl
    {
    
    <Designer(GetType(MarqueeControlLibrary.Design.MarqueeControlRootDesigner), _
     GetType(IRootDesigner))> _
    Public Class MarqueeControl
        Inherits UserControl
    
  3. Öffnen Sie die Quelldatei MarqueeControlRootDesigner im Code-Editor. Importieren Sie am Anfang der Datei die folgenden Namespaces:

    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. Ändern Sie die Deklaration von MarqueeControlRootDesigner, um von der Klasse DocumentDesigner zu erben. Wenden Sie das ToolboxItemFilterAttribute an, um die Designerinteraktion mit der Toolbox anzugeben.

    Hinweis

    Die Definition für die MarqueeControlRootDesigner-Klasse wurde in einen Namespace namens „MarqueeControlLibrary.Design“ eingeschlossen. Diese Deklaration platziert den Designer in einem speziellen Namespace, der für entwurfsbezogene Typen reserviert ist.

    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. Definieren Sie den Konstruktor für die MarqueeControlRootDesigner-Klasse. Fügen Sie eine WriteLine-Anweisung in den Konstruktortext ein. Dies ist nützlich für das Debuggen.

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

Erstellen einer Instanz Ihres benutzerdefinierten Steuerelements

  1. Fügen Sie dem MarqueeControlTest-Projekt ein neues UserControl-Element hinzu. Geben Sie der neuen Quelldatei den Basisnamen DemoMarqueeControl.

  2. Öffnen Sie die Datei DemoMarqueeControl im Code-Editor. Importieren Sie am Anfang der Datei den MarqueeControlLibrary-Namespace:

    Imports MarqueeControlLibrary
    
    using MarqueeControlLibrary;
    
  3. Ändern Sie die Deklaration von DemoMarqueeControl, um von der Klasse MarqueeControl zu erben.

  4. Erstellen Sie das Projekt.

  5. Öffnen Sie „Form1“ im Windows Forms-Designer.

  6. Suchen Sie in der Toolbox nach der Registerkarte MarqueeControlTest-Komponenten, und öffnen Sie sie. Ziehen Sie ein DemoMarqueeControl aus der Toolbox auf Ihr Formular.

  7. Erstellen Sie das Projekt.

Einrichten des Projekts für das Entwurfszeitdebuggen

Bei der Entwicklung einer benutzerdefinierten Entwurfszeitfunktionalität ist es notwendig, Ihre Steuerelemente und Komponenten zu debuggen. Es gibt eine einfache Möglichkeit, Ihr Projekt einzurichten, um das Debuggen zur Entwurfszeit zu ermöglichen. Weitere Informationen finden Sie unter Exemplarische Vorgehensweise: Debuggen von benutzerdefinierten Windows Forms-Steuerelementen zur Entwurfszeit.

  1. Klicken Sie mit der rechten Maustaste auf das MarqueeControlLibrary-Projekt, und wählen Sie Eigenschaften aus.

  2. Wählen Sie im Dialogfeld MarqueeControlLibrary-Eigenschaftenseiten die Seite Debuggen aus.

  3. Wählen Sie im Abschnitt Startaktion die Option Externes Programm starten aus. Sie debuggen eine separate Instanz von Visual Studio, also klicken Sie auf die Schaltfläche mit Auslassungspunkten (The Ellipsis button (...) in the Properties window of Visual Studio), um nach der Visual Studio-IDE zu suchen. Die ausführbare Datei heißt „devenv.exe“, und wenn Sie die Datei am Standardspeicherort installiert haben, lautet der zugehörige Pfad %ProgramFiles(x86)%\Microsoft Visual Studio\2019\<edition>\Common7\IDE\devenv.exe.

  4. Wählen Sie OK aus, um das Dialogfeld zu schließen.

  5. Klicken Sie mit der rechten Maustaste auf das MarqueeControlLibrary-Projekt, und wählen Sie Als Startprojekt festlegen aus, um diese Debugkonfiguration zu aktivieren.

Prüfpunkt

Sie können nun das Entwurfszeitverhalten Ihres benutzerdefinierten Steuerelements debuggen. Nachdem Sie sich davon überzeugt haben, dass die Debugumgebung korrekt eingerichtet ist, testen Sie die Verbindung zwischen dem benutzerdefinierten Steuerelement und dem benutzerdefinierten Designer.

So testen Sie die Debugumgebung und die Designerzuordnung

  1. Öffnen Sie die Quelldatei „MarqueeControlRootDesigner“ im Code-Editor, und platzieren Sie einen Haltepunkt in der WriteLine-Anweisung.

  2. Drücken Sie F5, um die Debugsitzung zu starten.

    Eine neue Instanz von Visual Studio wird erstellt.

  3. Öffnen Sie die Projektmappe „MarqueeControlTest“ in der neuen Visual Studio-Instanz. Sie können die Projektmappe ganz einfach finden, indem Sie im Menü Datei die Option Zuletzt verwendete Projekte auswählen. Die Projektmappendatei „MarqueeControlTest.sln“ wird als zuletzt verwendete Datei aufgeführt.

  4. Öffnen Sie DemoMarqueeControl im Designer.

    Die Debuginstanz von Visual Studio erhält den Fokus, und die Ausführung stoppt an Ihrem Haltepunkt. Drücken Sie F5, um die Debugsitzung fortzusetzen.

An diesem Punkt sind alle Voraussetzungen für die Entwicklung und das Debuggen Ihres benutzerdefinierten Steuerelements und des zugehörigen benutzerdefinierten Designers gegeben. Im verbleibenden Teil dieses Artikels geht es um die Einzelheiten der Implementierung von Features für das Steuerelement und den Designer.

Implementieren des benutzerdefinierten Steuerelements

Das MarqueeControl ist ein UserControl mit einigen Anpassungen. Es macht zwei Methoden verfügbar: Start, das die Marquee-Animation startet, und Stop, das die Animation beendet. Da MarqueeControl untergeordnete Steuerelemente enthält, die die Schnittstelle IMarqueeWidget implementieren, zählen Start und Stop jedes untergeordnete Steuerelement auf und rufen die Methoden StartMarquee und StopMarquee für jedes untergeordnete Steuerelement auf, das IMarqueeWidget implementiert.

Das Aussehen der Steuerelemente MarqueeBorder und MarqueeText hängt vom Layout ab, daher überschreibt MarqueeControl die Methode OnLayout und ruft PerformLayout für untergeordnete Steuerelemente dieses Typs auf.

Dies ist der Umfang der MarqueeControl-Anpassungen. Die Laufzeitfeatures werden durch die Steuerelemente MarqueeBorder und MarqueeText implementiert, die Entwurfszeitfeatures werden durch die Klassen MarqueeBorderDesigner und MarqueeControlRootDesigner implementiert.

So implementieren Sie Ihr benutzerdefiniertes Steuerelement

  1. Öffnen Sie die Quelldatei MarqueeControl im Code-Editor. Implementieren Sie die Methoden Start und 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. Überschreiben Sie die OnLayout -Methode.

    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
    

Erstellen eines untergeordneten Steuerelements für Ihr benutzerdefiniertes Steuerelement

MarqueeControl hostet zwei Arten von untergeordneten Steuerelementen: das Steuerelement MarqueeBorder und das Steuerelement MarqueeText.

  • MarqueeBorder: Dieses Steuerelement zeichnet einen Rahmen aus „Lichtern“ um seine Ränder. Sie leuchten nacheinander auf, sodass der Eindruck entsteht, sie würden sich um den Rand herum bewegen. Die Geschwindigkeit, mit der die Lichter blinken, wird durch eine Eigenschaft namens UpdatePeriod gesteuert. Verschiedene weitere benutzerdefinierte Eigenschaften bestimmen andere Aspekte des Aussehens des Steuerelements. Die beiden Methoden StartMarquee und StopMarquee steuern, wann die Animation beginnt und endet.

  • MarqueeText: Dieses Steuerelement erzeugt eine blinkende Zeichenfolge. Wie beim Steuerelement MarqueeBorder wird die Geschwindigkeit, mit der der Text blinkt, durch die Eigenschaft UpdatePeriod gesteuert. Das Steuerelement MarqueeText verfügt ebenso wie das Steuerelement MarqueeBorder über die Methoden StartMarquee und StopMarquee.

Zur Entwurfszeit können diese beiden Steuerelementtypen mit MarqueeControlRootDesigner in beliebiger Kombination zu einem MarqueeControl hinzugefügt werden.

Die gemeinsamen Merkmale der beiden Steuerelemente werden in der Schnittstelle IMarqueeWidget zusammengefasst. Dadurch kann MarqueeControl alle Marquee-bezogenen untergeordneten Steuerelemente erkennen und entsprechend verarbeiten.

Um die intermittierende Animation zu implementieren, verwenden Sie BackgroundWorker-Objekte aus dem System.ComponentModel-Namespace. Sie könnten zwar Timer-Objekte verwenden, aber wenn viele IMarqueeWidget-Objekte vorhanden sind, kann der einzelne UI-Thread möglicherweise nicht mit der Animation Schritt halten.

So erstellen Sie ein untergeordnetes Steuerelement für Ihr benutzerdefiniertes Steuerelement

  1. Fügen Sie dem MarqueeControlLibrary-Projekt ein neues Klassenelement hinzu. Geben Sie der neuen Quelldatei den Basisnamen „IMarqueeWidget“.

  2. Öffnen Sie die Quelldatei IMarqueeWidget im Code-Editor, und ändern Sie die Deklaration von class in interface:

    // This interface defines the contract for any class that is to
    // be used in constructing a MarqueeControl.
    public interface IMarqueeWidget
    {
    
    ' This interface defines the contract for any class that is to
    ' be used in constructing a MarqueeControl.
    Public Interface IMarqueeWidget
    
  3. Fügen Sie den folgenden Code zur Schnittstelle IMarqueeWidget hinzu, um zwei Methoden und eine Eigenschaft bereitzustellen, die die Marquee-Animation beeinflussen:

    // 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. Fügen Sie dem Projekt MarqueeControlLibrary ein neues Element Benutzerdefiniertes Steuerelement hinzu. Geben Sie der neuen Quelldatei den Basisnamen „MarqueeText“.

  5. Ziehen Sie eine BackgroundWorker-Komponente aus der Toolbox auf Ihr MarqueeText-Steuerelement. Diese Komponente ermöglicht es dem MarqueeText-Steuerelement, sich selbst asynchron zu aktualisieren.

  6. Legen Sie im Fenster Eigenschaften die Eigenschaften WorkerReportsProgress und WorkerSupportsCancellation der Komponente BackgroundWorker auf true fest. Diese Einstellungen ermöglichen es der Komponente BackgroundWorker, das Ereignis ProgressChanged periodisch auszulösen und asynchrone Aktualisierungen abzubrechen.

    Weitere Informationen finden Sie unter BackgroundWorker-Komponente.

  7. Öffnen Sie die Quelldatei MarqueeText im Code-Editor. Importieren Sie am Anfang der Datei die folgenden Namespaces:

    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. Ändern Sie die Deklaration von MarqueeText, um von Label zu erben und die Schnittstelle IMarqueeWidget zu implementieren:

    [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. Deklarieren Sie die Instanzvariablen, die den verfügbar gemachten Eigenschaften entsprechen, und initialisieren Sie sie im Konstruktor. Das Feld isLit bestimmt, ob der Text in der durch die Eigenschaft LightColor angegebenen Farbe gezeichnet werden soll.

    // 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. Implementieren Sie die IMarqueeWidget-Schnittstelle.

    Die Methoden StartMarquee und StopMarquee rufen die Methoden RunWorkerAsync und CancelAsync der Komponente BackgroundWorker auf, um die Animation zu starten und zu stoppen.

    Die Attribute Category und Browsable werden auf die Eigenschaft UpdatePeriod angewendet, sodass sie in einem benutzerdefinierten Abschnitt des Eigenschaftsfensters namens „Marquee“ erscheint.

    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. Implementieren Sie die Eigenschaftenaccessoren. Sie machen zwei Eigenschaften für Clients verfügbar: LightColor und DarkColor. Die Attribute Category und Browsable werden auf diese Eigenschaften angewendet, sodass sie in einem benutzerdefinierten Abschnitt des Eigenschaftsfensters namens „Marquee“ erscheinen.

    [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. Implementieren Sie die Handler für die Ereignisse DoWork und ProgressChanged der Komponente BackgroundWorker.

    Der Ereignishandler DoWork wartet die durch UpdatePeriod festgelegte Anzahl von Millisekunden und löst dann das Ereignis ProgressChanged aus, bis Ihr Code die Animation durch den Aufruf von CancelAsync beendet.

    Der Ereignishandler ProgressChanged schaltet zwischen dem hellen und dem dunklen Textzustand um, sodass der Eindruck entsteht, dass der Text blinkt.

    // 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. Überschreiben Sie die Methode OnPaint, um die Animation zu aktivieren.

    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. Drücken Sie F6, um die Projektmappe zu erstellen.

Erstellen des untergeordneten MarqueeBorder-Steuerelements

Das Steuerelement MarqueeBorder ist etwas ausgefeilter als das Steuerelement MarqueeText. Es umfasst mehr Eigenschaften und die Animation in der OnPaint-Methode ist aufwändiger. Im Prinzip ist es dem Steuerelement MarqueeText recht ähnlich.

Da das Steuerelement MarqueeBorder untergeordnete Steuerelemente enthalten kann, muss es über Layout-Ereignisse informiert sein.

So erstellen Sie das MarqueeBorder-Steuerelement

  1. Fügen Sie dem Projekt MarqueeControlLibrary ein neues Element Benutzerdefiniertes Steuerelement hinzu. Geben Sie der neuen Quelldatei den Basisnamen „MarqueeBorder“.

  2. Ziehen Sie eine BackgroundWorker-Komponente aus der Toolbox auf Ihr MarqueeBorder-Steuerelement. Diese Komponente ermöglicht es dem MarqueeBorder-Steuerelement, sich selbst asynchron zu aktualisieren.

  3. Legen Sie im Fenster Eigenschaften die Eigenschaften WorkerReportsProgress und WorkerSupportsCancellation der Komponente BackgroundWorker auf true fest. Diese Einstellungen ermöglichen es der Komponente BackgroundWorker, das Ereignis ProgressChanged periodisch auszulösen und asynchrone Aktualisierungen abzubrechen. Weitere Informationen finden Sie unter BackgroundWorker-Komponente.

  4. Klicken Sie im Fenster Eigenschaften auf die Schaltfläche Ereignisse. Fügen Sie Handler für die Ereignisse DoWork und ProgressChanged hinzu.

  5. Öffnen Sie die Quelldatei MarqueeBorder im Code-Editor. Importieren Sie am Anfang der Datei die folgenden Namespaces:

    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. Ändern Sie die Deklaration von MarqueeBorder, um von Panel zu erben und die Schnittstelle IMarqueeWidget zu implementieren.

    [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. Deklarieren Sie zwei Enumerationen zur Verwaltung des Zustands des Steuerelements MarqueeBorder: MarqueeSpinDirection bestimmt die Richtung, in der sich die Lichter um den Rand „bewegen“, und MarqueeLightShape bestimmt die Form der Lichter (quadratisch oder kreisförmig). Platzieren Sie diese Deklarationen vor der MarqueeBorder-Klassendeklaration.

    // 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. Deklarieren Sie die Instanzvariablen, die den verfügbar gemachten Eigenschaften entsprechen, und initialisieren Sie sie im Konstruktor.

    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. Implementieren Sie die IMarqueeWidget-Schnittstelle.

    Die Methoden StartMarquee und StopMarquee rufen die Methoden RunWorkerAsync und CancelAsync der Komponente BackgroundWorker auf, um die Animation zu starten und zu stoppen.

    Da das Steuerelement MarqueeBorder untergeordnete Steuerelemente enthalten kann, zählt die Methode StartMarquee alle untergeordneten Steuerelemente auf und ruft StartMarquee für diejenigen auf, die IMarqueeWidget implementieren. Die StopMarquee-Methode verfügt über eine ähnliche Implementierung.

    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. Implementieren Sie die Eigenschaftenaccessoren. Das Steuerelement MarqueeBorder verfügt über mehrere Eigenschaften zur Steuerung seines Aussehens.

    [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. Implementieren Sie die Handler für die Ereignisse DoWork und ProgressChanged der Komponente BackgroundWorker.

    Der Ereignishandler DoWork wartet die durch UpdatePeriod festgelegte Anzahl von Millisekunden und löst dann das Ereignis ProgressChanged aus, bis Ihr Code die Animation durch den Aufruf von CancelAsync beendet.

    Der Ereignishandler ProgressChanged erhöht die Position des „Basislichts“, von dem aus der Hell-/ Dunkelzustand der anderen Lichter bestimmt wird, und ruft die Methode Refresh auf, damit das Steuerelement neu gezeichnet wird.

    // 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. Implementieren Sie die Hilfsmethoden IsLit und DrawLight.

    Die Methode IsLit bestimmt die Farbe eines Lichts an einer bestimmten Position. „Eingeschaltete“ Lichter werden in der Farbe gezeichnet, die durch die Eigenschaft LightColor angegeben ist, und „dunkle“ Lichter werden in der Farbe gezeichnet, die durch die Eigenschaft DarkColor angegeben ist.

    Die Methode DrawLight zeichnet ein Licht mit der entsprechenden Farbe, Form und Position.

    // 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. Überschreiben Sie die OnLayout- und OnPaint-Methoden.

    Die Methode OnPaint zeichnet die Lichter entlang der Ränder des Steuerelements MarqueeBorder.

    Da die Methode OnPaint von den Abmessungen des Steuerelements MarqueeBorder abhängt, müssen Sie sie bei jeder Änderung des Layouts aufrufen. Um dies zu erreichen, überschreiben OnLayout sie und rufen Refresh auf.

    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
    

Erstellen eines benutzerdefinierten Designers für Schatten- und Filtereigenschaften

Die Klasse MarqueeControlRootDesigner stellt die Implementierung für den RootDesigner bereit. Zusätzlich zu diesem Designer, der mit dem MarqueeControl arbeitet, benötigen Sie einen benutzerdefinierten Designer, der ausdrücklich dem Steuerelement MarqueeBorder zugeordnet ist. Dieser Designer bietet ein benutzerdefiniertes Verhalten, das im Kontext des benutzerdefinierten Stamm-Designers angemessen ist.

Insbesondere sorgt das Steuerelement MarqueeBorderDesigner für ein „Shadowing“ und die Filterung bestimmter Eigenschaften des Steuerelements MarqueeBorder, wodurch dessen Interaktion mit der Entwurfsumgebung verändert wird.

Das Abfangen von Aufrufen des Eigenschaftsaccessors einer Komponente wird als „Shadowing“ bezeichnet. Dadurch kann ein Designer den vom Benutzer festgelegten Wert nachverfolgen und diesen Wert optional an die entworfene Komponente weitergeben.

In diesem Beispiel erfolgt ein Shadowing der Eigenschaften Visible und Enabled durch die Eigenschaft MarqueeBorderDesigner, was den Benutzer daran hindert, das Steuerelement MarqueeBorder während der Entwurfszeit als unsichtbar oder deaktiviert festzulegen.

Designer können außerdem Eigenschaften hinzufügen und entfernen. In diesem Beispiel wird die Eigenschaft Padding zur Entwurfszeit entfernt, da das Steuerelement MarqueeBorder die Auffüllung basierend auf der Größe der durch die Eigenschaft LightSize angegebenen Lichter programmgesteuert festlegt.

Die Basisklasse für MarqueeBorderDesigner ist ComponentDesigner, die über Methoden verfügt, mit denen die Attribute, Eigenschaften und Ereignisse eines Steuerelements zur Entwurfszeit geändert werden können:

Wenn Sie die öffentliche Schnittstelle einer Komponente mithilfe dieser Methoden ändern, befolgen Sie diese Regeln:

  • Stellen Sie sicher, dass Elemente nur in den PreFilter-Methoden hinzugefügt oder entfernt werden.

  • Ändern vorhandener Elemente nur in den PostFilter-Methoden.

  • Rufen Sie die Basisimplementierung immer zuerst in den PreFilter-Methoden auf.

  • Rufen Sie die Basisimplementierung immer zuletzt in den PostFilter-Methoden auf.

Durch die Einhaltung dieser Regeln wird sichergestellt, dass alle Designer in der Entwurfszeitumgebung eine einheitliche Sicht auf alle zu entwerfenden Komponenten haben.

Die Klasse ComponentDesigner bietet ein Wörterbuch zum Verwalten der Werte von Eigenschaften mit Shadowing, sodass Sie keine spezifischen Instanzvariablen erstellen müssen.

So erstellen Sie einen benutzerdefinierten Designer für das Shadowing und Filterung von Eigenschaften

  1. Klicken Sie mit der rechten Maustaste auf den Ordner Design, und fügen Sie eine neue Klasse hinzu. Geben Sie der Quelldatei den Basisnamen MarqueeBorderDesigner.

  2. Öffnen Sie die Quelldatei „MarqueeBorderDesigner“ im Code-Editor. Importieren Sie am Anfang der Datei die folgenden Namespaces:

    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. Ändern Sie die Deklaration von MarqueeBorderDesigner, um von ParentControlDesigner zu erben.

    Da das Steuerelement MarqueeBorder untergeordnete Steuerelemente enthalten kann, erbt MarqueeBorderDesigner von ParentControlDesigner, das die Interaktion zwischen übergeordneten und untergeordneten Steuerelementen verwaltet.

    namespace MarqueeControlLibrary.Design
    {
        public class MarqueeBorderDesigner : ParentControlDesigner
        {
    
    Namespace MarqueeControlLibrary.Design
    
        Public Class MarqueeBorderDesigner
            Inherits ParentControlDesigner
    
  4. Überschreiben Sie die Basisimplementierung von 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. Implementieren Sie die Enabled-Eigenschaft und die Visible-Eigenschaft. Diese Implementierungen sorgen für ein Shadowing der Eigenschaften des Steuerelements.

    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
    

Verarbeiten von Komponentenänderungen

Die Klasse MarqueeControlRootDesigner stellt die benutzerdefinierte Entwurfszeitfunktionalität für Ihre MarqueeControl-Instanzen bereit. Der größte Teil der Entwurfszeitfunktionalität wird von der Klasse DocumentDesigner geerbt. Ihr Code implementiert zwei spezifische Anpassungen: die Verarbeitung von Komponentenänderungen und das Hinzufügen von Designerverben.

Wenn Benutzer ihre MarqueeControl-Instanzen entwerfen, verfolgt Ihr Stamm-Designer Änderungen an MarqueeControl und dessen untergeordneten Steuerelementen. Die Entwurfszeitumgebung stellt den nützlichen Dienst IComponentChangeService bereit, um Änderungen am Komponentenzustand nachzuverfolgen.

Sie rufen einen Verweis auf diesen Dienst ab, indem Sie die Umgebung mit der Methode GetService abfragen. Wenn die Abfrage erfolgreich ist, kann Ihr Designer einen Handler für das ComponentChanged-Ereignis anfügen und alle erforderlichen Aufgaben ausführen, um einen konsistenten Zustand zur Entwurfszeit beizubehalten.

Im Fall der MarqueeControlRootDesigner-Klasse rufen Sie die Refresh-Methode für jedes IMarqueeWidget-Objekt auf, das in der Klasse MarqueeControl enthalten ist. Dies führt dazu, dass das Objekt IMarqueeWidget bei Eigenschaftenänderungen ordnungsgemäß neu gezeichnet wird, beispielsweise bei Änderung der Eigenschaften des übergeordneten Objekts Size.

So verarbeiten Sie Komponentenänderungen

  1. Öffnen Sie die Quelldatei MarqueeControlRootDesigner im Code-Editor, und überschreiben Sie die Methode Initialize. Rufen Sie die Basisimplementierung von Initialize auf, und fragen Sie IComponentChangeService ab.

    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. Implementieren Sie den Ereignishandler OnComponentChanged. Testen Sie den Typ der sendenden Komponente. Wenn es sich um IMarqueeWidget handelt, rufen Sie die zugehörige Methode Refresh auf.

    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
    

Hinzufügen von Designerverben zu Ihrem benutzerdefinierten Designer

Ein Designerverb ist ein mit einem Ereignishandler verknüpfter Menübefehl. Designerverben werden zur Entwurfszeit dem Kontextmenü einer Komponente hinzugefügt. Weitere Informationen finden Sie unter DesignerVerb.

Sie werden Ihren Designern zwei Verben hinzufügen: Test ausführen und Test beenden. Mit diesen Verben können Sie das Laufzeitverhalten von MarqueeControl zur Entwurfszeit anzeigen. Diese Verben werden MarqueeControlRootDesigner hinzugefügt.

Beim Aufruf von Test ausführen ruft der Ereignishandler des Verbs die Methode StartMarquee für MarqueeControl auf. Beim Aufruf von Test beenden ruft der Ereignishandler des Verbs die Methode StopMarquee für MarqueeControl auf. Die Implementierung der Methoden StartMarquee und StopMarquee ruft diese Methoden für enthaltene Steuerelemente auf, die IMarqueeWidget implementieren, sodass alle enthaltenen Steuerelemente IMarqueeWidget ebenfalls in den Test einbezogen werden.

So fügen Sie Ihren benutzerdefinierten Designern Designerverben hinzu

  1. Fügen Sie in der Klasse MarqueeControlRootDesigner Ereignishandler mit den Namen OnVerbRunTest und OnVerbStopTest hinzu.

    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. Verbinden Sie diese Ereignishandler mit den entsprechenden Designerverben. MarqueeControlRootDesigner erbt eine DesignerVerbCollection von seiner Basisklasse. Sie erstellen zwei neue DesignerVerb-Objekte und fügen sie dieser Auflistung in der Methode Initialize hinzu.

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

Erstellen eines benutzerdefinierten UITypeEditors

Beim Erstellen einer benutzerdefinierten Entwurfszeitfunktionalität für Benutzer ist es oft wünschenswert, eine benutzerdefinierte Interaktion mit dem Eigenschaftenfenster zu ermöglichen. Sie können dies erreichen, indem Sie einen UITypeEditor erstellen.

Das MarqueeBorder-Steuerelement macht mehrere Eigenschaften im Eigenschaftenfenster verfügbar. Zwei dieser Eigenschaften, MarqueeSpinDirection und MarqueeLightShape, werden durch Enumerationen dargestellt. Um die Verwendung eines UITypEditors zu veranschaulichen, wird der Eigenschaft MarqueeLightShape eine Klasse UITypeEditor zugeordnet.

So erstellen Sie einen benutzerdefinierten UITypEditor

  1. Öffnen Sie die Quelldatei MarqueeBorder im Code-Editor.

  2. Deklarieren Sie in der Definition der Klasse MarqueeBorder eine Klasse namens LightShapeEditor, die von UITypeEditor abgeleitet ist.

    // 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. Deklarieren Sie eine IWindowsFormsEditorService-Instanzvariable mit dem Namen editorService.

    private IWindowsFormsEditorService editorService = null;
    
    Private editorService As IWindowsFormsEditorService = Nothing
    
  4. Überschreiben Sie die GetEditStyle -Methode. Diese Implementierung gibt DropDown zurück, wodurch der Entwurfsumgebung mitgeteilt wird, wie der LightShapeEditor angezeigt werden soll.

    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. Überschreiben Sie die EditValue -Methode. Diese Implementierung fragt die Entwurfsumgebung nach einem IWindowsFormsEditorService-Objekt ab. Bei einer erfolgreichen Abfrage wird ein LightShapeSelectionControl erstellt. Die Methode DropDownControl wird aufgerufen, um den LightShapeEditor zu starten. Der Rückgabewert dieses Aufrufs wird an die Entwurfsumgebung zurückgegeben.

    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
    

Erstellen eines Ansichtssteuerelements für Ihren benutzerdefinierten UITypeEditor

Die Eigenschaft MarqueeLightShape unterstützt zwei Arten von Lichtformen: Square und Circle. Sie erstellen ein benutzerdefiniertes Steuerelement, das ausschließlich dazu dient, diese Werte im Eigenschaftenfenster grafisch darzustellen. Dieses benutzerdefinierte Steuerelement wird von Ihrem UITypeEditor zur Interaktion mit dem Eigenschaftenfenster verwendet.

So erstellen Sie ein Ansichtssteuerelement für Ihren benutzerdefinierten UITypeEditor

  1. Fügen Sie dem MarqueeControlLibrary-Projekt ein neues UserControl-Element hinzu. Geben Sie der neuen Quelldatei den Basisnamen LightShapeSelectionControl.

  2. Ziehen Sie zwei Panel-Steuerelemente aus der Toolbox auf das LightShapeSelectionControl. Nennen Sie sie squarePanel und circlePanel. Ordnen Sie sie nebeneinander an. Legen Sie die Eigenschaft Size der beiden Panel-Steuerelemente auf (60, 60) fest. Legen Sie die Eigenschaft Location des squarePanel-Steuerelemente auf (8, 10) fest. Legen Sie die Eigenschaft Location des circlePanel-Steuerelemente auf (80, 10) fest. Und legen Sie schließlich die Eigenschaft Size von LightShapeSelectionControl auf (150, 80) fest.

  3. Öffnen Sie die Quelldatei LightShapeSelectionControl im Code-Editor. Importieren Sie am Anfang der Datei den System.Windows.Forms.Design-Namespace:

    Imports System.Windows.Forms.Design
    
    using System.Windows.Forms.Design;
    
  4. Implementieren Sie Click-Ereignishandler für die Steuerelemente squarePanel und circlePanel. Diese Methoden rufen CloseDropDown auf, um die Bearbeitungssitzung für den benutzerdefinierten UITypeEditor zu beenden.

    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. Deklarieren Sie eine IWindowsFormsEditorService-Instanzvariable mit dem Namen editorService.

    Private editorService As IWindowsFormsEditorService
    
    private IWindowsFormsEditorService editorService;
    
  6. Deklarieren Sie eine MarqueeLightShape-Instanzvariable mit dem Namen lightShapeValue.

    private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
    
    Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
    
  7. Fügen Sie im Konstruktor LightShapeSelectionControl die Click-Ereignishandler an die Ereignisse Click der Steuerelemente squarePanel und circlePanel an. Definieren Sie außerdem eine Konstruktorüberladung, die den MarqueeLightShape-Wert aus der Entwurfsumgebung dem Feld lightShapeValue zuweist.

    // 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. Trennen Sie in der Dispose-Methode die Click-Ereignishandler.

    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. Klicken Sie im Projektmappen-Explorer auf die Schaltfläche Alle Dateien anzeigen. Öffnen Sie die Datei „LightShapeSelectionControl.Designer.cs“ oder „LightShapeSelectionControl.Designer.vb“, und entfernen Sie die Standarddefinition der Methode Dispose.

  10. Implementiert die LightShape-Eigenschaft.

    // 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. Überschreiben Sie die OnPaint -Methode. Durch diese Implementierung werden ein ausgefülltes Quadrat und ein Kreis gezeichnet. Außerdem wird der ausgewählte Wert hervorgehoben, indem ein Rahmen um eine der beiden Formen gezeichnet wird.

    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
    

Testen des benutzerdefinierten Steuerelements im Designer

An diesem Punkt können Sie das MarqueeControlLibrary-Projekt kompilieren. Testen Sie Ihre Implementierung, indem Sie ein Steuerelement erstellen, das von der Klasse MarqueeControl erbt, und dieses in einem Formular verwenden.

So erstellen Sie eine benutzerdefinierte MarqueeControl-Implementierung

  1. Öffnen Sie DemoMarqueeControl im Windows Forms-Designer. Dies erzeugt eine Instanz des Typs DemoMarqueeControl und zeigt sie in einer Instanz des Typs MarqueeControlRootDesigner an.

  2. Öffnen Sie in der Toolbox die Registerkarte MarqueeControlLibrary-Komponenten. Sie sehen, dass hier die Steuerelemente MarqueeBorder und MarqueeText zur Auswahl stehen.

  3. Ziehen Sie eine Instanz des Steuerelements MarqueeBorder auf die DemoMarqueeControl-Entwurfsoberfläche. Docken Sie dieses MarqueeBorder-Steuerelement an das übergeordnete Steuerelement an.

  4. Ziehen Sie eine Instanz des Steuerelements MarqueeText auf die DemoMarqueeControl-Entwurfsoberfläche.

  5. Erstellen Sie die Projektmappe.

  6. Klicken Sie mit der rechten Maustaste auf DemoMarqueeControl, und wählen Sie im Kontextmenü die Option Test ausführen aus, um die Animation zu starten. Klicken Sie auf Test beenden, um die Animation zu beenden.

  7. Öffnen Sie Form1 in der Designansicht.

  8. Platzieren Sie zwei Button-Steuerelemente im Formular. Geben Sie ihnen die Namen startButton und stopButton, und ändern Sie die den Wert der Text-Eigenschaft in Starten bzw. Beenden.

  9. Implementieren Sie Click-Ereignishandler für beide Button-Steuerelemente.

  10. Öffnen Sie in der Toolbox die Registerkarte MarqueeControlTest-Komponenten. Sie sehen, dass DemoMarqueeControl zur Auswahl steht.

  11. Ziehen Sie eine Instanz von DemoMarqueeControl auf die Form1-Entwurfsoberfläche.

  12. Rufen Sie in den Click-Ereignishandlern die Methoden Start und Stop für das DemoMarqueeControl auf.

    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. Legen Sie das Projekt MarqueeControlTest als Startprojekt fest, und führen Sie es aus. Sie sehen das Formular, in dem Ihr DemoMarqueeControl angezeigt wird. Klicken Sie auf die Schaltfläche Starten, um die Animation zu starten. Sie sollten sehen, dass der Text blinkt und die Lichter sich um den Rand bewegen.

Nächste Schritte

Das MarqueeControlLibrary-Beispiel veranschaulicht eine einfach Implementierung von benutzerdefinierten Steuerelementen und zugehörigen Designern. Sie können dieses Beispiel auf verschiedene Weise verfeinern:

  • Ändern Sie die Eigenschaftswerte für das DemoMarqueeControl im Designer. Fügen Sie weitere MarqueBorder-Steuerelemente hinzu, und docken Sie sie innerhalb ihrer übergeordneten Instanzen an, um einen Schachtelungseffekt zu erzielen. Experimentieren Sie mit verschiedenen Einstellungen für UpdatePeriod und die lichtbezogenen Eigenschaften.

  • Erstellen Sie eigenen Implementierungen von IMarqueeWidget. Sie könnten zum Beispiel ein blinkendes „Neonschild“ oder ein animiertes Schild mit verschiedenen Bildern erstellen.

  • Passen Sie die Entwurfszeitfunktionalität weiter an. Sie könnten versuchen, ein Shadowing auf mehr als nur die Eigenschaften Enabled and Visible anzuwenden, und Sie könnten neue Eigenschaften hinzufügen. Fügen Sie neue Designerverben hinzu, um häufige Aufgaben wie das Andocken von untergeordneten Steuerelementen zu vereinfachen.

  • Lizenzieren Sie das MarqueeControl.

  • Steuern Sie, wie Ihre Steuerelemente serialisiert werden und wie der Code für sie generiert wird. Weitere Informationen finden Sie unter Kompilieren und Generieren von dynamischem Quellcode.

Siehe auch