Message Map 사용

API 2009. 1. 22. 10:13

메시지 맵이란 무엇인가?


메시지 맵을 이해 하기 전에 먼저 WIN32 API가 어떤 방식으로 구동 되는 지를 알아야 한다.  윈도우 GUI프로그래밍 언어인 WIN32 API는

메시지 처리방식으로 프로그램이 움직인다.

메시지 처리방식이란 런타임중 환경의 변화등이 있었을때 예를 들어 키보드를 눌렀다거나 화면에 변화가 왔다거나 하는 등의 변화가 있을때

운영체제인 윈도우로 부터 메시지가 송신되고 우리가 만든 프로그램에서 그메시지를 처리하는 방식으로 프로그램이 움직이게 된다.

API에는 이 메시지 처리를 담당하는 함수가 존재하는데 그것을 메시지 처리 프로시저라고도  한다.

메시지 처리 프로시저의 프로토타입(함수 원형)은  다음과 같다.

LRESULT CALLBACK WndProc(HWND ,UINT ,WPARAM ,LPARAM )

이런 프로시저를 보통 사용할 때는 다음과 같이 사용한다.

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    switch(iMessage)

    {

       case WM_PAINT:

                /화면에 변화가 있었을 때 WM_PAINT 메시지가 발생하고 그때 처리해야할 일들을 이곳에서 처리/

          break;

       case WM_KEYDOWN:

                /키보드가 눌려지면 WM_KEYDOWN 메시지가 발생하고 그때 처리해야할 일들을 이곳에서 처리/

          break;

             .

             .

             .

    }

 return (DefWindowProc(hWnd,iMessage,wParam,lParam));
}

위에서 볼수 있는 것과 같이 프로그램 구동중에 생길 수 있는 모든 메시지들을 처리하게 되며 메시지가 왔을때 그것을 처리하기 위해

분기명령인 switch문을 사용 하게 된다.

프로그램의 크기가 적어 처리 해야할 메시지가 많지 않을 경우에는 별 문제가 되지 않지만 프로그램의 크기가 커져  처리해야할 메시지가 많아지고 각 메시지당 처리해야할 일들이 많아진다면 소스코드의 복잡성이 늘어나며 소스의 이해도가 떨어지게 된다.


어차피 switch문은 해당 하는 곳으로 바로 점프하는 분기방식아닌가 !!!!

해당 하는 곳말고는 없는 코드나 마찬가지가 아닌가 !!!!

메시지가 올때마다 해당하는 메시지에 맞는  함수를 호출 시켜 줄수 있다면 !!!!!!


이런 생각에서 나온게 바로 메시지 맵이란 기법이다.


사설이 길었다..... 본론으로..

메시지 맵이란 각각의 메시지 처리함수로 만들어 처리함수 마다 해당 메시지에 대한 처리만을 담당하게 하고 메시지가 호출되면 해당 함수를

호출 시켜주는 기능을 말한다.


기존의 메시지 처리함수(WndProc)에서는 들어오는 메시지를  메시지 맵에서 찾아 해당 하는 메시지 처리함수를 호출하는 일만 처리하게 된다.


이렇게 메시지 맵 기법이 사용 가능한것은 모든 메시지가 같은 메시지 처리함수에서 처리된다는 점 때문이다.

같은 함수내에서 처리하는 방식만 틀리기 때문에 함수포인트를 이용해서 함수를 분리해 내는 것이 가능한것이다.


여기서 또하나 !!

함수 포인터란 무었인가?

아는 사람은 살포시 패스~!! 모르는 사람은 필독!! 모르면  다음 부터 이해하기 힘이 든다.

프로그램을 실행 시킨다는 의미는 프로그램을 메모리에 올린다는 의미다. 변수가 메모리에 올라가듯 함수역시 메모리 어딘가에 올라가게 되는 것이다.

메모리에 올라간다는 것은 당연 해당 번지가 할당되게 되며 그 해당 번지가 함수의 주소가 되는 것이다. 해당 주소를 찾아가면 그 함수를 실행

