C#에서는 Listener 패턴을 구현하기 쉽다. 특히 event라는 예약어를 제공하고 있어서, Callback 형태의 구현이 어렵지 않다.

class Test
{
        int m_nCurrentIndex = 0;
        public delegate void IndexChangedEventHandler();
        public event IndexChangedEventHandler ChangeCurrentIndex;
        public void ChangePage(int nIndex)
        {
               m_nCurrentIndex = nIndex;
               ChangeCurrentIndex();
               m_nCurrentIndex = m_nCurrentIndex + 1;
        }
}

위의 코드는 바로 그 event 형태로 어떻게 구현하는지를 나타낸다.

이를 사용하는 로직은 아래와 같다.

 

class Program
{
        [STAThread]
        static void Main()
        {
             Test test = new Test();

             test.ChangeCurrentIndex +=  new IndexChangedEventHandler(test_ChangeCurrentIndex);
             test.ChangePage(1);

             Console.WriteLine(“Complete!”);
        }

        private void test_ChangeCurrentIndex()
        {
               Console.WriteLine(“event!”);
        }
}

 

“test.ChangeCurrentIndex” 이렇게 쓰고 “+=” 만 추가하면, 자동으로 코드가 생성되면서 아래에 함수가 하나 생긴다.
이것이 바로 event 구현 작업이다. 즉 test 내부에서 “ChangeCurrentIndex()” 이 함수가 불리는 순간, 이벤트 구독한 함수로 호출을 하게 된다.

즉 역으로 함수를 부르는 일종의 Callback  함수로 생각하면 된다.

위의 작업을 한 줄씩 실행되는 순서를 보면 아래와 같은 순서로 진행된다.

  1. Test test = new Test(); –> Test 클래스의 인스턴스를 만든다.
  2. test.ChangeCurrentIndex +=  new IndexChangedEventHandler(test_ChangeCurrentIndex); –> 이벤트를 구독한다.
  3. test.ChangePage(1); –> Test 클래스의 ChangePage 함수를 호출한다.
  4. m_nCurrentIndex = nIndex;   -> TestClass 내부 : m_nCurrentIndex에 nIndex 값을 대입한다.
  5. ChangeCurrentIndex();   -> Event를 발생시킨다.
  6. Console.WriteLine(“event!”)  -> Main 쪽의 이벤트 구독할 때 등록한 함수(test_ChangeCurrentIndex)로 들어가서 “event!” 라는 문자열을 찍는다.
  7. m_nCurrentIndex = m_nCurrentIndex + 1; –> +1 를 한다. 그리고 ChangePage 함수를 종료한다.
  8. Console.WriteLine(“Complete!”);   -> Main 으로 돌아와서 Complete를 찍고 종료한다.


이것을 실행하는 순서를 다이어그램으로 나타내면 아래와 같다.

그림2

 

Test라는 클래스와 Program이라는 클래스가 서로 통신을 한다고 했을 때, 위와 같은 Listen 구조가 안된다면, 결국 Program 이라는 클래스에서 호출하는 작업이 없다면 Test가 Program으로 데이터를 보낼 방법이 없다. 만일 저 위와 같은 형태가 안되는 구조라면, Test 클래스 안에는 Program 개체의 레퍼런스를 들고 있어야 한다.

class Test
{

        Program m_parent = null;
        int m_nCurrentIndex = 0;

        ~~~~~~~~~
}

그리고 Program 클래스 안에는 Test를 통해 처리해야 할 함수 부분에서 private를 public 으로 바꿔야된다.

        public void test_ChangeCurrentIndex()
        {
               Console.WriteLine(“event!”);
        }

호출할 때는 기존에 함수형태로 된 것을 Program을 통해서 부르도록 해야 된다.

        public void ChangePage(int nIndex)
        {
               ~~~~~~~~~~~
               m_parent .test_ChangeCurrentIndex();
               ~~~~~~~~~~~
        }

서로 인스턴스를 주고 받아야 저런 통신이 가능하게 되는 것이다.

하지만 위와 같이 하게 되는 경우 같은 DLL 이나 EXE 의 경우면 어차피 같은 프로젝트 안에 있는 내용이니 큰 문제가 없지만, 만일 서로 다른 프로젝트로 되어 구성된 경우라면, 서로 인스턴스를 주고 받으려면 상당히 난감해 질 수 밖에 없다.

이렇게 훌륭한 event 구성에도 심각한 문제가 있다.

그건 바로 실행되는 순서에 있다. 실행되는 순서 중 5~7 사이를 보도록 하자.
만일 5->7->6 의 순서로 실행하고 싶다면 어떻게 해야 할까? 즉, ChangePage 함수가 종료되면 자동으로 test_ChangeCurrentIndex 가 실행되고 싶을 때라는 것이다. 간단한 구조의 프로그램이나, 구성의 경우에는 이런 부분을 신경 쓰지 않지만, Multi Thread 나 Windows UI 응용 프로그램인 경우 문제가 발생한다.

왜 문제가 발생할까?

만일 저 Event 구조가 연달아 실행되는 형태라면? 이라는 가정으로 시작해보자.

image

