LazarusCompleteGuide:6.3

From 흡혈양파의 번역工房
Jump to navigation Jump to search

LCL (라자루스 컴포넌트 라이브러리)

LCL은 엄청난 수의 클래스를 포함하기 때문에 전체 클래스 트리(tree)를 그리자면 여러 장의 A3 용지를 인쇄해야 할 것이다. 하지만 트리의 최상단에 5가지 기본적인 클래스가 있는데, 이는 모든 라자루스 GUI 애플리케이션에 나타난다. 이에 해당하는 클래스를 아래 그림에 소개하겠다:

그림 6.2: LCL의 기반 클래스


그림에서 메인 클래스는 다음과 같다:

TLCLComponent - LCL의 모든 컴포넌트는 해당 클래스에서 간접적으로 파생된다. (그림 6.4 참고) Tcontrol - LCL의 모든 시각적 컴포넌트는 해당 컴포넌트에서 파생된다. (그림 6.4 참고) TWinControl - 창 핸들이 있는 모든 시각적 컨트롤은 해당 클래스에서 파생된다. (그림 6.5 참고) TForm - 에디터에서 설계된 모든 폼의 조상 클래스. TApplication - 전역적 GUI 애플리케이션 객체.


도표의 기타 컴포넌트들은 기능이 매우 적어 라자루스에서 GUI 프로그래밍을 이해하는 데에 도움이 되지 않는다 (이러한 컴포넌트들은 기능을 집합으로 모아 라자루스 LCL 관리자들의 작업을 촉진시키도록 돕는다).


콘솔 애플리케이션은 보통 하향식 모형(top-down model)에 따라 프로그래밍된다. 모든 코드와 프로시저에 시작과 끝이 명확하여 프로그램 순서가 명확하다. 일반적으로 콘솔 순서는 다음과 같다:

begin
  InitializeData;
  If Option1 then
    DoTask1
  else if Option2 then
    DoTask2;
  FinalizeData;
end.


그림 6.3: TLCLComponent와 그 자식 컴포넌트


실행되어야 하는 작업에 따라 (Option1 또는 Option2) 적절한 서브루틴이 실행된다. 표준 파스칼은 이러한 유형의 프로그래밍에 적절하며, 오브젝트 파스칼도 그에 못지않다.


GUI 프로그램은 완전히 다른 방식으로 처리된다. 프로그램 내 액션은 전적으로 사용자가 제어하며, 프로그램의 어떤 부분을 활성화할 것인지에 대한 결정도 사용자가 애플리케이션을 구성하는 다양한 컨트롤을 클릭하여 이루어진다. 사용자 액션은 윈도잉 시스템(windowing system)에 의해 프로그램으로 전달된다. 윈도잉 시스템은 GUI 시스템에 따라 다른 메커니즘을 이용하여 프로그램으로 메시지를 전달한다ㅡ이 점에서 Windows는 X-Windows와 다르게 작동한다.

그림 6.4: TControl과 그의 자손들


애플리케이션 객체

GUI 프로그램이 라자루스에서 어떻게 실행되는지 설명하기 위해서 먼저 일반적인 라자루스 프로그램 파일을 살펴보도록 하자. 새 프로젝트가 시작되면 라자루스 IDE는 .lpi 확장자로 된 XML 구성파일 외에도 확장자가 .lpr (라자루스 프로그램의 줄임말)인 파일을 생성한다.

program Project1;
uses
  Interfaces, Forms, Unit1, LResources; // Minimum needed for a GUI program.
begin
  Application.Initialize; // Initialize the LCL
  Application.CreateForm(TForm1, Form1); // Create the first (main) form
  Application.Run; // Run the message queue.
end.


라자루스 프로그램에 관한 프로그램 파일을 살펴보려면 프로젝트 인스펙터(Project 메뉴에서)를 열고 project1.lpr 파일을 더블 클릭한다.


위에서 볼 수 있듯이 모든 로직(logic)은 애플리케이션 객체 인스턴스가 처리한다. 이것은 메인 애플리케이션 로직을 처리하는 TApplication 클래스의 인스턴스다. 이는 창들을 생성하여 메시지 루프를 실행한다. 라자루스가 호출하는 주 메소드는 Initialize, CreateForm, Run, 3개가 있는데, 이를 아래에서 설명하고자 한다. TApplication 의 전체 선언은 다음과 같은 모습이다:

TApplication = class(TCustomApplication)
private
 // all private variables
public
 constructor Create(AOwner: TComponent); override;
 destructor  Destroy; override;
 procedure ControlDestroyed(AControl: TControl);
 function  BigIconHandle: HIcon;
 function  SmallIconHandle: HIcon;
 procedure BringToFront;
 procedure CreateForm(InstanceClass: TComponentClass; out Reference);
 procedure UpdateMainForm(AForm: TForm);
 procedure QueueAsyncCall(const AMethod: TDataEvent; Data: PtrInt);
 procedure RemoveAsyncCalls(const AnObject: TObject);
 procedure ReleaseComponent(AComponent: TComponent);
 function  ExecuteAction(ExeAction: TBasicAction): Boolean; override;
 function  UpdateAction(TheAction: TBasicAction): Boolean; override;
 procedure HandleException(Sender: TObject); override;
 procedure HandleMessage;
 function  HelpCommand(Command: Word; Data: PtrInt): Boolean;
 function  HelpContext(Sender: TObject; const Position: TPoint;
              Context: THelpContext): Boolean;
 function  HelpContext(Context: THelpContext): Boolean;
 function  HelpKeyword(Sender: TObject; const Position: TPoint;
              const Keyword: String): Boolean;
 function  HelpKeyword(const Keyword: String): Boolean;
 procedure ShowHelpForObject(Sender: TObject);
 procedure RemoveStayOnTop(const ASystemTopAlso: Boolean = False);
 procedure RestoreStayOnTop(const ASystemTopAlso: Boolean = False);
 function  IsWating: boolean;
 procedure CancelHint;
 procedure HideHint;
 procedure HintMouseMessage(Control : TControl; var AMessage: TLMessage);
 procedure Initialize; override;
 function  MessageBox(Text, Caption: PChar; Flags: Longint): Integer;
 procedure Minimize;
 procedure ModalStarted;
 procedure ModalFinished;
 procedure Restore;
 procedure Notification(AComponent: TComponent; Operation: TOperation);
              override;
 procedure ProcessMessages;
 procedure Idle(Wait: Boolean);
 procedure Run;
 procedure ShowException(E: Exception); override;
 procedure Terminate; override;
 procedure DisableIdleHandler;
 procedure EnableIdleHandler;
 procedure NotifyUserInputHandler(Msg: Cardinal);
 procedure NotifyKeyDownBeforeHandler(Sender: Tobject; var Key: Word;
              Shift: TShiftState);
 procedure NotifyKeyDownHandler(Sender: TObject; var Key: Word; Shift: TShiftState);
 procedure ControlKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
 procedure ControlKeyUp(Sender: TObject; var Key; Word; Shift: TShiftState);
 procedure AddOnIdleHandler(Handler: TIdleEvent; AsFirst: Boolean=True);
 procedure RemoveOnIdleHandler(Handler: TIdleEvent);
 procedure AddOnIdleEndHandler(Handler: TNotifyEvent; AsFirst: Boolean=True);
 procedure RemoveOnIdleEndHandler(Handler: TNotifyEvent);
 procedure AddOnUserInputHandler(Handler: TOnUserInputEvent; AsFirst: Boolean=True);
 procedure RemoveOnUserInputHandler(Handler: TOnUserInputEvent);
 procedure AddOnKeyDownBeforeHandler(Handler: TKeyEvent; AsFirst: Boolean=True);
 procedure RemoveOnKeyDownBeforeHandler(Handler:TKeyEvent);
 procedure AddOnKeyDownHandler(Handler: TKeyEvent; AsFirst: Boolean=True);
 procedure RemoveOnKeyDownHandler(Handler: TKeyEvent);
 procedure AddOnActivateHandler(Handler: TNotifyEvent; AsFirst: Boolean=True);
 procedure RemoveOnActivateHandler(Handler: TNotifyEvent);
 procedure AddOnDeactivateHandler(Handler: TNotifyEvent; AsFirst: Boolean=True);
 procedure RemoveOnDeactivateHandler(Handler: TNotifyEvent);
 procedure AddOnExceptionHandler(Handler: TExceptionEvent; AsFirst: Boolean=True);
 procedure RemoveOnExceptionHandler(Handler: TExceptionEvent);
 procedure AddOnEndSessionHandler(Handler: TNotifyEvent; AsFirst: Boolean=True);
 procedure RemoveOnEndSessionHandler(Handler: TNotifyEvent);
 procedure AddOnQueryEndSessionHandler(Handler: TQueryEndSessionEvent;
              AsFirst: Boolean=True);
 procedure RemoveOnQueryEndSessionHandler(Handler: TQueryEndSessionEvent);
 procedure AddOnMinimizeHandler(Handler: TNotifyEvent; AsFirst: Boolean=True);
 procedure RemoveOnMinimizeHandler(Handler: TNotifyEvent);
 procedure AddOnModalBeginHandler(Handler: TNotifyEvent; AsFirst: Boolean=True);
 procedure RemoveOnModalBeginHandler(Handler: TNotifyEvent);
 procedure AddOnModalEndHandler(Handler: TNotifyEvent; AsFirst: Boolean=True);
 procedure RemoveOnModalEndHandler(Handler: TNotifyEvent);
 procedure AddOnRestoreHandler(Handler: TNotifyEvent; AsFirst: Boolean=True);
 procedure RemoveOnRestoreHandler(Handler: TNotifyEvent);
 procedure AddOnDropFilesHandler(Handler: TDropFilesEvent; AsFirst: Boolean=True);
 procedure RemoveOnDropFilesHandler(Handler: TDropFilesEvent);
 procedure AddOnHelpHandler(Handler: THelpEvent; AsFirst: Boolean=True);
 procedure RemoveOnHelpHandler(Handler: THelpEvent);
 procedure AddOnHintHandler(Handler: TNotifyEvent; AsFirst: Boolean=True);
 procedure RemoveOnHintHandler(Handler: TNotifyEvent);
 procedure AddOnShowHintHandler(Handler: TShowHintEvent; AsFirst: Boolean=True);
 procedure RemoveOnShowHintHandler(Handler: TShowHintEvent);
 procedure RemoveAllHandlersOfObject(AnObjct: TObject); virtual;
 procedure DoBeforeMouseMessage(CurMouseControl: TControl);
 function  IsShortcut(var Message: TLMKey): boolean;
 procedure IntfQueryEndSession(var Cancel : boolean);
 procedure IntfEndSession;
 procedure IntfAppActivate;
 procedure IntfAppDeactivate;
 procedure IntfAppMinimize;
 procedure IntfAppRestore;
 procedure IntfDropFiles(const FileNames: Array of String);
 procedure IntfThemeOptionChange(AThemeServices: TThemeServices;
              AOption: TThemeOption);
 function  IsRTLLang(ALang: String): Boolean;
 function  Direction(ALang: String): TBiDiMode;

public
 procedure DoArrowKey(AControl: TWinControl; var Key: Word; Shift: TShiftState);
 procedure DoEscapeKey(AControl: TWinControl; var Key: Word; Shift: TShiftState);
 procedure DoReturnKey(AControl: TWinControl; var Key: Word; Shift: TShiftState);
 procedure DoTabKey(AControl: TWinControl; var Key: Word; Shift: TShiftState);
 property Active: boolean read GetActive;
 property ApplicationType : TApplicationType read FApplicationType write
              FApplicationType;
 property BidiMode: TBiDiMode read FBidiMode write SetBidiMode;
 property CaptureExceptions: boolean read FCaptureExceptions
              write SetCaptureExceptions;
 property FindGlobalComponentEnabled: boolean read FFindGlobalComponentEnabled
              write FFindGlobalComponentEnabled;
 property Flags: TApplicationFlags read FFlags write SetFlags;

 //property HelpSystem : IHelpSystem read FHelpSystem;

 property Hint: string read FHint write SetHint;
 property HintColor: TColor read FHintColor write SetHingColor;
 property HintHidePause: Integer read FHintHidePause write FHintHidePause;
 property HintHidePausePerChar: Integer read FHintHidePausePerChar write
              FHintHidePausePerChar;
 property HintPause: Integer read FHintPause write FHintPause;
 property HintShortCuts: Boolean read FHintShortCuts write FHintShortCuts;
 property HintShortPause: Integer read FHintShortPause write FHintShortPause;
 property Icon: TIcon read FIcon write SetIcon;
 property Navigation: TApplicationNavigationOptions read FNavigation write
              SetNavigation;
 property MainForm: TForm read FMainForm;
 property ModalLevel: Integer read FModalLevel;
 property MouseControl: TControl read FMouseControl;
 property TaskBarBehavior: TTaskBarBehavior read FTaskBarBehavior write
              SetTaskBarBehavior;
 property OnActionExecute: TActionEvent read FOnActionExecute write
              FOnActionExecute;
 property OnActionUpdate: TActionEvent read FOnActionUpdate write
              FOnActionUpdate;
 property OnActivate: TNotifyEvent read FOnActivate write FOnActivate;
 property OnDeactivate: TNotifyEvent read FOnDeactivate write FOnDeactivate;
 property OnIdle: TIdleEvent read FOnIdle write FOnIdle;
 property OnIdleEnd: TNotifyEvent read FOnIdleEnd write FOnIdleEnd;
 property OnEndSession: TNotifyEvent read FOnEndSession write FOnEndSession;
 property OnQueryEndSession: TQueryEndSessionEvent read FOnQueryEndSession
              write FOnQueryEndSession;
 property OnMinimize: TNotifyEvent read FOnMinimize write FOnMinimize;
 property OnModalBegin: TNotifyEvent read FOnModalBegin write FOnModalBegin;
 property OnModalEnd: TNotifyEvent read FOnModalEnd write FOnModalEnd;
 property OnRestore: TNotifyEvent read FOnRestore write FOnRestore;
 property OnDropFiles: TDropFilesEvent read FOnDropFiles write FOnDropFiles;
 property OnHelp: THelpEvent read FOnHelp write FOnHelp;
 property OnHint: TNotifyEvent read FOnHint write FOnHint;
 property OnShortcut: TShortcutEvent read FOnShortcut write FOnShortcut;
 property OnShowHint: TShowHintEvent read FOnShowHint write FOnShowHint;
 property OnUserInput: TOnUserInputEvent read FOnUserInput write FOnUserInput;
 property OnDestroy: TNotifyEvent read FOnDestroy write FOnDestroy;
 property ShowButtonGlyphs: TApplicationShowGlyphs read FShowButtonGlyphs
              write SetShowButtonGlyphs default sbgAlways;
 property ShowMenuGlyphs: TApplicationShowGlyphs read FShowMenuGlyphs
              write SetShowMenuGlyphs default sbgAlways;
 property ShowHint: Boolean read FShowHint write SetShowHint;
 property ShowMainForm: Boolean read FShowMainForm write FShowMainForm
              default True;
 property Title: String read GetTitle write SetTitle;
end;


그림 6.5: TWinControl과 그 자손들


이것은 TApplication 클래스에 필요한 많은 다른 타입과 함께 Forms 유닛에서 정의된다. 이러한 타입들 중 언급할 만한 가치가 있는 타입이 하나 있는데, 그 이유는 애플리케이션 타입을 명시하기 때문이다:

TApplicationType = (atDefault, atDesktop, atPDA, atKeyPadDevice );


Initialization

프로그램을 개발하면서 TApplication 프로퍼티가 설정되면 코드의 추가 행들이 자동으로 프로그램 파일에 (.lpr) 표시되기도 하고, CreateForm 호출과 함께 추가 폼이 추가되기도 한다.


Initialize 메소드에서 LCL 은 이 목적을 위해 설치되어 있다. 다양한 검사(check)가 이루어지며, 다양한 리소스가 로딩된다. 실리는 별로 없지만 TApplication 의 다른 메소드들이 자신의 작업을 적절하게 수행하도록 보장하기 위해 항상 호출되어야 한다. 이 initialization 단계를 건너뛰면 프로그램이 실행 시 오류가 발생할 수 있다.


LCL 의 initialization이 완료되면 애플리케이션 객체는 CreateForm 메소드를 사용해 메인 폼을 생성한다:

Application.CreateForm(TForm1, Form1);


이 메소드는 TForm1 클래스의 폼을 인스턴스화하고, 그에 대한 참조를 변수 Form1에 저장한다.


TForm1 클래스와 Form1 변수 모두 IDE에 의해 unit1 유닛에 생성된다. 인스턴스가 생성되고 나면 인스턴스의 다양한 프로퍼티들이 설정되고, 폼 파일에 명시된 바대로 컨트롤들로 채워진다. 폼은 아직 표시되지 않지만 메모리에 존재하며, 사용자에게 숨겨질 뿐이다.


하나 이상의 폼을 설계할 경우 프로그램 파일에 여러 개의 CreateForm 문을 삽입해야 할 것이다. 그리고 프로그램이 시작되면 이 폼들이 모두 생성되지만 여전히 숨겨져 있다. 메인 폼은 항상 이러한 방식으로 생성되는 첫 폼이다.


사용자는 프로그램 시작 시 자동으로 생성할 (그리고 자동 생성하지 않을) 폼을 제어할 수 있다. 프로젝트 옵션 대화창에서 Form 탭을 이용해 자동 생성하고 싶은 폼을 명시할 수 있다. 자동으로 생성되지 않는 폼들도 물론 원한다면 사용 전에 수동으로 생성하면 된다. 사용 전에 생성하지 못한 경우 런타임 오류를 야기할 수 있는데, 그 폼의 클래스와 관련된 폼 변수가 유효하지 않기 때문일 것이다.

그림 6.6: 자동으로 생성된 폼 vs. 이용 가능한 폼


모든 폼이 생성되고 나면 Run 메소드가 호출되어 아래 세 가지 작업을 실행한다:

  • 메인 폼을 표시한다. 메인 폼은 항시 처음으로 생성된 폼이다.
  • 메시지 루프를 실행한다.
  • 마지막 폼이 닫히고 나면 (보통 메인 폼이다) Run 메소드가 끝나고, 뒤이어 프로그램이 끝난다.


메시지 루프는 사실상 프로그램의 주요 부분이다. 이것은 위젯 셋마다 (또는 GUI 시스템마다) 다르게 구현되지만 본질적으로 메시지를 이용할 수 있는지를 검사하고, 이용할 수 있다고 판단되면 LCL가 이것을 처리하게 될 적절한 컨트롤 또는 창으로 메시지를 전송하는 루프이다.


메인 창이 닫히자마자 (프로그램 코드에 명시적으로, 또는 사용자 액션에 의해) 메시지 루프가 중단되고 Run 메소드가 끝난다. 이후 Application 객체가 해제(free)되고, 여전히 열려 있던 2차적 폼들 역시 해제(free)된다.


이벤트에 애플리케이션 로직 프로그래밍하기

애플리케이션의 Run 메소드는 메시지 루프를 실행한다. 이는 TApplication 객체의 ProcessMessages 메소드에서 이루어진다.


기본적으로 Run 메소드는 다음과 같은 모양을 한다:

procedure TApplication.Run;
begin
  ShowMainForm;
  while not Terminated do
    ProcessMessages;
end;


애플리케이션의 메인 폼이 닫히면 Terminated 변수가 True 로 설정된다. 이는 메인 루프를 종료시키고 Run 프로시저를 끝낸다.


ProcessMessages 호출은 윈도잉 시스템에 프로그램에 관한 메시지가 있는지 검사하고, 메시지가 발견되면 메시지의 대상이 되는 컨트롤로 그 메시지를 전송한다. 메시지를 수신하는 컨트롤은 두 가지 선택권을 가진다:

  1. 메시지를 완전히 처리할 수 있다. 예를 들어, paint 메시지는 컨트롤 스스로 처리한다. 각 컨트롤은 어떻게 그림을 그리는지 알고 있으므로 메시지를 어떻게 할 것인지 알 것이다.
  2. 추가 액션이 필요하다. 버튼이 클릭되는 경우를 예로 들 수 있다. 버튼은 클릭하면 무엇을 해야 할지 모르기 때문에 버튼을 클릭 시 실행되어야 하는 애플리케이션 로직(logic)의 제공 여부는 프래그래머에게 달려 있다.


물론 첫 번째 상황은 애플리케이션 프로그래머와 무관하다. 두 번째, 추가 액션이 필요한 상황은 아래 절에서 다루겠다.


이벤트 핸들러 사용하기: 절차적 타입의 프로퍼티

보통 이벤트는 절차적 타입의 프로퍼티 혹은 변수이며, 변수가 설정되면 그것이 가리키는 프로시저가 실행될 것이다. LCL 에 걸쳐 사용되는 기본 절차적 타입은 TNotifyEvent 이다:

TNotifyEvent = Procedure(Sender : TObject) of object;


모든 이벤트 핸들러는 폼 클래스의 메소드로서 구현되기 때문에 위는 메소드 포인터다. TNotifyEvent 는 단일 파라미터, Sender를 가진다. 이벤트 핸들러가 호출될 때 Send는 이벤트를 트리거한 인스턴스를 가리키는 포인터를 포함할 것이다. 이는 TObject 타입이므로, 어떤 컴포넌트에서든 사용할 수 있겠다.


다행히 이벤트 핸들러를 생성하고 필요 변수 또는 프로퍼티로 연결하는 일은 IDE가 자동으로 실행하기 때문에 따로 할 필요가 없다. 오브젝트 인스펙터의 Events 탭은 그림 6.7과 같이, 현재 선택된 컴포넌트에 가능한 모든 published 이벤트를 열거한다.

그림 6.7: 오브젝트 인스펙터의 Events 탭


리스트에서 이벤트를 하나 선택할 때는 두 가지 선택권이 있다:


첫 번째는 기존 이벤트 핸들러를 선택하는 것이다. 예를 들어, 3개의 버튼이 그들의 OnClick 이벤트로 연결된 동일한 이벤트 핸들러를 가질 수 있다. 핸들러는 어떤 버튼이 Sender 인자(argument)를 통해 이벤트를 발생(fire)시켰는지 구별할 수 있다:

TMainForm = class(TForm)
  Button1: TButton;
  Button2: TButton;
  Button3: TButton;
procedure ButtonClick(Sender: TObject);
private
{ private declarations }
public
{ public declarations }
end;

  Var MainForm : TMainForm;

implementation

{ TMainForm }

procedure TMainForm.ButtonClick(Sender: TObject);
// The same event handler used for all 3 buttons.
begin
  ShowMessage('Button "'+(Sender as TButton).Name+ '" was clicked.');
end;


그림 6.8: 오브젝트 인스펙터에서 이벤트 핸들러 선택하기


또 다른 선택 방법은, 새 이벤트 핸들러를 생성하는 것이다. 줄임 부호 버튼을 클릭하면 라자루스에서 제시한 이름으로ㅡ컴포넌트 이름 뒤에 이벤트 이름이 붙은ㅡ새 이벤트 핸들러를 생성할 것이다. 사용자는 자신이 선호하는 이름을 먼저 입력함으로써 자동 명명을 피한 후 줄임 부호 버튼을 클릭한다.


컴포넌트 이름과 같이 이벤트 핸들러 이름도 폼의 메소드를 위해 사용되므로, 유효한 파스칼 식별자여야 한다.


호출된 버튼 BPress 가 폼에 드롭되었는데, 사용자가 버튼의 OnClick 이벤트에 ShowHello 라는 이름을 입력한다고 가정하자. 줄임 부호 버튼을 누르면 유닛의 코드는 아래로 변경된다:

TMainForm = class(TForm)
  BPress: TButton;
  procedure ShowHello(Sender: TObject);
private
  { private declarations }
public
  { public declarations }
end;

  var MainForm: TMainForm;

implementation
{ TMainForm }

procedure TMainForm.ShowHello(Sender: TObject);
begin

end;


빈 메소드가 생성되면서 사용자의 코드로 채워질 준비가 될 것이다.


메소드 핸들러는 폼 선언의 첫 번째 섹션에 자동으로 생성된다. 해당 섹션은 Published 메소드와 필드용으로 예약되어 있으며, Form Designer 와 Object Inspector 가 사용하게 될 유일한 섹션이다. 해당 섹션에서 메소드가 누락되면 오브젝트 인스펙터에서 이용할 수 없을 것이다. 반대로 폼 선언의 Published 섹션으로 private 메소드를 이동시키면 Object Inspector 에서 이용할 수 있게 된다.


Published 섹션을 독점적으로 사용하는 이유는, 스트리밍 시스템은 폼을 위해 생성된 RTTI 에서 (런타임 타입 정보) 메소드를 검색하기 위해 폼 파일 내의 정보를 이용하기 때문이다. RTTI 는 published 섹션에 대해서만 생성되므로 published 메소드만 이용할 수 있다.


메소드가 생성되자마자 이는 애플리케이션 로직으로 채워진다. 사용자가 입력하는 코드는 물론 애플리케이션에서 이벤트의 목적에 따라 다르다.


컴포넌트와 컨트롤의 이벤트

각 컨트롤은 윈도잉 시스템으로부터 수신하는 다양한 메시지에 따라 고유의 메시지 이벤트 핸들러를 가진다. 컴포넌트 참조와 관련된 장은 각 컴포넌트가 지원하는 이벤트를 열거한다.


하지만 모든 이벤트 핸들러가 GUI 시스템이 전송한 메시지로 인해 호출되는 것은 아니다. 사용자 액션 외에도 프로그램 내에는 명확하게 식별되는 순간(clearly identified moment)이 필요하다. 위의 콘솔 예제를 살펴보면, 데이터의 initialization과 finalization을 위한 호출이 두 개가 있었다. 사용자가 프로그램 흐름을 지시하는 GUI 애플리케이션에선 이것이 어떻게 이루어질까?