시킬수 있게 되는 것이다. 하지만 무턱대고 찾아간다고 함수가 실행 되는 것이 아니다. 변수 역시 자료형을 알아야  포인터를 이용해서 사용이 가능하듯 함수 역시 자료형이 존재 하며 함수의 자료형은 그함수의 프로토타입을 의미하기도 한다. 함수의 자료형이라고 어렵게 생각할거 없다.

함수의 프로토 타입에서 함수의 이름부분을 ( * ) 로 바꿔주면 그만이다.


이쯤에서 함수포인터에 대한 연습한번 !!

기존의 메시지 처리함수의 프로토타입(원형)을 가지고 함수 포인터를 만들어 보자


LRESULT  WndProc(HWND ,UINT ,WPARAM ,LPARAM )

프로토 타입이 위와 같으므로  메시지 처리 함수의 자료형은 다음과 같이 만들수 있다.


LRESULT (*) (HWND ,UINT ,WPARAM ,LPARAM );

 

붉은색 으로 표시한 곳을 보듯이 함수명 부분을 (*)로 변경 해주면 자료형이다. 어렵지 않다.

함수의 자료형이 있다면 함수형의 변수 역시 선언 가능하지 않겠는가!!

그렇다 있다.

(*) 부분에 변수 명을 써주면 된다. 어떻게? ( * Fxn) 이렇게 쓰면 된다.

LRESULT (*)(HWND ,UINT ,WPARAM ,LPARAM ) 형 함수를 가리키는 함수포인터 Fxn을 선언 하는 것이다.



- 메시지 맵의 구현

1.메시지 맵에 사용할 구조체를 생성 한다.

typedef struct _DecodeMsg
{
 UINT Code;
 LRESULT (*Fxn)(HWND ,UINT ,WPARAM ,LPARAM );
}DecodeMsg;


Code는 실제 메시지의 값을 저장할 곳이며 아래있는 함수 포인터는 메시지 처리함수에서 발생한 메시지와 Code를 비교하여 해당하는 함수포인트를 호출할때 사용하게 될것이다.


2.메시지 맵 구현( 메시지와 해당하는 함수포인트로 구성된 구조체의 배열 )

const DecodeMsg MsgProc[] = {      // : []는 배열을 나타내는 대괄호다. 폰트가 대괄호를 []로 표시해 버린다....  ;;
 WM_PAINT,Dopaintmain,
 WM_CREATE,Docreatemain,
 WM_TIMER,Dotimermain,
 WM_KEYDOWN,Dokeydownmain,
 WM_DESTROY,Dodestroymain

                     .

                     .

                     .
};
위에서 Dopaintmain,Docreatemain,Dotimermain등은 미리 정의 되어있는 각각의 메시지를 처리하게될 함수다.

함수의 이름은 그 함수의  해당 메모리 주소를 나타낸다. 배열의 이름이 배열의 시작 번지를 나타내는 것이랑 같다고 생각하자


각각의 함수 프로토 타입은

LRESULT Dopaintmain(HWND hWnd , UINT iMessage , WPARAM wParam , LPARAM lParam)

처럼 메시지처리 함수(WndProc)과 같은 타입으로 정의 되어야 한다. 그래야 함수 포인터를 사용해서 호출이 가능하다.


3.메시지 처리함수 내에서의 메시지 비교 와 함수 호출( 실재 메시지 맵을 사용해서 메시지를 처리하는 모습 )

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
 int Cnt;
 for (Cnt = 0;Cnt < dim(MsgProc);Cnt++)
 {
    if (iMessage == MsgProc[Cnt].Code)
    {
       return (*MsgProc[Cnt].Fxn)(hWnd,iMessage,wParam,lParam);
    }
 }
 return (DefWindowProc(hWnd,iMessage,wParam,lParam));
}

메시지가 발생 하게 되면 기존에 만들어 두었던 메시지 맵의 Code와 비교를 하게된다. 비교 후 같은 메시지를 발견 하게되면

