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


1.7. VCL(Visual Component Library)

어플리케이션 개발자에게 VCL은 어플리케이션을 개발할 때 사용할 수 있는 콤포넌트들의 모음이라 할 수 있지만 콤포넌트 개발자에게 VCL은 새로운 콤포넌트를 만들 때 사용할 수 있는 엄청난 양의 기능을 가지고 있는 클래스 라이브러리라 할 수 있다. 콤포넌트를 개발한다는 것은 이 거대한 VCL의 계층 구조 속에 자신이 만든 클래스를 끼워 넣는 작업이라 할 수 있겠다. 이번 절에서는 콤포넌트를 개발할 때 주로 사용되고 자주 만나는 클래스들에 대해 알아 보도록 하자.
1.7.1. VCL의 계층 구조
델파이의 모든 콤포넌트는 VCL이라는 거대한 클래스 계층 구조의 일부분이다. 그림 1-10은 콤포넌트를 개발할 때 자주 사용하는 클래스들을 기준으로 VCL의 계층도를 간단하게 표현한 것이다. 제일 위에는 모든 클래스들의 최상위 부모 클래스인 TObject가 있고 TObject의 자손으로 객체를 스트림(메모리 스트림, 파일 스트림 등)에 저장할 수 있는 기능을 가진 TPersistent 클래스가 있다. 델파이는 폼 파일을 스트림을 이용해서 구현하므로 모든 콤포넌트들의 부모 클래스인 TComponent는 TPersistent 클래스에서 상속 받았다.

그림 1-10 VCL의 계층 구조


TComponent를 시작으로 우리가 콤포넌트를 개발할 때 부모 클래스로 선택할 수 있는 클래스들이 나타난다. TComponent는 TTimer, TTable처럼 시각적인 기능이 필요없는 콤포넌트의 부모 클래스로 사용하며 다른 콤포넌트를 소유할 수 있는 기능을 가지고 있다. TControl 콤포넌트는 시각적인 콤포넌트를 만들 때 사용하는데 그림을 보면 알겠지만 시각적인 콤포넌트는 두 가지 클래스로 나누어 진다. TGraphicControl과 TWinControl로서 둘의 큰 차이점은 TWinControl은 윈도 핸들을 가지기 때문에 윈도 입력 포커스를 가질 수 있고 TGraphicControl은 그렇지 않다는 것이다. 나아가서 윈도 핸들을 가지는 콤포넌트는 두 가지 범주로 나눌 수 있는데 하나는 에디트, 체크 버튼, 라디오 버튼, 그룹 박스등과 같이 공통 윈도 콘트롤을 서브 클래싱하는 콤포넌트들로서 TWinControl에서 직접 상속 받아서 만들어진 콤포넌트들이고 나머지는 TCustomControl로서 TWinControl로부터 파생되기 때문에 윈도 입력 포커스를 가질 수 있으며 자체적으로 윈도 모양을 그릴 수 있도록 Canvas 객체와 Paint 메소드를 추가로 가지고 있는 콤포넌트이다.
그 밖에 콤포넌트 개발자들에게 유용한 TStringList나 TList, TRegistry, TCollection 등의 지원 클래스들이 많이 있다.
1.7.2. TComponent
TComponent 클래스는 폼 디자이너가 콤포넌트를 관리할 때 필요한 프로퍼티와 메소드를 제공한다. 여러가지 프로퍼티 중에서 콤포넌트의 현재 상태를 나타내는 집합형 프로퍼티인 ComponentState가 아주 유용한데 ComponentState 프로퍼티에 설정할 수 있는 값들은 아래 표와 같다.

표 1-7 ComponentState 플래그
의미
csAncestor콤포넌트가 조상 폼으로부터 상속(전래)되었음을 나타내며 csDesiging 플래그가 설정되어 있을 때만 설정된다.
csDesigning폼 디자이너가 콤포넌트를 조작하고 있음을 나타낸다.
csDestroying콤포넌트가 파괴되고 있음을 나타낸다.
csFixups콤포넌트가 아직 로드 되지 않은 폼에 있는 콤포넌트와 연결되어 있음을 나타낸다. 연결된 콤포넌트가 모두 로드될 때 해제 된다.
csFreeNotification자신이 소멸될 때 아직 소멸되지 않은 다른 폼에 자신이 소멸됨을 통보해 준다.
csInline콤포넌트가 설계 시에 변경될 수 있는 최상위 콤포넌트이며 폼에 삽입될 수 있음을 나타낸다. 이 플래그는 읽고 저장할 때 중첩 프레임임을 지시하기 위해 사용한다.
csLoadingFiler 객체가 콤포넌트를 읽어 들이고 있음을 나타낸다. 이 플래그는 콤포넌트가 생성될 때 설정되고 콤포넌트가 소유하고 있는 자식 콤포넌트들이 모두 로드 될 때까지 해제 되지 않는다.
csReading콤포넌트 프로퍼티 값을 스트림으로부터 읽고 있음을 나타낸다. csReading이 설정 되어 있을 때는 csLoading 플래그도 항상 설정되어 있다.
csUpdating상속된 콤포넌트가 조상 폼에서 발생한 변화를 갱신하고 있음을 나타낸다. csAncestor 플래그가 설정되어 있을 때만 설정된다.
csWriting콤포넌트가 프로퍼티 값을 스트림에 기록하고 있음을 나타낸다.


콤포넌트는 ComponentState 프로퍼티를 통해 어떤 작업을 해야 할 지 말아야 할지를 결정할 수 있는데 아래 예제 코드처럼 설계 시에는 동작하지 않고 실행 시에만 어떤 작업을 수행하도록 할 때 사용할 수 있다. 이 예제는 extctrls.pas에 정의되어 있는 TImage 콤포넌트의 Paint 프로시저인데 설계 시에만 콤포넌트의 외곽에 점선을 그려주도록 하기 위해 ComponentState 프로퍼티의 csDesigning 플래그를 사용하고 있다.

