SqueakByExample:11.4

From 흡혈양파의 번역工房
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

나만의 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 를 의미하는것 같습니다