해당하는 메시지의 함수를 호출하게 된다.


메시지 맵에서 메시지를 찾을수 없는(메시지맵에서 처리하지않은)메시지는 반복문을 빠져나와

DefWindowProc 함수가 이를 처리하게 된다.


실재 MFC가 이런 메시지 맵 방식으로 메시지를 처리하며 메시지 맵이 어떤 방식으로 움직이는 지를 알아 놓으면  MFC를 이해 하는데 많은 도움이

될것이며 이런 기법을 응용해서 좀더 구조적인 프로그래밍을 할 수 있을 것이다.

자 메시지 맵은 여기까지 !!

 
Posted by 응이

#include<windows.h>
#define ID_WRITE 100
LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
DWORD WINAPI MechaRead(LPVOID NotUse);

HINSTANCE g_hInst;
HWND HWndMain;
LPCTSTR lpszClass=TEXT("메카트로닉스");
int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance
           ,LPSTR lpszCmdParam,int nCmdShow)
{
  HWND hWnd;
  MSG Message;
  WNDCLASS WndClass;
  g_hInst = hInstance;

  WndClass.cbClsExtra=0;
  WndClass.cbWndExtra=0;
  WndClass.hbrBackground=(HBRUSH)(COLOR_BTNFACE+1);
  WndClass.hCursor=LoadCursor(NULL,IDC_ARROW);
  WndClass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
  WndClass.hInstance=hInstance;
  WndClass.lpfnWndProc=WndProc;
  WndClass.lpszClassName=lpszClass;
  WndClass.lpszMenuName=NULL;
  WndClass.style=CS_HREDRAW | CS_VREDRAW;
  RegisterClass(&WndClass);

  hWnd=CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
    NULL,(HMENU)NULL,hInstance,NULL);
  ShowWindow(hWnd,nCmdShow);

  while(GetMessage(&Message,NULL,0,0)){
    TranslateMessage(&Message);
    DispatchMessage(&Message);
  }
  return (int)Message.wParam;
}
//위 까지는 기본적인 API 메인 부분이다
HWND  PrintWnd,WriteWnd,WriteWnd2;//각종 컨트롤들의 제어를 위한 핸들러
HANDLE  hComm;//통신을 위해 사용할 포트 핸들러를 저장할 변수
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
  HDC        hdc;
  PAINTSTRUCT    ps;
  DWORD      dwWritten;//포트를 열고 읽을 때 사용할 변수로 얼마나 읽고 썼는지를 담을 변수
  DCB        sPState;//시리얼 통신포트 설정을 위한 구조체
  const char    *ucpData = "TEST ";//테스트용으로 사용할 전송 메시지
  DWORD      ThreadID;//스레드 생성시 얻어지는 아이디
  static HANDLE  hThread;//스레드 핸들러
 
