다음을 통해 공유


연습: Visual Studio의 디자인 타임 기능을 사용하는 Windows Forms 컨트롤 만들기

업데이트: 2007년 11월

연결된 사용자 지정 디자이너를 만들어 사용자 지정 컨트롤의 디자인 타임 환경을 향상시킬 수 있습니다.

이 연습에서는 사용자 지정 컨트롤의 사용자 지정 디자이너를 만드는 방법을 보여 줍니다. MarqueeControl 형식과 MarqueeControlRootDesigner라는 연결된 디자이너 클래스를 구현합니다.

MarqueeControl 형식은 애니메이션 조명과 깜박이는 텍스트로 극장 홍보물과 같은 화면 표시를 구현합니다.

이 컨트롤의 디자이너는 사용자 지정 디자인 타임 환경을 제공하는 디자인 환경과 상호 작용합니다. 사용자 지정 디자이너를 사용하여 애니메이션 조명 및 깜박이는 텍스트와 함께 사용자 지정 MarqueeControl 구현을 여러 가지 조합으로 구성할 수 있습니다. 이렇게 구성된 컨트롤을 다른 모든 Windows Forms 컨트롤과 같이 폼에서 사용할 수 있습니다.

이 연습에서 수행할 작업은 다음과 같습니다.

  • 프로젝트 만들기

  • 컨트롤 라이브러리 프로젝트 만들기

  • 사용자 지정 컨트롤 프로젝트 참조

  • 사용자 지정 컨트롤 및 해당 사용자 지정 디자이너 정의

  • 사용자 지정 컨트롤의 인스턴스 만들기

  • 디자인 타임 디버깅을 위한 프로젝트 설정

  • 사용자 지정 컨트롤 구현

  • 사용자 지정 컨트롤의 자식 컨트롤 만들기

  • MarqueeBorder 자식 컨트롤 만들기

  • 사용자 지정 디자이너를 만들어 속성 숨기기 및 필터링

  • 구성 요소의 변경 내용 처리

  • 사용자 지정 디자이너에 디자이너 동사 추가

  • 사용자 지정 UITypeEditor 만들기

  • 디자이너에서 사용자 지정 컨트롤 테스트

작업이 끝나면 다음과 같은 사용자 지정 컨트롤이 만들어집니다.

가능한 MarqueeControl 배치

완성된 코드 목록에 대한 자세한 내용은 방법: 디자인 타임 기능을 활용하는 Windows Forms 컨트롤 만들기를 참조하십시오.

참고:

실제 설정이나 버전에 따라서 화면에 나타나는 대화 상자와 메뉴 명령이 도움말의 설명과 다를 수 있습니다. 설정을 변경하려면 도구 메뉴에서 설정 가져오기 및 내보내기를 선택합니다. 자세한 내용은 Visual Studio 설정을 참조하십시오.

사전 요구 사항

이 연습을 따라 하려면 다음과 같은 요건을 갖추어야 합니다.

  • Visual Studio가 설치된 컴퓨터에서 Windows Forms 응용 프로그램 프로젝트를 만들고 실행할 수 있는 권한

프로젝트 만들기

첫 번째 단계는 응용 프로그램 프로젝트를 만드는 것입니다. 사용자 지정 컨트롤을 호스팅하는 응용 프로그램을 빌드하는 데 이 프로젝트를 사용합니다.

프로젝트를 만들려면

컨트롤 라이브러리 프로젝트 만들기

다음 단계에서는 컨트롤 라이브러리 프로젝트를 만듭니다. 새 사용자 지정 컨트롤과 해당 사용자 지정 디자이너를 만듭니다.