그 답은, 사용자가 트리거하는 이벤트와 직접 연관은 없지만 애플리케이션 로직이 지시하는 이벤트를 도입하는 것이다. 예를 들어, 애플리케이션이 폼을 형성하는 지점은 명확하게 정의되어 있다. 폼이 파괴될 때도 마찬가지다. 여기 명확하게 식별 가능한 순간(clearly identifiable moment)이 두 번 있는데, 이 순간들은 폼의 수명에서 한 번씩만 발생하도록 보장된다. 폼의 생성은 리소스의 할당을 권장하는 순간으로 (예: stringlist, 아니면 기타 객체를 생성하기 위한) 폼의 수명에 걸쳐 필요로 할 것이다. 폼의 파괴는 폼이 생성될 때 할당되던 리소스를 해제하도록 권장하는 순간이다. 따라서 폼의 OnCreate 이벤트는 폼이 인스턴스화될 때 트리거되며, 그 OnDestroy 이벤트는 해당 인스턴스가 다시 파괴될 때 트리거된다: 리소스를 할당하고 할당 해제하는 완벽한 순간들이다.


OnCloseQuery 이벤트는 폼이 닫힐 때 트리거된다. 이를 사용해 데이터를 저장할 필요가 있을 때를 비롯해 필요 시 폼이 닫히지 않도록 막을 수 있다.


많은 컨트롤에 의해 노출되는 OnPaint 이벤트는 커스텀 그리기(custom drawing)에 사용될 수 있다. GUI 시스템이 만일 컨트롤을 화면에 다시 그려야한다고 결정하면 이벤트가 트리거된다.


그 외에도 많은 이벤트들이 있는데, 모두 다른 시점에 트리거된다. 해당 내용을 모두 논하는 것은 본 장의 주제 범위를 벗어난 일이다. 그 중 대부분은 이름으로 자체 설명이 가능하므로 그 기능이 무엇인지 추측할 수 있을 것이다.


TApplication으로 작업하기

TApplication은 TComponent 에서 파생된 것으로 Application 인스턴스가 생성하는 모든 폼은 TApplication 인스턴스가 소유한다. 따라서 Application 인스턴스가 해제(free)되면 (자동으로 발생) 사용자가 닫지 않은 폼들도 모두 닫히고 해제(free)될 것이다.


Application 객체에는 여러 개의 프로퍼티와 이벤트가 있는데, 이들은 객체의 행위를 제어 시 사용할 수 있다. 이러한 이벤트와 프로퍼티는 코드에서 설정하거나 폼의 TApplicationProperties 컴포넌트에 (컴포넌트 팔레트의 Additional tab 에서 이용 가능) 드롭할 수 있고, 그 프로퍼티는 오브젝트 인스펙터를 통해 설정 가능하다. 그리고 나면 컴포넌트는 전역 Application 객체의 이벤트와 프로퍼티를 자동으로 설정할 것이다. 객체에 이용 가능한 이벤트와 프로퍼티를 아래 표에 소개하겠다.

프로퍼티 목적
CaptureExceptions 예외는 메인 루프에서 처리해야 하는가?
Helpfile 애플리케이션에 관한 표준 도움말 파일.
Hint 현재 힌트.
HintColor 힌트창의 색상.
HintHidePause 힌트를 표시할 시간, 밀리미터 초.
HintPause 힌트가 표시되기까지 시간, 밀리미터 초.
HintShortCuts 힌트에 바로가기(shortcut)를 표시해야 하는가?
HintShortPause 바로가기 힌트가 표시되기까지 시간, 밀리미터 초.
ShowButtonGlyphs 표준 글리프(glyph)를 버튼 위에 표시해야 하는가?
ShowHint 힌트를 표시해야 하는가?
ShowMainForm 메인 폼을 명시적으로 표시해야 하는가?
ShowMenuGlyphs 글리프를 메뉴에 표시해야 하는가?
Title 애플리케이션 제목.
표 6.4: TApplication의 GUI 프로퍼티


이벤트 트리거 시기
OnDropFiles 사용자가 애플리케이션 내 어떤 창으로든 파일을 드롭한다.
OnEndSession 윈도잉 시스템이 애플리케이션을 중단하고 있다 (윈도우만 해당).
OnException 표시되어야 할 예외가 발생한다.
OnHelp 도움말을 요청한다.
OnHint 힌트를 표시해야 한다.
OnIdle 애플리케이션이 유휴 상태(idle)가 된다 (처리해야 할 메시지가 더 이상 없다).
OnIdlend 모든 아이들(idle) 액션이 처리되었다.
OnMinimize 애플리케이션이 최소화된다.
OnQueryEndSession 윈도잉 시스템이 애플리케이션을 중단시킬 권한을 요청한다.
OnRestore 애플리케이션이 복원되었다.
OnShowHint 힌트가 표시된다.
OnUserInput 사용자 액션 메시지가 수신되었다 (클릭 1회 또는 키 1회 누르기).
표 6.5: TApplication의 이벤트


이뿐 아니라 Application 객체는 명령 행을 처리하고 애플리케이션 실행 파일의 위치를 결정하는 메소드도 가진다. 이러한 옵션과 메소드는 TApplicationApplicationProperties 컴포넌트를 통해 이용할 수 없으므로 코드를 통해 이용해야 한다. 이를 아래 표에 표시하였다.

메소드 목적
FindOptionIndex 명령 행 옵션의 위치를 얻는다.
GetOptionValue 명령 행 옵션의 값을 얻는다 (-o 값에서와 같이).
HasOption 옵션이 명령 행에 명시되었는지 확인한다.
CheckOptions 명령 행의 모든 옵션이 유효한지 확인한다.
GetEnvironmentList 환경 변수의 리스트를 리턴한다.
표 6.6: TApplication의 메소드


메소드 목적
ConsoleApplication 애플리케이션이 콘솔 애플리케이션이라면 True.
Location 애플리케이션 바이너리의 디렉터리.
Params 명령 행 옵션으로의 색인(indexed) 접근.
ParamCount 명령 행 옵션의 수.
EnvironmentVariable 환경 변수의 값.
OptionChar 명령 행 스위치에 사용되는 문자 (보통 ‘-‘).
CaseSensitiveOptions 대·소문자를 관찰하는 명령 행 옵션을 체크해야 하는가?
StopOnException 예외를 잡을 때 애플리케이션을 중단해야 하는가?
표 6.7: TApplication의 프로퍼티


아래 토막 코드는 일반적인 명령 행 옵션 처리의 사용을 보여주는데, 아래를 이용해 명령 행에 구성 파일의 위치를 명시할 수 있다:

-c /path/to/file 또는
--config=/path/to/file


명시되지 않았다면, 애플리케이션 디렉터리 내 'settings.ini' 파일이 사용된다. 메인 폼은 이것이 생성될 때 내용을 확인한 후 다양한 설정을 읽는다:

procedure TMainForm.FormCreate(Sender: TObject);
  Var ConfigFileName : string;
begin
  // Ensure case insensitive treatment of command-line options
  Application.CaseSensitiveOptions := False;
  // if the name of a configuration file was specified, save it.
  If Application.HasOption('c','config') then
    ConfigFileName := Application.GetOptionValue('c','config')
  else
    // If no configuration file is given, use fallback location
     ConfigFileName:=Application.Location + 'settings.ini';
  ReadSettings(Configfilename); // actually read the configuration file.
end;


Windows

애플리케이션에서 눈에 보이는 창들은 모두 TForm 에서 파생되므로, TForm 은 가장 중요한 TWinControl 자손들 중 하나가 되겠다. 아래 절에서 상세히 논하겠지만 주요 프로퍼티를 아래 표에 소개하겠다:

프로퍼티 목적
ActiveControl 폼이 초기에 표시될 때 포커스를 둘 컨트롤.
AllowDropFiles 해당 폼에 파일을 드롭할 수 있는가?
AutoScroll 내용이 폼 크기에 비해 너무 클 때 폼에 스크롤바를 표시해야 하는가?
BorderIcons 어떤 테두리를 표시할 것인지 설정한다.
Caption 창 제목.
DefaultMonitor 어떤 모니터에 폼을 처음 표시할 것인가?
Font 자식 컨트롤에 대한 기본 폰트.
FormStyle 폼 스타일 (대화창, 크기 조정 등).
HorzScrollbar 가로 스크롤바 설정.
Icon 창 제목에 표시되는 아이콘.
KeyPreview 포커스된 컨트롤로 키스트로크(keystroke)를 보내기 전에 폼으로 먼저 전송해야 하는가?
Menu 어떤 TMainMenu 인스턴스가 폼의 메뉴 역할을 해야 하는가?
PixelsPerInch 화면 해상도.
Position 폼의 처음 위치.
SessionProperties 폼 세션에 걸쳐 저장해야 할 컴포넌트 프로퍼티는?
ShowInTaskBar 폼을 작업 표시줄에 어떻게 표시해야 하는가?
VertScrollbar 세로 스크롤바 설정.
WindowState 처음 창 상태.
표 6.8: TForm의 프로퍼티


이벤트 트리거 시기
OnActive 폼이 활성화된다 (예: 포커스를 받는다).
OnClose 폼이 닫힌다 (보이지 않는다).
OnCloseQuery 폼을 닫으려면 승인이 필요하다.
OnCreate 폼 인스턴스가 생성된다.
OnDeactivate 폼이 비활성화된다 (예: 포커스를 잃는다).
OnDestroy 폼 인스턴스가 파괴된다.
OnDropFiles 사용자가 폼에 파일을 드롭한다.
OnHide 폼이 보이지 않게 된다.
OnShortCut 단축키를 누른다.
OnShow 폼이 보이게 된다.
OnwindowStateChange WindowState 프로퍼티가 변경된다.
표 6.9: TForm의 이벤트


TForm 의 인스턴스들은 GUI 프로그램 내 일반 창들과 같이 프로그램에서 다른 요소들을 위한 상자들이다. 프로그램의 다른 GUI 요소들과 달리 새 폼은 항상 새 클래스이며 TForm 의 자손이다 (TForm 은 이러한 접근법이 사용되는 유일한 클래스이다). 즉, 애플리케이션은 TForm 인스턴스를 표시하지 않지만 항상 그 자손이 (예: TForm1) 됨을 의미한다.


다른 모든 컨트롤에 대해서는 표준 클래스 자체가 인스턴스화된다. 예를 들어, 사용자가 새 버튼을 생성하면 TButton 의 인스턴스가 초기화된다. 일반적으로 애플리케이션 프로그래머는 둘의 차이를 굳이 신경 쓰지 않아도 된다: 라자루스 IDE가 모든 세부 내용을 관리함과 동시 폼 파일 내에서 TForm 자손의 선언을 생성하고 폼과 동일한 이름으로 된 변수를 생성하기 때문이다. 각 폼은 구분된 유닛에서 생성된다. 유닛별로 하나 이상의 폼을 생성하기란 불가능하다. 유닛의 이름은 폼 이름과 다를 수 있지만 라자루스는 각 폼과 유닛에 기본 이름을 할당한다:

unit Unit1; // Proposed unit name
  {$mode objfpc}{$H+} // Compiler mode
interface

uses
  Classes, SysUtils, FileUtil, LResources, Forms, Controls, Graphics, Dialogs;

type
  TForm1 = class(TForm) // Proposed name for the first form in the application.
    private
    { private declarations }
    public
    { public declarations }
  end;

  var Form1: TForm1; // Global Form variable to access the form instance.

implementation

initialization
  {$I Unit1.lrs} // Resource file for the form.
end.


많은 프로그래머들은 'frm' 유닛 뒤에 폼의 이름을 붙여 명명하는 경향이 있지만 (예: 'frmMain') 꼭 따를 의무는 없다.


모든 GUI 애플리케이션은 최소 하나의 창을 가지기 때문에 라자루스 IDE가 새 GUI 애플리케이션을 생성하면 자동으로 애플리케이션의 첫 폼을 정의한다-그 선언은 앞 절에 소개하였다. 이 첫 폼은 기본 값으로 TForm1 으로 명명되지만 물론 변경할 수 있다. 사실 폼 클래스를 좀 더 기술적으로 재명명하는 것은 훌륭한 실습이 된다 (예: 메인 폼에 대해 TMainForm 로 명명).


애플리케이션에서 첫 폼은 메인 폼이라는 특별한 역할을 한다. LCL 은 이 폼을 자동으로 표시한다는 점에서 특별하다-다른 폼들은 모두 코드에 수동으로 표시해야 하는데, 관련 내용은 아래에서 논하겠다. 첫 번째 창도 특별하다고 할 수 있는데, 해당 창이 닫히면 다른 창들이 보이더라도 애플리케이션이 중단되기 때문이다. 메인 창이 닫히고 나면 LCL 은 다른 창들의 폼 인스턴스들을 해제(free)하여 창들을 닫는다.


TForm으로 작업하기

창은 TForm 의 메소드들을 이용해 수동으로 조작된다: 생성, 표시, 활성화, 숨김, 닫힘, 파괴될 수 있다. TForm 은 기타 폼 조작 메소드들도 제공한다. TForm 은 TCustomForm 의 자손이기 때문에 프로그래머는 자신이 필요로 하는 다른 창 관련 작업을 하는 메소드를 검색 시 이 클래스의 메소드를 검색해야 한다. 이용할 수 있는 메소드 대부분은 해당 이벤트를 가진다. 예를 들어, OnShow 이벤트는 Show 메소드와 관련이 있다.


창 표시하기

메인 창은 LCL 에 의해 자동으로 표시되는 반면 다른 모든 창들은 프로그램 코드를 통해 수동으로 표시되어야 한다. 창이 표시될 때 OnShow 이벤트가 트리거된다. 이 이벤트에서 당신은 원하는 initialization을 수행할 수 있다: 예를 들면, 창의 캡션 설정, 창의 특정 위치 명시, 또는 특정 창 컨트롤로 포커스 설정이 가능하다. 아래 코드는 두 번째 창을 표시하고, 활성화 컨트롤을 설정하기 위한 시도이다:

procedure TForm1.ButtonClick(Sender : TObject);
begin
  With Form2 do
  begin
    ActiveControl := Button1; // This will have no effectーsee the following event handler
    Show;
  end;
end;


두 번째 폼의 FormShow 이벤트에서는 활성화 컨트롤 또한 설정된다:

TForm2.FormShow(Sender : TObject);
begin
  Caption := 'Window opened at ' + FormatDateTime('c',Now);
  ActiveControl := Button2; // overrides any previous setting.
end;


TForm 에는 창을 표시하는 메소드가 두 가지 있다: 모달(modal) 디스플레이와 비 모달(non-modal) 디스플레이. 모달 창은 다른 창들이 닫힐 때까지 그 창들을 차단한다. 여러 개의 비 모달 창들이 표시되면, 사용자는 여러 창들을 하나씩 전환할 수 있다. 라자루스 IDE의 디자이너 창은 비 모달 창이다.


라자루스 IDE가 시작되면 많은 창이 한 번에 표시된다. 하지만 메인 창은 언제나 메뉴와 컴포넌트 팔레트가 있는 창 하나 뿐이다. 기타 모든 창들은 수동으로 시작된다.


비 모달 창을 표시하려면 Show 메소드를 사용해야만 한다. 라자루스 IDE에서 찾을 수 있는 것과 동일한 효과를 얻기 위해선 아래를 실행할 수 있다.

  1. 새 애플리케이션을 생성하라. IDE는 Form1이라 불리는 메인 폼을 생성할 것이다.
  2. 파일 메뉴를 통해 두 번째 폼을 생성하라. Form2이라 불리고, unit2라는 유닛에 위치할 것이다.
  3. 메인 폼의 (Form1) OnShow 이벤트에서 OnShow 이벤트 핸들러를 생성하라. OnShow 이벤트 핸들러가 생성되고 나면 이를 이용해 Form2를 표시할 수 있는데, 아래와 같다:
implementation

uses unit2;
{ TForm1 }

procedure TForm1.FormShow(Sender: TObject);
begin
  Form2.Show;
end;


성공적인 실행을 위해선 unit2 유닛을 unit 1 의 uses 리스트에 포함시켜야 한다: 추가를 실패할 경우 Form2을 인식할 수 없다는 컴파일러 오류가 발생할 것이다.


두 번째로, IDE가 두 번째 폼 또는 Form2 변수를 자동 생성해야 한다: 그렇지 않을 경우, TForm2 클래스의 인스턴스를 이용해 Form2 변수가 초기화될 것이다.


Show 메소드를 대신해 폼의 Visible 프로퍼티를 True로 설정할 수 있는데, Show를 호출할 때와 동일한 효과가 발생할 것이다:

procedure TForm1.FormShow(Sender: TObject);
begin
  Form2.Visible := True;
end;

위의 두 문장은 동일하다.


앞 절의 예제에서 한 번에 두 개의 폼을 표시한 바 있다. 둘은 동시에 표시되므로 사용자는 두 창을 한 번에 작업이 가능하므로 종종 그렇게 작업할 것이다. 두 번째 창을 표시하기 위한 코드에서:

procedure TForm1.FormShow(Sender: TObject);
begin
  Form2.Show;
end;


Show 로의 호출이 즉시 리턴됨을 주목해야 한다. 두 번째 폼이 표시되고 나면 첫 번째 폼의 이벤트 핸들러가 끝나고, 프로그램 흐름 제어는 프로그램의 메인 이벤트 루프로 리턴되어 이벤트가 발생 시 그를 계속 보낼 것이다. 이러한 창의 이용 방식은 흔하다.


프로그래머는 사용자 입력을 요청하는 추가 창을 표시해야 하는 경우가 많을 것인데, 이때는 필요한 입력을 사용자가 제공할 때까지 프로그램의 진행을 중단해야 한다. 이 입력은 사용자의 이름을 질문하는 등과 같이 꽤 간단하다ㅡ메시지에 대한 응답으로 버튼을 클릭하거나, 실제 타입한 입력이 될 수도 있다.


이러한 상황을 대비해 TForm은 Show 메소드에 대해 변형(variation), 즉 ShowModal 라 불리는 함수를 제공하는데, 이는 다음과 같이 선언된다:

Function ShowModal: Integer;


ShowModal 은 폼을 표시하고, 그에 따라 사용자가 어떻게든 창을 닫을 때까지 메시지 루프를 실행한다. 그러면 이는 정수(integer)를 리턴한다 (상태 코드를 나타내는 정수).


ShowModal 호출을 설명하기 위해선 앞 절에 나타낸 프로그램을 아래와 같이 변경할 수 있다.

procedure TForm1.FormShow(Sender: TObject);
begin
  Form2.ShowModal;
end;


프로그램을 실행하면 사용자는 무슨 수를 써서라도 Form2을 닫기 전까진 메인 폼에 어떤 것도 할 수 없음을 발견할 것이다.


대화창으로 설계되어 ShowModal 을 이용해 표시될 예정인 폼의 경우, ShowModal function 의 결과가 될 상태 코드를 리턴하는 것이 가능하다. 상태는 TForm 의 ModalResult 프로퍼티를 통해 설정할 수 있다. 프로퍼티를 작성하자마자 폼은 스스로 닫힐 것이다.


종종 OK 버튼이 폼 위에 위치하는데 이는 사용자가 모든 데이터를 모두 제공하였는지, 폼을 닫길 원하는지를 표시한다. OK 버튼의 OnClick 은 아래와 같이 코드화할 수 있다:

procedure TForm2.Button1Click(Sender: TObject);
begin
  ModalResult := mrOK;
end;


코드가 실행되자마자 폼은 닫히고, 이를 표시하기 위해 사용되었던 ShowModal 호출은 mrOK 의 리턴 값과 함께 리턴될 것이다. 어떤 값이든 리턴 값으로 이용할 수 있지만 아래 상수들은 사전 정의되어 시스템 대화창에서 자주 사용된다 (앞에 mr가 붙으면 ModalResult 를 의미). ModalResult 에 할당할 수 있는 사전 정의된 값들을 아래 표에 소개하겠다:

값(value) 의미
mrNone 결과가 없음, 사용자가 창 테두리에 있는 Close 버튼을 클릭하면 리턴됨.
mrOK OK 버튼의 응답
mrCancel Cancel 버튼의 응답
mrAbort Abort 버튼의 응답
mrRetry Retry 버튼의 응답
mrYes Yes 버튼의 응답
mrNo No 버튼의 응답
mrAll All 버튼의 응답
mrNoToAll 'No to All' 버튼의 응답
mrYesToAll 'Yes to All' 버튼의 응답
표 6.10: ModalResult 프로퍼티에 허용되는 값


창 닫기

Close 메소드

창을 닫는 방법에는 여러 가지가 있다: 코드를 통해, 또는 사용자가 창 테두리에 위치한 시스템 메뉴를 닫기 버튼을 클릭하는 방법이 있다. 각 그래픽 시스템은 고유의 방법으로 사용자가 창을 닫을 수 있도록 허용하는데, 보통은 단축키를 제공한다. 윈도우에선 [Alt]+[F4], 또는 [Ctrl]+[Alt]+X 키가 된다. 이러한 키 이벤트는 가로챌 수(intercept) 없으며, 창 시스템이 직접 닫기 메시지로 변환하는데, 이 메시지는 Application.Run 내에서 애플리케이션의 메시지 루프에 의해 처리된다. 그 결과 LCL 은 폼을 닫는다.


코드로 창을 닫으려면 close method 를 이용하면 된다. 이것이 close message 를 창으로 전송하면 거기서부터는 LCL이 알아서 처리할 것이다. 앞의 예제에서 들었던 두 번째 폼은 창에서 사용자 클릭을 허용하여 닫을 수 있겠다. 이러한 행위를 프로그램화하려면 Form2의 OnClick 이벤트 핸들러에 아래 코드를 삽입한다:

procedure TForm2.FormClick(Sender: TObject);
begin
  Close;
end;


사용자가 마우스로 클릭하자마자 폼은 닫힐 것이다. 아래 코드 또한 창을 닫는 방법이다:

procedure TForm2.FormClick(Sender: TObject);
begin
  Visible:= False;
end;


창을 닫는 방식이 어떻건 (코드 또는 시스템 이벤트) 창을 닫는 순간 OnClose 이벤트가 트리거되는 결과를 낳는다.


폼을 닫아선 안 되는 상황도 있다. 예를 들어, 사용자 입력을 요하는 폼에서는 원하는 데이터를 모두 올바르게 입력할 때까진 폼을 닫아선 안 된다. 창 테두리에 Close 버튼의 사용을 비활성화하고, 프로그래머가 제공하는 다른 버튼들만 이용해 폼을 닫게끔 만들 수도 있을 것이다.


폼 닫기를 제어할 수 있도록 OnCloseQuery 이벤트가 제공되는데, 이는 닫기 메시지가 도착할 때 트리거되며, 아래와 같은 모양을 한다:

procedure TForm2.FormCloseQuery(Sender: TObject; var CanClose: boolean)
begin

end;


CanClose 변수는 기본 값으로 True로 설정됨을 명심한다. 어떠한 이유로 인해 폼을 아직 닫아선 안 된다면, CanClose 변수를 False로 설정할 수 있다. 폼은 닫히지 않은 채 여전히 표시될 것이다. 아래 코드는 창이 닫히지 않도록 만드는 코드이다.

procedure TForm2.FormCloseQuery(Sender: TObject; var CanClose: boolean)
begin
  CanClose := False;
end;


OnCloseQuery 핸들러는 항상 호출되며 심지어 Close 메소드를 사용 시에도 호출됨을 명심한다. 위의 창을 닫는 유일한 방법은 폼 인스턴스를 해제(free)하는 방법뿐이다:

Form2.Free;


이런 경우, 창은 사실상 '닫히는' 것이 아니라 메모리에서 제거되는 것이다.


어떠한 OnCloseQuery 이벤트도 설정되지 않은 경우, 혹은 설정되었지만 CanClose 변수에 True로 리턴되는 경우, 폼을 닫을 수 있다. 폼이 닫히더라도 여러 가능성이 있다:

  1. 폼을 단순히 표시되지 않도록 만들었으나 여전히 메모리에 상주한다.
  2. 폼이 최소화되었다.
  3. 폼을 메모리에서 제거해야 한다.


예를 들어, 보조 창을 닫을 때에는 메모리에 유지시켜 다음에 원할 때 표시하도록 만들 수 있다.


LCL 은 이러한 결정을 내릴 수 있는 이벤트, OnClose 이벤트를 제공하는데, 아래와 같은 모습이다:

procedure TForm2.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
 // Code here what must happen with the form instance when the form is closed
end;


OnClose 이벤트 핸들러의 CloseAction 파라미터는 폼을 닫으면 어떤 일이 발생하는지를 결정한다. 아래 값 중 하나로 선택할 수 있다:

  • caNone 기본 액션이 실행된다.액션은 폼의 유형에 따라 좌우된다.
  • caHide 폼이 단순히 숨겨진다 (보이지 않도록 만들어짐). 메인 폼을 제외한 대부분 폼의 기본 액션이다.
  • caMinimize 폼이 최소화된다.
  • caFree 폼이 파괴되고 메모리에서 제거된다. 메인 폼에 대한 기본 액션이다.


폼이 라자루스가 생성한 기본 폼 인스턴스라면, caFree 결과의 사용은 권하지 않는다: 사용할 경우, 폼이 파괴된 후 전역 변수 내에 폼의 참조가 더 이상 유효하지 않게 될 것이다.


OnClose 이벤트는 추가 창을 숨기거나 특정 리소스를 삭제하는 등 정리(housekeeping) 작업을 수행하는 데에 사용할 수도 있다.


창 숨기기

실제로 창을 닫고 메모리에서 제거하는 작업이 불필요한 경우도 종종 있다. 그 때는 invisible 하게 만들어 숨기는 것으로 충분하다. 이를 위해 Hide method 를 이용하거나, Visible 프로퍼티를 False로 설정하면 된다. 둘 중 하나를 성공적으로 수행하고 나면 폼이 숨겨질 것이다. 숨겨진 폼을 다시 표시하려면 Show method 를 호출하거나, Visible 을 True로 설정한다.


