SqueakByExample:11.4

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

나만의 morph 를 생성하고 그리기

Morph 의 합성을 이용해서 많은 종류의 편리하고, 재미있는 그래픽 표현을 만들 수 있지만, 가끔은 완전히 다른 뭔가를 만들고 싶을때가 있겠죠. 그런 작업을 하기 위해서는, Morph 의 서브클래스를 만들어서 drawOn: 메서드를 오버라이드override 해서 morph 의 모양새를 바꿀 수 있습니다.

Morphic 프레임워크는 화면에 morph 를 다시 표시해야하는 경우, 메시지 drawOn: 을 morph 에 보냅니다. drawOn: 메시지의 인자는, Canvas 클래스(또는 서브클래스)의 인스턴스로서, morph 가 스스로를 canvas 의 bounds[1] 내에 표시할거라는걸 예상할 수 있겠죠.


Squeak comment.png클래스 브라우저를 사용하여 Morph 로부터 상속받은 새로운 클래스 CrossMorph를 정의하십시오:


클래스 11.2: CrossMorph를 정의하기

Morph subclass: #CrossMorph
  instanceVariableNames: ''
  classVariableNames: ''
  poolDictionaries: ''
  category: 'SBE--Morphic'


아래와 같이 drawOn: 메서드를 정의할 수 있습니다:


메서드 11.3: CrossMorph 그리기.

drawOn: aCanvas
  | crossHeight crossWidth horizontalBar verticalBar |
  crossHeight := self height / 3.0 .
  crossWidth := self width / 3.0 .
  horizontalBar := self bounds insetBy: 0 @ crossHeight.
  verticalBar := self bounds insetBy: crossWidth @ 0.
  aCanvas fillRectangle: horizontalBar color: self color.
  aCanvas fillRectangle: verticalBar color: self color


그림 11.3: CrossMorph 가 할로를 표시한 모습. 원하는 만큼 CrossMorph 크기를 변경할 수 있습니다.


Morph 에 bound 메시지를 전송하면, Rectangle 클래스의 인스턴스인 bounding box 가 반환되며, 이것은 morph 가 Rectangle 의 인스턴스 라는 의미가 됩니다. Rectangle 은 geometry 와 관련된 다른 사각형을 행성하는등의 많은 메시지를 사용할 수 있습니다; 위의 예제에서는, 인수로 점point 을 사용하는 insetBy: 메시지를 사용해서, 줄어든 높이로 첫번째 직사각형을 만들고, 줄어든 너비로 다른 직사각형을 만들고 있습니다.


Squeak comment.png새로운 morph 를 테스트 하기 위해 CrossMorph new openInWorld 를 실행하십시오.


실행하면 그림 11.3 과 같은 결과를 얻을 수 있습니다. 그렇지만 마우스에 반응하는장소-morph 를 잡기위해 클릭할 수 있는 영역-는 bounding box 전체가 된다는걸 알아차렸을거라고 생각합니다. 이걸 고쳐보도록 하죠.

Morphic 프레임워크는, 어떤 morph 가 마우스의 커서 아래에 있는지 알아내야할 필요가 있을때, 마우스의 포인터 아래에 bounding box 가 있는 모든 morph 에 대해서 containsPoint: 메시지를 보냅니다. 즉 마우스에 반응하는 영역을 morph 의 십자 부분으로 제한하고 싶다면, containsPoint: 메서드를 오버라이드 할 필요가 있는거죠.


Squeak comment.png클래스 CrossMorph 에서 다음 메서드를 정의하십시오:


메서드11.4: CrossMorph 의 반응 영역sensitive zone 설정하기

containsPoint: aPoint
  | crossHeight crossWidth horizontalBar verticalBar |
  crossHeight := self height / 3.0.
  crossWidth := self width / 3.0.
  horizontalBar := self bounds insetBy: 0 @ crossHeight.
  verticalBar := self bounds insetBy: crossWidth @ 0.
   (horizontalBar containsPoint: aPoint)
    or: [verticalBar containsPoint: aPoint]


이 메서드는 drawOn: 과 동일한 로직을 사용하기 때문에, containsPoint: 에서 true 를 반환하는 영역이, drawOn 에 의해 색칠이 가능한 영역과 일치한다는 것을 확신할 수 있죠. Rectangle 클래스의 containsPoint: 메서드를 활용하면, 이런 번거로운 작업을 해낼 수 있습니다.