만일 저런 형태라면, E에서 부터 시작해서 A까지 모든 작업이 끝나야 실제 동작이 끝나게 된다. 이 경우 함수를 연달아 부른 구조와 별 다를게 없다. 그래서 간혹 중간에 시간이 오래 걸리는 작업이 있는 경우, 메인 프로그램의 UI가 마치 다운된 것과 같다.

UI가 다운되지 않은 것처럼 하기 위해서는 E 가 완전히 끝난 뒤, D가 실행되고, 다음에는 D가 완전히 다 실행한 뒤, C가 실행되는 형태가 되어야 한다.

즉 맨 위의 예제 코드를 기준으로 보면, 1->2->3->4->5->6->7->8 이 아니라, 1->2->3->4->5->7->6->8 또는 1->2->3->4->5->7->8->6 순으로 실행되면 되는 것이다. 어찌되었던 간에, ChangePage 메소드가 종료 된 뒤에, 실행을 요청한 코드가 실행되어야 한다는 것이다.

이렇게 하려면 어떻게 해야 할까?

1가지 방법은 Windows Message를 사용하는 것이다. SendMessage 나, PostMessage 같은 것으로 호출하는 방법이다. 최소한 SendMessage나 PostMessage는 Windows Message Queue에다 던지는 형식이기 때문에, Windows Message Queue에서 해당 메시지가 나올 때 까지 나머지 부분을 계속 실행하게 된다. 즉 5 번 단계의 실행에서 바로 7단계로 넘어 간 뒤에, Windows Message Queue에서 Message 가 언제 나오는지에 따라, 6 번은 그에 맞게 실행되는 것이다.

이 작업은 .NET에서 Windows Message를 다루는 방법이므로 일단 이 방법은 넘어가도록 하자.

순수 .NET 코드에서는 어떻게 해야 할까?

바로 Invoke 라는 함수를 쓰는 것이다.

Invoke의 정확한 목적은 해당 개체에 있는 함수를 강제적으로 동작하게 하는 것인데, 이것은 상대 클래스내의 함수를 무조건 실행하는 것이다. public 이든, protected 든, 심지어 private 일지라도 강제적으로 실행하는 방법이다.

이 때, 중요한 것은 Invoke 명령어는 System.Windows.Form 네임스페이스에 있는 Control 기반의 Windows 개체만 동작한다는 것이다.  위의 예제 코드는 다음과 같이 다시 작성을 해야 한다. 먼저 Windows Form 기반 응용 프로그램이여야 할 것이다.

public partial class Form1 : Form
{
        Test m_test = null;
        public Form1()
        {
            InitializeComponent();
            m_test = new Test(this);
            m_test.ChangeCurrentIndex +=  new IndexChangedEventHandler(m_test_ChangeCurrentIndex);
            test.ChangePage(1);
            Console.WriteLine(“Complete!”);
        }

        private void m_test_ChangeCurrentIndex()
        {
               Console.WriteLine(“event!”);
        }
}

 

class Test
{
        Form m_parent = null;
        int m_nCurrentIndex = 0;
        public delegate void IndexChangedEventHandler();
        public event IndexChangedEventHandler ChangeCurrentIndex;
        public Test(Form parent)
        {
             m_parent = parent;
        }
        public void ChangePage(int nIndex)
        {
               m_nCurrentIndex = nIndex;
               ChangeCurrentIndex();
               m_nCurrentIndex = m_nCurrentIndex + 1;
        }
}

앞서 event 기반의 Listen 패턴 관련해서 상호간 인스턴스가 필요없다고 했는데, 이번에는 필요하다. 왜냐면, event를 호출하는 로직을 할 때, Invoke를 해야 하는데, 그 Invoke를 당하는 대상의 인스턴스가 필요하기 때문이다.

다음 로직을 보자. 이제 ChangePage 라는 함수를 수정해야 하기 때문이다.

public void ChangePage(int nIndex)
        {
               m_nCurrentIndex = nIndex;
               m_parent.Invoke(ChangeCurrentIndex);
               m_nCurrentIndex = m_nCurrentIndex + 1;
        }

앞에서는 ChageCurrentIndex 라는 이벤트를 직접 함수처럼 실행했지만, 이번에는 Invoke 라는 것을 사용해서 실행하기 때문이다. 즉 Windows개체.Invoke(delegation함수) 형태로 실행하는 것이다.

m_parent.Invoke(ChangeCurrentIndex)라는 문구가 올 때, ChageCurrentIndex 에 연결된 함수가 실행되지 않고, 일단 m_parent 한테 실행하라고 지시만 한 상태가 된다. 일단 저 안의 함수 내용이 완료될 때까지는 저 Invoke가 실행되지 않는다. 그러므로 ChangeCurrentIndex에 구독한 부분은 나중에 실행되게 되고, 바로 그 아래에 있는 코드가 실행되게 된다. 그리고 그 함수가 완전히 종료되면, 그제서야 Invoke 한 함수를 실행하게 되는 것이다.

이 모든 작업은 COM Component 기반의 동작이 가능한 System.Windows.Form 계열의 Windows Form, Control 등만이 가능한 기능인 것이다.

Posted by 하인도

예전 Windows App을 API 기반의 C 로 만들거나, MFC로 만들 때, 각종 Event 처리를 위해, 독자적인 ID를 만들었다. 그런데, 미처 인식을 못한 것이, 어떤 ID를 사용해서 운영을 해야 겹치는 경우 없이 제대로 되는지 정하지 않고 임의대로 했다.