폼이 숨겨지면 OnHide 이벤트 핸들러가 트리거된다. 폼이 숨겨졌을 때 이를 이용해 추가 액션을 취할 수 있다.


두 개의 창을 동시에 표시하고 숨길 수 있는 프로그램으로 설명을 해보겠다.


첫 번째 단계는 세 개의 폼이 있는 새 프로젝트를 생성하는 것이다: Form1, Form2, Form3.


Form1 의 OnClick 핸들러에서 아래의 코드가 실행된다:

procedure TForm1.FormClick(Sender: TObject);
begin
  Form2.Visible := Not Form2.Visible;
end;


Form2 이 보이지 않았다면 이제 보일 것이고, 본래 보였다면 숨겨질 것이다.


두 번째 폼의 OnShow 와 OnHide 이벤트는 아래 코드로 채워진다:

procedure TForm2.FormShow(Sender: TObject);
begin
  Form3.Show;
end;

procedure TForm2.FormHide(Sender: TObject);
begin
  Form3.Close;
end;

Form2가 표시되자마자 Form3도 표시된다. 마찬가지로 Form2가 숨겨지면 Form3도 숨겨진다.


창 전환하기

가끔은 창이 다른 창 뒤에 숨기도 하는데, 라자루스 IDE에서는 많은 창으로 프로젝트를 작업 시 꽤 자주 발생하는 일이다. 창을 젤 앞으로 가져와 활성 창으로 만들기 위해선 아래와 같이 BringToFront 메소드를 이용하면 된다:

procedure TForm2.FormShow(Sender: TObject);
begin
  Form3.Show; // ensure the window is visible
  Form3.BringToFront; // Make it the active window
end;


첫 행은 Form3이 눈에 보이도록 하고, 두 번째 행은 Form3을 애플리케이션 젤 앞에 오도록 만든다. BringToFont 를 좀 더 정교하게 이용한 예제는 창의 열거(Enumerating)에 관한 절에서 찾을 수 있다.


사용자가 창을 전환하면 포커스가 다른 창으로 바뀐다: 새로 포커스된 창이 애플리케이션의 활성 창이 된다. 이에 대해 OnActivate 이벤트 핸들러를 구현함으로써 반응할 수 있다. 아래 코드는 활성 창인지 아닌지에 따라 창 제목을 변경한다:

procedure TMainForm.FormAtivate(Sender: TObject);
begin
  Caption := FSavedCaption + ' (Active)';
end;


FSavedCaption 변수는 폼이 생성될 때 초기화된다:

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FSavedCaption := Caption;
end;


OnActivate 이벤트(폼이 활성화되면 트리거)와 비슷하게 OnDeActivate 이벤트는 폼이 포커스를 잃을 때 트리거된다.


아래 이벤트 핸들러는 이벤트를 신호로 보내도록 폼의 Caption을 변경한다:

procedure TMainForm.FormDeactivate(Sender: TObject);
begin
  Caption := FSavedCaption + ' (Inactive)';
end;


TForm의 프로퍼티

여느 컨트롤과 마찬가지로 폼의 위치 또한 Top 과 Left 프로퍼티로 결정된다. 위치는 항상 화면의 상단 좌측 코너, 0,0 좌표를 기준으로 이루어지는데, MDI 부모 창의 원점(origin)을 기준으로 위치가 달라지는 MDI 창은 제외된다 (FormStyle=fsMDIChild).


X-Windows 또는 OS 자체에서 (Mac OS 또는 Windows) 창 관리자는 화면의 원점을 변경하여 작업 표시줄을 표시할 수 있음을 주목한다; 프로그램 내에서 이를 피해갈 방법은 없다.


마찬가지로 창의 크기는 Width 와 Height 프로퍼티로 결정한다.


크기에 창 장식(window decorations; 테두리)은 포함되지 않는다.


기본 설정은, 폼이 처음으로 표시되면 Top, Left, Width, Height 프로퍼티가 오브젝트 인스펙터의 값으로부터 설정된다. 하지만 런타임 시에 프로그램에 따라 변경된다. 아래 코드는 폼을 클릭할 때마다 중앙을 중심으로 유지하면서 폼이 줄어든다.

procedure TMainForm.FormClick(Sender: TObject);
begin
  // Increase top, left
  Top := Top +10;
  Left := left + 10;
  Width := Width -20; // 20: 10 left, 10 right.
  Height := Height -20; // 20: 10 top, 10 bottom.
end;


이를 실행하면 깜박임(flickering)이 야기되는데 (폼에 컨트롤이 많은 경우 깜박임이 증가), 클릭할 때마다 폼이 위치와 모양을 네 번씩 변경하기 때문이다. 경계 사각형(Bounding rectangle) 프로퍼티인 BoundsRect 를 이용해 폼의 위치와 크기를 빠르게 설정하는 방법도 있다:

procedure TMainForm.FormClick(Sender: TObject);
  Var R : TRect;
begin
  // Get the current boundsrect
  R := BoundsRect;
  { InflateRect 'inflates' the rectangle with the indicated sizes, keeping its centre unchanged }
  InflateRect(R,-10,-10);
  // Apply the new size in 1 call !
  BoundsRect := R;
end;


그림 6.9: 이런식의 모니터 설정(dual head)을 이용 시, poDesktopCenter과 동일한 위치의 폼은 두 번째 모니터 우측 가장자리에서 잘린 채 표시될 것이다.


TRect는 Top, Left, Bottom, Right 사 면의 위치로 직사각형을 설명한다. 위의 두 번째 예제에서는 깜박임 횟수가 훨씬 줄어드는데, 클릭할 때마다 새 크기가 한 번씩 적용되기 때문이다.


기본 값으로는, 폼의 초기 위치와 크기가 Top, Left, Width, Height의 값으로 설정된다. 하지만 Position 프로퍼티를 이용해 이를 변경 가능하며, 아래 값을 이용할 수 있다:

값(value) 효과
poDefault 창 관리자가 폼의 위치와 크기 조정을 허용한다.
poDefaultPosOnly 창 관리자가 설계된 크기를 이용해 폼을 위치시키도록 허용한다.
poDefaultSizeOnly 창 관리자가 설계된 위치를 이용해 창의 크기를 설정하도록 허용한다.
poDesigned 폼 위치와 크기가 설계한 그대로이다.
poDesktopCenter 데스크톱을 중심으로 (=모든 모니터) 한 위치, 크기는 설계한 대로 진행된다. 사용자에게 여러 모니터가 있는 경우 단일 데스크톱을 형성하고, 폼이 여러 모니터에 부분적으로 표시될 수도 있다 (그림 6.9 참고).
poMainFormCenter 메인 폼을 중심으로 한 위치, 크기는 설계대로.
poOwnerFormCenter 소유자 폼을 중심으로 한 위치, 크기는 설계대로.
poScreenCenter 현재 모니터를 중심으로 한 위치, 크기는 설계대로.
표 6.11: TForm.position에 대한 상수


Position 프로퍼티의 효과는 버튼을 누르면 복사가 가능한 단일 창을 제공하는 작은 프로그램으로 쉽게 설명할 수 있다. 새 창의 Position은 새 폼의 Owner와 마찬가지로ㅡ현재 폼 또는 Application 인스턴스-radiogroup 을 이용해 선택 가능하다. 프로그램에는 세 가지 메소드가 있다:

// BClose button can be used to close the current form.
procedure TMainForm.BcloseClick(Sender: TObject);
begin
  Close;
end;


// BDuplicate button can be used to duplicate the main form
procedure TMainForm.BDuplicateClick(Sender: TObject);
Var C : TComponent;
    F : TMainForm;
begin
  // Decide who is the owner for the new form
  If (RGOwner.ItemIndex=0) then
    C := Self
  else
    C := Application;
  // Create a new form instance with the chosen owner
  F := TMainForm.Create(C);
  { Set the position property based on the value in the RGPosition radiogroup }
  F.Position := TPosition(RGPosition.ItemIndex);
  // Now show the form in the chosen position.
  F.Show;
end;

{ In the OnShow event handler, we display the value of the 'Position' property in the form's caption }
procedure TMainForm.FormShow(Sender: TObject);
  Var S : String;
begin
  // GetEnumName returns the name of the Position property; requires 'uses typinfo;'
  S := GetEnumName(TypeInfo(TPosition),Ord(Position));
  // Set the caption
  Caption := 'Form placed using position : ' + S;
end;


테두리 프로퍼티

창 테두리를 어떻게 그릴 것이며 어떻게 행동할 것인지를 제어하는 프로퍼티에는 네 가지가 있다: Caption, BorderWidth, BorderStyle, BorderIcons.


가장 분명한 프로퍼티는 작업 표시줄의 창 리스트에서 창을 식별하는 데 사용되는 창의 제목, 즉 Caption 이다. 기본 값으로 Caption 은 폼의 이름으로 설정되는데, 이는 주로 이름만으로 기능을 알 수 없으므로 변경하는 편이 낫다. 오브젝트 인스펙터에서 설정하거나 런타임 시에 설정하는 방법이 있다ㅡ창 제목은 즉시 업데이트될 것이다. 유닉스 윈도잉 시스템에서 동일한 창 제목을 한 번 이상 사용 시 창 관리자는 제목에 숫자를 덧붙일 수도 있음을 명심한다. 그림 6.11을 참고한다.


이러한 작은 프로그램의 결과를 아래 그림에 실어보고자 한다:

그림 6.10: Position 프로퍼티의 설정이 다른 다양한 폼들


그림 6.11: 동일한 숫자로 된 윈도우에 번호가 붙어 있다.


BorderIcons 프로퍼티는 (TBorderIcons 타입) 제목 표시줄에 어떤 아이콘을 그릴 것인지 결정한다. 이는 집합형(set type)으로, 아래 값의 조합을 포함할 수 있다:

값(value) 효과
biSystemMenu 창 동작과 함께 작은 메뉴를 표시하는 버튼.
biMinimize 창을 최소화하는 버튼.
biMaximize 창을 최대화하는 (복구시키는) 버튼.
biHelp help 함수를 활성화하는 버튼.
표 6.12: LCL 애플리케이션에 이용할 수 있는 제목 표시줄 버튼


이러한 값은 Windows 운영체제에 적용되며, 유닉스 또는 MacOS 에서 창 관리자는 이러한 프로퍼티가 유용하기도, 때로는 유용하지 않기도 할 것이다. 예를 들어, 리눅스에서 KDE 창 관리자는 이러한 프로퍼티를 전혀 반가워하지 않을 것이다.


BorderWidth 프로퍼티는 창의 테두리 너비를 명시한다. 테두리 너비는 안쪽으로 적용되지만 창의 ClientRect 프로퍼티엔 영향을 미치지 않는다: ClientRect 는 창 내부의 컴포넌트와 관련해 이용 가능한 공간을 명시하는 직사각형이지만, 여기서 BorderWidth 는 제해야 한다.


예를 들어보자. 폼에 메모를 드롭하고 그 Aligh 프로퍼티를 alClient 로 설정하면, 이 메모는 폼에서 이용할 수 있는 모든 공간을 채우려고 시도할 것이다 ('레이아웃 관리하기' 절에서 상세히 다룰 것이다).


메인 폼의 OnShow 와 OnResize 를 (MainForm 라 불림) 아래 프로시저로 설정하면, 폼의 Caption에 폼의 ClientRect 값뿐만 아니라 메모의 BoundsRect 값도 표시될 것이다:

procedure TMainForm.ShowSizes(Sender: TObject);
  Var S1,S2 : String;
begin
  With ClientRect do
    S1 := Format('(%d,%d) - (%d,%d)',[Left,Top,Right,Bottom]);
  With Memo.BoundsRect do
    S2 := Format('(%d,%d) - (%d,%d)',[Left,Top,Right,Bottom]);
  Caption := format('%s - %d : %s',[S1,BorderWidth,S2]);
end;


프로그램을 실행하면 아래와 비슷한 화면이 뜰 것이다:

그림 6.12: BorderWidth 프로퍼티의 효과


값(value) 의미
bsNone 창에 테두리가 전혀 없으며, 크기를 조정할 수 없다.
bsSingle 창에 하나의 테두리가 있으며, 크기를 조정할 수 없다.
bsSizeable 창에 하나의 테두리가 있으며, 크기를 조정할 수 있다.
bsDialog 창에 'dialog' 테두리가 있으며, 크기를 조정할 수 없다.
bsToolWindow 창에 'toolwindow' 테두리가 있으며(매우 작은 테두리), 크기를 조정할 수 없다.
bsSizeToolWin 창에 'toolwindow' 테두리가 있으며(매우 작은 테두리), 크기를 조정할 수 있다.
표 6.13; borderStyle 프로퍼티는 창 타입을 결정한다.


하지만 가장 중요한 프로퍼티는 BorderStyle 프로퍼티다.


이는 열거형(enumerated) 값으로, 아래 값 중 하나를 취한다:



툴 창에는 일반 창보다 작은 제목 표시줄이 있으며, MS-Windows의 윈도우 리스트에는 나타나지 않는다. Dialog 창은 윈도우 리스트에 표시된다. 크기 조정이 불가한 창에는 최소화 및 최대화 버튼이 없다. 유닉스와 같은 플랫폼들의 경우, 이러한 행위는 창 관리자에 따라 좌우된다. 예를 들어, KDE 창 관리자는 위의 프로퍼티 대부분을 준수하지 않는다.


FormStyle 프로퍼티

TForm 의 FormStyle 프로퍼티는 어떤 종류의 창을 생성할 것인지 결정하며 아래 값을 취할 수 있다:

값(value) 의미
fsNormal 일반 창. 대부분 창은 이 타입에 해당한다.
fsStayOnTop 다른 창들보다 위에 위치하기를 시도하는 창.
fsSplash 프로그램 시작 시 사용되는 창. 일반 창과 마찬가지로 행동한다.
fsMDIChild MDI 자식 창. 윈도우와 Qt 위젯 셋에서만 작동한다.
fsMDIForm MDI 부모 창. 윈도우와 Qt 위젯 셋에서만 작동한다.
표 6.14: FormStyle 프로퍼티에 가능한 값


fsMDIForm 스타일로 된 폼은 fsMDIChild 스타일로 된 창들의 부모 창이다. 자식 창들은 부모 창을 떠날 수 없지만 그 내부에선 이동할 수 있으며, 부모 창에서 최소화 또는 최대화가 가능하다.


이는 Qt 또는 Windows 위젯 셋이 LCL과 GTK 에 사용되었을 때만 적용되며, Carbon 에는 MDI 창이란 개념이 없다.


fsStayOnTop 창 스타일은 작은 툴 창과 같이 다른 창들보다 앞에 위치한 작은 창에 사용된다. 애플리케이션은 fsStayOnTop 스타일로 된 창을 하나 이상 생성할 수 있다.


최소화와 최대화: WindowState

대부분 운영체제에서는 사용자로 하여금 창이 전체 데스크톱을 차지하도록 만드는 방법을 하나쯤 제공한다: 창의 최대화. 이러한 작업은 운영체제 창 관리자에 의해 실행된다. 그 반대로 창의 최소화도 가능하다-이 상태에선 창이 작업 표시줄이나 작업 리스트에 아이콘으로만 표시될 것이다 (OS가 그러한 기능을 제공 시에만).


창의 현재 상태는 WindowState 프로퍼티를 통해 찾을 수 있다.


아래 표에 실린 값이 가능하다:

WindowState 의미
wsMinimized 창이 최소화된다 (아이콘화).
wsNormal 창이 일반 크기이다.
wsMaximized 창이 최대화된다.
표 6.15: WindowState 프로퍼티에 가능한 값


유닉스 창 관리자는 wsMaximinzed 상태를 제공하지 않음을 주목하라: 창을 전체 데스크톱을 덮도록 크기를 조정할 수는 있지만 일반 상태와 구별이 불가하다. 폼이 상태를 변경하면 OnWindowStateChange 이벤트가 트리거되며, 창 상태의 변경에 응답하는 데에 사용될 수 있다.


WindowState 프로퍼티를 작성할 수도 있다. 아래 코드는 창을 더블클릭할 때마다 일반, 최대화, 최소화 상태로 차례로 바뀌어 Caption에 현재 상태를 변경한다.

procedure TMainForm.FormDblClick(Sender: TObject);
begin
  if WindowState = wsNormal then
    WindowState := wsMaximized
  else if WindowState = wsMaximized then
    WindowState := wsMinimized;
end;

procedure TMainForm.FormWindowStateChange(Sender: TObject);
begin
  Caption := GetEnumName(TypeInfo(TWindowState),Ord(WindowState));
end;


동일한 창의 다중 인스턴스 표시하기

기본 값으로 라자루스 IDE는 프로그램 시작 시 설계된 폼을 모두 생성하는데, 바로 이 점 때문에 아래와 같은 문들이 문제없이 작동한다:

procedure TForm1.FormClick(Sender: TObject);
begin
  Form2.Visible := Not Form2.Visible;
end;


이 코드에서는 변수 Form2가 TForm2 로의 참조를 포함한다고 가정하는데, 라자루스 IDE에 의해 자동으로 생성된 모든 창들이라면 True 이다.


자동으로 생성되지 않은 폼들에 대한 폼 변수도 존재하긴 하지만 Nil을 포함한다. True 인 경우 (Form2는 자동 생성되지 않음) 위의 코드를 이용 시 "접근 위반"이라는 오류 메시지와 함께 프로그램이 충돌할 것이다.


프로그래머는 자동으로 생성되지 않은 폼들에 대해선 스스로 폼을 생성해야만 한다. 폼은 다른 여느 클래스와 같아 아래와 같이 간단히 생성할 수 있다:

procedure TForm1.FormClick(Sender: TObject);
begin
 // If Form2 does not exist yet, create it:
  If (Form2 = Nil) then
    Form2 := TForm2.Create(Application);
  Form2.Visible := Not Form2.Visible;
end


이 코드에서,

Form2 := TForm2.Create(Application);


위의 문(statement)은 Form2이 자동으로 생성될 때 IDE가 프로그램에 생성하는 문과 거의 동일하다.

Application.CreateForm(TForm2,Form2);


차이점이라면, 후자 명령의 경우 처음으로 생성된 폼에 대한 참조를 유지하고 이를 메인 폼으로 설정하기 때문에 TApplication.Run 메소드는 어떤 폼을 표시할 것인지 알고 있다는 점이다.


동적으로 생성되는 창

때로는 동일한 폼에 대한 다중 인스턴스를 생성하고 인스턴스마다 서로 다른 데이터를 표시하도록 만들 필요가 있다. 예를 들어, 송장작성(invoicing) 애플리케이션에서는 두 개의 송장을 시각적으로 한 번에 비교하도록 그에 대한 세부내용을 표시해야 하거나, 혹은 두 명의 고객에 대한 연락처 세부내용을 비교해야 하는 경우가 있다. Outlook 또는 Thunderbird와 같은 이메일 프로그램은 다수의 이메일의 동시 보기 및 동시 작성을 허용한다.


아래와 같은 구문을 이용하여

Application.CreateForm(TForm2,Form2)


IDE가 자동으로 폼을 생성하도록 만들거나,

Form2 := TForm2.Create(Application);


위를 이용해 프로그래머가 수동으로 폼을 생성하는 방법이 있는데, 어떤 방법을 사용하건 Form2는 TForm2 의 인스턴스를 가리키는 결과를 낳는다. Form2 변수는 하나만 존재하며, 변수는 폼 인스턴스에 대한 하나의 참조만 포함할 수 있으므로, TForm2 의 인스턴스 두 개를 자동으로 생성하기란 불가능하다는 규칙을 따른다.


단일 폼의 다중 인스턴스를 생성하기 위해선 폼을 수동으로 생성해야 한다:

Form2 := TForm2.Create(Application);
Form2.Show;


이는 TForm2 의 인스턴스를 생성하고 표시하며, 그에 대한 참조를 Form2 변수에 보관한다. 이 문을 여러 번 반복하면 다수의 TForm2 인스턴스를 생성하지만, 마지막에 생성된 인스턴스만 Form2 변수에 저장될 것이다.


Owner는 Application 인스턴스에 설정되어 있음을 명심하자. 그 결과 LCL 은 폼 인스턴스의 Name 프로퍼티를 변경할 것이다: 설계 시 Name 프로퍼티와 다를 것이다. 대신, Name 프로퍼티를 설계 시 이름으로 설정하려면 그에 정수(integer)를 추가하면 된다 (Form2_2, Form2_3과 같이).


LCL 은 생성된 인스턴스를 추적하는 내장된 방식을 따로 제공하지 않는다 (하지만 2.9절 내용을 참고하라). 따라서 만일 자신이 생성한 폼의 리스트를 보고 싶다면 TStringList 인스턴스를 이용하는 등 수동으로 설정해야만 한다.


아래 작은 프로그램은 메인 폼을 더블 클릭할 때마다 두 번째 폼을 생성하여 모든 인스턴스로의 참조를 유지한다.

TForm1 = class(TForm)
  procedure FormCreate(Sender: TObject);
  procedure FormDblClick(Sender: TObject);
  procedure FormDestroy(Sender: TObject);
  procedure RemoveForm(Sender: TObject);
 private
 { private declarations }
 public
 { public declarations }
  FForms:TStringList;
  Delta : Integer;
end;

  var Form1: TForm1;

implementation
{ TForm1 }

uses secondform; // Secondform contains the definition of TForm2

procedure TForm1.FormCreate(Sender: TObject);
begin
  Fforms := TStringList.Create; // create a stringlist to store form instances.
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeAndNil(FForms); // Free the stringlist in which form instances are stored.
end;

procedure TForm1.FormDblClick(Sender: TObject);
begin
  Form2 := TForm2.Create(Application); // Create a form instance
  Form2.OnDestroy := @RemoveForm; // Attach a hook so we are notified when it is destroyed
  Form2.Top := Form2.Top+Delta; // Move the form a bit so they don't all appear on top of each other.
  Form2.Left := Form2.Left+Delta;
  Form2.Show; // Show it
  FForms.AddObject(Form2.Name,Form2); // Add the form name and instance to the form list.
  Inc(Delta,30); // increase the position delta
end;

procedure TForm1.RemoveForm(Sender: TObject);
  Var I : Integer;
begin
  I := FForms.IndexOfObject(Sender); // this is called whenever a Form2 instance is destroyed.
  If (I <> -1) then
  begin
    // Found it in the list, so display a message in caption
    Caption := 'Deleted instance ' + FForms[i];
    FForms.Delete(I); // and remove it from the list.
  end;
end;


RemoveForm 이벤트 핸들러를 주목하자: 생성된 폼 인스턴스가 파괴되면 인스턴스로의 참조를 폼 리스트에서 제거해야 한다. Form2 인스턴스는 사실 닫힐 때 해제(free)된다는 점에 특히 주의를 기울여야 한다: 그것의 OnClose 이벤트 핸들러에 대한 CloseAction 파라미터는 다음과 같이 caFree로 설정해야 한다:

procedure TForm2.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  CloseAction := caFree;
end;


기본 값으로는 모달(modal) 폼을 닫으면 숨겨질 뿐이기 때문에 해당 이벤트 핸들러는 폼 리스트를 유지 시 항상 코드화되어야 한다. 몇 개의 창을 생성하고 하나를 삭제한 후 이 프로시저를 실행한 결과를 그림 6.13에서 볼 수 있다:

그림 6.13: 수동으로 생성한 다중 창


특수 창

프로그래머는 모든 폼을 설계할 필요 없이 LCL 에 이미 포함된 단순한 폼을 유용하게 이용할 수 있다. 라자루스에서 동적으로 생성한 단순한 폼들도 여러 개 있으며, 그 기능은 소수의 간단한 함수에 래핑(wrapped)된다. 기본적으로 두 종류의 단순한 폼이 제공된다:

  • 메시지와 아이콘이 표시된 단순한 폼. 사용자는 해당 폼의 닫기만 실행할 수 있다.
  • 메시지와 아이콘, 표준 버튼 집합이 표시된 위의 변형(variation).
  • 사용자가 타이핑한 값을 입력할 수 있는 단순한 폼 (또는 대화창 취소가 가능한).


LCL 은 이러한 폼들을 미리 설계하여 제공하므로 위와 같은 단순한 폼들을 사용자가 따로 설계할 필요는 없다. 이와 관련된 내용은 아래 여러 단락에 걸쳐 논하겠다. 운영체제들은 이미 만들어진 대화창도 제공한다: 파일과 디렉터리 선택 대화창; 프린터 선택 및 설치 대화창; 찾기 및 바꾸기 대화창. 이러한 대화창들은 복잡하며 많은 옵션을 포함한다: 단순한 함수에 포함하기엔 너무 많다. 따라서 폼에 드롭할 수 있는 컴포넌트로서 이용할 수 있어야 하며, 그 옵션은 프로퍼티에 래핑된다. 이러한 대화창 컴포넌트들은 컴포넌트 관련 장에서 상세히 다루고 있다.


ShowMessage

ShowMessage는 LCL 의 이미 만들어진 창 중에 가장 단순한 형태이다. 이는 메시지와 대화창을 닫는 OK 버튼을 표시한다. 그 선언은 매우 간단하다:

Procedure ShowMessage(Const aMsg : String);


사용자가 OK 버튼을 클릭하면 함수가 리턴한다. 다른 상호작용은 전혀 불가능하며, 액션이 성공적으로 실행되었는지 (아닌지) 사용자에게 알려주는 기능만 한다. 아래 그림은 메시지 대화창이 활성화된 모습이다:

그림 6.14: 메시지 대화창


ShowMessage을 조금 변형시킨 두 개의 변형(variation)을 살펴보자:

procedure ShowMessageFmt(const aMsg: string; Params: array of const);
procedure ShowMessagePos(const aMsg: string; X, Y: Integer);


ShowMessageFmt 는 먼저 표준 Format 호출과 마찬가지로 제공된 인자(argument)를 이용해 메시지를 포맷한 후 ShowMessage 와 정확히 동일한 행위를 한다. 두 번째 프로시저는 메시지 대화창을 특정 좌표로 (X, Y) 위치시킨다.


MessageDlg

가끔은 사용자 응답을 필요로 하기도 한다 - 간단한 Yes 또는 No, 또는 Retry 혹은 Cancel 이 추가로 붙기도 한다. ShowMessage 호출은 이에 적합하지 않으므로, MessageDlg 를 호출하면 약간 확장된 함수를 제공받을 수 있다. 이는 아래와 같이 정의된다:

function MessageDlg(const aMsg: string; DlgType: TMsgDlgType;
	 Buttons: TMsgDlgButtons; HelpCtx: Longint): Integer;


위를 호출하면 아이콘, 텍스트 문자열 (aMsg 에 명시된), 하나 또는 이상의 버튼이 포함된 대화창이 표시된다. 이 아이콘은 사전 정의된 여러 아이콘들 중 하나로, DlgType 파라미터를 통해 선택된다. 이 파라미터에 가능한 값은 아래 표에 소개되어 있다:

상수 함수
mtInformation 정보를 제공하는 대화창.
mtConfirmation 승인을 요청하는 대화창.
mtWarning 잠재적으로 위험한 액션을 경고하는 대화창.
mtError 오류 상황을 보고하는 대화창.
mtCustom 제목 표시줄의 프로그램 이름과 정보 아이콘이 있는 커스텀 대화창.
표 6.16: 가능한 메시지 대화창 타입 (DlgType 파라미터의 값)


대화창에 어떤 버튼(들)을 표시할 것인지는 세 번째 파라미터에 의해 결정된다:


Buttons

이는 set 파라미터로, 가능한 함수 리턴 값을 결정하기도 한다. 리턴 값은 ModalResult 에 가능한 값들 중 하나이기도 한데, 대화창은 모달(modally)식으로 표시되기 때문이며, 프로그램 로직이 지속되기 전에 닫아야 한다. 가능한 리턴 값은 아래 표에 실려 있다.

상수 함수와 버튼 값
mrYes 버튼을 (caption: Yes) 눌렀을 때.
mrNo 버튼을 (caption: No) 눌렀을 때.
mrOK 버튼을 (caption: OK) 눌렀을 때.
mrCancel 버튼을 (caption: Cancel) 눌렀을 때.
mrAbort 버튼을 (caption: Abort) 눌렀을 때.
mrRetry 버튼을 (caption: Retry) 눌렀을 때.
mrIgnore 버튼을 (caption: Ignore) 눌렀을 때.
mrAll 버튼을 (caption: All) 눌렀을 때.
mrYesToAll 버튼을 (caption: Yes to all) 눌렀을 때.
mrNoToAll 버튼을 (caption: No to all) 눌렀을 때.
mrNone 버튼을 이용해 대화창을 닫히지 않고, 사용자가 시스템 메뉴를 이용해 인스턴스에 해당하는 창을 직접 닫았을 때.
표 6.17: MessageDlg 함수의 리턴 값


HelpCtx 파라미터를 이용해 help 컨텍스트를 제공할 수도 있다. 아래 스크린 샷은 MessageDlg 함수를 실행한 모습이다:

그림 6.15: 표준 MessageDlg 함수의 실행


다시 말하지만 기본 MessageDlg 함수는 덧붙이는것에 따라 약간의 다른 기능을 제공한다:

function MessageDlg(const aCaption, aMsg: string; DlgType: TMsgDlgType;
	 Buttons: TMsgDlgButtons; HelpCtx: Longint): Integer;

function MessageDlg(const aCaption, aMsg: string; DlgType: TMsgDlgType;
	 Buttons: TMsgDlgButtons; HelpCtx: Longint; DefaultButton: TMsgDlgBtn):
	 Integer;

function MessageDlg(const aCaption, aMsg: string; DlgType: TMsgDlgType;
	 Buttons: TMsgDlgButtons; const HelpKeyword: string): Integer;


첫 번째는 대화창의 캡션을 (aCaption) 명시하도록 해주고, 두 번째는 어떤 버튼을 기본 버튼으로 해야 하는지를 명시하도록 해준다 (Yes 버튼이 있다면 일반 변형(regular variant)에서 기본 값에 해당한다). 세 번째 변형은 help 컨텍스트 대신 help 키워드를 명시하도록 해준다.


아래 MessageDlg 함수의 변형 두 가지는 대화창을 위치시킬 정확한 위치(X, Y)를 명시하도록 해준다:

function MessageDlgPos(const aMsg: string; DlgType: TMsgDlgType;
	 Buttons: TMsgDlgButtons; HelpCtx: Longint; X, Y: Integer): Integer;

function MessageDlgPosHelp(const aMsg: string; DlgType: TMsgDlgType;
	 Buttons: TMsgDlgButtons; HelpCtx: Longint; X, Y: Integer;
	 	  const HelpFileName: string): Integer;


두 번째 변형에서는 사용할 help 파일의 이름도 명시 가능하다.


QuestionDlg

연구에 따르면 사용자들은 팝업 창의 질문을 대충 읽는 바람에 무엇을 승낙하는지 생각하지도 않고 Yes를 누르는 경향이 있음을 보였다. 따라서 사용자에게 Yes와 No 버튼을 제공하는 대신 질문을 바꾸어 말하고, 닫기 버튼에 자체에 대한 캡션처럼 질문에 대한 적절한 응답을 제시하는 편이 낫다. 사용자는 종종 아래와 같은 대화창을 볼 수 있을 것이다:

그림 6.16: MessageDlg 함수의 그릇된 사용


사용자는 생각하기도 전에 습관적으로 Yes를 누르게 되어 데이터를 잃게 된다! QuestionDlg function 은 고급 대화창을 제공하도록 해준다. 함수는 아래와 같이 정의된다:

function QuestionDlg(const aCaption, aMsg:string; DlgType: TMsgDlgType;
	 Buttons: array of const; HelpCtx: Longint): TModalResult;

function QuestionDlg(const aCaption, aMsg:string; DlgType: TMsgDlgType;
	 Buttons: array of const; const HelpKeyword: string): TModalResult;


MessageDlg 함수와 같이 첫 번째 세 개의 인자(argument)는 대화창에 대한 caption, message, icon 이다. 하지만 버튼 배열은 다르다: Button 은 TModalResult 값과 문자열 값이 혼합된 배열이다. 각 TModalResult 값에 대한 버튼이 생성된다. TModalResult 값 다음에 문자열 값이 따라오면 그 문자열은 TModalResult 값과 관련된 기본 캡션 대신 버튼에 대한 캡션으로 사용될 것이다.


예를 들어, 아래와 같은 배열은:

[mrNo,'Discard data', mrYes,'Keep data', mrCancel]


아래와 같이 세 개의 버튼을 표시한다:

  • 첫 번째 버튼에 캡션 Discard(삭제하기) 데이터가 있고, 대화창을 누르면 mrNo 가 리턴된다.
  • 두 번째 버튼에 캡션 Keep(저장하기) 데이터가 있고, 대화창을 누르면 mrYes 가 리턴된다.
  • 마지막 버튼에 캡션 Cancel(취소하기)가 있고, 대화창을 누르면 mrCancel 이 리턴된다.


실행되면 아래와 같은 모습을 할 것이다:

그림 6.17: QuestionDlg 함수를 이용한 좀 더 효과적인 승인 대화창


이와 같은 경고 대화창을 생성하는 데에는 다음과 같은 코드가 사용된다:

Resourcestring
  SWarning = 'Warning !'; // Some messages
  SDataModified = 'Data in this form was modified.'+sLinebreak+
    'What do you want to do?';
  SDiscard = 'Discard modifications';
  SKeep = 'Save modifications';

procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: boolean);
begin
  CanClose := Not MText.Modified; // If nothing was modified, we can close.
  If Not CanClose then // Ask the user what to do
    case QuestionDlg(SWarning,SDataModified,mrWarning,
      [mrYes,sKeep,mrNo,SDiscard,mrCancel],0) of
      mrYes : Canclose := True;
      mrNo  : begin
                CanClose := True;
                // Save Data
              end;
    // in all other cases, the form cannot be closed.
    end;
