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


1.5. 메소드

1.5.1. 개요
비록 프로퍼티가 콤포넌트와 사용자간의 기본적인 인터페이스를 제공해 주지만 프로퍼티는 단지 콤포넌트의 속성만을 관리하지 콤포넌트의 기능을 표현할 수는 없다. 예를 들어 많은 콤포넌트들이 자신의 데이터를 클립보드에 복사하기, 잘라내기, 붙이기 등의 기능을 가지고 있는데 이런 작업들은 프로퍼티를 통해서 수행될 수 없다. 이처럼 프로퍼티로 표현할 수 없는 콤포넌트의 기능을 정의할 때 사용하는 것이 메소드이다. 메소드는 일반 함수나 프로시저처럼 정의하지만 해당 클래스와 강한 결속력을 가지게 된다.
메소드를 정의할 때
  • 콤포넌트를 사용하려면 반드시 호출해 주어야 하는 메소드
  • 특정 순서대로 호출해 줘야 동작하는 메소드
  • 콤포넌트의 프로퍼티나 이벤트를 비정상적인 상태로 만들어 버리는 메소드
등은 가급적 만들지 말아야 한다.
메소드의 이름은 축약 형태로 만들지 말고 가급적 기능을 나타내는 동사를 모두 사용해서 메소드 이름만 봐도 어떤 기능을 하는 메소드인지 알 수 있도록 작성해야 한다. 메소드가 함수라면 반환하는 값이 무엇인지도 알 수 있도록 해 주면 좋다. 아래 표는 잘 작성된 메소드 이름과 그렇지 못한 메소드의 예를 보여 준다.

표 1-4 좋은 메소드 이름
좋다나쁘다
procedure CutToClipboardProcedure CutClip
procedure SetHeightProcedure SetH
function GetTextLengthFunction GetLen


1.5.2. 메소드 포인터(Method Pointers)
메소드 포인터는 프로시저 포인터와 달리 독립 프로시저를 가리키지 않고 클래스의 메소드를 가리키는 포인터이다. 메소드 포인터는 델파이에서 주로 이벤트를 다룰 때 사용하는데 클래스와 클래스사이에 서로의 메소드를 연결하는데 사용하면 유용하다.
메소드 포인터 형은 프로시저 포인터 형과 같이 선언하지만 마지막에 of object 키워드가 추가된다.
아래 예제를 보면 ProcShow는 프로시저 포인터 변수로 MethodShow는 메소드 포인터 변수로 선언되었다. btnExecProc을 누르면 ProcShow 변수에 ShowValue 프로시저를 대입한 뒤 ProcShow를 실행하고 btnExecMethod를 누르면 TMethodPointerExam 클래스의 인스턴스를 생성한 뒤에 인스턴스의 ShowValue 프로시저를 MethodShow 변수에 대입하고 실행한다. 만약에 프로시저 포인터에 메소드 포인터를 대입하거나 그 반대의 경우로 코딩하면 아래 그림과 같이 형이 다르다는 컴파일 에러를 볼 수 있을 것이다.

리스트 1.11 메소드 포인터 예제
unit fMethodPointerExam;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TProcedurePointerType = procedure (Value : Integer);
  TMethodPointerType = procedure (Value : Integer) of object;

  TMethodPointerExam = class
    procedure ShowValue(Value : Integer);
  end;

  TForm1 = class(TForm)
    btnExecProc: TButton;
    btnExecMethod: TButton;
    procedure btnExecProcClick(Sender: TObject);
    procedure btnExecMethodClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    ProcShow : TProcedurePointerType;
    MethodShow : TMethodPointerType;
  end;

var
  Form1: TForm1;

procedure ShowValue(Value : Integer);

implementation

{$R *.DFM}

procedure ShowValue(Value : Integer);
begin
  Application.MessageBox(PChar('Value = ' + IntToStr(Value)),
                         PChar('프로시저 포인터'),
                         MB_ICONINFORMATION + MB_OK);