COMMTIMEOUTS  cTime; //타임아웃 구조체(통신의 지연시간 대기시간 등등)
  TCHAR Buff[256];//버퍼로 사용할 공간
  int len;//문자열의 길이를 담을 변수
  switch(iMessage)
  {
  case WM_CREATE:
    HWndMain = hWnd;

//전송이나 수신에 사용할 에디터창을 생성하고 핸들러를 리턴 받고있다.
    CreateWindow(TEXT("static"),TEXT("전송받은 문자"),WS_CHILD|WS_VISIBLE,30,10,100,25,hWnd,(HMENU)-1,g_hInst,NULL);
    CreateWindow(TEXT("static"),TEXT("수신한 문자"),WS_CHILD|WS_VISIBLE,260,10,100,25,hWnd,(HMENU)-1,g_hInst,NULL);
    CreateWindow(TEXT("static"),TEXT("수신할 문자"),WS_CHILD|WS_VISIBLE,330,260,100,25,hWnd,(HMENU)-1,g_hInst,NULL);
    PrintWnd  = CreateWindow(TEXT("edit"), NULL, WS_CHILD|WS_VISIBLE|ES_MULTILINE|ES_AUTOVSCROLL|WS_BORDER,
      20, 40, 200, 200, hWnd, (HMENU)-1, g_hInst, NULL);
    WriteWnd2  = CreateWindow(TEXT("edit"), NULL, WS_CHILD|WS_VISIBLE|ES_MULTILINE|ES_AUTOVSCROLL|WS_BORDER,
      260, 40, 200, 200, hWnd, (HMENU)-1, g_hInst, NULL);
    WriteWnd  = CreateWindow(TEXT("edit"), NULL, WS_CHILD|WS_VISIBLE|ES_MULTILINE|ES_AUTOVSCROLL|WS_BORDER,
      20,260, 300, 25, hWnd, (HMENU)ID_WRITE, g_hInst, NULL);
//포트를 열고 핸들러를 리턴 받는다.
    hComm = CreateFile(TEXT("COM7"),GENERIC_READ | GENERIC_WRITE, 0 , NULL, OPEN_EXISTING, 0,0);
//오픈시 에러를 검출하고 있다. 에러라면 INVALID_HANDLE_VALUE(-1)이 리턴 된다.
    if(INVALID_HANDLE_VALUE == hComm)
    {
      MessageBox(hWnd, TEXT("포트 열 수 없음"), TEXT("ERROR"), MB_OK);
      DestroyWindow(hWnd);
      return 0;
    }
//4096 입력 버퍼 크기, 3096 츨력 버퍼 크기
    if(0 == SetupComm(hComm, 4096, 3096))         {
      MessageBox(hWnd, TEXT("버퍼 설정 에러"), TEXT("ERROR"), MB_OK);
      DestroyWindow(hWnd);
      return 0;
    }

//모든 출력을 중단하고 출력 버퍼 클리어 
    if( 0 == PurgeComm(hComm, PURGE_TXABORT | PURGE_TXCLEAR))      {
      MessageBox(hWnd, TEXT("버퍼 초기화 에러"), TEXT("ERROR"), MB_OK);
      DestroyWindow(hWnd);
      return 0;
    }
//포트설정 구조체의 크기
    sPState.DCBlength = sizeof(sPState);
 //시리얼 포트 상태를 조사한다.
    if(0 == GetCommState(hComm, &sPState))       {
      MessageBox(hWnd, TEXT("시리얼 상태 읽기 에러"), TEXT("ERROR"), MB_OK);
      DestroyWindow(hWnd);
      return 0;
    }

//타임아웃 구조체를 설정 하고 있다.

//한문자를 수신한후 다음 문자가 수신될때까지 기다리는 시간 이시간이 지나면 버퍼를 리턴한다.
    cTime.ReadIntervalTimeout      = MAXDWORD;

    cTime.ReadTotalTimeoutMultiplier  = 0;//한문자를 수신하는데 걸리는 시간

//읽을 바이트수에 ReadTotalTimeoutMultiplier 값을 곱한 값에 ReadTotalTimeoutConstant값을 더해

//총 타임아웃시간을 구할수 있다.
    cTime.ReadTotalTimeoutConstant    = 0; 
    cTime.WriteTotalTimeoutMultiplier  = 0;
    cTime.WriteTotalTimeoutConstant    = 0;
    SetCommTimeouts(hComm, &cTime);
//보우율,전송비트,페리티모드,스탑비트등을 설정 한다.
    sPState.BaudRate  = CBR_115200;
    sPState.ByteSize  = 8;
    sPState.Parity    = NOPARITY;
    sPState.StopBits  = ONESTOPBIT;
//설정한 포트 값을 적용 시킨다.
    if( 0 == SetCommState(hComm, &sPState))   //시리얼 포트 상태를 조사
    {
      MessageBox(hWnd, TEXT("시리얼 상태 설정 에러"), TEXT("ERROR"), MB_OK);
      DestroyWindow(hWnd);
      return 0;
    }

//스레트를 생성한다. MechaRead 함수는 아래 정의 되어 있는 수신용 함수다
    hThread = CreateThread(NULL, 0, MechaRead, NULL, 0, &ThreadID);
    return 0;

  case WM_PAINT:
    hdc = BeginPaint(hWnd,&ps);
    EndPaint(hWnd,&ps);
    return 0;
  case WM_COMMAND:
    switch(LOWORD(wParam))//컨트롤에서 메시지가 오면
    {
    case ID_WRITE://전송 부분의 컨트롤의 메시지였다면
      switch(HIWORD(wParam))
      {
      case EN_CHANGE://에디트창의 문자열에 변화가 있었을 경우
        len = GetWindowText(WriteWnd,Buff,sizeof(Buff));//에디트창의 문자열을 읽어 들인다
        if (Buff[len-1]=='\n')//문자열의 마지막이 엔터일때까지 기다리다 엔터라면 전송을 시작한다.
        {
          Buff[len] = '\0';
          WriteFile(hComm, Buff,lstrlen(Buff),&dwWritten,0);//버퍼의 내용을 전송한다.
          if (!dwWritten)
          {
            MessageBox(hWnd,TEXT("전송 실패"),TEXT("실패"),MB_OK);
          }
          SetWindowText(WriteWnd,TEXT(""));//전송후 에디트창의 값을 리셋한다.

          //전송후 전송한 메시지를 다른 에디터창에 나타낸다.
          SendMessage(WriteWnd2, EM_REPLACESEL, (WPARAM)TRUE,(LPARAM)Buff);
        }
        break;
      }
      break;
    }
    return 0;
  case WM_LBUTTONDOWN:

//마우스 왼쪽 클릭시 테스트용 메시지를 전송한다.
    WriteFile(hComm, ucpData, (DWORD)strlen(ucpData),&dwWritten,0);
    return 0;

  case WM_DESTROY:
    CloseHandle(hThread);//생성한 스레드는 반드시 닫아준다
    CloseHandle(hComm);//오픈한 포트 역시 닫아 주어야 한다.
    PostQuitMessage(0);
    return 0;
  }
  return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