end;


InputQuery

여태까지 살펴본 표준 대화창들은 사용자가 값을 제공하도록 허용하지 않는다: 사용자에게 클릭할 수 있는 버튼만 제공했을 뿐, 어떤 내용도 입력하지 못하도록 했다. 그럼에도 불구하고 프로그래머는 종종 사용자에게 이름, 번호와 같은 값을 요청해야 하는 경우가 있다. InputQuery 함수는 정확히 이러한 상황에 필요하다:

function InputQuery(const ACaption, APrompt : String;
	 var Value : String) : Boolean;


InputQuery 는 ACaption 캡션이 있는 창을 팝업시키는데, 사용자는 여기에 짧은 답을 입력할 수 있다 (기본 답변은 AValue에서 제공할 수 있다). 창에는 OK 와 Cancel 버튼이 있으며, 짧은 메시지를 (APrompt) 표시할 수 있다. 함수는 사용자가 AValue에 타이핑한 값을 리턴할 것인데, 사용자가 OK 버튼으로 함수를 닫으면 True 결과를, 아니면 False를 리턴할 것이다.


아래 코드는 사용자에게 폼의 캡션을 변경할 것인지 여부를 질문하는 동시 현재 캡션의 값을 시작 기본 값(starting default)으로 제공한다:

Resourcestring
  STitleChange = 'Change window caption';
  SEnterNewTitle = 'Enter the new value for the window title:';

procedure TMainForm.Button1Click(Sender: TObject);
  Var S: String;
begin
  S := Caption;
  if InputQuery(STitleChange,SEnterNewTitle,True,S) then Caption := S;
end;


해당 코드의 결과는 아래와 같다:

그림 6.18: InputQuery 함수의 사용 예제


때로는 비밀 번호를 요청해야하는 경우도 있을 것이다.


따라서 InputQuery 의 변형(variant)이 제공된다:

function InputQuery(const ACaption, APrompt : String; MaskInput : Boolean; var Value : String) : Boolean;


MaskInput 값이 True일 경우 입력된 값은 가려질 것이다 (‘*’와 같은 비밀번호 문자만 표시될 것이다).


사용자가 대화창을 닫기 위해 어떤 버튼을 눌렀는지 관심이 없다면 아래 변형(ariant)을 사용할 수 있다:

function InputBox(const ACaption, APrompt, ADefault : String) : String;
function PasswordBox(const ACaption, APrompt : String) : String;


첫 번째 함수는 사용자가 대화창을 취소하면 ADefault를 리턴하고, 두 번째 함수는 항상 비어 있는 비밀번호로 시작하여 사용자가 대화창을 취소하면 빈 문자열을 리턴한다.


창 환경

TScreen

라자루스는 TApplication 인스턴스와 매우 유사한 방식으로 Screen 변수를 통하여 이용 가능한 전역적 TScreen 인스턴스를 유지한다. 여기에는 현재 데스크톱에 관한 정보가 포함되어 있으며, 주로 현재 데스크톱 환경에 관한 모든 종류, 즉 크기, 현재 커서 등과 같은 정보를 제공하는 많은 프로퍼티들로 구성된다. 애플리케이션의 현재 폼 인스턴스에 관한 정보도 포함한다: 전역적 애플리케이션 인스턴스에 의해 생성되든 아니면 코드에서 수동으로 생성되든 상관없다. 각 폼이 인스턴스화되면 스스로를 전역 화면 인스턴스(global screen instance)로 등록한다. TScreen 클래스의 모든 프로퍼티를 아래 표에 열거하였다:

프로퍼티 다음에 관한 정보 제공
ActiveControl 현재 활성화된 TWincontrol 인스턴스.
ActiveCustomForm 현재 활성화된 TCustomForm 인스턴스.
ActiveForm 현재 활성화된 TForm 인스턴스.
Cursor 현재 커서.
Cursors 등록된 커서 (배열 프로퍼티다).
CustomFormCount TCustomForm 인스턴스의 현재 번호.
CustomForms 이용 가능한 모든 TCustomForm 인스턴스 (배열 프로퍼티다).
CustomFormZOrderCount ZOrder로 정렬된 TCustomForm 인스턴스의 수.
CustomFormsZOrdered 이용 가능한 TCustomForm 인스턴스의 배열, ZOrder 순서.
DesktopHeight 데스크톱 높이 픽셀.
DesktopWidth 데스크톱 너비 픽셀.
FocusedForm 현재 활성화된 TForm 인스턴스.
FormCount TForm 인스턴스의 현재 번호.
Forms 이용 가능한 모든 TForm 인스턴스 (배열 프로퍼티다).
DataModuleCount 애플리케이션에 활성화된 데이터 모듈의 수.
DataModules 모든 이용 가능한 TDatamodule 인스턴스의 배열.
HintFont 시스템 내 힌트에 공통으로 사용되는 폰트.
IconFont 시스템 내 아이콘에 공통으로 사용되는 폰트.
MenuFont 시스템 내 메뉴에 공통으로 사용되는 폰트.
SystemFont 기본 텍스트 폰트.
Fonts 모든 설치된 폰트 이름이 있는 TStrings 인스턴스.
Height 화면의 높이.
MonitorCount 모니터의 수.
Monitors 이용 가능한 모든 모니터의 배열.
PixelsPerInch 화면의 DPI (인치당 도트 수).
PrimaryMonitor 주요 모니터 데이터.
Width 화면의 너비.
표 6.18: TScreen의 프로퍼티


TScreen 인스턴스는 두 개의 이벤트만 가진다:

이벤트 트리거 시기
OnActiveControlChange 포커스된 컨트롤이 변경될 때.
OnActiveFormChange 활성화된 (포커스된) 폼이 변경될 때.
표 6.19: TScreen의 이벤트


Screen 인스턴스는 사용자 애플리케이션의 창에 대한 리스트를 생성할 때 일반적으로 사용되는데, 이를 이용해 Window 메뉴를 덧붙일 수 있다. 뿐만 아니라, 인스턴스를 이용해 창들 중 하나를 활성화시킬 수도 있다. 아래 코드를 통해 과정을 설명하겠다:

procedure TMainForm.RefreshWindowList(List: TStrings);
  Var I : Integer;
      F : TForm;
begin
  List.Clear;
  For I:= 0 to Screen.FormCount - 1 do
  begin
    F := Screen.Forms[i];
    List.AddObject(F.Caption,F)
  end;
end;


procedure TMainForm.MWindowsClick(Sender: TObject);
  Var I : Integer;
      MI : TMenuItem;
begin
  MWindows.Clear;
  RefreshWindowList(FForms);
  For I:= 0 to FForms.Count - 1 do
  begin
    MI := TMenuItem.Create(Self);
    MI.Caption := FForms[i];
    MI.OnClick := @RaiseWindow;
    MI.Tag := I;
    MWindows.Add(MI);
  end;
end;


procedure TMainForm.RaiseWindow(Sender : Tobject);
  Var I : integer;
      F : TForm;
begin
  I := (Sender as TMenuItem).Tag;
  F := TForm(FForms.Objects[I]);
  F.SetFocus;
end;


이 코드의 결과를 아래 그림에 나타내보겠다:

그림 6.19: Window 메뉴 실행


폼에 내용 추가하기: 컨트롤

빈창이 있는 프로그램은 별로 흥미롭지 못한 것은 확실하다: 따라서 빈창은 컨트롤로 (위젯이라고도 알려짐) 채울 필요가 있겠다. 컨트롤은 시각적 TComponent 자손들로서, 다시 말하자면 폼이나 창에 사용자가 애플리케이션을 실행 시 눈으로 볼 수 있음을 의미한다. GUI 애플리케이션에 사용되는 모든 컨트롤은 TControl 또는 TWinControl 의 자손들이다. 두 클래스의 차이를 아래 절에서 설명하겠지만, 먼저 IDE에서 컨트롤을 폼으로 추가하는 과정을 살펴보겠다.


컨트롤 처리하기

컨트롤 추가하기

컨트롤을 폼에 추가하는 것은 꽤 간단하다: 라자루스 메인 창의 컴포넌트 팔레트 탭 중 하나를 골라 폼 위에 원하는 컨트롤을 위치시킨 후 그 아이콘을 클릭한다. 툴버튼이 누름(depressed) 상태로 남겨지는데, 컨트롤이 추가되고 있음을 나타낸다. 그리고 폼을 클릭한다. 이는 클릭이 발생한 상단 좌측 모서리와 함께 컨트롤을 위치시킨다 (Top과 Left 프로퍼티는 마우스 클릭 위치 좌표로 설정될 것이다). 컨트롤이 폼 상에 기본 크기로 표시된 후 선택된다. 이 프로퍼티는 오브젝트 인스펙터에서 즉시 변경하거나 마우스를 이용해 폼 위에서 크기 및 위치를 조정할 수 있다.


컨트롤에는 기본 이름이 주어진다-클래스 이름 뒤에 (첫자 'T'는 제거됨) 번호가 하나 붙는다. (Environment→Options 대화창의 Form Editor 탭에서 Ask name on create 옵션이 체크되어 있을 경우, Choose Name 대화창이 뜨면서 사용자는 기본 이름을 수락하거나 다른 이름을 입력할 수 있다.) 예를 들어, 라벨이 (TLabel 클래스의 라벨, 컴포넌트 팔레트의 Standard 탭 상에) 빈 폼에 드롭되면, 그 기본 이름은 Label1이 될 것이다.


직후에 두 번째 라벨이 드롭되면 Label2라고 명명될 것이다 (그림 6.20 참고).

그림 6.20: IDE에서 폼에 컨트롤 추가하기


라자루스는 컨트롤이 폼에 드롭될 때마다 각 컨트롤을 폼의 클래스 정의에 필드로서 추가함으로써 폼의 클래스 정의를 변경한다. 두 개의 라벨을 추가한 앞의 예제에서 폼 클래스는 아래와 같을 것이다:

{ TForm1 }

TForm1 = class(TForm)
  Label1: TLabel;
  Label2: TLabel;
 private
  { private declarations }
 public
  { public declarations }
end;


클래스 정의에서 필드 이름은 컨트롤의 실제 이름이라는 사실을 명심하라. 사용자는 오브젝트 인스펙터에서 Name 프로퍼티를 편집하여 컨트롤 이름을 변경할 수 있다. 컨트롤은 폼 클래스에 필드로서 정의되어 있기 때문에 Name 은 유효한 파스칼 식별자여야 한다 (예: 문자 또는 밑줄 표시 '_' 로 시작되어야 하고, 문자와 숫자로만 구성되어야 한다. 공백은 허용되지 않는다.) 각 컨트롤은 유일한 이름을 가져야 한다는 규칙도 준수한다.


오브젝트 인스펙터에서 컨트롤의 Name 을 변경하면 폼의 클래스 정의 내 Name 뿐만 아니라 이 컨트롤을 참조하는 폼의 메소드 내 코드에서도 Name 이 자동 변경될 것이다.


폼 인스턴스는 (TComponent 의 자손) 그 위에 드롭되는 각 컨트롤을 소유한다: 즉, 런타임 시에 폼이 생성되면 각 컨트롤의Owner 프로퍼티가 폼 인스턴스로 설정될 것이다. 따라서 사용자는 아래 두 가지 방법 중 하나를 이용해 폼 상의 어떤 컨트롤로든 접근할 수 있다.


  1. 폼의 클래스 선언에서 필드에 접근:
    Label1.Caption := 'Some caption';
    
    현재 컨트롤로 접근하는 방법 중 가장 쉽다: 폼에서 이름 필드는 실제 컨트롤 인스턴스로의 참조를 포함한다.
  2. FindComponent ()를 이용해 이름을 검색:
    (FindComopnent('Label1') as TLabel).Caption := 'Some caption';
    
    FindComponent는 TComponent 결과를 리턴하기 때문에 적절한 클래스로 타입캐스트되어야 한다. 런타임 시 수동으로 생성된 컴포넌트들의 경우 이 방법이 유일한 검색 방법이다.


부모 컨트롤

앞 절에서 설명하였듯, 모든 컨트롤은 폼이 소유한다.


간단히 말해, 폼이 파괴되면 그 모든 컨트롤들도 파괴됨을 의미한다.


이것은 컨트롤의 비시각적 계층구조, 소유자가 소유한(owner-owned) 계층구조이다.


폼에는 부모-자식 계층구조라는 두 번째 계층구조가 있다. 이는 시각적 계층구조이다. 컨트롤은 폼뿐만 아니라 다른 컨트롤로 드롭되기도 한다. 예를 들자면, 버튼을 패널(panel)에 드롭할 수도 있다. 이를 실행 시 패널은 버튼의 부모가 될 것이다. 패널은 버튼을 소유하진 않지만 자식 컨트롤의 위치는 항상 (Top과 Left 프로퍼티로 결정됨) 그 부모 컨트롤을 기준으로 위치하기 때문에 버튼에 영향을 미친다.


그 결과, 부모 컨트롤이 이동하면 그 자식 컨트롤들도 모두 이동한다. 부모 컨트롤이 눈에 보이지 않으면 그 자식 컨트롤들도 모두 눈에 보이지 않는다. 부모는 자식의 크기를 제한할 수도 있다ㅡ자식 컨트롤의 크기가 부모 컨트롤의 크기를 초과할 경우 잘릴 것이다.


모든 컨트롤이 다른 컨트롤들에 대해 부모 역할을 할 수 있는 것은 아니다ㅡ특정 컨트롤만 자식 컨트롤을 수용한다. 예를 들어, 컨트롤은 버튼 위로 드롭할 수 없다ㅡ이미지 또는 라벨조차 드롭할 수 없을 것이다.


컨트롤이 폼 또는 폼의 컨트롤 중 하나로 드롭되는 순간 컨트롤의 Parent 프로퍼티가 설정된다. 컨트롤을 다른 부모로 드래그하는 것은 불가능하다: 컨트롤은 그 부모의 경계 내에서만 드래그할 수 있다. 컨트롤의 부모를 변경하기 위해서는 컨트롤을 자르고 (클립보드로) 새 부모 컨트롤에 붙여넣기 해야 한다. 폼 위에 드롭되는 각 컨트롤에 대해 부모 역할을 하는 폼의 경우도 마찬가지다. 컨트롤을 한 폼에서 다른 폼으로 이동 시에는 자르기와 붙여넣기를 이용해야 한다.


Cut과 Paste 객체는 까다로울 수 있는데 특히 일부 운영체제에서 더 그러하다. 그러한 운영체제로 작업할 경우 폼 정의를 수동으로 편집이 것이 가능하다. 이를 위해선 폼 디자이너의 View 소스 (.lfm) 컨텍스트 메뉴를 이용하면 되는데, 메뉴를 선택하면 폼의 정의가 텍스트로 표시될 것이다. 정의는 부모-자식 계층구조를 명확히 보여줄 것이며, 때로는 시각적 폼 디자이너에서 자르기와 붙여넣기를 하는 것보다 코드 행을 조작하는 편이 더 수월하다. 폼 정의의 예제를 살펴보자:

object Form1: TForm1
  Left = 250
  Height = 240
  Top = 250
  Width = 320
  Caption = 'Form1'
  ClientHeight = 240
  ClientWidth = 320
  LCLVersion = '0.9.29'
  object Button1: TButton
    Left = 28
    Height = 25
    Top = 27
    Width = 75
    Caption = 'Button1'
    TabOrder = 0
  end
  object Button2: TButton
    Left = 29
    Height = 25
    Top = 66
    Width = 75
    Caption = 'Button2'
    TabOrder = 1
  end
  object Panel1: TPanel
    Left = 29
    Height = 50 
    Top = 112   
    Width = 170 
    Caption = 'Panel1'
    TabOrder = 2
  end
end

Panel1 코드 섹션을 (회색영역) 드래그하여 Button1 컨트롤의 정의 앞에 표시할 경우, 버튼은 자동적으로 패널의 자식 컨트롤이 된다. 그 반대 절차도 가능하다.[1]


폼 정의를 이렇게 조작할 때는 버튼의 위치가 물론 이전처럼 남아 있지 않을 것이다: 부모가 변경되면 부모를 기준으로 측정되는 그 위치 또한 변경될 것이다. 위치는 텍스트 또는 아니면 폼 디자이너 모드로 전환하여 시각적으로 수정할 수 있다. 이를 위해선 폼 소스 (.lfm) 창을 닫고, View 메뉴에서 Toggle form/unit view 항목을 이용하거나 기본 단축키 [F12]를 이용해 Form Designer를 복구시켜야 한다. .lfm 을 수동으로 조작 시 위험 요소가 있으므로 .lfm을 직접 편집하기 전에는 폼의 모든 파일을 (.pas와 .lfm) 백업할 것을 권장한다.


초기 컨트롤 크기

단순 클릭으로 폼에 컨트롤을 드롭할 때 기본 크기가 주어진다ㅡ크기는 특정 컨트롤의 기본 값에 따라 좌우된다. 하지만 폼에 새 컨트롤을 드롭 시에는 클릭 앤 드래그(click-and-drag)가 가능하다. 마우스를 클릭한 채로 놓지 않고 드래그를 하면 컨트롤의 초기 윤곽을 원하는 크기로 설계할 수 있다.


하지만 일부 컨트롤은 크기 설정이 불가할 것이다. 예를 들어, 메인 메뉴의 크기는 메뉴 항목의 수에 따라 결정되며, 메인 메뉴는 제목 표시줄 아래 폼의 상단 테두리에 맞춰 알아서 정렬된다.


컨트롤의 다중 인스턴스 추가하기

폼에 컨트롤을 드롭하자마자 컨트롤 추가에 사용한 컴포넌트 팔레트 툴바 버튼이 선택 해제될 것이다. 따라서 동일한 컨트롤을 다시 추가하고 싶다면 툴바의 버튼을 다시 클릭하기만 하면 된다. 이러한 행위는 의도적이라 할 수 있는데, 폼에서 연속으로 클릭하면 대부분은 새로 드롭된 컨트롤의 크기 또는 위치가 조정될 것이기 때문이다.