procedure TImage.Paint;
var
  Save: Boolean;
begin
{ 설계 시에만 콤포넌트의 외곽에 점선을 그려준다. }
  if csDesigning in ComponentState then
    with inherited Canvas do
    begin
      Pen.Style := psDash;
      Brush.Style := bsClear;
      Rectangle(0, 0, Width, Height);
    end;
  Save := FDrawing;
  FDrawing := True;
  try
    with inherited Canvas do
      StretchDraw(DestRect, Picture.Graphic);
  finally
    FDrawing := Save;
  end;
end;


델파이 2.0부터 폼 상속 기능을 지원하면서 ComponentStyle 이라는 집합형 프로퍼티가 생겼는데 이 프로퍼티는 폼이 상속될 때 콤포넌트가 어떻게 동작해야 할지를 결정한다. 사용할 수 있는 플래그는 csInheritable과 csCheckPropAvail이 있다. 모든 콤포넌트는 생성자에서 ComponentStyle 프로퍼티에 csInheritable 플래그가 설정되며 기본적으로 폼 상속을 지원한다. ComponentStyle 프로퍼티에 csInheritable 플래그가 설정되어 있지 않으면 이 콤포넌트를 포함하고 있는 폼은 상속될 수 없다. csCheckPropAvail 플래그는 COM(Component Object Model) 콘트롤에서 사용되는데 콤포넌트가 프로퍼티를 읽을 수 있는가를 검사해야 할지를 나타낸다. 오브젝트 인스펙터는 직접 COM 콘트롤의 프로퍼티 값을 읽을 수 있는지를 결정할 수 없기 때문에 COM 콘트롤은 모두 이 플래그를 설정해야 한다. 하지만 대부분의 경우 COM 콘트롤을 임포트할 때 부모 클래스로 사용하는 TOLEControl 클래스에서 이 플래그를 자동으로 설정해주니까 신경 쓰지 않아도 된다.
TComponent는 다른 콤포넌트를 소유할 수 있으며 다른 콤포넌트에 소유 될 수 있다. TComponent는 Owner 프로퍼티를 통해서 자신을 소유하고 있는 콤포넌트가 무엇인지 알 수 있으며 Components 프로퍼티를 통해서 자신이 소유하고 있는 콤포넌트의 리스트를 알 수 있다. TComponent의 생성자는 새로 생성되는 콤포넌트의 소유주를 인자로 받게 되어 있어서 소유주가 nil이 아닌 경우에 소유주의 Components 프로퍼티에 자신을 추가한다. 콤포넌트 리스트를 이용해서 TComponent는 자신이 소멸될 때 자동으로 자신이 소유하고 있는 콤포넌트들을 소멸하도록 되어 있다. 예를 들어서 폼 클래스인 TForm은 TComponent의 자손 클래스이기 때문에 폼이 소유하고 있는 모든 콤포넌트 들은 폼이 소멸될 때 자동으로 소멸된다. 따라서 어플리케이션 개발자가 동적으로 생성한 콤포넌트도 일일이 해제해 주지 않아도 생성할 때 소유주만 정상적으로 입력해 주었다면 자동으로 소멸된다. ComponentCount 프로퍼티는 프로퍼티 이름에서도 알 수 있겠지만 자신이 소유하고 있는 콤포넌트의 개수를 나타낸다. 그리고 FindComponent 메소드를 이용해서 자신이 소유하고 있는 콤포넌트 중에서 특정 이름을 가지는 콤포넌트를 찾을 수 있다. 아래 예제는 Components 프로퍼티를 이용해서 폼이 소유하고 있는 콤포넌트 중 TButton 콤포넌트인 것만 추출하는 예이다.

procedure TForm1.FindButton(var ButtonList: TStrings);
var
  nIndex : Integer;
begin
  for nIndex := 0 to Pred(ComponentCount) do
  begin
    if Components[nIndex] is TButton then
    begin
      ButtonList.Add(Components[nIndex].Name);
    end;
  end;
end;


TComponent 클래스가 가지고 있는 메소드 중에 Notification 이라는 메소드가 있다. 콤포넌트의 Components 프로퍼티에 새 콤포넌트가 추가되면 콤포넌트는 기존에 소유하고 있던 각 콤포넌트들의 Notification 메소드를 호출해서 새로운 콤포넌트가 추가되었음을 알려 준다. 마찬가지로 콤포넌트 리스트에서 콤포넌트가 삭제되면 다른 콤포넌트에게 삭제되었음을 알려 준다. 예를 들어 설명하자면 폼을 설계할 때 폼에 새로운 콤포넌트를 팔레트로부터 끌어다 놓으면 폼의 콤포넌트 리스트에는 그 콤포넌트가 추가 되며 폼은 기존에 소유하고 있던 다른 콤포넌트에게 Notification 메소드를 이용해서 새 콤포넌트가 추가 되었음을 알려준다. 이 개념은 실행 시에는 물론이고 설계 시에 아주 중요한데 아래와 같은 상황을 생각해 보면 쉽게 이해할 수 있을 것이다.
새 프로젝트를 하나 만들고 폼에 TLabel 콤포넌트와 TEdit 콤포넌트를 하나씩 올려 놓자. 그리고 아래 그림처럼 TLabel 콤포넌트의 FocusControl 프로퍼티를 TEdit 콤포넌트로 설정하자. 어플리케이션 사용자가 키보드를 이용해서 TLabel 콤포넌트의 단축키를 누르면 TLabel 콤포넌트는 입력 포커스를 가질 수 없기 때문에 자기에게 오는 입력 포커스를 FocusControl에 설정된 콤포넌트로 넘겨주도록 되어 있다. 그런데 아래 상황에서 TEdit 콤포넌트를 삭제하고 실행하면 어떻게 될까? 만약에 Notification 메소드가 없다면 TLabel 콤포넌트는 TEdit 콤포넌트가 삭제되었다는 사실을 알 수 없게 되고 결국 FocusControl 프로퍼티는 존재하지 않는 콤포넌트에 대한 레퍼런스를 가지고 있게 된다. 이 상태에서 사용자가 단축키를 누르면 Access Violation 에러가 나게 될 것이다. Notification 메소드는 이런 상황에서 아주 유용하다. 아래 코드는 TLabel 콤포넌트의 부모 클래스인 TCustomLabel의 Notification 메소드인데 어떤 콤포넌트가 삭제될 때 삭제되는 콤포넌트가 자신이 FocusControl 프로퍼티로 참조하고 있는 콤포넌트이면 FFocusControl을 nil 값으로 설정하도록 하고 있다. 독자가 새로 개발하는 콤포넌트가 다른 콤포넌트를 참조한다면 반드시 Notification 메소드를 오버라이드해서 적절한 조치를 취해주어야 함을 명심해야 한다.

 
그림 1-11 Notification 예제




