연습: 디자인 타임 기능을 활용하는 컨트롤 만들기

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

주의

이 콘텐츠는 .NET Framework용으로 작성되었습니다. .NET 6 이상 버전을 사용하는 경우 주의해서 이 콘텐츠를 사용합니다. Windows Forms용 디자이너 시스템이 변경되었으며, .NET Framework 이후 디자이너 변경 내용 문서를 검토하는 것이 중요합니다.

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

MarqueeControl 형식은 애니메이션 광원과 깜박이는 텍스트가 있는 극장 윤곽과 유사한 디스플레이를 구현합니다.

이 컨트롤의 디자이너는 디자인 환경과 상호 작용하여 사용자 지정 디자인 타임 환경을 제공합니다. 사용자 지정 디자이너를 사용하면 애니메이션 광원과 깜박이는 텍스트를 여러 조합으로 사용하여 사용자 지정 MarqueeControl 구현을 어셈블할 수 있습니다. 다른 Windows Forms 컨트롤과 같은 폼에서 어셈블리된 컨트롤을 사용할 수 있습니다.

이 연습을 마치면 사용자 지정 컨트롤이 다음과 같이 표시됩니다.

움직이는 말하는 텍스트와 시작 및 중지 단추를 보여주는 앱.

전체 코드 목록은 방법: 디자인 타임 기능을 활용하는 Windows Forms 컨트롤 만들기를 참조하세요.

사전 요구 사항

이 연습을 완료하려면 Visual Studio가 필요합니다.

프로젝트를 만듭니다.

첫 번째 단계에서는 애플리케이션 프로젝트를 만듭니다. 이 프로젝트를 사용하여 사용자 지정 컨트롤을 호스트하는 애플리케이션을 빌드합니다.

Visual Studio 새 Windows Forms 애플리케이션 프로젝트를 만들고 이름을 MarqueeControlTest로 지정합니다.

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

  1. 솔루션에 Windows Forms 제어 라이브러리 프로젝트를 추가합니다. 프로젝트 이름을 MarqueeControlLibrary로 지정합니다.

  2. 솔루션 탐색기를 사용하여 선택한 언어에 따라 “UserControl1.cs” 또는 “UserControl1.vb”라는 원본 파일을 삭제하여 프로젝트의 기본 컨트롤을 삭제합니다.

  3. MarqueeControlLibrary 프로젝트에 새 UserControl 항목을 추가합니다. 새 원본 파일에 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. MarqueeControlTest 프로젝트에 새 UserControl 항목을 추가합니다. 새 원본 파일에 DemoMarqueeControl이라는 기본 이름을 지정합니다.

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

    Imports MarqueeControlLibrary
    
    using MarqueeControlLibrary;
    
  3. MarqueeControl 클래스에서 상속할 DemoMarqueeControl의 선언을 변경합니다.

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

  5. Windows Form 디자이너에서 Form1을 엽니다.

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

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

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

사용자 지정 디자인 타임 환경을 개발하는 경우 컨트롤 및 구성 요소를 디버그해야 합니다. 디자인 타임에 디버깅을 허용하도록 프로젝트를 설정하는 간단한 방법이 있습니다. 자세한 내용은 연습: 디자인 타임에 사용자 지정 Windows Forms 컨트롤 디버깅을 참조하세요.

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

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

  3. 작업 시작 섹션에서 외부 프로그램 시작을 선택합니다. 별도 Visual Studio 인스턴스를 디버깅하게 되므로, 줄임표(Visual Studio 속성 창에 있는 줄임표 단추(...)) 단추를 클릭하여 Visual Studio IDE를 탐색합니다. 실행 파일의 이름이 devenv.exe 기본 위치에 설치한 경우 해당 경로는 %ProgramFiles(x86)%\Microsoft Visual Studio\2019\<edition>\Common7\IDE\devenv.exe입니다.

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

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

검사점

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

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

  1. 코드 편집기에서 MarqueeControlRootDesigner 원본 파일을 열고 WriteLine 문에 중단점을 놓습니다.

  2. F5 키를 눌러 디버깅 세션을 시작합니다.

    새 Visual Studio 인스턴스가 만들어집니다.

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

  4. 디자이너에서 DemoMarqueeControl을 엽니다.

    Visual Studio 디버깅 인스턴스는 중단점에서 포커스를 가져오고 실행이 중지됩니다. F5 키를 눌러 디버깅 세션을 계속합니다.