end;

{ TMethodPointerExam }

procedure TMethodPointerExam.ShowValue(Value: Integer);
begin
  Application.MessageBox(PChar('Value = ' + IntToStr(Value)),
                         PChar('메소드 포인터'),
                         MB_ICONINFORMATION + MB_OK);
end;

procedure TForm1.btnExecProcClick(Sender: TObject);
begin
  Self.ProcShow := ShowValue;
  Self.ProcShow(1004);
  // Self.MethodShow := ShowValue;
  // 위와 같이 대입하면 컴파일 시 형 호환 에러가 난다.
end;

procedure TForm1.btnExecMethodClick(Sender: TObject);
var
  TempClass : TMethodPointerExam;
begin
  TempClass := TMethodPointerExam.Create;
  try
    Self.MethodShow := TempClass.ShowValue;
    Self.MethodShow(1004);
    // Self.ProcShow := TempClass.ShowValue;
    // 위와 같이 대입하면 컴파일 시 형 호환 에러가 난다.
  finally
    TempClass.Free;
  end;
end;

end.




그림 1-9 형 호환 컴파일 에러


1.6. 이벤트

1.6.1. 개요
어플리케이션 사용자가 화면에서 버튼을 누르면 버튼 콤포넌트는 OnClick 이벤트를 발생시킨다. 버튼 위에서 마우스를 움직이면 마우스 포인터의 위치가 변경 되었다는 이벤트인 OnMouseMove가 발생한다. 키보드로 글자를 입력하면 OnKeyPress 이벤트가 발생한다. 이처럼 이벤트는 콤포넌트 내부에서 어플리케이션 개발자가 처리해 줄 필요가 있는 어떤 사건이 발생했다는 것을 알려 주고 사건을 처리할 시작점을 제공하는 역할을 한다.
어플리케이션 개발자는 이벤트 핸들러를 작성해서 이벤트에 대한 적절한 처리를 해준다. 이벤트 핸들러는 보통 이벤트를 발생시킨 콤포넌트를 포함하고 있는 폼 클래스의 메소드로 정의된다. 어플리케이션 개발자가 이벤트 핸들러를 작성해 준다는 것은 콤포넌트로부터 상속 받아서 원하는 기능을 하는 새로운 콤포넌트를 작성하지 않고 콤포넌트의 기능을 확장한다라고 얘기할 수 있다. 즉 이벤트는 새 콤포넌트를 만들지 않고 원하는 기능을 그 콤포넌트에 추가할 수 있는 여지를 남겨 준다는 말이 된다.
예를 들어 버튼 콤포넌트는 OnClick 이벤트를 가지고 있는데 사용자가 버튼을 누를 때마다 OnClick이벤트는 계속 발생하고 어플리케이션 개발자는 오브젝트 인스펙터로 OnClick 이벤트 핸들러를 작성해서 버튼이 눌려질 때 마다 적당한 응답을 해주도록 콤포넌트의 기능을 확장할 수 있다.
이벤트는 실제로 메소드 포인터로 구현되는데 리스트 6.12에서 버튼 콤포넌트 btnExecProc의 OnClick 메소드 포인터에 폼 클래스의 btnExecProcClick 메소드가 연결되는 것처럼 한 클래스와 다른 클래스 사이에 서로의 메소드를 연결시켜주는 역할을 한다.
이벤트를 만들려면 메소드 포인터 형의 변수를 하나 만들고 이 변수를 직접 조작하는 프로퍼티를 선언해 주면 된다. 이벤트도 프로퍼티이기 대문에 Published 영역에 선언하면 오브젝트 인스펙터에서 이벤트 핸들러를 작성할 수 있고 Public으로 선언하면 실행시에 이벤트 핸들러를 대입해서 사용할 수 있다.
아래 리스트는 controls.pas에 있는 TControl 콤포넌트의 일부분을 발췌한 것으로 이벤트를 어떻게 선언하고 사용하는지 알 수 있을 것이다.