메서드 11.3 과 11.4 에 있는 코드는 2 가지 문제를 가지고 있습니다. 눈에 띄는 문제점으로는 코드가 중복되고 있다는거죠. 이건 매우 큰 잘못입니다: 왜냐하면 horizonatalBar 또는 verticalBar 가 계산된 방식을 변경할 필요가 있을때, 2 개 지점중 한쪽의 코드를 변경하는 작업을 쉽게 잊어버리기 때문입니다. 해결하려면 새로운 2 개 메서드에서 계산에 관련된 부분을 분리해낸 후, private 프로토콜에 넣으면 됩니다.


메서드 11.5: horizontalBar.

horizontalBar
  | crossHeight |
  crossHeight := self height / 3.0.
   self bounds insetBy: 0 @ crossHeight


메서드 11.6: verticalBar.

verticalBar
  | crossWidth |
  crossWidth := self width / 3.0.
   self bounds insetBy: crossWidth @ 0


이렇게 만든 메서드를 이용해서 drawOn: 와 containsPoint: 양쪽을 다 정의하면 됩니다.


메서드 11.7: 리팩토링한 CrossMorph>>drawOn:.

drawOn: aCanvas
  aCanvas fillRectangle: self horizontalBar color: self color.
  aCanvas fillRectangle: self verticalBar color: self color


메서드 11.8: 리팩토링한 CrossMorph>>containsPoint:.

containsPoint: aPoint
   (self horizontalBar containsPoint: aPoint)
     or: [self verticalBar containsPoint: aPoint]


OverdrawBug.png HairlineBug.png
그림 11.4: colour 로 2 번 채워진 십자의 중심부분 그림 11.5: 채워지지 않은 픽셀의 열을 보여주는 십자 모양의 Morph


private 메서드에 의미있는 이름을 붙인 덕분에, 이 코드는 보다 이해가 쉬워졌습니다. 이제 간단히 두번째의 문제를 알 수 있게 됐죠: 십자의 중앙부분이 horizontalBar 와 verticalBar 의 양쪽에 걸쳐져 있기때문에 2 번 그려지는 문제입니다. 이런 버그는 투명하지 않는 생상으로 십자를 그린다면 별 문제가 아닐겁니다만, 그림 11.4 에서 확인할 수 있듯이 반투명 십자를 그리는 순간, 확실히 버그가 드러날겁니다.


Squeak comment.png다음 코드를 워크스페이스에서 한 라인씩 실행하십시오.

m := CrossMorph new bounds: (0@0 corner: 300@300).
m openInWorld.
m color: (Color blue alpha: 0.3).


이 문제를 해결하려면 verticalBar 를 세부분으로 나눠서, 위쪽과 아래쪽만 칠하면 됩니다. Rectangle 클래스에서 이런 여려운 일을 해주는 메서드를 다시 발견할 수 있습니다: r1 areasOutside: r2 는 r2 의 바깥쪽으로 삐져나온 부분에 해당되는 r1 을 구성하는 사각형의 배열을 반환합니다. 아래쪽에 수정된 코드가 있습니다:


메서드 11.9: 개량된 drawOn: 메서드, 이 메서드는 십자의 중심부분을 한번만 채웁니다.

drawOn: aCanvas
  | topAndBottom |
  aCanvas fillRectangle: self horizontalBar color: self color.
  topAndBottom := self verticalBar areasOutside: self horizontalBar.
  topAndBottom do: [ :each | aCanvas fillRectangle: each color: self color]


위의 코드는 잘 작동하는걸로 보입니다만, 몇번정도 십자의 크기를 변경하려고 하는경우, 그림 11.5 처럼 특정 크기에서 1 pixel 만큼의 선이 십자의 아래부분과 위쪽을 나눠버리는것을 확인할 수 있습니다. 이 문제는 어림수(짐작하는수)처리rounding 때문에 일어납니다: 색을 채우려고 하는 사각형의 크기가 정수integer가 아닌경우, fillRectangle: color: 는 적당히 수치를 조절하기때문에 1 pixel 만큼 문제가 생기게 됩니다. 이 문제는, 사각형의 크기를 계산할때 명시적으로 값을 대입하는것으로 해결할 수 있습니다.


메서드 11.10: 정확한 CrossMorph>>horizontalBar rounding 처리

horizontalBar
  | crossHeight |
  crossHeight := (self height / 3.0) rounded.
   self bounds insetBy: 0 @ crossHeight


메서드 11.11: 정확한 CrossMorph>>verticalBar rounding 처리

verticalBar
  | crossWidth |
  crossWidth := (self width / 3.0) rounded.
   self bounds insetBy: crossWidth @ 0


Notes

  1. 이 경우는 그릴 morph 의 bounds 가 아니라 그림판이 될 대상 canvas 의 bounds 를 의미하는것 같습니다