이 시점에서 사용자 지정 컨트롤 및 관련 사용자 지정 디자이너를 개발하고 디버그할 수 있는 모든 것이 준비됩니다. 이 문서의 나머지 부분에서는 컨트롤 및 디자이너의 기능을 구현하는 세부 정보에 중점을 줍니다.

사용자 지정 컨트롤 구현

MarqueeControl에는 약간의 사용자 지정이 있는 UserControl입니다. 선택 윤곽 애니메이션을 시작하는 Start메서드와 애니메이션을 중지하는 Stop 메서드, 이렇게 두 가지 메서드를 노출합니다. MarqueeControl에는 IMarqueeWidget 인터페이스, StartStop를 구현하고 각 자식 컨트롤을 열거하고 IMarqueeWidget을(를) 구현하는 각 자식 컨트롤에서 각각StartMarqueeStopMarquee 메서드를 호출하는 자식 컨트롤이 포함되어 있기 때문입니다.

MarqueeBorderMarqueeText 컨트롤의 모양은 레이아웃에 따라 달라지므로 MarqueeControlOnLayout 메서드를 재정의하고 이 형식의 자식 컨트롤에서 PerformLayout을(를) 호출합니다.

이는 MarqueeControl 사용자 지정의 범위입니다. 런타임 기능은 MarqueeBorderMarqueeText 컨트롤에 의해 구현되며 디자인 타임 기능은 MarqueeBorderDesignerMarqueeControlRootDesigner 클래스에 의해 구현됩니다.

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

  1. 코드 편집기에서 MarqueeControl 원본 파일을 엽니다. StartStop 메서드를 구현합니다.

    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
    

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

MarqueeControlMarqueeBorder 컨트롤과 MarqueeText 컨트롤이라는 두 가지 종류의 자식 컨트롤을 호스트합니다.

  • MarqueeBorder: 이 컨트롤은 가장자리 주위에 “광원”의 테두리를 그립니다. 광원이 순서대로 깜박이기 때문에 테두리 주위를 이동하는 것처럼 보입니다. 광원 플래시가 호출된 UpdatePeriod 속성에 의해 제어되는 속도입니다. 다른 여러 사용자 지정 속성은 컨트롤의 모양에 대한 다른 측면을 결정합니다. 애니메이션이 시작되고 중지되는 시기를 제어하는 두 메서드(StartMarqueeStopMarquee)입니다.

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