procedure TCustomLabel.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited Notification(AComponent, Operation);
  if (Operation = opRemove) and (AComponent = FFocusControl) then
    FFocusControl := nil;
end;


그런데 위와 같은 상황이 하나의 폼 내에서만 이루어 진다면 아무 문제가 없다. 하지만 다른 폼에 있는 콤포넌트를 참조하도록 설정했다면 어떻게 될까? 안타깝게도 Notification 메소드는 자신이 소유하고 있는 콤포넌트에 대해서만 사용할 수 있다. 그래서 TComponent는 FFreeNotifies 라는 리스트를 별도로 관리하는데 자신이 소멸될 때 FFreeNotifies 리스트에 있는 모든 콤포넌트의 Notification 메소드를 호출하도록 되어 있다. 그렇다면 FFreeNotifies 리스트는 어떻게 관리될까? TComponent는 FreeNotification 이라는 메소드를 제공하는데 아래 예제를 보면 쉽게 이해될 것이다.

procedure TCustomLabel.SetFocusControl(Value: TWinControl);
begin
  FFocusControl := Value;
  if Value <> nil then Value.FreeNotification(Self);
end;


즉 콤포넌트가 다른 콤포넌트를 참조하도록 설정될 때 참조되는 콤포넌트의 FreeNotification 메소드에 인자를 Self로 호출해서 자신을 FFreeNotifies 리스트에 추가하도록 요청한다. 이렇게 되면 참조되는 콤포넌트는 자신이 소멸될 때 FFreeNotifies 리스트를 통해서 자신을 참조하고 있는 모든 콤포넌트들에게 자신이 소멸되고 있음을 알려줄 수 있다.
다음으로 콤포넌트 개발자에게 유용한 메소드가 Loaded라는 메소드인데 이 메소드는 콤포넌트가 생성되고 자신의 프로퍼티를 폼 파일에서 읽어 들인 직후에 호출된다. Loaded 는 가상 메소드이기 때문에 프로퍼티가 읽혀진 후에 어떤 작업을 하고 싶으면 Loaded 메소드를 오버라이드해서 하고 싶은 작업을 추가해주면 된다. Loaded 메소드는 콤포넌트가 화면에 보여지기 전에 호출된다.
1.7.3. TControl
TControl 클래스를 직접 부모 클래스로 사용할 일은 없지만 모든 비주얼 콤포넌트의 부모 클래스로서 TControl 클래스는 중요한 프로퍼티를 가지고 있다.
TControl 클래스는 Parent 라는 프로퍼티를 가지고 있다. Owner 프로퍼티와 종종 혼돈을 하는 경우가 있는데 Owner와 달리 콤포넌트의 Parent는 반드시 윈도 핸들을 가지고 있어야 한다. Owner 는 소유의 개념이지만 Parent는 포함의 개념이다. 따라서 윈도 핸들을 가지는 콤포넌트만이 Parent 가 될 수 있기 때문에 TWinControl이나 그 자손 클래스만이 Parent 프로퍼티로 설정될 수 있다.
다음으로 TControl은 ControlStyle이라는 집합형 프로퍼티를 가지는데 주로 콤포넌트의 생성자에서 설정된다. 아래 표는 ControlStyle 프로퍼티에 설정될 수 있는 값들을 나타낸다.

표 1-8 ControlStyle 플래그
플래그의미
csAcceptControls윈도 핸들을 가지는 콤포넌트에만 유효한 플래그로서 설계 시에 다른 콤포넌트의 Parent 콘트롤이 될 수 있음을 나타낸다.
csCaptureMouse마우스 이벤트를 캡쳐한다. 콘트롤 영역 안에서 마우스 버튼을 누르고 콘트롤 영역 밖에서 떼도 MouseUp 이벤트를 받을 수 있도록 해 준다.
csDesignInteractive설계 시에 콘트롤을 조작하기 위해서 오른쪽 마우스 버튼이 눌려지면 이 메시지를 왼쪽 버튼이 눌려진 것으로 변환해 준다.
csClickEvents클릭 메시지를 받을 수 있다.
csFramed콘트롤 프레임이 3D 효과를 가질 수 있다. Ctl3D 프로퍼티가 동작하기 위해 필요하다.
csSetCaption사용자가 Caption 프로퍼티를 별도로 설정하지 않았으면 Caption 프로퍼티를 Name 프로퍼티와 같게 만든다.
csOpaque자신의 클라이언트 영역을 모두 그린다. 이 플래그가 없으면 자식 콤포넌트에 의해 가려지는 클라이언트 영역은 그리지 않는다.
csDoubleClicks이 플래그가 설정되어 있으면 더블 클릭 메시지를 받을 수 있다. 아니면 클릭 메시지로 변환한다.
csFixedWidth콘트롤의 폭이 스케일의 영향을 받지 않는다.
csFixedHeight콘트롤의 높이가 스케일의 영향을 받지 않는다.
csNoDesignVisible설계 시에 보이지 않게 한다.
csReplicatablePaintTo 메소드를 이용해서 자신의 이미지를 다른 Canvas에 복사될 수 있는지를 나타낸다. 쉽게 TDBCtrlGrid에서 복제될 수 있는지를 나타낸다고 보면 된다.
csNoStdEvents마우스나 키보드등의 표준 이벤트를 무시한다.
csDisplayDragImage마우스로 자신이 드래그될 때 이미지 리스트에서 이미지를 가져와 나타낼 지를 설정한다.
csActionClient액션 객체에 연결될 수 있는지를 나타낸다. Action 프로퍼티가 설정되면 이 플래그도 설정되고 Action 프로퍼티가 해제되면 이 플래그도 해제된다.
csMenuEvents시스템 메뉴에 반응한다.


