TheSpecUIframework:Chapter 08
- 동적 Spec
동적 Spec
지금까지는 정적인 사용자 인터페이스를 살펴봤습니다. UI 가 열리고 나서 위젯이 변경되지 않으며 UI 레이아웃도 변경되지 않습니다. 또한, UI 의 코드를 작성할 때 프로그래머는 정확히 얼만큼의, 어떤 위젯을 사용해야 하는지 알고 있습니다. 그렇지만, 사용자 인터페이스는 보다 동적이어야 할 필요가 있는 경우도 있습니다. 예를 들어, 파일 대화 상자는 현재 선택된 파일에 대한 미리보기 영역을 가질 수 있습니다. 파일이 텍스트인 경우 해당 내용은 텍스트 필드에 표시되며, 이미지인 경우에는 이미지가 표시됩니다. 또 다른 경우는, 개발시점에서 데이터 항목이 어떻게 표시 될지 결정할 수 없는 데이터 모음을 시각화하는 것입니다. 예를 들어, 족보(genealogy) 응용 프로그램에서 부모를 클릭하면 모든 자식에 대한 정보가 표시됩니다. 각 자식은 복잡하며 편집 가능한 위젯을 사용하여 표시됩니다.
Spec 의 동적 기능 덕분에 이러한 종류의 사용자 인터페이스를 지원할 수 있으며, 관련된 내용을 이제부터 설명하겠습니다. 먼저 이미 열려있는 UI 를 동적으로 변경하는 방법을 살펴보겠습니다. 그 다음에, UI 를 작성할 때 이를 하드 코딩하는 대신 UI 가 열리는 시점에서 위젯의 선택을 연기하는 방법을 보여드리겠습니다. 세번째로, 하나의 (큰)코드 내 에서 완전한 UI 를 스크립팅 하겠습니다.
이미 열려있는 UI를 동적으로 변경
Spec 의 첫 번째 동적 기능은 이미 열려있는 UI 의 레이아웃과 내용을 변경하는 것입니다. 이 작업은 needRebuild: 및 buildWithSpecLayout: 메소드를 사용하여 수행됩니다.
- needRebuild: 이 메서드는 전체 UI 를 처음부터 다시 작성하지 않아도 된다는 신호로 사용됩니다
- buildWithSpecLayout: 이 메서드는 UI 재작성을 시작시킵니다. 재작성되는 UI 는 인수로 주어진 SpecLayout 인스턴스에 지정된대로 위젯을 레이아웃하는 것으로 제한됩니다.
작동 예제
이 두 가지 방법을 사용하기 위해 다중 데이터 뷰어 예제 사용자 인터페이스를 만듭니다. 왼쪽에는 다양한 종류의 데이터 목록이 나와 있으며, 오른쪽에는 이 데이터를 가장 적절한 방식으로 보여주는 view 위젯이있다. 다중 데이터 뷰어는 가로형에서 세로형으로 레이아웃을 변경할 수 있으며, 변경한 경우에는 데이터 목록이 맨 위이고 뷰 위젯이 맨 아래에 있게 됩니다. 이런 변경작업은 버튼을 클릭하여 수행됩니다. 그림 8-1 에서 멀티 데이터 뷰어 및, 그림 8-2 에서 세로 모드에 선택한 항목이 있는걸 확인할 수 있습니다.
UI 를 열어서 변경하기 전에, 클래스 정의, 창 크기 및 제목 정의부터 시작해서 목록의 내용을 설정하는 기능등의 UI 를 설정하는 작업을 먼저 해야합니다.
ComposableModel subclass: #DynamicViewer
instanceVariableNames: 'list view button state'
classVariableNames: ''
package: 'Spec-BuildUIWithSpec'
DynamicViewer >> extent
^ 400@350
DynamicViewer >> title
^ 'Dynamic Multi-Data Viewer'
DynamicViewer >> items: aCollection
list items: aCollection
비슷하지만, initializeWidgets 메소드는 간단합니다. 목록에는 15 자로 축소된 printString 이 표시됩니다. UI 는 $h 문자에 state 를 설정하여 가로 레이아웃을 사용하고 있으며 레이블을 기본값으로 설정합니다(그림 8-1 참고).
DynamicViewer >> initializeWidgets
list := self newList.
list displayBlock: [ :item | item printString contractTo: 15 ].
button := self newButton.
button label: 'Change!'.
state := $h.
view := self defaultView.
DynamicViewer >> defaultView
| cm |
cm := self newLabel.
cm label: 'Select something from the list please.'.
^cm
윈도우의 레이아웃은 아래의 horizontalSpec 메소드에 의해 주어진다. 다시 말하지만, 여기서 놀랄만한 부분은 없습니다. <spec : #default> 프라그마(pragma) 덕분에 기본 레이아웃이 됩니다. (자세한 내용은 5 장 참조).
DynamicViewer class >> horizontalSpec
<spec: #default>
^ SpecColumnLayout composed
newRow: [ :r |
r newColumn: [:c | c add: #list] left: 0 right: 0.7.
r newColumn: [:c | c add: #view] left: 0.32 right: 0];
newRow: [ :r | r add: #button ] height: self toolbarHeight;
yourself.
이제부터, 이 UI 를 열고 표시할 항목의 목록을 제공 할 수 있습니다. 예를 들어, 아래 코드는 그림 8-1 과 같은 UI 를 생성합니다.
| viewer |
viewer := DynamicViewer new.
viewer openWithSpec.
viewer items: {
42 .
'Everberg' .
#thumbsUp asIcon .
#(SGO CDG ZYR BRU) .
(OrderedCollection withAllSubclasses
collect:[: cls | cls comment]) asArray}.
UI 레이아웃의 변경
'Change!' 버튼을 클릭하지만, 아직은 아무 것도 하지 않습니다. 무언가를 작동하게 하기위해, initializePresenter 에서 다음과 같이 버튼의 액션 블록을 정의해야 합니다. 이 액션 블록에서 먼저 $h 와 $v 사이의 상태를 적절하게 전환합니다. 두번째로, self needRebuild: false 구문을 사용해서 완전한 UI 가 처음부터 재구성 될 필요는 없다는 신호를 보냅니다. 덕분에, UI 의 다음 재빌드는 레이아웃 작업만 수행하는 것으로 제한되게 됩니다. 세 번째이자 마지막으로, 레이아웃 변경은 buildWithSpecLayout: 메시지에 의해 시동됩니다. 인수로 SpecLayout 을 가지게 되며, UI 의 레이아웃을 지정된 레이아웃으로 변경합니다.
DynamicViewer >> initializePresenter
button action: [
state := (state = $v) ifTrue: [ $h ] ifFalse: [ $v ].
self needRebuild: false.
self buildWithSpecLayout: self currentSpec.
].
UI 의 새로운 레이아웃은, 아래에 표시된 currentSpec 메소드에 의해 반환됩니다. state 에 저장된 값에 따라, horizontalSpec 또는 verticalSpec 메서드로 간단하게 위임합니다.
DynamicViewer >> currentSpec.
^ state = $v
ifTrue: [ self class verticalSpec ]
ifFalse: [ self class horizontalSpec ]
DynamicViewer class >> verticalSpec
^ SpecColumnLayout composed
newRow: [:r | r add: #list] top: 0 bottom: 0.7;
newRow: [:r | r add: #view] top: 0.32 bottom: 0.02;
newRow: [:r | r add: #button] height: self toolbarHeight;
yourself.
앞의 내용은, 뷰어의 레이아웃을 가로 모드에서 세로 모드로 전환 할 수 있도록 하기 위해 필요한 모든 내용입니다. 핵심은 self needRebuild: false 와 self buildWithSpecLayout: <XXX> 의 두 줄입니다. 나머지는 실제로 <XXX> 값을 정의하는 데 사용되는 장치입니다.
위젯을 다른것으로 교체하기
근본적으로, 위젯을 다른 것으로 바꾸는 것은 UI 의 레이아웃을 변경하는 특별한 경우에 해당될 뿐입니다. 사용 된 레이아웃은 실제로 동일 하며, 하나의 위젯 인스턴스가 먼저 다른 인스턴스로 대체됩니다.
이를 표시하기 위해서, view 위젯에 표시해서 목록의 선택된 항목을 시각화하는 코드를 구현합니다. 아래 코드와 같이 initializePresenter 에 새 코드를 추가하면 됩니다. 목록 선택 변경 블록은 앞에서 설명한 버튼 동작 블록과 동일한 레이아웃 동작을 수행합니다. 차이점은 새 위젯을 view 인스턴스 변수에 지정하는 부분입니다. 예를 들자면, UI 는 그림 8-1 처럼 Form 을 표시하게 됩니다.
DynamicViewer >> initializePresenter
button action: [
state := (state = $v) ifTrue: [ $h ] ifFalse: [ $v ].
self needRebuild: false.
self buildWithSpecLayout: self currentSpec.
].
list whenSelectedItemChanged: [ :new |
view := self widgetFor: new.
self needRebuild: false.
self buildWithSpecLayout: self currentSpec.
].
widgetFor: 메소드의 의무는, 선택된 아이템을 보여주는 정확한 위젯의 인스턴스를 반환하는 것입니다. 이 예제를 작게 유지하기 위해서, 기본적으로 선택된 항목의 유형에 대한 큰 switch 문을 근본적으로 구현하는, 객체지향적이지 않은 코드를 제공합니다. 깨끗한 객체 지향 구현은 이중-디스패치를 사용해야하며, 이것을 읽고 있는 당신에게 연습으로 남겨두겠습니다.
DynamicViewer >> widgetFor: aDatum
| cm |
aDatum isNil ifTrue: [ ^ self defaultView ].
aDatum isForm ifTrue: [
cm := self newImage.
cm image: aDatum.
^ cm ].
aDatum isArray ifTrue: [
cm := self newList.
cm items: aDatum.
^ cm ].
"default case"
cm := self newText.
cm text: aDatum asString.
^cm
widgetFor: 에 대한 코드는 좋은 객체지향의 사례가 아닙니다. 이 장의 문맥에서 우리는 예제를 자급자족[1] 하게 하기위해서, 못생긴 코드를 작성하기로 선택했습니다. 이와 같은 코드를 작성하는 학생이라면, 우리 사무실에 초대해서 긴 대화를 나눠야 할겁니다. |
UI 와 위젯을 이용해서 동적으로 채우기(populating)
UI 에 표시 될 위젯의 수를 언제나 미리 결정할 수있는 것은 아니며, 그렇다면 UI 를 열때 위젯을 결정해야 합니다. Spec 은 이런 방법을 지원합니다. 동적 채우기(Dynamic population)는 assign:to: 메서드를 사용하여 다른 위젯에 대한 가상 인스턴스 변수를 동적으로 만들고, 인스턴스인 경우라면 레이아웃을 정의하여 ComposableModel 대신 DynamicComposableModel 을 서브클래싱해서 수행됩니다.
다중 데이터 뷰어에 적용하기
이를 보여주기 위해, 이전 섹션의 동적 다중 데이터 뷰어를 확장해 보겠습니다. 그림 8-3 에서 보이는것 처럼, 목록보기 에서와는 다르게 배열의 내용을 다르게 표시합니다. 이 상세보기를 위해 새로운 DynamicArrayViewer 위젯을 만들예정이며, 이를 위해서 일단 DynamicViewer >> widgetFor: 구현을 변경해서 위젯을 반환하십시오:
DynamicViewer >> widgetFor: aDatum
| cm |
aDatum isNil ifTrue: [ ^ self defaultView ].
aDatum isForm ifTrue: [
cm := self newImage.
cm image: aDatum.
^ cm ].
aDatum isArray ifTrue: [
cm := DynamicArrayViewer on: aDatum.
cm owner: self. " needed because we do not use 'instantiate:' "
^ cm].
"default case"
cm := self newText.
cm text: aDatum asString.
^ cm
배열 내용을 보여주는 위젯 구현하기
이제부터 배열의 자세한 내용을 보여주는 위젯을 구현합니다. DynamicComposableModel 의 하위 클래스이며 배열을 보유하고있는 인스턴스 변수 collection 과, 맨 위에 레이블을 보유하고있는 label 이 있습니다.
DynamicComposableModel subclass: #DynamicArrayViewer
instanceVariableNames: 'collection label'
classVariableNames: ''
package: 'Spec-BuildUIWithSpec'
collection 인스턴스 변수는 다음과 같이 on: 메시지를 사용해서 위젯이 인스턴스화 될 때 초기화됩니다. initialize 메소드가 호출되기 전에 이 collection 변수를 초기화 해야하는데, 왜냐하면 initializeWidgets 에서 표시 할 항목을 결정해야 하기 때문입니다.
DynamicArrayViewer class >> on: aCollection
| inst |
inst := self basicNew.
inst collection: aCollection.
inst initialize.
^inst.
initializeWidgets 메서드는 컬렉션을 반복하고 각각의 항목에 대해 TextModel 을 인스턴스화 합니다. 인스턴스화 이후에, 다음에서 보이듯이 assign:to: 메서드를 사용해서 UI 에 추가됩니다.
assign:to: 는 ComposableModel 의 인스턴스와 해당 인스턴스를 나타내는데 사용되는 심볼을 사용합니다. Spec 은 (가상의)접근자 메소드와 함께 변수를 가상으로 만듭니다. 예를 들어, 다음의 코드는 txt_1:, txt_2: 처럼, 접근자 txt_1, txt_2 등을 생성합니다.
DynamicArrayViewer >> initializeWidgets
label := self newLabel.
label label: 'An array with ', collection size asString , ' elements:'.
1 to: collection size do: [ :count | | model |
model := self newText.
model text: (collection at: count) asString.
self assign: model to: ('txt_',count asString) asSymbol ]
이러한 가상 접근자를 생성하면, 마치 일반 변수인 것처럼 접근자를 통해 나머지 코드에서 위젯을 참조 할 수 있습니다. |
이제부터 해야할 일은 이 UI 의 위젯에 대한 레이아웃을 정의하는 것입니다. ComposableModel 의 클래스 측면 레이아웃 기능 외에도 DynamicComposableModel 을 사용하면 레이아웃을 인스턴스 측면에서 지정할 수도 있습니다. 필요한 SpecLayout 인스턴스를 반환하기 위해서, layout 메서드를 재정의하여 이 작업을 진행하거나, layout: 접근자를 사용하여 레이아웃을 설정해도 됩니다. 이 두 가지 옵션은 6 장에서 설명한 창 제목 및 크기에서 가능한 옵션과 유사합니다.
지금의 예제에서는, 위젯 수가 인스턴스마다 다르므로 인스턴스 측면에서 레이아웃을 정의해야 합니다. 여기서는 다음과 같이 layout 메서드를 오버라이드 하도록 선택합니다. 이 코드는, 열을 생성하며, initializeWidgets 에서 생성된 모든 위젯을 추가합니다. 동적으로 생성된 위젯은 컬렉션을 반복하여 재구성(reconstructed)된 이름인 txt_1, txt_2 등으로 참조됩니다. 하지만 이런 방법은 차선책이며, 다른 구현방법은 이 장의 끝에서 보여드리도록 하겠습니다.
DynamicArrayViewer >> layout
| col |
col := SpecColumnLayout composed.
col add: #label.
1 to: collection size do: [:count|
col add: ('txt_',count asString) asSymbol ].
^ col
이것이 UI 를 동적으로 채우는 데 필요한 전부입니다. 이 UI 는 ComposableModel 의 하위 클래스이므로 독립실행형으로 열 수도 있습니다. 예를 들어, 아래의 코드조각은 그림 8-3 처럼 열리게 됩니다.
(DynamicArrayViewer on:
(OrderedCollection withAllSubclasses
collect:[: cls | cls comment]))
openWithSpec.
initializeWidgets 의 대체 구현
지금까지의 내용에서, 위젯의 가상변수 이름을 만들 때 initializeWidgets 과 layout 이 두 번 작동된다는 것을 확인했습니다. layout 메서드를 재정의하는 대신에, 다음과 같이 initializeWidgets 내부에서 layout: 접근자를 사용한다면 두번 작동되는 문제를 피할 수 있습니다.:
DynamicArrayViewer >> initializeWidgets
| col |
col := SpecColumnLayout composed.
label := self newLabel.
label label: 'An array with ', collection size asString , ' elements:'.
col add: #label.
1 to: collection size do: [ :count | | model nam |
model := self newText.
model text: (collection at: count) asString.
nam := ('txt_',count asString) asSymbol.
self assign: model to: nam.
col add: nam ]
self layout: col.
이렇게 구현하게 되면 위젯이 만들어질때 레이아웃을 만들게 됩니다. 레이아웃이 생성된 직후에 각 위젯에 레이아웃의 참조를 추가합니다. 메소드가 끝나면, UI 의 레이아웃은 layout: 메서드를 사용해서 설정되기 때문에, 앞의 구현 에서처럼 layout 을 재정의 할 필요가 없습니다.
Playground 에서 UI 를 해킹하기
DynamicComposableModel 덕분에 Spec 은 스크립팅 같은 개발 스타일을 허용할 수 있으며, 전체 UI 는 대부분 점차적으로 구축 된 하나의 거대한 코드에 정의됩니다. 이 내용은, 여러 메서드의 정의 뿐만 아니라 UI 를 위한 클래스의 생성을 생략하고 싶은 "quick and dirty" 프로토 타이핑 작업에 유용합니다. 이 섹션에서는 Playground 안에서 간단한 UI, 스크립팅 스타일을 함께 해킹 할 수 있는 방법을 살펴보겠습니다.
여기서 만들어진 내용은, UI 는 숫자와 버튼을 표시하는 간단한 예입니다. 하나는 숫자를 증가시키고 하나는 감소시키기위한 것입니다. 이 예제는 Pharo 5 이미지에 있는 프로그램 코드에서 변형된 것입니다: DynamicSpecExample >> openOnInteger 를 작성한 Torsten Bergmann 에게 감사드립니다.
DynamicComposableModel 을 인스턴스화하는 것으로 예제 코드는 시작되며, title: 및 extent: 메서드를 사용해서 UI 의 제목과 크기를 설정합니다(5장 참조). 지역 변수 num 은 UI 에 표시된 숫자를 가지게 됩니다.
| ui num |
num := 0.
ui := DynamicComposableModel new.
ui title: 'I am dynamic'.
ui extent: 250@70.
그런 다음 instantiateModels: 메소드를 사용하여 위젯을 UI 에 추가합니다. 이 메소드는 인수로 한쌍의 콜렉션을 가지게 됩니다. 쌍의 첫 번째 요소는 위젯의 이름인데, 인스턴스 변수가 정상적인 정적 UI 인 경우라면, 이 인스턴스 변수의 이름이 됩니다. 쌍의 두 번째 요소는, 위젯의 유형을 결정하는 ComposableModel 의 하위 클래스입니다.
ui instantiateModels: #(
text LabelModel
plus ButtonModel
minus ButtonModel ).
그러므로 이 UI 는 LabelModel 인스턴스인 text 위젯과 plus 및 minus 의 두 개 ButtonModel 위젯을 각각 보유합니다. 근본적으로, 위의 코드는 앞에서 살펴봤던 내용과 비교했을때 유일한 차이점입니다. 이전 섹션에 표시된 assign:to: 메소드에서와 같이, Spec 은 위젯에 대한 접근자(가상)를 자동으로 생성하기 때문에, 나머지 코드에서도 접근자를 사용할 수 있기 때문입니다.
위젯을 정의하고 인스턴스화하면 일반적인 initializeWidgets 또는 initializePresenter 메소드에서 처럼 위젯을 구성 할 수 있습니다.
ui text label: num asString.
ui minus
label: '-';
state: false;
action: [
num := num -1.
ui text label: num asString ].
ui plus
label: '+';
state: false;
action: [
num := num +1.
ui text label: num asString ].
마지막으로 해야 할 일은, UI 에 레이아웃을 제공하는 것입니다. 이 작업은 레이아웃을 지정하는 SpecLayout 인스턴스를 전달하는 layout: 메서드를 호출하여 수행됩니다.
ui layout: (SpecLayout composed
newColumn: [ :c |
c
add: #text height: 25;
newRow: [ :r | r add: #minus ; addSplitter; add: #plus ] height: 25 ];
yourself).
ui openWithSpec.
이렇게 하면 Pharo 가 제공해야하는(예를들면 브라우저) 모든 코드 작성 및 관리 기능을 무시하게 되며, UI 에 대한 코드의 서로 다른 책임을 명확하게 구분하지 않습니다. 그리고 앞으로 서브클래싱을 통해 (프로그램을) 확장하는것이 힘들어집니다. 돌려 말하자면, 이런 방법은 사용자 인터페이스를 해킹하는 빠르고 쉬운 방법이지만 실제 프로그램 작성에 사용해서는 안되는 방법입니다. |
결론
이 장에서는 보다 동적인 사용자 인터페이스를 허용하는 Spec 의 기능에 대해 논의했습니다. Spec 이 제공하는 기능을 통해, 레이아웃 및 콘텐츠를 즉시 변경하는 것은 물론이거니와, 프로그램이 동작하는 시점에서 UI 의 위젯을 결정할 수도 있었습니다. 그리고 동적기능에 대한 설명으로 끝을 맺었습니다. 덕분에 하나의 커다란 코드 블록, 스크립팅 스타일로 UI 를 완벽하게 구성할 수 있었습니다.
Notes
- ↑ 다른 코드와 의존성을 가져가지 않게 하려는 의도