//스레드 함수의 원형은 반드시 아래와 같아야 한다.
DWORD WINAPI MechaRead(LPVOID NotUse)
{
  TCHAR  tt[3]={};
  DWORD  dwRead;

  while(1)
  {
    Sleep(50);//스레드 함수에서 과도한 CPU의 점유를 막기위해 잠시 잠들어 있게 한다.

//포트를 통해 전송된 문자를 읽어 들인다. 읽어들인 문자가 없어도 블러킹 되어 있지 않고 리턴한다.

//위에서 타임아웃 구조체를 설정해 주었기 때문에 블러킹 되지 않는것이다.
    ReadFile(hComm, tt,2,&dwRead,NULL);
    if(0 != dwRead)//전송된 문자가 있었다면
    {

//에디터창에 수신된 문자열을 출력한다.
      SendMessage(PrintWnd, EM_REPLACESEL, (WPARAM)TRUE,(LPARAM)tt);
    }
  }
}
 

Posted by 응이

1)윈도우 스레드

- 스레드 생성하기

윈도우에서 스레드를 구동시키기 위해서는 Win32 API인 CreateThread 함수를 사용하면 된다. 이 함수는 윈도우에서 사용하는 가장 저수준의 스레드 구동 함수다.

함수의 원형은 다음과 같다.

CreateThread(

LPSECURITY_ATTRIBUTES lpThreadAttributes,                      //Security Descriptor

SIZE_T dwStackSize,                                            //초기 스택 크기

LPTHREAD_START_ROUTINE lpStartAddress,                         //스레드 함수

LPVOID lpParameter,                                            //스레드 인자

DWORD dwCreationFlags,                                         //생성 옵션

LPDWORD lpThreadId                                             //스레드 아이디

);

첫 번째 인자생성될 스레드에 대한 사용권한을 지정하는데 사용하며 보통 NULL로 설정하게 되고 이는 THREAD_ALL_ACCESS를 의미하므로 어떤 함수든 이 스레드의 핸들을 이용해서 스레드를 제어할수 있다.

 