TControl 클래스는 기본적으로 [csCaptureMouse,csClickEvents, csSetCaption, csDoubleClicks] 값으로 ControlStyle 프로퍼티를 초기화한다. ControlStyle 프로퍼티는 콘트롤의 성질을 나타내기 때문에 인스턴스마다 다르게 설정될 수 없으며 실행 시에 다른 값으로 바뀔 수 없다. 다음으로 TControl 클래스는 실행 시에 콘트롤의 현재 상태를 나타내는 ControlState라는 집합형 프로퍼티를 제공하는데 아래 표는 사용 가능한 상태값들을 보여준다.

표 1-9 ControlState 플래그
플래그의미
csLButtonDown왼쪽 마우스 버튼이 눌린 채 아직 떼지 않았다.
csClickedcsLButtonDown과 같지만 ControlStyle 프로퍼티에 csClickEvents가 설정되어 있을 때만 설정되며 ButtonDown 메시지가 Click으로 해석되었음을 나타낸다.
csPalette시스템 팔레트가 변경되었지만 콘트롤과 자식 콘트롤들이 아직 자신의 팔레트를 리얼라이즈하지 않았음을 나타내다.
csReadingState스트림으로부터 자신의 상태를 읽어들이고 있음을 나타낸다.
csAlignmentNeeded자신의 위치를 재조정해야 할 필요가 있음을 나타낸다.
csFocusing입력 포커스를 가지도록 해 주는 메시지를 처리하고 있음을 나타낸다.
csCreating자신과 자식 콘트롤들이 생성되고 있음을 나타내며 모두 생성되면 해제된다.
csPaintCopy콘트롤이 복제되고 있음을 나타낸다. ControlStyle 프로퍼티에 csReplicatable 플래그가 설정되어 있어야만 이 플래그도 설정될 수 있다.
csCustomPaint사용자 Paint 메시지를 처리하고 있음을 나타낸다.
csDestroyingHandle윈도 핸들을 해제하고 있음을 나타낸다.
csDockingDocking 되고 있음을 나타낸다.


