チュートリアル: デザイン時機能を活用したコントロールを作成する

カスタム コントロールのデザイン時エクスペリエンスは、関連するカスタム デザイナーを作成することによって拡張できます。

注意事項

このコンテンツは .NET Framework 用に作成しました。 .NET 6 以降のバージョンを使用している場合は、このコンテンツの使用にご注意ください。 Windows フォーム用のデザイナー システムが変更されたため、「.NET Framework 以降のデザイナーの変更」の記事をご確認ください (重要です)。

この記事では、カスタム コントロール用のカスタム デザイナーを作成する方法を示します。 MarqueeControl 型と、MarqueeControlRootDesigner という名前の関連付けられたデザイナー クラスを実装します。

MarqueeControl 型を使用して、アニメーション化されたライトと点滅するテキストを含む映画館のマーキーに似た表示を実装します。

このコントロールのデザイナーはデザイン環境と対話して、デザイン時のカスタム エクスペリエンスを提供します。 カスタム デザイナーを使用すると、アニメーション化されたライトと点滅するテキストのさまざまな組み合わせで、MarqueeControl のカスタム実装を組み立てることができます。 組み立てられたコントロールは、他の Windows フォーム コントロールと同様に、フォーム上で使用できます。

このチュートリアルを終了すると、カスタム コントロールは次のような表示になります。

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

完全なコード リストについては、「方法: デザイン時機能を活用した Windows フォーム コントロールを作成する」を参照してください。

前提条件

このチュートリアルを完了するには、Visual Studio が必要です。

プロジェクトの作成

最初にアプリケーションのプロジェクトを作成します。 このプロジェクトを使用して、カスタム コントロールをホストするアプリケーションをビルドします。

Visual Studio で新しい Windows フォーム アプリケーション プロジェクトを作成し、MarqueeControlTest という名前を付けます。

コントロール ライブラリ プロジェクトを作成する

  1. Windows フォーム コントロール ライブラリ プロジェクトをソリューションに追加します。 プロジェクトの名前は MarqueeControlLibrary にします。

  2. ソリューション エクスプローラーを使用して、選択した言語に対応する "UserControl1.cs" または "UserControl1.vb" という名前のソース ファイルを削除することにより、プロジェクトの既定のコントロールを削除します。

  3. 新しい UserControl 項目を MarqueeControlLibrary プロジェクトに追加します。 新しいソース ファイルのベース名を MarqueeControl にします。

  4. ソリューション エクスプローラーを使用して、MarqueeControlLibrary プロジェクトに新しいフォルダーを作成します。

  5. [デザイン] フォルダーを右クリックして、新しいクラスを追加します。 MarqueeControlRootDesigner という名前を指定します。

  6. System.Design アセンブリの型を使用する必要があるため、この参照を MarqueeControlLibrary プロジェクトに追加します。

カスタム コントロール プロジェクトを参照する

カスタム コントロールをテストするには、MarqueeControlTest プロジェクトを使用します。 MarqueeControlLibrary アセンブリにプロジェクト参照を追加すると、テスト プロジェクトによってカスタム コントロールが認識されるようになります。

MarqueeControlTest プロジェクトで、プロジェクト参照を MarqueeControlLibrary アセンブリに追加します。 MarqueeControlLibrary アセンブリを直接参照するのではなく、 [参照の追加] ダイアログ ボックスの [プロジェクト] タブを使用するようにしてください。

カスタム コントロールとそのカスタム デザイナーを定義する

このカスタム コントロールは UserControl クラスから派生します。 これにより、コントロールに他のコントロールを含めることができ、コントロールに多数の既定の機能が提供されます。

このカスタム コントロールに、カスタム デザイナーを関連付けます。 これにより、このカスタム コントロール専用にカスタマイズされた独自のデザイン エクスペリエンスを作成できます。

コントロールをデザイナーに関連付けるには、DesignerAttribute クラスを使用します。 カスタム コントロールのデザイン時動作全体を開発しているため、カスタム デザイナーで IRootDesigner インターフェイスを実装します。