그 중 WM_USER 값이다.
WM_USER는 사용자가 정의해서 쓸 수 있는 ID 정도로만 인식하고 있었다. 이 값으로 시작해서 최대 0x7c00 까지 정의해서 쓸 수 있다. 10진수로 한다면, 1024 ~ 31744 까지 정의해서 쓸 수 있다. 그래서 예전에는 사용할 ID 값을 아래와 같이 정의하곤 했다.

#define WM_CUSTOMUPDATE WM_USER + 100

즉 1024 + 100 = 1124 를 WM_CUSTOMUPDATE 라는 이벤트 ID로 사용한다는 의미.
하지만, 내가 안 쓰더라도, 별도로 사용되는 컨트롤이나, 3th-Party의 컨트롤의 경우 저 이벤트를 사용하는 경우가 종종 있어 자칫 종종 겹치는 경우가 있었다. 그래서 위 처럼 + 100 정도가 아닌, 5000 정도로 팍팍 띄워서 정의하기도 했다.

그런데, 알고 보니, WM_APP 라는 값이 있었다. 이 값은 최소한 컨트롤 같은 곳에서 사용하지 않는 완전 App 개발자 용으로 남겨놓은 영역이라고 한다. 이 값은 0x8000 ~ 0xBFFF 값이다. 10진수로 한다면 32768 ~ 49151 으로 볼 수 있다.

이번에 C#으로 이것저것 만들다가, 결국 unmanaged code들을 짜게 되었는데, 이 때 Windows Message를 주로 핸들링 하게 되었는데, 이벤트 ID를 대충 대충 정해서 구현한 것 같았다. 가급적 저 ID 정의 Rule에 맞추어서 정의해야 할 것 같다.

이 내용은 아래 링크에서 참고해  정리했다.(EUC-KR 이여서, 일부 Web Browser에서 종종 깨져 보인다.)

http://winapi.co.kr/reference/Message/WM_USER.htm

Posted by 하인도

정확히는 버튼이 사라지는 문제는 아니고, 단지, Focus가 된 부분만 버튼이 보이는 문제를 의미한다.

VGridControl 에 ButtonEdit를 가져다 붙인 것인데, 이상하게 Focus 가 된 줄만 버튼이 보이고, 나머지는 보이지 않는다. 다른 줄에 Focus를 보내면 그제서야 나온다는…

