저작권에 대한 공지
이 문서에 포함된 모든 글과 소스 코드는 작자인 저 김성동의 지적 재산이지만 무단으로 배포하거나 책이나 강의 교재등을 위한 복제 등 저작권을 저해하는 행위를 제외하고는 자유롭게 사용할 수 있습니다.
또한 이 페이지에 대한 링크는 자유롭게 할 수 있으나 전문을 전제하는 일은 허용하지 않습니다.


1.8. 메시지 처리

1.8.1. 개요
윈도가 이벤트 지향, 메시지 기반 운영체제라는 사실은 윈도 프로그램을 한번이라도 공부해 본 사람은 익히 들어 알고 있을 것이다. 메시지는 마우스를 움직인다든지 키보드로 입력하는 등의 사건이나 윈도 크기가 변경되었다든지 시스템 색을 변경했다든지 하는 상태 변화등에 의해서 발생한다. 메시지가 발생하면 윈도는 두 가지 방법으로 메시지를 전달하는데 첫번째로 각 어플리케이션마다 가지고 있는 메시지 큐에 메시지를 추가하는 방법이 있고 두번째로 빠른 처리를 위해 어플리케이션의 윈도 프로시저에 직접 메시지를 전달하는 방법이 있다.

그림 1-14 윈도 메시지 전달 흐름


메시지 큐는 FIFO(First In/First Out)구조로 동작하며 임시로 메시지를 저장한다. 어플리케이션의 메시지 큐에 저장된 메시지는 어플리케이션의 WinMain 함수에서 큐에 저장된 순서대로 처리되는데 WinMain 함수는 지속적으로 큐를 검사해서 메시지가 있으면 적절한 윈도에 메시지를 전달하고 그 메시지를 메시지 큐에서 삭제하며 메시지 큐에 WM_QUIT 메시지가 들어올 때까지 이런 작업을 반복한다.
어플리케이션의 각 윈도(포커스를 가질 수 있는 각종 콘트롤도 포함된다.)는 메시지를 실제로 처리해 주는 윈도 프로시저라는 것을 가지고 있다. RegisterClass나 RegisterClassEx API를 이용해서 윈도 클래스를(Window Class)를 등록할 때 윈도 프로시저를 지정하게 되어 있고 CreateWindowEx등의 API로 등록한 윈도 클래스의 인스턴스를 생성하면 그 윈도의 메시지는 등록할 때 지정한 윈도 프로시저에게 보내진다. 윈도 프로시저는 네 개의 인자를 가지는 함수로 선언하며 보통 case 문을 사용해서 구현한다.
아래 코드는 윈도 프로시저의 대표적인 예이다.

function WndProc(hWnd : THandle; nMsg : UINT; wParam, lParam : Cardinal) : Cardinal; export; stdcall;
begin
  Result := 0;
  case nMsg of
    WM_LBUTTONDOWN :
      begin
        { 생략 }
      end;
    WM_LBUTTONUP :
      begin
        { 생략 }
      end;
    WM_MOUSEMOVE :
      begin
        { 생략 }
      end;
    WM_SIZE :
      begin
        { 생략 }
      end;
  else
    DefWindowProc(hWnd, nMsg, wParam, lParam);  
  end;
end;


윈도 프로시저의 첫번째 인자인 hWnd는 메시지를 받는 윈도의 핸들을 나타내며 두번째 인자 nMsg는 처리할 메시지를 나타내며 Messages.pas 유닛에 정의되어 있다. 세번째, 네번째 인자인 wParam과 lParam은 nMsg에 어떤 메시지가 들어 오느냐에 따라 다른 의미를 가진다. 예로 nMsg가 WM_LBUTTONDOWN 메시지의 경우 wParam 은 눌려진 키에 대한 가상 코드가 넘어오고 lParam에는 현재 마우스 포인터의 위치가 넘어 온다. WM_SIZE인 경우에는 wParam에 SIZE_MAXIMIZED, SIZE_MINIMIZED 등의 크기 조절 타입이 넘어오며 lParam에는 윈도 크기가 넘어 온다. 델파이에서는 각 메시지에 따라 wParam과 lParam의 의미를 쉽게 이해할 수 있도록 messages.pas 유닛에 각각의 메시지에 대해 별도로 구조체를 정의해 놓았다. 마우스 관련 메시지인 경우 TWMMouse 구조체를 사용하는데 아래와 같이 선언되어 있다.

TWMMouse = packed record
    Msg: Cardinal;
    Keys: Longint;
    case Integer of
      0: (
        XPos: Smallint;
        YPos: Smallint);
      1: (
        Pos: TSmallPoint;
        Result: Longint);
  end;


특별히 구조체를 정의해 놓지 않은 메시지의 경우에는 아래와 같이 TMessage 구조체를 사용한다.

PMessage = ^TMessage;
  TMessage = packed record
    Msg: Cardinal;
    case Integer of
      0: (
        WParam: Longint;
        LParam: Longint;
        Result: Longint);
      1: (
        WParamLo: Word;
        WParamHi: Word;
        LParamLo: Word;
        LParamHi: Word;
        ResultLo: Word;
        ResultHi: Word);
  end;


