TheSpecUIframework:Chapter 07
- 고급 위젯
고급 위젯
Spec 의 일부 사용자 인터페이스 요소는, 예를 들어 버튼이나 라벨 등에 비해 고급 기능을 제공하므로 설정이 복잡합니다. 이 장에서는 4 가지 고급 위젯을 보여주고 어떻게 사용하고 구성 할 수 있는지를 보여줍니다. 먼저 텍스트 입력 위젯에 대해 토론한 다음에, 라디오 버튼과 탭을 처리하고 툴바와 팝업 메뉴로 마무리합니다.
TextModel
Spec 에서 사용자가 텍스트를 입력하거나 여러 줄 텍스트를 표시하는 것은 TextModel 을 사용하여 수행됩니다. 이제부터 TextModel 모델의 일반적인 구성 및 한 줄짜리 TextInputFieldModel 에서 제공하는 추가 기능에 대해 설명합니다.
수정할 수 없는 텍스트 입력 필드
TextModel 의 텍스트 필드는 기본적으로 편집이 가능하지만, UI 가 편집 불가능한 여러 줄의 텍스트를 표시하는 경우에도 유용합니다. 이를 위해 간단한 예를 들어, 다음과 같이 모델에 "disable"을 보냅니다.
| cm |
cm := TextModel new.
cm text: Object comment.
cm disable.
cm openWithSpec.
위의 예제는 Object 클래스의 클래스 주석을 편집 할 수 없는 방식으로 보여줍니다. 텍스트는 계속 선택, 복사 및 검색 할 수 있습니다.
노란색 삼각형을 제거하고, 각 편집 작업을 수행
TextModel 은 필드의 오른쪽 위에 노란색 삼각형을 배치해서, 텍스트를 편집 할 때 시각적 피드백을 제공합니다. 또한 위젯이 제거 된 경우, 위젯이 포함 된 창을 닫으면 확인 대화 상자가 사용자에게 표시됩니다. 이 'has been edited' 플래그는 모델에서 'aceept' 메시지를 호출하여 재설정됩니다. accept 메시지를 보내는 것은, 위젯이 닫혀있을 때 변경된 텍스트가 손실되지 않았는지 확인하도록 위젯에 지시하는 것을 의미하지만, 이것으로 사용자가 가능한 손실에 대해 통보받을 수 있는것은 아닙니다.
예를 들어, 아래 코드를 실행하고 UI 가 몇 개의 문자를 입력하면 열립니다. 3 초후 삼각형은 사라지며, 창 닫기는 사용자의 확인을 요구하지 않습니다.
| cm |
cm := TextModel new.
cm openWithSpec.
[3 seconds wait. cm accept] fork
accept 호츨에서 실제로 발생되는 것은 별도로 설정할 수 있습니다.
또한 각 키 입력시 자동으로 수락하도록 모델을 구성하여, 편집 플래그의 기능을 효과적으로 제거 할 수 있습니다. 이런 작동은 "autoAccept : true"를 보내서 수행됩니다. 기본 동작은 모델의 "text" 필드를 사용자 인터페이스에 있는 텍스트로 설정하는 것입니다. 이 동작을 추가하려면 모델에 acceptBlock: 메시지를 보내십시오. 이 메시지는 인수가 하나인 블록을 사용합니다. 블록의 인수는 사용자 인터페이스에있는 텍스트입니다.
다음 예제에서는 autoAccept 및 acceptBlock: 메시지를 결합해서, 텍스트를 편집 할 때마다 변형된 텍스트를 표시하는 텍스트 필드를 만드는 방법을 보여줍니다.
| cm |
cm := TextModel new.
cm autoAccept: true.
cm acceptBlock: [ :txt | GrowlMorph openWithContents: txt.].
cm openWithSpec.
키보드 단축키
텍스트 입력 필드에서는 복사 및 붙여넣기 동작 이전에 키보드 바로 가기를 제공해서 다른 작업을 제공할 필요가 경우가 있습니다. 이러한 사용자 지정 작업 정의는 bindKeyCombination:action: 메시지를 사용하여 수행됩니다.
키보드 단축키는 반드시 Command(MAC의 단축키)를 동반하는 변경자(modifier)와 결합되어야 할 필요는 없습니다. 예를 들어, 단순히 문자 x 또는 공백 문자가 되어도 상관은 없습니다. |
예를 들어, 아래 코드는 command-i를 누르면 텍스트에 대한 속성을 엽니다. 위젯에 입력 된 문자를 항상 유지하려면 "text" 에 자동수신(auto-accept)을 "true" 로 설정해야합니다.
| cm |
cm := TextModel new.
cm autoAccept: true.
cm bindKeyCombination: $i command toAction: [ cm text inspect ].
cm openWithSpec
bindKeyCombination:action: 메서드는 실제로 ComposableModel 에 정의되어 있으므로 모든 UI 위젯에서 사용할 수 있습니다. 다른 표준 위젯에서는 키보드 단축키가 아닌 키보드 작업을 묶는 데 사용됩니다. 예를 들어, ButtonModel 의 초기설정자(initializer)의 마지막 줄은, 다음과 같이 버튼 동작의 실행을 스페이스 바에 할당합니다:
self bindKeyCombination: Character space toAction: [ self action ].
단일행 입력 필드 추가기능
TextInputFieldModel 은 단일행 입력을위한 TextModel 의 하위 클래스입니다. Enter 키 또는 Return 키를 눌렀을때, 캐리지 리턴(carriage return)이 발생되는것이 아니라 필드의 승인(accept) 작업이 실행됩니다. 그러나 이 위젯은 여러 줄의 텍스트를 표시하는 것이 가능합니다(예 : 여러 줄 텍스트를 붙여 넣는 경우). 하지만 텍스트의 수직 공간이 부족한 상황이라면 스크롤바는 표시되지 않습니다.
또한 유용한 몇 가지 기능을 추가로 가지고 있습니다:
- 흐릿하게 나오는 텍스트(ghost text)는 ghostText: 메시지로 설정할 수 있습니다.
- 별표만 표시하는 암호 필드는 beEncrypted 메시지를 보내 지정합니다.
- Entry completion can be added by using the ==entryCompletion:== message. The argument must be an ==EntryCompletion== instance, and we refer to that class for examples.
- 입력 완료는 entryCompletion: 메시지를 사용하여 추가 할 수 있습니다. 인수는 EntryCompletion 인스턴스가 되어야하며, 예제에서는 해당 클래스를 참조합니다.
- 입력 필드에 acceptOnCR:false 를 보내면 Enter 또는 Return 키를 눌러도 승인(accept) 작업은 실행되지 않으며, 결과적으로 사용자는 여러 줄의 텍스트를 입력 할 수 있게 됩니다.
RadioButtonModel
라디오 버튼을 사용하면 그룹에서 최대 하나의 옵션만을 선택하게 할 수 있으며, 드롭 다운 메뉴와는 다르게 이 그룹의 모든 항목이 화면에 표시됩니다. Spec 의 RadioButtonModel 은 RadioButtonGroup 을 사용하여 그룹을 관리합니다.
UI 예제로서 그림 7-1 에 표시된 기본적인 세탁기 제어 패널을 보여 드리겠습니다. 라디오 버튼에는 두 개의 그룹이 있습니다. 하나는 직물의 종류를 위한 것이고 다른 하나는 물의 온도(섭씨)를 위한 것입니다. 마지막으로 여분의 헹굼주기를 선택할 수 있는 라디오 버튼이 하나 더 있습니다.
UI 의 코드는 RadioButtonExample 클래스에 있으며, 이 클래스의 정의 및 레이아웃은 간단합니다:
ComposableModel subclass: #RadioButtonExample
instanceVariableNames: 'rinse f1 f2 f3 t1 t2 t3'
classVariableNames: ''
package: 'Spec-BuildUIWithSpec'
RadioButtonExample class >> defaultSpec
^SpecColumnLayout composed
newRow: [:r |
r newColumn: [:c | c add: #f1 ; add: #f2 ; add: #f3 ];
newColumn: [:c | c add: #t1 ; add: #t2 ; add: #t3 ]];
newRow: [:r | r add: #rinse ];
yourself
RadioButtonExample >> extent
^160@150
initializeWidgets 메소드에서 fabric 에 대한 라디오 버튼인 f1 부터 f3 을 fabric 버튼 그룹에 추가하고, 온도 버튼 t1 에서 t3 까지를 temperature 버튼 그룹으로 설정합니다. 이러한 버튼 그룹은 UI 에서 사용되는 것은 때문에, 클래스의 인스턴스 변수가 아닌 메서드의 로컬 변수로 사용됩니다.
아래의 코드에서 예로 든 방법을 찾을 수 있습니다. 먼저 두개의 버튼 그룹을 만든 다음 rinse 버튼을 만든 다음에, 다른 temperature 및 fabric 버튼을 만들어서 그룹으로 추가합니다. rinse 버튼은 선택 취소되도록 구성되며, f1 및 t1 버튼은 해당 그룹의 기본 버튼으로서, UI 가 열릴 때 선택되게 됩니다. 보다 명확하게 알아보기 위해서는, 더 의미가 있는 변수 이름을 사용하는 것이 좋습니다.
RadioButtonExample >> initializeWidgets
| fabric temperature |
fabric := RadioButtonGroup new.
temperature := RadioButtonGroup new.
rinse := self newRadioButton.
rinse label: 'Rinse Extra';
canDeselectByClick: true.
f1 := self newRadioButton.
f1 label: 'Cotton'.
fabric addRadioButton: f1; default: f1.
f2 := self newRadioButton.
f2 label: 'Synthetic'.
fabric addRadioButton: f2.
f3 := self newRadioButton.
f3 label: 'Delicate'.
fabric addRadioButton: f3.
t1 := self newRadioButton.
t1 label: '60'.
temperature addRadioButton: t1; default: t1.
t2 := self newRadioButton.
t2 label: '40'.
temperature addRadioButton: t2.
t3 := self newRadioButton.
t3 label: '30'.
temperature addRadioButton: t3
마지막으로 서로 다른 fabric 버튼을 클릭 할 때, 발생 되어야 하는 로직을 포함합니다. Synthetic 버튼을 선택하면, 60 도로 설정된 온도는 40 도로 낮아집니다. Delicate 버튼을 선택하면 온도는 30 도로 낮아지며, 다른 fabric 버튼은 사용할 수 없게 된다는 것을 그림 7-1 에서 확인할 수 있습니다. 반대로 다른 버튼을 눌러서 Delicate 버튼이 선택되지 않은 상황이라면, 다른 fabric 버튼은 다시 활성화됩니다.
RadioButtonExample >> initializePresenter
f2 activationAction: [ t1 state ifTrue: [ t2 state: true ] ].
f3 activationAction: [
t1 disable; state: false.
t2 disable; state: false.
t3 state: true ].
f3 deactivationAction: [ t1 enable. t2 enable ]
이 기능을 통해 "RadioButtonModel"및 "RadioButtonGroup"의 주요 기능을 시연 해 보았습니다. 이 클래스의 추가 기능에 대해서는 구현된(implementation) 소스 코드를 참조하십시오.
TabModel
위젯들이 같이 작동하도록 만들어진 두 번째 클래스 집합은 TabManagerModel 및 TabModel 클래스입니다. 탭이있는 UI 에서 TabManagerModel 은 다른 탭의 구성을 가지고 있으며, TabModel 은 탭 자체를 나타냅니다. 두개의 클래스가 어떻게 협력하는지 보여주기 위해서, 그림 7-2 처럼 섹션 7-2 의 세탁기 예제를 세탁 건조기로 확장해 보겠습니다. 이 예제는 탭이 있는 UI 를 가지고 있으며, 하나의 탭 은 세척 부분을 위한 것이고, 두 번째 탭은 스핀 사이클 및 건조이며, 세 번째 탭은 정보를 표시하게 됩니다.
탭을 사용하려면, 오직 TabManagerModel 만 UI의 인스턴스 변수로 유지하며 레이아웃에 추가해야합니다.
ComposableModel subclass: #TabMgrExample
instanceVariableNames: 'tabmgr'
classVariableNames: ''
package: 'Spec-BuildUIWithSpec'
TabMgrExample >> extent
^250@160
TabMgrExample class >> defaultSpec
^SpecLayout composed
add: #tabmgr;
yourself
탭을 표시하려면, 다음에 보이는것 처럼 addTab: 메시지를 사용해서 탭 관리자에 추가해야 합니다.
TabMgrExample >> initializeWidgets
| tab |
tabmgr := self newTabManager.
tab := self newTab.
tab model: RadioButtonExample new.
tab label: 'Wash'; closeable: false;
icon: Smalltalk ui icons smallScreenshot.
tabmgr addTab: tab.
TabManagerModel 을 만든 뒤에, 코드는 TabModel 을 만들고 RadioButtonExample 클래스의 인스턴스를 표시하도록 구성합니다. model: 메시지는 Spec 의 UI 재사용 원칙에 따라 ComposableModel 을 사용합니다. 마지막으로 탭의 추가 속성을 설정해서 레이블, 아이콘 및 탭을 닫을 수 없도록 만듭니다.
지금까지가, UI 의 첫 번째 버전을 가지고 7-2 절의 UI 를 탭으로 보여주는 데 필요한 것입니다.
TabManagerModel 은 동적이며, 새 탭을 추가하고 UI 가 열려있을 때는 탭을 제거 할 수도 있습니다. 이렇게 하려면 addTab: 및 removeTab: 메시지를 TabManagerModel 에 보내서, 인수로 탭을 추가하고 없애면 됩니다. 닫을 수 있는 탭에는 사용자가 UI 에서 탭을 제거 할 수 있는 닫기 버튼이 있습니다.
사용자 인터페이스의 두 번째 부분을 추가해서, 옷을 건조시키기 위해 다음과 같이 "initializeWidgets"를 확장합니다:
TabMgrExample >> initializeWidgets
[...]
tab := self newTab.
tab model: (self dryModel).
tab label: 'Dry'; closeable: false;
icon: Smalltalk ui icons smallNew.
tabmgr addTab: tab.
이 탭은 일반적인 경우에, UI 의 각 탭에 대한 UI 클래스가 있음을 의미하는 ComposableModel 의 인스턴스로 구성되어야 합니다.
하지만 간단한 UI 탭을 위해서 클래스를 생성하는것은, 너무 무거울 수도 있습니다. 때문에, 동적 Spec(8 장 참조)을 사용해서 탭의 내용을 만드는 것도 가능합니다.
예를 들어, dryModel 의 코드는 DynamicComposableModel 을 구성해서 두 개의 슬라이더를 표시합니다. 첫 번째는 회전주기(spin cycle)의 최대 속도를 선택하며, 두 번째는 건조주기(drying cycle)가 지속되는 시간을 선택합니다.
TabMgrExample >> dryModel
| model |
model := DynamicComposableModel new.
model instantiateModels: #(spin SliderModel dry SliderModel).
model spin label: 'Spin speed'; min: 400; max: 1600; quantum: 400.
model dry label: 'Dry time'; min: 0; max: 120; quantum: 10.
model layout: (
SpecColumnLayout composed
add: #spin height: 30; add: #dry height: 30;
yourself).
^model.
정말 단순한 UI 가 아니라면 DynamicComposableModel 은 사용하지 않는 것이 좋습니다. DynamicComposableModel 의 사용에 대한 더 많은 참고 사항은 8.3 절에 있습니다. |
UI 구성의 의 마지막으로 info 탭이 있습니다. 이 탭을 사용하면 TabManager 가 선택한 탭을 가져 오는 기능을 보여주고, 탭 선택에 대한 작업을 수행 할 수 있습니다. info 탭은 인스턴스 변수로 유지되며, 탭을 생성하기 전에 초기화하는 status 텍스트 필드를 포함합니다.
ComposableModel subclass: #TabMgrExample
instanceVariableNames: 'tabmgr status'
classVariableNames: ''
package: 'Spec-BuildUIWithSpec'
TabMgrExample >> initializeWidgets
[...]
self createStatus.
tab := self newTab.
tab model: status.
tab label: 'Info'; closeable: false;
icon: Smalltalk ui icons smallInfo.
tabmgr addTab: tab.
createStatus 메서드에서, TextModel 을 인스턴스화하고, 사용자가 그것을 편집할 수 없게 하며, 초기 텍스트를 설정합니다.
TabMgrExample >> createStatus
status := TextModel new.
status disable;
text: 'Welcome to Washing Machine 2.0!\History: Wash ' withCRs.
마지막으로 initializePresenter 에서 탭을 선택하면, 현재 선택된 탭의 레이블이 상태 텍스트로 추가됩니다. 이 작업은 탐색기록을 생성하며, 세탁 건조기의 UI 를 완성합니다.
TabMgrExample >> initializePresenter
tabmgr whenTabSelected: [
status text: (String streamContents: [:s |
s nextPutAll: status text.
s nextPutAll: ' > '.
s nextPutAll: tabmgr selectedTab label])].
툴바와 팝업메뉴
Spec 의 툴바와 팝업 메뉴는 세가지 클래스 간의 공동 작업 결과입니다:
- 다른 MenuGroupModel 인스턴스를 포함한 MenuModel 및 그 인스턴스를 구분자(splitter)로 나누어 표시.
- 여러개의 MenuGroupModel 인스턴스 각각은, MenuItemModel 인스턴스를 포함.
- 특정한 외형 및 동작을 가지는 메뉴 항목을 나타내는 다양한 MenuItemModel인스턴스.
현재, Spec 은 UI 의 연관(contextual) 메뉴를 직접 지원하지 않습니다. 하지만 UI 에 툴바를 추가하는 것은 레이아웃에 추가되는 다른 위젯 일 뿐이므로 쉽습니다. 예를 들어, Pharo 5 의 표준인 WatchpointWindow 클래스를 생각해 보십시오. 이 클래스는 menu 인스턴스 변수에 포함 된 메뉴를 가지며, 레이아웃 메소드는 툴바를 위젯 열의 첫 번째 위젯으로 배치합니다:
WatchpointWindow >> defaultSpec
^ SpecColumnLayout composed
add: #menu height: self toolbarHeight;
add: #list;
add: #inspectIt height: self toolbarHeight
MenuModel 클래스는 프로그래머가 인스턴스화 해야하는 유일한 클래스입니다. 그룹을 만들려면 블록을 인수로 사용해서 MenuModel 에 addGroup: 을 보냅니다. 이 작업을 통해 그룹이 만들어져 메뉴에 추가됩니다. 블록은 생성된 그룹의 인스턴스 하나만 인수로 받습니다. 마찬가지로 addItem: 을 MenuGroupModel 에 보내면, 새로운 MenuItemModel 이 만들어지며 그룹에 추가됩니다. addItem: 의 인수는 1 인수 블록이며, 이 인수는 생성된 메뉴 항목입니다. 이 설명은 조금 복잡합니다만, 결과 코드는 다음 예제처럼 매우 읽기 쉽습니다.
MenuItemModel 은 다음과 같은 기본 설정 메서드를 제공합니다:
- name: 메뉴 항목의 텍스트를 설정.
- icon: 아이콘을 설정.
- description: 툴팁을 추가.
- shortcut: 항목에 대한 shortcut 을 추가.
- action: 항목이 선택 될 때 실행할 블록을 추가.
- subMenu: 이 아이템에 대응하는 하위 메뉴를 나타내는 MenuModel 을 포함
MenuItemModel 은 도구 모음 및 메뉴 항목으로 사용되기 때문에, 텍스트없이 또는 아이콘없이 메뉴 항목을 만들 수 있으며, 툴팁은 툴바 및 메뉴 항목으로 사용될 때 어느 경우에도 작동합니다. 마지막 두 가지 기본설정 옵션이 있는데, 동작을 실행하거나 하위 메뉴를 여는 것은 상호 배타적이지 않습니다. 예를 들어 하위 메뉴가 있는 항목 위로 마우스를 가져 가면 하위 메뉴가 열리기는 하지만, 여전히 메뉴 항목 자체를 선택할 수 있습니다.
메뉴 구성을 보여주기 위해서, 이전 섹션의 세탁기 예제를 확장해 보겠습니다. 기술자가 기계를 문제 해결하거나 수리할 때 사용할 메뉴가 추가되었습니다. 이렇게 하기 위해서, menu 인스턴스 변수와 접근자를 populateMenu 메소드와 TabMgrExample 클래스에 추가합니다. initializeWidgets 메소드에는 self populateMenu 행이 추가되며 이 메소드의 코드는 다음과 같습니다:
TabMgrExample >> populateMenu
| submenu |
menu := MenuModel new.
submenu := MenuModel new.
submenu addGroup: [ :group |
group addItem: [ :item |
item name: 'Soft Reset';
action: [ status text: 'History: Wash '.];
icon: Smalltalk ui icons exception ].
group addItem: [ :item|
item name: 'Hard Reset';
action: [ GrowlMorph openWithContents: 'Just pull the plug!' ];
icon: Smalltalk ui icons smallError ] ].
submenu 라는 임시 변수를 정의한 후, 위의 코드는 menu 및 submenu 변수를 새로운 MenuModel 인스턴스로 초기화 합니다. 예를 들어, 코드는 우선 하위 메뉴를 구성해서, 소프트 및 하드 리셋 메뉴 항목에 각각의 아이콘과 작업을 추가합니다. 이 메뉴 항목의 코드는 간단합니다(GrowlMorph 는 우리의 세탁기에서도 작동한다고 가정하겠습니다).
이제 메인 메뉴의 정의를 계속하겠습니다. 이전에 말했듯이, 마우스 오른쪽 버튼을 클릭 할 때 나타나는 팝업 메뉴를 만드는 쉬운 방법은 없습니다. buildWithSpecAsPopup popUpInWorld 를 MenuModel 인스턴스로 보내면 pop-up menu 를 표시 할 수 있지만, 이 코드를 마우스 오른쪽 버튼으로 클릭하게 하는 것은 간단하지 않습니다.
여기에서 사용하는 해결 방법은, 메뉴가 선택 될 때에 대한 팝업 메뉴 항목을 정의하는 것입니다. 이런 처리는 메뉴가 보이지 않을 때 메뉴 항목을 선택할 수 있는 방법이 없기때문에, 마치 닭과 달걀의 문제처럼 보일수도 있습니다. 하지만 그렇지는 않습니다. 왜냐하면 메뉴 항목에 바로 가기 키를 연결할 수 있기 때문에 바로 가기를 누르면 메뉴는 나타납니다.
아래 코드에서는 populateMenu 메서드를 계속 진행하고 주 메뉴를 채웁니다. 첫 번째 항목은 메뉴 공개에 대한 항목입니다. Windows 및 Linux 에서는 control-r 을 누르며, $r meta 키 조합으로 지정된대로, Mac 에서는 command-r 을 눌러서 발동됩니다. 이 메뉴 항목은 툴팁을 지정하는 방법도 보여줍니다. 그 외의 나머지 코드는 간단합니다.
menu addGroup: [ :group |
group addItem: [ :item |
item name: 'Reveal this menu';
action: [menu buildWithSpecAsPopup popUpInWorld];
description: 'This entry exits to have a shortcut for this menu.';
shortcut: $r meta ].
group addItem: [ :item |
item name: 'Status Info';
action: [ GrowlMorph openWithContents: tabmgr selectedTab label];
icon: Smalltalk ui icons help] ].
마지막으로 주 메뉴에 두 번째 그룹을 추가해서, 위의 두 항목과 아래 항목 사이에 구분자(splitter)를 표시합니다. 마지막 항목은 메서드의 시작 부분에서 만들어진 하위 메뉴를 보여줍니다.
menu addGroup: [ :group |
group addItem: [ :item |
item name: 'Actions';
subMenu: submenu ]].
menu applyTo: self.
위 메소드의 마지막 부분에서 applyTo: 메소드가 메뉴로 전송되어 정의된 바로 가기(shortcuts)가 TabMgrExample 위젯에 등록됩니다. 메뉴 항목의 바로 가기를 누르면, 해당 메뉴 항목의 동작이 시작(trigger)됩니다.
메뉴를 UI 에 연결하려 할 때 다른 방법이 필요한건 아닙니다. 예제에서는 툴바 위젯이 표시되지 않기 때문에, 레이아웃 메소드에 메뉴를 추가 할 필요는 없습니다. 하지만 도구 모음을 추가 할 경우, 이 섹션의 시작 부분에있는 WatchpointWindow 예제에서 처럼 menu 를 레이아웃에 추가해야합니다.
마지막으로 메뉴가 완전히 정적인 것만은 아닙니다. enabled:false 를 보내면 메뉴 항목을 비활성화 하게 되며, 반대의 경우라면 enabled:true 를 보내면 됩니다. 또한, 항목 및 그룹을 추가 및 제거해서 메뉴의 구조를 변경하는 것도 가능합니다. 위의 예제에서, 메뉴는 각 팝업에서 다시 작성되기 떄문에 변경 사항은 즉시 표시됩니다. 대조적으로, 툴바로 사용되는 경우에, 메뉴는 위젯이기 때문에 8 장에서 논의된 것처럼 UI 를 다시 만들어야 합니다.
결론
이 장에서는 고급 위젯을 구성하고 사용하는 방법을 살펴봤습니다. TextModel 과 그 하위 클래스인 TextInputFieldModel, RadioButtonModel, 그리고 그룹화 기능 및 TabModel 과 탭 관리자, 그리고 다양한 클래스들을 포함하는 MenuModel 등을 살펴봤습니다. 일부 예제에서는 동적 Spec 을 사용했으며, 이에 대한 자세한 내용은 다음 장에서 설명하겠습니다.