리스트 1.12 이벤트 선언
type
  TMouseEvent = procedure(Sender: TObject; Button: TMouseButton;
    Shift: TShiftState; X, Y: Integer) of object;
  TMouseMoveEvent = procedure(Sender: TObject; Shift: TShiftState;
    X, Y: Integer) of object;
  TDragOverEvent = procedure(Sender, Source: TObject; X, Y: Integer;
    State: TDragState; var Accept: Boolean) of object;
  TDragDropEvent = procedure(Sender, Source: TObject;
    X, Y: Integer) of object;

  TControl = class(TComponent)
  private
    { 생략 }
    FOnMouseDown: TMouseEvent;
    FOnMouseMove: TMouseMoveEvent;
    FOnMouseUp: TMouseEvent;
    FOnDragDrop: TDragDropEvent;
    FOnDragOver: TDragOverEvent;
    FOnResize: TNotifyEvent;
    //  TNotifyEvent = procedure(Sender: TObject) of object;
    //  TNotifyEvent 형은 classes.pas에 정의되어 있다.
    FOnClick: TNotifyEvent;
    FOnDblClick: TNotifyEvent;
    { 생략 }
  protected
    { 생략 }
    property OnClick: TNotifyEvent
      read FOnClick
      write FOnClick
      stored IsOnClickStored;
    property OnDblClick: TNotifyEvent
      read FOnDblClick
      write FOnDblClick;
    property OnDragDrop: TDragDropEvent
      read FOnDragDrop
      write FOnDragDrop;
    property OnDragOver: TDragOverEvent
      read FOnDragOver
      write FOnDragOver;
    property OnMouseDown: TMouseEvent
      read FOnMouseDown
      write FOnMouseDown;
    property OnMouseMove: TMouseMoveEvent
      read FOnMouseMove
      write FOnMouseMove;
    property OnMouseUp: TMouseEvent
      read FOnMouseUp
      write FOnMouseUp;
    property OnResize: TNotifyEvent
      read FOnResize
      write FOnResize;
  public
    { 생략 }
  published
    { 생략 }
  end;


1.6.2. 표준 이벤트
델파이에 포함된 모든 콤포넌트들은 여러 가지 표준 이벤트를 protected 멤버로 가지고 있다. 표 6.5에 있는 이벤트들은 모든 콤포넌트들이 가지고 있고 표 6.6에 있는 이벤트는 윈도 콤포넌트(윈도 핸들을 가지는 콤포넌트)들만이 가지고 있는 표준 이벤트이다. 이들 표준 이벤트들은 protected 멤버로 선언되어 있기 때문에 자신이 만든 콤포넌트에서 설계 시 또는 실행 시에 사용하려면 반드시 public이나 published 영역으로 다시 선언해 주어야 한다.

표 1-5 모든 콤포넌트가 가지고 있는 표준 이벤트
이벤트이벤트 발생 시기
OnClick왼쪽 마우스 버튼으로 콤포넌트를 클릭하면 발생한다.
OnDblClick왼쪽 마우스 버튼으로 콤포넌트를 더블 클릭하면 발생한다.
OnMouseDown콤포넌트 영역 위에서 마우스 버튼을 눌렀을 때 발생한다.
OnMouseMove콤포넌트 영역 위에서 마우스 포인터가 움직이면 발생한다.
OnMouseUp콤포넌트 영역 위에서 마우스 버튼을 눌렀다 뗐을 때 발생한다.
OnDragOver객체를 콤포넌트 영역 위로 끌고 오면(Drag) 발생한다.
OnDragDrop끌고 온 객체를 콤포넌트 위에서 놓으면(Drop) 발생한다.
OnStartDrag콤포넌트 영역 위에서 마우스 왼쪽 버튼을 누르고 드래그하면 발생한다.
OnEndDrag콤포넌트 자신의 드래그 작업이 종료하면 발생한다.