1.8.2. 윈도 메시지
위에서 설명한 윈도 메시지 처리 방법은 델파이에서는 실제로 거의 쓰이지 않는다. 처리해야 할 메시지가 많은 경우 윈도 프로시저의 크기가 커지고 복잡해져서 코드를 관리하기가 상당히 힘이 들기 때문에 델파이에서는 사용하기 쉽게 메시지 핸들러 메소드라는 개념을 만들어 놓았다. 실제로 앞에서 설명한 가상 메소드와 유사한데 virtual 지시자 대신에 message 지시자를 사용한다.

procedure WMLButtonDown(var Message: TWMLButtonDown); message WM_LBUTTONDOWN;
procedure WMLButtonDown(var Message: TWMLButtonDown); message WM_LBUTTONDOWN;
procedure WMMouseMove(var Message: TWMMouseMove); message WM_MOUSEMOVE;
procedure WMLButtonUp(var Message: TWMLButtonUp); message WM_LBUTTONUP;
procedure WMCancelMode(var Message: TWMCancelMode); message WM_CANCELMODE;
procedure WMSize(var Message: TWMSize); message WM_SIZE;
procedure WMKeyDown(var Message: TWMKeyDown); message WM_KEYDOWN;
procedure WMChar(var Message: TWMChar); message WM_CHAR;


메시지 핸들러 메소드는 위와 같이 선언하는데 반드시 프로시저로 선언해야 하며 이름은 일반적으로 위의 예처럼 처리하는 메시지가 무엇인지 알 수 있도록 하지만 아무렇게나 이름을 지어도 상관없다. 그리고 하나의 인자를 가지는데 반드시 var 형으로 선언해야 하며 마지막에 message 지시자와 처리할 메시지를 써 준다.
이렇게 처리하고자 하는 메시지에 대해 메시지 핸들러 메소드를 선언해 주면 메시지가 도착할 때 마다 해당 메시지 핸들러를 호출해 준다. 그런데 앞에서 얘기한 메시지 큐하고는 개념이 완전히 다른 것 같다. 어떻게 이렇게 처리가 될까? Controls.pas를 열어서 TControl을 선언한 곳으로 가서 찬찬히 살펴 보면 WndProc이라는 가상 메소드가 정의되어 있음을 알 수 있다. 그리고 WndProc 메소드는 아래와 같이 정의되어 있다.

TControl = class(TComponent)
  protected
    { 생략 }
    procedure WndProc(var Message: TMessage); virtual;
    { 생략 }
  end;

{ 생략 }

procedure TControl.WndProc(var Message: TMessage);
var
  Form: TCustomForm;
begin
  if (csDesigning in ComponentState) then
  begin
    { 생략 }
  end
  else if (Message.Msg >= WM_KEYFIRST) and (Message.Msg <= WM_KEYLAST) then
  begin
    { 생략 }
  end
  else if (Message.Msg >= WM_MOUSEFIRST) and (Message.Msg <= WM_MOUSELAST) then
  begin
    { 생략 }
  end
  else if Message.Msg = CM_VISIBLECHANGED then
    with Message do
      SendDockNotification(Msg, WParam, LParam);
  Dispatch(Message);
end;


WndProc 메소드는 마지막에 TObject 클래스에 정의된 Dispatch 메소드를 호출하고 있는 것을 볼 수 있는데 Dispatch 메소드는 message 지시자로 선언해서 객체의 메소드 테이블에 추가된 메시지 핸들러 중에서 Message 인자의 Msg 필드와 연결된 메시지 핸들러가 있는지 검사해서 있다면 그 메시지 핸들러를 호출해 주고 없으면 DefaultHandler를 호출해 주도록 되어 있다.

1.9. 요약

지금까지 콤포넌트를 개발할 때 필요한 여러가지 개념과 원리에 대해서 알아 보았다. 이 장에서 살펴 본 내용은 콤포넌트를 개발함에 있어 토대가 되는 개념이므로 잘 숙지하고 있어야 한다. 여기서 설명한 내용 이외에도 윈도 시스템의 전통적인 프로그래밍 방법과 기본적인 원리, 예를 들어 글꼴, GDI, 리소스, 시스템 메시지, 시스템 API 등에 대해서도 알아 두면 많은 도움이 될 것이다.
다음 장에서는 이번 장에서 설명한 개념들이 어떻게 실제로 적용되는지 여러 가지 콤포넌트를 개발해 보면서 자세하게 알아 보도록 하겠다.


저작권에 대한 공지
이 문서에 포함된 모든 글과 소스 코드는 작자인 저 김성동의 지적 재산이지만 무단으로 배포하거나 책이나 강의 교재등을 위한 복제 등 저작권을 저해하는 행위를 제외하고는 자유롭게 사용할 수 있습니다.
또한 이 페이지에 대한 링크는 자유롭게 할 수 있으나 전문을 전제하는 일은 허용하지 않습니다.