1.7.4. TGraphicControl
TGraphicControl은 입력 포커스를 가질 필요가 없고 다른 콘트롤의 부모가 될 필요가 없는 비주얼 콘트롤을 만들 때 부모 클래스로 선택하는 클래스이다. TGraphicControl은 사용자와의 상호 작용이 필요치 않은 콘트롤을 만들 때 많이 사용한다. Windows 95나 Windows 98에서는 이전 버전보다 사용할 수 있는 리소스의 양이 많이 늘긴 했지만 여전히 한계가 있다. 윈도 핸들도 윈도의 리소스이기 때문에 비주얼 효과는 가지면서도 윈도 핸들을 아낄 수 있는 방법을 TGraphicControl은 제공해 준다. 하지만 윈도 핸들을 가질 수 없기 때문에 TGraphicControl과 자손 클래스들은 입력 포커스를 가지지는 못하지만 마우스 이벤트는 받을 수 있다. 이것이 가능한 이유는 TGraphicControl의 부모 윈도에 있다. TGraphicControl의 부모 윈도는 반드시 TWinControl의 자손 클래스이어야 한다. (폼도 TWinControl의 자손 클래스임을 생각하면 쉽게 이해될 것이다.) TWinControl은 자신에게 온 마우스 메시지가 자신의 자식 콘트롤 위에서 발생했다면 그 콘트롤에게로 메시지를 전달해 준다. (TWinControl의 IsControlMouseMsg 함수가 이 역할을 한다.)
TGraphicControl은 자신을 화면에 그릴 수 있도록 하기 위해서 윈도 디바이스 콘텍스트를 사용하기 쉽게 잘 포장해 놓은 TCanvas 클래스 형의 Canvas 프로퍼티와 자신이 그려질 필요가 있을 때 마다 호출되는 Paint 메소드를 가지고 있다. TGraphicControl의 자손 클래스는 Paint 메소드를 오버라이드하고 Canvas를 이용해서 자신을 화면에 그려주면 된다.
TGraphicControl을 사용할 때 주의해야 할 것은 TGraphicControl과 그 자손 클래스도 ControlStyle 프로퍼티에 csAcceptControls 플래그를 설정할 수는 있지만 이 플래그를 설정한 콤포넌트에 다른 콘트롤을 자식으로 넣으려고 하면 델파이는 새로 생성할 콤포넌트의 부모가 될 콤포넌트에서 윈도 핸들을 찾으려 할 것이기 때문에 Access Violation 에러를 발생시키게 된다는 것이다.
1.7.5. TWinControl
TWinControl은 윈도 시스템에서 제공하는 콘트롤들(TEdit, TButton, TTreeView, TListView, TToolbar등)을 서브 클래싱할 때 사용하는 클래스로서 윈도 핸들은 가지지만 스스로 자신을 화면에 알아서 그려주기 때문에 기본적으로 별도로 자신을 그릴 때 필요한 메소드나 프로퍼티를 제공하지 않는다. TWinControl은 윈도 시스템 기본 콘트롤 뿐만이 아니고 볼랜드 C++ 시절 사용했던 BWCC.DLL과 같이 DLL로 구현된 콘트롤들을 서브 클래싱할 때도 사용한다.
앞에서 기본적으로라는 말을 했는데 기본적으로는 그렇지만 방법이 없는 것은 아니다. 윈도 기본 콘트롤 중 XX_OWNERDRAW 속성을 가질 수 있는 콘트롤 들이 있는데 이 속성을 설정해주면 기본적으로 그려지는 것을 내 입맛에 맞게 확장할 수 있다. 물론 WM_PAINT 메시지를 가로채서 할 수도 있다.
TWinControl은 Handle 이라는 프로퍼티를 제공하는 데 이 프로퍼티가 현재 서브 클래싱한 콘트롤의 윈도 핸들이며 핸들을 필요로 하는 각종 윈도 API에 사용할 수 있다. 또한 입력 포커스를 가질 수 있기 때문에 CanFocus, Focused, TabStop, TabOrder, OnKeyDown, OnKeyPress등 입력 포커스 및 키보드와 관련된 여러가지 프로퍼티, 메소드, 이벤트도 함께 제공한다.
TWinControl를 부모로 하는 콤포넌트를 개발하려고 하는 개발자에게 유용한 프로퍼티와 메소드를 알아보자.
첫번째로 CreateParams 와 CreateWnd 메소드인데 이 두 가상 메소드는 TWinControl이 서브 클래싱하는 윈도 콘트롤의 핸들을 생성할 때 호출된다.
CreateHandle 메소드는 CreateWnd 메소드를 호출하고 CreateWnd는 CreateParams를 호출해서 윈도 생성 정보를 만든 다음 CreateWindowHandle 메소드를 호출한다. CreateWindowHandle 메소드는 CreaetParams 메소드로 얻어진 윈도 생성 정보를 가지고 CreateWindowEx API를 호출해서 실제로 윈도 핸들을 만든다. 이 과정에서 CreateParams는 윈도 생성 정보를 만드는데 콤포넌트 개발자는 이 메소드를 종종 오버라이드 해서 생성할 윈도의 스타일들을 바꾸곤 한다. 예를 들어 보자. TButton 콤포넌트는 윈도 기본 콘트를 중 BUTTON 윈도 클래스로 만들어 지는데 TButton은 이미지를 표시할 수 없기 때문에 델파이에서는 TBitBtn 콤포넌트를 만들어 놓았다. 윈도 BUTTON 콘트롤은 버튼에 이미지를 그려 주기 위해서 CreateWindowEx로 버튼을 생성할 때 버튼 스타일에 BS_OWNERDRAW 스타일을 추가해 주어야 하는데 이런 스타일을 변경해 줄 수 있는 메소드가 바로 CreateParams이다. TBitBtn 콤포넌트는 Buttons.pas에서 발췌한 아래 코드에서 보는 바와 같이 CreateParams를 오버라이드하고 Params.Style에 BS_OWNERDRAW 플래그를 설정하고 있다.


TBitBtn = class(TButton)
  private
{ 생 략 }
  protected
    procedure CreateParams(var Params: TCreateParams); override;
  public
    { 생 략 }
  published
    { 생 략 }
  end;
     
procedure TBitBtn.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  with Params do Style := Style or BS_OWNERDRAW;
end;


TBitBtn에서 실제로 버튼에 이미지를 그려주는 루틴은 buttons.pas 유닛을 보면 알겠지만 WM_DRAWITEM 메시지를 받아서 처리해 준다.

CreateWnd 메소드는 윈도 핸들이 완전히 생성된 후에 별도의 초기화 작업이 필요한 경우에 사용한다. EDIT 윈도 클래스를 서브 클래싱하는 TCustomEdit 콤포넌트의 경우 EDIT 콘트롤의 입력 크기 제한 프로퍼티를 설정하기 위해서 CreateWnd 메소드를 오버라이드하는데 StdCtrls.pas 유닛을 보면 아래와 같이 정의하고 있다.

procedure TCustomEdit.CreateWnd;
begin
  FCreating := True;
  try
    inherited CreateWnd;
  finally
    FCreating := False;
  end;
  DoSetMaxLength(FMaxLength);
  Modified := FModified;
  if FPasswordChar <> #0 then
    SendMessage(Handle, EM_SETPASSWORDCHAR, Ord(FPasswordChar), 0);
  UpdateHeight;
end;


그런데 콤포넌트를 개발하다 보면 어떤 작업을 수행하기 위해서는 반드시 윈도 핸들이 생성되어 있어야 되는 경우가 있다. 예를 들어 어플리케이션이 실행될 때 폼 파일에서 프로퍼티 값을 읽어 와서 설정하면 프로퍼티 쓰기 메소드가 실행 되는데 이 메소드에서 윈도 핸들이 필요할 경우가 생긴다. 이럴 경우 HandleAllocated 메소드와 HandleNeeded 메소드를 사용하는데 HandleAllocated 메소드는 윈도 핸들이 생성되었는지를 알 수 있으며 HandleNeeded 메소드는 핸들이 생성되어 있지 않은 경우에 CreateHandle 메소드를 호출해서 핸들을 생성해 준다. 아래 예제는 TCustomEdit의 프로퍼티 쓰기 메소드인 SetMaxLength 메소드와 TCustomComboBox의 프로퍼티 쓰기 메소드인 SetSelText 메소드이다. SetSelText 메소드의 경우 SendMessage 를 이용하고 있기 때문에 HandleNeeded 메소드를 사용해서 반드시 윈도 핸들을 생성하도록 하고 있다.