표 1-6 모든 윈도 콤포넌트가 가지고 있는 표준 이벤트
이벤트이벤트 발생 시기
OnEnter콤포넌트가 키보드 입력 포커스를 가지면 발생한다.
OnExit콤포넌트가 가지고 있던 키보드 입력 포커스를 잃어 버리면 발생한다.
OnKeyDown컴포넌트가 키보드 입력 포커스를 가지고 있는 상태에서 키를 누를 때 발생한다.
OnKeyPress문자가 입력되면 발생한다.
OnKeyUp컴포넌트가 키보드 입력 포커스를 가지고 있는 상태에서 키를 눌렀다 뗄 때 발생한다.


만약에 자신의 콤포넌트에서 이들 표준 이벤트들이 발생했을 때 어떤 처리를 하고 싶으면 이벤트 핸들러를 작성하고 이들 이벤트 핸들러를 각 이벤트에 대입해 주면 된다. 하지만 이런 방법으로 처리했을 경우 어플리케이션 개발자들은 이 이벤트를 사용할 수 없게 된다.(사용할 수는 있지만 콤포넌트 개발자가 지정한 이벤트 핸들러를 대체해 버린다.) 이런 문제 때문에 각각의 이벤트에는 이벤트를 실제로 발생시켜주는 가상 메소드들이 유사한 이름으로 정의되어 있다. 예를 들어 OnClick 이벤트에는 Click 메소드, OnEndDrag 에는 DoEndDrag 이런 식으로 정의되어 있다. 이들 메소드 들은 protected 영역에 가상 메소드로 선언되어 있기 때문에 파생 클래스에서 오버라이드해서 내부 이벤트 처리 방법을 변경해 줄 수가 있다. 예를 들어 OnClick 이벤트가 발생할 때 마다 시스템 내부 스피커로 경고음을 내고 싶은 경우 다음과 같이 할 수 있을 것이다.

type
  TMyButton = class(TButton)
  protected
    procedure Click; override;
    // Click 메소드 오버라이드
  end;

{ 생략 }

procedure TMyButton.Click;
begin
  // 원래 메소드 실행
  inherited Click;
  // 경고음 출력
  MessageBeep(0);
end;


1.6.3. 새 이벤트 만들기
대부분의 경우 기본 이벤트만으로 충분하지만 가끔씩 나만의 이벤트를 만들어야 할 경우가 생긴다. 이벤트를 새로 만드는 작업은 그리 어려운 일이 아니다. 언제 이벤트가 발생해야 하는지 결정하고 이벤트 핸들러가 사용할 메소드 형을 결정하고 이벤트 핸들러를 저장할 데이터 필드를 만들어 주고 적절한 상황에서 이벤트를 발생시켜주기만 하면 된다.
보통 발생 상황을 기준으로 이벤트를 크게 두 가지 종류로 구분해 볼 수 있는데 하나는 시스템 이벤트, 즉 WM_LBUTTONDOWN 같은 윈도에서 발생하는 메시지에 대응하는 이벤트이고 다른 하나는 콤포넌트의 상태가 변경되었음을 알려 주는 이벤트이다. 이벤트를 언제 발생시켜야 할지를 결정했다면 다음은 이벤트 핸들러의 메소드 형을 결정해야 한다. 단순히 사건의 발생만 알려 주는 이벤트일 수도 있고 사건의 발생과 함께 어떤 정보를 같이 줄 수도 있고 아니면 사용자로부터 어떤 반환 값을 받을 수도 있다.
단순히 사건의 발생만 알려 주는 이벤트의 경우 이벤트를 누가 발생시켰는지를 알려 주는 Sender 인자 하나만 가지고 있는 TNotifyEvent 형을 사용하면 된다. 정보를 제공하는 이벤트의 경우는 MouseMove 이벤트를 예로 들자면 마우스 포인터가 움직였다는 사건과 함께 마우스 포인터가 어느 위치에 있는지도 알려 준다. 이처럼 제공하고자 하는 정보를 메소드의 인자로 제공하면 된다. 반환 값을 필요로 하는 경우에는 메소드의 인자를 var 형으로 선언해 주면 된다. 예를 들어 키보드 이벤트(KeyDown, KeyPress, KeyUp) 같은 경우 Key 인자를 var 형으로 가지고 있는데 Key 인자로 들어 온 키 코드를 사용자가 변형해서 다른 코드를 넘겨 줄 수 있다.
이해를 돕기 위래 간단한 이벤트를 하나 만들어 보자. 한/영 전환키가 눌러질 때 마다 키보드 입력 모드가 변경되었다는 것을 알려 주는 이벤트를 가지고 있는 콤포넌트를 하나 만들어 보자.
이벤트를 언제 발생시켜야 하는 지는 결정했으니까 이벤트 핸들러가 사용할 메소드 형을 선언해 보자. 이벤트가 발생할 때 마다 현재 키보드 입력 모드가 무엇인지를 알려주면 좋을 것이다. 그래서 다음과 같이 정의한다.