컨트롤 라이브러리 프로젝트를 만들려면

  1. 솔루션에 Windows 컨트롤 라이브러리 프로젝트를 추가합니다. 자세한 내용은 새 프로젝트 추가 대화 상자를 참조하십시오. 프로젝트 이름을 "MarqueeControlLibrary"로 지정합니다.

  2. 솔루션 탐색기를 사용하여 선택한 언어에 따라 이름이 "UserControl1.cs" 또는 "UserControl1.vb" 소스 파일을 삭제하여 프로젝트의 기본 컨트롤을 삭제합니다. 자세한 내용은 방법: 항목 제거, 삭제 및 제외를 참조하십시오.

  3. MarqueeControlLibrary 프로젝트에 새 UserControl 항목을 추가합니다. 새 소스 파일의 기본 이름을 "MarqueeControl"로 지정합니다.

  4. 솔루션 탐색기를 사용하여 MarqueeControlLibrary 프로젝트에 새 폴더를 만듭니다. 자세한 내용은 방법: 새 프로젝트 항목 추가를 참조하십시오. 새 폴더 이름을 "Design"으로 지정합니다.

  5. Design 폴더를 마우스 오른쪽 단추로 클릭하고 새 클래스를 추가합니다. 소스 파일의 기본 이름을 "MarqueeControlRootDesigner"로 지정합니다.

  6. System.Design 어셈블리의 형식을 사용해야 하므로 이 참조를 MarqueeControlLibrary 프로젝트에 추가합니다. 자세한 내용은 방법: Visual Studio에서 참조 추가 및 제거(C#)를 참조하십시오.

사용자 지정 컨트롤 프로젝트 참조

MarqueeControlTest 프로젝트를 사용하여 사용자 지정 컨트롤을 테스트합니다. MarqueeControlLibrary 어셈블리에 프로젝트 참조를 추가할 때 테스트 프로젝트에서 사용자 지정 컨트롤을 인식하게 됩니다.

사용자 지정 컨트롤 프로젝트를 참조하려면

  • MarqueeControlTest 프로젝트에서 MarqueeControlLibrary 어셈블리에 프로젝트 참조를 추가합니다. MarqueeControlLibrary 어셈블리를 직접 참조하는 대신 참조 추가 대화 상자에서 프로젝트 탭을 사용해야 합니다.

사용자 지정 컨트롤 및 해당 사용자 지정 디자이너 정의

사용자 지정 컨트롤은 UserControl 클래스에서 파생됩니다. 따라서 컨트롤에 다른 컨트롤이 포함될 수 있으며 여러 가지 기본 기능이 제공됩니다.

사용자 지정 컨트롤에는 사용자 지정 디자이너가 연결됩니다. 이를 통해 특별히 자신의 사용자 지정 컨트롤에 맞는 고유의 디자인 환경을 만들 수 있습니다.

DesignerAttribute 클래스를 사용하여 컨트롤을 해당 디자이너와 연결합니다. 사용자 지정 컨트롤의 전체 디자인 타임 동작을 개발하므로 사용자 지정 디자이너에는 IRootDesigner 인터페이스가 구현됩니다.

사용자 지정 컨트롤과 해당 사용자 지정 디자이너를 정의하려면

  1. 코드 편집기에서 MarqueeControl 소스 파일을 엽니다. 파일의 맨 위에서 다음과 같은 네임스페이스를 가져옵니다.

    Imports System
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Drawing
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
  2. DesignerAttribute를 MarqueeControl 클래스 선언에 추가합니다. 이렇게 하면 사용자 지정 컨트롤이 해당 디자이너에 연결됩니다.

    <Designer(GetType(MarqueeControlLibrary.Design.MarqueeControlRootDesigner), _
     GetType(IRootDesigner))> _
    Public Class MarqueeControl
        Inherits UserControl
    
     [Designer( typeof( MarqueeControlLibrary.Design.MarqueeControlRootDesigner ), typeof( IRootDesigner ) )]
        public class MarqueeControl : UserControl
        {
    
  3. 코드 편집기에서 MarqueeControlRootDesigner 소스 파일을 엽니다. 파일의 맨 위에서 다음과 같은 네임스페이스를 가져옵니다.

    Imports System
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Drawing.Design
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Drawing.Design;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
  4. DocumentDesigner 클래스에서 상속하도록 MarqueeControlRootDesigner 선언을 변경합니다. ToolboxItemFilterAttribute를 적용하여 디자이너와 도구 상자의 상호 작용을 지정합니다.

    참고   MarqueeControlRootDesigner 클래스에 대한 정의는 "MarqueeControlLibrary.Design" 네임스페이스에 포함되어 있습니다. 이 선언을 통해 디자이너는 디자인 관련 형식용으로 예약된 특수 네임스페이스에 배치됩니다.

    Namespace MarqueeControlLibrary.Design
    
        <ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _
        ToolboxItemFilterType.Require), _
        ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _
        ToolboxItemFilterType.Require)> _
        <System.Security.Permissions.PermissionSetAttribute(System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _
        Public Class MarqueeControlRootDesigner
            Inherits DocumentDesigner
    
    namespace MarqueeControlLibrary.Design
    {
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
        [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
        [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] 
        public class MarqueeControlRootDesigner : DocumentDesigner
        {
    
  5. MarqueeControlRootDesigner 클래스의 생성자를 정의합니다. WriteLine 문을 생성자 본문에 삽입합니다. 이는 디버깅에 유용합니다.

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

사용자 지정 컨트롤의 인스턴스 만들기

컨트롤의 사용자 지정 디자인 타임 동작을 관찰하기 위해 컨트롤의 인스턴스를 MarqueeControlTest 프로젝트의 폼에 배치합니다.

사용자 지정 컨트롤의 인스턴스를 만들려면

  1. MarqueeControlTest 프로젝트에 새 UserControl 항목을 추가합니다. 새 소스 파일의 기본 이름을 "DemoMarqueeControl"로 지정합니다.

  2. 코드 편집기에서 DemoMarqueeControl 파일을 엽니다. 파일의 맨 위에서 MarqueeControlLibrary 네임스페이스를 가져옵니다.

Imports MarqueeControlLibrary
using MarqueeControlLibrary;
  1. MarqueeControl 클래스에서 상속하도록 DemoMarqueeControl 선언을 변경합니다.

  2. 프로젝트를 빌드합니다.

  3. Windows Forms 디자이너에서 Form1을 엽니다.

  4. 도구 상자에서 MarqueeControlTest 구성 요소 탭을 찾아 엽니다. 도구 상자의 DemoMarqueeControl을 폼으로 끌어 옵니다.

  5. 프로젝트를 빌드합니다.

디자인 타임 디버깅을 위한 프로젝트 설정

사용자 지정 디자인 타임 환경을 개발하는 경우 컨트롤과 구성 요소를 디버깅할 필요가 있습니다. 디자인 타임에 디버깅할 수 있도록 프로젝트를 설정할 수 있는 간단한 방법이 있습니다. 자세한 내용은 연습: 디자인 타임에 사용자 지정 Windows Forms 컨트롤 디버깅을 참조하십시오.

디자인 타임 디버깅을 위해 프로젝트를 설정하려면

  1. MarqueeControlLibrary 프로젝트를 마우스 오른쪽 단추로 클릭하고 속성을 선택합니다.

  2. "MarqueeControlLibrary 속성 페이지" 대화 상자에서 구성 속성 페이지를 선택합니다.

  3. 시작 작업 구역에서 시작 외부 프로그램을 선택합니다. 별도의 Visual Studio 인스턴스를 디버깅하므로 줄임표(VisualStudioEllipsesButton 스크린 샷) 단추를 클릭하여 Visual Studio IDE를 찾아봅니다. 실행 파일의 이름은 devenv.exe이며, 기본 위치에 설치한 경우 경로는 %programfiles%\Microsoft Visual Studio 9.0\Common7\IDE\devenv.exe입니다.

  4. 확인을 클릭하여 대화 상자를 닫습니다.

  5. MarqueeControlLibrary 프로젝트를 마우스 오른쪽 단추로 클릭하고 "시작 프로젝트로 설정"을 선택하여 이 디버깅 구성을 사용합니다.

검사점

이제 사용자 지정 컨트롤의 디자인 타임 동작을 디버깅할 준비가 되었습니다. 디버깅 환경이 올바르게 설정된 것을 확인했으면 사용자 지정 컨트롤과 사용자 지정 디버거 사이의 연결을 테스트합니다.

디버깅 환경과 디자이너 연결을 테스트하려면

  1. 코드 편집기에서 MarqueeControlRootDesigner 소스 파일을 열고 WriteLine 문에 중단점을 설정합니다.

  2. F5 키를 눌러 디버깅 세션을 시작합니다. Visual Studio의 새 인스턴스가 만들어집니다.

  3. Visual Studio의 새 인스턴스에서 "MarqueeControlTest" 솔루션을 엽니다. 파일 메뉴에서 최근에 사용한 프로젝트를 선택하여 솔루션을 쉽게 찾을 수 있습니다. "MarqueeControlTest.sln" 솔루션 파일이 최근에 사용한 파일로 나열됩니다.

  4. 디자이너에서 DemoMarqueeControl을 엽니다. Visual Studio의 디버깅 인스턴스에 포커스가 부여되고 중단점에서 실행이 중지됩니다. F5 키를 눌러 디버깅 세션을 계속합니다.

이제 사용자 지정 컨트롤과 연결된 해당 사용자 지정 디자이너를 개발하고 디버깅할 수 있도록 모든 준비가 되었습니다. 이 연습의 나머지 부분에서는 컨트롤과 디자이너의 기능 구현에 대한 자세한 내용을 중점적으로 살펴봅니다.

사용자 지정 컨트롤 구현

MarqueeControl은 약간의 사용자 지정이 포함된 UserControl입니다. 이 컨트롤은 움직이는 텍스트 애니메이션을 시작하는 Start와 애니메이션을 중지하는 Stop 등 두 개의 메서드를 노출합니다. MarqueeControl에는 IMarqueeWidget 인터페이스를 구현하는 자식 컨트롤을 포함하므로 Start와 Stop은 각 자식 컨트롤을 열거하고 IMarqueeWidget을 구현하는 각 자식 컨트롤에서 StartMarquee와 StopMarquee 메서드를 각각 호출합니다.

MarqueeBorder 및 MarqueeText 컨트롤의 모양은 레이아웃에 따라 좌우되므로 MarqueeControl은 OnLayout 메서드를 재정의하고 이 형식의 자식 컨트롤에서 PerformLayout을 호출합니다.

이것이 MarqueeControl 사용자 지정의 범위입니다. 런타임 기능은 MarqueeBorder 및 MarqueeText 컨트롤에 의해 구현되며 디자인 타임 기능은 MarqueeBorderDesigner 및 MarqueeControlRootDesigner 클래스에 의해 구현됩니다.

사용자 지정 컨트롤을 구현하려면

  1. 코드 편집기에서 MarqueeControl 소스 파일을 엽니다. Start 및 Stop 메서드를 구현합니다.

    Public Sub Start()
        ' The MarqueeControl may contain any number of 
        ' controls that implement IMarqueeWidget, so 
        ' find each IMarqueeWidget child and call its
        ' StartMarquee method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StartMarquee()
            End If
        Next cntrl
    End Sub
    
    
    Public Sub [Stop]()
        ' The MarqueeControl may contain any number of 
        ' controls that implement IMarqueeWidget, so find
        ' each IMarqueeWidget child and call its StopMarquee
        ' method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StopMarquee()
            End If
        Next cntrl
    End Sub
    
    public void Start()
    {
        // The MarqueeControl may contain any number of 
        // controls that implement IMarqueeWidget, so 
        // find each IMarqueeWidget child and call its
        // StartMarquee method.
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StartMarquee();
            }
        }
    }
    
    public void Stop()
    {
        // The MarqueeControl may contain any number of 
        // controls that implement IMarqueeWidget, so find
        // each IMarqueeWidget child and call its StopMarquee
        // method.
        foreach( Control cntrl in this.Controls )
        {
            if( cntrl is IMarqueeWidget )
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StopMarquee();
            }
        }
    }
    
  2. OnLayout 메서드를 다시 정의합니다.

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

사용자 지정 컨트롤의 자식 컨트롤 만들기

MarqueeControl은 MarqueeBorder 컨트롤과 MarqueeText 컨트롤 등 두 종류의 자식 컨트롤을 호스팅합니다.

  • MarqueeBorder: 이 컨트롤은 "조명"의 가장자리 둘레에 테두리를 그립니다. 조명은 차례로 깜박이므로 조명이 테두리를 따라 움직이는 것처럼 보입니다. 조명이 깜박이는 속도는 UpdatePeriod라는 속성으로 제어됩니다. 몇 가지 다른 사용자 지정 속성에 따라 컨트롤 모양의 다른 부분이 결정됩니다. StartMarquee와 StopMarquee라는 두 가지 메서드는 애니메이션이 시작되고 중지되는 시점을 제어합니다.

  • MarqueeText: 이 컨트롤은 깜박이는 문자열을 그립니다. MarqueeBorder 컨트롤과 마찬가지로 텍스트가 깜박이는 속도는 UpdatePeriod 속성으로 제어됩니다. MarqueeText 컨트롤에도 MarqueeBorder 컨트롤과 마찬가지로 StartMarquee 및 StopMarquee 메서드가 있습니다.

디자인 타임에 MarqueeControlRootDesigner에서 이 두 컨트롤 형식을 원하는 조합으로 MarqueeControl에 추가할 수 있습니다.

두 컨트롤의 공통 기능은 IMarqueeWidget이라는 인터페이스로 구성됩니다. 따라서 MarqueeControl은 움직이는 텍스트 관련 자식 컨트롤을 모두 찾아 특별하게 처리합니다.

주기적인 애니메이션 기능을 구현하려면 System.ComponentModel 네임스페이스의 BackgroundWorker 개체를 사용합니다. Timer 개체를 사용할 수 있지만 IMarqueeWidget 개체가 많을 때는 단일 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 인터페이스에 추가하여 움직이는 텍스트 애니메이션을 조작하는 두 개의 메서드와 한 개의 속성을 노출합니다.

    ' This interface defines the contract for any class that is to
    ' be used in constructing a MarqueeControl.
    Public Interface IMarqueeWidget
    
       ' This method starts the animation. If the control can 
       ' contain other classes that implement IMarqueeWidget as
       ' children, the control should call StartMarquee on all
       ' its IMarqueeWidget child controls.
       Sub StartMarquee()
    
       ' This method stops the animation. If the control can 
       ' contain other classes that implement IMarqueeWidget as
       ' children, the control should call StopMarquee on all
       ' its IMarqueeWidget child controls.
       Sub StopMarquee()
    
       ' This method specifies the refresh rate for the animation,
       ' in milliseconds.
       Property UpdatePeriod() As Integer
    
    End Interface
    
    // This interface defines the contract for any class that is to
    // be used in constructing a MarqueeControl.
    public interface IMarqueeWidget
    {
        // This method starts the animation. If the control can 
        // contain other classes that implement IMarqueeWidget as
        // children, the control should call StartMarquee on all
        // its IMarqueeWidget child controls.
        void StartMarquee();
    
        // This method stops the animation. If the control can 
        // contain other classes that implement IMarqueeWidget as
        // children, the control should call StopMarquee on all
        // its IMarqueeWidget child controls.
        void StopMarquee();
    
        // This method specifies the refresh rate for the animation,
        // in milliseconds.
        int UpdatePeriod
        {
            get;
            set;
        }
    }
    
  4. MarqueeControlLibrary 프로젝트에 새 사용자 지정 컨트롤 항목을 추가합니다. 새 소스 파일의 기본 이름을 "MarqueeText"로 지정합니다.

  5. 도구 상자의 BackgroundWorker 구성 요소를 MarqueeText 컨트롤로 끌어 옵니다. 이 구성 요소를 통해 MarqueeText 컨트롤이 비동기적으로 직접 업데이트될 수 있습니다.

  6. 속성 창에서 BackgroundWorker 구성 요소의 WorkerReportsProgess 및 WorkerSupportsCancellation 속성을 true로 설정합니다. 이러한 설정을 통해 BackgroundWorker 구성 요소는 ProgressChanged 이벤트를 주기적으로 발생시키고 비동기 업데이트를 취소할 수 있습니다. 자세한 내용은 BackgroundWorker 구성 요소를 참조하십시오.

  7. 코드 편집기에서 MarqueeText 소스 파일을 엽니다. 파일의 맨 위에서 다음과 같은 네임스페이스를 가져옵니다.

    Imports System
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Drawing
    Imports System.Threading
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
    using System;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Drawing;
    using System.Threading;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
  8. Label에서 상속하고 IMarqueeWidget 인터페이스를 구현하도록 MarqueeText 선언을 다음과 같이 변경합니다.

    <ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", _
    ToolboxItemFilterType.Require)> _
    Partial Public Class MarqueeText
        Inherits Label
        Implements IMarqueeWidget
    
    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeText", ToolboxItemFilterType.Require)]
    public partial class MarqueeText : Label, IMarqueeWidget
    {
    
  9. 노출된 속성에 해당하는 인스턴스 변수를 선언하고 해당 생성자에서 초기화합니다. isLit 필드는 텍스트를 LightColor 속성에 지정된 색으로 그릴지 여부를 결정합니다.

    ' When isLit is true, the text is painted in the light color;
    ' When isLit is false, the text is painted in the dark color.
    ' This value changes whenever the BackgroundWorker component
    ' raises the ProgressChanged event.
    Private isLit As Boolean = True
    
    ' These fields back the public properties.
    Private updatePeriodValue As Integer = 50
    Private lightColorValue As Color
    Private darkColorValue As Color
    
    ' These brushes are used to paint the light and dark
    ' colors of the text.
    Private lightBrush As Brush
    Private darkBrush As Brush
    
    ' This component updates the control asynchronously.
    Private WithEvents backgroundWorker1 As BackgroundWorker
    
    
    Public Sub New()
        ' This call is required by the Windows.Forms Form Designer.
        InitializeComponent()
    
        ' Initialize light and dark colors 
        ' to the control's default values.
        Me.lightColorValue = Me.ForeColor
        Me.darkColorValue = Me.BackColor
        Me.lightBrush = New SolidBrush(Me.lightColorValue)
        Me.darkBrush = New SolidBrush(Me.darkColorValue)
    End Sub 'New
    
    // When isLit is true, the text is painted in the light color;
    // When isLit is false, the text is painted in the dark color.
    // This value changes whenever the BackgroundWorker component
    // raises the ProgressChanged event.
    private bool isLit = true;
    
    // These fields back the public properties.
    private int updatePeriodValue = 50;
    private Color lightColorValue;
    private Color darkColorValue;
    
    // These brushes are used to paint the light and dark
    // colors of the text.
    private Brush lightBrush;
    private Brush darkBrush;
    
    // This component updates the control asynchronously.
    private BackgroundWorker backgroundWorker1;
    
    public MarqueeText()
    {
        // This call is required by the Windows.Forms Form Designer.
        InitializeComponent();
    
        // Initialize light and dark colors 
        // to the control's default values.
        this.lightColorValue = this.ForeColor;
        this.darkColorValue = this.BackColor;
        this.lightBrush = new SolidBrush(this.lightColorValue);
        this.darkBrush = new SolidBrush(this.darkColorValue);
    }
    
  10. IMarqueeWidget 인터페이스를 구현합니다.

    StartMarquee 및 StopMarquee 메서드는 BackgroundWorker 구성 요소의 RunWorkerAsyncCancelAsync 메서드를 호출하여 애니메이션을 시작하고 중지합니다.

    CategoryBrowsable 특성은 UpdatePeriod 속성에 적용되므로 속성 창의 "움직이는 텍스트"라는 사용자 지정 구역에 나타납니다.

    Public Overridable Sub StartMarquee() _
    Implements IMarqueeWidget.StartMarquee
        ' Start the updating thread and pass it the UpdatePeriod.
        Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod)
    End Sub
    
    Public Overridable Sub StopMarquee() _
    Implements IMarqueeWidget.StopMarquee
        ' Stop the updating thread.
        Me.backgroundWorker1.CancelAsync()
    End Sub
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property UpdatePeriod() As Integer _
    Implements IMarqueeWidget.UpdatePeriod
    
        Get
            Return Me.updatePeriodValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 Then
                Me.updatePeriodValue = Value
            Else
                Throw New ArgumentOutOfRangeException("UpdatePeriod", "must be > 0")
            End If
        End Set
    
    End Property
    
    public virtual void StartMarquee()
    {
        // Start the updating thread and pass it the UpdatePeriod.
        this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod);
    }
    
    public virtual void StopMarquee()
    {
        // Stop the updating thread.
        this.backgroundWorker1.CancelAsync();
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public int UpdatePeriod
    {
        get
        {
            return this.updatePeriodValue;
        }
    
        set
        {
            if (value > 0)
            {
                this.updatePeriodValue = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0");
            }
        }
    }
    
  11. 속성 접근자를 구현합니다. LightColor와 DarkColor 등 두 가지 속성을 클라이언트에 노출합니다. CategoryBrowsable 특성은 이러한 속성에 적용되므로 속성 창의 "움직이는 텍스트"라는 사용자 지정 구역에 속성이 나타납니다.

    <Category("Marquee"), Browsable(True)> _
    Public Property LightColor() As Color
    
        Get
            Return Me.lightColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The LightColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then
                Me.lightColorValue = Value
                Me.lightBrush = New SolidBrush(Value)
            End If
        End Set
    
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property DarkColor() As Color
    
        Get
            Return Me.darkColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The DarkColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then
                Me.darkColorValue = Value
                Me.darkBrush = New SolidBrush(Value)
            End If
        End Set
    
    End Property
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color LightColor
    {
        get
        {
            return this.lightColorValue;
        }
        set
        {
            // The LightColor property is only changed if the 
            // client provides a different value. Comparing values 
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.lightColorValue.ToArgb() != value.ToArgb())
            {
                this.lightColorValue = value;
                this.lightBrush = new SolidBrush(value);
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color DarkColor
    {
        get
        {
            return this.darkColorValue;
        }
        set
        {
            // The DarkColor property is only changed if the 
            // client provides a different value. Comparing values 
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.darkColorValue.ToArgb() != value.ToArgb())
            {
                this.darkColorValue = value;
                this.darkBrush = new SolidBrush(value);
            }
        }
    }
    
  12. BackgroundWorker 구성 요소의 DoWorkProgressChanged 이벤트에 대한 처리기를 구현합니다.

    코드에서 CancelAsync를 호출하여 애니메이션을 중지할 때까지 DoWork 이벤트 처리기는 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 Sub backgroundWorker1_DoWork( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.DoWorkEventArgs) _
    Handles backgroundWorker1.DoWork
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
    
        ' This event handler will run until the client cancels
        ' the background task by calling CancelAsync.
        While Not worker.CancellationPending
            ' The Argument property of the DoWorkEventArgs
            ' object holds the value of UpdatePeriod, which 
            ' was passed as the argument to the RunWorkerAsync
            ' method. 
            Thread.Sleep(Fix(e.Argument))
    
            ' The DoWork eventhandler does not actually report
            ' progress; the ReportProgress event is used to 
            ' periodically alert the control to update its state.
            worker.ReportProgress(0)
        End While
    End Sub
    
    
    ' The ProgressChanged event is raised by the DoWork method.
    ' This event handler does work that is internal to the
    ' control. In this case, the text is toggled between its
    ' light and dark state, and the control is told to 
    ' repaint itself.
    Private Sub backgroundWorker1_ProgressChanged( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
    Handles backgroundWorker1.ProgressChanged
        Me.isLit = Not Me.isLit
        Me.Refresh()
    End Sub
    
    // This method is called in the worker thread's context, 
    // so it must not make any calls into the MarqueeText control.
    // Instead, it communicates to the control using the 
    // ProgressChanged event.
    //
    // The only work done in this event handler is
    // to sleep for the number of milliseconds specified 
    // by UpdatePeriod, then raise the ProgressChanged event.
    private void backgroundWorker1_DoWork(
        object sender,
        System.ComponentModel.DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;
    
        // This event handler will run until the client cancels
        // the background task by calling CancelAsync.
        while (!worker.CancellationPending)
        {
            // The Argument property of the DoWorkEventArgs
            // object holds the value of UpdatePeriod, which 
            // was passed as the argument to the RunWorkerAsync
            // method. 
            Thread.Sleep((int)e.Argument);
    
            // The DoWork eventhandler does not actually report
            // progress; the ReportProgress event is used to 
            // periodically alert the control to update its state.
            worker.ReportProgress(0);
        }
    }
    
    // The ProgressChanged event is raised by the DoWork method.
    // This event handler does work that is internal to the
    // control. In this case, the text is toggled between its
    // light and dark state, and the control is told to 
    // repaint itself.
    private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
    {
        this.isLit = !this.isLit;
        this.Refresh();
    }
    
    
  13. 애니메이션을 사용하도록 OnPaint 메서드를 재정의합니다.

    Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
        ' The text is painted in the light or dark color,
        ' depending on the current value of isLit.
        Me.ForeColor = IIf(Me.isLit, Me.lightColorValue, Me.darkColorValue)
    
        MyBase.OnPaint(e)
    End Sub
    
    protected override void OnPaint(PaintEventArgs e)
    {
        // The text is painted in the light or dark color,
        // depending on the current value of isLit.
        this.ForeColor =
            this.isLit ? this.lightColorValue : this.darkColorValue;
    
        base.OnPaint(e);
    }
    
  14. F6 키를 눌러 솔루션을 빌드합니다.

MarqueeBorder 자식 컨트롤 만들기

MarqueeBorder 컨트롤은 MarqueeText 컨트롤보다 약간 더 복잡합니다. 속성이 더 많으며 OnPaint 메서드에 애니메이션이 더 많이 사용되지만 원칙적으로 MarqueeText 컨트롤과 매우 비슷합니다.

MarqueeBorder 컨트롤은 자식 컨트롤을 가질 수 있으므로 Layout 이벤트를 인식해야 합니다.

MarqueeBorder 컨트롤을 만들려면

  1. MarqueeControlLibrary 프로젝트에 새 사용자 지정 컨트롤 항목을 추가합니다. 새 소스 파일의 기본 이름을 "MarqueeBorder"로 지정합니다.

  2. 도구 상자의 BackgroundWorker 구성 요소를 MarqueeBorder 컨트롤로 끌어 옵니다. 이 구성 요소를 통해 MarqueeBorder 컨트롤이 비동기적으로 직접 업데이트될 수 있습니다.

  3. 속성 창에서 BackgroundWorker 구성 요소의 WorkerReportsProgess 및 WorkerSupportsCancellation 속성을 true로 설정합니다. 이러한 설정을 통해 BackgroundWorker 구성 요소는 ProgressChanged 이벤트를 주기적으로 발생시키고 비동기 업데이트를 취소할 수 있습니다. 자세한 내용은 BackgroundWorker 구성 요소를 참조하십시오.

  4. 속성 창에서 이벤트 단추를 클릭합니다. DoWorkProgressChanged 이벤트에 처리기를 연결합니다.

  5. 코드 편집기에서 MarqueeBorder 소스 파일을 엽니다. 파일의 맨 위에서 다음과 같은 네임스페이스를 가져옵니다.

    Imports System
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Drawing
    Imports System.Drawing.Design
    Imports System.Threading
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
    using System;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Drawing;
    using System.Drawing.Design;
    using System.Threading;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
  6. Panel에서 상속하고 IMarqueeWidget 인터페이스를 구현하도록 MarqueeBorder 선언을 다음과 같이 변경합니다.

    <Designer(GetType(MarqueeControlLibrary.Design.MarqueeBorderDesigner)), _
    ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", _
    ToolboxItemFilterType.Require)> _
    Partial Public Class MarqueeBorder
        Inherits Panel
        Implements IMarqueeWidget
    
    [Designer(typeof(MarqueeControlLibrary.Design.MarqueeBorderDesigner ))]
    [ToolboxItemFilter("MarqueeControlLibrary.MarqueeBorder", ToolboxItemFilterType.Require)]
    public partial class MarqueeBorder : Panel, IMarqueeWidget
    {
    
  7. MarqueeBorder 컨트롤의 상태를 관리하기 위한 두 개의 열거형, 즉 테두리를 따라 조명이 "회전"되는 방향을 결정하는 MarqueeSpinDirection과 조명의 모양(사각형 또는 원형)을 결정하는 MarqueeLightShape를 선언합니다. 이러한 선언을 MarqueeBorder 클래스 선언보다 먼저 지정합니다.

    ' This defines the possible values for the MarqueeBorder
    ' control's SpinDirection property.
    Public Enum MarqueeSpinDirection
       CW
       CCW
    End Enum
    
    ' This defines the possible values for the MarqueeBorder
    ' control's LightShape property.
    Public Enum MarqueeLightShape
        Square
        Circle
    End Enum
    
    // This defines the possible values for the MarqueeBorder
    // control's SpinDirection property.
    public enum MarqueeSpinDirection
    {
        CW,
        CCW
    }
    
    // This defines the possible values for the MarqueeBorder
    // control's LightShape property.
    public enum MarqueeLightShape
    {
        Square,
        Circle
    }
    
  8. 노출된 속성에 해당하는 인스턴스 변수를 선언하고 해당 생성자에서 초기화합니다.

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

    StartMarquee 및 StopMarquee 메서드는 BackgroundWorker 구성 요소의 RunWorkerAsyncCancelAsync 메서드를 호출하여 애니메이션을 시작하고 중지합니다.

    MarqueeBorder 컨트롤에는 자식 컨트롤이 포함될 수 있으므로 StartMarquee 메서드는 모든 자식 컨트롤을 열거하고 IMarqueeWidget을 구현하는 해당 자식 컨트롤에 대해 StartMarquee를 호출합니다. StopMarquee 메서드에도 비슷한 구현이 사용됩니다.

    Public Overridable Sub StartMarquee() _
    Implements IMarqueeWidget.StartMarquee
        ' The MarqueeBorder control may contain any number of 
        ' controls that implement IMarqueeWidget, so find
        ' each IMarqueeWidget child and call its StartMarquee
        ' method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StartMarquee()
            End If
        Next cntrl
    
        ' Start the updating thread and pass it the UpdatePeriod.
        Me.backgroundWorker1.RunWorkerAsync(Me.UpdatePeriod)
    End Sub
    
    
    Public Overridable Sub StopMarquee() _
    Implements IMarqueeWidget.StopMarquee
        ' The MarqueeBorder control may contain any number of 
        ' controls that implement IMarqueeWidget, so find
        ' each IMarqueeWidget child and call its StopMarquee
        ' method.
        Dim cntrl As Control
        For Each cntrl In Me.Controls
            If TypeOf cntrl Is IMarqueeWidget Then
                Dim widget As IMarqueeWidget = CType(cntrl, IMarqueeWidget)
    
                widget.StopMarquee()
            End If
        Next cntrl
    
        ' Stop the updating thread.
        Me.backgroundWorker1.CancelAsync()
    End Sub
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Overridable Property UpdatePeriod() As Integer _
    Implements IMarqueeWidget.UpdatePeriod
    
        Get
            Return Me.updatePeriodValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 Then
                Me.updatePeriodValue = Value
            Else
                Throw New ArgumentOutOfRangeException("UpdatePeriod", _
                "must be > 0")
            End If
        End Set
    
    End Property
    
    public virtual void StartMarquee()
    {
        // The MarqueeBorder control may contain any number of 
        // controls that implement IMarqueeWidget, so find
        // each IMarqueeWidget child and call its StartMarquee
        // method.
        foreach (Control cntrl in this.Controls)
        {
            if (cntrl is IMarqueeWidget)
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StartMarquee();
            }
        }
    
        // Start the updating thread and pass it the UpdatePeriod.
        this.backgroundWorker1.RunWorkerAsync(this.UpdatePeriod);
    }
    
    public virtual void StopMarquee()
    {
        // The MarqueeBorder control may contain any number of 
        // controls that implement IMarqueeWidget, so find
        // each IMarqueeWidget child and call its StopMarquee
        // method.
        foreach (Control cntrl in this.Controls)
        {
            if (cntrl is IMarqueeWidget)
            {
                IMarqueeWidget widget = cntrl as IMarqueeWidget;
                widget.StopMarquee();
            }
        }
    
        // Stop the updating thread.
        this.backgroundWorker1.CancelAsync();
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public virtual int UpdatePeriod
    {
        get
        {
            return this.updatePeriodValue;
        }
    
        set
        {
            if (value > 0)
            {
                this.updatePeriodValue = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("UpdatePeriod", "must be > 0");
            }
        }
    }
    
    
  10. 속성 접근자를 구현합니다. MarqueeBorder 컨트롤에는 이 컨트롤의 모양을 제어하는 속성이 몇 가지 있습니다.

    <Category("Marquee"), Browsable(True)> _
    Public Property LightSize() As Integer
        Get
            Return Me.lightSizeValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 AndAlso Value <= MaxLightSize Then
                Me.lightSizeValue = Value
                Me.DockPadding.All = 2 * Value
            Else
                Throw New ArgumentOutOfRangeException("LightSize", _
                "must be > 0 and < MaxLightSize")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightPeriod() As Integer
        Get
            Return Me.lightPeriodValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value > 0 Then
                Me.lightPeriodValue = Value
            Else
                Throw New ArgumentOutOfRangeException("LightPeriod", _
                "must be > 0 ")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightColor() As Color
        Get
            Return Me.lightColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The LightColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.lightColorValue.ToArgb() <> Value.ToArgb() Then
                Me.lightColorValue = Value
                Me.lightBrush = New SolidBrush(Value)
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property DarkColor() As Color
        Get
            Return Me.darkColorValue
        End Get
    
        Set(ByVal Value As Color)
            ' The DarkColor property is only changed if the 
            ' client provides a different value. Comparing values 
            ' from the ToArgb method is the recommended test for
            ' equality between Color structs.
            If Me.darkColorValue.ToArgb() <> Value.ToArgb() Then
                Me.darkColorValue = Value
                Me.darkBrush = New SolidBrush(Value)
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property LightSpacing() As Integer
        Get
            Return Me.lightSpacingValue
        End Get
    
        Set(ByVal Value As Integer)
            If Value >= 0 Then
                Me.lightSpacingValue = Value
            Else
                Throw New ArgumentOutOfRangeException("LightSpacing", _
                "must be >= 0")
            End If
        End Set
    End Property
    
    
    <Category("Marquee"), Browsable(True), _
    EditorAttribute(GetType(LightShapeEditor), _
    GetType(System.Drawing.Design.UITypeEditor))> _
    Public Property LightShape() As MarqueeLightShape
    
        Get
            Return Me.lightShapeValue
        End Get
    
        Set(ByVal Value As MarqueeLightShape)
            Me.lightShapeValue = Value
        End Set
    
    End Property
    
    
    <Category("Marquee"), Browsable(True)> _
    Public Property SpinDirection() As MarqueeSpinDirection
    
        Get
            Return Me.spinDirectionValue
        End Get
    
        Set(ByVal Value As MarqueeSpinDirection)
            Me.spinDirectionValue = Value
        End Set
    
    End Property
    
    [Category("Marquee")]
    [Browsable(true)]
    public int LightSize
    {
        get
        {
            return this.lightSizeValue;
        }
    
        set
        {
            if (value > 0 && value <= MaxLightSize)
            {
                this.lightSizeValue = value;
                this.DockPadding.All = 2 * value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("LightSize", "must be > 0 and < MaxLightSize");
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public int LightPeriod
    {
        get
        {
            return this.lightPeriodValue;
        }
    
        set
        {
            if (value > 0)
            {
                this.lightPeriodValue = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("LightPeriod", "must be > 0 ");
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color LightColor
    {
        get
        {
            return this.lightColorValue;
        }
    
        set
        {
            // The LightColor property is only changed if the 
            // client provides a different value. Comparing values 
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.lightColorValue.ToArgb() != value.ToArgb())
            {
                this.lightColorValue = value;
                this.lightBrush = new SolidBrush(value);
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public Color DarkColor
    {
        get
        {
            return this.darkColorValue;
        }
    
        set
        {
            // The DarkColor property is only changed if the 
            // client provides a different value. Comparing values 
            // from the ToArgb method is the recommended test for
            // equality between Color structs.
            if (this.darkColorValue.ToArgb() != value.ToArgb())
            {
                this.darkColorValue = value;
                this.darkBrush = new SolidBrush(value);
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public int LightSpacing
    {
        get
        {
            return this.lightSpacingValue;
        }
    
        set
        {
            if (value >= 0)
            {
                this.lightSpacingValue = value;
            }
            else
            {
                throw new ArgumentOutOfRangeException("LightSpacing", "must be >= 0");
            }
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    [EditorAttribute(typeof(LightShapeEditor), 
         typeof(System.Drawing.Design.UITypeEditor))]
    public MarqueeLightShape LightShape
    {
        get
        {
            return this.lightShapeValue;
        }
    
        set
        {
            this.lightShapeValue = value;
        }
    }
    
    [Category("Marquee")]
    [Browsable(true)]
    public MarqueeSpinDirection SpinDirection
    {
        get
        {
            return this.spinDirectionValue;
        }
    
        set
        {
            this.spinDirectionValue = value;
        }
    }
    
    
  11. BackgroundWorker 구성 요소의 DoWorkProgressChanged 이벤트에 대한 처리기를 구현합니다.

    코드에서 CancelAsync를 호출하여 애니메이션을 중지할 때까지 DoWork 이벤트 처리기는 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 Sub backgroundWorker1_DoWork( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.DoWorkEventArgs) _
    Handles backgroundWorker1.DoWork
        Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
    
        ' This event handler will run until the client cancels
        ' the background task by calling CancelAsync.
        While Not worker.CancellationPending
            ' The Argument property of the DoWorkEventArgs
            ' object holds the value of UpdatePeriod, which 
            ' was passed as the argument to the RunWorkerAsync
            ' method. 
            Thread.Sleep(Fix(e.Argument))
    
            ' The DoWork eventhandler does not actually report
            ' progress; the ReportProgress event is used to 
            ' periodically alert the control to update its state.
            worker.ReportProgress(0)
        End While
    End Sub
    
    
    ' The ProgressChanged event is raised by the DoWork method.
    ' This event handler does work that is internal to the
    ' control. In this case, the currentOffset is incremented,
    ' and the control is told to repaint itself.
    Private Sub backgroundWorker1_ProgressChanged( _
    ByVal sender As Object, _
    ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
    Handles backgroundWorker1.ProgressChanged
        Me.currentOffset += 1
        Me.Refresh()
    End Sub
    
    // This method is called in the worker thread's context, 
    // so it must not make any calls into the MarqueeBorder
    // control. Instead, it communicates to the control using 
    // the ProgressChanged event.
    //
    // The only work done in this event handler is
    // to sleep for the number of milliseconds specified 
    // by UpdatePeriod, then raise the ProgressChanged event.
    private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
    {
        BackgroundWorker worker = sender as BackgroundWorker;
    
        // This event handler will run until the client cancels
        // the background task by calling CancelAsync.
        while (!worker.CancellationPending)
        {
            // The Argument property of the DoWorkEventArgs
            // object holds the value of UpdatePeriod, which 
            // was passed as the argument to the RunWorkerAsync
            // method. 
            Thread.Sleep((int)e.Argument);
    
            // The DoWork eventhandler does not actually report
            // progress; the ReportProgress event is used to 
            // periodically alert the control to update its state.
            worker.ReportProgress(0);
        }
    }
    
    // The ProgressChanged event is raised by the DoWork method.
    // This event handler does work that is internal to the
    // control. In this case, the currentOffset is incremented,
    // and the control is told to repaint itself.
    private void backgroundWorker1_ProgressChanged(
        object sender,
        System.ComponentModel.ProgressChangedEventArgs e)
    {
        this.currentOffset++;
        this.Refresh();
    }
    
  12. 도우미 메서드인 IsLit와 DrawLight를 구현합니다.

    IsLit 메서드는 지정된 위치의 조명 색을 결정합니다. "밝아진" 조명은 LightColor 속성에 지정된 색으로 그려지고 "어두워진" 조명은 DarkColor 속성에 지정된 색으로 그려집니다.

    DrawLight 메서드는 적절한 색, 모양, 위치를 사용하여 밝기를 나타냅니다.

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

    OnPaint 메서드는 MarqueeBorder 컨트롤의 가장자리를 따라 조명을 나타냅니다.

    OnPaint 메서드는 MarqueeBorder 컨트롤의 차원에 따라 좌우되므로 레이아웃이 바뀔 때마다 이 메서드를 호출해야 합니다. 이렇게 하려면 OnLayout을 재정의하고 Refresh를 호출합니다.

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

사용자 지정 디자이너를 만들어 속성 숨기기 및 필터링

MarqueeControlRootDesigner 클래스는 루트 디자이너의 구현을 제공합니다. MarqueeControl에서 작동하는 이 디자이너 외에도 특별히 MarqueeBorder 컨트롤에 연결되는 사용자 지정 디자이너가 필요합니다. 이 디자이너는 사용자 지정 루트 디자이너의 컨텍스트에서 적합한 사용자 지정 동작을 제공합니다.

특히 MarqueeBorderDesigner는 MarqueeBorder 컨트롤의 특정 속성을 "숨기고" 필터링하여 디자인 환경과의 상호 작용을 변경합니다.

구성 요소의 속성 접근자에 대한 호출을 차단하는 것을 "숨김"이라고 합니다. 디자이너는 숨김을 통해 사용자가 설정한 값을 추적하고 필요할 경우 디자인할 구성 요소에 이 값을 전달할 수 있습니다.

예를 들어, VisibleEnabled 속성은 MarqueeBorderDesigner에 의해 숨겨지며, 이로써 디자인 타임 중에 사용자가 MarqueeBorder 컨트롤을 표시하지 않거나 비활성화할 수 없습니다.

디자이너는 속성을 추가하고 제거할 수도 있습니다. 예를 들어, MarqueeBorder 컨트롤은 LightSize 속성에 지정된 조명의 크기를 기준으로 채우기를 프로그래밍 방식으로 설정하므로 Padding 속성은 디자인 타임에 제거됩니다.

MarqueeBorderDesigner의 기본 클래스는 디자인 타임에 컨트롤에 의해 노출되는 특성, 속성 및 이벤트를 변경할 수 있는 메서드가 있는 ComponentDesigner입니다.

이러한 메서드를 사용하여 구성 요소의 공용 인터페이스를 변경할 때는 다음과 같은 규칙을 따라야 합니다.

  • PreFilter 메서드의 항목만 추가하거나 제거합니다.

  • PostFilter 메서드의 기존 항목만 수정합니다.

  • 기본 구현을 항상 PreFilter 메서드에서 처음 호출합니다.

  • 기본 구현을 항상 PostFilter 메서드에서 마지막으로 호출합니다.

이러한 규칙을 따르면 디자인 타임 환경의 모든 디자이너에서 디자인되는 모든 구성 요소를 일관된 방식으로 볼 수 있습니다.

ComponentDesigner 클래스는 숨겨진 속성 값을 관리하는 데 필요한 사전을 제공하므로 특정 인스턴스 변수를 만들지 않아도 됩니다.

사용자 지정 디자이너를 만들어 속성을 숨기고 필터링하려면

  1. Design 폴더를 마우스 오른쪽 단추로 클릭하고 새 클래스를 추가합니다. 소스 파일의 기본 이름을 "MarqueeBorderDesigner"로 지정합니다.

  2. 코드 편집기에서 MarqueeBorderDesigner 소스 파일을 엽니다. 파일의 맨 위에서 다음과 같은 네임스페이스를 가져옵니다.

    Imports System
    Imports System.Collections
    Imports System.ComponentModel
    Imports System.ComponentModel.Design
    Imports System.Diagnostics
    Imports System.Windows.Forms
    Imports System.Windows.Forms.Design
    
    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
  3. ParentControlDesigner에서 상속하도록 MarqueeBorderDesigner 선언을 변경합니다.

    MarqueeBorder 컨트롤에는 자식 컨트롤이 포함될 수 있으므로 MarqueeBorderDesigner는 부모-자식 상호 작용을 처리하는 ParentControlDesigner에서 상속됩니다.

    Namespace MarqueeControlLibrary.Design
    
        <System.Security.Permissions.PermissionSetAttribute(System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _
        Public Class MarqueeBorderDesigner
            Inherits ParentControlDesigner
    
    namespace MarqueeControlLibrary.Design
    {
        [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] 
        public class MarqueeBorderDesigner : ParentControlDesigner
        {
    
  4. PreFilterProperties의 기본 구현을 재정의합니다.

    Protected Overrides Sub PreFilterProperties( _
    ByVal properties As IDictionary)
    
        MyBase.PreFilterProperties(properties)
    
        If properties.Contains("Padding") Then
            properties.Remove("Padding")
        End If
    
        properties("Visible") = _
        TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _
        CType(properties("Visible"), PropertyDescriptor), _
        New Attribute(-1) {})
    
        properties("Enabled") = _
        TypeDescriptor.CreateProperty(GetType(MarqueeBorderDesigner), _
        CType(properties("Enabled"), _
        PropertyDescriptor), _
        New Attribute(-1) {})
    
    End Sub
    
    protected override void PreFilterProperties(IDictionary properties)
    {
        base.PreFilterProperties(properties);
    
        if (properties.Contains("Padding"))
        {
            properties.Remove("Padding");
        }
    
        properties["Visible"] = TypeDescriptor.CreateProperty(
            typeof(MarqueeBorderDesigner),
            (PropertyDescriptor)properties["Visible"],
            new Attribute[0]);
    
        properties["Enabled"] = TypeDescriptor.CreateProperty(
            typeof(MarqueeBorderDesigner),
            (PropertyDescriptor)properties["Enabled"],
            new Attribute[0]);
    }
    
  5. EnabledVisible 속성을 구현합니다. 이러한 구현에서는 컨트롤의 속성이 숨겨집니다.

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

구성 요소의 변경 내용 처리

MarqueeControlRootDesigner 클래스는 MarqueeControl 인스턴스에 사용자 지정 디자인 타임 환경을 제공합니다. 대부분의 디자인 타임 기능이 DocumentDesigner 클래스에서 상속되므로 코드에는 구성 요소의 변경 내용 처리와 디자이너 동사 추가 등 두 가지의 구체적인 사용자 지정이 구현됩니다.

사용자가 MarqueeControl 인스턴스를 디자인하면 루트 디자이너에서 MarqueeControl과 해당 자식 컨트롤의 변경 내용을 추적합니다. 디자인 타임 환경에는 구성 요소의 상태 변경을 추적할 수 있는 편리한 서비스인 IComponentChangeService가 제공됩니다.

GetService 메서드를 사용하여 환경에 대해 쿼리하면 이 서비스에 대한 참조를 가져올 수 있습니다. 쿼리에 성공하면 디자이너가 ComponentChanged 이벤트에 대한 처리기를 연결하고 디자인 타임에 일관된 상태를 유지하는 데 필요한 모든 작업을 수행할 수 있습니다.

MarqueeControlRootDesigner 클래스의 경우 MarqueeControl에 포함된 각 IMarqueeWidget 개체에 대해 Refresh를 호출합니다. 이렇게 하면 IMarqueeWidget 개체가 해당 부모의 Size와 같은 속성이 변경될 경우 적절히 다시 그려집니다.

구성 요소의 변경 내용을 처리하려면

  1. 코드 편집기에서 MarqueeControlRootDesigner 소스 파일을 열고 Initialize 메서드를 재정의합니다. Initialize의 기본 구현을 호출하고 IComponentChangeService를 쿼리합니다.

    MyBase.Initialize(component)
    
    Dim cs As IComponentChangeService = _
    CType(GetService(GetType(IComponentChangeService)), _
    IComponentChangeService)
    
    If (cs IsNot Nothing) Then
        AddHandler cs.ComponentChanged, AddressOf OnComponentChanged
    End If
    
    base.Initialize(component);
    
    IComponentChangeService cs =
        GetService(typeof(IComponentChangeService)) 
        as IComponentChangeService;
    
    if (cs != null)
    {
        cs.ComponentChanged +=
            new ComponentChangedEventHandler(OnComponentChanged);
    }
    
  2. OnComponentChanged 이벤트 처리기를 구현합니다. 보내는 구성 요소의 형식을 테스트하고 IMarqueeWidget이면 해당 Refresh 메서드를 호출합니다.

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

사용자 지정 디자이너에 디자이너 동사 추가

디자이너 동사는 이벤트 처리기에 연결된 메뉴 명령입니다. 디자인 타임에 구성 요소의 바로 가기 메뉴에 디자이너 동사가 추가됩니다. 자세한 내용은 DesignerVerb를 참조하십시오.

테스트 실행과 테스트 중지 등 두 개의 디자이너 동사를 디자이너에 추가합니다. 이러한 동사를 사용하면 디자인 타임에 MarqueeControl의 런타임 동작을 볼 수 있습니다. 이러한 동사는 MarqueeControlRootDesigner에 추가됩니다.

테스트 실행이 호출되면 동사 이벤트 처리기가 MarqueeControl에서 StartMarquee 메서드를 호출합니다. 테스트 중지가 호출되면 동사 이벤트 처리기가 MarqueeControl에서 StopMarquee 메서드를 호출합니다. StartMarquee 및 StopMarquee 메서드 구현에서는 IMarqueeWidget을 구현하는 포함된 컨트롤에서 이러한 메서드를 호출하므로 포함된 모든 IMarqueeWidget 컨트롤도 테스트에 참여합니다.

사용자 지정 디자이너에 디자이너 동사를 추가하려면

  1. MarqueeControlRootDesigner 클래스에서 OnVerbRunTest 및 OnVerbStopTest라는 이벤트 처리기를 추가합니다.

    Private Sub OnVerbRunTest( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Dim c As MarqueeControl = CType(Me.Control, MarqueeControl)
        c.Start()
    
    End Sub
    
    Private Sub OnVerbStopTest( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Dim c As MarqueeControl = CType(Me.Control, MarqueeControl)
        c.Stop()
    
    End Sub
    
    private void OnVerbRunTest(object sender, EventArgs e)
    {
        MarqueeControl c = this.Control as MarqueeControl;
    
        c.Start();
    }
    
    private void OnVerbStopTest(object sender, EventArgs e)
    {
        MarqueeControl c = this.Control as MarqueeControl;
    
        c.Stop();
    }
    
  2. 이러한 이벤트 처리기를 해당 디자이너 동사에 연결합니다. MarqueeControlRootDesigner는 해당 기본 클래스에서 DesignerVerbCollection을 상속합니다. 새 DesignerVerb 개체를 두 개 만들어 Initialize 메서드의 이 컬렉션에 추가합니다.

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

사용자 지정 UITypeEditor 만들기

사용자를 위한 사용자 지정 디자인 타임 환경을 만들 때는 속성 창과의 사용자 지정 상호 작용을 만드는 것이 좋은 경우가 많습니다. UITypeEditor를 만들어 이 작업을 수행할 수 있습니다. 자세한 내용은 방법: UI 형식 편집기 만들기를 참조하십시오.

MarqueeBorder 컨트롤은 몇 가지 속성을 속성 창에 노출합니다. 이러한 속성 중 MarqueeSpinDirection과 MarqueeLightShape는 열거형으로 나타냅니다. 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.
    Friend Class LightShapeEditor
        Inherits UITypeEditor
    
    // This class demonstrates the use of a custom UITypeEditor. 
    // It allows the MarqueeBorder control's LightShape property
    // to be changed at design time using a customized UI element
    // that is invoked by the Properties window. The UI is provided
    // by the LightShapeSelectionControl class.
    internal class LightShapeEditor : UITypeEditor
    {
    
  3. editorService라는 IWindowsFormsEditorService 인스턴스 변수를 선언합니다.

    Private editorService As IWindowsFormsEditorService = Nothing
    
    private IWindowsFormsEditorService editorService = null;
    
  4. GetEditStyle 메서드를 다시 정의합니다. 이 구현에서는 LightShapeEditor 표시 방법을 디자인 환경에 알려 주는 DropDown을 반환합니다.

    Public Overrides Function GetEditStyle( _
    ByVal context As System.ComponentModel.ITypeDescriptorContext) _
    As UITypeEditorEditStyle
        Return UITypeEditorEditStyle.DropDown
    End Function
    
    
    public override UITypeEditorEditStyle GetEditStyle(
    System.ComponentModel.ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.DropDown;
    }
    
  5. EditValue 메서드를 다시 정의합니다. 이 구현에서는 디자인 환경의 IWindowsFormsEditorService 개체를 쿼리합니다. 쿼리에 성공하면 LightShapeSelectionControl을 만듭니다. LightShapeEditor를 시작하기 위해 DropDownControl 메서드가 호출됩니다. 이 호출에서 반환된 값이 디자인 환경으로 반환됩니다.

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

사용자 지정 UITypeEditor에 대한 뷰 컨트롤 만들기

  1. MarqueeLightShape 속성은 두 가지 형식의 조명 모양인 Square와 Circle을 지원합니다. 속성 창에서 이러한 값을 그래픽으로 표시하는 용도로만 사용하는 사용자 지정 컨트롤을 만듭니다. 이 사용자 지정 컨트롤은 UITypeEditor에서 속성 창과 상호 작용하기 위해 사용됩니다.

UI 형식 편집기에 대한 뷰 컨트롤을 만들려면

  1. MarqueeControlLibrary 프로젝트에 새 UserControl 항목을 추가합니다. 새 소스 파일의 기본 이름을 "LightShapeSelectionControl"로 지정합니다.

  2. 도구 상자의 Panel 컨트롤 두 개를 LightShapeSelectionControl로 끌어 옵니다. 컨트롤 이름을 각각 squarePanel과 circlePanel로 지정합니다. 그런 다음 컨트롤을 나란히 정렬하고 두 Panel 컨트롤의 Size 속성을 (60, 60)으로 설정합니다. squarePanel 컨트롤의 Location 속성은 (8, 10)으로 설정하고 circlePanel 컨트롤의 Location 속성은 (80, 10)으로 설정합니다. 끝으로 LightShapeSelectionControl의 Size 속성은 (150, 80)으로 설정합니다.

  3. 코드 편집기에서 LightShapeSelectionControl 소스 파일을 엽니다. 파일의 맨 위에서 System.Windows.Forms.Design 네임스페이스를 가져옵니다.

Imports System.Windows.Forms.Design
using System.Windows.Forms.Design;
  1. squarePanel 및 circlePanel 컨트롤의 Click 이벤트 처리기를 구현합니다. 이러한 메서드는 CloseDropDown을 호출하여 사용자 지정 UITypeEditor 편집 세션을 종료합니다.

    Private Sub squarePanel_Click( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Me.lightShapeValue = MarqueeLightShape.Square
        Me.Invalidate(False)
        Me.editorService.CloseDropDown()
    
    End Sub
    
    
    Private Sub circlePanel_Click( _
    ByVal sender As Object, _
    ByVal e As EventArgs)
    
        Me.lightShapeValue = MarqueeLightShape.Circle
        Me.Invalidate(False)
        Me.editorService.CloseDropDown()
    
    End Sub
    
            private void squarePanel_Click(object sender, EventArgs e)
            {
                this.lightShapeValue = MarqueeLightShape.Square;
    
                this.Invalidate( false );
    
                this.editorService.CloseDropDown();
            }
    
            private void circlePanel_Click(object sender, EventArgs e)
            {
                this.lightShapeValue = MarqueeLightShape.Circle;
    
                this.Invalidate( false );
    
                this.editorService.CloseDropDown();
            }
    
  2. editorService라는 IWindowsFormsEditorService 인스턴스 변수를 선언합니다.

Private editorService As IWindowsFormsEditorService
private IWindowsFormsEditorService editorService;
  1. lightShapeValue라는 MarqueeLightShape 인스턴스 변수를 선언합니다.

    Private lightShapeValue As MarqueeLightShape = MarqueeLightShape.Square
    
    private MarqueeLightShape lightShapeValue = MarqueeLightShape.Square;
    
  2. LightShapeSelectionControl 생성자에서 squarePanel 및 circlePanel 컨트롤의 Click 이벤트에 Click 이벤트 처리기를 연결합니다. 또한 디자인 환경의 MarqueeLightShape 값을 lightShapeValue 필드에 할당하는 생성자 오버로드를 정의합니다.

    ' This constructor takes a MarqueeLightShape value from the
    ' design-time environment, which will be used to display
    ' the initial state.
     Public Sub New( _
     ByVal lightShape As MarqueeLightShape, _
     ByVal editorService As IWindowsFormsEditorService)
         ' This call is required by the Windows.Forms Form Designer.
         InitializeComponent()
    
         ' Cache the light shape value provided by the 
         ' design-time environment.
         Me.lightShapeValue = lightShape
    
         ' Cache the reference to the editor service.
         Me.editorService = editorService
    
         ' Handle the Click event for the two panels. 
         AddHandler Me.squarePanel.Click, AddressOf squarePanel_Click
         AddHandler Me.circlePanel.Click, AddressOf circlePanel_Click
     End Sub
    
            // This constructor takes a MarqueeLightShape value from the
            // design-time environment, which will be used to display
            // the initial state.
            public LightShapeSelectionControl( 
                MarqueeLightShape lightShape,
                IWindowsFormsEditorService editorService )
            {
                // This call is required by the designer.
                InitializeComponent();
    
                // Cache the light shape value provided by the 
                // design-time environment.
                this.lightShapeValue = lightShape;
    
                // Cache the reference to the editor service.
                this.editorService = editorService;
    
                // Handle the Click event for the two panels. 
                this.squarePanel.Click += new EventHandler(squarePanel_Click);
                this.circlePanel.Click += new EventHandler(circlePanel_Click);
            }
    
  3. Dispose 메서드에서 Click 이벤트 처리기를 분리합니다.

    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        If disposing Then
    
            ' Be sure to unhook event handlers
            ' to prevent "lapsed listener" leaks.
            RemoveHandler Me.squarePanel.Click, AddressOf squarePanel_Click
            RemoveHandler Me.circlePanel.Click, AddressOf circlePanel_Click
    
            If (components IsNot Nothing) Then
                components.Dispose()
            End If
    
        End If
        MyBase.Dispose(disposing)
    End Sub
    
            protected override void Dispose( bool disposing )
            {
                if( disposing )
                {
                    // Be sure to unhook event handlers
                    // to prevent "lapsed listener" leaks.
                    this.squarePanel.Click -= 
                        new EventHandler(squarePanel_Click);
                    this.circlePanel.Click -= 
                        new EventHandler(circlePanel_Click);
    
                    if(components != null)
                    {
                        components.Dispose();
                    }
                }
                base.Dispose( disposing );
            }
    
  4. 솔루션 탐색기에서 모든 파일 표시 단추를 클릭합니다. LightShapeSelectionControl.Designer.cs 또는 LightShapeSelectionControl.Designer.vb 파일을 열고 Dispose 메서드의 기본 정의를 제거합니다.

  5. LightShape 속성을 구현합니다.

    ' LightShape is the property for which this control provides
    ' a custom user interface in the Properties window.
    Public Property LightShape() As MarqueeLightShape
    
        Get
            Return Me.lightShapeValue
        End Get
    
        Set(ByVal Value As MarqueeLightShape)
            If Me.lightShapeValue <> Value Then
                Me.lightShapeValue = Value
            End If
        End Set
    
    End Property
    
            // LightShape is the property for which this control provides
            // a custom user interface in the Properties window.
            public MarqueeLightShape LightShape
            {
                get
                {
                    return this.lightShapeValue;
                }
    
                set
                {
                    if( this.lightShapeValue != value )
                    {
                        this.lightShapeValue = value;
                    }
                }
            }
    
  6. OnPaint 메서드를 재정의합니다. 이를 구현하면 안쪽을 채운 사각형과 원이 그려집니다. 또한 선택한 값을 강조 표시하기 위해 두 도형 중 하나에 테두리가 그려집니다.

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

디자이너에서 사용자 지정 컨트롤 테스트

이 시점에서 MarqueeControlLibrary 프로젝트를 빌드할 수 있습니다. MarqueeControl 클래스를 상속하는 컨트롤을 만들고 폼에서 사용하여 구현을 테스트합니다.

사용자 지정 MarqueeControl 구현을 만들려면

  1. Windows Forms 디자이너에서 DemoMarqueeControl을 엽니다. 이렇게 하면 DemoMarqueeControl 형식의 인스턴스가 만들어지고 이 인스턴스가 MarqueeControlRootDesigner 형식의 인스턴스에 표시됩니다.

  2. 도구 상자에서 MarqueeControlLibrary 구성 요소 탭을 엽니다. 선택 항목에 사용할 수 있는 MarqueeBorder 및 MarqueeText 컨트롤이 표시됩니다.

  3. MarqueeBorder 컨트롤의 인스턴스를 DemoMarqueeControl 디자인 화면으로 끌어 옵니다. 이 MarqueeBorder 컨트롤을 부모 컨트롤에 도킹합니다.

  4. MarqueeText 컨트롤의 인스턴스를 DemoMarqueeControl 디자인 화면으로 끌어 옵니다.

  5. 솔루션을 빌드합니다.

  6. DemoMarqueeControl을 마우스 오른쪽 단추로 클릭하고 바로 가기 메뉴에서 테스트 실행 옵션을 클릭하여 애니메이션을 시작합니다. 테스트 중지를 클릭하여 애니메이션을 중지합니다.

  7. 디자인 뷰에서 Form1을 엽니다.

  8. 두 개의 Button 컨트롤을 폼에 놓습니다. 해당 컨트롤 이름을 startButton과 stopButton으로 지정하고 Text 속성 값을 각각 시작과 중지로 변경합니다.

  9. Button 컨트롤에 대한 Click 이벤트 처리기를 구현합니다.

  10. 도구 상자에서 MarqueeControlTest 구성 요소 탭을 엽니다. 선택 항목에 사용할 수 있는 DemoMarqueeControl이 표시됩니다.

  11. DemoMarqueeControl 인스턴스를 Form1 디자인 화면으로 끌어 옵니다.

  12. Click 이벤트 처리기에서 Start 및 Stop 메서드를 DemoMarqueeControl에서 호출합니다.

Private Sub startButton_Click(sender As Object, e As System.EventArgs)
    Me.demoMarqueeControl1.Start()
End Sub 'startButton_Click

Private Sub stopButton_Click(sender As Object, e As System.EventArgs)
Me.demoMarqueeControl1.Stop()
End Sub 'stopButton_Click
private void startButton_Click(object sender, System.EventArgs e)
{
    this.demoMarqueeControl1.Start();
}

private void stopButton_Click(object sender, System.EventArgs e)
{
    this.demoMarqueeControl1.Stop();
}
  1. MarqueeControlTest 프로젝트를 시작 프로젝트로 설정하여 실행합니다. DemoMarqueeControl을 표시하는 폼이 나타납니다. 시작 단추를 클릭하여 애니메이션을 시작합니다. 텍스트가 깜박이고 테두리를 따라 조명이 이동합니다.

다음 단계

MarqueeControlLibrary는 간단하게 구현한 사용자 지정 컨트롤과 연결된 디자이너를 보여 줍니다. 다음과 같은 방법으로 이 샘플을 좀 더 복잡하게 만들 수 있습니다.

  • 디자이너에서 DemoMarqueeControl에 대한 속성 값을 변경합니다. MarqueBorder 컨트롤을 더 추가하고 해당 부모 인스턴스 안에 도킹하여 중첩된 효과를 만듭니다. UpdatePeriod와 조명 관련 속성에 대한 여러 가지 설정을 테스트합니다.

  • IMarqueeWidget의 자체 구현을 작성합니다. 예를 들어, 깜박이는 "네온 사인"이나 여러 이미지를 사용한 애니메이션 사인을 만들 수 있습니다.

  • 디자인 타임 환경을 구체적으로 사용자 지정합니다. EnabledVisible 이 외의 더 많은 속성에 대한 숨김을 시도할 수 있으며 새 속성을 추가할 수 있습니다. 새 디자이너 동사를 추가하여 자식 컨트롤 도킹과 같은 일반적인 작업을 단순화합니다.

  • MarqueeControl 사용권을 얻습니다. 자세한 내용은 방법: 구성 요소 및 컨트롤 라이센스를 참조하십시오.

  • 컨트롤이 serialize되는 방법과 컨트롤의 코드가 생성되는 방법을 제어합니다. 자세한 내용은 동적 소스 코드 생성 및 컴파일을 참조하십시오.

참고 항목

작업

방법: 디자인 타임 기능을 활용하는 Windows Forms 컨트롤 만들기

참조

UserControl

ParentControlDesigner

DocumentDesigner

IRootDesigner

DesignerVerb

UITypeEditor

BackgroundWorker

기타 리소스

디자인 타임 지원 확장

사용자 지정 디자이너

.NET Shape Library: A Sample Designer