procedure TCustomEdit.SetMaxLength(Value: Integer);
begin
  if FMaxLength <> Value then
  begin
    FMaxLength := Value;
    if HandleAllocated then DoSetMaxLength(Value);
  end;
end;

procedure TCustomComboBox.SetSelText(const Value: string);
begin
  if FStyle < csDropDownList then
  begin
    HandleNeeded;
    SendMessage(FEditHandle, EM_REPLACESEL, 0, Longint(PChar(Value)));
  end;
end;


1.7.6. TCustomControl
TCustomControl은 TWinControl에 TGraphicControl의 기능을 추가했다고 보면 된다. 자신을 화면에 그려줄 때 사용할 Canvas와 Paint 메소드가 준비되어 있다. 대부분의 경우 새로운 비주얼 콤포넌트를 만들 경우 TCustomControl을 부모 클래스로 사용하게 된다.
1.7.7. 폼 파일이 저장되는 방법
앞에서도 몇 번 얘기했지만 델파이는 폼을 저장할 때 DFM 확장자를 가지는 폼 파일을 사용한다. 델파이는 폼을 저장할 때 classes.pas에 정의되어 있는 WriteComponentResFile 프로시저를 이용한다. WriteComponentResFile 프로시저의 원형은 아래와 같이 선언되어 있다.

procedure WriteComponentResFile(const FileName: string; Instance: TComponent);


저장할 파일 이름과 TComponent의 인스턴스를 인자로 받는다. 폼도 TComponent를 부모 클래스로 사용하고 있기 때문에 아래와 같이 사용할 수 있다.

WriteComponentResFile('Unit1.dfm', Form1);


WriteComponentResFile 프로시저는 TFileStream 객체를 만들고 TFileStream의 WriteComponentRes 메소드를 호출한다. 실제로 TFileStream 의 부모 클래스인 TStream에 선언되어 있는 WriteComponentRes 메소드는 WriteDescendentRes를 호출하고 WriteDescendentRes는 WriteResourceHeader, WriteDescendent, FixupResourceHeader를 차례대로 호출한다. 마지막으로 WriteDescendent 메소드는 TWriter 객체를 생성하고 실제로 폼을 저장하는 루틴인 TWriter 객체의 WriteDescendent 메소드를 호출한다. 이 메소드는 폼 자신과 폼의 자식 콘트롤들의 Published 프로퍼티 리스트를 RTTI를 통해 구하고 이들을 차례대로 저장한다.
WriteComponentResFile은 전역으로 선언되어 있기 때문에 자신의 콤포넌트를 별도 파일로 저장하고 싶다면 이 프로시저를 호출해서 쉽게 해결 할 수 있다. 마찬가지로 폼 파일에서 읽어 들일 때는 ReadComponentResFile 메소드를 사용하면 된다.
1.7.8. Published 되지 않은 프로퍼티를 저장하는 법
기본적으로 이전 절에서 얘기한 방법으로 델파이는 Published 프로퍼티만 폼 파일에 저장한다. 하지만 델파이는 Published 되지 않은 프로퍼티도 저장할 수 있을 뿐만 아니라 너무 복잡해서 델파이가 어떻게 저장하고 읽어 들여야 할지 모르는 프로퍼티도 저장할 수 있는 방법을 제공한다. VCL의 계층도를 살펴보면 TComponent 클래스는 TPersistent 클래스로부터 파생되는데 TPersistent 클래스는 DefineProperties 라는 가상 메소드를 가지고 있다. 이 메소드를 이용하면 델파이에게 Published 되지 않은 조금은 특수한 프로퍼티를 어떻게 읽고 쓰는지를 가르쳐 줄 수 있다.
DefineProperties 메소드는 TFiler 형 인자를 하나 제공하는데 TFiler는 TReader와 TWriter의 부모 클래스로서 추상 클래스이다. 따라서 DefineProperties 메소드는 프로퍼티를 읽고 쓸 때 같이 사용된다. TFiler는 DefineProperty 메소드를 제공하는데 이 메소드로 각 프로퍼티를 어떻게 취급할 지를 지정한다. DefineProperty 메소드는 프로퍼티 이름, 읽기 함수, 쓰기 함수, 저장할 데이터가 있는지 없는지를 결정하는 논리형 이렇게 네 가지 인자를 가진다.
아래 예제는 grids.pas 유닛에 정의되어 있는 TCustomGrid에서 DefindProperties를 오버라이드한 예이다.

procedure TCustomGrid.ReadColWidths(Reader: TReader);
var
  I: Integer;
begin
  with Reader do
  begin
    ReadListBegin;
    for I := 0 to ColCount - 1 do ColWidths[I] := ReadInteger;
    ReadListEnd;
  end;
end;

procedure TCustomGrid.ReadRowHeights(Reader: TReader);
var
  I: Integer;
begin
  with Reader do
  begin
    ReadListBegin;
    for I := 0 to RowCount - 1 do RowHeights[I] := ReadInteger;
    ReadListEnd;
  end;
end;

procedure TCustomGrid.WriteColWidths(Writer: TWriter);
var
  I: Integer;
begin
  with Writer do
  begin
    WriteListBegin;
    for I := 0 to ColCount - 1 do WriteInteger(ColWidths[I]);
    WriteListEnd;
  end;
end;

procedure TCustomGrid.WriteRowHeights(Writer: TWriter);
var
  I: Integer;
begin
  with Writer do
  begin
    WriteListBegin;
    for I := 0 to RowCount - 1 do WriteInteger(RowHeights[I]);
    WriteListEnd;
  end;
end;