두 번째 인자스택 크기는 생성되는 스레드의 스택 크기를 정하는 인자며 만약 0으로 주게 되면 기본 값으로 1MByte의 스택이 생성된다.

 

세 번째 인자lpStartAddress는 생성된 스레드가 실행시킬 함수며 함수의 원형은 아래와 같다.

DWORD WINAPI ThreadFunc(LPVOID lpParam)

 

네 번째 인자스레드로 실행 시킬 함수의 인자값이며 인자로 포이트 포인트가 필요한 이유는 인자의 수와 자료형을 알수 없기 때문이다.

 

다섯 번째 인자스레드 생성 옵션에 해당하며 이 인자에 0을 주면스레드가 생성된 후 스레드가 바로 동작하는 것을 으미한다. 반면 CREATE_SUSPENDED를 인자로 주면 스레드가 멈춰있는 상태로 생성되는 것을 의미한다. 만약 스레드가 Suspend, 즉 몀춰있는 상태라면 ResumeThread 함수를 통해서 실행 가능한 상태를 의미하는 동작대기(Ready)상태로 바꿀수 있다.

 

마지막 인자인 여섯 번째 인자생성된 스레드의 스레드 아이디 값을 받는 인자다.

 

 

- 스레드 생성예(스레드 생성 버튼 클릭시 스레드를 생성하는 예이다)

 

DWORD WINAPI ThreadFunc(LPVOID lpParam)   //스레드로 생성될 함수 원형은 정해진 틀에 맞게 만들어 줘야한다.

{

   AfxMessageBox(L"나는 스레드~");

   return 0;

}

void CUdpMfcDlg::OnBnClickedThread()                                  //버튼 클릭시 호출되는 함수로 스레드를 생성하게 된다.

{

    DWORD dwThreadID;                                                      //생성될 스레드의 아이디를 받을 변수다

    HANDLE hThread;                                                        //생성될 스레드의 핸들러를 받을 변수다.

    int i; //카운터로 사용할 변수

    for (i=0;5>i;++i)                                                      // 5개의 스레드를 생성하기 위해 반복문을 돌린다.

    {

       // CreateThread 함수의 인자는 (스레드사용권한,스택크기,호출함수,함수인자,생성옵션,생성된아이디) 며

       // 생성된 스레드의 핸들러를 반환한다.

         hThread = CreateThread( NULL , 0 , ThreadFunc , 0 , 0 , &dwThreadID ); //스레드 생성 함수를 호출

    }

    if (hThread == NULL)                                             //핸들러가 비어 있다면 스레드 생성에 실패 한 것이다.

    {

         AfxMessageBox(L"스레드 생성 실패!!");

    }

    else

    {

        AfxMessageBox(L"스레드 생성 성공");

        CloseHandle(hThread);                                   //스레드 핸들은 CloseHandle을 사용해서 해제 해주어야 한다.

    }

}

 

- 스레드 제어/종료

생성된 스레드 내에서 자신의 핸들을 얻고 싶을 때는 아래 함수를 사용해서 얻어낼수 있다.

HANDLE GetCurrentThread(VOID)

 

생성된 스레드 내에서 스레드를 끝내려 한다면 스레드 내에서 return해서 종료하거나 ExitThread()를 사용해서 종료 시킬수 있다.

VOID ExitThread(DWORD dwExitCode)

스레드를 생성 하게 되면 윈도우는 각 스레드를 관리하는 스레드 객체를 커널에서 마련해서 관리하게 된다. 그리고 스레드가 시작되면 스레드 객체의 스레드의 종료 살태 코드는 STILL_ACTIVE를 갖게 된다. 그렇지만 스레드가 종료되면 스레드 객체는 시그널(종료)된 상태가 되고 스레드의 종료 상태 코드는 ExitThread 함수 호출시 지정한 dwExitCode로 바뀌게 된다.

 
Posted by 응이

Dream come true.
응이

달력

태그목록