DeepintoPharo:Chapter 11
- 제 11 장 Roassal을 이용한 재빠른 시각화
Roassal을 이용한 재빠른 시각화
- Vanessa Peña-Araya 참여 (van.c.pena@gmail.com)
많은 양의 데이터에 의미를 부여하기란 적절한 툴이 없이는 힘들다. 텍스트 출력(textual output)은 그 표현의 유연성(expressiveness)과 상호작용의 지원에 제한이 있는 것으로 알려져 있다.
Roassal은 재빠른 시각화 엔진이다. Roassal은 객체나 그 관계와 관련해 정의된 임의의 데이터를 시각화하고 그것과 상호작용하기 위해 만들어졌다. Roassal은 보통 상호작용적 시각화를 생성하는 데 사용된다. Roassal을 이용한 애플리케이션의 범위는 다양하다. 예를 들어, Moose 공동체는 소프트웨어를 시각화하는 데 Roassal을 사용한다.
이번 장에서는 Roassal의 원리를 사용하고, 자신의 데이터를 빠르게 렌더링하기 위해 Rossal의 표현적인 API의 사용을 설명하겠다. 본 장을 마칠 즈음엔 상호작용적이고 시각적인 표현을 생성할 수 있을 것이다.
Roassal의 개발은 ESUG.org의 후원으로 이루어지고 있다. 자세한 정보는 Roassal 웹사이트를 방문하면 찾을 수 있다.
설치 및 첫 시각화
Roassal은 Moose 배포판에[1] 속한다. 따라서 어떠한 설치도 불필요하며, 첫 번째 시각화를 곧바로 진행할 수 있다.
Gofer와 Metacello 덕분에 Roassal를 새로운 Pharo 이미지에 설치하기란 쉽다. 워크스페이스를 열고 아래를 실행하면 된다.
Gofer new smalltalkhubUser: 'ObjectProfile'
project: 'Roassal';
package: 'ConfigurationOfRoassal';
load.
(Smalltalk at: #ConfigurationOfRoassal) load
Rossal은 Pharo 버전 1.4, 2.0, 3.0에서 실행되는 것으로 알려진다.
첫 시각화.
우리가 보일 첫 시각화는 Collection 클래스 계층구조를 나타낸다. 이는 각 클래스를 상자로 정의하고, 각 상자는 그 서브클래스로 연결된다. 각 상자는 표현하는 클래스의 인스턴스 변수 개수와 메서드 개수를 표시한다.
이러한 시각화를 만드는 방법을 이번 장에서 설명할 것이다. 다음 장에서는 Mondrian의 도메인 특정 언어(DSL)를 이용해 시각화를 생성하는 방법을 자세히 살펴볼 것인데, 그 과정에서 Roassal의 일부인 Mondrian 빌더를 이용할 것이다.
Roassal Easel
Roassal easel은 상호작용적으로 시각화를 스크립팅하기 위한 툴이다. easel을 이용하면 프로그래머가 easel에서 작업을 수행하는 painter가 되어 생성, 조정, 삭제는 몇 개의 (키) 누름으로 가능해진다.
Roassal easel은 Pharo World 메뉴에서 접근할 수 있다. R 아이콘[2]을 살펴보라.
Easel은 두 개의 독립된 창으로 구성되는데, 하나는 왼쪽 측면에 위치하여 오른쪽에 있는 텍스트 창에 적힌 스크립트를 렌더링한다. 에디터에서 수락을 (Cmd-s, Alt-s/오른쪽 마우스 클릭 후 accept를 누름) 통해 시각화가 업데이트될 것이다. 이는 시스템 브라우저에서 메서드를 수락 시 사용되는 키 누름과 같다. 이를 사용 시 장점은 피드백 루프가 짧다는 것으로, 스크립트의 의미는 항상 하나의 키 누름으로 가능하다.
시각화 창은 다수의 시각화 예제를 포함하는데, 단계별(step-by-step) 지침서도 이에 포함된다. 예제들은 두 가지 범주, ROExample과 ROMondrianExample로 나뉘며, 시각화 창의 윗부분에 examples 버튼을 클릭하여 접근할 수 있다.
ROMondrianExample 범주는 Roassal의 상단에 빌드된 도메인 특정 언어인 Modrian으로 생성된 예제들을 포함한다. 이 예제들은 주로 시각화를 만드는 데 ROMondrianViewBuilder 클래스를 사용한다. ROExample 범주는 Roassal을 직접 설명한다.
Roassal 코어 모델
각 시각화의 루트는 렌더링되어야 할 모든 그래픽 구성 요소에 대한 컨테이너 역할을 하는 ROView 클래스의 인스턴스다. 그러한 구성 요소들은 ROAbstractComponent의 서브클래스의 인스턴스로서, 주로 ROElement와 ROEdge의 인스턴스들이다. 주로 그래픽 구성 요소는 domain object에 대한 참조를 유지한다. 몇몇 시각적 프로퍼티들은 (크기 또는 색상) domain object로부터 직접 추론할 수 있다. 이와 관련된 점은 곧 다시 살펴보겠다. 현재로선 기본적인 연산을 먼저 설명하겠다.
요소 추가하기. 그래픽 구성 요소를 확인하기 위한 첫 번째 단계는 구성 요소를 뷰로 추가한 후 뷰를 여는 것이다. 다음의 코드 조각은 정확히 이러한 일을 수행한다.
view := ROView new.
element := ROElement new size: 100.
view add: element.
view open.
|
위 코드는 100 픽셀 크기의 사각형으로 된 단일 요소가 있는 시각화를 생성하여 연다. 하지만 이 코드를 실행하면 시각화에 어떤 변화도 발생하지 않는다. 요소가 효과적으로 뷰에 추가되었지만 어떻게 렌더링되어야 하는지는 요소에게 알려주지 않은 것이다.
Shape 추가하기. 요소의 시각적 측면은 ROShape의 서브클래스의 인스턴스인 shapes에 의해 주어진다. 기본적으로 모든 요소는 어떤 모양(shape)[3]도 갖고 있지 않다. 모양(테두리)을 요소로 추가해보자.
view := ROView new.
element := ROElement new size: 100.
element addShape: ROBorder. "added line"
view add: element.
view open.
|
요소로 모양을 추가하는 일은 추가하고자 하는 모양이 있는 addShape: 메시지를 추가하기만 하면 된다는 사실은 놀라운 일도 아니다. 자주 사용되는 연산이기 때문에 + 메시지를 이용해도 같은 효과를 얻을 수 있다. 아니면 element + ROBorder 를 작성할 수도 있겠다.
본 예제에서는 ROBorder 모양을 추가하였다. 이름에서 제시하듯 ROBorder는 ROElement로 사각형 모양의 테두리를 추가한다. 기본 값으로 ROBorder는 검정색이다. 다른 모양들도 이용 가능한데, 맞춤형 라벨, 원, 또는 안이 채워진 사각형이 포함된다. 그러한 모양은 정교한 시각적 측면을 생성하도록 구성되기도 한다. 모양에 대한 개념은 11.3절에서 더 상세히 다루겠다.
이벤트에 반응하기. 현재 우리가 생성한 하나의 요소가 할 수 있는 일은 많지 않다. 해당 요소로 하여금 클릭, 드래그 앤 드롭, 키 누름 등의 사용자 액션을 인식하게 만들기 위해선 이벤트 콜백을 명시할 필요가 있다.
대부분의 사용자 인터페이스 및 그래픽 프레임워크와 마찬가지로 사용자가 실행할 각 액션마다 이벤트를 생성한다. 그러한 이벤트는 ROEvent의 서브클래스의 인스턴스에 해당한다. 그래픽 요소가 이벤트에 반응하도록 만들기 위해선 블록을 이벤트 클래스로 연관시켜야 하고 그래픽 요소로 부착시켜야 한다.
가령, 예제의 사각형을 사용자 클릭에 반응하도록 만들기 위해선 이벤트 핸들러를 추가해야 하는데, 이벤트가 발생하면 실행될 블록을 예로 들 수 있겠다.
view := ROView new.
element := ROElement new size: 100.
element + ROBorder.
"Open an inspector when clicking"
element on: ROMouseClick do: [ :event | event inspect ].
view add: element.
view open.
이제 사각형을 클릭하면 인스펙터가 열릴 것이다. 한편 우리는 addShape: 보다는 + 메시지를 선호하는 편인데, 짧으면서도 충분한 정보를 제공하기 때문이다.
복잡한 응답을 위한 상호작용. 사용자 액션에 직접 응답하는 방법이 보통 그래픽 프레임워크에서 널리 사용되긴 하지만 복잡한 상황을 처리하기엔 너무 단순한 경우가 종종 있다. 마우스 버튼을 누른 채 마우스를 움직이면서 발생하는 드래그 앤 드롭을 고려해보자. 드래그 앤 드롭은 공통 연산(common operation)이지만 꽤 복잡하다. 예를 들어, 픽셀에서 마우스의 이동은 요소의 계획에 반영되어야 하고 시각화는 새로고침(refresh)되어야 한다. 이는 공통 연산이기 때문에 프로그래머가 element on: ROMouseDrag do: [ ... ] 와 같은 구조체를 굳이 사용하지 않아도 되도록 해준다.
대신 우리는 이벤트 핸들러의 재사용과 구성을 위한 가벼운 메커니즘으로 interactions를 제공한다. 이동이 불가한 요소를 드래그 가능하게 만들기 위해선 element @ RODraggable 을 이용하면 간단히 해결된다. @ 메서드는 addInteraction: 의 단축키다. 다른 상호작용(interactions)은 11.7 절에서 상세히 다루겠다.
RODraggable 은 Roassal의 모든 상호작용의 루트에 해당하는 ROInteraction의 서브클래스이다. RODraggable은 마우스 드래그에 대한 마우스의 반응을 허용한다. 따라서 우리가 제시한 작은 예제는 아래와 같이 재정의된다.
view := ROView new.
element := ROElement new size: 100.
element
+ ROBorder "-> add shape"
@ RODraggable. "-> add interaction"
view add: element.
view open.
추가 요소. 흥미로운 시각화는 많은 수의 요소를 포함하는 경향이 있다. 요소들은 ROView에서 add: 를 연속적으로 호출하여 하나씩 추가하거나, addAll: 를 한 번만 전송하여 한꺼번에 추가할 수 있다. 아래를 살펴보자.
위의 코드는 두 개의 사각형 요소가 있고 상단 좌측 모서리에 origin이 있는 창을 연다. 먼저 크기가 각각 50과 100 으로 된 요소를 두 개 생성하고, addAll: 메시지를 이용해 요소들을 뷰로 추가한다. 두 개의 요소는 테두리를 갖고 있으며 드래그 가능하게 만든다. 이 예제에서 모양과 상호작용은 뷰를 열기 전에 추가됨을 주목한다. 물론 뷰를 열고 난 후에 실행할 수도 있다. 그래픽 구성 요소들은 한 번만 추가하고 렌더링하면 마음껏 수정이 가능하다.
포인트를 매개변수로 하여 translateBy: 또는 translateTo: 를 전송하면 요소를 전환(translate)할 수 있다. 매개변수는 단계(step)나 위치를 픽셀로 표현한다. 축(axe)은 그림 11.2에서 표시하고 있는데, x-축은 좌측에서 우측 방향으로 증가하며, y-축은 위에서 아래 방향으로 증가한다.
시각화가 하나 이상의 요소를 포함할 경우 각 요소를 자동으로 위치시키는 알고리즘이 있다면 좋겠다. 그러한 알고리즘은 레이아웃(layout)이라 불린다. Roassal은 공간에 요소들을 위치시킴으로써 정렬시키는 레이아웃을 많이 제공한다. Roassal에서 레이아웃은 ROLayout의 서브클래스이다. 레이아웃은 11.5절에 설명되어 있다.
중첩 요소. ROElement 객체는 ROElement 요소를 포함할 수도 있다. 이러한 포함 관계를 중첩(nesting)이라 부른다. 중첩은 요소를 트리 모양으로 구조화할 수 있게 해준다. 뿐만 아니라 아래 예제에서 볼 수 있듯이 자식들의 위치가 부모의 위치에 비례한다. 즉, 우리가 부모 노드를 전환하면 자식 노드들도 전환될 것이란 의미다.
중첩 요소들은 default별로 연장이 가능하기 때문에 자식 노드를 전환할 때 그 부모의 범위(bound)는 새로운 위치에 해당 요소를 포함하도록 확장될 것이다.
각 요소는 resize(크기 조절) 전략을 갖고 있는데, 이는 resizeStrategy 인스턴스 변수에 보관된다. 기본적으로 resize 전략은 ROExtensibleParent의 인스턴스로서, 부모가 그 모든 자식 요소의 크기에 맞춰 자신의 범위를 확장시킬 것이란 의미다. 이용 가능한 resize 전략의 수는 많으며 ROAbstractResizeStrategy 클래스의 서브클래스를 살펴보면 되는데, 그 서브클래스 각각은 요소들이 사용하게 될 전략을 정의하기 때문이다.
지금까지 상호작용, 모양, 자식 요소를 소개하고, 객체 도메인(object domain)을 갖게 될 가능성을 간략하게 언급하였다. 표로 소개하자면 요소 표현은 그림 11.3과 같다.
뷰의 카메라 이동하기. 뷰는 translateBy: 와 translateTo: 메시지에 응답하기도 한다. 위치는 뷰가 변경하는 것처럼 보이지만 사실상 그 카메라가 변경하는 것이다. ROCamera의 인스턴스에 의해 표현되는 뷰의 카메라 구성 요소는 visualization 객체를 실제로 살펴보는 관점이다. 카메라에 관해서는 11.8절에서 찾아볼 수 있다.
Collection 계층구조 예제
예제로 우리는 본 장의 앞 부분에서 살펴본 Collection 계층구조 시각화를 생성할 것이다. 다음 단계를 통해 빌드할 것이다.
- 특정 모양이 없는 데이터를 모두 추가하라. 이번 경우 데이터는 모든 서브클래스가 포함된 Collection 클래스다.
- 특성에 따라 각 클래스를 렌더링하라.
- 클래스와 그 슈퍼클래스 사이에 연결(link)을 추가하라.
- 요소를 레이아웃이 있는 계층구조로 배열하라.
이번 절에서는 첫 단계, 계층구조의 각 클래스를 나타내는 모든 요소를 추가하는 것으로 시작하겠다.
이는 컬렉션으로부터 ROElements를 빌드하는 데 도움이 되는 ROElement 클래스로 forCollection: 메시지를 전송하여 쉽게 완료할 수 있다. 해당 메시지의 리턴 값으로부터 각 ROElement는 매개변수로부터 각 요소의 표현이다. 각각으로 border 모양을 추가하고, 쉬운 조작을 위해 드래그 가능하게 만든다. 마지막으로 뷰에서 모든 요소를 보기 위해 기본 레이아웃을 적용한다. 레이아웃이 어떻게 작용하는지는 추후에 더 설명하겠다.
view := ROView new.
classElements := ROElement forCollection: Collection withAllSubclasses.
classElements
do: [:c | c + ROBorder.
c @RODraggable ].
view addAll: classElements.
ROHorizontalLineLayout new on: view elements.
view open.
모양 상세히 열거하기
그래픽 구성 요소는 ROShape의 서브클래스이거나 그러한 클래스의 인스턴스에 해당하는 인자를 가진 + (또는 addShape:) 메시지를 전송하면 모양이 주어진다.
이와 비슷한 메시지로 @ 가 있는데, 이 또한 기본 값을 오버라이드하기 위해 클래스 또는 인스턴스를 인자로 취할 수 있다.
+의 매개변수가 shape인 경우, 색상과 같은 속성이 채워지거나 테두리 색상이 개별적으로 설정될 수 있다. 클래스가 매개변수로서 전달되면 요소는 각 속성마다 기본 값을 가진 그 클래스의 인스턴스로 모양이 주어질 것이다.
이용 가능한 모양으로는 라벨(ROLabel), 테두리(ROBorder), 박스(ROBox), 원(ROEllipse)가 있다. 기본 값으로 ROLabel은 요소의 모델(예: 객체 도메인)에 대한 printString 값을 표시할 것이다. 해당 값은 그림 11.5에서 보이는 바와 같이 커스텀 텍스트를 설정하여 변경 가능하다. ROElement로 ROBorder, ROBox, ROEllipse를 적용할 경우 모양이 요소의 범위(bound)로 조정될 것이다. 색상, 테두리 색상, 테두리 너비와 같은 속성을 shape로 설정하는 것도 가능하다. 이를 그림 11.6, 그림 11.7, 그림 11.8에 표시하겠다.
ROElement new
model: 'foo';
size: 100;
+ ROLabel.
|
ROElement new
size: 100;
+ ROBorder.
|
ROElement new
size: 200;
+ (ROBox new
color: Color green;
borderColor: Color red;
borderWidth: 4 ).
|
element := ROElement new
size: 100.
shape := ROEllipse new
color: Color yellow;
borderColor: Color blue;
borderWidth: 2.
element + shape.
|
Shapes 구성하기. 좀 더 정교한 시각적 면을 생성하기 위해 shape들을 구성할 수 있다. 하나 이상의 ROShape로 형성된 요소를 가지려면 원하는 모양으로 + 메시지를 여러 번 전송하면 된다 (그림 11.9)
이는 요소에 연관된 shape들의 사슬을 구축하는데, 그 중 첫 번째 구성 요소는 마지막으로 추가된 모양이고 마지막 요소는 빈 모양(RONullShape)의 인스턴스다.
Collection 계층구조 예제
이제 Collection 계층구조 예제에서 클래스로 몇 가지 모양을 추가할 것이다. 각 클래스 표현은 클래스의 인스턴스 변수 개수를 나타내는 너비와 그 메서드 개수를 나타내는 높이를 갖게 될 것이다.
view := ROView new.
classElements := ROElement forCollection: Collection withAllSubclasses.
classElements do: [:c |
c width: c model instVarNames size.
c height: c model methods size.
c + ROBorder.
c @ RODraggable ].
view addAll: classElements.
ROHorizontalLineLayout new on: view elements.
view open.
Edges: 요소 연결하기
Roassal을 이용하면 요소들 간 관계를 나타내기 위해 요소들 간 링크를 빌드할 수 있다. 두 요소 간 링크는 ROEdge 클래스의 인스턴스이다. 기본 값으로 edge는 빈 모양에 해당하는 RONullShape의 인스턴스로 형성된다. 이 때문에 edge를 렌더링하기 위해서는 직선 모양으로 형성될 필요가 있는데, 직선 모양은 ROAbstractLine의 어떤 서브클래스든 가능하다. 아래 코드는 두 요소 간 edge의 생성을 묘사한다. 먼저 두 개의 요소를 생성할 것이다. 다음으로 두 요소를 매개변수로서 사용하는 edge를 생성하여 직선 (ROLine의 인스턴스) 모양으로 구성할 것이다. 마지막으로 두 개의 요소와 edge를 뷰로 추가한다.
edge에 모양 추가하기. 표준 모양 외에도 직선 모양의 유형에는 여러 가지가 있는데, 가령 ROOrthoHorizontalLineShape를 예로 들 수 있겠다. 이들은 모두 ROAbstractLine 클래스의 서브클래스로서 ROLine도 마찬가지다. 몇 가지 예를 그림 11.12와 그림 11.13에서 소개하겠다.
edge + ROLine.
|
edge + ROOrthoHorizontalLineShape.
|
직선에 화살표 추가하기. 직선은 하나 또는 이상의 화살표를 포함할 수 있다. 화살표는 ROAbstractArrow의 서브클래스의 인스턴스로서, ROArrow 또는 ROHorizontalArrow를 들 수 있다. 직선 모양에 화살표를 추가하기 위해 add: 메시지를 사용하는데, 이는 그림 11.14와 그림 11.15에 표시하겠다.
edge + (ROLine new add: ROArrow new).
|
edge + (ROOrthoHorizontalLineShape new
add: ROHorizontalArrow new)
|
기본 값으로 화살표는 edge 끝에 위치할 것이지만 해당 위치는 add:offset: 을 이용해 맞춤설정이 가능하다. 오프셋 매개변수는 0과 1 사이에 해당하는 수치여야 하고, 화살표가 위치한 직선 길이의 비율을 나타낸다. 예를 들어, 오프셋이 0.5일 경우 화살표는 그림 11.16과 같이 직선의 중간으로 설정된다.
edge + (ROLine new add: ROArrow new
offset: 0.5).
|
직선이 하나 이상의 화살표를 포함하는 경우 화살표마다 여러 개의 오프셋을 설정할 수 있다.
line := ROLine new.
line add: ROArrow new offset: 0.1.
line add: ROArrow new offset: 0.5.
edge + line.
|
Collection 계층구조 예제
이제 요소 간 링크를 만드는 방법을 숙지했을 것이다. 다음 코드를 이용해 각 클래스와 그 슈퍼클래스 간 edge를 생성할 수 있다. 이를 위해선 먼저 edge를 빌드하기 위해 연관(association)의 컬렉션을 생성할 필요가 있다. 각 연관은 시작점을 연관 키로 나타내고 끝지점은 연관 값으로 나타낸다. 해당 예제에서 각 연관은 클래스를 표현하는 ROElement부터 시작해 그 슈퍼클래스를 표현하는 ROElement까지 이어진다.
연관을 갖고 있다면 linesFor: 메시지를 전송하여 ROEdge의 인스턴스를 생성한다. 해당 메시지는 연관의 컬렉션을 매개변수로 취하고 edge의 컬렉션을 리턴한다.
view := ROView new.
classElements := ROElement forCollection: Collection withAllSubclasses.
view addAll: classElements.
associations := OrderedCollection new.
classElements do: [:c |
c width: c model instVarNames size.
c height: c model methods size.
c + ROBorder.
c @ RODraggable.
(c model superclass = Object)
ifFalse: [ associations add: ((view elementFromModel: c model superclass) -> c)]
].
edges := ROEdge linesFor: associations.
view addAll: edges.
ROHorizontalLineLayout new on: view elements.
view open
이제 Collection 계층구조에 원하는 모양으로 된 클래스를 갖고 있으며 클래스는 각자의 슈퍼클래스로 연결되었다. 하지만 실제 계층구조가 보이지 않는다. 그 이유는 뷰의 모든 요소를 배열하기에 적절한 레이아웃이 필요하기 때문이다. 다음 절에서는 레이아웃을 요소로 적용하는 방법을 다루고자 한다.
레이아웃
레이아웃은 요소의 컬렉션이 어떻게 자동으로 배열되는지를 정의한다. 레이아웃을 적용하기 위해서는 ROElement의 컬렉션을 매개변수로 한 on: 메시지를 사용해야 한다. 그림 11.19의 예제와 같이 spriteOn: 라는 편의(convenience) 메시지를 이용해 각각의 크기가 50이고 빨간색 테두리를 가지며 드래그가 가능하게 구성된 ROElements의 컬렉션을 생성한다. 이후 그리드(grid)에 요소를 배열하기 위해 레이아웃을 적용하겠다.
view := ROView new.
view addAll: (ROElement spritesOn: (1 to: 4)).
ROGridLayout on: view elements.
view open.
|
그림 11.20은 Roassal에서 이용 가능한 레이아웃을 몇 가지 보여준다. 본문에 실리지 않은 것을 포함해 아래의 레이아웃은 ROLayout의 서브클래스로 찾을 수 있다.
(a) ROGridLayout |
(b) ROCircleLayout |
(c) ROTreeLayout |
(d) ROTreeMapLayout |
(e) ROVerticalLineLayout |
(f) ROHorizontalLineLayout |
레이아웃이 요소의 컬렉션으로 적용되면서 여러 요소의 집합들은 여러 레이아웃을 가질 수 있다. 아래 예제에서는 두 개의 요소 컬렉션이 두 개의 레이아웃으로 배열된다. 첫 번째는 요소를 수직선을 따라, 두 번째는 수평선을 따라 배열한다. 먼저 수직선을 따라 배열된 요소들을 생성하고, ROVerticalLineLayout을 적용하여 label을 이용해 모양을 형성한다. 이후 두 번째 그룹에는 ROHorizontalLineLayout을 이용해 똑같이 실행하고 겹침(overlapping)을 피하기 위해 공간을 둔다.
| view verticalElements horizontalElements |
view := ROView new.
verticalElements := ROElement spritesOn: (1 to: 3).
ROVerticalLineLayout on: verticalElements.
verticalElements do: [ :el | el + ROLabel ].
horizontalElements := ROElement spritesOn: (4 to: 6).
ROHorizontalLineLayout on: horizontalElements.
horizontalElements do: [ :el |
el + ROLabel.
el translateBy: (60@ 0) ]. "spacing"
view
addAll: horizontalElements;
addAll: verticalElements.
view open.
중첩된 구조 내 레이아웃. 중첩된 요소의 레이아웃은 각 요소의 컨테이너에 비례한다. 아래 예제에서는 두 개의 요소가 생성되는데, 각 요소는 그리드로서 배열된 3개의 자식 요소들을 갖는다. 마지막으로 수평선 레이아웃을 이용해 부모 요소들을 배열하고자 한다.
새 레이아웃 생성하기. Roassal은 많은 수의 레이아웃을 제공한다 (이 글을 작성할 무렵엔 약 23개의 레이아웃이 존재). 하지만 특정 표현을 수용하기 위해 새 레이아웃을 필요로 하는 경우가 발생할지도 모른다. 이번 절에서는 레이아웃을 집중적으로 다룬다. 새 레이아웃을 생성하기 전에 먼저 레이아웃이 어떻게 구조화되는지를 이해해야 할 것이다.
모든 레이아웃 클래스는 ROLayout으로부터 상속된다. 해당 클래스는 클래스 측이나 인스턴스로부터 레이아웃을 적용하는 데에 가장 흔히 사용되는 메서드인 on: 을 정의한다. on: 메서드는 레이아웃을 적용하는 데 주요 메서드인 executeOnElements: 를 호출한다. 이 메서드를 아래 코드에 표시하겠다.
ROLayout >> executeOnElements: elements
"Execute the layout, myself, on the elements"
maxInterations := elements size.
self doInitialize: elements.
self doExecute: elements asOrderedCollection.
self doPost: elements.
executeOnElements: 메서드는 아래 3개의 hook 메서드를 호출한다.
- doInitialize: 레이아웃을 시작하기 전에 실행되는 메서드. 정렬해야 하는 그래프를 준비해야 하는 경우 유용하다.
- doExecute: 레이아웃 알고리즘을 적용한다. 그에 따라 요소들이 이동된다.
- doPost: 레이아웃을 실행한 이후에 실행되는 메서드.
Pre/post-processing 이 정의될 수도 있다. 이는 레이아웃이 다단계이거나 적절한 이벤트를 방출해야 하는 경우 유용하다. 이러한 액션은 ROLayoutBegin과 ROLayoutEnd 이벤트를 이용해 콜백으로서 설정된다. ROLayoutBegin과 ROLayoutEnd는 각각 doInitialize: 와 doPost: 에 의해 발표(announce)된다. 그 사용 예제를 아래 코드에 소개하겠다.
| layout t |
t := 0.
layout := ROHorizontalLineLayout new.
layout on: ROLayoutBegin do: [ :event | t := t + 1 ].
layout on: ROLayoutEnd do: [ :event | t := t + 1 ].
layout applyOn: (ROElement forCollection: (1 to: 3)).
self assert: (t = 2).
doExecute: 메서드는 특정 알고리즘을 이용해 요소들을 배열한다. 이 메서드는 배치할 요소들의 컬렉션을 매개변수로서 취한다.
이제 ROLayout 클래스의 구조를 알게 되었으니 RODiagonalLineLayout 이라 불리는 새 레이아웃을 정의하여 대각선을 따라 요소를 위치시킬 것이다. ROLayout의 서브클래스를 생성하는 것이 첫 번째 단계가 되겠다.
ROLayout subclass: #RODiagonalLineLayout
instanceVariableNames: 'initialPosition'
classVariableNames: ''
poolDictionaries: ''
category: 'Roassal-Layout'
인스턴스 변수 initialPosition 는 가상 직선이 어디서 시작하는지, 다시 말하자면 직선의 첫 번째 요소가 어디에 위치할 것인지 정의한다. 해당 변수는 initialize 메서드에서 설정된다.
RODiagonalLineLayout >> initialize
super initialize.
initialPosition := 0@0.
RODiagonalLineLayout >> initialPosition: aPoint
initialPosition := aPoint
RODiagonalLineLayout >> initialPosition
^ initialPosition
레이아웃이 적용되기 전이나 후에 특수 액션을 실행할 필요가 있다면 doInitialize: 또는 doPost: 메서드를 오버라이드할 것이다. 하지만 본문의 예제는 이에 해당하지 않는다. 우리가 겹쳐 써야 하는 메서드는 사실상 작업을 실행하는 doExecute: 로, 이 메서드는 가상의 대각선을 따라 모든 요소를 이동하는 일을 수행한다.
RODiagonalLineLayout >> doExecute: elements
| position |
position := initialPosition.
elements do: [:el |
el translateTo: position.
position := position + el extent ]
아래 코드를 이용해 레이아웃을 테스트해볼 수 있다.
| view elements |
view := ROView new.
elements := ROElement spritesOn: (1 to: 3).
view addAll: elements.
RODiagonalLineLayout on: view elements.
view open.
|
Roassal에서 레이아웃에 대한 한 가지 주요점으로 배치할 요소의 크기를 고려하는 것을 들 수 있다. 새 레이아웃을 정의할 때는 자신의 알고리즘이 요소의 크기를 사용하도록 만들 것을 명심하라.
Collection 계층구조 예제
Collection 예제에 대한 계층구조가 필요하므로 적절한 시각화를 얻기 위해선 ROTreeLayout이 유용하겠다.
"Create the elements to be displayed"
view := ROView new.
classElements := ROElement forCollection: Collection withAllSubclasses.
view addAll: classElements.
associations := OrderedCollection new.
classElements do: [:c |
"Make each element reflect their model characteristics"
c width: c model instVarNames size.
c height: c model methods size.
"we add shape for the element to be seen"
c + ROBorder.
"we make it draggable by the mouse"
c @ RODraggable.
"Create associations to build edges"
(c model superclass = Object)
ifFalse: [ associations add: ((view elementFromModel: c model superclass) -> c)]
].
"Add edges between each class and its superclass"
edges := ROEdge linesFor: associations.
view addAll: edges.
"Arrange all the elements as a hierarchy"
ROTreeLayout new on: view elements.
view open
그 결과 시각화는 그림 11.24의 모습과 같을 것이다.
이벤트와 콜백
Roassal은 뷰 자체를 포함해 시각성 내 어떤 시각적 구성 요소든 이벤트를 방출하고 그에 반응하도록 허용한다. Roassal에서 정의되는 이벤트에는 두 가지 종류가 있다. 첫 번째 이벤트는 저수준으로서 마우스 이동이나 클릭, 또는 키 누름과 같은 사용자 액션을 나타낸다. 두 번째 이벤트는 뷰 자체가 트리거한 이벤트를 의미하는데 주로 카메라의 이동, 레이아웃 적용, 뷰의 새로고침이 포함된다. 모든 이벤트는 ROEvent 클래스로부터 상속된다.
이벤트가 작동하는 방식을 확인하기 위해 마우스 클릭에 반응하는 시각화를 예로 들 예정인데, 클릭이 이루어지는 곳으로 요소를 전환한다. 마우스 이벤트를 처리하는 이벤트 클래스로는 ROMouseClick, ROMouseMove, ROMouseEnter, ROMouseLeave가 있으며, 키 누름을 처리하는 이벤트로 ROKeyDown 클래스가 있다.
우선 ROLeftMouseClick 이벤트를 이용해 마우스 왼쪽을 클릭하면 시각화가 반응하도록 만들 것이다. 반응은 이벤트의 위치로 요소를 전환하기 위한 애니메이션을 생성할 것이다.
아래 코드에서와 같이 Roassal 객체가 이벤트에 반응하도록 설정하기 위해 on:do: 메시지를 이용한다. 첫 번째 매개변수는 예상되는 이벤트의 클래스여야 하고, 두 번째 매개변수는 이벤트를 수신 시 실행되어야 하는 액션을 정의하는 블록이어야 한다.
view := ROView new.
el := ROElement sprite.
view add: el.
view
on: ROMouseLeftClick
do: [ :event | ROLinearMove new for: el to: event position ].
view open.
ROLinearMove는 Roassal 상호작용 중 하나에 해당한다. 이름이 제시하듯이 선형적 이동(linear move)으로 전환되어야 하는 요소에 대한 애니메이션을 생성한다. 상호작용에 관한 내용은 다음 절에서 더 설명하겠다.
상호작용 계층구조
그래픽 요소는 콜백이나 상호작용을 설정함으로써 이벤트에 응답한다. 콜백을 어떻게 설정하는지는 이미 제시하였으니 이번 절에서는 상호작용에 관해 상세히 다루겠다.
모든 Roassal 상호작용의 루트 클래스는 ROInteraction이다. 상호작용은 ROInteraction의 서브클래스나 그러한 클래스의 인스턴스를 매개변수로 하여 @ 메시지를 전송함으로써 요소로 설정된다. 요소로 적용 가능한 상호작용은 다양한데, RODraggable 이나 ROGrowable 을 예로 들 수 있겠다. RODraggable 은 요소를 마우스로 드래그 가능하게 해주며, ROGrowable은 클릭 시 요소의 크기를 증가시킨다.
요소는 하나 이상의 상호작용을 가질 수 있다. 예를 들자면 요소로 RODraggable이나 ROGrowable을 적용할 수도 있는데, 아래 코드를 통해 설명하겠다. 요소를 클릭하여 크게 만들거나 뷰로 드래그하라.
| view element |
view := ROView new.
element := ROElement new size: 10.
element
+ ROBox;
@ RODraggable;
@ ROGrowable.
view add: element.
view open.
마우스를 요소 위에서 클릭하면 표시되는 팝업 요소처럼 일부 상호작용은 준비하기가 조금 복잡한 것이 사실이다.
Roassal에서 이용 가능한 상호작용 중 몇 가지만 본문에 제시하겠다.
ROAbstractPopup
ROAbstractPopup은 팝업을 표시함으로써 요소로 하여금 이벤트 위의 마우스에 반응하도록 해준다. 팝업에는 두 가지 유형이 있는데, 첫 번째는 기본 값으로 요소 모델의 printString 값과 함께 상자를 표시하는 ROPopup, 두 번째는 커스텀 뷰를 표시하는 ROPopupView가 있다.
요소에 팝업을 추가하려면 ROPopup 클래스를 인자로 하여 @ 메시지를 전송하면 된다. 문자열을 매개변수로 한 text: 메시지를 이용해 커스텀 텍스트를 마련하는 것도 가능하다.
아래 예제에서는 임의의 문자열을 그 모델로 하여 ROElement 클래스로 spriteOn: 메시지를 전송함으로써 요소를 생성하고자 한다. 그 결과 요소의 크기는 50이 되고, 빨간 테두리를 가지며, 마우스로 드래그가 가능해진다. 마지막으로 요소에 ROPopup을 추가한다.
view := ROView new.
el := ROElement spriteOn: 'baz'.
el @ ROPopup. "Or with custom text -> (ROPopup text: 'this is custom text')"
view add: el.
view open.
ROPopupView는 팝업시킬 뷰의 정의를 필요로 하기 때문에 조금 더 복잡하다. 이러한 상호작용은 표시할 새로운 뷰가 있는 ROpopupView로 view: 메시지를 전송하면 생성할 수 있다. 매개변수는 뷰를 정의하는 블록이 될 수도 있다. 마우스가 요소 위에 있으면 같은 요소를 매개변수로 이용해 블록이 평가되며, 이에 따라 뷰가 동적으로 생성될 수 있다.
아래 예제는 5개의 요소가 있는 뷰를 생성한다. 각 요소 위로 마우스를 갖다 대면 팝업이 표시되면서 반응한다. 팝업 뷰는 마우스가 위치한 요소 모델과 같은 개수의 노드를 가진 뷰를 생성하는 블록으로 정의된다. 예를 들어, 그림 11.7에서 볼 수 있듯이 마우스가 노드 "3" 위로 지나가면 3개의 회색 상자가 있는 팝업창이 나타난다.
view := ROView new.
elements := ROElement spritesOn: (1 to: 5).
"create the view to popup"
viewToPopup := [ :el | | v |
v := ROView new.
"Add as many elements as the value represented"
v addAll: (ROElement forCollection: (1 to: el model)).
v elementsDo: [ :e | e size: 20; + ROBox ].
ROGridLayout on: v elements.
v ].
elements do: [ :e | e + ROLabel; @ (ROPopupView view: viewToPopup)].
view addAll: elements.
ROHorizontalLineLayout on: view elements.
view open.
RODynamicEdge
데이터 요소와 그 관계의 시각화를 반복해야 하는 경우 마우스가 요소를 가리키면 나가는 edge가 표시된다. 요소로 들어가거나 떠날 때 콜백의 올바른 조합을 시도하는 대신 RODynamicEdge 상호작용을 사용할 경우 작업을 상당히 줄여준다.
아래 예제는 마우스를 일부 요소 위에서 맴돌 때 몇 행이 표시되도록 한다.
| rawView el1 el2 el3 |
rawView := ROView new.
rawView add: (el1 := ROBox element size: 20).
rawView add: (el2 := ROBox element size: 20).
rawView add: (el3 := ROBox element size: 20).
ROCircleLayout on: (Array with: el1 with: el2 with: el3).
el1 @ RODraggable.
el2 @ RODraggable.
el3 @ RODraggable.
el1 @ (RODynamicEdge toAll: (Array with: el2 with: el3) using: (ROLine arrowed color: Color red)).
rawView open
ROAnimation
애니메이션 또한 Roassal에서 상호작용에 해당한다 (예: ROAnimation은 ROInteraction의 서브클래스다). 일부 애니메이션은 요소들이 일정한 속도(ROLinearMove), 일정한 가속(ROMotionMove), 또는 수학 함수에 따라(ROFunctionMove) 선형적으로 전환되도록 허용한다. ROZoomInMove와 ROZoomOutMove 클래스가 제공하는 애니메이션은 뷰를 포커스 인, 포커스 아웃시킨다. 모든 애니메이션은 ROAnimation의 서브클래스이다.
각 애니메이션에는 완료해야 할 다수의 사이클이 있는데, 각 사이클은 doStep 메시지를 전송하여 실행된다. ROAnimation은 after: 메시지를 이용하여 애니메이션이 완료된 후 실행되어야 하는 블록을 설정하도록 해준다. 애니메이션이 완료된 후 실행되어야 하는 액션은 애니메이션이 트리거되기 전에 설정되어야 하며, 이를 어길 경우 실행되지 않음을 주목하라.
view := ROView new.
element := ROElement new.
element size: 10.
element + (ROEllipse color: Color green).
view add: element.
element translateBy: 30@20.
ROFunctionMove new
nbCycles: 360;
blockY: [ :x | (x * 3.1415 / 180) sin * 80 + 50 ];
on: element.
view open.
그림 11.27은 ROLinearMove를 나타낸다. 아래 코드는 요소가 ROFunctionMove를 이용해 sinus 곡선을 따르도록 해준다.
뷰의 카메라 이해하기
뷰의 카메라는 실제로 뷰를 바라보는 관점을 나타낸다.
translateBy: 또는 translateTo: 메시지가 뷰로 전송되면 뷰 자체가 아니라 사실상 그 카메라가 이동한다. 카메라의 위치는 position 인스턴스 변수에 의해 주어진다. 카메라의 위치는 수동으로 translateBy: 또는 translateTo: 메시지를 카메라로 전송하여 설정되지만 매개변수의 값으로 부정 값(negated value)을 사용한다. 이는 뷰를 10 픽셀만큼 가로 및 세로로 이동해야 하는 경우 아래와 같이 실행할 수 있음을 의미한다.
view translateBy: 10@10
혹은 뷰의 카메라를 수동으로 전환하는 방법도 있다.
view camera translateBy: (-10)@(-10)
카메라에는 우리가 실제로 바라보는 extent(범위), 그리고 더 먼 범위를 나타내는 real extent(실제 범위)가 있다. 실제 뷰의 카메라의 범위는 캔버스에 뷰가 그려지는 방식에 영향을 미친다. 뷰를 렌더링할 때는 각 포인트, 각도 혹은 그려야 하는 다른 모양이 카메라의 범위에 따라 구성된다. 이는 카메라의 시야에 비례한 가상 포인트에서 각 절대 위치를 변형함으로써 이루어진다. 예를 들어 뷰에서 이를 확대하면 extent 상의 내용이 "확장"되어 real extent를 채우고, 객체의 크기는 더 커진다. 카메라의 extent와 real extent는 각각 extent: 와 realExtent: 접근자를 이용해 수정된다. 카메라는 시각화의 창 크기를 보관하기도 한다.
카메라는 extent를 이용해 계산되는 뷰의 높이(altitude)를 갖는다. extent 값이 작을수록 카메라는 낮게 위치하고, extent 값이 크면 카메라는 높게 위치한다. 카메라의 높이는 숫자를 매개변수로 이용하여 altitude: 메시지를 전송함으로써 설정된다. 카메라는 회전이 불가하며 전환만 가능하다. 이는 카메라가 항상 뷰를 수직으로 바라봄을 의미한다.
그림 11.28은 방금 언급한 내용을 표시하는데, 뷰와 연관된 정보를 모두 나타낸다. 시각화의 시각적 부분이 카메라의 extent에 의해 주어진다는 것도 확인할 수 있겠다.
ROZoomMove 상호작용은 카메라의 범위에 영향을 미친다. 이러한 상호작용은 카메라의 위치를 수정하고 원하는 직사각형에 들어맞도록 확장시킨다. 예를 들어, 뷰의 특정 요소를 포커스 인 하기 위해 확대하면 ROZoomMove는 요소의 경계에 들어맞도록 카메라를 전환하고 확장한다. 이러한 움직임은 카메라의 altitude를 변경하여 시뮬레이션이 가능하다.
카메라를 이용해 탐색을 위한 미니맵(minimap) 빌드하기. Roassal이 제공하는 상호작용과 애니메이션 모델은 복잡한 행위를 지원한다. 아래 코드를 고려해보자.
| view eltos |
view := ROView new.
view @ RODraggable .
view on: ROMouseRightClick do: [ :event |
ROZoomInMove new on: view ].
view on: ROMouseLeftClick do: [ :event |
ROZoomOutMove new on: view ].
eltos := ROElement spritesOn: (1 to: 400).
eltos do: [:el | el + ROLabel ].
view addAll: eltos.
ROGridLayout new on: view elements.
"Mini map opens by pressing m"
view @ ROMiniMap.
view open.
이 코드는 400개의 라벨이 붙은 요소가 있는 뷰를 연다. 요소들은 그리드 레이아웃을 이용해 정렬된다. 왼쪽 마우스 버튼을 클릭하면 뷰가 확대된다. 오른쪽 마우스를 클릭하면 축소된다. m 키를 누르면 미니맵이 열릴 것이다. 해당 기능은 ROMiniMap 상호작용을 이용해 활성화된다.
ROMiniMap은 시각화의 완전한 시야를 제공하는 새 창을 연다. 뿐만 아니라 본래 뷰의 카메라를 이용함으로써 탐색을 수월하게 해준다.
미니맵은 시각화의 작은 버전, 그리고 메인 뷰의 창에서 현재 시각적 부분을 나타내는 lupa (확대경)로 구성된다.
본문 예제로 돌아와, 상호작용은 뷰로 @ROMiniMap 메시지를 전송하여 추가하고 "m"을 누르면 뷰가 열린다 (그림 11.29 참고).
뷰의 더 작은 버전은 특정 모양, 즉 ROViewDisplayer의 서브클래스인 ROMiniMapDisplayer를 이용해 표시된다. ROViewDisplayer는 요소에 뷰를 표시하는 모양이다 (기본적으로 팝업 뷰에 사용됨). 두 가지의 차이점은 ROMiniMapDisplayer는 고유의 카메라를 사용한다는 점으로, 뷰의 카메라와는 다른 extent를 가진다. 따라서 동일한 뷰지만 여러 크기로 볼 수 있도록 해준다.
Lupa 크기는 창의 시각적 부분을 표현하고 그 위치는 뷰의 카메라 위치에 연관된다. 뷰가 포인트로 전환되면 lupa는 그 위치를 변경하여 따르는데, 카메라 위치를 나타내는 포인트는 ROMiniMapDisplayer 카메라 extent 상의 포인트로 전환된다. 뷰를 확대하거나 축소하면 카메라의 extent가 변경되어 lupa의 크기가 증가하거나 감소한다.
Pharo를 넘어서
Roassal은 다른 스몰토크 dialect로 쉽게 이식되도록 설계되었다. 현재는 VisualWorks, Amber, VA Smalltalk로 이식되고 있다.
그림 11.30이 나타내는 바와 같이 Roassal은 3가지 구성 요소로 이루어진다.
- Roassal Core. 메인 클래스를 정의하는 패키지의 집합으로, ROView, ROElement, ROShape, ROCamera가 이에 해당한다. 이는 모든 테스트를 포함하기도 한다.
- Mondrian DSL. Roassal-Builder와 Roassal-Builder-Tests 패키지로 구성된다.
- 플랫폼 의존적인 패키지. Roassal이 이식되는 스몰토크 dialect 전용으로 존재한다.
플랫폼 의존적인 패키지에는 몇 가지 클래스가 구현되어야 한다. 주요 클래스로는 뷰를 렌더링할 수 있는 native canvas 클래스, 캔버스를 포함하도록 객체를 리턴하고 모든 외부 이벤트를 위임할 수 있는 위젯 팩토리 클래스를 들 수 있겠다. 첫 번째는 ROAbstractCanvas이며, 두 번째는 RONativeWidgetFactory의 서브클래스여야 한다.
ROPlatform 클래스는 의존적 패키지와 코어 패키지 간 연계(bridge)를 어떻게 구현해야하는지 정의한다. 해당 클래스는 이름에 따라 클래스를 보관하는 canvasClass와 widgetFactory와 같은 인스턴스 변수를 정의한다. 각 플랫폼 의존적 패키지는 자신의 플랫폼 클래스를 ROPlatform의 서브클래스로서 구현하고, 구현된 모든 플랫폼 의존적 클래스를 참조해야 한다. 내부적으로 이러한 클래스 중 하나라도 필요할 때마다 코어 패키지는 필요한 클래스를 리턴하기 위해 ROPlatform의 현재 인스턴스에 의존한다.
요약
Roassal은 객체의 어떤 그래프든 시각화할 수 있도록 해준다. 본 장에서는 Roassal의 주요 기능을 살펴보았다.
- 그래픽 요소를 생성하고 원하는 모양대로 형성한다.
- edge를 생성하여 그래픽 요소들 간 관계를 나타낸다.
- 요소의 컬렉션을 자동으로 배열하기 위해 레이아웃을 적용한다.
- 콜백과 정의된 상호작용을 설정함으로써 요소들이 이벤트에 반응하도록 만든다.
- 뷰의 카메라와 상호작용을 통해 시각화 포인트를 이동시킨다.
http://objectprofile.com 에서는 Rossal에 관련된 스크린샷, 온라인 예제, 스크린캐스트를 찾을 수 있다.
감사의 말. 우선 Roassal을 개발해준 Chris Thorgrimsson과 ESUG에 감사의 말을 전한다.
본문을 검토해준 Nicolas Rosselot Urrejola 와 Stephane Ducasse에게 매우 감사드린다. 또 Roassal의 디자인에 관한 여러 논의를 제공해주신 Pietriga와 Tudor Girba에게도 감사를 표하는 바이다.
Notes
- ↑ http://www.moosetechnology.org/
- ↑ Glamour를 기반으로 한 easel도 World 메뉴의 Moose section에서 제공됨을 주목한다. Glamour를 기반으로 한 easel은 본문에 제시된 easel과 유사하다. 해당 버전에 전용으로 만들어진 표현은 moose 서적, http://themoosebook.org 에서 찾을 수 있을 것이다.
- ↑ 사실 요소는 항상 shape, 즉 RONullShape의 인스턴스를 갖고 있다. 본문에서는 null 객체 디자인 패턴이 사용되었다.