カスタム コントロールとそのカスタム デザイナーを定義するには

  1. コード エディターMarqueeControl ソース ファイルを開きます。 ファイルの先頭で、次の名前空間をインポートします。

    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Drawing
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
  2. DesignerAttributeMarqueeControl のクラス宣言に追加します。 これにより、カスタム コントロールがデザイナーに関連付けられます。

    [Designer( typeof( MarqueeControlLibrary.Design.MarqueeControlRootDesigner ), typeof( IRootDesigner ) )]
    public class MarqueeControl : UserControl
    {
    
    <Designer(GetType(MarqueeControlLibrary.Design.MarqueeControlRootDesigner), _
     GetType(IRootDesigner))> _
    Public Class MarqueeControl
        Inherits UserControl
    
  3. コード エディターMarqueeControlRootDesigner ソース ファイルを開きます。 ファイルの先頭で、次の名前空間をインポートします。

    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Drawing.Design;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Drawing.Design
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
  4. DocumentDesigner クラスを継承するように MarqueeControlRootDesigner の宣言を変更します。 ToolboxItemFilterAttribute を適用して、デザイナーとツールボックスの対話を指定します。

    注意

    MarqueeControlRootDesigner クラスの定義は、MarqueeControlLibrary.Design という名前空間で囲まれています。 この宣言により、デザイン関連の型用に予約された特別な名前空間にデザイナーが配置されます。

    namespace MarqueeControlLibrary.Design
    {
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
        public class MarqueeControlRootDesigner : DocumentDesigner
        {
    
    Namespace MarqueeControlLibrary.Design
    
        <ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _
        ToolboxItemFilterType.Require), _
        ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _
        ToolboxItemFilterType.Require)> _
        Public Class MarqueeControlRootDesigner
            Inherits DocumentDesigner
    
  5. MarqueeControlRootDesigner クラスのコンストラクターを定義します。 コンストラクターの本体に WriteLine ステートメントを挿入します。 これはデバッグに役立ちます。

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

カスタム コントロールのインスタンスを作成する

  1. 新しい UserControl 項目を MarqueeControlTest プロジェクトに追加します。 新しいソース ファイルのベース名を DemoMarqueeControl にします。

  2. DemoMarqueeControl ファイルをコード エディターで開きます。 ファイルの先頭で MarqueeControlLibrary 名前空間をインポートします。

    Imports MarqueeControlLibrary
    
    using MarqueeControlLibrary;
    
  3. MarqueeControl クラスを継承するように DemoMarqueeControl の宣言を変更します。

  4. プロジェクトをビルドします。

  5. Windows フォーム デザイナーで Form1 を開きます。

  6. ツールボックス[MarqueeControlTest コンポーネント] タブを見つけて開きます。 ツールボックスからフォームに DemoMarqueeControl をドラッグします。

  7. プロジェクトをビルドします。

デザイン時デバッグ用にプロジェクトを設定する

カスタム デザイン時エクスペリエンスを開発している場合は、コントロールとコンポーネントをデバッグする必要があります。 デザイン時にデバッグできるようにプロジェクトを設定する簡単な方法があります。 詳細については、「チュートリアル: カスタム Windows フォーム コントロールのデザイン時のデバッグ」を参照してください。

  1. MarqueeControlLibrary プロジェクトを右クリックして、 [プロパティ] を選択します。

  2. [MarqueeControlLibrary プロパティページ] ダイアログ ボックスで、 [デバッグ] ページを選択します。

  3. [開始動作] セクションで [外部プログラムを起動する] を選択します。 Visual Studio の別のインスタンスをデバッグするので、省略記号 (The Ellipsis button (...) in the Properties window of Visual Studio) ボタンをクリックして、Visual Studio IDE を参照します。 実行可能ファイルの名前は devenv.exe であり、既定の場所にインストールした場合、そのパスは %ProgramFiles(x86)%\Microsoft Visual Studio\2019\<edition>\Common7\IDE\devenv.exe になります。

  4. [OK] を選択してダイアログ ボックスを閉じます。

  5. MarqueeControlLibrary プロジェクトを右クリックし、 [スタートアップ プロジェクトに設定] を選択して、このデバッグ構成を有効にします。

Checkpoint

これで、カスタム コントロールのデザイン時動作をデバッグする準備ができました。 デバッグ環境が正しく設定されていることを確認した後、カスタム コントロールとカスタム デザイナーの関連付けをテストします。

デバッグ環境とデザイナーの関連付けをテストするには

  1. コード エディターで MarqueeControlRootDesigner ソース ファイルを開き、WriteLine ステートメントにブレークポイントを設定します。

  2. F5 キーを押してデバッグ セッションを開始します。

    Visual Studio の新しいインスタンスが作成されます。

  3. Visual Studio の新しいインスタンスで、MarqueeControlTest ソリューションを開きます。 [ファイル] メニューの [最近使ったプロジェクト] を選択することで、ソリューションを簡単に見つけることができます。 MarqueeControlTest.sln ソリューション ファイルが、最近使用したファイルとして一覧に表示されます。

  4. デザイナーで DemoMarqueeControl を開きます。

    Visual Studio のデバッグ インスタンスにフォーカスが設定され、ブレークポイントで実行が停止します。 F5 キーを押してデバッグ セッションを続けます。

このようになれば、カスタム コントロールとそれに関連付けられているカスタム デザイナーを開発してデバッグするためのすべての準備が整っています。 この記事の残りの部分では、コントロールとデザイナーの機能の実装について詳しく説明します。

カスタム コントロールを実装する

MarqueeControl は、少しだけカスタマイズされている UserControl です。 それからは、マーキー アニメーションを開始する Start と、アニメーションを停止する Stop の、2 つのメソッドが公開されています。 MarqueeControl には IMarqueeWidget インターフェイスを実装する子コントロールが含まれているため、StartStop により、各子コントロールが列挙され、IMarqueeWidget が実装されている子コントロールごとに、それぞれ StartMarquee メソッドと StopMarquee メソッドが呼び出されます。

MarqueeBorder および MarqueeText コントロールの外観はレイアウトに依存しているため、MarqueeControl によって OnLayout メソッドがオーバーライドされ、この型の子コントロールでは PerformLayout が呼び出されます。

これは MarqueeControl のカスタマイズの範囲です。 実行時の機能は MarqueeBorder および MarqueeText コントロールによって実装され、デザイン時の機能は MarqueeBorderDesigner および MarqueeControlRootDesigner クラスによって実装されています。

カスタム コントロールを実装するには

  1. コード エディターMarqueeControl ソース ファイルを開きます。 Start および Stop メソッドを実装します。

    public void Start()
    {
        // The MarqueeControl may contain any number of
        // controls that implement IMarqueeWidget, so
        // find each IMarqueeWidget child and call its
        // StartMarquee method.
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StartMarquee();
            }
        }
    }
    
    public void Stop()
    {
        // The MarqueeControl may contain any number of
        // controls that implement IMarqueeWidget, so find
        // each IMarqueeWidget child and call its StopMarquee
        // method.
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StopMarquee();
            }
        }
    }
    
    Public Sub Start()
        ' The MarqueeControl may contain any number of 
        ' controls that implement IMarqueeWidget, so 
        ' find each IMarqueeWidget child and call its
        ' StartMarquee method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StartMarquee()
            End If
        Next cntrl
    End Sub
    
    
    Public Sub [Stop]()
        ' The MarqueeControl may contain any number of 
        ' controls that implement IMarqueeWidget, so find
        ' each IMarqueeWidget child and call its StopMarquee
        ' method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StopMarquee()
            End If
        Next cntrl
    End Sub
    
  2. OnLayout メソッドをオーバーライドします。

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

カスタム コントロールの子コントロールを作成する

MarqueeControl により、MarqueeBorder コントロールと MarqueeText コントロールという 2 種類の子コントロールがホストされます。

  • MarqueeBorder: このコントロールにより、端の周りに "ライト" の境界線が描画されます。 ライトは規則的に点滅するため、境界線が移動しているように見えます。 ライトが点滅する速さは、UpdatePeriod というプロパティによって制御されます。 他のいくつかのカスタム プロパティにより、コントロールの外観に関する他の側面が決まります。 StartMarqueeStopMarquee という名前の 2 つのメソッドにより、アニメーションの開始と停止のタイミングが制御されます。

  • MarqueeText: このコントロールにより、点滅する文字列が描画されます。 MarqueeBorder コントロールと同様に、テキストが点滅する速さは、UpdatePeriod プロパティによって制御されます。 MarqueeText コントロールにも、MarqueeBorder コントロールと同じように、StartMarquee および StopMarquee メソッドがあります。

デザイン時には、MarqueeControlRootDesigner を使用することで、これら 2 つのコントロール型を任意の組み合わせで MarqueeControl に追加できます。

2 つのコントロールの共通機能は、IMarqueeWidget という名前のインターフェイスにまとめられています。 これにより、MarqueeControl でマーキー関連の子コントロールを検出し、特別な処理を行うことができます。

周期的なアニメーション機能を実装するには、System.ComponentModel 名前空間の BackgroundWorker オブジェクトを使用します。 Timer オブジェクトを使用することもできますが、多くの IMarqueeWidget オブジェクトが存在すると、1 つの UI スレッドではアニメーションを維持できないことがあります。

