ページ

2008年5月20日火曜日

ThinButton(その7)ボタンの部品化(ThinButtonBar)

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

次は描画とイベントハンドリングを司る ThinButtonBarクラス。まずはインターフェイス定義から。

ThinButtonBar

@interface ThinButtonBar : NSView {

NSMutableArray* _list;
CGFloat _offsetX;
NSTrackingArea* _tracking_area;
id _delegate;
ThinButton* _pushed_button;
}

- (void)addButtonWithImageResource:(NSString*)resource tag:(UInt)tag;
- (void)setDelegate:(id)delegate;

@end


NSViewのサブクラスで drawRect: や mouseDown:などのオーバライドが中心となる。クライアントコードでは addButtonWithImageRsource:tag: を使ってボタンを追加(定義)していく。今回は登録された順番に左から右へ横並びでボタンが表示されるようにレイアウトする。 tag はボタンが押されたときに Delegateへ渡される値。

まずは初期化コードから。ThinButtonの配列を格納する NSMutableArrayや NSShadowを作っておく。
ThinButtonBar
static NSShadow* _shadow = nil;

-(id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
_list = [[NSMutableArray alloc] init];
_offsetX = 0.0;

if (!_shadow) {
_shadow = [[NSShadow alloc] init];
[_shadow setShadowOffset:NSMakeSize(1.0, -1.0)];
[_shadow setShadowBlurRadius:2.0];
[_shadow setShadowColor:[[NSColor blackColor] colorWithAlphaComponent:0.5]];
}

_tracking_area = nil;
_delegate = nil;
}
return self;
}


続いてボタン追加のメソッド。ちょっぴり長い。最初(1)に NSImageを作った後、ThinButtonを生成し _listへ追加する。次(2)に追加したボタンの幅だけ NSViewの大きさを拡張する。NSViewの拡張に合わせて(3)マウストラッキングエリアを更新する。これはNSViewの大きさ全体を登録しておく。どのボタンがマウスカーソルの下に来ているかはこのクラスで判定して処理する。最後(4)に再描画しておしまい。
- (void)addButtonWithImageResource:(NSString*)resource tag:(UInt)tag
{
//
// (1) setup ThinButton object
//
NSImage *image = [[[NSImage alloc]
initWithContentsOfFile:[[NSBundle mainBundle]
pathForImageResource:resource]] autorelease];

[image setFlipped:YES];

NSRect frame;
frame.origin.x = _offsetX;
frame.origin.y = 0.0;
frame.size = [image size];
frame.size.width += 1.0;
frame.size.height += 1.0;

ThinButton *button = [[[ThinButton alloc] initWithImage:image
frame:frame
tag:tag] autorelease];
[_list addObject:button];


//
// (2) managing offset and frame
//
_offsetX += frame.size.width + TB_MARGIN_WIDTH;

NSSize new_size = [self frame].size;
new_size.width = _offsetX;
if (new_size.height < frame.size.height) {
new_size.height = frame.size.height;
}
[self setFrameSize:new_size];


//
// (3) rearrange tracking area
//
if (_tracking_area) {
[self removeTrackingArea:_tracking_area];
[_tracking_area release];
}

NSRect tracking_rect = [self frame];
tracking_rect.origin = NSZeroPoint;
_tracking_area = [[NSTrackingArea alloc] initWithRect:tracking_rect
options:(NSTrackingMouseEnteredAndExited |
NSTrackingMouseMoved |
NSTrackingActiveInKeyWindow)
owner:self
userInfo:nil];
[self addTrackingArea:_tracking_area];

//
// (4) redraw
//
[self setNeedsDisplay:YES];
}



続いて描画コード。_list からボタン(ThinButton)をひとつずつ取り出して NSImage#drawAtPoint: で描画する。ボタンの状態によって描画時の透明度を制御している。
- (void)drawRect:(NSRect)rect {

[NSGraphicsContext saveGraphicsState];
[_shadow set];

CGFloat alpha;
for (ThinButton *button in _list) {
switch ([button state]) {
case TB_STATE_NORMAL:
alpha = 0.25;
break;
case TB_STATE_OVER:
alpha = 1.0;
break;
case TB_STATE_PUSHED:
alpha = 0.5;
break;
}

[[button image] drawAtPoint:[button frame].origin
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:alpha];

}
[NSGraphicsContext restoreGraphicsState];
}


後はマウスイベントハンドリングのコードが続く。オーバライドしたのは次のメソッド。

mouseEntered: マウスカーソルがボタンの上に載った時の処理(状態を TB_STATE_OVERへ変更)
mouseExited: マウスカーソルがビューの外に出た時の処理(状態を TB_STATE_NORMALへ戻す)
mouseMoved: マウスカーソルがビュー内で移動した時の処理(カーソル下のボタンの状態を TB_STATE_OVERへ変更し、それ以外を TB_STATE_NORMALにする)
mouseDown: マウスが押されたときの処理(状態を TB_STATE_PUSHEDへ変更)
mouseUp:   マウスが離された時の処理(状態を TB_STATE_OVERへ変更し、_delegateの clickedAtTag: を呼出す)

- (void)mouseEntered:(NSEvent *)theEvent {

[self changeState:TB_STATE_OVER withEvent:theEvent];
}

- (void)mouseExited:(NSEvent *)theEvent {
if (!_pushed_button) {
[self changeState:TB_STATE_NORMAL withEvent:theEvent];
}
}

- (void)mouseMoved:(NSEvent *)theEvent {
[self changeState:TB_STATE_OVER withEvent:theEvent];
}

- (void)mouseDown:(NSEvent *)theEvent {
_pushed_button = [self changeState:TB_STATE_PUSHED withEvent:theEvent];
}

- (void)mouseUp:(NSEvent *)theEvent {
ThinButton *hitButton = [self changeState:TB_STATE_OVER withEvent:theEvent];

if (hitButton == _pushed_button) {
if (hitButton && [_delegate respondsToSelector:@selector(clickedAtTag:)]) {
[_delegate performSelector:@selector(clickedAtTag:)
withObject:[NSNumber numberWithInt:[hitButton tag]]];
}
}
_pushed_button = nil;
}


ボタンを離す mouseUp: で _delegate#clickedAtTag: を呼出すようにしている。コンパイル時の Warningが嫌だったので直接[_delegate clickedAtTag:]と呼出すのではなく performSelector:withObject: を使ってイベントを伝達させている。念のため respondsToSelector: でメソッドの実装チェックも行っておいた。


どのメソッドも ThinButtonBar#changeState:withEvent: を呼出している。ここではマウスカーソル下のボタンを指定状態(state)へ変更し、それ以外のボタンの状態を TB_STATE_NORMALへ変更している。状況によってはこのコード処理は冗長な部分もあるのだが、この1つのメソッドに処理がまとめられたので、呼出しもとのメソッドがかなりすっきりとした。
- (ThinButton*)changeState:(UInt)state withEvent:(NSEvent*)theEvent
{
NSPoint p = [self convertPoint:[theEvent locationInWindow] fromView:nil];
ThinButton* hitButton = nil;
for (ThinButton* button in _list) {
if ([button hitAtPoint:p]) {
hitButton = button;
[button setState:state];
} else {
[button setState:TB_STATE_NORMAL];
}
}
[self setNeedsDisplay:YES];
return hitButton;
}