컴포넌트 팔레트에서 매번 폼의 인스턴스를 선택할 필요 없이 다중 인스턴스를 추가하는 방법에는 두 가지가 있다:

  1. 폼을 클릭할 때 [Shift] 키를 누른 채로 유지한다. 그리고 나서 ([Shift]는 여전히 누른 채로) 클릭할 때마다 선택된 컨트롤의 새 인스턴스가 폼으로 추가될 것이다.2# 툴바 버튼을 클릭할 때 [Shift] 키를 누른다. 그리고 나면 폼을 클릭할 때마다 선택된 컨트롤의 새 인스턴스가 폼에 추가되며, [Shift] 키를 계속 누를 필요가 없어진다.


두 번째 방법을 사용한다면, 중복 컨트롤이 모두 위치되고 나서 툴바 버튼을 클릭하거나 컴포넌트 팔레트의 각 탭 좌측 끝에 있는 선택 화살표를 클릭함으로써 툴바 버튼의 선택을 끌 수 있다. 그렇게 되면 일반 작동으로 복구시킬 것이다.


다중 컨트롤의 프로퍼티 설정하기

컨트롤이 설정되면 사용자는 오브젝트 인스펙터에서 다양한 프로퍼티를 살펴보고 설정할 수 있다. 여러 컨트롤에 대해 동일한 프로퍼티를 설정해야 하는 경우 (예: 사용자의 모든 라벨의 텍스트를 오른쪽으로 정렬하기 위한 목적), 지루한 작업이 될 수 있다.


다행히 다수의 컨트롤을 한 번에 선택하는 것이 가능하다. [Shift] 키를 누른 채로 각 컨트롤을 차례로 클릭하여 프로퍼티를 설정한다. 오브젝트 인스펙터는 선택된 컨트롤이 공통적으로 공유하는 프로퍼티만 보여줄 것이며, 만일 이 공통 프로퍼티 중 어떤 것이라도 변경할 경우 선택된 컨트롤에 대해 새 값이 설정될 것이다.


일부 프로퍼티는 이러한 다중 설정 기법을 허용하지 않는다. 어떤 프로퍼티가 이를 허용하는지에 대한 일반적인 규칙은 없지만, 특수 프로퍼티 에디터가 있는 프로퍼티에서 허용하지 않는 경우가 있다.


컨트롤 삭제하기

컨트롤을 삭제하려면 선택 후 [Delete] 키를 누른다. 그리고 나면 컨트롤이 삭제되고 폼의 클래스 정의에서 해당 필드가 제거될 것이다. 컨트롤이 코드에서 사용되었다면 그 코드는 수동으로 조정되어야 할 것이다.


여러 컨트롤을 한 번에 삭제하려면 Shift 키를 누른 채 클릭하여 선택한 후 [Delete]를 누르면 선택된 컨트롤이 모두 삭제될 것이다. 삭제되는 동안 [Shift] 키를 누르고 있을 경우 컨트롤이 삭제 전 클립보드로 복사되어 어디든 붙여넣기 할 수 있다 (예: 다른 부모 컨트롤로).


TControl과 TWinControl

모든 시각적 컴포넌트는 TControl 의 자손이며, 일부는 TWinControl (자체가 TControl 의 자손)의 자손이기도 하다. TWinControl 은 중요한 추가 프로퍼티를 소개한다. 컨트롤을 잘 처리하려면 TControl 과 TWinControl 의 차이를 이해하고, 사용 중인 컨트롤이 두 클래스 중 어디서 파생되었는지 아는 것이 중요하다.


TControl

TControl 은 모든 시각적 컴포넌트에 대한 기반 클래스로서, 모든 시각적 요소들은ㅡ폼 클래스 자체도 포함ㅡTControl 에서 파생된다. 프로그래머는 TControl 클래스의 자손을 사용할 뿐, 절대로 그 클래스를 실제로 사용한 적은 없다. TControl 은 GUI 요소가 필요로 하는 기능을 모두 소개한다:

  • 위치와 크기, 그리고 이를 변경하는 데에 필요한 로직을 폼의 레이아웃 변경내용에 따라 제공한다.
  • 커서를 명시할 수 있다.
  • 마우스 이벤트의 처리를 제공한다.
  • 기본 드래그 앤 드롭 또는 도킹(docking) 기능을 제공한다.
  • 폼에 컨트롤을 그릴 수 있는 수단을 제공한다.


이 모든 기능은 TControl 에 도입되었다 (하지만 오브젝트 인스펙터로 굳이 노출할 필요는 없다). 리스트에서 키스트로크(keystroke)의 처리가 누락되었음을 주목한다. 직접 TControl 클래스 자손에 대한 그 외 제약으로는 다음이 포함된다:

  • 자식 컨트롤을 포함할 수 없다.
  • Windowed 컨트롤에 항상 위치해야 한다 (추후 상세 설명).
  • 포커스를 받을 수 없다. 키스트로크(keystroke) 처리의 결여와 연관되는데, 키스트로크는 최근 포서크된 컨트롤로 전송되기 때문이다. 직접 TConstrol 자손은 포커스를 받을 수 없으므로 키스트로크를 처리할 수 없다.
  • 그릴 수 있는 캔버스가 없다. 직접 TControl 자손은 부모 컨트롤에 스스로를 그려야 한다.


이 클래스에 대한 메인 프로퍼티는 아래 표와 같다. 대부분 이름만으로 기능을 알 수 있다:

프로퍼티 목적과 설명
Left 부모 컨트롤을 기준으로 한 0 기준(zero-based) 가로 위치, 최좌측 위치를 0으로 함.
Top 부모 컨트롤을 기준으로 한 0 기준(zero-based) 세로 위치, 최상단 위치를 0으로 함.
Width 컨트롤의 너비.
Height 컨트롤의 높이.
Cursor 커서가 컨트롤 위에서 이동할 때 커서의 모양.
Align 컨트롤이 부모 컨트롤에서 스스로 정렬하는 방법.
DragCursor 컨트롤을 이용해 무언가를 드래그할 때 사용할 커서.
DragKind 드래그 작동 유형: 드래그 앤 드롭 또는 도킹.
DragMode 드래그 앤 드롭을 누가 처리할 것인가?
ParentColor 컨트롤에 그 부모 컨트롤과 동일한 색상을 사용해야 하는가?
ParenFont 컨트롤에 그 부모 컨트롤과 동일한 폰트를 사용해야 하는가?
ParentHint ShowHint 프로퍼티는 부모 컨트롤과 동일해야 하는가?
Text 컨트롤에 표시되는 텍스트 (표시되는 텍스트가 있을 경우).
Constraints 컨트롤에 대한 크기 제약 (최소 및 최대 크기).
BorderSpacing 컨트롤과 주위 컨트롤의 테두리 사이의 공간.
PopupMenu 컨트롤을 오른쪽 마우스로 클릭했을 때 표시되는 팝업 메뉴로의 참조.
표 6.20: 중요한 TControl 프로퍼티


위의 표에 실린 프로퍼티는 모든 상황에서 이용할 수 있는 것은 아닌데, 가령 어떤 컨트롤들은 IDE의 오브젝트 인스펙터에 프로퍼티를 모두 노출하지 않는다. TControl 클래스는 그것이 제공하는 기능과 관련된 수많은 이벤트를 가진다:

이벤트 트리거 시기
OnContextPopup 팝업메뉴가 닫힐 때
OnClick 마우스를 1회 클릭할 때
OnDblClick 마우스를 2회 클릭할 때
OnTripleClick 마우스를 3회 클릭할 때
OnQuadClick 마우스를 4회 클릭할 때
OnDragDrop 드래그 앤 드롭 동작이 시작될 때
OnDragOver 객체를 컨트롤 위로 드래그할 때
OnEndDock 도킹(docking) 동작이 끝날 때
OnEndDrag 사용자가 객체를 컨트롤 위로 드롭할 때
OnMouseDown 마우스 버튼을 누를 때
OnMouseUp 마우스 버튼을 해제할 때
OnMouseMove 마우스가 컨트롤 위에서 움직일 때
OnMouseEnter 마우스가 컨트롤의 경계 사각형으로 들어갔을 때
OnMouseLeave 마우스가 컨트롤의 경계 사각형에서 나왔을 때
OnMouseWheel 사용자가 마우스 휠을 스크롤할 때
OnMouseWheelDown 사용자가 마우스 휠을 누를 때
OnMouseWheelUp 사용자가 마우스 휠을 해제할 때
OnStartDock 도킹 동작이 시작될 때
OnStartDrag 드래그 동작이 시작될 때
OnEditingDone 편집 동작이 끝날 때
표 6.21: 이용 가능한 TControl 이벤트


다시 말하지만, 모든 TControl 자손들이 오브젝트 인스펙터에서 모든 이벤트를 publish하는 것은 아니다.


TWinControl

TWinControl 은 TControl 자손으로, TControl 에 누락된 아래의 기능들을 제공한다.

  • 다른 컨트롤들을 TWinControl 에 드롭할 수 있다 (모든 TWinControl 자손들이 이를 허용하는 것은 아니지만).
  • TWinControl 은 포커스를 받을 수 있으므로, 폼에 활성화된 컨트롤이 될 수 있다.
  • TWinControl 은 포커스를 받을 수 있기 때문에 키스트로크(keystroke)를 처리할 수 있다. 포커스를 받는 한, 모든 키스트로크는 이 컨트롤로 전송된다.
  • TWinControl 은 고유의 캔버스(canvas)를 갖는다.


이러한 추가 기능 때문에 TWinControl 은 일반 TControl 보다 더 많은 시스템 리소스를 사용한다. 추가 프로퍼티와 이벤트를 통해 더 많은 가능성이 노출되는데, 이를 아래 표에 소개하겠다.

프로퍼티 용도
BorderStyle 테두리 스타일 설정 (주로 TForm 에서만 지원)
BorderWidth 테두리 너비 설정
ChildSizing 자식이 해당 컨트롤 위에서 레이아웃과 크기를 조정하는 방법을 결정
DockSite 다른 컨트롤들을 해당 컨트롤 위에 도킹할 것인지 결정
DoubleBuffered 컨트롤의 디스플레이가 이중 버퍼(double-buffer) 되었는지 인식
TabOrder 컨트롤의 탭 순서 설정
TabStop [Tab] 키가 해당 컨트롤을 지나칠 것인지, 멈출 것인지 결정
UseDockManager 해당 컨트롤에 도킹 관리자를 사용할 것인지, 도킹을 수동으로 처리할 것인지 결정
표 6.22: TControl에서 이용할 수 없는 TWinControl 프로퍼티


이벤트 용도
OnAlignPosition 컨트롤의 위치조정
OnDockDrop 어떤 컨트롤이 해당 컨트롤에 도킹되었을 때
OnDockOver 사용자가 해당 컨트롤에 다른 컨트롤을 드래그하여 도킹시킬 때
OnEnter 컨트롤에 포커스를 줄 때
OnExit 컨트롤이 포커스를 잃었을 때
OnKeyDown 사용자가 키보드의 아무 키나 누를 때
OnKeyPress 일반 (문자와 숫자) 키를 누를 때
OnKeyUp 사용자가 키보드의 키를 눌렀다가 해제할 때
OnUnDock 컨트롤이 도킹해제(undocked)되었을 때
OnUTF8KeyPress 일반 키(regular key)를 누를 때, 그리고 키가 UTF-8로 변환될 때
표 6.23: TControl에서 이용할 수 없는 TWinControl 이벤트


TwinControl 에는 자손이 몇 개 있는데 (TCustomControl 과 TScrollingWinControl) 대부분은 LCL 내부적으로, 최종 사용자 기능을 포함하지 않는다.


LCL 의 대부분 컨트롤은 TWinControl 에서 계승된다. TControl 에서 직접 파생된 컨트롤은 매우 소수에 불과하다 (TLabel 은 중요한 예외).


이용 가능한 컨트롤의 개요

어떤 컨트롤을 이용할 것인가? 하는 질문은 두 가지 요인에 따라 좌우된다: 애플리케이션이 어떻게 생겼는지와 사용자가 무엇을 할 수 있는지-애플리케이션이 사용자로부터 기대하는 액션-가 된다. 이를 고려해 사용자는 컨트롤의 특정 카테고리를 선택할 수 있다 (컨트롤의 클래스 계층구조의 고려 또는 어떤 컴포넌트 탭을 살펴보아야 하는지와는 별도로).


아래 절부터는 사용자가 이용할 수 있는 LCL 컨트롤의 개요를 제공하되, 클래스 계층구조나 컴포넌트 팔레트에 차지하는 탭을 중심으로 하기보다는 컨트롤이 실행하는 작업이나 함수를 기준으로 그룹화하여 제공된다. 컴포넌트 팔레트의 레이아웃은 설계 고려사항보다는 역사적(경험적) 요인들을 따라 결정되었지만 이것이 이용 가능한 컨트롤의 배열과 관련해 항상 최선의 방식인 것은 아니다. 아래 소개한 리스트는 완전한 리스트는 아니지만-완전한 컨트롤 참조는 다음 장에서 다룰 것이다-특정 작업을 해내기 위해 어떤 컨트롤을 이용할 수 있는지 어느 정도 지표를 제공할 것이다.


사용자 작업을 시작하기 위한 컨트롤

사용자가 특정 작업을 성취하고자 할 때는 (볼드체 텍스트 설정, 파일 열기, 디자이너에서 폼에 컨트롤 추가하기 등) 해당 액션을 시작하기 위해 어딘가를 클릭할 수 있어야 한다.


보통은 클릭만으로도 액션을 완료하기에 충분하지만 대부분 그 클릭은 작업을 완료하는데 필요로 하는 일련의 액션들 중 첫 번째에 불과하다. 작업을 시작하는 컨트롤은 다양하다. 아래 표에 소개하겠다:

컨트롤 탭(tab) 설명
TButton Standard 캡션을 표시하는 단순 버튼.
TBitBtn Additional 캡션과 이미지를 표시하는 단순 버튼.
TSpeedButton Additional 캡션과 이미지가 있지만 포커스를 받지 않는 버튼.
TToolBar
&TToolButton
Common controls 이미지와 캡션이 각각 표시되는 버튼으로 구성된 열.
TMenu Standard 완전한 메뉴 구조, 폼의 상단에 표시.
TPopupMenu Standard 오른쪽 마우스 클릭으로 호출되는 메뉴 구조.
표 6.24: 작업을 시작하는 컨트롤


간단한 항목을 사용자에게 표시하기

당시 수동적인 사용자에게 무언가를 표시하기 위한 용도로 설계된 컨트롤이 다수가 있다ㅡ사용자가 동작을 실행하도록 돕기 위해 설계된 컨트롤은 아니다. 하지만 사용자는 마우스로 해당 컨트롤을 클릭할 수 있으며, 필요 시 추가 효과나 액션의 코드화에 사용되기도 한다.

컨트롤 탭(tab) 설명
TLabel Standard 어떠한 마크업(mark-up)도 없는 간단한 텍스트 구문.
TImage Additional 간단한 이미지 컨트롤.
TShape Additional 여러 기하학적 형태의 디스플레이를 제공한다.
TPaintBox Additional 좀 더 복잡한 모양의 그리기를 허용한다.
TBarChart Misc 히스토그램을 표시 (막대 그래프).
TCalendar Misc 달력에 일자 표시.
TProgressBar Common Controls 시간이 소요되는 동작의 진행사항을 표시한다.
TPopupNotifier Common Controls 사용자가 컨트롤 위에서 마우스를 움직이면 추가 정보를 표시한다.
Tbevel Standard 시각적으로 요소들을 그룹화한다.
표 6.25: 상태와 간단한 모양을 표시하는 컨트롤


간단한 사용자 선택권 제공하기