カスタム コントロールの子コントロールを作成するには

  1. 新しいクラス項目を MarqueeControlLibrary プロジェクトに追加します。 新しいソース ファイルのベース名を "IMarqueeWidget" にします。

  2. IMarqueeWidget ソース ファイルをコード エディターで開き、宣言を class から interface に変更します。

    // This interface defines the contract for any class that is to
    // be used in constructing a MarqueeControl.
    public interface IMarqueeWidget
    {
    
    ' This interface defines the contract for any class that is to
    ' be used in constructing a MarqueeControl.
    Public Interface IMarqueeWidget
    
  3. 次のコードを IMarqueeWidget インターフェイスに追加して、マーキー アニメーションを操作する 2 つのメソッドと 1 つのプロパティを公開します。

    // This interface defines the contract for any class that is to
    // be used in constructing a MarqueeControl.
    public interface IMarqueeWidget
    {
        // This method starts the animation. If the control can
        // contain other classes that implement IMarqueeWidget as
        // children, the control should call StartMarquee on all
        // its IMarqueeWidget child controls.
        void StartMarquee();
    
        // This method stops the animation. If the control can
        // contain other classes that implement IMarqueeWidget as
        // children, the control should call StopMarquee on all
        // its IMarqueeWidget child controls.
        void StopMarquee();
    
        // This method specifies the refresh rate for the animation,
        // in milliseconds.
        int UpdatePeriod
        {
            get;
            set;
        }
    }
    
    ' This interface defines the contract for any class that is to
    ' be used in constructing a MarqueeControl.
    Public Interface IMarqueeWidget
    
       ' This method starts the animation. If the control can 
       ' contain other classes that implement IMarqueeWidget as
       ' children, the control should call StartMarquee on all
       ' its IMarqueeWidget child controls.
       Sub StartMarquee()
       
       ' This method stops the animation. If the control can 
       ' contain other classes that implement IMarqueeWidget as
       ' children, the control should call StopMarquee on all
       ' its IMarqueeWidget child controls.
       Sub StopMarquee()
       
       ' This method specifies the refresh rate for the animation,
       ' in milliseconds.
       Property UpdatePeriod() As Integer
    
    End Interface
    
  4. 新しいカスタム コントロール項目を MarqueeControlLibrary プロジェクトに追加します。 新しいソース ファイルのベース名を "MarqueeText" にします。

  5. ツールボックスから MarqueeText コントロールに BackgroundWorker コンポーネントをドラッグします。 このコンポーネントにより、MarqueeText コントロールでそれ自体を非同期に更新できるようになります。

  6. [プロパティ] ウィンドウで、BackgroundWorker コンポーネントの WorkerReportsProgress および WorkerSupportsCancellation プロパティを true に設定します。 これらの設定により、BackgroundWorker コンポーネントで、定期的に ProgressChanged イベントを発生させ、非同期更新を取り消すことができます。

    詳細については、「BackgroundWorker コンポーネント」を参照してください。

  7. コード エディターMarqueeText ソース ファイルを開きます。 ファイルの先頭で、次の名前空間をインポートします。

    using System;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Drawing;
    using System.Threading;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Drawing
    Imports System.Threading
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
  8. Label を継承し、IMarqueeWidget インターフェイスを実装するように、MarqueeText の宣言を変更します。

    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
    public partial class MarqueeText : Label, IMarqueeWidget
    {
    
    <ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _
    ToolboxItemFilterType.Require)> _
    Partial Public Class MarqueeText
        Inherits Label
        Implements IMarqueeWidget
    
  9. 公開されたプロパティに対応するインスタンス変数を宣言し、コンストラクターでそれらを初期化します。 isLit フィールドにより、LightColor プロパティで指定されている色でテキストを描画するかどうかが決まります。

    // When isLit is true, the text is painted in the light color;
    // When isLit is false, the text is painted in the dark color.
    // This value changes whenever the BackgroundWorker component
    // raises the ProgressChanged event.
    private bool isLit = true;
    
    // These fields back the public properties.
    private int updatePeriodValue = 50;
    private Color lightColorValue;
    private Color darkColorValue;
    
    // These brushes are used to paint the light and dark
    // colors of the text.
    private Brush lightBrush;
    private Brush darkBrush;
    
    // This component updates the control asynchronously.
    private BackgroundWorker backgroundWorker1;
    
    public MarqueeText()
    {
        // This call is required by the Windows.Forms Form Designer.
        InitializeComponent();
    
        // Initialize light and dark colors
        // to the control's default values.
        this.lightColorValue = this.ForeColor;
        this.darkColorValue = this.BackColor;
        this.lightBrush = new SolidBrush(this.lightColorValue);
        this.darkBrush = new SolidBrush(this.darkColorValue);
    }
    
    ' When isLit is true, the text is painted in the light color;
    ' When isLit is false, the text is painted in the dark color.
    ' This value changes whenever the BackgroundWorker component
    ' raises the ProgressChanged event.
    Private isLit As Boolean = True
    
    ' These fields back the public properties.
    Private updatePeriodValue As Integer = 50
    Private lightColorValue As Color
    Private darkColorValue As Color
    
    ' These brushes are used to paint the light and dark
    ' colors of the text.
    Private lightBrush As Brush
    Private darkBrush As Brush
    
    ' This component updates the control asynchronously.
    Private WithEvents backgroundWorker1 As BackgroundWorker
    
    
    Public Sub New()
        ' This call is required by the Windows.Forms Form Designer.
        InitializeComponent()
    
        ' Initialize light and dark colors 
        ' to the control's default values.
        Me.lightColorValue = Me.ForeColor
        Me.darkColorValue = Me.BackColor
        Me.lightBrush = New SolidBrush(Me.lightColorValue)
        Me.darkBrush = New SolidBrush(Me.darkColorValue)
    End Sub
    
  10. IMarqueeWidget インターフェイスを実装します。

    StartMarquee および StopMarquee メソッドにより、BackgroundWorker コンポーネントの RunWorkerAsync および CancelAsync メソッドが呼び出されて、アニメーションが開始および停止されます。

    Category および Browsable 属性は UpdatePeriod プロパティに適用されるため、プロパティ ウィンドウの "Marquee" という名前のカスタム セクションに表示されます。

    public virtual void StartMarquee()
    {
        // Start the updating thread and pass it the UpdatePeriod.
        this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod);
    }
    
    public virtual void StopMarquee()
    {
        // Stop the updating thread.
        this.backgroundWorker1.CancelAsync();
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public int UpdatePeriod
    {
        get
        {
            return this.updatePeriodValue;
        }
    
        set
        {
            if (value > 0)
            {
                this.updatePeriodValue = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0");
            }
        }
    }
    
    Public Overridable Sub StartMarquee() _
    Implements IMarqueeWidget.StartMarquee
        ' Start the updating thread and pass it the UpdatePeriod.
        Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod)
    End Sub
    
    Public Overridable Sub StopMarquee() _
    Implements IMarqueeWidget.StopMarquee
        ' Stop the updating thread.
        Me.backgroundWorker1.CancelAsync()
    End Sub
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property UpdatePeriod() As Integer _
    Implements IMarqueeWidget.UpdatePeriod
    
        Get
            Return Me.updatePeriodValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 Then
                Me.updatePeriodValue = Value
            Else
                Throw New ArgumentOutOfRangeException("UpdatePeriod", "must be > 0")
            End If
        End Set
    
    End Property
    
  11. プロパティのアクセサーを実装します。 LightColorDarkColor の 2 つのプロパティをクライアントに公開します。 Category および Browsable 属性はこれらのプロパティに適用されるため、プロパティ ウィンドウの "Marquee" という名前のカスタム セクションにプロパティが表示されます。

    [Category("Marquee")]
    [Browsable(true)]
    public Color LightColor
    {
        get
        {
            return this.lightColorValue;
        }
        set
        {
            // The LightColor property is only changed if the
            // client provides a different value. Comparing values
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.lightColorValue.ToArgb() != value.ToArgb())
            {
                this.lightColorValue = value;
                this.lightBrush = new SolidBrush(value);
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color DarkColor
    {
        get
        {
            return this.darkColorValue;
        }
        set
        {
            // The DarkColor property is only changed if the
            // client provides a different value. Comparing values
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.darkColorValue.ToArgb() != value.ToArgb())
            {
                this.darkColorValue = value;
                this.darkBrush = new SolidBrush(value);
            }
        }
    }
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightColor() As Color
    
        Get
            Return Me.lightColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The LightColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then
                Me.lightColorValue = Value
                Me.lightBrush = New SolidBrush(Value)
            End If
        End Set
    
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property DarkColor() As Color
    
        Get
            Return Me.darkColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The DarkColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then
                Me.darkColorValue = Value
                Me.darkBrush = New SolidBrush(Value)
            End If
        End Set
    
    End Property
    
  12. BackgroundWorker コンポーネントの DoWork および ProgressChangedイベントのハンドラーを実装します。

    DoWork イベント ハンドラーは、コードで CancelAsync が呼び出されてアニメーションが停止されるまで、UpdatePeriod によって指定されているミリ秒数だけスリープ状態になった後、ProgressChanged イベントを発生させます。

    ProgressChanged イベント ハンドラーによりテキストの淡色と濃色の状態が切り替えられ、点滅のように見えます。

    // This method is called in the worker thread's context,
    // so it must not make any calls into the MarqueeText control.
    // Instead, it communicates to the control using the
    // ProgressChanged event.
    //
    // The only work done in this event handler is
    // to sleep for the number of milliseconds specified
    // by UpdatePeriod, then raise the ProgressChanged event.
    private void backgroundWorker1_DoWork(
        object sender,
        System.ComponentModel.DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;
    
        // This event handler will run until the client cancels
        // the background task by calling CancelAsync.
        while (!worker.CancellationPending)
        {
            // The Argument property of the DoWorkEventArgs
            // object holds the value of UpdatePeriod, which
            // was passed as the argument to the RunWorkerAsync
            // method.
            Thread.Sleep((int)e.Argument);
    
            // The DoWork eventhandler does not actually report
            // progress; the ReportProgress event is used to
            // periodically alert the control to update its state.
            worker.ReportProgress(0);
        }
    }
    
    // The ProgressChanged event is raised by the DoWork method.
    // This event handler does work that is internal to the
    // control. In this case, the text is toggled between its
    // light and dark state, and the control is told to
    // repaint itself.
    private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
    {
        this.isLit = !this.isLit;
        this.Refresh();
    }
    
    
    ' This method is called in the worker thread's context, 
    ' so it must not make any calls into the MarqueeText control.
    ' Instead, it communicates to the control using the 
    ' ProgressChanged event.
    '
    ' The only work done in this event handler is
    ' to sleep for the number of milliseconds specified 
    ' by UpdatePeriod, then raise the ProgressChanged event.
    Private Sub backgroundWorker1_DoWork( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.DoWorkEventArgs) _
    Handles backgroundWorker1.DoWork
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
    
        ' This event handler will run until the client cancels
        ' the background task by calling CancelAsync.
        While Not worker.CancellationPending
            ' The Argument property of the DoWorkEventArgs
            ' object holds the value of UpdatePeriod, which 
            ' was passed as the argument to the RunWorkerAsync
            ' method. 
            Thread.Sleep(Fix(e.Argument))
    
            ' The DoWork eventhandler does not actually report
            ' progress; the ReportProgress event is used to 
            ' periodically alert the control to update its state.
            worker.ReportProgress(0)
        End While
    End Sub
    
    
    ' The ProgressChanged event is raised by the DoWork method.
    ' This event handler does work that is internal to the
    ' control. In this case, the text is toggled between its
    ' light and dark state, and the control is told to 
    ' repaint itself.
    Private Sub backgroundWorker1_ProgressChanged( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
    Handles backgroundWorker1.ProgressChanged
        Me.isLit = Not Me.isLit
        Me.Refresh()
    End Sub
    
  13. アニメーションを有効にするように、OnPaint メソッドをオーバーライドします。

    protected override void OnPaint(PaintEventArgs e)
    {
        // The text is painted in the light or dark color,
        // depending on the current value of isLit.
        this.ForeColor =
            this.isLit ? this.lightColorValue : this.darkColorValue;
    
        base.OnPaint(e);
    }
    
    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
        ' The text is painted in the light or dark color,
        ' depending on the current value of isLit.
        Me.ForeColor = IIf(Me.isLit, Me.lightColorValue, Me.darkColorValue)
    
        MyBase.OnPaint(e)
    End Sub
    
  14. F6 キーを押してソリューションをビルドします。

MarqueeBorder 子コントロールを作成する

MarqueeBorder コントロールは、MarqueeText コントロールより少し高度です。 さらに多くのプロパティがあり、OnPaint メソッドのアニメーションも複雑になります。 原則としては、MarqueeText コントロールと非常によく似ています。

MarqueeBorder コントロールは子コントロールを持つことができるため、Layout イベントを認識する必要があります。

MarqueeBorder コントロールを作成するには

  1. 新しいカスタム コントロール項目を MarqueeControlLibrary プロジェクトに追加します。 新しいソース ファイルのベース名を "MarqueeBorder" にします。

  2. ツールボックスから MarqueeBorder コントロールに BackgroundWorker コンポーネントをドラッグします。 このコンポーネントにより、MarqueeBorder コントロールでそれ自体を非同期に更新できるようになります。

  3. [プロパティ] ウィンドウで、BackgroundWorker コンポーネントの WorkerReportsProgress および WorkerSupportsCancellation プロパティを true に設定します。 これらの設定により、BackgroundWorker コンポーネントで、定期的に ProgressChanged イベントを発生させ、非同期更新を取り消すことができます。 詳細については、「BackgroundWorker コンポーネント」を参照してください。

  4. [プロパティ] ウィンドウで、 [イベント] ボタンを選択します。 DoWork および ProgressChanged イベントのハンドラーをアタッチします。

  5. コード エディターMarqueeBorder ソース ファイルを開きます。 ファイルの先頭で、次の名前空間をインポートします。

    using System;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Drawing;
    using System.Drawing.Design;
    using System.Threading;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Drawing
    Imports System.Drawing.Design
    Imports System.Threading
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
  6. Panel を継承し、IMarqueeWidget インターフェイスを実装するように、MarqueeBorder の宣言を変更します。

    [Designer(typeof(MarqueeControlLibrary.Design.MarqueeBorderDesigner ))]
    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
    public partial class MarqueeBorder : Panel, IMarqueeWidget
    {
    
    <Designer(GetType(MarqueeControlLibrary.Design.MarqueeBorderDesigner)), _
    ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _
    ToolboxItemFilterType.Require)> _
    Partial Public Class MarqueeBorder
        Inherits Panel
        Implements IMarqueeWidget
    
  7. MarqueeBorder コントロールの状態を管理するための 2 つの列挙型を宣言します。MarqueeSpinDirection により、ライトが境界の周りを "スピン" する方向が決まります。MarqueeLightShape により、ライトの形状 (正方形または円形) が決まります。 これらの宣言を、MarqueeBorder クラスの宣言の前に配置します。

    // This defines the possible values for the MarqueeBorder
    // control's SpinDirection property.
    public enum MarqueeSpinDirection
    {
        CW,
        CCW
    }
    
    // This defines the possible values for the MarqueeBorder
    // control's LightShape property.
    public enum MarqueeLightShape
    {
        Square,
        Circle
    }
    
    ' This defines the possible values for the MarqueeBorder
    ' control's SpinDirection property.
    Public Enum MarqueeSpinDirection
       CW
       CCW
    End Enum
    
    ' This defines the possible values for the MarqueeBorder
    ' control's LightShape property.
    Public Enum MarqueeLightShape
        Square
        Circle
    End Enum
    
  8. 公開されたプロパティに対応するインスタンス変数を宣言し、コンストラクターでそれらを初期化します。

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

    StartMarquee および StopMarquee メソッドにより、BackgroundWorker コンポーネントの RunWorkerAsync および CancelAsync メソッドが呼び出されて、アニメーションが開始および停止されます。

    MarqueeBorderコントロールは子コントロールを含むことができるため、StartMarquee メソッドによってすべての子コントロールが列挙され、IMarqueeWidget が実装されているものに対して StartMarquee が呼び出されます。 StopMarquee メソッドの実装も同様です。

    public virtual void StartMarquee()
    {
        // The MarqueeBorder control may contain any number of
        // controls that implement IMarqueeWidget, so find
        // each IMarqueeWidget child and call its StartMarquee
        // method.
        foreach (Control cntrl in this.Controls)
        {
            if (cntrl is IMarqueeWidget)
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StartMarquee();
            }
        }
    
        // Start the updating thread and pass it the UpdatePeriod.
        this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod);
    }
    
    public virtual void StopMarquee()
    {
        // The MarqueeBorder control may contain any number of
        // controls that implement IMarqueeWidget, so find
        // each IMarqueeWidget child and call its StopMarquee
        // method.
        foreach (Control cntrl in this.Controls)
        {
            if (cntrl is IMarqueeWidget)
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StopMarquee();
            }
        }
    
        // Stop the updating thread.
        this.backgroundWorker1.CancelAsync();
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public virtual int UpdatePeriod
    {
        get
        {
            return this.updatePeriodValue;
        }
    
        set
        {
            if (value > 0)
            {
                this.updatePeriodValue = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0");
            }
        }
    }
    
    
    Public Overridable Sub StartMarquee() _
    Implements IMarqueeWidget.StartMarquee
        ' The MarqueeBorder control may contain any number of 
        ' controls that implement IMarqueeWidget, so find
        ' each IMarqueeWidget child and call its StartMarquee
        ' method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StartMarquee()
            End If
        Next cntrl
    
        ' Start the updating thread and pass it the UpdatePeriod.
        Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod)
    End Sub
    
    
    Public Overridable Sub StopMarquee() _
    Implements IMarqueeWidget.StopMarquee
        ' The MarqueeBorder control may contain any number of 
        ' controls that implement IMarqueeWidget, so find
        ' each IMarqueeWidget child and call its StopMarquee
        ' method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StopMarquee()
            End If
        Next cntrl
    
        ' Stop the updating thread.
        Me.backgroundWorker1.CancelAsync()
    End Sub
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Overridable Property UpdatePeriod() As Integer _
    Implements IMarqueeWidget.UpdatePeriod
    
        Get
            Return Me.updatePeriodValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 Then
                Me.updatePeriodValue = Value
            Else
                Throw New ArgumentOutOfRangeException("UpdatePeriod", _
                "must be > 0")
            End If
        End Set
    
    End Property
    
  10. プロパティのアクセサーを実装します。 MarqueeBorder コントロールには、外観を制御するための複数のプロパティがあります。

    [Category("Marquee")]
    [Browsable(true)]
    public int LightSize
    {
        get
        {
            return this.lightSizeValue;
        }
    
        set
        {
            if (value > 0 && value <= MaxLightSize)
            {
                this.lightSizeValue = value;
                this.DockPadding.All = 2 * value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("LightSize", "must be > 0 and < MaxLightSize");
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public int LightPeriod
    {
        get
        {
            return this.lightPeriodValue;
        }
    
        set
        {
            if (value > 0)
            {
                this.lightPeriodValue = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("LightPeriod", "must be > 0 ");
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color LightColor
    {
        get
        {
            return this.lightColorValue;
        }
    
        set
        {
            // The LightColor property is only changed if the
            // client provides a different value. Comparing values
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.lightColorValue.ToArgb() != value.ToArgb())
            {
                this.lightColorValue = value;
                this.lightBrush = new SolidBrush(value);
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color DarkColor
    {
        get
        {
            return this.darkColorValue;
        }
    
        set
        {
            // The DarkColor property is only changed if the
            // client provides a different value. Comparing values
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.darkColorValue.ToArgb() != value.ToArgb())
            {
                this.darkColorValue = value;
                this.darkBrush = new SolidBrush(value);
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public int LightSpacing
    {
        get
        {
            return this.lightSpacingValue;
        }
    
        set
        {
            if (value >= 0)
            {
                this.lightSpacingValue = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("LightSpacing", "must be >= 0");
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    [EditorAttribute(typeof(LightShapeEditor),
         typeof(System.Drawing.Design.UITypeEditor))]
    public MarqueeLightShape LightShape
    {
        get
        {
            return this.lightShapeValue;
        }
    
        set
        {
            this.lightShapeValue = value;
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public MarqueeSpinDirection SpinDirection
    {
        get
        {
            return this.spinDirectionValue;
        }
    
        set
        {
            this.spinDirectionValue = value;
        }
    }
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightSize() As Integer
        Get
            Return Me.lightSizeValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 AndAlso Value <= MaxLightSize Then
                Me.lightSizeValue = Value
                Me.DockPadding.All = 2 * Value
            Else
                Throw New ArgumentOutOfRangeException("LightSize", _
                "must be > 0 and < MaxLightSize")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightPeriod() As Integer
        Get
            Return Me.lightPeriodValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 Then
                Me.lightPeriodValue = Value
            Else
                Throw New ArgumentOutOfRangeException("LightPeriod", _
                "must be > 0 ")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightColor() As Color
        Get
            Return Me.lightColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The LightColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then
                Me.lightColorValue = Value
                Me.lightBrush = New SolidBrush(Value)
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property DarkColor() As Color
        Get
            Return Me.darkColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The DarkColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then
                Me.darkColorValue = Value
                Me.darkBrush = New SolidBrush(Value)
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightSpacing() As Integer
        Get
            Return Me.lightSpacingValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value >= 0 Then
                Me.lightSpacingValue = Value
            Else
                Throw New ArgumentOutOfRangeException("LightSpacing", _
                "must be >= 0")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True), _
    EditorAttribute(GetType(LightShapeEditor), _
    GetType(System.Drawing.Design.UITypeEditor))> _
    Public Property LightShape() As MarqueeLightShape
    
        Get
            Return Me.lightShapeValue
        End Get
    
        Set(ByVal Value As MarqueeLightShape)
            Me.lightShapeValue = Value
        End Set
    
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property SpinDirection() As MarqueeSpinDirection
    
        Get
            Return Me.spinDirectionValue
        End Get
    
        Set(ByVal Value As MarqueeSpinDirection)
            Me.spinDirectionValue = Value
        End Set
    
    End Property
    
  11. BackgroundWorker コンポーネントの DoWork および ProgressChangedイベントのハンドラーを実装します。

    DoWork イベント ハンドラーは、コードで CancelAsync が呼び出されてアニメーションが停止されるまで、UpdatePeriod によって指定されているミリ秒数だけスリープ状態になった後、ProgressChanged イベントを発生させます。

    ProgressChanged イベント ハンドラーにより、他のライトの明/暗状態が決定される "基本" ライトの位置がインクリメントされ、Refresh メソッドが呼び出されてコントロール自体が再描画されます。

    // This method is called in the worker thread's context,
    // so it must not make any calls into the MarqueeBorder
    // control. Instead, it communicates to the control using
    // the ProgressChanged event.
    //
    // The only work done in this event handler is
    // to sleep for the number of milliseconds specified
    // by UpdatePeriod, then raise the ProgressChanged event.
    private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;
    
        // This event handler will run until the client cancels
        // the background task by calling CancelAsync.
        while (!worker.CancellationPending)
        {
            // The Argument property of the DoWorkEventArgs
            // object holds the value of UpdatePeriod, which
            // was passed as the argument to the RunWorkerAsync
            // method.
            Thread.Sleep((int)e.Argument);
    
            // The DoWork eventhandler does not actually report
            // progress; the ReportProgress event is used to
            // periodically alert the control to update its state.
            worker.ReportProgress(0);
        }
    }
    
    // The ProgressChanged event is raised by the DoWork method.
    // This event handler does work that is internal to the
    // control. In this case, the currentOffset is incremented,
    // and the control is told to repaint itself.
    private void backgroundWorker1_ProgressChanged(
        object sender,
        System.ComponentModel.ProgressChangedEventArgs e)
    {
        this.currentOffset++;
        this.Refresh();
    }
    
    ' This method is called in the worker thread's context, 
    ' so it must not make any calls into the MarqueeBorder
    ' control. Instead, it communicates to the control using 
    ' the ProgressChanged event.
    '
    ' The only work done in this event handler is
    ' to sleep for the number of milliseconds specified 
    ' by UpdatePeriod, then raise the ProgressChanged event.
    Private Sub backgroundWorker1_DoWork( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.DoWorkEventArgs) _
    Handles backgroundWorker1.DoWork
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
    
        ' This event handler will run until the client cancels
        ' the background task by calling CancelAsync.
        While Not worker.CancellationPending
            ' The Argument property of the DoWorkEventArgs
            ' object holds the value of UpdatePeriod, which 
            ' was passed as the argument to the RunWorkerAsync
            ' method. 
            Thread.Sleep(Fix(e.Argument))
    
            ' The DoWork eventhandler does not actually report
            ' progress; the ReportProgress event is used to 
            ' periodically alert the control to update its state.
            worker.ReportProgress(0)
        End While
    End Sub
    
    
    ' The ProgressChanged event is raised by the DoWork method.
    ' This event handler does work that is internal to the
    ' control. In this case, the currentOffset is incremented,
    ' and the control is told to repaint itself.
    Private Sub backgroundWorker1_ProgressChanged( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
    Handles backgroundWorker1.ProgressChanged
        Me.currentOffset += 1
        Me.Refresh()
    End Sub
    
  12. ヘルパー メソッド IsLitDrawLight を実装します。

    IsLit メソッドにより、特定の位置でのライトの色が決定されます。 "明状態" のライトは、LightColor プロパティによって指定された色で描画されます。"暗状態" の場合は、DarkColor プロパティによって指定された色で描画されます。

    DrawLight メソッドにより、適切な色、形状、位置を使用してライトが描画されます。

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

    OnPaint メソッドにより、MarqueeBorder コントロールの端に沿ってライトが描画されます。

    OnPaint メソッドは MarqueeBorder コントロールの寸法に依存するため、レイアウトが変更されるたびに呼び出す必要があります。 これを実現するには、OnLayout をオーバーライドして、Refresh を呼び出します。

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

プロパティのシャドウおよびフィルター処理を行うカスタム デザイナーを作成する

MarqueeControlRootDesigner クラスにより、ルート デザイナーの実装が提供されます。 MarqueeControl で動作するこのデザイナーに加えて、MarqueeBorder コントロールに特に関連付けられたカスタム デザイナーが必要です。 このデザイナーにより、カスタム ルート デザイナーのコンテキストに適したカスタム動作が提供されます。

具体的には、MarqueeBorderDesigner により MarqueeBorder コントロールの特定のプロパティの "シャドウ" およびフィルター処理が行われ、デザイン環境との相互作用が変更されます。

コンポーネントのプロパティ アクセサーの呼び出しをインターセプトすることは、"シャドウ" と呼ばれます。これにより、デザイナーはユーザーによって設定された値を追跡し、必要に応じてその値を設計されているコンポーネントに渡すことができます。

この例では、Visible および Enabled プロパティが MarqueeBorderDesigner によってシャドウ処理されます。これにより、デザイン時にユーザーは MarqueeBorder コントロールを非表示にしたり無効にしたりすることができなくなります。

デザイナーでプロパティを追加および削除することもできます。 この例では、パディングは LightSize プロパティによって指定されているライトのサイズに基づいて MarqueeBorder コントロールでプログラムにより設定されるため、Padding プロパティはデザイン時には削除されます。

MarqueeBorderDesigner の基底クラスは ComponentDesigner であり、デザイン時にコントロールによって公開される属性、プロパティ、イベントを変更できるメソッドがあります。

これらのメソッドを使用してコンポーネントのパブリック インターフェイスを変更する場合は、次の規則に従います。

  • 項目の追加または削除は、PreFilter メソッドにおいてのみ行います

  • 既存の項目の変更は、PostFilter メソッドにおいてのみ行います

  • PreFilter メソッドでは常に基本実装を最初に呼び出します

  • PostFilter メソッドでは常に基本実装を最後に呼び出します

これらの規則に従うことにより、デザイン時環境のすべてのデザイナーで、デザイン対象のすべてのコンポーネントのビューが、一貫したものになります。

ComponentDesigner クラスにより、シャドウ処理されるプロパティの値を管理するためのディクショナリが提供されます。これにより、特定のインスタンス変数を作成する必要がなくなります。

プロパティのシャドウおよびフィルター処理を行うカスタム デザイナーを作成するには

  1. [デザイン] フォルダーを右クリックして、新しいクラスを追加します。 ソース ファイルのベース名を MarqueeBorderDesigner にします。

  2. コード エディターで MarqueeBorderDesigner ソース ファイルを開きます。 ファイルの先頭で、次の名前空間をインポートします。

    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
  3. ParentControlDesigner を継承するように MarqueeBorderDesigner の宣言を変更します。

    MarqueeBorder コントロールは子コントロールを含むことができるため、MarqueeBorderDesigner は、親子の相互作用を処理する ParentControlDesigner を継承します。

    namespace MarqueeControlLibrary.Design
    {
        public class MarqueeBorderDesigner : ParentControlDesigner
        {
    
    Namespace MarqueeControlLibrary.Design
    
        Public Class MarqueeBorderDesigner
            Inherits ParentControlDesigner
    
  4. PreFilterProperties の基本実装をオーバーライドします。

    protected override void PreFilterProperties(IDictionary properties)
    {
        base.PreFilterProperties(properties);
    
        if (properties.Contains("Padding"))
        {
            properties.Remove("Padding");
        }
    
        properties["Visible"] = TypeDescriptor.CreateProperty(
            typeof(MarqueeBorderDesigner),
            (PropertyDescriptor)properties["Visible"],
            new Attribute[0]);
    
        properties["Enabled"] = TypeDescriptor.CreateProperty(
            typeof(MarqueeBorderDesigner),
            (PropertyDescriptor)properties["Enabled"],
            new Attribute[0]);
    }
    
    Protected Overrides Sub PreFilterProperties( _
    ByVal properties As IDictionary)
    
        MyBase.PreFilterProperties(properties)
    
        If properties.Contains("Padding") Then
            properties.Remove("Padding")
        End If
    
        properties("Visible") = _
        TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _
        CType(properties("Visible"), PropertyDescriptor), _
        New Attribute(-1) {})
    
        properties("Enabled") = _
        TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _
        CType(properties("Enabled"), _
        PropertyDescriptor), _
        New Attribute(-1) {})
    
    End Sub
    
  5. Enabled プロパティと Visible プロパティを実装します。 これらの実装により、コントロールのプロパティのシャドウ処理が行われます。

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

コンポーネントの変更を処理する

MarqueeControlRootDesigner クラスにより、MarqueeControl インスタンスのカスタム デザイン時エクスペリエンスが提供されます。 ほとんどのデザイン時機能は、DocumentDesigner クラスから継承されます。 コードでは、コンポーネントの変更の処理とデザイナー動詞の追加という 2 つの特定のカスタマイズを実装します。

ユーザーが MarqueeControl のインスタンスをデザインすると、ルート デザイナーにより MarqueeControl とその子コントロールまで変更が追跡されます。 デザイン時環境により、コンポーネントの状態に対する変更を追跡するための便利なサービスである IComponentChangeService が提供されます。

このサービスへの参照を取得するには、GetService メソッドを使用して環境のクエリを実行します。 クエリが成功した場合は、デザイナーで ComponentChanged イベントのハンドラーをアタッチし、デザイン時に一貫した状態を維持するために必要なすべてのタスクを実行できます。

MarqueeControlRootDesigner クラスの場合は、MarqueeControl に格納されている各 IMarqueeWidget オブジェクトで Refresh メソッドを呼び出します。 これにより、親の Size ようなプロパティが変更されたときは、IMarqueeWidget オブジェクトによって適切に再描画されます。

コンポーネントの変更を処理するには

  1. コード エディターMarqueeControlRootDesigner ソース ファイルを開き、Initialize メソッドをオーバーライドします。 Initialize の基本実装を呼び出し、IComponentChangeService のクエリを実行します。

    base.Initialize(component);
    
    IComponentChangeService cs =
        GetService(typeof(IComponentChangeService))
        as IComponentChangeService;
    
    if (cs != null)
    {
        cs.ComponentChanged +=
            new ComponentChangedEventHandler(OnComponentChanged);
    }
    
    MyBase.Initialize(component)
    
    Dim cs As IComponentChangeService = _
    CType(GetService(GetType(IComponentChangeService)), _
    IComponentChangeService)
    
    If (cs IsNot Nothing) Then
        AddHandler cs.ComponentChanged, AddressOf OnComponentChanged
    End If
    
  2. OnComponentChanged イベント ハンドラーを実装します。 送信コンポーネントの型をテストし、それが IMarqueeWidget である場合は、その Refresh メソッドを呼び出します。

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

デザイナー動詞をカスタム デザイナーに追加する

デザイナー動詞は、イベント ハンドラーにリンクされたメニュー コマンドです。 デザイナー動詞は、デザイン時にコンポーネントのショートカット メニューに追加されます。 詳細については、「DesignerVerb」を参照してください。

"テストを実行する" と "テストを停止する" という 2 つのデザイナー動詞をデザイナーに追加します。 これらの動詞を使用すると、デザイン時に MarqueeControl の実行時の動作を表示できます。 これらの動詞は MarqueeControlRootDesigner に追加されます。

"テストを実行する" が呼び出されると、動詞イベント ハンドラーによって MarqueeControlStartMarquee メソッドが呼び出されます。 "テストを停止する" が呼び出されると、動詞イベント ハンドラーによって MarqueeControlStopMarquee メソッドが呼び出されます。 StartMarquee および StopMarquee メソッドの実装により、IMarqueeWidget が実装されている含まれるコントロールでこれらのメソッドが呼び出されるため、含まれる IMarqueeWidget コントロールもテストに参加します。

デザイナー動詞をカスタム デザイナーに追加するには

  1. MarqueeControlRootDesigner クラスで、OnVerbRunTest および OnVerbStopTest という名前のイベント ハンドラーを追加します。

    private void OnVerbRunTest(object sender, EventArgs e)
    {
        MarqueeControl c = this.Control as MarqueeControl;
    
        c.Start();
    }
    
    private void OnVerbStopTest(object sender, EventArgs e)
    {
        MarqueeControl c = this.Control as MarqueeControl;
    
        c.Stop();
    }
    
    Private Sub OnVerbRunTest( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Dim c As MarqueeControl = CType(Me.Control, MarqueeControl)
        c.Start()
    
    End Sub
    
    Private Sub OnVerbStopTest( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Dim c As MarqueeControl = CType(Me.Control, MarqueeControl)
        c.Stop()
    
    End Sub
    
  2. これらのイベント ハンドラーを、対応するデザイナー動詞に接続します。 MarqueeControlRootDesigner は、基底クラスから DesignerVerbCollection を継承します。 2 つの新しい DesignerVerb オブジェクトを作成し、Initialize メソッドでそれらをこのコレクションに追加します。

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

カスタム UITypeEditor を作成する

ユーザーのためのカスタム デザイン時エクスペリエンスを作成するときは、多くの場合、プロパティ ウィンドウとのカスタム対話を作成することをお勧めします。 これは、UITypeEditor を作成することによって実現できます。

MarqueeBorder コントロールにより、いくつかのプロパティがプロパティ ウィンドウに公開されます。 これらのプロパティのうちの 2 つである MarqueeSpinDirectionMarqueeLightShape は、列挙型によって表されます。 UI 型エディターの使用方法を示すため、MarqueeLightShape プロパティには UITypeEditor クラスが関連付けられています。

カスタム UI 型エディターを作成するには

  1. コード エディターMarqueeBorder ソース ファイルを開きます。

  2. MarqueeBorder クラスの定義で、UITypeEditor から派生する LightShapeEditor という名前のクラスを宣言します。

    // 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. editorService という名前の IWindowsFormsEditorService インスタンス変数を宣言します。

    private IWindowsFormsEditorService editorService = null;
    
    Private editorService As IWindowsFormsEditorService = Nothing
    
  4. GetEditStyle メソッドをオーバーライドします。 この実装から返される DropDown により、LightShapeEditor を表示する方法がデザイン環境に指示されます。

    public override UITypeEditorEditStyle GetEditStyle(
    System.ComponentModel.ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.DropDown;
    }
    
    Public Overrides Function GetEditStyle( _
    ByVal context As System.ComponentModel.ITypeDescriptorContext) _
    As UITypeEditorEditStyle
        Return UITypeEditorEditStyle.DropDown
    End Function
    
    
  5. EditValue メソッドをオーバーライドします。 この実装により、デザイン環境で IWindowsFormsEditorService オブジェクトのクエリが行われます。 成功した場合は、LightShapeSelectionControl が作成されます。 LightShapeEditor を開始するために、DropDownControl メソッドが呼び出されます。 この呼び出しからの戻り値が、デザイン環境に返されます。

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

カスタム UITypeEditor のビュー コントロールを作成する

MarqueeLightShape プロパティにより、SquareCircle の 2 種類のライト形状がサポートされます。 プロパティ ウィンドウでこれらの値をグラフィカルに表示するためだけに使用されるカスタム コントロールを作成します。 このカスタム コントロールは、プロパティ ウィンドウと対話するために UITypeEditor によって使用されます。

カスタム UI 型エディターのビュー コントロールを作成するには

  1. 新しい UserControl 項目を MarqueeControlLibrary プロジェクトに追加します。 新しいソース ファイルのベース名として LightShapeSelectionControl を指定します。

  2. ツールボックスから LightShapeSelectionControl に 2 つの Panel コントロールをドラッグします。 squarePanelcirclePanel という名前を指定します。 それらを並べて配置します。 両方の Panel コントロールの Size プロパティを (60, 60) に設定します。 squarePanel コントロールの Location プロパティを (8, 10) に設定します。 circlePanel コントロールの Location プロパティを (80, 10) に設定します。 最後に、LightShapeSelectionControlSize プロパティを (150, 80) に設定します。

  3. コード エディターLightShapeSelectionControl ソース ファイルを開きます。 ファイルの先頭で System.Windows.Forms.Design 名前空間をインポートします。

    Imports System.Windows.Forms.Design
    
    using System.Windows.Forms.Design;
    
  4. squarePanel および circlePanel コントロールの Click イベント ハンドラーを実装します。 これらのメソッドでは、カスタム UITypeEditor 編集セッションを終了するために CloseDropDown が呼び出されます。

    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. editorService という名前の IWindowsFormsEditorService インスタンス変数を宣言します。

    Private editorService As IWindowsFormsEditorService
    
    private IWindowsFormsEditorService editorService;
    
  6. lightShapeValue という名前の MarqueeLightShape インスタンス変数を宣言します。

    private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
    
    Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
    
  7. LightShapeSelectionControl コンストラクターで、Click イベント ハンドラーを squarePanel および circlePanel コントロールの Click イベントにアタッチします。 また、デザイン環境の MarqueeLightShape の値を lightShapeValue フィールドに割り当てるコンストラクターのオーバーロードを定義します。

    // This constructor takes a MarqueeLightShape value from the
    // design-time environment, which will be used to display
    // the initial state.
    public LightShapeSelectionControl(
        MarqueeLightShape lightShape,
        IWindowsFormsEditorService editorService )
    {
        // This call is required by the designer.
        InitializeComponent();
    
        // Cache the light shape value provided by the
        // design-time environment.
        this.lightShapeValue = lightShape;
    
        // Cache the reference to the editor service.
        this.editorService = editorService;
    
        // Handle the Click event for the two panels.
        this.squarePanel.Click += new EventHandler(squarePanel_Click);
        this.circlePanel.Click += new EventHandler(circlePanel_Click);
    }
    
    ' This constructor takes a MarqueeLightShape value from the
    ' design-time environment, which will be used to display
    ' the initial state.
     Public Sub New( _
     ByVal lightShape As MarqueeLightShape, _
     ByVal editorService As IWindowsFormsEditorService)
         ' This call is required by the Windows.Forms Form Designer.
         InitializeComponent()
    
         ' Cache the light shape value provided by the 
         ' design-time environment.
         Me.lightShapeValue = lightShape
    
         ' Cache the reference to the editor service.
         Me.editorService = editorService
    
         ' Handle the Click event for the two panels. 
         AddHandler Me.squarePanel.Click, AddressOf squarePanel_Click
         AddHandler Me.circlePanel.Click, AddressOf circlePanel_Click
     End Sub
    
  8. Dispose メソッドで、Click イベント ハンドラーをデタッチします。

    protected override void Dispose( bool disposing )
    {
        if( disposing )
        {
            // Be sure to unhook event handlers
            // to prevent "lapsed listener" leaks.
            this.squarePanel.Click -=
                new EventHandler(squarePanel_Click);
            this.circlePanel.Click -=
                new EventHandler(circlePanel_Click);
    
            if(components != null)
            {
                components.Dispose();
            }
        }
        base.Dispose( disposing );
    }
    
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        If disposing Then
    
            ' Be sure to unhook event handlers
            ' to prevent "lapsed listener" leaks.
            RemoveHandler Me.squarePanel.Click, AddressOf squarePanel_Click
            RemoveHandler Me.circlePanel.Click, AddressOf circlePanel_Click
    
            If (components IsNot Nothing) Then
                components.Dispose()
            End If
    
        End If
        MyBase.Dispose(disposing)
    End Sub
    
  9. ソリューション エクスプローラーで、 [すべてのファイルを表示] ボタンをクリックします。 LightShapeSelectionControl.Designer.cs または LightShapeSelectionControl.Designer.vb ファイルを開き、Dispose メソッドの既定の定義を削除します。

  10. LightShape プロパティを実装します。

    // LightShape is the property for which this control provides
    // a custom user interface in the Properties window.
    public MarqueeLightShape LightShape
    {
        get
        {
            return this.lightShapeValue;
        }
        
        set
        {
            if( this.lightShapeValue != value )
            {
                this.lightShapeValue = value;
            }
        }
    }
    
    ' LightShape is the property for which this control provides
    ' a custom user interface in the Properties window.
    Public Property LightShape() As MarqueeLightShape
    
        Get
            Return Me.lightShapeValue
        End Get
    
        Set(ByVal Value As MarqueeLightShape)
            If Me.lightShapeValue <> Value Then
                Me.lightShapeValue = Value
            End If
        End Set
    
    End Property
    
  11. OnPaint メソッドをオーバーライドします。 この実装により、塗りつぶされた正方形と円が描画されます。 また、どちらか一方の形状の周りに境界線が描画され、選択された値が強調表示されます。

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

デザイナーでカスタム コントロールをテストする

この段階で、MarqueeControlLibrary プロジェクトをビルドすることができます。 MarqueeControl クラスを継承するコントロールを作成し、それをフォームで使用することにより、実装をテストします。

カスタム MarqueeControl の実装を作成するには

  1. Windows フォーム デザイナーで DemoMarqueeControl を開きます。 これにより、DemoMarqueeControl 型のインスタンスが作成され、MarqueeControlRootDesigner 型のインスタンス内に表示されます。

  2. ツールボックスで、 [MarqueeControlLibrary コンポーネント] タブを開きます。選択できる MarqueeBorder および MarqueeText コントロールが表示されます。

  3. MarqueeBorder コントロールのインスタンスを DemoMarqueeControl のデザイン画面にドラッグします。 この MarqueeBorder コントロールを親コントロールにドッキングします。

  4. MarqueeText コントロールのインスタンスを DemoMarqueeControl のデザイン画面にドラッグします。

  5. ソリューションをビルドします。

  6. DemoMarqueeControl を右クリックし、ショートカット メニューから [テストを実行する] オプションを選択して、アニメーションを開始します。 [テストを停止する] をクリックして、アニメーションを停止します。

  7. デザイン ビューで [Form1] を開きます。

  8. フォームに 2 つの Button コントロールを配置します。 それらに startButton および stopButton という名前を設定し、Text プロパティの値をそれぞれ Start および Stop に変更します。

  9. 両方の Button コントロールの Click イベント ハンドラーを実装します。

  10. ツールボックスで、 [MarqueeControlTest コンポーネント] タブを開きます。選択できる DemoMarqueeControl が表示されます。

  11. DemoMarqueeControl のインスタンスを Form1 のデザイン画面にドラッグします。

  12. Click イベント ハンドラーで、DemoMarqueeControlStart および Stop メソッドを呼び出し ます。

    Private Sub startButton_Click(sender As Object, e As System.EventArgs)
        Me.demoMarqueeControl1.Start()
    End Sub 'startButton_Click
    
    Private Sub stopButton_Click(sender As Object, e As System.EventArgs)
    Me.demoMarqueeControl1.Stop()
    End Sub 'stopButton_Click
    
    private void startButton_Click(object sender, System.EventArgs e)
    {
        this.demoMarqueeControl1.Start();
    }
    
    private void stopButton_Click(object sender, System.EventArgs e)
    {
        this.demoMarqueeControl1.Stop();
    }
    
  13. MarqueeControlTest プロジェクトをスタートアップ プロジェクトとして設定し、実行します。 フォームに DemoMarqueeControl が表示されます。 [開始] ボタンを選択して、アニメーションを開始します。 テキストが点滅し、ライトが境界線内を移動するはずです。

次のステップ

MarqueeControlLibrary では、カスタム コントロールとそれに関連付けられたデザイナーの簡単な実装が示されています。 いくつかの方法で、このサンプルをより高度なものにすることができます。

  • DemoMarqueeControl のプロパティの値をデザイナーで変更します。 MarqueBorder コントロールをさらに追加し、親インスタンス内にドッキングして、入れ子になった効果を作成します。 UpdatePeriod およびライト関連のプロパティについて、さまざまな設定を調べます。

  • IMarqueeWidget の独自の実装を作成します。 たとえば、点滅する "ネオン サイン" や、複数の画像が含まれるアニメーション化されたサインを作成してみます。

  • デザイン時エクスペリエンスをさらにカスタマイズします。 EnabledVisible 以外のプロパティをシャドウ処理してみたり、新しいプロパティを追加します。 新しいデザイナー動詞を追加して、子コントロールのドッキングなどの一般的なタスクを簡素化します。

  • MarqueeControl にライセンスを設定します。

  • コントロールのシリアル化方法とコードの生成方法を制御します。 詳細については、「動的なソース コードの生成とコンパイル」を参照してください。

関連項目