이 속성 값이 어디서 설정하는 것인지 한 참을 찾다가, DevExpress의 Support 페이지에서 찾았다.
(http://www.devexpress.com/Support/Center/p/Q96026.aspx)
Foucs가 안가더라도 모두 보이게하는 방법을 찾고 있었다.

간단했다. Grid 개체에 ShowButtonMode 라는 Property에 ShowAlways로 설정하면 되었다.
코드로 표기하면 아래와 같다.

gridCtrl.ShowButtonMode = DevExpress.XtraVerticalGrid.ShowButtonModeEnum.ShowAlways;

그러면 원하는 형태인 모든 Row에 해당 컨트롤들이 보인다.

Posted by 하인도

Visual Studio를 써서 팀 작업을 할 때, TFS(Team Foundation Server)를 활용할 때가 있는데, 단순한 소스 버전관리를 벗어나서 실제 각 작업에 대한  계획이나, 한 내용들을 기입할 수 있다.

Agile 기반으로 프로젝트를 만들었다면 Bug, Issue, Task, Test Case, User Story 등 원한 형태로 생성이 가능하다.
지금 현재는 Task들을 정리해서 기록하여 정리하고 있다.
그런데, 2010 부터는(정확히는 2008 부터 되는 것 같으나, 2008은 사용해본 기억이 없음) 이 Task들을 계층적으로 적용할 수 있다고 한다. 그래서 마치 MS Project 에서 Task 밑에 Task 개념으로 표시하는 방법으로 구성할 수 있다.

몇가지 삽질을 하고 난 뒤, 그 표현 방법을 소개하려고 한다.

1. Task 생성.

Task 생성은 간단하다. 일단 Team Explorer를 띄우고 Work Items 위에서 메뉴를 띄워 New Task를 선택한다.

간단하게 만들도록 한다. 최소한 제목만 제대로 넣어도 된다.
그리고 Assigned To: 에서 자신의 이름을 꼭 넣도록 하자. 그래야 자신의 내용을 쉽게 찾아 볼 수 있다.
그리고 Save Work Item을 꼭 눌러서 내용을 저장해서 서버에 올리도록 한다.
 

2. 하위 Task 생성.

다시 Team Explorer 에서 Work Items –> Team Queries 안에서 My Tasks를 클릭해서 열도록 한다.
그러면 앞서 넣은 Task를 볼 수 있는데, 그 Task 에서 오른 쪽 클릭을 눌러 “New Linked Work Item”을 눌러
상위 혹은 하위 작업을 추가한다.

그러면 Link Type 선택과 기본적으로 필요한 이름과 설명 정도를 넣는 창이 나온다.

여기서 Link가 되는 형태(Link Type)에는 여러가지 종류가 있다.

  • Child : 앞서 선택한 작업을 Parent로 두는 하위 작업을 만들 때 사용한다.
  • Parent : 앞서 선택한 작업을 Child로 포함하는 상위 작업을 만들 때 사용한다.
  • Predecessor : 앞서 선택한 작업을 하기 위해 먼저 처리할 작업을 만들 때 사용한다.
  • Relate : 앞서 선택한 작업과 일부분 혹은 전부가 연관되는 형태의 작업을 만들 때 사용한다.(수평적 연결)
  • Shared Step : 앞서 선택한 작업을 수행하기 위한 공통적인 작업을 만들 때 사용한다.
  • Successor : ??
  • Test Case : 앞서 선택한 작업에 대한 테스트 작업을 만들 때 사용한다.
  • Tested By : ??
  • Tests

그런데,실제로 단순하게 계층형 작업 구성을 할 때는 Child와 Parent 만 사용할 것이다.
(프로젝트의 진행상 Task가 복잡하게 나열되면 거의 대부분의 유형의 Link들을 사용할 것 같다.)

일단, Child 든 Parent 든, Task를 만들도록 한다.

만든 뒤, 아랫쪽 정보들이 나온 탭 중에 All Link를 누르면 현재 설정한 대로 Link가 걸렸는지 체크가 가능하다.
(상위 작업의 Child로 만들었으므로, 상위 작업이 Parent로 표시)

확인이 되었으면 Save를 꼭해서 서버에 저장하도록 한다.

3. Query View를 바꾸기.

자, 그런데, Parent 관계를 세우기는 했는데, My Tasks를 열어 보면 가관이다.
아무리 링크를 걸고 넣어도 나오는것 쭉 나열형 뿐!

이를 계층형으로 표시하는 것을 바꿔야 한다.
바꾸려면, 보기를 위한 Query를 수정한다.
수정하려는 Query 항목에서 메뉴를 띄운 뒤, Edit Query를 한다.
여기서는 어차피 My Tasks만 보니까, 당연히 My Tasks에서 Edit Query를 해본다.

다른 부분은 손대지 말고, Type of Query 부분에서 Tree of Work Items를 선택한다.

어떻게 나오는지 체크해보려면 아래 쪽에 있는 Task들에서 Refresh를 해서 보도록 한다. 그러면 최소한 어떠한 형태로 나오는지 미리 보여준다.

이제 테스트 까지 완료되었으면 Save Query 버튼을 눌러 꼭 저장하고 닫도록 한다.

그러면 다음 부터는 계층형으로 Task를 볼 수 있다.

 

4. 기존 Task들을 계층화 시키기.

위의 방법은 새로운 Task를 추가할 때 이야기라면, 이번에는 기존에 만들어진 Task들의 관계를 맺을 때다.

만일 기존에 “예전작업”이라는 Task가 있었는데, 이 작업을 상위 작업 아래에 넣는 방법을 예제로써 언급할 것이다.

먼저 예전 작업이든, 상위 작업이든, 연결의 중심이 될 항목에서 메뉴를 띄워 “ Link to An Existing Item…”을 선택한다.

그러면 이 기준 작업(여기서 선택한것은 예전 작업)을 기준으로 어떻게 누구를 붙일 것인지를 입력할 수 있는 창이 뜬다.

이 예전 작업이 상위 작업아래로 들어갈 테니, Parent로 고치고…

Work item IDs 에서 Browse 버튼을 클릭해서 기존 Item을 찾는다.

연결된 Item이 위치한 Project를 선택하고(특별한 경우가 아니면 그대로 둔다.) Saved query 중에서 원하는 아이템이 보이는 Query를 선택한다.
그리고 하위에 Find 버튼을 눌러서 아이템을 나열시켜 원하는 아이템을 선택한다.
( 느낌표 표시가 난 Task는 자기 자신으로 선택하지 말자! )

OK를 누르면 항목이 자동으로 채워진다. 그림으로 확인해보고, 이상 있으면 수정하고, 없으면 OK를 클릭한다.

그럼 위의 내용대로 해당 Task의 수정화면이 뜨는데, 내용을 적당히 수정하고, 상단의 Save Results을 클릭한다.

Posted by 하인도

NTFS로 포멧된 폴더에 다양한 사용 권한을 걸어 사용하다가,
운영체제를 자주 변경하는 경우, 데이터용으로 사용한 파티션의 파일들이
알 수 없는 권한이 걸려 Access Denine이 걸리는 경우가 많다.

보통 C:\는 운영체제용으로 사용하고, D:\를 데이터로 하는데, C:\ 야 설치 중에 Format을 하니, 큰 문제가 없지만, 백업 처럼 사용하는 D:\ 드라이브에서는 어떻게 될지 모른다. 특히 공유 폴더들을 설정하거나, 개인용 폴더 설정등을 하다가 보면, 자신도 모르게 파일 권한이 변경이 되어 있고,
나중에 운영체제를 바꿔서 해당 폴더를 접근하려다 보면, 접근 불가에 빠진다.
그렇다고 속성을 열어서 이렇게 저렇게 수정해봐도 도저히 불가능한 경우에도 빠지게 된다.

이 경우 보안 속성을 어떻게든 초기화 해야 하는데, 이 경우에 처리하는 방법이다.

1. 시작 –> cmd를 검색 –> cmd를 관리자 권한으로 실행한다.

2. 권한을 바꾸려는 폴더 이름이 D:\Works 면 다음과 같이 입력한다.

icacls D:\Works /T /Q /C /RESET

만일 드라이브 전체라면, 해당 드라이브로 옮겨가서 D:\Works 대신 * 라고 넣는다.

icacls * /T /Q /C /RESET

3. 기다린다.

 

파일이 많으면 생각보다 긴 시간이 소요된다.
이렇게 바꾸면 최소한 기존에 설정된 각종 보안 설정 값들이 초기화 되서, 접근 되지 않는 경우가 많이 없어진다.

Posted by 하인도

출처 : 이선우 과장님.

.NET 응용 프로그램에 app.config 라는 설정을 넣는 부분이 있다.(ASP.NET의 web.config과 유사). 응용 프로그램이 최초 실행될때 기본적으로 설정할 때 사용되는 값들인데, 이 부분을 직접 Text로 열어 편집하곤 했다.

appsettings_view
이 부분을 직접 편집하는 것도 방법이겠지만, Resource 편집하듯 설정하는 방법이 있다.
솔루션 목록에서 윈도우 응용 프로그램 프로젝트 위에서 속성을 클릭하면 응용 프로그램 속성창이 뜨는데,
그 중 Settings 탭에 위치한 항목이 바로 그것이다.

property_settings

그런데 저 Type을 보면 알겠지만, .NET에서 제공되는 형만 존재한다.
만일 enum 형태로 사용자가 만든 별도의 형이 있다면? 그것을 사용하는 방법이 있다면?

바로 그것이다.예를 들어보자.만일 다음과 같은 enum이 존재한다면..

enums_example

TestType 이라는 형태로 저 Settings에 표현하려면??

물론 위와 같은 TestType을 먼저 선언한다.
그리고 난 뒤, 아까 Settings 에 들어가도록 한다. 그리고 Type 부분의 콤보 박스를 클릭한다.

settings_types

클릭하게 되면 여기서 사용가능한 Type들이 나열된다. 그러나 우리가 원하는 Type은 없다.
이제 맨 아래쪽에 있는 Browse... 를 선택하도록 한다.
이제 Select a Type 이라는 항목을 볼 수 있다. 여기서 아까 선언했던 enum을 찾도록 한다.
selectappsettingtype
찾았으면 선택한 뒤 OK를 클릭한다.

그리고 원하는 값을 선택한 뒤, 저장한다.
selectsettingsvalues

 

자! 그런데 여기서 문제 그 하나.

먼저 위에서 처럼 enum을 선언했는데, browse를 해도 그 enum을 찾을 수 없는 경우.
이 부분 때문에 이선우 과장님이 이리저리 헤매다가 그 차이점을 밝혔다.
즉 저 enum 선언은 반드시 app.config에 걸리는 응용 프로그램 외에 설정해야 된다는 것이다.
다시 말해 만일 app.config가 TestApp 라는 응용 프로그램에 존재한다면, Class Library 프로젝트를 별도로 만들어
그 안에서 선언해야 된다는 것이다.

correcteddefine

지금 필자가 구현한 코드 부분을 예로 든다면, 실제 실행되는 응용 프로그램이 TwainGui. 즉 app.config는 저 안에 있다. 하지만, enum은 별도의 Class Library 프로젝트인 TwainLib 라는 위치에 있다. 그것을 선언한 것.

저렇게 만들어 컴파일이 정상적으로 된다면, TwainGui에서 참조를 걸어 놓으면 위의 예제처럼 Browse 할 때 저 enum을 볼 수 있다.

 

이걸로 땡일까? 부부~ 이제 그 문제 2.

사용자 정의된 이 특수한 Type에 대한 안의 값을 선택하는 콤보 부분을 주목하자.
correcteddefine2 
아주 자연스럽게 나타나는데, 실제로 그럴까?

10이면 10, 100이면 100 대부분 아래와 같이 Type 목록으로 나오지 않고, 달랑 한개의 값만 표시된다.

correcteddefine3

실제로 그 안의 값을 열어보면, 마치 빈 TextBox 마냥 직접 사람이 입력하게 만든다.
말이 쉽지 실제로 그 값들이 기묘한 문자열이라면 찾기 무리.
처리 방법은 매우 간단하다. 일단 위의 값을 그대로 저장한 상태로 컴파일을 완료한다.
그리고 Visual Studio을 껏다가 다시켠다. 그리고 프로젝트 Reload.

즉 프로젝트를 Reload 함으로써 Reset이 되어야 올바른 값 목록이 나온다는 것이다.

일단 여기까지!

Posted by 하인도

.NET Application에서 외부 이벤트를 끌어들이는 방법을 설명하고자 한다.

사실 Windows Event에서는 Managed 환경에서 제공되는 한정된 이벤트 처리를 의외로 간단하다. 하지만, Win32 기반의 외부 응용 프로그램에서 제공되는 커스텀 이벤트 처리에 대해서는 어찌되어야 할지 명확히 알지 못하는 경우가 많다. 이 부분을 언급하려고 한다.

먼저 .NET의 응용 프로그램은 Application 이라는 개체를 자체적으로 가지고 있게 된다. Static 기반으로 되어 있어, From을 띄우거나, 메시지를 가져오고 메시지를 보내는 등 어떻게 보면 OS 처럼 최하위단을 제어하고 움직이는 중요한 부분이다. 바로 이 부분을 활용하여 외부의 이벤트를 끌어오는 것이다.

이 작업을 수행하려면, 외부 이벤트를 수신하는 Windows 개체에 IMessageFilter 라는 인터페이스가 필요하다.
이 인터페이스는 System.Windows.Forms 라는 네임스페이스 상에 존재한다.

제일 먼저 이 인터페이스를 상속 받게 한다.
imessagefilter_init

그 다음은 해당 Windows 소스 내에 IMessageFilter 에 대한 구현을 구성한다.

imessagefilter_createmethod

즉 저 PerFilterMessage 라는 부분에서 실제적인 처리를 시도하게 된다.

Message 객체는 다음과 같이 구성되어 있다.

  • HWnd : 윈도우 Handle 주소값.
  • Msg : Windows 기반 메시지 ID
  • WParam : Windows 메시지와 같이 나온 값 - Word (unsigned int) 단위의 값
  • LParam : Windows 메시지와 같이 나온 값 - Long (unsinged Long) 단위의 값

Windows API 프로그래밍을 해본적이 있다면 바로 직감이 오겠지만...
바로 저 PerFilterMessage가 PumpMessage를 의미하는 것이고,
Message 객체는 Windows 메시지로 처리할 때 쓰는 모든 값이다.

이제 필요한 메시지를 직접적으로 처리해 주면 되는 것이다.

Posted by 하인도

.NET을 이용해서 C++/C 로 만든 Windows DLL들을 호출하는 방법이 있다.

그 방법은 DllImport 라고 하는 System.Runtime.InteropServices 네임스페이스의 프로퍼티 클래스를 사용하면된다.

간단한 사용예는 아래와 같다.

[DllImport("twainDSM.dll")]
private static extern TwRC DSMSetParent( [In, Out] TwIdentity origin, IntPtr zeroptr, TwDG dg, TwDAT dat, TwMSG msg, ref IntPtr refptr );

DllImport 에 원하는 DLL을 선언하고, static extern 메소드에 걸어주면 된다.
만일 입출력이 겸해지는 파라미터 인 경우 [In,Out] 을 사용하면 되고, 입력이면 그대로 [in]을 쓰면 된다.

그런데, 하나의 DLL 함수에 여러가지 형태로 정의하고 싶은 경우가 있다.
입력값이 미묘하게 틀린경우 그에 맞추기만 하면 되는 경우다.

예를 들면 TWAIN에서 제공하는 DLL 함수 중 DSM_Entry 라는 함수가 있다.

TW_UINT16 FAR PASCAL DSM_Entry( pTW_IDENTITY pOrigin,
                                  pTW_IDENTITY pDest,
                                  uint    DG,
                                  uint    DAT,
                                  uint    MSG,
                                 (void**)    pData);

그 형태는 위와 같다. 즉 첫번째 두번째 파라미터는 규격화 된 것이지만, 맨 마지막 같은 경우에는 특정한 형태가 없는 값이다. 또, 두번째 파라미터가 null 인 경우도 있을 것이다. 호출하는 방법에 따라 그 방법이 다양한데, 이것을 C#에서는 아래와 같이 표현 할 수 있다.

[DllImport("twainDSM.dll", EntryPoint="#1")]
private static extern TwRC DSM_Entry1( [In, Out] TwIdentity origin, IntPtr zeroptr, TwDG dg, TwDAT dat, TwMSG msg, ref IntPtr refptr );

[DllImport("twainDSM.dll", EntryPoint="#1")]
private static extern TwRC DSM_Entry2( [In, Out] TwIdentity origin, IntPtr zeroptr, TwDG dg, TwDAT dat, TwMSG msg, [In, Out] TwIdentity idds );

[DllImport("twainDSM.dll", EntryPoint="#1")]
private static extern TwRC DSM_Entry3( [In, Out] TwIdentity origin, IntPtr zeroptr, TwDG dg, TwDAT dat, TwMSG msg, [In, Out] TwStatus dsmstat );

위의 첫번째 예에서 달라진 것은 EntryPoint를 명시한 것이다. 즉 DLL에서 제공하는 첫번째 메소드를 쓴다는 것이다. 함수 이름이야 뭐가 되든간에, 무조건 DLL에서 제공하는 첫번째 함수를 쓴다는 것이다. 파라미터도 최대한 DSM_Entry의 함수를 그대로 따르되, 두번째 파라미터가 Null 이라면 zeroptr에 IntPtr.Zero를 넣는 것이다. 그리고 맨 마지막 파라미터 같이 다양한 형태로 때려 박는 스타일일때는 그에 필요한 형을 그대로 집어넣는다. 그러면 .NET에서 DLL 함수와 연결할때, 알아서 해당 부분을 (void **)로써 제공할 뿐이라는 것이다. 다른 것들도 마찬가지로, 각 변수들의 크기만 잘 맞출 수 있다면 ( int 면 4바이트, short 이면 2바이트 등등) 그게 무슨 값이든 상관 없이 적용할 수 있다는 것이다.

Unmanaged .NET은 하면 할 수록 나름 매력이 솔솔 풍기는 기분이다.

Posted by 하인도

.NET 2.0 부터 제공되는 Generic 형 Class. System.Collection.Generic 이라는 네임스페이스에 존재하는 내용이다. Collection에서는 모든 값들을 Object를 사용하기 때문에 매번 Casting 을 해야 했고, 그에 따른 성능상 문제로 인해 대안점 처럼 꺼내든 것이다. 뭐 Generic의 역사나 기타 방향성문제는 다른 문서들을 참고하시고..

여기서는 generic에 있는 각 형태에서 제공하는 find라는 함수를 이용하는 방법을 언급하려 한다. 물론 foreach와 같은 순환문을 이용해서 직접 find 함수를 구현할 수 있지만, 최소한 내부에 구현된 find 함수의 성능이 그나마 내가 직접 짠 것 보다는 좋을 것이라는 생각에 최대한 애용하는 편이다.

이 find 함수의 기본 형은 아래와 같다.

public T Find (
	Predicate<T> match
)

 

위의 형태에 대한 정의 내용을 보면 Predicate<T> 라는 게 있는데, 바로 이 부분에 대리자 역할을 하는 함수가 필요로 한다. match 라는 의미는 자신이 찾는 값의 형태인 경우에는 true 혹은 false를 돌려달라는 의미다.

여기서 개인적으로 가진 의문은 왜 find를 저렇게 구현했을까 했던 점이다. 만일 List<int> aaa 라는 개체를 만들었다면, aaa.find(1111) 식으로 aaa.find(<T>) 형태로 제공되었으면 그냥 썼을텐데, 저런식으로 만드니 원 접근이 될 터인가.. 했다. 하지만, 정작 <T> 안의 값의 형태가 복합형이고, 그 안에 찾는 조건이 복잡한 경우라면 오히려 위의 방법은 좋은 대안 식이라고나 할까?

 

일단 MSDN에서 제공되는 예제를 먼저 보자.

using System;
using System.Collections.Generic;

public class Example
{
    public static void Main()
    {
        List<string> dinosaurs = new List<string>();

        dinosaurs.Add("Compsognathus");
        dinosaurs.Add("Amargasaurus");
        dinosaurs.Add("Oviraptor");
        dinosaurs.Add("Velociraptor");
        dinosaurs.Add("Deinonychus");
        dinosaurs.Add("Dilophosaurus");
        dinosaurs.Add("Gallimimus");
        dinosaurs.Add("Triceratops");

        Console.WriteLine("\nFind(EndsWithSaurus): {0}", 
            dinosaurs.Find(EndsWithSaurus));
    }

    // Search predicate returns true if a string ends in "saurus".
    private static bool EndsWithSaurus(String s)
    {
        if ((s.Length > 5) && 
            (s.Substring(s.Length - 6).ToLower() == "saurus"))
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

 

끝의 글자가 saurus 로 끝나는 명칭을 찾는 로직. 저기서 보면 Find 안에 특정 값이 들어간게 아니고, 특정 함수가 들어가 있다. 즉 값의 결과가 true/false로 떨어지는 독자적인 함수가 있으면 된다.  위의 예제는 바로 List에 담긴 내용들 중 맨 끝의 글자가 saurus가 들어 있는지 체크한 뒤 있으면 true, 없으면 false로 돌려준다.

대충 이해가 되려나 싶다.

그런데, 내가 걸린 문제는 이렇다. 위의 예제대로라면, 실제 find를 사용하는 것은 그 찾으려는 값이 밖에 있다는 사실. 만일 위의 예제대로 라면, 찾기 로직 안에 saurus 라는 값 처럼 아예 박혀 있어야 되는데, 실제로는 저 값이 외부에서 받는 값일때는 걸리게 된다.

예를 들어보자. 사용자가 검색하는 단어를 기준으로 찾는다고 하자.
입력되는 값에 따라 찾는 내용이 달라져야 되는데, 이 경우에는 위와 같은 방법을 사용하면 곤란해질 수 밖에 없다. 그렇다면?

위의 예제를 보면 함수를 넣기 위해 static private로 만들었는데, 그것을 delegate라는 대리자를 사용해서 원큐로 만드는 것이다. txtInput 이라는 Text 박스에서 입력값이 생겼을 때 찾기 명령을 실행 시키는 예제로직을 만들어보았다.

 

List<string> aryData = new List<string>();

public Test()
{
    aryData.Add("apple");
    aryData.Add("pineapple");
    aryData.Add("steel");
}

private void txtInput_TextChanged(object sender, EventArgs e)
{
    string sResult = aryData.Find(delegate(string s) {
        return s.Equals(txtInput.Text);
    });

    if(string.IsNullOrEmpty(sResult))
        MessageBox.Show(sResult);
}

 

대략적으로 보면 알겠지만,

Find 라는 함수 뒤에 delegate 라는 대리자를 선언하고, 그 안에 가상 함수로 들어오는 파라미터 값을 정의한다. string s 부분이 있는데, 그 부분이 바로 Find 에서 역으로 호출 될 때 들어오는 값이다. 만일 List<int> 라면 int 가 되어야 할 것이다.

그리고 최종적으로 돌려주는 값은 true, false로 나올 수 있는 값이면 된다.

그러면 실제적으로 Find의 Return 값은 찾은 값을 돌려주게 된다. 지금은 List<string> 이니 돌려주는 값은 당연히 string. 만일 int면 int가 될 것이다.

 

정리하며.

물론 사람이 만든 코드이기 때문에, 성능이 월등하리라 볼 수는 없다. 하지만, 최소한 그 분야의 대가들이며, 또 내부적인 구현 시, 순수 .NET으로 만들지 않고, 성능을 위해 억지로 Binary로 만들기도 한다. 즉 알지는 못하나, 최소한 날로 짜는 것 보다 더 나은 성능을 보여줄 수 있다는 것이다.

이런 내 함수들을 활용 방법이 의외 복잡하더라도 작정하고 사용하다 보면 지금 보다 더 나은 성능을 보장할 수 있다.

 

복합형 값용 List<T>.Find 사용방법 예제.

사람 정보 중 이름이 같은 데이터를 메시지 박스에 표현 하는 방법

class PersonData
{
    public PersonData(string sName, string sSocialNo)
    {
        _name = sName;
        _socialNo = sSocialNo;
    }

    string _name;
    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value;
        }
    }

    string _socialNo;
    public string SocialNo
    {
        get
        {
            return _socialNo;
        }
        set
        {
            _socialNo = value;
        }
    }

}

List<PersonData> aryData = new List<PersonData>();

public Test()
{
    aryData.Add(new PersonData("홍길동", "123444-444422"));
    aryData.Add(new PersonData("조조", "123444-444422"));
    aryData.Add(new PersonData("유비", "123444-444422"));
}

private void txtInput_TextChanged(object sender, EventArgs e)
{
    PersonData sResult = aryData.Find(delegate(PersonData p) {
        return p.Name.Equals(txtInput.Text);
    });

    if(sResult != null)
        MessageBox.Show(sResult.Name + sResult.SocialNo);
}

Posted by 하인도

단순한 컨트롤인 TextBox 나 ListBox 같은 것은 그냥 쓰면 되는데, 복합 컨트롤을 만들 필요가 있는 경우 사용자 컨트롤을 만들어 사용하곤 한다. 즉 TextBox와 ListBox가 하나로 합쳐진 특수 목적 컨트롤 같은 것을 만들때다.

compositectrl1

저렇게 컨트롤 안에서 동작만 되면 문제가 없는데, 만일 외부로 이벤트를 노출 시킬 필요가 있는 경우가 있다. 위의 그림을 예로 들면 아랫쪽이 ListBox인데, ListBox의 선택이 변경되는 경우 저 통짜 컨트롤을 쓰는 곳에서 그 이벤트를 받기 위해서 구성하는 경우다.

어떻게 해야 될까 고민을 하다가 링크 하나를 발견했다. ( http://www.akadia.com/services/dotnet_user_controls.html )

만드는 방법은 간단하다. 딱 두 줄에 해당하는 내용을 넣으면 된다.

public delegate void 이벤트이름Handler();
public event 이벤트이름Handler 이벤트이름;

위와 같이 구성하면 되는데, 위의 컨트롤에서 사용한 내용은 아래와 같다.

// 컨트롤 내부 소스
public delegate void SelectChangedHander(); 
event SelectChangedHander SelectChanged;

event로 SelectChanged 라는 이름으로 하고 밖에서 연결할 때 사용하는 끈 역할을 하는 Handler를 delegate(대리자)로 선언하여 처리하는 것이다. 그러면 밖에는 이 Handler를 통해 이벤트 가입을 하는 것이다. 가입방법은 예전에도 많이 봤던 그런 형태가 된다. ( 저 이벤트를 받을 메인 프로그램에 선언하는 그 형태. )

// 컨트롤을 사용하는 메인 프로그램 소스
this.categoryCtrl.SelectChanged += new CategoryCtrl.SelectChangeHander(this.categoryCtrl_SelectChanged);

그러면 컨트롤 측에서 이벤트를 발생을 어떻게 하는 걸까?
그 역시 간단하다.

이벤트 이름을 함수로 호출하듯 하면 된다.

// 컨트롤 내부 소스 중 이벤트가 발생되는 시점
if (SelectChanged != null)
   SelectChanged();

보면 뜬금 없이 SelectChage가 null 인지 체크하는 경우가 있는데, 컨트롤이 제대로 초기화되지 않은 상황에서 호출되는 불상사를 막기 위한 안전 장치이다. 무시해도 되긴 하지만, 가급적이면 저렇게 안전장치를 거는걸 권장한다.

자,위와 같이 구성했다면, 이렇게 된다.

컨트롤내부 실행 중

-> SelectChange() 함수 호출

-> SelectChanageHandler로 연결한 모든 부분에 이벤트 발송

-> SelectChangedHandler에 인자값으로 넘긴 함수를 호출
   (위의 예제로 있는 categoryCtrl_SelecChanged() 함수 같은거)

기존에는 그냥 만들어진 이벤트를 소비하기만 했다면 자신만의 이벤트로 작업을 한다면 이런 부분을 활용해 보는 것도 나쁘지 않을 것이다.(생각해보니, 이렇게 만들 수 밖에 없기는 하다.)

Posted by 하인도