가끔씩 당신은 Yes 혹은 No(Do 또는 Don't)로 답을 해야 하는 간단한 선택권을 애플리케이션 사용자에게 제시할 필요가 있다. 아니면 사전에 정의된 옵션 리스트를 제공하여 그 중 하나 또는 그 이상을 선택하도록 요청하는 경우도 있다. 옵션 리스트가 긴 경우 리스트를 항상 표시할 것인지를 결정하고, 완전한 리스트를 보기 위해 사용자가 먼저 클릭해야 하는지를 결정해야 할 것이다.

컨트롤 탭(tab) 설명
TCheckBox Standard 간단한 예/아니오 선택.
TRadioButton Standard 간단한 예/아니오 선택, 주로 하나의 옵션만 선택 가능한 그룹에 사용.
TRadioGroup Standard 하나만 선택할 수 있는 라디오버튼 그룹.
TCheckGroup Standard 다수의 항목을 선택할 수 있는 확인상자 그룹.
TComboBox Standard 사용자는 선택 집합 중 하나를 선택할 수 있다. 각 선택은 문자열이다. 선택 리스트는 드롭다운 리스트에 표시된다. 선택된 항목만 눈에 보인다.
TListBox Standard 사용자는 선택 집합 중 하나 또는 그 이상을 선택할 수 있다. 각 선택은 문자열이다. 선택 리스트는 항상 눈에 보인다.
표 6.26: 선택권을 제공하는 컨트롤


간단한 사용자 입력 얻기

때때로 애플리케이션 사용자가 간단한 예/아니오 또는 사전 정의된 값 리스트의 선택 이상의 정보를 입력해야 하는 경우가 있다. 예를 들어, 애플리케이션 사용자가 자신의 이름을 입력하거나 데이터가 저장되는 곳의 파일명을 제공할 때가 있다.

컨트롤 탭(tab) 설명
TEdit Standard 단일 행 텍스트 엔트리.
TMemo Standard 다중 행 텍스트 엔트리.
TSpinEdit Misc 값을 증가 및 감소시키기 위한 업/다운 버튼이 있는 숫자 값 엔트리.
TButtonEdit Misc 추가 액션의 실행 버튼이 있는 단일 행 텍스트 엔트리.
TOpenFileDialog Dialogs 파일을 읽기 위한 파일명 선택하기.
TSaveFileDialog Dialogs 파일을 쓰기 위한 파일명 선택하기.
TColorDiaglog Dialogs 색상 선택하기.
TPrinterDialog Dialogs 프린트 선택하기. TFontDialogDialogsSelecting는 폰트.
표 6.27: 입력 컨트롤


복잡한 컨트롤

여기까지 소개한 컨트롤들은 상대적으로 간단한 수준에 속하며 정보의 일부분만 요청하거나 표시한다. 한 가지 종류 이상의 정보를 표시하거나, 정보를 그룹화 및 배열하기 위해 화면을 구분된 부분으로 나누는, 좀 더 복잡한 컨트롤도 있다.

컨트롤 탭(tab) 설명
TPageControl Common Controls 디스플레이 영역을 다중 시트(sheet)로 나눈다.
TTreeView Common Controls 트리와 같은 구조를 표시한다.
TListView Common Controls 리스트 내 각 항목이 하나 이상의 자료로 구성된 리스트 구조를 표시한다.
TStringGrid Additional 문자열 항목으로 채워진 격자를 표시한다.
표 6.28: 복잡한 컨트롤


레이아웃과 설계

레이아웃(layout)은 GUI 프로그램을 설계 시 매우 중요한 문제다. 애플리케이션의 레이아웃을 설계할 때는 고려해야 할 사항들이 많다:

  • 화면 해상도와 폰트 크기가 다양해질 수 있다.
  • 테마의 폰트 및 버튼 크기가 변경될 수 있다.
  • 국제화 애플리케이션에서 표시된 텍스트는 크기마다 다양할 수 있으므로 일부 컨트롤은 전환 또는 크기를 조정하여 해석된 텍스트의 새로운 크기를 수용할 수 있어야 한다.


이러한 고려사항들은 폼 상에서 컨트롤의 크기 조정이나 이동을 요할지도 모른다. 분명한 것은 크기 조정이나 위치 조정을 수동으로 실행할 수 있다는 점이다. 수고스러운 작업이 될 수도 있지만 다행히 특정 프로퍼티들이 올바르게 설정되어 있다는 점을 감안하면 LCL 이 잘 해결해준다고 할 수 있겠다.


일부 위젯 셋은 객체 레이아웃을 이용해 크기 및 위치 조정을 다룬다: GTK와 Qt 위젯 셋이 이에 해당한다. 마이크로소프트사 윈도우는 레이아웃을 제공하지 않고 고정된 위치와 크기를 사용한다. 델파이의 VCL을 모델로 한 Lazarus Component Library(LCL) 또한 고정된 위치 및 크기 접근법을 사용하는데, 기본적으로 컨트롤이 위치나 크기를 변경하지 않음을 의미한다. 하지만 이러한 행위는 라자루스 IDE의 오브젝트 인스펙터에서 설정 가능한 published 프로퍼티 몇 가지를 이용해 수정할 수 있다. 위치와 크기를 결정하는 주요 published 프로퍼티는 다음과 같다.

프로퍼티 설명
Top, Left 이 프로퍼티들은 컨트롤의 위치를 결정한다. 항상 부모 컨트롤의 클라이언트 영역 상단 좌측 모서리를 기준으로 제공되며, 픽셀로 측정한다.
Width, Height 이 프로퍼티들은 컨트롤의 크기를 픽셀로 결정한다. 양수여야 하며, 부모 컨트롤이 허용하는 경계를 넘어설 수 있다. 이러한 크기 초과 값이 부모 컨트롤을 기준으로 설정된다면 어떤 일이 발생할까.
Align 이 프로퍼티는 부모 컨트롤의 테두리 중 하나를 따라 컨트롤을 배열하는 데에 사용된다. 기본 값 alNone은 이 기능을 비활성화시킨다.
Anchors 이 프로퍼티는 컨트롤의 테두리 일부를 부모 컨트롤 테두리에 따라 배열하거나 (기본 값), 폼 상에 있는 다른 컨트롤의 테두리를 따라 배열한다. 부모 컨트롤의 크기가 조정되거나 연결된 컨트롤이 이동 및 크기가 조정되면, 현재 컨트롤의 앵커(anchor) 또한 이동이나 크기가 조정될 것이다.
AutoSize 이 프로퍼티는 컨트롤에게 내용이 그 안에 들어맞도록 스스로 크기를 조정하라고 알린다.
OnResize 이 이벤트는 컨트롤의 크기가 조정될 때 액션을 실행할 경우 사용된다. 다른 프로퍼티들이 크기를 조정할 만큼 유연하지 못한 경우 이를 이용해 수동으로 크기 조정을 한다. 예를 들어, 격자의 경우 이 이벤트를 이용해 열을 다시 설계하여 항상 격자 전체 너비를 차지하도록 한다.
BorderSpacing 컨트롤과 그것이 고정(anchored)된 다른 컨트롤들을 구분시킬 공간의 양이다 (픽셀).
Constraints 컨트롤이 크기 조정 시 가질 수 있는 최소 및 최대 크기를 결정한다.
표 6.29: TControl의 가장 중요한 레이아웃 값


코드에 다음 public 프로퍼티들도 사용할 수 있다:


BoundsRect 는 컨트롤의 윤곽을 정의하는 TRect 레코드이다. 이를 이용 시, Top, Left, Width, Height 를 네 번의 호출 대신 한 번의 호출만으로 설정할 수 있다. ClientRect는 자식 컨트롤에 이용할 수 있는 영역이다. 컨트롤의 테두리는 포함하지 않는다. 메인 메뉴가 있는 폼의 경우, ClientRect 는 메뉴 아래부터 시작한다. AnchorSizes. 이 프로퍼티들은 현재 컨트롤이 고정된 컨트롤들을 결정한다.


수동 크기 조정

수동으로 크기를 조정하기란 지루한 작업이긴 하지만 이를 제외한 별다른 방도가 없는 경우가 있다. 수동 크기 조정은 메모를 포함하는 간단한 폼을 이용해 쉽게 설명할 수 있는데, 여기서 폼의 테두리로부터 동일한 거리에 항상 테두리를 유지하고자 한다 (예: 폼과 함께 증대 및 축소). Close 버튼은 폼의 하단 우측 모서리에 항상 유지되어야 하며, 설계 시 크기를 유지해야 한다.


그림 6.21를 참고한다:

그림 6.21: 수동 크기 조정, 동일한 창에 크기가 다른 4개의 인스턴스


이러한 레이아웃을 수동으로 얻으려면 폼의 아래와 오른쪽 테두리를 기준으로 버튼 위치에 대한 오프셋(offset)과, 폼의 하단 우측 테두리를 기준으로 메모의 오른쪽과 아래쪽에 대한 오프셋을 결정할 필요가 있다.


이러한 거리는 폼의 OnCreate 이벤트에서 결정된다.

procedure TMainForm.FormCreate(Sender: TObject);
begin
  MHDiff := ClientHeight-MSize.Height;
  MWDiff := ClientWidth-MSize.Width;
  BTOff := ClientHeight-BCLose.Top;
  BLOff := ClientWidth-BClose.Left;
end;


버튼은 BClose라고 부르고, 메모는 MSize라고 부른다. ClientWidth 와 ClientHeight 는 폼의 너비와 높이로, 폼 내에 있는 컨트롤에 이용할 수 있다. 이제 4개의 변수, MHDiff, MWDiff, BTOff, BLOff 를 폼의 OnResize 이벤트에 사용할 수 있다:

procedure TMainForm.FormResize(Sender: Tobject);
begin
  Msize.Height := ClientHeight-MHDiff;
  Msize.Width := ClientWidth-MWDiff;
  Bclose.Top := ClientHeight-BTOff;
  Bclose.Left := ClientWidth-BLoff;
end;


위의 코드에서 볼 수 있듯이 그다지 어려운 일이 아니다. 하지만 폼 상에 컨트롤이 많은 경우 코드가 훨씬 더 복잡해서 유지하기가 힘들어질 수 있음은 분명하다. 다행히도 이 작업을 수행하는 다른 방법들이 있다.


정렬을 이용한 크기 조정

Align 프로퍼티는 정렬뿐만 아니라 크기 조정에도 유용한 프로퍼티다. 범위가 제한되어 있긴 하지만 많은 사례에서 사용할 수 있으며, 이 프로퍼티만 이용하여 꽤 복잡한 레이아웃을 정렬하는 경우도 종종 있을 것이다. Align 프로퍼티는 아래 값을 가질 수 있다:

상수 기능
alNone 어떤 정렬도 이루어지지 않는다. 컨트롤은 설계된 위치를 유지한다.
alTop 컨트롤이 항상 부모 컨트롤의 상단 끝에 붙어(glued)있다. 높이는 일관되기 유지되지만 너비는 부모 컨트롤의 너비를 따름을 의미한다.
alBottom 컨트롤이 항상 부모 컨트롤의 하단 끝에 붙어(glued)있다. 높이는 일관되기 유지되지만 너비는 부모 컨트롤의 너비 따름을 의미한다.
alLeft 컨트롤이 항상 부모 컨트롤의 좌측 끝에 붙어(glued)있다. 너비 프로퍼티는일관되기 유지되지만 높이는 부모 컨트롤의 높이를 따름을 의미한다.
alRight 컨트롤이 항상 부모 컨트롤의 우측 끝에 붙어(glued)있다. 너비 프로퍼티는일관되기 유지되지만 높이는 부모 컨트롤의 높이를 따름을 의미한다.
alClient 컨트롤이 부모 컨트롤에 이용 가능한 공간을 채우도록 크기가 조정된다.
alCustom [현재 사용되지 않음]
표 6.30: 이용 가능한 배열 값


Align 프로퍼티를 alTop 으로 동시에 설정하는 컨트롤이 몇 개가 있음을 주목한다. 이러한 일이 발생할 경우, 첫 번째 컨트롤은 부모 컨트롤의 상단 끝에 붙어 있고, 두 번째는 첫 번째 컨트롤의 하단에 붙어 있다: 둘은 부모 컨트롤의 상단 가장자리에 겹친다.

그림 6.22: AlignSize 프로젝트


위의 그림은 폼이 커지거나 줄어들면 함께 커지거나 줄어드는 메모를 표시하는데, 버튼은 하단 우측 모서리에 계속 위치한다. 이러한 폼은 디자이너에서 아래와 같은 방법으로 생성할 수 있다:

  • PButtons 패널을 추가한다. Align 을 alBottom 으로 설정한다. BevelInner 와 bevelOuter 를 bvNone 으로 설정한다.
  • 두 번째 패널, PClose를 PButtons에 드롭하고 Align 프로퍼티를 alRight 로 설정, BevelInner 와 Bevelouter 는 bvNone 으로 설정한다.
  • PClose 패널에 BClose 버튼을 드롭한다.
  • Msize 메모를 폼에 추가하고, Align 프로퍼티를 alClient 로, BorderSpacing.Around 는 8로 설정한다.
  • PClose와 BClose의 크기를 조정하여 Close 버튼이 메모와 함께 오른쪽으로 정렬되도록 한다.


이제 폼의 크기를 조정하면 앞의 레이아웃팅(layouting)이 수동으로 코드화된 폼과 동일하게 행동한다. 이 방법을 이용한 레이아웃팅은 빠르고 쉽지만 몇 가지 단점이 있다: TPanel 이 windowed 컨트롤이라 추가 리소스를 취한다; 그리고 포커스를 받을 수 있기 때문에 다른 컨트롤들의 탭 순서를 방해하므로 (TabStop 을 False로 설정하지 않는 한) 사용을 자제해야 한다. 다행히 LCL 은 폼의 크기를 올바르게 조정할 수 있는 다른 방법들을 제공한다.


앵커를 이용한 크기 조정

앵커는 자동 컨트롤 크기 조정을 위한 강력한 메커니즘이다. 라자루스는 델파이의 VCL에 있는 앵커라는 개념에 추가 앵커 기능을 더한다. LCL 앵커는 컨트롤을 그 부모뿐 아니라 이웃하는 컨트롤로 고정시킬 수 있다. 고정(anchoring)은 Anchors 프로퍼티를 통해 이루어지는데, 아래 값으로 설정이 가능하다:

akTop 컨트롤의 상단면은 부모 컨트롤의 상단면과 동일한 거리로 유지된다.
akLeft 컨트롤의 좌측면은 부모 컨트롤의 좌측면과 동일한 거리로 유지된다.
akRight 컨트롤의 우측면은 부모 컨트롤의 우측면과 동일한 거리로 유지된다.
akBottom 컨트롤의 하단면은 부모 컨트롤의 하단면과 동일한 거리로 유지된다. 필요 시 컨트롤을 세로로 크기 조정할 수 있다.


기본 Anchors 설정은 [akTop, akLeft]로, 컨트롤이 현재 위치와 크기를 유지함을 의미한다. TEdit의 Anchors 프로퍼티를 [akTop, akLeft, akRight]로 설정하면 그 위치는 유지하되 확대와 축소가 가능하여 폼의 우측면으로부터 동일한 거리를 항상 유지할 것이다.


메모와 버튼에 관한 앞의 예제를 컨트롤의 Anchors 프로퍼티만 이용해 재작업할 수 있다. 메모의 앵커를 [akTop, akLeft, akRight, akBottom]으로 설정하기만 하면 메모의 모든 가장자리들이 폼의 해당 가장자리로부터 동일한 위치에 유지될 수 있다. 버튼의 Anchors 프로퍼티는 [akRight, akBottom]에 설정되어 있으므로 폼의 하단 우측 모서리를 기준으로 동일한 위치에 계속 유지되도록 보장한다.

그림 6.23: 메모를 올바르게 정렬된 채 유지하는 Anchor 설정


라자루스는 앵커를 이용할 수 있는 추가 가능성을 제공한다. 컨트롤 간 구분 거리를 명시하여 컨트롤을 서로 고정하는 것도 가능하다. 이는 좀 더 복잡한 레이아웃에 유용하다. 옆에 라벨이 있는 편집 컨트롤(edit control)을 상상해보자. 캡션이 변경되면 라벨의 크기가 변경될 수 있다 (예: 다른 언어로 해석 시). 이 때 편집 컨트롤의 위치를 조정하여 그 라벨로부터 동일한 거리를 유지해야 한다.


이는 라벨과 두 개의 편집 컨트롤이 포함된 'anchors' 프로젝트에서 설명하고 있는데, 그림 6.24에서 볼 수 있듯이 두 컨트롤 중 하나는 라벨과 동일한 상단 위치에 있다.

그림 6.24: 하나의 컨트롤을 다른 컨트롤로 고정하기


두 번째 편집 컨트롤의 텍스트는 OnChange 이베트 내에서 라벨의 Caption으로 복사된다. 첫 번째 편집 컨트롤(edit)의 Anchors 프로퍼티는 그림 6.25와 같이 Anchors 프로퍼티의 프로퍼티 에디터를 이용해 편집이 가능하다.

그림 6.25: Anchors 에디터


스크린 샷에서 볼 수 있듯, 상단, 좌측, 우측 앵커가 설정되었다. 상단과 우측 앵커는 형제(sibling) 집합이 없다. 즉, 부모 컨트롤에 고정된다는 의미인데, 이번 경우 폼이 되겠다. 좌측 앵커에는 LEAnchored 라벨로 설정된 형제(sibling) 컨트롤이 있다. 중간 speedbutton를 누르면 편집 컨트롤이 라벨의 우측면에 고정됨을 의미한다. 좌측면이나 중앙에 고정할 수도 있다.


대화창을 닫고 프로젝트를 실행할 때는 라벨의 캡션을 변경할 수 있는 두 번째 편집 컨트롤에 입력함으로써 앵커 설정의 효과를 볼 수 있다. 라벨의 Autosize 프로퍼티는 True로 설정되어 있기 때문에 그 라벨은 크기만 변경될 것이며, 첫 편집 컨트롤은 라벨과 동일한 거리를 유지하도록 크기가 조정될 것이다. 편집 컨트롤의 우측 가장자리는 폼의 테두리와 일관된 거리로 유지된다.


이러한 메커니즘이 몇몇 매우 복잡한 레이아웃팅(layouting)의 설치를 허용한다는 사실이 명백해진다. 거의 모든 상황에서 이러한 메커니즘을 이용해 해결할 수 있겠다. Anchors 프로퍼티 외에 대화창 또한 BorderSpacing 프로퍼티를 설정한다. 이 프로퍼티는 다른 컨트롤들과의 구분 거리, 즉 유지해야 하는 전체적 거리를 명시한다. 또한 AnchorSides 프로퍼티가 설정된다. 이는 unpublished 프로퍼티로서, 오브젝트 인스펙터에선 조작이 불가능하다. AnchorSides 는 인접한 컨트롤들과 (그림 6.25의 sibling) 앵커 '면들(sides)' (세 개의 속도버튼으로 표시)로의 연결을 유지한다.


ChildSizing을 이용한 레이아웃팅

LCL 은 주로 고정된 위치의 레이아웃팅 전략을 사용하지만 컨트롤을 격자와 같은 방식으로 배열하여 컨트롤의 크기가 조정되면 함께 크기가 조정되는 격자형 셀 안에 컨트롤을 유지시키는 방법을 제공한다. 이때는 몇몇 컨트롤이 가진 ChildSizing 프로퍼티를 이용할 수 있다. 이 프로퍼티는 TControlChildSizing 타입이자 TPersistent 자손으로, 두 가지 메인 프로퍼티를 가진다. 첫 번째는 Layout 으로, 컨트롤을 격자 내에 어떻게 위치시킬 것인지를 결정한다. 아래 값을 취할 수 있다:

값(value) 효과
cclNone 어떠한 재배열도 이루어지지 않음을 의미한다: 컨트롤은 설계된 위치의 좌측에 있다. 이것은 기본 값으로, 크기가 전혀 조정되지 않는다.
cclLeftToRightThenTopToBottom 컨트롤이 좌측부터 시작해 가로 선으로 그룹화되어, 행을 채운 후 선이 꽉 차면 다음 행에 계속됨을 의미한다.
cclTopToBottomThenLeftToRight 행이 상단에서 하단으로 진행된다는 점을 제외하면 위의 값과 동일하다.
표 6.31: 이용 가능한 레이아웃 값


TControlChildSizing 의 두 번째 프로퍼티는 ControlsPerLine 으로, 행별로 그룹화되는 컨트롤의 수를 의미한다.


컨트롤들은 사이에 어느 정도 공간을 두고 그룹화된다. 컨트롤들 간 공간의 양은 아래 네 개의 정수 프로퍼티로 조절된다:

값(value) 효과
LeftRightSpacing 행의 첫 번째 컨트롤과 좌측 테두리 간 간격.
TopBottomSpacing 첫 번째 컨트롤과 상단 테두리 간 간격.
HorizontalSpacing 컨트롤들 간 가로 간격.
VerticalSpacing 컨트롤들 간 세로 간격. 컨트롤의 크기가 조정되면 그것이 포함하는 컨트롤을 위치 또는 크기를 조정하라고 지시받을 수 있다. 이러한 행위는 아래 네 가지 프로퍼티에 의해 조정된다:
EnlargeHorizontal 컨트롤이 확대되면 가로 방향으로 해야 할 일.
EnlargeVertical 컨트롤이 확대되면 세로 방향으로 해야 할 일.
ShrinkHorizontal 컨트롤이 내용을 포함하기에 너무 작을 때 가로 방향으로 해야 할 일.
ShrinkVertical 컨트롤이 내용을 포함하기에 너무 작을 때 세로 방향으로 해야 할 일.
표 6.32: 이용 가능한 ChildSizing 값


이 네 가지 프로퍼티들은 TChildControlResizeStyle 이라는 동일한 타입의 것이다. 이러한 열거형은 아래 네 가지 값을 가진다:

값(value) 효과
crsAncorAligning 컨트롤의 위치와 크기를 유지할 뿐이다 (델파이와 같이).
crsScaleChilds 자식 컨트롤의 크기를 변경하고(scale), 자식 간 공간을 일관되게 유지한다.
crsHomogenousChildGrowth 각 자식 컨트롤을 동일하게 확대하여 각 자식의 크기에 동일한 픽셀 수가 추가되도록 한다.
crsHomogenousSpaceGrowth 자식 컨트롤 간 공간을 동일하게 확대하여 각 공간에 동일한 픽셀 수가 추가되도록 한다.
표 6.33: 이용 가능한 TChildControlResizeStyle 값


이러한 메커니즘은 기반이 되는 위젯 셋 프로퍼티를 사용하지 않음을 명심한다. 크기 조정과 위치 조정은 순전히 LCL 에 의해 이루어지므로, 모든 지원 플랫폼에서 이용할 수 있으며, 기본 위젯 셋이 이를 지원하지 않는 경우에도 이용할 수 있다.


스플리터를 이용한 사용자 제어 레이아웃

앞에 여러 절에 걸쳐 설명한 레이아웃 컨트롤은 컨트롤이 위치한 폼의 크기를 변경하는 사용자만 위주로 한다. 만일 애플리케이션 사용자가 컨트롤의 상대적 크기를 변경하길 원한다면 어떨까?


예를 들어, 이메일 읽기 애플리케이션에서 이메일의 리스트와 각 이메일의 세부내용은 같은 창에 표시되곤 한다. 사용자는 두 컨트롤 간 구분자를 드래그하여ㅡ이 구분자는 스플리터(splitter)라고 알려져 있다ㅡ메일의 세부내용과 리스트의 크기를 조정할 수 있다.


라자루스는 두 가지 유형의 스플리터를 제공한다:

  • TSplitter: 해당 스플리터는 폼 위의 컨트롤 중 하나만 '따라다녀' 이 컨트롤의 크기를 조정하는 데에 사용될 수 있다. 나머지 컨트롤들은 스스로 크기를 조정할 수 있어야 한다 (보통 Align 프로퍼티를 alClient로 설정함으로써).
  • TPairSplitter: 해당 컨트롤은 두 개의 하위 패널로 나뉘는 패널 역할을 하며, 간단한 TSplitter 컴포넌트에 연결된다. 당신은 컨트롤을 두 개의 패널로 드롭할 수 있으며, TPairSplitter의 중앙 ‘손잡이(grip)’를 이동하면 두 개의 하위 패널과 그들이 포함한 컨트롤들의 크기가 조정될 것이다.


TSPlitter 컴포넌트는 항상 두 개의 인접한 컨트롤과 함께 사용되어야 하는데, 그 중 하나는 Align 프로퍼티를 alClient 로 설정해야 한다. 나머지 컨트롤은 Align 프로퍼티가 alTop, alBottom, alLeft, alRight 중 하나로 설정된다.


TSplitter 컨트롤은 고유의 Align 프로퍼티를 두 번째 컨트롤의 값과 동일하게 설정해야 한다. 그래야만 해당 컨트롤의 테두리 중 하나를 따라다닐 수 있고, 사용자가 스플리터를 드래그하면 컨트롤의 크기가 조정될 것이다. 첫 번째 컨트롤은 Align 프로퍼티를 alClient 로 설정되어 있으므로 크기를 조정하고 나머지 공백을 차지할 것이다.


이는 이메일을 읽는 애플리케이션의 창에서 설명할 수 있다:


그러한 창에는 리스트상자, 좌측 정렬, 좌측 정렬된 splitter (SPDemo)가 포함되어 있다. 창의 나머지는 Align 프로퍼티가 alClient 로 설정된 메모가 차지한다. 리스트상자는 이메일 목록을 나타내며, 메모는 최근 선택된 이메일의 내용을 표시한다.


splitter 컨트롤에는 그 모양과 행위를 제어하는 몇 가지 프로퍼티가 있는데, 아래 표에 소개하겠다:

프로퍼티 설명
AutoSnap True로 설정 시, 컨트롤 크기가 Minsize 보다 작아지면 스플리터는 컨트롤을 숨길 것이다. True가 아닐 시, MinSize는 컨트롤의 최소 허용 크기가 되어 스플리터는 컨트롤을 MinSize보다 작게 만들지 않을 것이다.
Beveled 스플리터 모양을 경사지거나(beveled) 수평(flat)으로 제어한다.
MinSize 크기를 조정하는 컨트롤의 최소 크기: 스플리터가 컨트롤을 이 크기보다 작게 만들지 않을 것이다.

AutoSnap이 True일 경우, MinSize는 스플리터가 축소되는 컨트롤을 완전히 숨기는 크기를 표시한다.

ResizeStye 스플리터 컨트롤을 사용자가 드래그하는 동안 어떻게 그리는지 제어한다.
표 6.34: TSplitter 프로퍼티


이메일을 읽는 애플리케이션에는 사용자가 메일의 목록 위치와 (리스트 상자 내) 최근 선택한 메일의 내용이 포함된 메모의 위치를 변경할 수 있는 메뉴를 포함시킬 수 있다. 메뉴 항목이 Tag 프로퍼티 내 TAlign 의 값을 가진다고 가정할 때, 아래 OnClick 핸들러는 메모와 리스트상자의 위치를 변경할 수 있을 것이다:

procedure TMainForm.MIAlignClick(Sender: TObject);
  Const H = [alLeft,alRight];
  Var OA,Al : TAlign;
begin
  al := TAlign((Sender as TMenuItem).Tag);
  OA := SPDemo.Align;
  LBMails.Align := al;
  SPDemo.Align := al;
  If (OA in H)<>(al in H) then
   if al in H then
     LBMails.Width := SPDemo.MinSize + 1
   else
     LBMails.Height := SPDemo.MinSize + 1;
end;


Align 프로퍼티의 새 값은 메뉴 항목의 Tag 프로퍼티에 저장된다. 첫 번째 작업은 이 값과 함께 최근 값을 복원시키는 것이다. 그러면 리스트상자 및 스플리터의 Align 프로퍼티가 새 값으로 설정된다.


이제 스플리터의 방향이 가령 가로에서 세로로 전환된다면, 컨트롤의 크기 또한 변경되어야 한다. 만일 alLeft 로 정렬되어 있다면 alTop 으로 전환 시 리스트상자가 공백을 모두 차지할 것이다: 그 이유는 리스트상자의 높이가 폼의 전체 높이와 동일하기 때문이다: 스플리터의 방향을 변경하면 높이를 변경할 뿐만 아니라 너비를 폼의 너비로 설정할 것이다: 그 결과 리스트상자는 전체 폼을 차지할 것이다! 따라서 높이는 어느 정도 적당한 값으로 설정되어야 한다. 이는 두 번째 루틴에서 이루어진다: 높이가 최소 크기 +1로 설정된다 (문제를 확인하려면 나머지 절반 루틴에서 그것을 코멘트 아웃(comment out)함으로써 코드를 비활성화시키면 된다).


이메일을 읽는 애플리케이션은 TPairSplitter 컴포넌트로 구성할 수 있는데 pairsplitter component 의 패널 하나마다 listbox 와 memo 컴포넌트를 하나씩 드롭하고 Align 프로퍼티를 alClient 로 설정하면 된다.


창의 방향을 변경하기 위한 메뉴 항목은 아래와 같이 훨씬 더 단순해질 것이다:

procedure TMainForm.SplitterTypeClick(Sender: TObject);
begin
  PSDemo.SplitterType := TPairSplitterType((Sender as TMenuItem).Tag);
end;


SplitterType 프로퍼티의 값을 명시하기 위해 Tag 프로퍼티가 다시 사용된다. 이번 예제에서 방향은 추가 코드 없이 변경할 수 있다.


데모 프로그램에는 TPairSplitter 컨트롤로 생성된 복잡한 레이아웃도 포함되어 있다. 이러한 레이아웃은 간단한 TSplitter 컨트롤을 이용할 때 필요로 하는 추가 TPanel 컨트롤이 없이도 구축할 수 있다.


액션

어떤 프로그램이든 사용자가 할 수 있는 일과 할 수 없는 일에 대한 피드백을 언제든지 제공함으로써 사용자에게 도움이 되어야 한다. 이러한 피드백을 제공한다는 것은, 버튼은 활성화 또는 비활성화가 가능해야 하고, 버튼 위의 이미지를 변경할 수 있어야 하며, 사용자가 입력한 데이터에 따라 버튼의 액션을 변경할 수 있음을 의미한다.


예를 들어, 로긴 대화창의 Login 버튼은 사용자 이름을 입력하기 전에 활성화되어선 안 되며, 비밀 번호가 필요하다면 번호도 입력해야 한다. 마찬가지로 저장할 내용이 있기 전까지는 Save 버튼도 활성화되어선 안 된다: 문서가 수정되었거나 그와 유사한 전제 조건이 충족되어야 한다. 이벤트 위주의 애플리케이션의 경우 이러한 필수 피드백은 주로 사용자 입력이 화면의 어떤 내용이 변경될 때 다양한 GUI 요소들의 상태를 업데이트함으로써 제공되는 것이 보통이다.


적절한 이벤트 핸들러를 통해 버튼의 상태를 설정할 수도 있다. 너무 많은 데이터의 (또는 컨트롤의) 변경사항을 모니터해야 한다면 쓰고 유지해야 할 이벤트 핸들러가 금세 엄청나게 늘어난다. 게다가 이벤트 핸들러는 다른 컨트롤들의 상태를 업데이트하는 것 외에 다른 작업에서도 사용하기 쉽다.


TActions 을 이용하여, 컨트롤을 검사하고 다른 컨트롤들의 상태를 업데이트하는 로직을 다른 프로그래밍 작업으로부터 구분할 수 있다. 액션의 상태는 애플리케이션이 유휴상태일 때, 예를 들어 사용자가 아무 일도 하고 있지 않을 때 지속적으로 검사(또는 업데이트)된다. 이로 인해 각 컨트롤의 이벤트 핸들러들은 상태를 검사할 필요가 없다. 대신 하나의 이벤트 핸들러가 주기적 간격으로 모든 컨트롤을 검사한다.


대부분 애플리케이션들은 이와 동일한 작업을 수행하는 방법들을 여러 가지 제공한다: 문서의 저장은 메뉴 엔트리를 이용하거나 툴바의 버튼을 이용해 실행할 수 있다. 메뉴 항목과 툴바 버튼이 동일한 이미지, 동일한 힌트를 표시하고 동시에 활성화 및 비활성화되는 것이 이상적이며, 꽤 많은 프로퍼티 또는 이벤트 핸들러들이 중복될 것이다-이벤트 핸들러는 공유가 가능하지만.


여기서는 또 액션이 코드를 중앙화하는 방법을 제공한다. 액션리스트에는 사용자가 실행할 수 있는 모든 액션이 모여 관리된다. 예를 들어, 'Save' 액션, 'Open' 액션 등이 포함된다. 이러한 액션들은 이제 여러 개의 GUI 요소들과 연관될 수 있다: 메뉴, 툴버튼, 일반 버튼, 확인버튼. 사용자가 GUI 요소들 중 하나를 활성화하자마자 액션이 실행된다: 일부 사전 정의된 액션들의 경우 LCL 에 의해 실행되는데, 예를 들자면 표준 TCutAction을 이용하면 현재 선택 부분을 잘라 클립보드에 복사된다. 다른 액션들의 경우 (표준 TAction) 사용자가 정의한 이벤트가 호출된다. 액션을 이용함으로써 프로그래머는 해당 액션을 실행할 수 있는 GUI 요소들 외에도 주어진 폼에 대해 자신이 원하는 사용자 액션을 정의하도록 강요된다. 프로그래머는 그러한 액션들을 actionlist 에 그룹화함으로써 잘 정의된 액션으로 조직된 리스트를 가진다. 약간의 추가 코드화를 통해 사용자는 액션을 자신에게 어떻게 표시할 것인지를 커스터마이즈-어떻게 메뉴를 구성할 것인지; 툴바를 어떻게 배치할 것인지; 심지어 액션을 실행하기 위해 어떤 바로가기를 이용할 수 있는지-하는 수단을 제공받을 수 있다.

액션리스트

액션은 비시각적 컴포넌트이다. 액션은 설계 시엔 상자에 불과한 TActionLists 에서 구성된다. 액션에는 시각적 컨트롤에서 종종 발견되는 프로퍼티에 해당하는 일련의 프로퍼티들이 있다.

프로퍼티 의미
Caption 컨트롤에 표시되는 텍스트.
Checked 메뉴, 툴바 또는 속도버튼과 확인상자용: 항목이 체크된 채로 표시될 것인 것 결정.
Enabled 컨트롤의 활성화 유무.
GroupIndex 메뉴 항목, 확인상자 또는 라디오상자용: 그룹.
HelpContext,
HelpKeyword,
HelpType
다양한 도움말 가능성(posibilities)
Hint 툴팁에 표시되는 힌트.
ImageIndex 컨트롤과 (또는 액션리스트와) 관련된 이미지리스트 내 ImageIndex
Shortcut, 액션과 관련된 단축키. 액션이 실행될 수 있다.
SecondaryShortCuts 단축키 중 하나를 이용
Visible 컨트롤이 보이는가, 보이지 않는가?
OnHint 컨트롤에 대한 힌트를 검색하기 위한 이벤트 핸들러.
OnExecute 이 핸들러는 액션이 활성화되고 스스로 실행할 필요가 있을 때 실행된다.
OnUpdate 이 핸들러는 애플리케이션의 유휴(idle) 루프에서 실행된다: 현재 폼의 상태에 따라 액션의 상태를 업데이트 하는 데 사용할 수 있다.
표 6.35: TAction 클래스의 프로퍼티


액션이 컨트롤과 관련되는 순간-그러한 컨트롤을 액션의 클라이언트라고 부른다-관련 프로퍼티들이 컨트롤로 복사된다. 이 프로퍼티 중 하나라도 변경되면 변경내용은 클라이언트 컨트롤로 즉시 전파된다.


적어도 다음 컨트롤들은 액션 클라이언트가 될 수 있다: TForm, TButton, TCheckBox, TRadioButton, TToolButton, TSpeedButton, TMenuItem: 이 모든 것은 Action 프로퍼티에 속한다.


이 프로퍼티가 설정되자마자 컨트롤은 액션의 클라이언트가 된다. 모든 경우에서, 컨트롤을 클릭하는 순간 그에 해당하는 액션이 실행되며, 이는 OnExecute 이벤트가 실행됨을 의미한다. 일부 표준 액션들은 잘 정의된 다양한 액션을 실행하여 (클립보드로 복사하기/자르기 등), 이벤트 핸들러를 필요로 하지 않지만 이를 명시할 수는 있다.


액션이 실행될 때 다음과 같은 연속 이벤트가 발생한다:

  1. 액션의 부분이 되는 액션리스트가 액션을 처리하라는 요청을 받는다.
  2. 액션리스트가 액션을 처리하지 않을 경우 애플리케이션이 액션을 처리하라는 요청을 받는다.
  3. 애플리케이션이 액션을 처리하지 않는다면 액션 스스로 실행을 시도한다ㅡ보통 자신의 OnExecute 이벤트를 실행함을 의미한다.


기본 값으로, 액션은 스스로 실행할 수 없을 경우 스스로 비활성화되므로, 비활성화가 되면 어떤OnExecute 이벤트도 설정되지 않는다.


새 액션 생성하기

기본 값으로 새 액션은 TAction 타입으로, 많은 기능을 한다: OnUpdate 와 OnExecute 와 같은 이벤트 핸들러를 통해 구체적인 기능이 제공된다. 하지만 프로그래머는 잘 정의된 프로시저를 실행하는 사전 정의된(predefined) 액션 클래스를 이용할 수 있다.


라자루스는 많은 사전 정의된 TAction 클래스를 제공하는데, 일부를 아래에 소개하겠다:

그룹 효과
Editing 편집 컨트롤과 상호작용하는 액션이 대부분: 선택내용 자르기, 복사하기, 붙여넣기, 전체 텍스트 선택하기, 선택취소, 삭제하기.
Help help 시스템을 호출하는 액션.
Dialog 폰트와 색상 대화창을 열기 위한 액션.
File 파일 메뉴용으로 설계된 액션, 예: 열기와 저장하기.
Databse TDataset 인스턴스에서 (또는 TDatasource) 실행되는 액션: 네비게이션 명령, 편집, Post, 취소하기, 삭제하기. 다른 모든 액션에 대해서는 표준 TAction 을 이용하거나, IDE에서 자신만의 액션 클래스를 프로그램화하여 등록할 수 있다.
표 6.36: 라자루스에서 표준 액션 클래스


커스텀 액션을 프로그래밍하기란 매우 쉽다. 세 개의 메소드만 오버라이드하면 된다. 이를 설명하기 위해 편집 컨트롤을 제거하는 간단한 액션을 프로그램화하고자 한다:

TClearAction = Class(TAction)
Public
  function  HandlesTarget(Target: TObject): Boolean; override;
  procedure UpdateTarget(Target: TObject); override;
  procedure ExecuteTarget(Target: TObject); override;
end;


가장 먼저 프로그램화할 메소드는 HandlesTarget method 이다. 액션을 업데이트하거나 실행할 때는 액션의 대상을 먼저 결정해야 한다. 이는 HandlesTarget method 로 일련의 컨트롤을 전달함으로써 실행된다. 아래 컨트롤이 전달된다:

  1. 최근 포커스된 컨트롤.
  2. 현재 폼.
  3. 폼에 표시되는 모든 컨트롤.


적절한 대상을 발견하자마자 검색이 중단된다. 대상을 발견한 직후에 UpdateTarget 또는 ExecuteTarget 메소드가 실행된다. 업데이트할 때 유효한 대상이 발견되지 않을 경우, DisableIfNoHandler 프로퍼티가 True라면 액션이 비활성화된다 (Taction 에 대한 기본 값).


따라서 TClearAction 과 관련해 HandlesTarget 은 대상이 커스텀 편집 컨트롤인지, 대상에 포커스가 주어졌는지를 검사해야 하는데, 이는 아래 코드를 이용해 실행할 수 있다:

function TClearAction.HandlesTarget(Target: TObject): Boolean;
begin
 Result := (Target is TCustomEdit) and (TCustomEdit(Target).Focused)
end;


대상이 발견되지 않으면 액션은 비활성화되고, 대상이 발견되면 사용자에게 피드백을 제공하는 것이 가능하다. Clear 액션의 경우, 편집 컨트롤에는 약간의 텍스트가 포함되어 있어야 한다. 텍스트가 없다면 액션은 비활성화되어야 한다:

procedure TClearAction.UpdateTarget(Target: TObject);
begin
 Enabled:=(TCustomEdit(Target).Text<>'')
end;


마지막으로, 액션이 실행될 때 ExecuteTarget 메소드가 호출되어 편집 컨트롤을 제거할 것이다. 이는 아래와 같이 실행된다:

procedure TClearAction.ExecuteTarget(Target: TObject);
begin
 If (Target is TCustomEdit) then
  (Target as TCustomEdit).Clear;
end;


Target이 실제 TCustomEdit 인지 알아보기 위한 검사는 일반적으로 불필요한데, 이는 Execute 메소드가 호출되기 전에 HandlesTarget이 검증하기 때문이다. IDE에서 액션을 등록하기 위해선 라자루스 패키지에 RegisterActions 의 호출을 삽입할 수 있는데, 아래 호출과 같은 모습일 것이다:

RegisterActions('Edit',[TClearAction],Nil);


첫 번째 파라미터는 액션 리스트 에디터(Action List Editor) 내 카테고리이다. 두 번째는 등록하고자 하는 액션 클래스의 리스트이며 (위의 예제에서는 하나만 해당), 마지막 파라미터는 이미지와 같이 클래스와 관련된 리소스를 포함할 수 있는 리소스 컴포넌트이다. 이는 본 저서에서 다루지 않을 것이다. 대신 데모 프로그램이 런타임 시에 액션을 생성할 것이다.


예제 프로그램에서 속도버튼, 편집 및 메모 컨트롤뿐만 아니라 여러 개의 버튼이 폼에 드롭된다. 폼의 OnCreate 핸들러에서 다음과 같은 액션이 생성된다:

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Aclear := TClearAction.Create(Self);
  AClear.Caption := '&Clear';
  AClear.ActionList := ALTest;
  SBClear.Action := AClear;
end;


액션은 속도버튼 SBClear 로 할당된다. (일반 버튼이 아니라) 속도버튼으로 만드는 이유는 속도버튼을 클릭할 때 포커스를 받지 않기 때문이다. 일반 버튼이었다면 액션은 절대 실행되지 않을 것이다: 클릭할 당시 버튼이 포커스를 받기 때문에 HandleTarget 은 항상 False를 리턴할 것이며, 그에 따라 액션이 절대 실행되지 않을 것이다. 데모 애플리케이션이 실행될 때는 그림 6.26과 같은 모습을 할 것이다.

림 6.26: 커스텀 액션 작동모습


버튼은 포커스가 편집 컨트롤 또는 메모 컨트롤에 있을 때에만 활성화되며, 텍스트를 포함한다.


드래그 앤 드롭

객체와 파일의 드래그 앤 드롭(Drag and Drop)은 GUI 환경에서 일반적으로 사용되는 기능이다. 따라서 라자루스는 드래그 앤 드롭을 완전하게 지원할 필요가 있다. 드래그 앤 드롭을 구현하는 방법을 설명하기 전에 우선 두 가지 유형의 드래그 앤 드롭을 구분하는 것이 필요하겠다.


첫 번째 유형은 사용자가 애플리케이션 내에서 객체를 드래그하여 드롭하는 경우다. 예를 들어, 선택한 텍스트를 새 위치로 드래그하거나, 하나의 리스트에서 다른 리스트로 항목을 드래그하는 것이 포함된다.


이러한 유형의 드래그 앤 드롭은 명시적으로 활성화 및 프로그램화되어야 한다.


두 번째 유형은, 사용자가 파일관리자(filemanager)로부터 애플리케이션으로 파일을 드롭하는 경우로, 애플리케이션이 드롭된 파일을 열도록 의도한 것이다. 이러한 유형의 드래그 앤 드롭도 지원하긴 하지만 첫 번째 유형과 다르게 처리된다. 두 유형 모두 아래에서 처리해보도록 하겠다.


파일의 드래그 앤 드롭

파일의 드래그 앤 드롭 처리는 꽤 쉽다. 두 개의 TForm 프로퍼티만 관여된다. 가장 먼저, 프로그래머는 폼이 AllowDropFiles 프로퍼티를 이용해 파일을 처리할 능력이 있음을 나타내야 한다. True로 설정하면 파일을 폼으로 드롭할 수 있다. (파일을 드래그할 때에는 커서가 이를 표시하도록 모양이 변경될 것이다) 파일이 폼에 드롭되고 나면 OnDropFiles 이벤트가 호출되며, 아래와 같은 이벤트 핸들러를 생성한다:

procedure TMainForm.FormDropFiles(Sender: TObject; const FileNames:
       array of String);
begin

end;


FileNames 파라미터는 문자열의 배열로, 폼 상에 드롭된 파일의 전체 경로명을 포함한다. 여기서 드롭된 파일명으로 해야 할 작업은 애플리케이션에 따라 좌우된다. 예를 들어보자.

procedure TMainForm.FormDropFiles(Sender: TObject; const FileNames:
       array of String);
  Var I : Integer;
begin
  // High(Filenames) is the last element in the open array.
  For I:= 0 to High(FileNames) do
  begin
    NewEditor(FileNames[i]);
  end;
end;


위의 코드에서 FileNames 배열이 순회(traverse)되어, 각 파일에 대해 새 에디터가 열린다. 파일의 드래그 앤 드롭은 이것으로 끝이 난다.


객체의 드래그 앤 드롭

라자루스는 객체의 드래그 앤 드롭도 제공하는데, 파일의 드래그 앤 드롭과 비교할 때 프로그램화가 그다지 힘들지 않다. 하지만 조금 더 많은 프로퍼티들이 수반된다:

이벤트/프로퍼티 용도
DragMode dmAutomatic 으로 설정되면, 사용자가 마우스를 컨트롤 위로 드래그하는 동시 드래그 앤 드롭 동작이 시작된다. dmManual 로 설정되면, StartDrag 를 이용해 드래그 앤 드롭 동작이 시작되어야 한다.
OnStartDrag 드래그 앤 드롭 동작이 실행될 때 이벤트가 호출된다.
OnDragOver 무언가 컨트롤 위로 드래그되었을 때만 이벤트가 호출된다. 해당 컨트롤로 드롭할 수 있는 항목인지 여부를 표시할 때 사용되어야 한다.
OnDragDrop 무언가 컨트롤 위로 드롭되었을 때 이벤트가 호출된다.
DragCursror 드래그 시 사용할 커서.
DragKind dkDrag 로 설정되어야 한다 (dkDock 은 완전히 다른 동작이므로).
표 6.37: 드래그 앤 드롭에 사용되는 이벤트와 프로퍼티


이러한 프로퍼티와 이벤트를 설명하기 위해 작은 애플리케이션을 설계해보겠다. 이 기법은 가능한 리스트로부터 몇 개의 값을 선택 시 주로 사용된다. 리스트상자들 간에 항목을 이동시킬 때는 버튼을 사용하거나 드래그 앤 드롭을 사용하는 방법이 있다. 리스트상자 항목은 다시 드래그 앤 드롭을 이용해 재배열이 가능하다.


두 개의 리스트상자는 LBLeft 와 LBRight 이다. 둘 다 DragMode 프로퍼티가 True로 설정된다.


이번에 간단한 예제에서 리스트상자는 OnStartDrag 이벤트를 필요로 하지 않는다.


LBLeft 리스트상자의 OnDragOver 이벤트는 아래와 같이 코드화되어야 한다:

procedure TMainForm.LBLeftDragOver(Sender, Source: TObject; X, Y:
   Integer; State: TDragState; var Accept: Boolean);
begin
   Accept := (Source=LBLeft) or (Source=LBRight);
end;


Sender 파라미터는 이벤트를 트리거하는 컨트롤이다 (이번 경우 LBLeft).


기본 값으로, Source 파라미터는 드래그 앤 드롭 동작을 시작하는 컨트롤이다. X, Y 파라미터들은 마우스가 Sender 컨트롤 위의 정확히 어디에서 움직이는지를 결정하는 데에 사용된다.


LBLeft 또는 LBRight 리스트에 의해 드래그가 실행될 경우, 위의 코드는 LBLeft 리스트상자로의 드롭이 허용됨을 LCL 에게 알린다.


LBRight 리스트상자에도 동일한 코드가 만들어진다:

procedure TMainForm.LBRightDragOver(Sender, Source: TObject; X, Y:
   Integer; State: TDragState; var Accept: Boolean);
begin
   Accept := (Source = LBLeft) or (Source = LBRight);
end;


항목이 LBLeft 또는 LBRight 리스트상자로 드롭되면, 이 컨트롤들에 해당하는 OnDragDrop 이벤트가 실행된다. 핸들러는 아래와 같이 코드화된다:

procedure TMainForm.LBLeftDragDrop(Sender, Source: TObject; X, Y: Integer);
  Var LBFrom,LBTO : TListBox;
begin
  // Convert source and target
  LBFrom := (Source as TListBox);
  LBTo := (Sender as TListBox);
  If (LBTo = LBFrom) then
    MoveItemsToIndex(LBTo,LBTo.GetIndexAtXY(X,Y))
  else
    MoveItemsToListBox(LBTo,LBFrom,LBTo.GetIndexAtXY(X,Y));
end;

procedure TMainForm.LBRightDragDrop(Sender, Source: TObject; X, Y: Integer);
  Var LBFrom,LBTO : TListBox;
begin
  // Convert source and target
  LBFrom := (Source as TListBox);
  LBTo := (Sender as TListBox);
  // Do what is necessary
  If (LBTo = LBFrom) then
    MoveItemsToIndex(LBTo,LBTo.GetIndexAtXY(X,Y))
  else
    MoveItemsToListBox(LBTo,LBFrom,LBTo.GetIndexAtXY(X,Y));
end;


먼저, 소스와 대상이 결정된다. 소스와 대상이 동일할 경우, 사용자가 리스트상자에서 항목을 재배열하고 있음을 의미한다. 소스와 대상이 다른 경우, 사용자가 어떤 리스트상자에서 다른 리스트상자로 항목을 이동하고 있음을 나타낸다. 다른 위치로 항목 이동은 두 가지 프로시저로 처리된다:

procedure TMainForm.MoveItemsToIndex(LB : TListBox; Newindex : Integer);
  Var  L : TStringList; I : Integer;
begin
  LB.Items.BeginUpdate; // reduce flicker during multiple operations.
  try
    // Temporary list for the selected objects.
    L:=TStringList.Create;
    try
      For I := LB.Count - 1 downto 0 do
        If LB.Selected[I] then
        begin
          // Save in temporary list
          L.AddObject(LB.Items[i],LB.Items.Objects[i]);
          // Delete in source
          LB.Items.Delete(I);
          // Correct index
          If I <= NewIndex then
            Dec(NewIndex);
        end;
      For I := 0 to L.Count - 1 do
        LB.Items.InsertObject(NewIndex,L[i],L.Objects[i]);
    finally
      L.Free; // free temporary list.
    end;
  finally
    LB.Items.EndUpdate; // Tell control it's time to update.
  end;
end;

procedure TMainForm.MoveItemsToListBox(lBTo,LBFrom : TListBox; Newinde:Integer);
  Var  I : integer;
begin
  // Add per default at the start.
  If NewIndex = -1 then
    NewIndex := 0;
  // Reduce flicker
  LBFrom.Items.BeginUpdate;
  try
    LBTo.Items.BeginUpdate;
    try
      // The items from source are processed in reverse order
      // So they appear in correct order in the target list.
      For I := LBFrom.Count - 1 downto 0 do
        If LBFrom.Selected[I] then
        begin
        LBTo.Items.InsertObject(Newindex,LBFrom.Items[i],LBFrom.Items.Objects[i]);
        LBFrom.Items.Delete(I);
        end;
    finally
      LBTo.Items.EndUpdate;
    end;
  finally
    LBFrom.Items.EndUpdate;
  end;
end;


프로그램을 완성하기 위해 리스트상자는 폼의 OnCreate 이벤트에서 몇몇 더미 항목(dummy item)으로 채워진다:

procedure TMainForm.FormCreate(Sender: TObject);
  Var I : Integer;
begin
 For I:= 1 to 10 do
  LBleft.Itmes.add('Item numbered ' + IntToStr(i));
 For I:= Ord('A') to Ord('J') do
  LBRight.Items.add('Item labeled ' + Char(i));
end;


여기서 볼 수 있듯이, 드래그 앤 드롭은 꽤 간단한 코드로 처리할 수 있다. 까다로운 루틴은 행을 실제로 이동시키는 루틴이다. 아래 스크린 샷은 프로그램을 실행시킨 모습이다:

그림 6.27: 두 컨트롤 간 드래그 앤 드롭을 단일 창에 표시


커스텀 드래그 객체 생성하기

위의 코드에서 리스트상자는 Source 파라미터를 직접 검사함으로써 드롭 작동이 허용되는지를 검사하였다. Source가 리스트상자 컨트롤 자체이거나 폼에 있는 다른 리스트상자 컨트롤이라면 드롭은 허용된다. 두 개 이상의 소스 컨트롤을 허용해야 하는 경우, 이 검사 코드는 급속도로 거대해진다. 그렇다면 소스 컨트롤이 다른 폼에 위치한다면 어떻게 될까? 어떻게 검사할 수 있을까?


드래그 앤 드롭 동작은 TDragObject 클래스를 이용해 LCL 내부에서 표시된다. 이 클래스는 공개적으로 사용되지 않을 뿐더러 앞 절에서 소개한 예제, 즉 동작이 백그라운드에서 실행되던 간단한 예제에서는 이 클래스가 눈에 보이지 않을 것이다. 위에 언급한 복잡한 시나리오를 언급하자면, LCL은 드래그 옵션이 시작될 때 프로그래머가 커스텀 TDragObject 를 생성할 수 있도록 허용한다. 이후 이 객체는 드래그 동작을 시작하는 컨트롤 대신 OnDragOver 와 OnDragDrop 이벤트의 Source 파라미터에 사용된다.


이 과정을 설명하기 위해 간단한 예제를 약간 변경하여 들어보겠다:


애플리케이션의 메인 폼은 생성 버튼 3개와 함께 리스트상자를 포함할 것이다.


사용자는 이차적 폼(secondary forms) 각각으로부터 항목을 메인 폼의 리스트상자로 드래그하여 옮길 수 있을 것이다. 이러한 셋업(setup)에서 메인 폼은 어디서 드래그 동작이 시작되었는지 검사하는 직접 검사하는 방법이 없다.


메인 폼 TMainForm 를 비롯해 이차적 폼 TListboxForm, TMemoForm, TListviewForm 가 호출될 것이다. 메인 폼에 있는 세 개의 버튼은 해당 이차적 폼의 새 인스턴스를 생성할 뿐이다:

procedure TMainForm.BListBoxWindowClick(Sender: TObject);
begin
  With TListBoxForm.Create(Self) do Show;
end;

procedure TMainForm.BMemoWindowClick(Sender: TObject);
begin
  With TMemoForm.Create(Self) do Show;
end;

procedure TMainForm.BShowListviewClick(Sender: TObject);
begin
  With TListViewForm.Create(Self) do Show;
end;


각 폼이 생성되면 먼저 폼의 캡션을 유일한 텍스트로 설정하여 (폼이 생성된 횟수) 사용자가 각 새 폼을 식별할 수 있도록 한다. 이후 컨트롤이 포함하는 다양한 항목들을 덧붙인다.


TMemoForm 의 경우 코드는 아래와 같은 모습이다:

procedure TMemoForm.FormCreate(Sender: TObject);
begin
  Inc(ThisFormCount);
  Caption := 'Memo form number ' + IntToStr(ThisFormCount);
  PopulateList(MText.Lines);
end;


MText는 TMemoForm 에 드롭되는 메모 컨트롤이다. PopulateList 호출은 dragdroplist 유닛에서 구현되는데, 단순히 무작위 수의 항목을 stringlist에 추가한다:

  Var Loffset : Integer;
Procedure PopulateList(List : TStrings);
  Var I,M : Integer;
begin
  M := 15 + Random(10);
  For I := 1 to M do
    List.Add('Item no '+ IntToStr(I + LOffset));
  Loffset := Loffset + M;
end;

procedure TListBoxForm.FormCreate(Sender: TObject);
begin
  Inc(ThisFormCount);
  Caption := 'ListBox form number ' + IntToStr(ThisFormCount);
  PopulateList(LBItems.Items);
end;


TListboxForm 과 TListviewForm 에 대해서도 비슷한 코드가 실행된다:

procedure TListviewForm.FormCreate(Sender: TObject);
  Var L : Tstrings;
      I : Integer;
begin
  Inc(ThisFormCount);
  Caption := 'Listview form number ' + IntToStr(ThisFormCount);
  L := TStringList.Create;
  try
    PopulateList(L);
    For I := 0 to L.Count-1 do
      LVItems.Items.Add.Caption := L[i];
  finally
    L.Free;
  end;
end;


이제 TMemo, TListView 또는 TListBox 컨트롤은 항목을 드래그하는 메인 폼의 리스트상자 컨트롤과 어떤 방법으로든 통신을 해야 한다.


따라서 우리는 드래그한 항목을 포함하기 위해 TDragObjectEx의 자손을 생성한다:

TStringsDragObject = Class(TDragObjectEx)
private
  FItems: Tstrings;
  procedure SetItems(const AValue: Tstrings);
Public
  Constructor Create(AControl : TControl); override;
  Destructor Destroy; override;
  property Items : Tstrings Read FItems Write SetItems;
end;


클래스는 TDragObjectEx 로부터 계승되는데, 그래야만 드래그 앤 드롭 동작이 완료 시 LCL 이 해당 객체를 자동으로 해제(free)시킬 것이기 때문이다.


메소드는 특별한 기능을 하지 않는다:

procedure TStringsDragObject.SetItems(const AValue: Tstrings);
begin
  if Fitems = AValue then exit;
  FItems.Assign(AValue);
end;

constructor TStringsDragObject.Create(AControl : TControl);
begin
  inherited Create(AControl);
  Fitems := TStringList.Create;
end;

destructor TStringsDragObject.Destroy;
begin
  FreeAndNil(Fitems);
  inherited Destroy;
end;


이 중 대부분은 메모리 관리에 속한다: 항목의 리스트가 올바르게 채워지고 객체가 파괴되면 해제(free)되도록 보장하는 것이다.


이제 객체가 이차적 폼의 listview, listbox, memo 컨트롤의 OnStartDrag 객체에서 생성된다. ListBoxForm의 경우 다음 코드가 필요하다:

procedure TListBoxForm.LBItemsStartDrag(Sender: TObject;
                                        var DragObject: TDragObject);
  Var SDO : TstringsDragObject;
      I : Integer;
begin
  SDO := TStringsDragObject.Create(LBItems);
  For I := 0 to LBItems.Count - 1 do
    If LBItems.Selected[i] then
      SDO.Items.Add(LBItems.Items[i]);
  DragObject := SDO;
end;


TStringDragObject 클래스가 생성되고, 그것의 Items 프로퍼티는 리스트상자에서 선택된 항목들로 채워지며, 마지막으로 DragObject 파라미터에서 리턴된다. TMemoForm 에 해당하는 코드는 심지어 더 간단하다:

procedure TMemoForm.MTextStartDrag(Sender: TObject;
                                   var DragObject: TDragObject);
Var SDO : TStringsDragObject;
begin
  SDO := TStringsDragObject.Create(MText);
  SDO.Items.Text := MText.SelText;
  DragObject := SDO;
end;


TListViewForm 에 대한 코드는 TListboxForm 의 핸들러의 코드에 더 가깝다:

procedure TListviewForm.LVItemsStartDrag(Sender: TObject;
                                         var DragObject: TDragObject);
  Var SDO : TStringsDragObject;
      I : Integer;
begin
  SDO := TStringsDragObject.Create(LVItems);
  For I := 0 to LVItems.Items.Count - 1 do
    If LVItems.Items[i].Selected then
      SDO.Items.Add(LVItems.Items[i].Caption);
  DragObject := SDO;
end;


이제 메인 폼의 리스트상자에 항목을 드래그하기 위한 모든 준비가 끝났다. 메인 리스트상자의 OnDragOver 이벤트는 아래와 같은 모양을 할 것이다:

procedure TMainForm.LBMainDragOver(Sender, Source: Tobject;
               X, Y: Integer; State: TDragState; var Accept: Boolean);
begin
  Accept := Source is TStringsDragObject;
end;


메인 폼의 리스트상자는 소스가 TStringsDragObject 인스턴스일 때만 드롭을 허용할 것이다. 드롭이 허용되면 사용자가 항목을 드롭할 때 아래와 같은 코드가 실행될 것이다:

procedure TMainForm.LBMainDragDrop(Sender, Source: TObject; X, Y: Integer);
  Var I,L : Integer;
      SDO : TStringsDragObject;
begin
  L := LBMain.GetIndexAtY(Y);
  If L = -1 then
  begin
    L := LBMain.Count-1;
    If L = -1 then
      L := 0;
  end;
  SDO := Source as TStringsDragObject;
  For I := SDO.Items.Count-1 downto 0 do
    LBMain.Items.Insert(L,SDO.Items[i]);
end;


먼저 드롭 위치가 계산되고 (몇몇 안정성 검사를 통해) TStringDragObject 로부터 모든 항목이 리스트상자로 복사된다.


애플리케이션의 실행 모습은 그림 6.28과 같다:

그림 6.28: 애플리케이션 내 여러 창들 간 드래그 앤 드롭 실행 모습


드래그 앤 드롭 동작의 설명을 마무리하기 위해 Controls 유닛에서 정의되는 드래그 앤 드롭 구조를 살펴보는 것이 유익하겠다:

{ TDragObjet }

TDragObject = class;

TDragKind = (dkDrag, dkDock);
TDragMode = (dmManual , dmAutomatic);
TDragState = (dsDragEnter, dsDragLeave, dsDragMove);
TDragMessage = (dmDragEnter, dmDragLeave, dmDragMove, dmDragDrop,
dmDragCancel, dmFindTarget);

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;
TStartDragEvent = procedure(Sender: TObject;
                           var DragObject: TDragObject) of object;
TEndDragEvent = procedure(Sender, Target: TObject; X,Y: Integer) of object;

TDragObject = class

private
public
  constructor Create(AControl: TControl); virtual;
  constructor AutoCreate(AControl: TControl);
  procedure HideDragImage; virtual;
  procedure ShowDragImage; virtual;
  property AlwaysShowDragImages: Boolean read FAlwaysShowDragImages
                                         write FAlwaysShowDragImages;
  property AutoCreated: Boolean read FAutoCreated;
  property AutoFree: Boolean read FAutoFree;
  property Control: TControl read FControl write FControl; // the dragged control
  property DragPos: TPoint read FDragPos write FDragPos;
  property DragTarget: TControl read FDragTarget write FDragTarget;
  property DragTargetPos: TPoint read FDragTargetPos write FDragTargetPos;
  property Dropped: Boolean read FDropped;
end;

TDragObjectClass = class of TDragObject;

{ TDragObjectEx }

TDragObjectEx = class(TDragObject)
public
  constructor Create(AControl: TControl); override;
end;


드래그 앤 도크

애플리케이션의 컨트롤에 도킹 기능이 있다면 사용자들은 사용자 인터페이스에 잘 정의된 부분들은 새 위치로 이동시켜 후에 고정된(anchored) 채로 유지할 수 있다 (아니면 일부 부분을 닫아서 인터페이스로부터 제거). 컨트롤이 새 위치에 어떻게 고정되는지는 이동하는 컨트롤 유형에 따라 좌우된다.


라자루스에서 지원하는 드래그 앤 도크(drag-and-dock)은 드래그 앤 드롭과 유사하지만, 컨트롤의 내용을 드래그하는 대신 (예: listview의 항목을 드래그하여 treeview 에 드롭하는) 컨트롤 자체를 다른 곳에 도킹하기 위해 화면을 거쳐 드래그한다는 점에서 다르다. 드래그 앤 도크와 드래그 앤 드롭 기법은 서로 유사하기 때문에 프로그램화를 위한 준비 및 시작점도 유사하다.


자동화된 드래그 앤 도크

컨트롤을 도킹 가능하게 만드는 작업은 두 개의 프로퍼티, DragKind 와 DragMode 가 책임진다. 전자의 기본 값은 dkDrop 으로 설정되고, 도킹을 위한 기능은 dkDock 로 설정되어 드래그 동작이 시작되는 시점을 표시하며, 드래그 대상은 컨트롤 내용 대신 컨트롤이어야 한다. (드래그 드로핑과 드래그 도킹을 제공하는 것도 가능한데, 관련 내용은 후에 논하겠다.) DragMode 는 dmAutomatic 으로 설정되어야 한다. 즉, LCL은 마우스가 클릭 후 드래그되는 것을 보자마자 드래그 동작을 자동으로 시작할 것을 의미한다.


오브젝트 인스펙터를 이용해 위에 언급한 프로퍼티들을 툴바 컨트롤용으로 설정할 수 있다. 툴바가 있는 애플리케이션이 실행되면 프로그래머는 툴바를 메인 폼 밖으로 드래그할 수 있음을 발견할 것이다. 툴바가 위치한 플로팅 창(floating window)이 닫히고 나면 사용자는 툴바 버튼이 사라진 것을 발견한다. 이러한 위험은 그러한 유형의 애플리케이션을 프로그램화 시 직면하는 일반적인 위험으로, 툴바를 표시하거나 숨기는 메인 메뉴의 View 메뉴 항목을 이용하거나 팝업 메뉴를 이용해 문제를 쉽게 수정할 수 있다. 그러한 메뉴 항목에 대한 이벤트 핸들러는 다음과 같다:

procedure TForm1.MenuItemShowToolbarClick(Sender: TObject);
begin
 TSampleToolbar.Parent:=Self;
 TSampleToolbar.Visible:=True;
end;


플로팅 툴바 창이 닫힌 후 메뉴 항목을 클릭하면 툴바가 창의 젤 위에 다시 위치할 것이다. 실제 애플리케이션에서는 물론 툴바가 눈에 보이는 한 이 메뉴 항목은 비활성화될 것이다.


컨트롤을 도킹시키는 작업은 창에서 제거하는 작업보다 복잡하지 않지만 추가 준비사항이 필요하다. 컨트롤을 다른 컨트롤로 도킹을 허용하려면 다른 컨트롤을 도킹 지점(docking site)으로 표시해야만 한다. 여러 TWinControl 자손들로 도킹하는 것도 가능하지만, 기본적으로 컨트롤은 도킹 지점이 될 수 없다. 다른 컨트롤의 도킹을 허용하기 위해서는 대상 컨트롤의 DockSite 프로퍼티가 True로 설정되어야 한다.


메인 폼 자체는 도킹 지점으로 만들어져선 안 된다. 이를 어길 시, 사용자가 폼 위에 아무 데나 컨트롤 툴바를 드롭할 수 있으므로 본래 의도와 어긋난다. 보통 툴바의 위치는 메인 창의 가장자리를 따라 위치한다. 이를 제공하기 위해선 폼에 네 개의 패널을 위치시키고 폼의 가장자리를 따라 정렬해야 한다. 각각의 Docksite 프로퍼티는 True로 설정되어야 하며, 그들의 AutoSize 프로퍼티들도 True로 설정되어야 한다는 점도 중요하다.


이는 패널의 크기가 그에 도킹된 컨트롤의 크기에 맞춰 조정되도록 보장한다. 라자루스 오브젝트 인스펙터에서 Autosize 를 True로 설정하면 패널이 사라지는 결과가 야기된다는 사실을 주목한다 (델파이와 반대로). 이러한 행위는 라자루스에선 일반적이다. 하지만 디자이너(Designer)에서 패널이 사라지지 않도록 하려면 AutoSize 프로퍼티를 False로 설정하고, 폼의 OnCreate 이벤트에 True로 설정해야 한다:

procedure TForm1.FormCreate(Sender: TObject);
begin
 PTop.AutoSize:=True;
 PBottom.AutoSize:=True;
 PLeft.AutoSize:=True;
 PRight.AutoSize:=True;
end;


이러한 준비작업이 끝나면 툴바를 폼의 한 면에서 다른 면으로 드래그할 수 있게 된다. 툴바의 Align 프로퍼티는 그것이 드롭되는 패널의 정렬에 일치하도록 설정해야 한다. 해당 기능은 컨트롤이 다른 컨트롤로 도킹될 때 실행되는 OnDockDrop 이벤트에서 설정할 수 있겠다:

procedure TForm1.SetDocksiteSize(Sender: TObject;
 Source: TDragDockObject; X,Y: Integer);

Var
 C : TControl;
begin
 C:=(Sender as TControl);
 if Source.Control is TToolbar then
 Source.Control.Align:=C.Align
 else
 Source.Control.Align:=alNone
end;


다른 컨트롤들은 Align 프로퍼티가 alNone 로 설정되어야 자연적 크기를 유지한다. 해당 이벤트에 대한 Source 파라미터를 주목하라: TDragDockObject 타입으로, 드래그 앤 도크 동작을 설명한다. 이는 다수의 프로퍼티를 갖고 있으며, 도킹 동작과 관련된 프로퍼티를 아래에 소개하고자 한다 (표 6.38 리스트 참고). 그림 6.29는 확장된 예제를 보여준다.


드래그되는 컨트롤.

프로퍼티 설명
DragPos 드래그 동작의 시작점.
DragTarget 현재 드래그하는 대상 컨트롤.
Dockrect 클릭했던 마우스를 해제하면 컨트롤이 도킹되는 영역을 표시하는 사각형.
DropAlign DropOnControl를 기준으로 컨트롤을 도킹 시 사용할 정렬.
DropOnControl 도킹 지점에 이미 도킹된 컨트롤, 드래그한 컨트롤이 도킹될 장소를 기준으로.
Floating 컨트롤이 현재 화면에 플로팅(floating) 상태인지 결정하는 데에 사용.
표 6.38: TDragDockObject의 프로퍼티 중 도킹에 가장 중요한 프로퍼티들


그림 6.29: 하단에 도킹된 툴바


도킹 지점 구현의 예제로 실험을 해보면 Autosize 프로퍼티에 눈에 거슬리는 단점이 보일 것이다: 도킹 직사각형이 (주로 도킹 지점의 윤곽으로 형성되는) 너비 또는 높이가 0이라는 사실이다. 이는 도킹 지점에 width 또는 height 가 없기 때문인데, 도킹 지점이 어떤 폼 가장자리를 기준으로 정렬할 것인지에 따라 좌우된다. 이러한 성가신 행위는 OnDockOver 이벤트에서 수동으로 수정할 수 있다:

procedure TForm1.PTopDockOver(Sender: TObject;
 Source: TDragDockObject;
 X,Y: Integer;
 State: TDragState;
 var Accept: Boolean);
Var
 R : TRect;
 C : TControl;
begin
 R:=Source.DockRect;
 C:=Sender as TControl;
 If (R.Bottom-R.Top)<=1 then
 Case C.Align of
 alTop : R.Bottom:=R.Bottom+Source.Control.Height;
 alBottom: R.Top:=R.Top-Source.Control.Height;
 end
 else If (R.Right-R.Left)<=1 then
 Case C.Align of
 alLeft : R.Right:=R.Right+Source.Control.Width;
 alRight : R.Left:=R.Left-Source.Control.Width;
 end;
 Source.DockRect:=R;
end


Source 객체의 DockRect 프로퍼티를 수정하여 컨트롤이 도킹되는 영역에서 사용자에게 피드백을 제공할 수 있다. 위의 코드는 단순히 사각형을 확대시켜 0이 아닌(non-zero) 크기를 가지도록 할 뿐이다.


툴바를 드래그하면 폼의 너비가 되므로 좌측 또는 우측 가장자리를 맴돌면 사각형이 폼의 크기로 증가할 것이다. 위 코드의 효과를 더 잘 보여주는 것은, 패널을 폼에 드롭하고 DragKind 와 DragMode 프로퍼티를 설정함으로써 드래그 가능하게 만들 때이다. 좌측이나 우측 가장자리 중 하나를 선택해 그 위로 패널을 드래그하면, 그림 6.30처럼 직사각형이 그려진다.

그림 6.30: DockRect의 크기 조정하기


좀 더 상세한 피드백을 제공할 수 있고, 도킹 메커니즘을 미세하게 조정하여 도킹 피드백이 트리거되기 전에 마우스가 도킹 영역 안쪽으로 최소 10픽셀에 위치하도록 할 수 있다 (10 픽셀 값은 하드코딩된다). 이 영역은 증가 또는 감소가 가능하며, 0으로도 설정 가능하다. 도킹 지점이 패널 컨트롤을 수용하지 못하도록 막을 수 있다. 이는 OnGetSiteInfo 이벤트에서 실행한다. 이 이벤트는 컨트롤을 드래그할 때 트리거된다. 폼의 가장자리를 따라 위치한 패널의 경우 이벤트를 아래와 같이 구현하면 된다:

procedure TForm1.PLeftGetSiteInfo(Sender: TObject; DockClient: TControl;
 var InfluenceRect: TRect; MousePos: TPoint;
 var CanDock: Boolean);
begin
 CanDock:=DockClient is TToolbar;
 If CanDock then
 Case (Sender as TControl).Align of
   alLeft,alRight : InflateRect(InfluenceRect,20,0);
   alBottom,alTop : InflateRect(InfluenceRect,0,20);
 end;
end


CanDock 파라미터를 이용해 도킹 지점이 드래그되는 컨트롤을 수용할 것임을 나타낼 수 있다. InfluenceRect 는 도킹 지점의 경계 사각형으로, 10 픽셀씩 증가한다. 위의 코드는 여기에 다시 20 픽셀을 추가한다. CanDock 파라미터가 True이고 (엔트리에서 그것의 값) 컨트롤이 InfluenceRect 내부에서 드래그될 경우, 도킹 사각형이 표시될 것이다.


CanDock 파라미터가 true이고 도킹 사각형이 표시되더라도 이벤트의 Accept 파라미터를 False로 설정하면 도킹이 거부될 것이기 때문에 OnDockOver 이벤트 내에 컨트롤의 도킹을 거절할 가능성이 있다.


프로그래머는 미세하게 조정한 도킹에 이를 이용할 수 있다. OnGetSiteInfo 는 도킹 지점이 도킹을 위한 컨트롤을 수용하는지만 나타낼 뿐이며, OnDockOver 이벤트는 도킹 지점의 특정 영역으로 도킹을 제한할 수 있다. 라자루스뿐만 아니라 델파이와의 작업도 원한다면 이러한 행위를 인식하게 될 것인데, 다중 툴 창들을 서로 위로 도킹하려고 시도할 때 직면하는 상황이기 때문이다.


폼에서 드래그 가능한 패널을 가지고 놀다 보면 패널을 클릭 시 패널이 플로팅(float) 됨을 재빨리 눈치챌 것이다. 이는 DragMode 가 dmAutomatic 으로 설정되면 MouseDown 이벤트가 드래그 동작을 시작하기 때문이다. 때로는 이러한 자동 행위가 오히려 짜증나기도 한다. 이러한 자동 행위를 수정하기 위해 전역 객체 Mouse에 대한 DragThreshold 와 DragImmediate 프로퍼티를 이용할 수 있겠다 (표 6.39 참고).

프로퍼티 설명
DragImmediate True로 설정 시, 드래그 가능한 컨트롤에서 mouse-down 이벤트를 사용 시 드래그 동작을 시작할 것이다. False로 설정 시, 드래그 동작은 마우스를 DragTreshold 픽셀만큼 드래그한 후에만 시작된다.
DragTreshold DragImmediate가 False일 때 드래그 동작을 시작하기 전 사용자가 마우스를 드래그해야 하는 거리. 기본 값은 5픽셀이다.
표 6.39: 드래그에 사용되는 마우스 프로퍼티


다시 패널을 클릭 가능하게 만들기 위해선 아래 코드를 폼의 OnCreate 이벤트에 위치시켜야 한다:

procedure TForm1.FormCreate(Sender: TObject);
begin
 Mouse.DragImmediate:=False;
 Mouse.DragThreshold:=50;
end;


수동 드래그 앤 도크

자동식 드래그 앤 도크는 쉽게 프로그램화가 가능하지만 머지않아 그 한계에 부딪힌다. Mouse 객체를 손보지 않고도 컨트롤을 클릭 가능하게 만드는 메커니즘이 또 있다. 이 메커니즘은 더 많은 작업을 요하지만 효과는 더 강력하다. 두 번째 메커니즘을 사용하기 위해선 DragMode 프로퍼티를 dmManual 로 설정해야 하는데, 이는 TControl 의 BginDrag 메소드를 이용해 드래그 동작이 수동으로 시작됨을 의미한다. 이 메소드는 아래와 같이 선언된다:

procedure BeginDrag(Immediate: Boolean; Threshold: Integer = -1);


호출되어 Immediate 가 True가 되면 해당 메소드는 드래그 동작을 시작할 것이다. Immediate 가 False로 설정 시, 드래그 동작은 마우스가 Threshold 픽셀만큼 드래그된 후에야 시작된다. Threshold 가 -1이라면 Mouse.Drag.Threshold의 값이 사용된다. 이제 해당 메소드를 이용해 패널의 OnMouseDown 이벤트에서 드래그 동작을 시작할 수 있다:

procedure TForm1.Panel1MouseDown(Sender: TObject;
 Button: TMouseButton;
 Shift: TShiftState;
 X, Y: Integer);
begin
 If (Button=mbLeft) and (ssCtrl in Shift) then
 Panel1.BeginDrag(False,10);
end;


이는 DragMode=dmAutomatic일 때 사례와 별반 다르지 않다. 따라서 이 메소드는 드래그 앤 드롭 동작과 드래그 앤 도크 동작 간에 전환 시 유용하다. 아래 코드는 [Ctrl] 키를 누른 상태에서 마우스를 드래그하면 드래그 앤 도크 동작을 시작한다. [Ctrl] 키를 누르지 않는다면 드래그 앤 드롭 동작이 시작된다:

procedure TForm1.Panel1MouseDown(Sender: TObject;
 Button: TMouseButton;
 Shift: TShiftState;
 X, Y: Integer);
begin
 If (Button=mbLeft) then
 begin
 if (ssCtrl in Shift) then
 Panel1.DragKind:=dkDock
 else
 Panel1.DragKind:=dkDrag;
 Panel1.BeginDrag(False,10);
 end;
end;


분명한 것은 패널에 대해서는 드래그 앤 드롭할 내용이 그다지 없지만 위와 같은 코드라면 격자, 리스트상자, 리스트뷰, 트리뷰와 같이 사용자가 항목을 쉽게 드래그 앤 드롭하거나 [Ctrl] 키를 이용해 컨트롤 자체를 드래그 앤 도크 할 수 있는 유용한 기능을 제공한다는 점이다.


이제까지 우리는 마우스를 이용해 컨트롤을 폼 밖으로 드래그하는 방법을 살펴보았다. 또한 ManualFloat 메소드를 이용해 수동으로 동작을 코드화할 수도 있는데, 이는 아래와 같이 선언된다:

function ManualFloat(TheScreenRect: TRect; KeepDockSiteSize: Boolean): Boolean;


TheScreenRect 파라미터는 컨트롤이 부동(floating) 일 때 그에 대한 경계 사각형이다ㅡ화면에 비례. 선택적 KeepDockSiteSize 파라미터는 현재 도킹 지점의 크기를 유지할 것인지 (기본 값) 아닌지를 결정한다. 예를 들어, 이 메소드를 패널의 더블 클릭 이벤트에서 사용할 수 있다:

procedure TForm1.Panel1DblClick(Sender: TObject);

Var
 R : TRect;

begin
 R:=Panel1.BoundsRect;
 R.TopLeft:=ClientToScreen(R.TopLeft);
 R.Right:=R.Left+Panel1.Width;
 R.Bottom:=R.Top+Panel1.Height;
 Panel1.ManualFloat(R);
end;


이 코드 대부분은 패널에 대한 경계 사각형의 크기와 위치를 화면을 기준으로 측정하는 역할을 한다.


컨트롤의 수동 플로팅(floating)과 비슷하여 코드를 통한 컨트롤의 도킹도 가능하다. 컨트롤에는 이러한 용도로 사용하도록 ManualDock 메소드가 있다:

function ManualDock(NewDockSite: TWinControl;
                    DropControl: TControl = nil;
                    ControlSide: TAlign = alNone;
                    KeepDockSiteSize: Boolean = true): Boolean;


NewDockSite 파라미터는 대상 도킹 컨트롤을 포함하며, 해당 호출에 유일하게 필요한 파라미터이다. DropControl 과 ControlSide 파라미터는 선택적이며, 도킹 지점에 이미 몇 개의 컨트롤이 도킹된 경우 사용된다. 이 두 개의 파라미터는 또한 도킹된 컨트롤의 상대적 위치를 명시하는 데에 사용되기도 한다. DropControl 은 새 컨트롤이 위치할 장소를 기준으로 한 컨트롤이며, ControlSide 는 컨트롤이 정확히 어디에 도킹될 것인지 결정한다. 해당 파라미터들은 앞서 설명한 TDragDockObject의 프로퍼티들, DropOnControl 과 DropAlign 에 해당한다.


마지막으로, KeepDockSiteSize 파라미터는 대상 도킹 컨트롤의 크기 조정 여부를 명시하는 데에 사용되기도 한다. 기본 값으로 컨트롤은 도킹 동작 전의 크기를 유지할 것이다.


툴바가 드래그 가능하게 만들어지면 사용자가 이것을 드래그하자마자 그 처음 위치는 손실된다. 이를 막기 위해 폼의 상단에 정렬된 패널을 이용해 도킹 영역(dock zone) 내에 폼의 상단을 만들 수 있다. 툴바는 이후 정렬되지 않은 채 폼의 어딘가로 드롭될 수 있고, 폼의 OnCreate 이벤트에서는 패널로 도킹된다. 이는 후에 복구할 수 있는 기본 시작 위치(default initial)를 생성한다. 예제에서 Toolbar1은 도킹 가능한 툴바이며, PTop 는 폼의 상단면에 있는 패널이다:

Toolbar1.ManualDock(PTop);


수동으로 툴바를 도킹하는 것이 타당한 이유가 한 가지 더 있다. 각 도킹 컨트롤은 OnUndock 이벤트를 가진다. 이 이벤트는 드래그 앤 도크 동작이 시작될 때 트리거된다: 컨트롤이 현재 도킹된 TWinControl 도킹 지점으로부터 트리거된다. 예를 들어, 드래그 앤 도크 동작을 막기 위해 사용될 수 있는데, 아래 예제를 들어보겠다:

procedure TForm1.PTopUnDock(Sender: TObject;
 Client: TControl;
 NewTarget: TWinControl;
 var Allow: Boolean);
begin
 Allow:=(NewTarget<>PBottom)
end;


Allow 파라미터는 LCL에게 언도킹(undock) 동작의 허용 여부를 알리고, NewTarget 는 사용자가 컨트롤의 도킹을 원하는 새 도킹 지점을 나타낸다. 이 이벤트는 사용자가 컨트롤을 어딘가에 실제로 드롭할 때 트리거되는데, 예를 들면 드래그 앤 도크 동작이 끝날 때 LCL은 컨트롤의 도킹을 시도한다. 이전 예제에서는 툴바가 처음에 도킹되지 않았다. 따라서 처음 도킹 시 어떤 OnUnDock 이벤트도 트리거되지 않는다. 수동으로 상단 패널에 컨트롤을 도킹시키면 툴바를 처음 도킹할 때 OnUnDock 이벤트의 트리거 또한 보장될 것이다.

컨트롤이 어딘가에 도킹되면 OnEndDock 이벤트가 트리거된다. 예를 들어, 아래와 같이 사용자에게 어느 정도 피드백을 제공할 때 사용할 수 있다:

procedure TForm1Toolbar1EndDock(Sender, Target: TObject;
 X, Y: Integer);
begin
 Caption:='Toolbar docked on '+TComponent(Target).Name;
end;


물론 .ini 파일로 새 도킹 위치를 저장하여 프로그램이 다시 실행될 때 ManualDock 을 이용해 복구시킬 수도 있다.


The DockManager

여태까지 설명한 기법을 이용하면 도킹 지점에서 한 번에 하나의 컨트롤만 허용되며, LCL은 새로 도킹된 컨트롤을 도킹 지점의 기존 컨트롤들의 맨 위에 위치시킨다. 따라서 마지막으로 드롭된 컨트롤만 눈에 보일 것이다.


TWinControl 이 도킹 지점이고, 컨트롤이 그 위에 도킹된다면 컨트롤의 위치는 Dockmanager-추상 클래스 TDockManager 의 인스턴스ㅡ에 의해 제어된다. 이러한 행위는 UseDockManager 를 False로 설정하여 비활성화시킬 수 있다. 어떤 Dockmanager 도 사용되지 않을 경우, 도킹된 컨트롤은 단순히 도킹 지점의 자식 컨트롤로 만들어진다.


UseDockManager 프로퍼티가 True일 경우-TForm 을 제외한 모든 컨트롤에 기본 값-도킹 관리자(dock manager) 인스턴스가 기존에 없었다면 컨트롤이 도킹되지마자 하나가 생성된다. 기본적으로는, 도킹 관리자 인스턴스의 실제 클래스는 controls 유닛 내 DefaultDockManagerClass 전역 변수에 의해 결정된다:

var
DefaultDockManagerClass: TDockManagerClass;


표준 도킹 관리자 구현은 (클래스 TDockTree 내) 도킹 지점에 컨트롤의 위치 조정을 위한 로직은 전혀 포함하지 않는다. TControl 의 자손들 중 일부, 즉 TPageControl 과 같은 자손들은 사용자 정의의(customized) 행위를 제공하기 위해 고유의 도킹 관리자 클래스를 구현한다. TPageControl 이 사용하는 도킹 관리자는 드롭된 컨트롤 각각을 페이지 컨트롤의 새 페이지로 도킹할 것이다. 그에 따라, 두 개의 도킹 가능 패널이 포함된 폼을 생성하고 도킹 지점에 해당하는 TPageControl 인스턴스를 생성함으로써 쉽게 표시할 수 있다. 두 개의 패널을 도킹 폼에 도킹하고 나면 그림 6.31과 같은 상황이 발생한다: 디자이너에서와 같이 원본 폼도 표시되어 차이를 보여준다.

그림 6.31: TPageControl 의 도킹관리자


표준 도킹 클래스가 도킹 지점에 도킹된 컨트롤을 이용해 특별한 일을 하는 것은 아니다. 하지만 ldocktree 유닛은 확장된 도킹 관리자 클래스를 구현한다. 이는 자동으로 DefaultDockManagerClass 로서 설치되어 훨씬 더 많은 기능을 제공한다.


물론 다른 도킹 관리자들을 설치하는 것도 가능하다. 라자루스 소스 트리의 examples/dockmanager directory는 다른 여러 효과들을 구현하는 도킹 관리자의 구현을 포함하며, 차이를 표시하기 위해 다른 컴파일러 방침에 따라 컴파일할 수 있는 데모 프로그램이 함께 제공된다.


또한 라자루스 IDE 자체에 도킹 지원을 추가하는 예제도 포함한다. 도킹을 사용할 경우-그리고 오늘날 사용자 인터페이스를 구축하는 데 필요한 필수 기능일 경우-이 예제들을 살펴보는 것이 좋으며, 도킹과 도킹 관리자의 구현과 관련해 많은 것을 배울 수 있을 것이다.


Notes

  1. wikI에서는 code를 drag하는 상태의 하이라이팅을 지원하지 않는다. 위 코드의 맨 마지막 Object선언부터 해당되는 end까지가 원문에서는 회색영역이다