procedure TCustomGrid.DefineProperties(Filer: TFiler);

  function DoColWidths: Boolean;
  begin
    if Filer.Ancestor <> nil then
      Result := not CompareExtents(TCustomGrid(Filer.Ancestor).FColWidths, FColWidths)
    else
      Result := FColWidths <> nil;
  end;

  function DoRowHeights: Boolean;
  begin
    if Filer.Ancestor <> nil then
      Result := not CompareExtents(TCustomGrid(Filer.Ancestor).FRowHeights, FRowHeights)
    else
      Result := FRowHeights <> nil;
  end;


begin
  inherited DefineProperties(Filer);
  if FSaveCellExtents then
    with Filer do
    begin
      DefineProperty('ColWidths', ReadColWidths, WriteColWidths, DoColWidths);
      DefineProperty('RowHeights', ReadRowHeights, WriteRowHeights, DoRowHeights);
    end;
end;


grids.pas 유닛을 보면 알겠지만 RowHeights와 ColWidths 프로퍼티는 내부 변수로 포인터를 사용하고 Column 개수와 Row 개수가 변경될 때마다 동적으로 메모리를 할당해서 사용하고 있다. 이 두 프로퍼티는 델파이에서 기본적으로 저장할 수 있는 방법을 제공해 주지 않기 때문에 Published 영역에 둘 수 없다. 하지만 사용자가 Column의 폭을 마우스로 조절했다면 그 값을 유지하고 있어야 할 필요가 있다. 어플리케이션 개발자가 Column의 폭을 조절했는데 실행 시에 원래 기본 크기로 돌아간다고 생각해 보라. 따라서 별도로 DefineProperties를 오버라이드해서 ColWidths와 RowHeights라는 이름으로 리스트를 저장하도록 하고 있다.
1.7.9. TCollection과 TCollectionItem
만약에 독자가 개발하는 콤포넌트에서 TList를 사용하고 있고 TList의 각 아이템으로 여러 개의 Published 프로퍼티를 가지고 있는 클래스를 사용한다고 가정하고 그 값들을 폼 파일에 저장해야 할 필요성이 있다고 생각해 보자. TList는 TPersistent에서 상속 받은 클래스가 아니기 때문에 기본적으로는 폼 파일에 저장할 수 없다. 그래서 앞 절에서 얘기한 DefineProperties를 오버라이드해서 구현한다고 생각해 보자. 위의 TCustomGrid의 경우 처럼 저장할 프로퍼티의 내용이 간단한 경우에는 까짓거 하면서 구현할 수 있겠지만 내용이 복잡한 경우에는 이것도 양이 만만치 않다.
이럴 때 사용하면 좋은 클래스가 바로 TCollection과 TCollectionItem이다. TCollection은 TList 대신 TCollectionItem은 TList의 각 Item을 대신해서 사용할 수 있다. TCollection과 TCollectionItem은 모두 TPersistent 클래스에서 파생된 클래스이므로 프로퍼티를 Published에 써 주기만 하면 저장 문제는 간단하게 해결된다. 또한 TList를 사용하면 메모리 누수 문제가 발생하지 않도록 많은 신경을 써야 하지만 TCollection은 걱정할 필요가 없다. 그리고 TList를 사용한 프로퍼티를 어플리케이션 개발자가 설계 시에 변경할 수 있게 하려면 프로퍼티 에디터도 만들어 주어야 한다. 하지만 TCollection은 이 문제도 해결해 준다. 델파이는 기본적으로 Collection 프로퍼티 에디터를 가지고 있기 때문이다.
당연한 얘기겠지만 TCollection과 TCollectionItem 클래스를 직접 사용하지는 않는다. TCollection과 TCollectionItem은 위에서 얘기한 문제점들을 해결해 주는 뼈대만 제공하기 때문에 이들 클래스를 부모로 하는 파생 클래스를 만들어서 사용한다.
TCollection과 TCollectionItem은 델파이 기본 콤포넌트에서도 많이 사용되고 있는데 대표적으로 TDBGrid의 TDBGridColumns와 TColumn, TStatusBar의 TStatusPanels와 TStatusPanel, TCoolBar의 TCoolBands와 TCoolBand, TTable의 TIndexDefs와 TIndexDef 등이 있다. TStatusBar에서 사용하고 있는 예를 잠깐 살펴 보자.

TStatusPanel = class(TCollectionItem)
  private
    { 생략 }
  protected
    { 생략 }
    function GetDisplayName: string; override;
  public
    { 생략 }
    procedure Assign(Source: TPersistent); override;
  published
    { 생략 }
  end;

  TStatusPanels = class(TCollection)
  private
    FStatusBar: TStatusBar;
    function GetItem(Index: Integer): TStatusPanel;
    procedure SetItem(Index: Integer; Value: TStatusPanel);
  protected
    function GetOwner: TPersistent; override;
    procedure Update(Item: TCollectionItem); override;
  public
    constructor Create(StatusBar: TStatusBar);
    function Add: TStatusPanel;
    property Items[Index: Integer]: TStatusPanel read GetItem write SetItem; default;
  end;

  { 생략 }

  TStatusBar = class(TWinControl)
  private
    { 생략 }
    FPanels: TStatusPanels;
    { 생략 }
    procedure SetPanels(Value: TStatusPanels);
  protected
    { 생략 }
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    { 생략 }
  published
    { 생략 }
    property Panels: TStatusPanels read FPanels write SetPanels;
  end;

implementation

{ TStatusPanel }

procedure TStatusPanel.Assign(Source: TPersistent);
begin
  if Source is TStatusPanel then
  begin
    Text := TStatusPanel(Source).Text;
    Width := TStatusPanel(Source).Width;
    Alignment := TStatusPanel(Source).Alignment;
    Bevel := TStatusPanel(Source).Bevel;
    Style := TStatusPanel(Source).Style;
  end
  else inherited Assign(Source);