type
  TdpbImeInputMode = ( dimHangul, dimEnglish );
  TdpbImeInputModeChange = procedure (Sender : TObject;
    ImeInputMode : TdpbImeInputMode) of object;


다음 이벤트 핸들러를 저장할 데이터 필드 FOnImeInputModeChange를 만들어 주고 이벤트 프로퍼티 OnImeInputModeChange를 만들어준다. 이벤트 프로퍼티의 이름은 일반적으로 On 으로 시작한다.
마지막으로 적절한 위치에서 이벤트를 발생시키도록 코딩한다. 여기서는 한/영 전환 키가 눌러지는지를 검사해야 하므로 KeyDown 메소드를 오버라이드 해서 눌려진 키 코드가 VK_HANGUL이면 이벤트를 발생시키도록 했다. 현재 IME의 입력 모드는 IME API를 이용해서 알 수 있다. IME API를 사용하려면 uses 절에 Imm 유닛을 추가해 주어야 한다.

리스트 1.13 dpbEdit.pas
unit dpbEdit;

{ dpb = Delphi Programming Bible }

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls,
  { IME 관련 루틴 }
  Imm;

type
  TdpbImeInputMode = ( dimHangul, dimEnglish );
{ 이벤트 핸들러가 사용할 메소드 형 }
  TdpbImeInputModeChange = procedure (Sender : TObject;
    ImeInputMode : TdpbImeInputMode) of object;

  TdpbEdit = class(TCustomEdit)
  private
    { Private declarations }
    FOnImeInputModeChange : TdpbImeInputModeChange;

    function GetImeInputMode: TdpbImeInputMode;
    procedure DoImeInputModeChange;
  protected
    { Protected declarations }
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
  public
    { Public declarations }
  published
    { Published declarations }
    property OnImeInputModeChange : TdpbImeInputModeChange
      read FOnImeInputModeChange
      write FOnImeInputModeChange;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('DPB', [TdpbEdit]);
end;

function TdpbEdit.GetImeInputMode: TdpbImeInputMode;
var
  dwSentence : DWORD;
  dwConversion : DWORD;
  IMC : HIMC;  { IME 핸들 }
begin
  IMC := ImmGetContext(Handle);
  ImmGetConversionStatus(IMC, dwConversion, dwSentence);
  if (dwConversion and IME_CMODE_HANGUL) = IME_CMODE_HANGUL then
    Result := dimHangul
  else
    Result := dimEnglish;
  ImmReleaseContext(Handle, IMC);
end;

{ TdpbEdit }
procedure TdpbEdit.DoImeInputModeChange;
begin
  if Assigned(FOnImeInputModeChange) then
    FOnImeInputModeChange(Self, GetImeInputMode);
end;

procedure TdpbEdit.KeyDown(var Key: Word; Shift: TShiftState);
begin
  inherited KeyDown(Key, Shift);
  if Key = VK_HANGUL then DoImeInputModeChange;
end;

end.





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