LazarusCompleteGuide:6.3: Difference between revisions
Onionmixer (talk | contribs) (번역수정) |
Onionmixer (talk | contribs) (LCG 6.3 페이지 내용 추가) |
||
Line 1: | Line 1: | ||
==LCL (라자루스 컴포넌트 라이브러리)== | ==LCL (라자루스 컴포넌트 라이브러리)== | ||
LCL은 엄청난 수의 클래스를 포함하기 때문에 전체 클래스 트리(tree)를 그리자면 여러 장의 A3 용지를 인쇄해야 할 것이다. 하지만 트리의 최상단에 5가지 기본적인 클래스가 있는데, 이는 모든 라자루스 GUI | LCL은 엄청난 수의 클래스를 포함하기 때문에 전체 클래스 트리(tree)를 그리자면 여러 장의 A3 용지를 인쇄해야 할 것이다. 하지만 트리의 최상단에 5가지 기본적인 클래스가 있는데, 이는 모든 라자루스 GUI 애플리케이션에 나타난다. 이에 해당하는 클래스를 아래 그림에 소개하겠다: | ||
[[image:lazarus_6.2.png|none|320px|thumb|그림 6.2: LCL의 기반 클래스]] | [[image:lazarus_6.2.png|none|320px|thumb|그림 6.2: LCL의 기반 클래스]] | ||
Line 1,441: | Line 1,441: | ||
아래와 같은 | 아래와 같은 구문을 이용하여 | ||
<syntaxhighlight lang="pascal"> | <syntaxhighlight lang="pascal"> | ||
Line 2,106: | Line 2,106: | ||
컴포넌트 팔레트에서 매번 폼의 인스턴스를 선택할 필요 없이 다중 인스턴스를 추가하는 방법에는 두 가지가 있다: | 컴포넌트 팔레트에서 매번 폼의 인스턴스를 선택할 필요 없이 다중 인스턴스를 추가하는 방법에는 두 가지가 있다: | ||
# 폼을 클릭할 때 [Shift] 키를 누른 채로 유지한다. 그리고 나서 ([Shift]는 여전히 누른 채로) 클릭할 때마다 선택된 컨트롤의 새 인스턴스가 폼으로 추가될 것이다. | # 폼을 클릭할 때 [Shift] 키를 누른 채로 유지한다. 그리고 나서 ([Shift]는 여전히 누른 채로) 클릭할 때마다 선택된 컨트롤의 새 인스턴스가 폼으로 추가될 것이다.2# 툴바 버튼을 클릭할 때 [Shift] 키를 누른다. 그리고 나면 폼을 클릭할 때마다 선택된 컨트롤의 새 인스턴스가 폼에 추가되며, [Shift] 키를 계속 누를 필요가 없어진다. | ||
# 툴바 버튼을 클릭할 때 [Shift] 키를 누른다. 그리고 나면 폼을 클릭할 때마다 선택된 컨트롤의 새 인스턴스가 폼에 추가되며, [Shift] 키를 계속 누를 필요가 없어진다. | |||
Line 2,372: | Line 2,371: | ||
|'''컨트롤'''||'''탭(tab)'''||'''설명''' | |'''컨트롤'''||'''탭(tab)'''||'''설명''' | ||
|- style="vertical-align:top;" | |- style="vertical-align:top;" | ||
|'''TLabel'''||Standard||어떠한 마크업(mark-up)도 없는 간단한 텍스트 | |'''TLabel'''||Standard||어떠한 마크업(mark-up)도 없는 간단한 텍스트 구문. | ||
|- style="vertical-align:top;" | |- style="vertical-align:top;" | ||
|'''TImage'''||Additional||간단한 이미지 컨트롤. | |'''TImage'''||Additional||간단한 이미지 컨트롤. | ||
Line 3,232: | Line 3,231: | ||
[[image:lazarus_6.27.png|none|344px|thumb|그림 6.27: 두 컨트롤 간 드래그 앤 드롭을 단일 창에 표시]] | [[image:lazarus_6.27.png|none|344px|thumb|그림 6.27: 두 컨트롤 간 드래그 앤 드롭을 단일 창에 표시]] | ||
====커스텀 드래그 객체 생성하기==== | |||
위의 코드에서 리스트상자는 Source 파라미터를 직접 검사함으로써 드롭 작동이 허용되는지를 검사하였다. Source가 리스트상자 컨트롤 자체이거나 폼에 있는 다른 리스트상자 컨트롤이라면 드롭은 허용된다. 두 개 이상의 소스 컨트롤을 허용해야 하는 경우, 이 검사 코드는 급속도로 거대해진다. 그렇다면 소스 컨트롤이 다른 폼에 위치한다면 어떻게 될까? 어떻게 검사할 수 있을까? | |||
드래그 앤 드롭 동작은 TDragObject 클래스를 이용해 LCL 내부에서 표시된다. 이 클래스는 공개적으로 사용되지 않을 뿐더러 앞 절에서 소개한 예제, 즉 동작이 백그라운드에서 실행되던 간단한 예제에서는 이 클래스가 눈에 보이지 않을 것이다. 위에 언급한 복잡한 시나리오를 언급하자면, LCL은 드래그 옵션이 시작될 때 프로그래머가 커스텀 TDragObject 를 생성할 수 있도록 허용한다. 이후 이 객체는 드래그 동작을 시작하는 컨트롤 대신 OnDragOver 와 OnDragDrop 이벤트의 Source 파라미터에 사용된다. | |||
이 과정을 설명하기 위해 간단한 예제를 약간 변경하여 들어보겠다: | |||
애플리케이션의 메인 폼은 생성 버튼 3개와 함께 리스트상자를 포함할 것이다. | |||
사용자는 이차적 폼(secondary forms) 각각으로부터 항목을 메인 폼의 리스트상자로 드래그하여 옮길 수 있을 것이다. 이러한 셋업(setup)에서 메인 폼은 어디서 드래그 동작이 시작되었는지 검사하는 직접 검사하는 방법이 없다. | |||
메인 폼 TMainForm 를 비롯해 이차적 폼 TListboxForm, TMemoForm, TListviewForm 가 호출될 것이다. 메인 폼에 있는 세 개의 버튼은 해당 이차적 폼의 새 인스턴스를 생성할 뿐이다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
각 폼이 생성되면 먼저 폼의 캡션을 유일한 텍스트로 설정하여 (폼이 생성된 횟수) 사용자가 각 새 폼을 식별할 수 있도록 한다. 이후 컨트롤이 포함하는 다양한 항목들을 덧붙인다. | |||
TMemoForm 의 경우 코드는 아래와 같은 모습이다: | |||
<syntaxhighlight lang="pascal"> | |||
procedure TMemoForm.FormCreate(Sender: TObject); | |||
begin | |||
Inc(ThisFormCount); | |||
Caption := 'Memo form number ' + IntToStr(ThisFormCount); | |||
PopulateList(MText.Lines); | |||
end; | |||
</syntaxhighlight> | |||
MText는 TMemoForm 에 드롭되는 메모 컨트롤이다. PopulateList 호출은 dragdroplist 유닛에서 구현되는데, 단순히 무작위 수의 항목을 stringlist에 추가한다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
TListboxForm 과 TListviewForm 에 대해서도 비슷한 코드가 실행된다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
이제 TMemo, TListView 또는 TListBox 컨트롤은 항목을 드래그하는 메인 폼의 리스트상자 컨트롤과 어떤 방법으로든 통신을 해야 한다. | |||
따라서 우리는 드래그한 항목을 포함하기 위해 TDragObjectEx의 자손을 생성한다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
클래스는 TDragObjectEx 로부터 계승되는데, 그래야만 드래그 앤 드롭 동작이 완료 시 LCL 이 해당 객체를 자동으로 해제(free)시킬 것이기 때문이다. | |||
메소드는 특별한 기능을 하지 않는다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
이 중 대부분은 메모리 관리에 속한다: 항목의 리스트가 올바르게 채워지고 객체가 파괴되면 해제(free)되도록 보장하는 것이다. | |||
이제 객체가 이차적 폼의 listview, listbox, memo 컨트롤의 OnStartDrag 객체에서 생성된다. ListBoxForm의 경우 다음 코드가 필요하다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
TStringDragObject 클래스가 생성되고, 그것의 Items 프로퍼티는 리스트상자에서 선택된 항목들로 채워지며, 마지막으로 DragObject 파라미터에서 리턴된다. TMemoForm 에 해당하는 코드는 심지어 더 간단하다: | |||
<syntaxhighlight lang="pascal"> | |||
procedure TMemoForm.MTextStartDrag(Sender: TObject; | |||
var DragObject: TDragObject); | |||
Var SDO : TStringsDragObject; | |||
begin | |||
SDO := TStringsDragObject.Create(MText); | |||
SDO.Items.Text := MText.SelText; | |||
DragObject := SDO; | |||
end; | |||
</syntaxhighlight> | |||
TListViewForm 에 대한 코드는 TListboxForm 의 핸들러의 코드에 더 가깝다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
이제 메인 폼의 리스트상자에 항목을 드래그하기 위한 모든 준비가 끝났다. 메인 리스트상자의 OnDragOver 이벤트는 아래와 같은 모양을 할 것이다: | |||
<syntaxhighlight lang="pascal"> | |||
procedure TMainForm.LBMainDragOver(Sender, Source: Tobject; | |||
X, Y: Integer; State: TDragState; var Accept: Boolean); | |||
begin | |||
Accept := Source is TStringsDragObject; | |||
end; | |||
</syntaxhighlight> | |||
메인 폼의 리스트상자는 소스가 TStringsDragObject 인스턴스일 때만 드롭을 허용할 것이다. 드롭이 허용되면 사용자가 항목을 드롭할 때 아래와 같은 코드가 실행될 것이다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
먼저 드롭 위치가 계산되고 (몇몇 안정성 검사를 통해) TStringDragObject 로부터 모든 항목이 리스트상자로 복사된다. | |||
애플리케이션의 실행 모습은 그림 6.28과 같다: | |||
[[image:lazarus_6.28.png|none|544px|thumb|그림 6.28: 애플리케이션 내 여러 창들 간 드래그 앤 드롭 실행 모습]] | |||
드래그 앤 드롭 동작의 설명을 마무리하기 위해 Controls 유닛에서 정의되는 드래그 앤 드롭 구조를 살펴보는 것이 유익하겠다: | |||
<syntaxhighlight lang="pascal"> | |||
{ 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; | |||
</syntaxhighlight> | |||
===드래그 앤 도크=== | |||
애플리케이션의 컨트롤에 도킹 기능이 있다면 사용자들은 사용자 인터페이스에 잘 정의된 부분들은 새 위치로 이동시켜 후에 고정된(anchored) 채로 유지할 수 있다 (아니면 일부 부분을 닫아서 인터페이스로부터 제거). 컨트롤이 새 위치에 어떻게 고정되는지는 이동하는 컨트롤 유형에 따라 좌우된다. | |||
라자루스에서 지원하는 드래그 앤 도크(drag-and-dock)은 드래그 앤 드롭과 유사하지만, 컨트롤의 내용을 드래그하는 대신 (예: listview의 항목을 드래그하여 treeview 에 드롭하는) 컨트롤 자체를 다른 곳에 도킹하기 위해 화면을 거쳐 드래그한다는 점에서 다르다. 드래그 앤 도크와 드래그 앤 드롭 기법은 서로 유사하기 때문에 프로그램화를 위한 준비 및 시작점도 유사하다. | |||
====자동화된 드래그 앤 도크==== | |||
컨트롤을 도킹 가능하게 만드는 작업은 두 개의 프로퍼티, DragKind 와 DragMode 가 책임진다. 전자의 기본 값은 dkDrop 으로 설정되고, 도킹을 위한 기능은 dkDock 로 설정되어 드래그 동작이 시작되는 시점을 표시하며, 드래그 대상은 컨트롤 내용 대신 컨트롤이어야 한다. (드래그 드로핑과 드래그 도킹을 제공하는 것도 가능한데, 관련 내용은 후에 논하겠다.) DragMode 는 dmAutomatic 으로 설정되어야 한다. 즉, LCL은 마우스가 클릭 후 드래그되는 것을 보자마자 드래그 동작을 자동으로 시작할 것을 의미한다. | |||
오브젝트 인스펙터를 이용해 위에 언급한 프로퍼티들을 툴바 컨트롤용으로 설정할 수 있다. 툴바가 있는 애플리케이션이 실행되면 프로그래머는 툴바를 메인 폼 밖으로 드래그할 수 있음을 발견할 것이다. 툴바가 위치한 플로팅 창(floating window)이 닫히고 나면 사용자는 툴바 버튼이 사라진 것을 발견한다. 이러한 위험은 그러한 유형의 애플리케이션을 프로그램화 시 직면하는 일반적인 위험으로, 툴바를 표시하거나 숨기는 메인 메뉴의 View 메뉴 항목을 이용하거나 팝업 메뉴를 이용해 문제를 쉽게 수정할 수 있다. 그러한 메뉴 항목에 대한 이벤트 핸들러는 다음과 같다: | |||
<syntaxhighlight lang="pascal"> | |||
procedure TForm1.MenuItemShowToolbarClick(Sender: TObject); | |||
begin | |||
TSampleToolbar.Parent:=Self; | |||
TSampleToolbar.Visible:=True; | |||
end; | |||
</syntaxhighlight> | |||
플로팅 툴바 창이 닫힌 후 메뉴 항목을 클릭하면 툴바가 창의 젤 위에 다시 위치할 것이다. 실제 애플리케이션에서는 물론 툴바가 눈에 보이는 한 이 메뉴 항목은 비활성화될 것이다. | |||
컨트롤을 도킹시키는 작업은 창에서 제거하는 작업보다 복잡하지 않지만 추가 준비사항이 필요하다. 컨트롤을 다른 컨트롤로 도킹을 허용하려면 다른 컨트롤을 도킹 지점(docking site)으로 표시해야만 한다. 여러 TWinControl 자손들로 도킹하는 것도 가능하지만, 기본적으로 컨트롤은 도킹 지점이 될 수 없다. 다른 컨트롤의 도킹을 허용하기 위해서는 대상 컨트롤의 DockSite 프로퍼티가 True로 설정되어야 한다. | |||
메인 폼 자체는 도킹 지점으로 만들어져선 안 된다. 이를 어길 시, 사용자가 폼 위에 아무 데나 컨트롤 툴바를 드롭할 수 있으므로 본래 의도와 어긋난다. 보통 툴바의 위치는 메인 창의 가장자리를 따라 위치한다. 이를 제공하기 위해선 폼에 네 개의 패널을 위치시키고 폼의 가장자리를 따라 정렬해야 한다. 각각의 Docksite 프로퍼티는 True로 설정되어야 하며, 그들의 AutoSize 프로퍼티들도 True로 설정되어야 한다는 점도 중요하다. | |||
이는 패널의 크기가 그에 도킹된 컨트롤의 크기에 맞춰 조정되도록 보장한다. 라자루스 오브젝트 인스펙터에서 Autosize 를 True로 설정하면 패널이 사라지는 결과가 야기된다는 사실을 주목한다 (델파이와 반대로). 이러한 행위는 라자루스에선 일반적이다. 하지만 디자이너(Designer)에서 패널이 사라지지 않도록 하려면 AutoSize 프로퍼티를 False로 설정하고, 폼의 OnCreate 이벤트에 True로 설정해야 한다: | |||
<syntaxhighlight lang="pascal"> | |||
procedure TForm1.FormCreate(Sender: TObject); | |||
begin | |||
PTop.AutoSize:=True; | |||
PBottom.AutoSize:=True; | |||
PLeft.AutoSize:=True; | |||
PRight.AutoSize:=True; | |||
end; | |||
</syntaxhighlight> | |||
이러한 준비작업이 끝나면 툴바를 폼의 한 면에서 다른 면으로 드래그할 수 있게 된다. 툴바의 Align 프로퍼티는 그것이 드롭되는 패널의 정렬에 일치하도록 설정해야 한다. 해당 기능은 컨트롤이 다른 컨트롤로 도킹될 때 실행되는 OnDockDrop 이벤트에서 설정할 수 있겠다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
다른 컨트롤들은 Align 프로퍼티가 alNone 로 설정되어야 자연적 크기를 유지한다. 해당 이벤트에 대한 Source 파라미터를 주목하라: TDragDockObject 타입으로, 드래그 앤 도크 동작을 설명한다. 이는 다수의 프로퍼티를 갖고 있으며, 도킹 동작과 관련된 프로퍼티를 아래에 소개하고자 한다 (표 6.38 리스트 참고). 그림 6.29는 확장된 예제를 보여준다. | |||
'''드래그되는 컨트롤.''' | |||
{| style="border: 1px solid black;" | |||
|- style="color: white; background-color: black;" | |||
|'''프로퍼티'''||'''설명''' | |||
|- style="vertical-align:top;" | |||
|DragPos'''||드래그 동작의 시작점. | |||
|- style="vertical-align:top;" | |||
|DragTarget'''||현재 드래그하는 대상 컨트롤. | |||
|- style="vertical-align:top;" | |||
|Dockrect'''||클릭했던 마우스를 해제하면 컨트롤이 도킹되는 영역을 표시하는 사각형. | |||
|- style="vertical-align:top;" | |||
|DropAlign'''||DropOnControl를 기준으로 컨트롤을 도킹 시 사용할 정렬. | |||
|- style="vertical-align:top;" | |||
|DropOnControl'''||도킹 지점에 이미 도킹된 컨트롤, 드래그한 컨트롤이 도킹될 장소를 기준으로. | |||
|- style="vertical-align:top;" | |||
|Floating'''||컨트롤이 현재 화면에 플로팅(floating) 상태인지 결정하는 데에 사용. | |||
|- style="color: black; background-color: gray;" | |||
| colspan="2" |표 6.38: TDragDockObject의 프로퍼티 중 도킹에 가장 중요한 프로퍼티들 | |||
|} | |||
[[image:lazarus_6.29.png|none|526px|thumb|그림 6.29: 하단에 도킹된 툴바]] | |||
도킹 지점 구현의 예제로 실험을 해보면 Autosize 프로퍼티에 눈에 거슬리는 단점이 보일 것이다: 도킹 직사각형이 (주로 도킹 지점의 윤곽으로 형성되는) 너비 또는 높이가 0이라는 사실이다. 이는 도킹 지점에 width 또는 height 가 없기 때문인데, 도킹 지점이 어떤 폼 가장자리를 기준으로 정렬할 것인지에 따라 좌우된다. 이러한 성가신 행위는 OnDockOver 이벤트에서 수동으로 수정할 수 있다: | |||
<syntaxhighlight lang="pascal"> | |||
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 | |||
</syntaxhighlight> | |||
Source 객체의 DockRect 프로퍼티를 수정하여 컨트롤이 도킹되는 영역에서 사용자에게 피드백을 제공할 수 있다. 위의 코드는 단순히 사각형을 확대시켜 0이 아닌(non-zero) 크기를 가지도록 할 뿐이다. | |||
툴바를 드래그하면 폼의 너비가 되므로 좌측 또는 우측 가장자리를 맴돌면 사각형이 폼의 크기로 증가할 것이다. 위 코드의 효과를 더 잘 보여주는 것은, 패널을 폼에 드롭하고 DragKind 와 DragMode 프로퍼티를 설정함으로써 드래그 가능하게 만들 때이다. 좌측이나 우측 가장자리 중 하나를 선택해 그 위로 패널을 드래그하면, 그림 6.30처럼 직사각형이 그려진다. | |||
[[image:lazarus_6.30.png|none|454px|thumb|그림 6.30: DockRect의 크기 조정하기]] | |||
좀 더 상세한 피드백을 제공할 수 있고, 도킹 메커니즘을 미세하게 조정하여 도킹 피드백이 트리거되기 전에 마우스가 도킹 영역 안쪽으로 최소 10픽셀에 위치하도록 할 수 있다 (10 픽셀 값은 하드코딩된다). 이 영역은 증가 또는 감소가 가능하며, 0으로도 설정 가능하다. 도킹 지점이 패널 컨트롤을 수용하지 못하도록 막을 수 있다. 이는 OnGetSiteInfo 이벤트에서 실행한다. 이 이벤트는 컨트롤을 드래그할 때 트리거된다. 폼의 가장자리를 따라 위치한 패널의 경우 이벤트를 아래와 같이 구현하면 된다: | |||
<syntaxhighlight lang="pascal"> | |||
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 | |||
</syntaxhighlight> | |||
CanDock 파라미터를 이용해 도킹 지점이 드래그되는 컨트롤을 수용할 것임을 나타낼 수 있다. InfluenceRect 는 도킹 지점의 경계 사각형으로, 10 픽셀씩 증가한다. 위의 코드는 여기에 다시 20 픽셀을 추가한다. CanDock 파라미터가 True이고 (엔트리에서 그것의 값) 컨트롤이 InfluenceRect 내부에서 드래그될 경우, 도킹 사각형이 표시될 것이다. | |||
CanDock 파라미터가 true이고 도킹 사각형이 표시되더라도 이벤트의 Accept 파라미터를 False로 설정하면 도킹이 거부될 것이기 때문에 OnDockOver 이벤트 내에 컨트롤의 도킹을 거절할 가능성이 있다. | |||
프로그래머는 미세하게 조정한 도킹에 이를 이용할 수 있다. OnGetSiteInfo 는 도킹 지점이 도킹을 위한 컨트롤을 수용하는지만 나타낼 뿐이며, OnDockOver 이벤트는 도킹 지점의 특정 영역으로 도킹을 제한할 수 있다. 라자루스뿐만 아니라 델파이와의 작업도 원한다면 이러한 행위를 인식하게 될 것인데, 다중 툴 창들을 서로 위로 도킹하려고 시도할 때 직면하는 상황이기 때문이다. | |||
폼에서 드래그 가능한 패널을 가지고 놀다 보면 패널을 클릭 시 패널이 플로팅(float) 됨을 재빨리 눈치챌 것이다. 이는 DragMode 가 dmAutomatic 으로 설정되면 MouseDown 이벤트가 드래그 동작을 시작하기 때문이다. 때로는 이러한 자동 행위가 오히려 짜증나기도 한다. 이러한 자동 행위를 수정하기 위해 전역 객체 Mouse에 대한 DragThreshold 와 DragImmediate 프로퍼티를 이용할 수 있겠다 (표 6.39 참고). | |||
{| style="border: 1px solid black;" | |||
|- style="color: white; background-color: black;" | |||
|'''프로퍼티'''||'''설명''' | |||
|- style="vertical-align:top;" | |||
|'''DragImmediate'''||True로 설정 시, 드래그 가능한 컨트롤에서 mouse-down 이벤트를 사용 시 드래그 동작을 시작할 것이다. False로 설정 시, 드래그 동작은 마우스를 DragTreshold 픽셀만큼 드래그한 후에만 시작된다. | |||
|- style="vertical-align:top;" | |||
|'''DragTreshold'''||DragImmediate가 False일 때 드래그 동작을 시작하기 전 사용자가 마우스를 드래그해야 하는 거리. 기본 값은 5픽셀이다. | |||
|- style="color: black; background-color: gray;" | |||
| colspan="2" |표 6.39: 드래그에 사용되는 마우스 프로퍼티 | |||
|} | |||
다시 패널을 클릭 가능하게 만들기 위해선 아래 코드를 폼의 OnCreate 이벤트에 위치시켜야 한다: | |||
<syntaxhighlight lang="pascal"> | |||
procedure TForm1.FormCreate(Sender: TObject); | |||
begin | |||
Mouse.DragImmediate:=False; | |||
Mouse.DragThreshold:=50; | |||
end; | |||
</syntaxhighlight> | |||
====수동 드래그 앤 도크==== | |||
자동식 드래그 앤 도크는 쉽게 프로그램화가 가능하지만 머지않아 그 한계에 부딪힌다. Mouse 객체를 손보지 않고도 컨트롤을 클릭 가능하게 만드는 메커니즘이 또 있다. 이 메커니즘은 더 많은 작업을 요하지만 효과는 더 강력하다. 두 번째 메커니즘을 사용하기 위해선 DragMode 프로퍼티를 dmManual 로 설정해야 하는데, 이는 TControl 의 BginDrag 메소드를 이용해 드래그 동작이 수동으로 시작됨을 의미한다. 이 메소드는 아래와 같이 선언된다: | |||
<syntaxhighlight lang="pascal"> | |||
procedure BeginDrag(Immediate: Boolean; Threshold: Integer = -1); | |||
</syntaxhighlight> | |||
호출되어 Immediate 가 True가 되면 해당 메소드는 드래그 동작을 시작할 것이다. Immediate 가 False로 설정 시, 드래그 동작은 마우스가 Threshold 픽셀만큼 드래그된 후에야 시작된다. Threshold 가 -1이라면 Mouse.Drag.Threshold의 값이 사용된다. 이제 해당 메소드를 이용해 패널의 OnMouseDown 이벤트에서 드래그 동작을 시작할 수 있다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
이는 DragMode=dmAutomatic일 때 사례와 별반 다르지 않다. 따라서 이 메소드는 드래그 앤 드롭 동작과 드래그 앤 도크 동작 간에 전환 시 유용하다. | |||
아래 코드는 [Ctrl] 키를 누른 상태에서 마우스를 드래그하면 드래그 앤 도크 동작을 시작한다. [Ctrl] 키를 누르지 않는다면 드래그 앤 드롭 동작이 시작된다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
분명한 것은 패널에 대해서는 드래그 앤 드롭할 내용이 그다지 없지만 위와 같은 코드라면 격자, 리스트상자, 리스트뷰, 트리뷰와 같이 사용자가 항목을 쉽게 드래그 앤 드롭하거나 [Ctrl] 키를 이용해 컨트롤 자체를 드래그 앤 도크 할 수 있는 유용한 기능을 제공한다는 점이다. | |||
이제까지 우리는 마우스를 이용해 컨트롤을 폼 밖으로 드래그하는 방법을 살펴보았다. 또한 ManualFloat 메소드를 이용해 수동으로 동작을 코드화할 수도 있는데, 이는 아래와 같이 선언된다: | |||
<syntaxhighlight lang="pascal"> | |||
function ManualFloat(TheScreenRect: TRect; KeepDockSiteSize: Boolean): Boolean; | |||
</syntaxhighlight> | |||
TheScreenRect 파라미터는 컨트롤이 부동(floating) 일 때 그에 대한 경계 사각형이다ㅡ화면에 비례. 선택적 KeepDockSiteSize 파라미터는 현재 도킹 지점의 크기를 유지할 것인지 (기본 값) 아닌지를 결정한다. 예를 들어, 이 메소드를 패널의 더블 클릭 이벤트에서 사용할 수 있다: | |||
<syntaxhighlight lang="pascal"> | |||
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; | |||
</syntaxhighlight> | |||
이 코드 대부분은 패널에 대한 경계 사각형의 크기와 위치를 화면을 기준으로 측정하는 역할을 한다. | |||
컨트롤의 수동 플로팅(floating)과 비슷하여 코드를 통한 컨트롤의 도킹도 가능하다. 컨트롤에는 이러한 용도로 사용하도록 ManualDock 메소드가 있다: | |||
<syntaxhighlight lang="pascal"> | |||
function ManualDock(NewDockSite: TWinControl; | |||
DropControl: TControl = nil; | |||
ControlSide: TAlign = alNone; | |||
KeepDockSiteSize: Boolean = true): Boolean; | |||
</syntaxhighlight> | |||
NewDockSite 파라미터는 대상 도킹 컨트롤을 포함하며, 해당 호출에 유일하게 필요한 파라미터이다. DropControl 과 ControlSide 파라미터는 선택적이며, 도킹 지점에 이미 몇 개의 컨트롤이 도킹된 경우 사용된다. 이 두 개의 파라미터는 또한 도킹된 컨트롤의 상대적 위치를 명시하는 데에 사용되기도 한다. DropControl 은 새 컨트롤이 위치할 장소를 기준으로 한 컨트롤이며, ControlSide 는 컨트롤이 정확히 어디에 도킹될 것인지 결정한다. 해당 파라미터들은 앞서 설명한 TDragDockObject의 프로퍼티들, DropOnControl 과 DropAlign 에 해당한다. | |||
마지막으로, KeepDockSiteSize 파라미터는 대상 도킹 컨트롤의 크기 조정 여부를 명시하는 데에 사용되기도 한다. 기본 값으로 컨트롤은 도킹 동작 전의 크기를 유지할 것이다. | |||
툴바가 드래그 가능하게 만들어지면 사용자가 이것을 드래그하자마자 그 처음 위치는 손실된다. 이를 막기 위해 폼의 상단에 정렬된 패널을 이용해 도킹 영역(dock zone) 내에 폼의 상단을 만들 수 있다. 툴바는 이후 정렬되지 않은 채 폼의 어딘가로 드롭될 수 있고, 폼의 OnCreate 이벤트에서는 패널로 도킹된다. 이는 후에 복구할 수 있는 기본 시작 위치(default initial)를 생성한다. 예제에서 Toolbar1은 도킹 가능한 툴바이며, PTop 는 폼의 상단면에 있는 패널이다: | |||
<syntaxhighlight lang="pascal"> | |||
Toolbar1.ManualDock(PTop); | |||
</syntaxhighlight> | |||
수동으로 툴바를 도킹하는 것이 타당한 이유가 한 가지 더 있다. 각 도킹 컨트롤은 OnUndock 이벤트를 가진다. 이 이벤트는 드래그 앤 도크 동작이 시작될 때 트리거된다: 컨트롤이 현재 도킹된 TWinControl 도킹 지점으로부터 트리거된다. 예를 들어, 드래그 앤 도크 동작을 막기 위해 사용될 수 있는데, 아래 예제를 들어보겠다: | |||
<syntaxhighlight lang="pascal"> | |||
procedure TForm1.PTopUnDock(Sender: TObject; | |||
Client: TControl; | |||
NewTarget: TWinControl; | |||
var Allow: Boolean); | |||
begin | |||
Allow:=(NewTarget<>PBottom) | |||
end; | |||
</syntaxhighlight> | |||
Allow 파라미터는 LCL에게 언도킹(undock) 동작의 허용 여부를 알리고, NewTarget 는 사용자가 컨트롤의 도킹을 원하는 새 도킹 지점을 나타낸다. 이 이벤트는 사용자가 컨트롤을 어딘가에 실제로 드롭할 때 트리거되는데, 예를 들면 드래그 앤 도크 동작이 끝날 때 LCL은 컨트롤의 도킹을 시도한다. 이전 예제에서는 툴바가 처음에 도킹되지 않았다. 따라서 처음 도킹 시 어떤 OnUnDock 이벤트도 트리거되지 않는다. 수동으로 상단 패널에 컨트롤을 도킹시키면 툴바를 처음 도킹할 때 OnUnDock 이벤트의 트리거 또한 보장될 것이다. | |||
컨트롤이 어딘가에 도킹되면 OnEndDock 이벤트가 트리거된다. 예를 들어, 아래와 같이 사용자에게 어느 정도 피드백을 제공할 때 사용할 수 있다: | |||
<syntaxhighlight lang="pascal"> | |||
procedure TForm1Toolbar1EndDock(Sender, Target: TObject; | |||
X, Y: Integer); | |||
begin | |||
Caption:='Toolbar docked on '+TComponent(Target).Name; | |||
end; | |||
</syntaxhighlight> | |||
물론 .ini 파일로 새 도킹 위치를 저장하여 프로그램이 다시 실행될 때 ManualDock 을 이용해 복구시킬 수도 있다. | |||
====The DockManager==== | |||
여태까지 설명한 기법을 이용하면 도킹 지점에서 한 번에 하나의 컨트롤만 허용되며, LCL은 새로 도킹된 컨트롤을 도킹 지점의 기존 컨트롤들의 맨 위에 위치시킨다. 따라서 마지막으로 드롭된 컨트롤만 눈에 보일 것이다. | |||
TWinControl 이 도킹 지점이고, 컨트롤이 그 위에 도킹된다면 컨트롤의 위치는 Dockmanager-추상 클래스 TDockManager 의 인스턴스ㅡ에 의해 제어된다. 이러한 행위는 UseDockManager 를 False로 설정하여 비활성화시킬 수 있다. 어떤 Dockmanager 도 사용되지 않을 경우, 도킹된 컨트롤은 단순히 도킹 지점의 자식 컨트롤로 만들어진다. | |||
UseDockManager 프로퍼티가 True일 경우-TForm 을 제외한 모든 컨트롤에 기본 값-도킹 관리자(dock manager) 인스턴스가 기존에 없었다면 컨트롤이 도킹되지마자 하나가 생성된다. 기본적으로는, 도킹 관리자 인스턴스의 실제 클래스는 controls 유닛 내 DefaultDockManagerClass 전역 변수에 의해 결정된다: | |||
<syntaxhighlight lang="pascal"> | |||
var | |||
DefaultDockManagerClass: TDockManagerClass; | |||
</syntaxhighlight> | |||
표준 도킹 관리자 구현은 (클래스 TDockTree 내) 도킹 지점에 컨트롤의 위치 조정을 위한 로직은 전혀 포함하지 않는다. TControl 의 자손들 중 일부, 즉 TPageControl 과 같은 자손들은 사용자 정의의(customized) 행위를 제공하기 위해 고유의 도킹 관리자 클래스를 구현한다. TPageControl 이 사용하는 도킹 관리자는 드롭된 컨트롤 각각을 페이지 컨트롤의 새 페이지로 도킹할 것이다. 그에 따라, 두 개의 도킹 가능 패널이 포함된 폼을 생성하고 도킹 지점에 해당하는 TPageControl 인스턴스를 생성함으로써 쉽게 표시할 수 있다. 두 개의 패널을 도킹 폼에 도킹하고 나면 그림 6.31과 같은 상황이 발생한다: 디자이너에서와 같이 원본 폼도 표시되어 차이를 보여준다. | |||
[[image:lazarus_6.31.png|none|443px|thumb|그림 6.31: TPageControl 의 도킹관리자]] | |||
표준 도킹 클래스가 도킹 지점에 도킹된 컨트롤을 이용해 특별한 일을 하는 것은 아니다. 하지만 ldocktree 유닛은 확장된 도킹 관리자 클래스를 구현한다. 이는 자동으로 DefaultDockManagerClass 로서 설치되어 훨씬 더 많은 기능을 제공한다. | |||
물론 다른 도킹 관리자들을 설치하는 것도 가능하다. 라자루스 소스 트리의 examples/dockmanager directory는 다른 여러 효과들을 구현하는 도킹 관리자의 구현을 포함하며, 차이를 표시하기 위해 다른 컴파일러 방침에 따라 컴파일할 수 있는 데모 프로그램이 함께 제공된다. | |||
또한 라자루스 IDE 자체에 도킹 지원을 추가하는 예제도 포함한다. 도킹을 사용할 경우-그리고 오늘날 사용자 인터페이스를 구축하는 데 필요한 필수 기능일 경우-이 예제들을 살펴보는 것이 좋으며, 도킹과 도킹 관리자의 구현과 관련해 많은 것을 배울 수 있을 것이다. | |||
Latest revision as of 03:49, 8 March 2013
LCL (라자루스 컴포넌트 라이브러리)
LCL은 엄청난 수의 클래스를 포함하기 때문에 전체 클래스 트리(tree)를 그리자면 여러 장의 A3 용지를 인쇄해야 할 것이다. 하지만 트리의 최상단에 5가지 기본적인 클래스가 있는데, 이는 모든 라자루스 GUI 애플리케이션에 나타난다. 이에 해당하는 클래스를 아래 그림에 소개하겠다:
그림에서 메인 클래스는 다음과 같다:
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.
실행되어야 하는 작업에 따라 (Option1 또는 Option2) 적절한 서브루틴이 실행된다. 표준 파스칼은 이러한 유형의 프로그래밍에 적절하며, 오브젝트 파스칼도 그에 못지않다.
GUI 프로그램은 완전히 다른 방식으로 처리된다. 프로그램 내 액션은 전적으로 사용자가 제어하며, 프로그램의 어떤 부분을 활성화할 것인지에 대한 결정도 사용자가 애플리케이션을 구성하는 다양한 컨트롤을 클릭하여 이루어진다. 사용자 액션은 윈도잉 시스템(windowing system)에 의해 프로그램으로 전달된다. 윈도잉 시스템은 GUI 시스템에 따라 다른 메커니즘을 이용하여 프로그램으로 메시지를 전달한다ㅡ이 점에서 Windows는 X-Windows와 다르게 작동한다.
애플리케이션 객체
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;
이것은 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 탭을 이용해 자동 생성하고 싶은 폼을 명시할 수 있다. 자동으로 생성되지 않는 폼들도 물론 원한다면 사용 전에 수동으로 생성하면 된다. 사용 전에 생성하지 못한 경우 런타임 오류를 야기할 수 있는데, 그 폼의 클래스와 관련된 폼 변수가 유효하지 않기 때문일 것이다.
모든 폼이 생성되고 나면 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 호출은 윈도잉 시스템에 프로그램에 관한 메시지가 있는지 검사하고, 메시지가 발견되면 메시지의 대상이 되는 컨트롤로 그 메시지를 전송한다. 메시지를 수신하는 컨트롤은 두 가지 선택권을 가진다:
- 메시지를 완전히 처리할 수 있다. 예를 들어, paint 메시지는 컨트롤 스스로 처리한다. 각 컨트롤은 어떻게 그림을 그리는지 알고 있으므로 메시지를 어떻게 할 것인지 알 것이다.
- 추가 액션이 필요하다. 버튼이 클릭되는 경우를 예로 들 수 있다. 버튼은 클릭하면 무엇을 해야 할지 모르기 때문에 버튼을 클릭 시 실행되어야 하는 애플리케이션 로직(logic)의 제공 여부는 프래그래머에게 달려 있다.
물론 첫 번째 상황은 애플리케이션 프로그래머와 무관하다. 두 번째, 추가 액션이 필요한 상황은 아래 절에서 다루겠다.
이벤트 핸들러 사용하기: 절차적 타입의 프로퍼티
보통 이벤트는 절차적 타입의 프로퍼티 혹은 변수이며, 변수가 설정되면 그것이 가리키는 프로시저가 실행될 것이다. LCL 에 걸쳐 사용되는 기본 절차적 타입은 TNotifyEvent 이다:
TNotifyEvent = Procedure(Sender : TObject) of object;
모든 이벤트 핸들러는 폼 클래스의 메소드로서 구현되기 때문에 위는 메소드 포인터다. TNotifyEvent 는 단일 파라미터, Sender를 가진다. 이벤트 핸들러가 호출될 때 Send는 이벤트를 트리거한 인스턴스를 가리키는 포인터를 포함할 것이다. 이는 TObject 타입이므로, 어떤 컴포넌트에서든 사용할 수 있겠다.
다행히 이벤트 핸들러를 생성하고 필요 변수 또는 프로퍼티로 연결하는 일은 IDE가 자동으로 실행하기 때문에 따로 할 필요가 없다. 오브젝트 인스펙터의 Events 탭은 그림 6.7과 같이, 현재 선택된 컴포넌트에 가능한 모든 published 이벤트를 열거한다.
리스트에서 이벤트를 하나 선택할 때는 두 가지 선택권이 있다:
첫 번째는 기존 이벤트 핸들러를 선택하는 것이다. 예를 들어, 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;
또 다른 선택 방법은, 새 이벤트 핸들러를 생성하는 것이다. 줄임 부호 버튼을 클릭하면 라자루스에서 제시한 이름으로ㅡ컴포넌트 이름 뒤에 이벤트 이름이 붙은ㅡ새 이벤트 핸들러를 생성할 것이다. 사용자는 자신이 선호하는 이름을 먼저 입력함으로써 자동 명명을 피한 후 줄임 부호 버튼을 클릭한다.
컴포넌트 이름과 같이 이벤트 핸들러 이름도 폼의 메소드를 위해 사용되므로, 유효한 파스칼 식별자여야 한다.
호출된 버튼 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에서 찾을 수 있는 것과 동일한 효과를 얻기 위해선 아래를 실행할 수 있다.
- 새 애플리케이션을 생성하라. IDE는 Form1이라 불리는 메인 폼을 생성할 것이다.
- 파일 메뉴를 통해 두 번째 폼을 생성하라. Form2이라 불리고, unit2라는 유닛에 위치할 것이다.
- 메인 폼의 (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로 리턴되는 경우, 폼을 닫을 수 있다. 폼이 닫히더라도 여러 가능성이 있다:
- 폼을 단순히 표시되지 않도록 만들었으나 여전히 메모리에 상주한다.
- 폼이 최소화되었다.
- 폼을 메모리에서 제거해야 한다.
예를 들어, 보조 창을 닫을 때에는 메모리에 유지시켜 다음에 원할 때 표시하도록 만들 수 있다.
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;
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을 참고한다.
이러한 작은 프로그램의 결과를 아래 그림에 실어보고자 한다:
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;
프로그램을 실행하면 아래와 비슷한 화면이 뜰 것이다:
값(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에서 볼 수 있다:
특수 창
프로그래머는 모든 폼을 설계할 필요 없이 LCL 에 이미 포함된 단순한 폼을 유용하게 이용할 수 있다. 라자루스에서 동적으로 생성한 단순한 폼들도 여러 개 있으며, 그 기능은 소수의 간단한 함수에 래핑(wrapped)된다. 기본적으로 두 종류의 단순한 폼이 제공된다:
- 메시지와 아이콘이 표시된 단순한 폼. 사용자는 해당 폼의 닫기만 실행할 수 있다.
- 메시지와 아이콘, 표준 버튼 집합이 표시된 위의 변형(variation).
- 사용자가 타이핑한 값을 입력할 수 있는 단순한 폼 (또는 대화창 취소가 가능한).
LCL 은 이러한 폼들을 미리 설계하여 제공하므로 위와 같은 단순한 폼들을 사용자가 따로 설계할 필요는 없다. 이와 관련된 내용은 아래 여러 단락에 걸쳐 논하겠다.
운영체제들은 이미 만들어진 대화창도 제공한다: 파일과 디렉터리 선택 대화창; 프린터 선택 및 설치 대화창; 찾기 및 바꾸기 대화창. 이러한 대화창들은 복잡하며 많은 옵션을 포함한다: 단순한 함수에 포함하기엔 너무 많다. 따라서 폼에 드롭할 수 있는 컴포넌트로서 이용할 수 있어야 하며, 그 옵션은 프로퍼티에 래핑된다. 이러한 대화창 컴포넌트들은 컴포넌트 관련 장에서 상세히 다루고 있다.
ShowMessage
ShowMessage는 LCL 의 이미 만들어진 창 중에 가장 단순한 형태이다. 이는 메시지와 대화창을 닫는 OK 버튼을 표시한다. 그 선언은 매우 간단하다:
Procedure ShowMessage(Const aMsg : String);
사용자가 OK 버튼을 클릭하면 함수가 리턴한다. 다른 상호작용은 전혀 불가능하며, 액션이 성공적으로 실행되었는지 (아닌지) 사용자에게 알려주는 기능만 한다. 아래 그림은 메시지 대화창이 활성화된 모습이다:
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 함수를 실행한 모습이다:
다시 말하지만 기본 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 버튼을 제공하는 대신 질문을 바꾸어 말하고, 닫기 버튼에 자체에 대한 캡션처럼 질문에 대한 적절한 응답을 제시하는 편이 낫다. 사용자는 종종 아래와 같은 대화창을 볼 수 있을 것이다:
사용자는 생각하기도 전에 습관적으로 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 이 리턴된다.
실행되면 아래와 같은 모습을 할 것이다:
이와 같은 경고 대화창을 생성하는 데에는 다음과 같은 코드가 사용된다:
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;
해당 코드의 결과는 아래와 같다:
때로는 비밀 번호를 요청해야하는 경우도 있을 것이다.
따라서 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;
이 코드의 결과를 아래 그림에 나타내보겠다:
폼에 내용 추가하기: 컨트롤
빈창이 있는 프로그램은 별로 흥미롭지 못한 것은 확실하다: 따라서 빈창은 컨트롤로 (위젯이라고도 알려짐) 채울 필요가 있겠다. 컨트롤은 시각적 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 참고).
라자루스는 컨트롤이 폼에 드롭될 때마다 각 컨트롤을 폼의 클래스 정의에 필드로서 추가함으로써 폼의 클래스 정의를 변경한다. 두 개의 라벨을 추가한 앞의 예제에서 폼 클래스는 아래와 같을 것이다:
{ TForm1 }
TForm1 = class(TForm)
Label1: TLabel;
Label2: TLabel;
private
{ private declarations }
public
{ public declarations }
end;
클래스 정의에서 필드 이름은 컨트롤의 실제 이름이라는 사실을 명심하라. 사용자는 오브젝트 인스펙터에서 Name 프로퍼티를 편집하여 컨트롤 이름을 변경할 수 있다. 컨트롤은 폼 클래스에 필드로서 정의되어 있기 때문에 Name 은 유효한 파스칼 식별자여야 한다 (예: 문자 또는 밑줄 표시 '_' 로 시작되어야 하고, 문자와 숫자로만 구성되어야 한다. 공백은 허용되지 않는다.) 각 컨트롤은 유일한 이름을 가져야 한다는 규칙도 준수한다.
오브젝트 인스펙터에서 컨트롤의 Name 을 변경하면 폼의 클래스 정의 내 Name 뿐만 아니라 이 컨트롤을 참조하는 폼의 메소드 내 코드에서도 Name 이 자동 변경될 것이다.
폼 인스턴스는 (TComponent 의 자손) 그 위에 드롭되는 각 컨트롤을 소유한다: 즉, 런타임 시에 폼이 생성되면 각 컨트롤의Owner 프로퍼티가 폼 인스턴스로 설정될 것이다. 따라서 사용자는 아래 두 가지 방법 중 하나를 이용해 폼 상의 어떤 컨트롤로든 접근할 수 있다.
- 폼의 클래스 선언에서 필드에 접근:현재 컨트롤로 접근하는 방법 중 가장 쉽다: 폼에서 이름 필드는 실제 컨트롤 인스턴스로의 참조를 포함한다.
Label1.Caption := 'Some caption';
- FindComponent ()를 이용해 이름을 검색:FindComponent는 TComponent 결과를 리턴하기 때문에 적절한 클래스로 타입캐스트되어야 한다. 런타임 시 수동으로 생성된 컴포넌트들의 경우 이 방법이 유일한 검색 방법이다.
(FindComopnent('Label1') as TLabel).Caption := 'Some caption';
부모 컨트롤
앞 절에서 설명하였듯, 모든 컨트롤은 폼이 소유한다.
간단히 말해, 폼이 파괴되면 그 모든 컨트롤들도 파괴됨을 의미한다.
이것은 컨트롤의 비시각적 계층구조, 소유자가 소유한(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)가 가능하다. 마우스를 클릭한 채로 놓지 않고 드래그를 하면 컨트롤의 초기 윤곽을 원하는 크기로 설계할 수 있다.
하지만 일부 컨트롤은 크기 설정이 불가할 것이다. 예를 들어, 메인 메뉴의 크기는 메뉴 항목의 수에 따라 결정되며, 메인 메뉴는 제목 표시줄 아래 폼의 상단 테두리에 맞춰 알아서 정렬된다.
컨트롤의 다중 인스턴스 추가하기
폼에 컨트롤을 드롭하자마자 컨트롤 추가에 사용한 컴포넌트 팔레트 툴바 버튼이 선택 해제될 것이다. 따라서 동일한 컨트롤을 다시 추가하고 싶다면 툴바의 버튼을 다시 클릭하기만 하면 된다. 이러한 행위는 의도적이라 할 수 있는데, 폼에서 연속으로 클릭하면 대부분은 새로 드롭된 컨트롤의 크기 또는 위치가 조정될 것이기 때문이다.
컴포넌트 팔레트에서 매번 폼의 인스턴스를 선택할 필요 없이 다중 인스턴스를 추가하는 방법에는 두 가지가 있다:
- 폼을 클릭할 때 [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를 참고한다:
이러한 레이아웃을 수동으로 얻으려면 폼의 아래와 오른쪽 테두리를 기준으로 버튼 위치에 대한 오프셋(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 으로 동시에 설정하는 컨트롤이 몇 개가 있음을 주목한다. 이러한 일이 발생할 경우, 첫 번째 컨트롤은 부모 컨트롤의 상단 끝에 붙어 있고, 두 번째는 첫 번째 컨트롤의 하단에 붙어 있다: 둘은 부모 컨트롤의 상단 가장자리에 겹친다.
위의 그림은 폼이 커지거나 줄어들면 함께 커지거나 줄어드는 메모를 표시하는데, 버튼은 하단 우측 모서리에 계속 위치한다. 이러한 폼은 디자이너에서 아래와 같은 방법으로 생성할 수 있다:
- 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]에 설정되어 있으므로 폼의 하단 우측 모서리를 기준으로 동일한 위치에 계속 유지되도록 보장한다.
라자루스는 앵커를 이용할 수 있는 추가 가능성을 제공한다. 컨트롤 간 구분 거리를 명시하여 컨트롤을 서로 고정하는 것도 가능하다. 이는 좀 더 복잡한 레이아웃에 유용하다. 옆에 라벨이 있는 편집 컨트롤(edit control)을 상상해보자. 캡션이 변경되면 라벨의 크기가 변경될 수 있다 (예: 다른 언어로 해석 시). 이 때 편집 컨트롤의 위치를 조정하여 그 라벨로부터 동일한 거리를 유지해야 한다.
이는 라벨과 두 개의 편집 컨트롤이 포함된 'anchors' 프로젝트에서 설명하고 있는데, 그림 6.24에서 볼 수 있듯이 두 컨트롤 중 하나는 라벨과 동일한 상단 위치에 있다.
두 번째 편집 컨트롤의 텍스트는 OnChange 이베트 내에서 라벨의 Caption으로 복사된다. 첫 번째 편집 컨트롤(edit)의 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 이벤트가 실행됨을 의미한다. 일부 표준 액션들은 잘 정의된 다양한 액션을 실행하여 (클립보드로 복사하기/자르기 등), 이벤트 핸들러를 필요로 하지 않지만 이를 명시할 수는 있다.
액션이 실행될 때 다음과 같은 연속 이벤트가 발생한다:
- 액션의 부분이 되는 액션리스트가 액션을 처리하라는 요청을 받는다.
- 액션리스트가 액션을 처리하지 않을 경우 애플리케이션이 액션을 처리하라는 요청을 받는다.
- 애플리케이션이 액션을 처리하지 않는다면 액션 스스로 실행을 시도한다ㅡ보통 자신의 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 로 일련의 컨트롤을 전달함으로써 실행된다. 아래 컨트롤이 전달된다:
- 최근 포커스된 컨트롤.
- 현재 폼.
- 폼에 표시되는 모든 컨트롤.
적절한 대상을 발견하자마자 검색이 중단된다. 대상을 발견한 직후에 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과 같은 모습을 할 것이다.
버튼은 포커스가 편집 컨트롤 또는 메모 컨트롤에 있을 때에만 활성화되며, 텍스트를 포함한다.
드래그 앤 드롭
객체와 파일의 드래그 앤 드롭(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;
여기서 볼 수 있듯이, 드래그 앤 드롭은 꽤 간단한 코드로 처리할 수 있다. 까다로운 루틴은 행을 실제로 이동시키는 루틴이다. 아래 스크린 샷은 프로그램을 실행시킨 모습이다:
커스텀 드래그 객체 생성하기
위의 코드에서 리스트상자는 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과 같다:
드래그 앤 드롭 동작의 설명을 마무리하기 위해 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의 프로퍼티 중 도킹에 가장 중요한 프로퍼티들 |
도킹 지점 구현의 예제로 실험을 해보면 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처럼 직사각형이 그려진다.
좀 더 상세한 피드백을 제공할 수 있고, 도킹 메커니즘을 미세하게 조정하여 도킹 피드백이 트리거되기 전에 마우스가 도킹 영역 안쪽으로 최소 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과 같은 상황이 발생한다: 디자이너에서와 같이 원본 폼도 표시되어 차이를 보여준다.
표준 도킹 클래스가 도킹 지점에 도킹된 컨트롤을 이용해 특별한 일을 하는 것은 아니다. 하지만 ldocktree 유닛은 확장된 도킹 관리자 클래스를 구현한다. 이는 자동으로 DefaultDockManagerClass 로서 설치되어 훨씬 더 많은 기능을 제공한다.
물론 다른 도킹 관리자들을 설치하는 것도 가능하다. 라자루스 소스 트리의 examples/dockmanager directory는 다른 여러 효과들을 구현하는 도킹 관리자의 구현을 포함하며, 차이를 표시하기 위해 다른 컴파일러 방침에 따라 컴파일할 수 있는 데모 프로그램이 함께 제공된다.
또한 라자루스 IDE 자체에 도킹 지원을 추가하는 예제도 포함한다. 도킹을 사용할 경우-그리고 오늘날 사용자 인터페이스를 구축하는 데 필요한 필수 기능일 경우-이 예제들을 살펴보는 것이 좋으며, 도킹과 도킹 관리자의 구현과 관련해 많은 것을 배울 수 있을 것이다.
Notes
- ↑ wikI에서는 code를 drag하는 상태의 하이라이팅을 지원하지 않는다. 위 코드의 맨 마지막 Object선언부터 해당되는 end까지가 원문에서는 회색영역이다