C#을 프로그래밍 언어로 사용하여 Windows Forms를 이용하여 만든 이미지 뷰어 컨트롤입니다. PictureBox 컨트롤이 내부적으로 스크롤 기능을 지원하지 않는점을 고려하여 디자인한 컨트롤이며, 다음의 기능들을 지원합니다.

 

  • 배율에 따른 이미지 확대/축소 기능
  • 이미지 축소 및 확대 시 부드럽게 이미지를 변조하는 기능
  • 키보드 스크롤, 마우스 드래그 스크롤

 

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

원본: http://www.syncfusion.com/FAQ/windowsforms/faq_c40c.aspx#q694q

 

Windows Forms 관련 프로젝트를 진행하던 중, 근래에 보기 드문 자료 모음을 하나 발견하였습니다. Windows Forms, ASP .NET, Silverlight 등 다양한 소프트웨어 컴포넌트를 개발하는 Syncfusion社의 직원으로 근무하면서 받은 여러 질문과 경험 들을 George Shephered님 께서 FAQ의 형태로 웹 페이지로 정리한 페이지입니다.

 

(http://www.syncfusion.com/faq/windowsforms/default.aspx) ASP.NET과 WPF FAQ도 같이 수록되어있으며, 이 중에서 Windows Forms에 대한 부분을 먼저 번안해서 올려보고자 합니다. 번역은 지속적으로 업데이트를 해나가도록 할 예정이며, 잘못 번역된 부분이나 의미가 모호한 부분에 대한 피드백을 주시면 감사하겠습니다. 또한, 최신 버전의 Windows Forms 기술에 대한 피드백도 덧붙여주시면 반영할 수 있도록 하겠습니다. (번역 과정에서 반영하지 못한 내용도 있을 수 있습니다.)

1.1 내 응용프로그램에서 EXE 파일을 어떻게 실행합니까?

System.Diagnostics 네임스페이스의 Process 클래스를 사용하면 됩니다.

 

[C#]

     Process proc = new Process();

     proc.StartInfo.FileName = @"Notepad.exe";

     proc.StartInfo.Arguments = "";

     proc.Start();

 

[VB.NET]

     Dim proc As New Process()

     proc.StartInfo.FileName = "Notepad.exe"

     proc.StartInfo.Arguments = ""

     proc.Start()

1.2. 게시자 정책 파일을 활용하면서 발생할 수 있는 문제점은 어떤 것들이 있습니까?

1) 게시자 정책 어셈블리의 이름을 정확히 지정해야만 합니다. 예를 들어, 원본 어셈블리 파일의 이름이 “TestAssebmly.dll” 이라고 가정하였을 때, 게시자 정책 어셈블리 파일의 이름은 “policy.1.0.TestAssembly.dll” 이라고 지정되어야 하며, 이는 “1.0.*” 버전 전체에 대한 어셈블리 리디렉션을 뜻하게 됩니다.

 

2) 게시자 정책 파일 내에서 어셈블리를 지칭할 때에는 어셈블리 이름에 확장자인 DLL을 붙이지 않도록 유의합니다.

다음은 잘못된 예입니다:

<assemblyIdentity name="TestAssembly.dll" publicKeyToken="f638d0a8d5996dd4" culture="neutral" />

대신 이와 같이 지정하십시오.

<assemblyIdentity name="TestAssembly" publicKeyToken="f638d0a8d5996dd4" culture="neutral" />

3) 원본 어셈블리 파일 이름에 사용하였던 강력한 이름을 그대로 게시자 정책 어셈블리에도 지정해야 합니다.

 

4) 게시자 정책 파일을 게시자 정책 어셈블리에 맞추어 배포해야 합니다. 게시자 정책 어셈블리만을 게시자 정책 파일 없이 전역 어셈블리 캐시에 단독으로 설치하는 것으로는 충분하지 않습니다. 또한, 게시자 정책 어셈블리를 만든 이후에 가하게 되는 게시자 정책 파일의 변경 사항들은 반영되지 않습니다.

 

5) 항상 “AL” 유틸리티를 사용할 때에는 /link 스위치를 사용하여 게시자 정책 파일을 지정하도록 합니다. /embed 스위치를 사용하지 마십시오. 지원되지 않는 듯합니다.

이 내용과 관련이 있는 링크 - http://msdn.microsoft.com/ko-kr/library/dz32563a.aspx (원문에 소개된 나머지 링크 2개는 이 글이 작성되는 현 시점에서는 유효하지 않아서 제외했습니다.)

1.3. 현재 시스템에서 실행되는 모든 프로세스의 목록을 어떻게 가져옵니까?

System.Diagnostics 네임스페이스의 Process.GetProcesses() 정적 메서드를 사용하시면 됩니다.

 