end;

function TStatusPanel.GetDisplayName: string;
begin
  Result := Text;
  if Result = '' then Result := inherited GetDisplayName;
end;

{ TStatusPanels }

constructor TStatusPanels.Create(StatusBar: TStatusBar);
begin
  inherited Create(TStatusPanel);
  FStatusBar := StatusBar;
end;

function TStatusPanels.Add: TStatusPanel;
begin
  Result := TStatusPanel(inherited Add);
end;

function TStatusPanels.GetItem(Index: Integer): TStatusPanel;
begin
  Result := TStatusPanel(inherited GetItem(Index));
end;

function TStatusPanels.GetOwner: TPersistent;
begin
  Result := FStatusBar;
end;

procedure TStatusPanels.SetItem(Index: Integer; Value: TStatusPanel);
begin
  inherited SetItem(Index, Value);
end;

procedure TStatusPanels.Update(Item: TCollectionItem);
begin
  if Item <> nil then
    FStatusBar.UpdatePanel(Item.Index, False) else
    FStatusBar.UpdatePanels(True, False);
end;

{ TStatusBar }

constructor TStatusBar.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  { 생략 }
  FPanels := TStatusPanels.Create(Self);
  { 생략 }
end;

destructor TStatusBar.Destroy;
begin
  { 생략 }
  FPanels.Free;
  inherited Destroy;
end;

procedure TStatusBar.SetPanels(Value: TStatusPanels);
begin
  FPanels.Assign(Value);
end;


TStatusBar에서 사용할 각 판넬을 TCollectionItem에서 상속 받아 TStatusPanel을 만들고 판넬의 리스트를 보관할 클래스로 TCollection에서 상속 받아서 TStatusPanels를 만들었다. TCollectionItem은 DisplayName 이라는 프로퍼티를 가지고 있는데 이 프로퍼티는 콜렉션 프로퍼티 에디터에 각 아이템을 표시할 때 사용하는 프로퍼티이다. 이 프로퍼티의 기본값은 TCollectionItem 파생 클래스의 클래스 이름인데 이렇게 되면 아래 그림처럼 각 아이템을 구분하기가 어려워 진다.

그림 1-12 DisplayName 프로퍼티가 기본 값일 때의 콜렉션 프로퍼티 에디터


그래서 가상 메소드로 선언되어 있는 이 프로퍼티의 읽기 메소드인 GetDisplayname을 보통 오버라이드해서 여러 가지 처리를 해 준다. 예를 들어 TStatusBar의 Panels 프로퍼티의 경우 각 Panel의 Text 프로퍼티를 변경하면 아래와 같이 보일 것이다.

그림 1-13 GetDisplayName 메소드를 오버라이드 한 후의 콜렉션 프로퍼티 에디터


다음으로 TPersistent 클래스는 Assign이라는 가상 메소드를 가지고 있는데 자신이 가지고 있는 값들을 같은 형의 다른 객체에 복사할 때 주로 사용한다. 예를 들어

var
  SourceList : TStrings;
  DestList : TStrings;
begin
  SourceList := TStringList.Create;
  DestList := TStringList.Create;
  try
    SourceList.LoadFromFile('example.Text');
    DestList.Assign(SourceList);
  finally
    DestList.Free;
    SourceList.Free;
  end;
end;


SourceList의 값을 DestList에 복사할 때 Assign 메소드를 사용하고 있다. 마찬가지로 TCollectionItem에서 상속 받는 클래스의 경우 자신 만의 프로퍼티를 추가했다면 반드시 Assign 메소드를 오버라이드해서 자신의 프로퍼티가 정상적으로 복사될 수 있도록 해 주어야 한다. TCollectionItem 클래스 뿐만이 아니고 대부분의 경우 TPersistent에서 상속 받아서 만들어지는 클래스는 가급적 Assign 메소드를 오버라이드해서 적절한 처리를 해 주는 것이 좋다.
그리고 위의 예제에서 보는 바와 같이 TCollection 클래스의 GetOwner 메소드의 반환 값이 TCollection을 소유하고 있는 콤포넌트가 되도록 해 주는 것이 좋다. 만약에 GetOwner를 오버라이드 하지 않고 상속된 값이 반환되면 콜렉션 프로퍼티 에디터가 정상적으로 동작하지 않는다.
마지막으로 TCollection 클래스를 콤포넌트의 프로퍼티로 사용하면 콤포넌트가 저장될 때 TCollection의 값들도 자동으로 저장된다. 하지만 TCollection 클래스를 단독으로 사용하고 이를 별도의 파일로 저장하려면 어떻게 하면 될까? 간단하다. TWriter와 TReader 클래스의 WriteCollection, ReadCollection 메소드를 사용하면 된다. 아래에 간단한 프로시저를 하나 만들어 보았으니 참고하기 바란다.

procedure WriteCollectionToFile(const AFileName : String; ACollection : TCollection);
var
  Stream : TMemoryStream;
  Writer: TWriter;
begin
  Stream := TMemoryStream.Create;
  try
    Writer := TWriter.Create(Stream, 1024);
    try
      Writer.WriteCollection(ACollection);
    finally
      Writer.Free;
    end;
    Stream.SaveToFile(AFileName);
  finally
    Stream.Free;
  end;
end;


procedure ReadCollectionFromFile(const AFileName : String; ACollection : TCollection);
var
  Stream : TMemoryStream;
  Reader: TReader;
begin
  Stream := TMemoryStream.Create;
  try
    Stream.LoadFromFile(AFileName);
    Reader := TReader.Create(Stream, 1024);
    try
      Reader.ReadValue;
      Reader.ReadCollection(ACollection);
    finally
      Reader.Free;
    end;
  finally
    Stream.Free;
  end;
end;





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