디자인 타임에 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.
        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. 도구 상자에서 BackgroundWorker 구성 요소를 MarqueeText 컨트롤로 끌어다 놓습니다. 이 구성 요소를 사용하면 MarqueeText 컨트롤 자체를 비동기적으로 업데이트할 수 있습니다.

  6. 속성 창에서 BackgroundWorker 구성 요소와 WorkerReportsProgressWorkerSupportsCancellation 속성을 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 인터페이스를 구현합니다.

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

    CategoryBrowsable 특성이 UpdatePeriod 속성에 적용되므로 “선택 윤곽”이라는 속성 창 사용자 지정 섹션에 나타납니다.

    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)을 클라이언트에 노출합니다. CategoryBrowsable 특성이 해당 속성에 적용되므로 “선택 윤곽”이라는 속성 창 사용자 지정 섹션에 나타납니다.

    [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 구성 요소와 DoWorkProgressChanged 이벤트에 대한 처리기를 구현합니다.

    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. 도구 상자에서 BackgroundWorker 구성 요소를 MarqueeBorder 컨트롤로 끌어다 놓습니다. 이 구성 요소를 사용하면 MarqueeBorder 컨트롤 자체를 비동기적으로 업데이트할 수 있습니다.

  3. 속성 창에서 BackgroundWorker 구성 요소와 WorkerReportsProgressWorkerSupportsCancellation 속성을 true로 설정합니다. 이러한 설정을 사용하면 BackgroundWorker 구성 요소가 주기적으로 ProgressChanged 이벤트를 발생시키고 비동기 업데이트를 취소할 수 있습니다. 자세한 내용은 BackgroundWorker 구성 요소를 참조하세요.

  4. 속성 창에서 이벤트 단추를 선택합니다. DoWorkProgressChanged 이벤트에 대한 처리기를 연결합니다.

  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 컨트롤의 상태를 관리하기 위한 두 개의 열거형을 선언합니다. 즉, 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 인터페이스를 구현합니다.

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

    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 구성 요소와 DoWorkProgressChanged 이벤트에 대한 처리기를 구현합니다.

    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. OnLayoutOnPaint 메서드를 재정의합니다.

    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 컨트롤의 “그림자”로 지정하고 필터링하여 디자인 환경과의 상호 작용을 변경합니다.

구성 요소의 속성 접근자에 대한 호출을 가로채는 것을 “그림자”라고 합니다. 디자이너는 사용자가 설정한 값을 추적하고 필요에 따라 해당 값을 디자인 중인 구성 요소에 전달할 수 있습니다.

이 예제에서는 디자인 타임 동안 사용자가 MarqueeBorder 컨트롤을 보이지 않거나 사용하지 않도록 설정할 수 없으므로 MarqueeBorderDesigner에 의해 Visible 속성과 Enabled 속성이 그림자로 표시됩니다.

디자이너는 속성을 추가 및 제거할 수도 있습니다. 이 예제에서는 MarqueeBorder 컨트롤이 LightSize 속성에 지정된 조명의 크기에 따라 패딩을 프로그래밍 방식으로 설정하기 때문에 디자인 타임에 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 컨트롤에는 자식 컨트롤이 포함될 수 있으므로 부모-자식 상호 작용을 처리하는 ParentControlDesigner에서 MarqueeBorderDesigner이(가) 상속됩니다.

    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. EnabledVisible 속성을 구현합니다. 이러한 구현은 컨트롤의 속성을 숨기게 합니다.

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

구성 요소 변경 처리

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

사용자가 MarqueeControl 인스턴스를 디자인할 때 루트 디자이너는 MarqueeControl 및 자식 컨트롤에 대한 변경 내용을 추적합니다. 디자인 타임 환경은 구성 요소 상태에 대한 변경 내용을 추적하기 위한 편리한 서비스(IComponentChangeService)를 제공합니다.

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

MarqueeControlRootDesigner 클래스 의 경우 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를 참조하세요.

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

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

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

  1. MarqueeControlRootDesigner 클래스에서 OnVerbRunTestOnVerbStopTest 이벤트 처리기를 추가합니다.

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

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

사용자 지정 UITypeEditor 만들기

사용자에 대한 사용자 지정 디자인 타임 환경을 만들 때 속성 창 사용자 지정 상호 작용을 만드는 것이 바람직한 경우가 많습니다. UITypeEditor만들면 됩니다.

MarqueeBorder 컨트롤은 속성 창 여러 속성을 노출합니다. 이러한 두 속성 중 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 메서드를 재정의합니다. 이 구현은 디자인 환경에 LightShapeEditor 표시 방법을 알려주는 DropDown을(를) 반환합니다.

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

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

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

MarqueeLightShape 속성은 두 가지 유형(SquareCircle)의 밝은 셰이프를 지원합니다. 속성 창에서 이러한 값을 그래픽으로 표시하기 위해서만 사용되는 사용자 지정 컨트롤을 만듭니다. 이 사용자 지정 컨트롤은 UITypeEditor에서 속성 창과 상호 작용하는 데 사용됩니다.

사용자 지정 UI 형식 편집기용 보기 컨트롤을 만들려면

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

  2. 도구 상자Panel 컨트롤을 LightShapeSelectionControl로 끌어옵니다. 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. squarePanelcirclePanel컨트롤에 대한 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 이벤트 처리기를 squarePanelcirclePanel 컨트롤의 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 Forms 디자이너에서 DemoMarqueeControl을 엽니다. 그러면 DemoMarqueeControl 형식의 인스턴스가 만들어지고 MarqueeControlRootDesigner 형식의 인스턴스에 표시됩니다.

  2. 도구 상자에서 MarqueeControlLibrary 구성 요소 탭을 엽니다. 선택할 수 있는 MarqueeBorderMarqueeText 컨트롤이 표시됩니다.

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

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

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

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

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

  8. Button 컨트롤을 양식에 배치합니다. startButtonstopButton의 이름을 지정하고 Text 속성 값을 각각 시작중지로 변경합니다.

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

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

  11. Form1 디자인 화면으로 DemoMarqueeControl의 인스턴스를 끕니다.

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

    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 라이선스를 부여합니다.

  • 컨트롤이 직렬화되는 방법 및 컨트롤에 대한 코드 생성 방법을 제어합니다. 자세한 내용은 동적 소스 코드 생성 및 컴파일을 참조하세요.

참고 항목