[C#]

     using System.Diagnostics;

     ...

     foreach ( Process p in Process.GetProcesses() )

          Console.WriteLine( p ); // string s = p.ToString();

 

[VB.NET]

     Imports System.Diagnostics

     ...

     Dim p As Process

     For Each p In Process.GetProcesses()

          Console.WriteLine(p) ' string s = p.ToString()

     Next p

1.4. 현재 시스템에서 GUI 형태로 실행되는 프로그램들 (단순한 창 목록 나열이 아닌)의 목록을 어떻게 가져옵니까?

플랫폼 호출 기능 (P/Invoke)을 이용하여 EnumWindows Win32 API를 호출할 수도 있지만, System.Diagnostics 네임스페이스의 Process.GetProcesses() 정적 메서드를 활용하면 플랫폼 호출 기능으로 발생하는 비용을 최소화하면서 원하는 목적을 쉽게 달성할 수 있습니다.

 

[C#]

     using System.Diagnostics;

     ...

     foreach ( Process p in Process.GetProcesses(System.Environment.MachineName) )

     {

          if( p.MainWindowHandle != IntPtr.Zero)

          {

               //this is a GUI app

               Console.WriteLine( p ); // string s = p.ToString();

          }

     }

 

[VB.NET]

     Imports System.Diagnostics

     ...

     Dim p As Process

     For Each p In Process.GetProcesses(System.Environment.MachineName)

          If p.MainWindowHandle <> IntPtr.Zero Then

               'this is a GUI app

               Console.WriteLine(p) ' string s = p.ToString();

          End If

     Next p

1.5. 프로그램을 하나만 실행시킬 수 있도록 하려면 어떻게 해야 합니까?

C# Corner 웹 사이트에 Saar Carmi님이 올려주신 샘플 (http://www.codeproject.com/KB/cs/restricting_instances.aspx) 의 내용에서는, System.Diagnostics 네임스페이스 안의 Process 클래스를 활용하여 이를 구현하고 있습니다.

 

역자 주) 이 방법은 인터넷 상에 알려져 있는 여러 방법들 중 하나를 소개하는 것입니다.

 

[C#]

public static Process RunningInstance()

{

     Process current = Process.GetCurrentProcess();

     Process[] processes = Process.GetProcessesByName (current.ProcessName);

      //Loop through the running processes in with the same name

      foreach (Process process in processes)

      {

          //Ignore the current process

           if (process.Id != current.Id)

          {

               //Make sure that the process is running from the exe file.

                if (Assembly.GetExecutingAssembly().Location.Replace("/", "\\") == current.MainModule.FileName)

                {

                    //Return the other process instance.

                    return process;

                }

           }

      }

      //No other instance was found, return null.

      return null;

}

 

[VB.NET]

Public Shared Function RunningInstance() As Process

     Dim current As Process = Process.GetCurrentProcess()

     Dim processes As Process() = Process.GetProcessesByName(current.ProcessName)

     'Loop through the running processes in with the same name

     Dim process As Process

     For Each process In processes

          'Ignore the current process

          If process.Id <> current.Id Then

               'Make sure that the process is running from the exe file.

               If [Assembly].GetExecutingAssembly().Location.Replace("/", "\") = current.MainModule.FileName Then

                    'Return the other process instance.

                    Return process

               End If

          End If

     Next process

     'No other instance was found, return null.

     Return Nothing

End Function 'RunningInstance

1.6. 현재 실행 중인 운영 체제를 파악하려면 어떻게 합니까?

System 네임스페이스의 Environment 클래스를 활용하면 운영 체제 정보를 가져올 수 있습니다.

 

[C#]

     string versionText = Environment.OSVersion.Version.ToString();

 

[VB.NET]

     Dim versionText As String = Environment.OSVersion.Version.ToString()

Version 속성은 Major, Minor와 같이 상세한 정보를 표현하는 멤버를 가지고 있습니다. 참고로, Windows XP의 버전은 5.1입니다.

1.7. 실행 중인 컴퓨터의 모든 IP 주소를 가져오려면 어떻게 합니까?

[C#]

     string s ="";

     System.Net.IPAddress[] addressList = Dns.GetHostByName(Dns.GetHostName()).AddressList;

     for (int i = 0; i < addressList.Length; i ++)

     {

          s += addressList[i].ToString() + "\n";

     }

     textBox1.Text = s;

 

[VB.NET]

     Dim s As String = ""

     Dim addressList As System.Net.IPAddress() = Dns.GetHostByName(Dns.GetHostName()).AddressList

     Dim i As Integer

     For i = 0 To addressList.Length - 1

          s += addressList(i).ToString() + ControlChars.Lf

     Next i

     textBox1.Text = s

1.8. 배포 대상 컴퓨터에 .NET Framework가 설치되어있지 않습니다. 이러한 경우 개발된 Windows Forms 응용프로그램을 실행할 수 있습니까?

아니오. 개발한 Windows Forms 응용프로그램을 실행하려면 반드시 배포 대상 컴퓨터에 .NET Framework가 설치되어있어야만 합니다. Microsoft에서 .NET 런타임 플랫폼 설치 패키지를 재 배포가 가능한 형태로 배포를 하고 있으니 참고하시기 바랍니다.

1.9. 실행 중인 EXE 파일의 경로를 가져오려면 어떻게 합니까?

System.Windows.Forms 네임스페이스의 Application.ExecutablePath 정적 속성을 활용하면 알 수 있습니다.

 

[C#]

     textBox1.Text = Application.ExecutablePath;

 

[VB.NET]

     TextBox1.Text = Application.ExecutablePath

1.10. 현재 실행 중인 코드가 어떤 어셈블리로부터 시작되어 실행 중인지 파악하려면 어떻게 합니까?

아래의 코드 단편은 어떤 어셈블리로부터 시작되어 코드가 실행 중에 있는지 파악하는 방법을 보여줍니다.

 

[C#]

MessageBox.Show(System.Reflection.Assembly.GetEntryAssembly().GetName().Name);

 

[VB.NET]

MessageBox.Show(System.Reflection.Assembly.GetEnTryAssembly().GetName().Name)

1.11. 전역 어셈블리 캐시에 어떠한 항목들이 설치되어있는지 어떻게 확인할 수 있습니까?

Windows 탐색기를 활용하여 %windir%\Assembly 폴더 (%windir%은 현재 실행 중인 Windows 운영 체제의 기본 설치 경로를 저장하는 시스템 환경 변수입니다.)를 열어보면 알 수 있습니다. 만약 .NET Framework가 설치되어있다면, 이 폴더에 대해 Windows 탐색기가 전용 폴더 보기 화면을 띄워줄 것입니다. 자세히 보기로 보기 설정을 바꾸면 설치된 항목들의 세부 사항을 한눈에 볼 수 있습니다.

1.12. System.Windows.Forms.Application.CompanyName 속성으로 반환되는 값을 수정하려면 어떻게 합니까?

이것은 어셈블리 특성으로부터 가져오는 값입니다. Visual Studio 개발 환경 아래에서는 프로젝트 내에 자동 생성되는 AssemblyInfo 파일 (C# 프로젝트의 경우 AssemblyInfo.cs, Visual Basic .NET 프로젝트의 경우 AssemblyInfo.vb)에 설정됩니다. 여기에는 회사 이름과 더불어 버전 번호 및 여러 가지 세부 설정을 바꿀 수 있습니다. 다음은 코드 예시입니다.

 

[C#]

[assembly: AssemblyCompany("Syncfusion, Inc.")]

[VB.NET]

<Assembly: AssemblyCompany("Syncfusion, Inc.")>

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

네이버 오픈 API를 데이터 소스로 사용하여 Windows Forms에 Binding하는 샘플을 간단히 만들어보았습니다. 지난번에 예를 들었던 난수 생성을 위한 데이터 소스에서 언급했던 IListSource 인터페이스의 사용법을 기초로 발전시킨 샘플입니다. :-)

 

using System;
using System.IO;
using System.Net;
using System.Xml;
using System.Data;
using System.Text;
using System.Collections;
using System.Globalization;
using System.ComponentModel;

namespace OpenApiBinding
{
    [ToolboxItem(true)]
    [DesignTimeVisible(true)]
    public partial class NaverSearchBinding : Component, IListSource
    {
        public NaverSearchBinding()
            : this(null)
        {
        }

        public NaverSearchBinding(Container container)
            : base()
        {
            if (container != null)
                container.Add(this);

            this.InitializeComponent();

            this.key = String.Empty;
            this.target = NaverSearchTarget.WebDocuments;
            this.query = String.Empty;
            this.display = 100;
            this.start = 1;
            this.sort = NaverSortMode.Similarity;
        }

        private string key;
        private NaverSearchTarget target;
        private string query;
        private int display;
        private int start;
        private NaverSortMode sort;

        [NonSerialized]
        private const string RequestUrl = "http://openapi.naver.com/search";

        [Browsable(true)]
        [DefaultValue("")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public string Key
        {
            get { return this.key; }
            set { this.key = (value ?? String.Empty).Trim(); }
        }

        [Browsable(true)]
        [DefaultValue(NaverSearchTarget.WebDocuments)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public NaverSearchTarget Target
        {
            get { return this.target; }
            set { this.target = value; }
        }

        [Browsable(true)]
        [DefaultValue("")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public string Query
        {
            get { return this.query; }
            set { this.query = (value ?? String.Empty).Trim(); }
        }

        [Browsable(true)]
        [DefaultValue(100)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int Display
        {
            get { return this.display; }
            set { this.display = value; }
        }

        [Browsable(true)]
        [DefaultValue(1)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public int Start
        {
            get { return this.start; }
            set { this.start = value; }
        }

        [Browsable(true)]
        [DefaultValue(NaverSortMode.Similarity)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        public NaverSortMode Sort
        {
            get { return this.sort; }
            set { this.sort = value; }
        }

        public override string ToString()
        {
            return String.Format(
                CultureInfo.CurrentCulture,
                "{{ Target: '{0}', Query: '{1}' }}",
                this.target,
                this.query);
        }

        private Uri CreateUri()
        {
            if (String.IsNullOrEmpty(this.key))
                return null;

            StringBuilder buffer = new StringBuilder();
            buffer.AppendFormat("key={0}&", this.key);

            switch (this.target)
            {
                case NaverSearchTarget.Blog:
                    buffer.Append("target=blog&");
                    break;

                case NaverSearchTarget.Cafe:
                    buffer.Append("target=cafe&");
                    break;

                case NaverSearchTarget.CafeArticles:
                    buffer.Append("target=cafearticle&");
                    break;

                case NaverSearchTarget.News:
                    buffer.Append("target=news&");
                    break;

                default:
                case NaverSearchTarget.WebDocuments:
                    buffer.Append("target=webkr&");
                    break;
            }

            buffer.AppendFormat("query={0}&", this.query);
            buffer.AppendFormat("display={0}&", this.display);
            buffer.AppendFormat("start={0}&", this.start);

            switch (this.sort)
            {
                default:
                case NaverSortMode.Similarity:
                    buffer.Append("sort=sim");
                    break;

                case NaverSortMode.Date:
                    buffer.Append("sort=date");
                    break;

                case NaverSortMode.Member:
                    buffer.Append("sort=member");
                    break;

                case NaverSortMode.NewArticles:
                    buffer.Append("sort=newArticles");
                    break;

                case NaverSortMode.Rank:
                    buffer.Append("sort=rank");
                    break;
            }

            return new Uri(String.Concat(RequestUrl, '?', buffer.ToString()));
        }

        public IList GetList()
        {
            Uri requestUri = this.CreateUri();

            if (requestUri == null)
                return null;

            WebRequest webRequest = WebRequest.Create(requestUri);

            using (WebResponse webResponse = webRequest.GetResponse())
            {
                using (Stream webResponseStream = webResponse.GetResponseStream())
                {
                    XmlDocument xmlDocument = new XmlDocument();
                    xmlDocument.Load(webResponseStream);

                    DataTable inferencedTable = new DataTable(this.target.ToString());

                    switch (this.target)
                    {
                        case NaverSearchTarget.Blog:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("bloggername", typeof(string));
                            inferencedTable.Columns.Add("bloggerlink", typeof(string));
                            break;

                        case NaverSearchTarget.Cafe:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("ranking", typeof(string));
                            inferencedTable.Columns.Add("member", typeof(string));
                            inferencedTable.Columns.Add("totalarticles", typeof(string));
                            inferencedTable.Columns.Add("newarticles", typeof(string));
                            break;

                        case NaverSearchTarget.CafeArticles:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("cafename", typeof(string));
                            inferencedTable.Columns.Add("cafeurl", typeof(string));
                            break;

                        case NaverSearchTarget.News:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            inferencedTable.Columns.Add("originallink", typeof(string));
                            inferencedTable.Columns.Add("pubDate", typeof(string));
                            break;

                        default:
                        case NaverSearchTarget.WebDocuments:
                            inferencedTable.Columns.Add("title", typeof(string));
                            inferencedTable.Columns.Add("link", typeof(string));
                            inferencedTable.Columns.Add("description", typeof(string));
                            break;
                    }

                    foreach (XmlNode eachNode in xmlDocument.SelectNodes("/rss/channel/item"))
                    {
                        XmlElement eachElement = eachNode as XmlElement;

                        if (eachElement == null)
                            continue;

                        int i = 0;
                        object[] values = new object[inferencedTable.Columns.Count];

                        foreach (DataColumn eachColumn in inferencedTable.Columns)
                            values[i++] = eachElement.SelectSingleNode(eachColumn.ColumnName).InnerXml;

                        inferencedTable.Rows.Add(values);
                    }

                    return inferencedTable.DefaultView;
                }
            }
        }

        public bool ContainsListCollection
        {
            get { return true; }
        }
    }

    [Serializable]
    public enum NaverSearchTarget : int
    {
        Blog,
        Cafe,
        CafeArticles,
        WebDocuments,
        News
    }

    [Serializable]
    public enum NaverSortMode : int
    {
        Similarity,
        Date,
        Member,
        NewArticles,
        Rank
    }
}

 

위의 소스 코드를 프로젝트에 추가하여 Windows Forms – 또는 – ASP.NET 프로젝트에 연결하거나, 독립적으로 객체를 만들어서 사용하는 것이 가능합니다. 다음은 Windows Forms 디자인 타임에서의 사용 예시입니다.

 

 

컴포넌트에서 API Key를 Key 프로퍼티에 설정하고, 검색어를 Query 프로퍼티에 설정하여 검색 결과를 수신할 수 있습니다. 데이터 그리드를 이용하여 수신한 데이터를 곧바로 보여줄 수도 있습니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

Visual Studio, 특히 Windows Forms 기반 프로젝트를 진행하면서, 사용자 정의 컴포넌트나 사용자 정의 컨트롤을 개발할 때 쉽게 파악하기 힘든 사항 중에 하나가 현재 로드된 컴포넌트나 컨트롤이 디자인 타임 위에서 실행 중인지 런타임 위에서 실행 중인지를 파악하는 것입니다. 저 또한 이 문제 때문에 꽤 많은 고민과 테스트를 수행해보았습니다만 시원치 않은 결과들 뿐이었습니다.

 

그러다가 결론을 하나 구했고 다음과 같은 내용들입니다.

 

  • Component.DesignMode 속성은 Component.Site 속성이 null 참조가 아니고, 지정된 Site 객체의 DesignMode 속성을 읽어서 반환하는 것이므로 큰 의미는 없습니다.
  • Component.Site 속성이 null 참조를 반환하는지 검사하는 방법은 논리적인 오류가 내포되어있을 가능성이 있습니다. 무조건 이런 검사를 사용하면, 디자인 타임이 아니면서도 Site 속성을 이용할 때 문제가 발생할 수 있습니다.
  • 외국 포럼의 자료를 검색한 결과 System.ComponentModel.LicenseManager 클래스의 UsageMode 속성을 이용하는 방법을 찾을 수 있었습니다. 이 속성은, LicenseManager 클래스의 Context 객체가 null 참조가 아니고, 해당 객체의 UsageMode 속성 값을 반환하는 것이며, null 참조일 경우 런타임으로 이해합니다.
  • Site 속성에 비해 훨씬 목적이 분명하고 제한적이므로 Site 속성을 이용한 판정보다는 안전한 선택이라고 예상됩니다.
  • 다만, 초기에 컴포넌트나 컨트롤의 생성자 단계에서만 유효한 정보이므로 이 때 캐치하지 못하면 디자인 타임 위인지 런타임 위인지 판정하지 못한채 런타임으로 인지하고 실행해버리므로 별도의 변수에 보관할 필요가 있습니다.

그 결과 아래와 같이 코드를 구성할 수 있었습니다.

 

public class MyComponent : Component

{

    private readonly bool inDesign;

    public MyComponent()

    {

         // 중략

         this.inDesign = (LicenseManager.UsageMode == LicenseUsageMode.Design);

    }
}

 

디자인 타임 때 만들어진 객체의 유효 기간은, Visual Studio에서 디자인 타임 편집 창을 닫기 전까지이므로 싱글턴 패턴을 이용하지 않는다면 만들어질 여러 MyComponent 객체 간의 간섭 효과는 걱정하지 않아도 될 것입니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

최근 진행 중인 씨티은행의 외환업무시스템 프레임워크 업그레이드 작업은 .NET Framework를 RAD (Rapid Application Development) 도구로 사용하는 것의 난해함에 대해 많은 것을 생각하게 하고 있습니다. .NET Framework 기반의 소프트웨어는 분명히 Unmanaged Code를 기초로 하는 다른 RAD 도구와는 다른 이점이 많이 있으며, 앞으로도 Windows 7과 더불어서 Desktop Scene의 현대화를 이끌어내는 주요한 키 포인트가 될 것으로 보이지만 풀어야 할 숙제 또한 많이 있을 것입니다.

 

다들 인지하고 계시는 부분일 수도 있지만, 또한 도움이 될 수 있을것이라 생각하여 오랫만에 간단한 샘플 하나를 올려봅니다. 이 방법을 통하여, Windows Forms가 가지고 있는 특유의 장점을 살리면서도, 코드를 복잡하게 만들지 않으면서, 손쉽게 다양한 유형의 데이터 바인딩을 다룰 수 있을 것이라 생각합니다.

 

이번 글에서 소개하는 데이터 바인딩 소스는 단순한 난수 생성에 한정되지만, XML 문서 구조를 유추하여 데이터 셋을 완성해야 할 필요가 있다거나 다양한 경우에 대응되는 데이터 변형 시나리오를 구축할 수 있을 것입니다.

 

System.ComponentModel.Component를 기본 클래스로 하는 컴포넌트를 만들고, System.ComponentModel.IListSource 인터페이스를 구현하는 컴포넌트를 만듭니다. Component 클래스를 기본 클래스로 한다는 것은 Visual Studio가 제공하는 Design Time 상호 운용성을 위한 기본 틀을 마련하는 것입니다.

 

IListSource 인터페이스는 ContainsListCollection 프로퍼티와 GetList 메서드로 구현이 됩니다. GetList 메서드를 통하여, 우리가 흔히 필요로 하는 데이터 소스를 직접 반환할 수 있으며, ContainsListCollection 프로퍼티는 좀 더 복잡한 유형의 데이터 소스를 관리할 수 있는 기준을 제공합니다. 보통의 경우, ContainsListCollection 프로퍼티의 값은 항상 false를 반환하도록 하고, GetList 메서드에서는 BindingList<T> 객체나 System.Data.DataView 객체를 반환합니다.

 

다음은 IListSource 인터페이스를 구현해 놓은 예시입니다.

 

        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public bool ContainsListCollection
        {
            get { return false; }
        }

 

        public IList GetList()
        {
            DataTable table = new DataTable();

            if (this.DesignMode && !this.liveBinding)
                return table.DefaultView;

            for (int i = 0; i < this.columnCount; i++)
            {
                DataColumn eachColumn = table.Columns.Add();

                eachColumn.ColumnName = String.Format(
                    CultureInfo.CurrentCulture,
                    "Column{0}", (i + 1));
                eachColumn.Caption = String.Format(
                    CultureInfo.CurrentCulture,
                    "Value #{0}", (i + 1));
                eachColumn.AllowDBNull = false;
                eachColumn.DataType = typeof(int);
                eachColumn.DefaultValue = 0;
            }

            for (int i = 0; i < this.rowCount; i++)
            {
                object[] dataArray = new object[this.columnCount];

                for (int j = 0; j < this.columnCount; j++)
                {
                    dataArray[j] = this.randomizer.Next(
                        this.minimumValue,
                        this.maximumValue);
                }

                table.Rows.Add(dataArray);
            }

            return table.DefaultView;
        }

 

위에서 굵게 강조표시한 부분이 설명하고자 하는 부분들입니다. ContainsListCollection 속성을 false로 반환하여 일반적인 데이터 소스임을 알리는 부분이 있습니다. 그리고, 이제까지 커뮤니티에서 자주 회자되는 내용들 중 하나입니다만, 디자인 타임을 정확히 구분하는 방법이 여기에 있습니다. protected 접근자로 보호되고 있고, 상속하여 재정의할 수 없는 DesignMode라는 속성을 이용하여 디자인 모드에서 취할 동작을 설정할 수 있습니다.

 

DesignMode 속성을 이용하여 디자인 타임에서 동적으로 데이터를 가져오도록 만들어서 다음 동작을 취할 수 있도록 정하는 것이 가능하며, 동시에 GetList 메서드 자체는 연계하기에 따라서 디자인 타임에 노출시킬 Verb (동사)와 연동시킬 수 있으므로, 데이터 바인딩에 시간이 많이 걸리는 동작을 정의하기에 편리합니다.

 

그리고 마지막으로, IList 인터페이스와 호환되면서도 우리가 논리적으로 편리하게 생각할 수 있는 데이터 모델인 System.DataView를 반환하는 것으로 코드의 대략적인 구조는 마무리됩니다. 이와 같은 형태로 완성된 컴포넌트를 실제로 데이터 바인딩에 연결시켜보도록 하겠습니다.

 

 

위에서 보이는것처럼, 난수를 데이터 테이블의 형태로 생성하였습니다. 이와 같이, 데이터베이스 서버가 아닌 곳의 일정한 자료를 데이터 소스로 사용하는 일이 반드시 제한적이지만은 않다는 것을 알 수 있으며, 응용하기에 따라서는 BindingSource 컴포넌트를 직접 오버라이드하여 데이터베이스가 아닌 대상을 놓고 Create, Read, Update, Delete 연산을 구현하는 것도 생각해볼 수 있을 것입니다.

 

이 샘플에 사용한 소스 코드를 하나 올려봅니다. 새 프로젝트에 추가하여 테스트해볼 수 있을 것입니다.

 

 

 

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

DevRay 스터디 그룹을 모집합니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

닷넷 프레임워크를 통하여 응용프로그램을 개발할 때, 특히 Windows Forms 기반의 다중 스레드 프로그램을 구성할 때 자주 적용되는 패턴이 하나 있다면, 단일 메서드를 가지고 PreCondition/PostCondition으로 양분하는 패턴이 있을 것입니다. 예를 들면 다음과 같습니다.

 

private void MyForm_Load(object sender, EventArgs e)
{

    if (this.InvokeRequired)

    {

         this.Invoke(new EventHandler(MyForm_Load), new object[] { sender, e });

         return;

    }

    // 실제 메서드 코딩
}

 

위와 같이 조건에 따라서, 같은 메서드를 상태에 변화를 가하여 다시 호출하는 기법은 Windows Forms 뿐만 아니라 일반적인 응용프로그램에서도 자주 활용될 수 있는 기법이 될 것입니다. 사실, 위와 같은 코드는 어려울 것이 없겠습니다만 저는 이를 일반화할 수 있는 방법을 고민하던 중에, Reflection과 Interop을 이용할 수 있다는 것을 발견하였습니다.

 

using System;
using System.Reflection;

namespace dotForex.Test
{
    class Program
    {
        static bool needInvoke = true;

        static void Main(string[] args)
        {
            if (needInvoke)
            {
                Action<string[]> func = Delegate.CreateDelegate(
                    typeof(Action<string[]>),
                    MethodInfo.GetCurrentMethod() as MethodInfo) as Action<string[]>;
                needInvoke = false;
                func(args);
                return;
            }
            else
            {
                Console.WriteLine("Test");
            }

            return;
        }
    }
}

 

위의 코드에서 강조표시된 부분이 일반화에서 핵심이 되는 부분들입니다. Delegate 클래스의 CreateDelegate 메서드를 통하여, 생성할 대리자의 형식을 지정하고, 현재 메서드의 정보를 가져와서 이를 대입하는 방법입니다.

 

위와 같은 기법을 통하여 아래와 같이 Windows Forms의 스레드 안정성을 고려한 이벤트 핸들러 처리를 일반화하는 것도 가능합니다.

 

using System;
using System.Reflection;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace dotForex.Application
{
    public sealed partial class ErrorReportForm : Form
    {
        public ErrorReportForm()
            : base()
        {
            this.InitializeComponent();
        }

        private object EnsureThreadSafe(MethodInfo targetMethod, Type targetDelegateType, params object[] arguments)
        {
            if (!this.InvokeRequired)
                throw new InvalidOperationException("EnsureThreadSafe is not required at this time.");

            if (targetMethod == null)
                throw new ArgumentNullException("targetMethod");

            if (targetDelegateType == null)
                throw new ArgumentNullException("targetDelegateType");

            if (!targetDelegateType.IsSubclassOf(typeof(Delegate)))
                throw new ArgumentException("Selected type is not a delegate type.", "targetDelegateType");

            Delegate methodDelegate = Delegate.CreateDelegate(
                targetDelegateType,
                targetMethod);

            if (methodDelegate == null)
                throw new Exception("Method cannot converted as delegate.");

            return this.Invoke(methodDelegate, arguments);
        }

        private void ErrorReportForm_Load(object sender, EventArgs e)
        {
            if (this.InvokeRequired)
            {
                this.EnsureThreadSafe(MethodInfo.GetCurrentMethod() as MethodInfo, typeof(EventHandler), sender, e);
                return;
            }

            // Method Coding Here
        }
    }
}

 

 

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

TransparencyKey를 통해서 편리한 도우미 유틸리티를 하나 만들어볼까 합니다. 바로 자동 클리핑 윈도우입니다. Window Region API를 이용하여 계산하고 자르는 작업을 하지 않고서도 간단하고 깔끔하게 이런 작업을 해낼 수 있습니다.

[Flags]
[Serializable]
public enum TransformOptions : int
{
    None = 0,
    HideAllControls = None + 1,
    RemoveWindowText = HideAllControls * 2,
    HideFromTaskbar = RemoveWindowText * 2,
    All = (HideAllControls | RemoveWindowText | HideFromTaskbar)
}

public static void TransformToCustomRegion(Form targetForm, Color transparentColor, TransformOptions options)
{
    if (targetForm == null)
        throw new ArgumentNullException("targetForm");

    if (targetForm.IsDisposed)
        throw new ObjectDisposedException("targetForm");

    if (targetForm.Visible)
        targetForm.Visible = false;

    targetForm.FormBorderStyle = FormBorderStyle.None;
    targetForm.BackColor = transparentColor;
    targetForm.TransparencyKey = targetForm.BackColor;

    if (((int)options & (int)TransformOptions.HideAllControls) != 0)
        foreach (Control c in targetForm.Controls)
            c.Visible = false;

    if (((int)options & (int)TransformOptions.RemoveWindowText) != 0)
        targetForm.Text = String.Empty;

    if (((int)options & (int)TransformOptions.HideFromTaskbar) != 0)
        targetForm.ShowInTaskbar = false;

    if (!targetForm.Visible)
        targetForm.Visible = true;
}

public static void TransformToCustomRegion(Form targetForm, Color transparentColor)
{
    TransformToCustomRegion(targetForm, transparentColor, TransformOptions.None);
}

// 실제 적용

private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.FillPie(Brushes.Aqua, 140, 0, 400, 400, 30, 80);
}

private void Form1_Load(object sender, EventArgs e)
{
    TransformToCustomRegion(this, Color.Empty);
}

여기서 TransformToCustomRegion 메서드가 동작하는 원리를 살펴보면, BackColor와 TransparencyKey의 값을 일치시켜주는 것과 함께, 창의 구성요소들을 제거하는 것입니다. 이로서, 창의 다른 구성 요소가 제거된 상태에서 순수한 컨텐츠만 Paint 이벤트를 통해서 그려지게 되는데, 이 중 색이 겹치지 않는 내용만이 남아서 창으로 존재하게 되고 나머지는 잘립니다.

이와 같은 원리를 이용하여 가운데에 구멍이 뚫려있는 창도 만들 수 있고, 예전 노턴 크래쉬가드 같은 프로그램처럼 방패모양 창도 구현할 수 있습니다. Hit-Test 구현만 정확히 되어주면 기존 제목 표시줄도 대체가 가능합니다. :-)

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

Paint 이벤트에서 그리는 함수가 아니지만 Paint 이벤트와 마찬가지로 작동하기 위해서는 그리는 방법을 다르게 구현해야 합니다. 이 문제 때문에 고민하시는 분이 꽤 있을 것으로 생각됩니다. 이 문제를 해결해줄 수 있는 클래스를 하나 소개합니다. 바로 Replay 클래스입니다.

Replay 클래스를 이용하여 멤버 메서드를 호출하면 모든 기록이 Replay 클래스 안에 보관되고, 나중에 ReplayWithoutResults 메서드를 호출하여 모든 메서드가 다시 호출될 수 있도록 하는 방식입니다. 이런 방법을 통하여 Paint 이벤트에서 그린 그림이 아니라도 복원이 가능합니다.

다음은 예제입니다.

// Graphics 객체를 Paint 이벤트가 아닌 곳에서 얻을 때,

// Replay 객체에 바로 집어넣습니다.

private void Form1_Load(object sender, EventArgs e)
{
    this.g = new Replay<Graphics>(Graphics.FromHwnd(this.panel1.Handle));
}

// Paint 이벤트에서는 Replay 객체가 가지고 있는 모든 메서드들을 호출합니다.

// g 객체의 ReplayWithoutResults 대신 정적 메서드를 부를 수도 있는데

// 이것은 Cross-Thread 호출을 위한 것입니다.

private void panel1_Paint(object sender, PaintEventArgs e)
{
    Replay<Graphics>.ReplayWithoutResults(this, this.g);
}

private Random r = new Random();
private Replay<Graphics> g;

// Graphics.DrawLine을 Replay 컨테이너를 통해서 호출하도록 합니다.

private void timer1_Tick(object sender, EventArgs e)
{
    g.Invoke("DrawLine", new object[] {
        new Pen(Color.FromArgb(r.Next(255), r.Next(255), r.Next(255))),
        new Point(r.Next(this.panel1.Width), r.Next(this.panel1.Height)),
        new Point(r.Next(this.panel1.Width), r.Next(this.panel1.Height)) });
}

이 소스 코드는 Apache License 버전 2.0 조건 아래에서 배포됩니다.

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)

프로그래밍을 하다보면 Windows Forms나 Windows Presentation Foundation과 같이 한 단락내에 한 객체에 대해서 여러 속성을 동시에 지정해야 하는 경우가 꼭 있기 마련입니다. VB.NET이나 Object Pascl의 경우 With 절을 이용하여 이런 일을 손쉽게 할 수 있도록 해줍니다만 C#의 경우 마땅히 좋은 방법이 없습니다. 게다가, 이렇게 여러 속성을 나열해놓는 코드를 작성하다보면 코드가 어지럽혀지기 쉬운듯 합니다.

Windows Forms나 Windows Presentation Foundation의 경우 대개 디자이너를 이용하여 작업하는 경우가 많으므로 별 다른 문제가 안되지만 가끔 컨트롤을 직접 추가해야 하거나 디자이너가 지원되지 않는 GTK# 등의 환경에서 저 개인적으로 요긴하게 쓰는 방식이 있어서 소개해봅니다.

Panel myPanel = new Panel();
{
    myPanel.BackColor = Color.Violet;
    // ...
    this.Controls.Add(myPanel);
}

위와 같이 myPanel을 최초로 생성하는 줄 다음에 별 다른 의미 없이 공 Bracket을 열고 myPanel에 관한 코드를 집어넣은 뒤 관련 처리가 끝나면 공 Bracket을 닫는 방식입니다. 이렇게 정리를 해두면 #region이나 #endregion보다 훨씬 읽기 편한것 같습니다. :-)

크리에이티브 커먼즈 라이선스
Creative Commons License
Posted by Windows Azure MVP 남정현 (rkttu.com)