ページ

2009年12月18日金曜日

NSTableView にカスタムセルを表示する (13) ボタンをつける〜マウスイベントの結果をセルで使う

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

(前回)Cocoaの日々: NSTableView にカスタムセルを表示する (12) ボタンをつける〜NSTableViewのサブクラスでマウスイベント処理

前回まででNSTableView(のサブクラス)でマウスイベント処理のカスタマイズができた。 このNSTableViewからのイベントは、最終的に今後セル上に配置する予定のボタンで処理できるようにする必要がある。

(イメージ)

マウスイベント
 ↓
CustomTableView(NSTableView)
 ↓
CustomCell(NSCell)
 ↓
ボタン(NSObject、多分)


ボタンを作成する前に、今回はマウスイベントを CustomCellへ伝える処理を考えてみる。


イベントの伝搬

前回実装した CustomTableView のマウスイベントハンドラにセルへイベントを伝搬させるコードを追加する。

※mouseDown: の場合

CustomTableView.m

- (void)mouseDown:(NSEvent*)theEvent
{
if ([self isVisible:theEvent]) {
CustomCell* targetCell = [self cellOnMouse:theEvent];
[targetCell handleMouseDown:theEvent];
}
[super mouseDown:theEvent];
}

セル側に handlMouseDown: を投げる。-[cellOnMouse:] はマウスが載っているセルを返す。

- (CustomCell*)cellOnMouse:(NSEvent*)theEvent
{
NSPoint mp = [self convertPointFromBase:[theEvent locationInWindow]];
NSInteger column = [self columnAtPoint:mp];
NSInteger row = [self rowAtPoint:mp];

CustomCell* targetCell = (CustomCell*)[self preparedCellAtColumn:column row:row];
return targetCell;
}

セルにイベントが送られるとたいていの場合セルの再描画が発生する。-[setNeedDisplay:YES] が手っ取り早いのだがパフォーマンスを考慮して必要なセルだけ再描画する。NSTableViewではセル単位での再描画を支援するメソッドがあるのでそれを使う。


- (void)redrawCell:(NSEvent*)theEvent
{
NSPoint mp = [self convertPointFromBase:[theEvent locationInWindow]];
NSInteger column = [self columnAtPoint:mp];
NSInteger row = [self rowAtPoint:mp];

if (column != previousColumn || row != previousRow) {
if (previousColumn >= 0 && previousRow >= 0) {

[self setNeedsDisplayInRect:
[self frameOfCellAtColumn:previousColumn row:previousRow]];
}
}
if (column >= 0 && row >= 0) {
[self setNeedsDisplayInRect:[self frameOfCellAtColumn:column row:row]];
}
previousColumn = column;
previousRow = row;
}


セルをまたぐマウスの移動の場合は、移動前のセルの再描画も必要なので直前のセル位置(previousRow, previousColumn)を取っておく。

mouseEntered: はこんな感じ。

- (void)mouseEntered:(NSEvent *)theEvent
{
if ([self isVisible:theEvent]) {
CustomCell* targetCell = [self cellOnMouse:theEvent];
[targetCell handleMouseEntered:theEvent];
[self redrawCell:theEvent];
}
[super mouseEntered:theEvent];
}



セルでのイベント処理

NSTableView でやっかいなのは1列につきセルのインスタンスが基本的に1つであること。テーブルが100行で構成されていても1つのインスタンスが使い回される。このことはセルに表示用の状態を持たせるのが難しいということを意味する。つまり各セルを描画する時に自分自身の表示状態を持たないために、状態に応じた描画ができない。objectValue 先のモデルへ持たせる方法もあるが、セルの選択状態など画面上だけで意味のある値(=一時的な値)をここへ持たせるのはあまりいい使い方ではない。

いろいろ考えた上、インスタンス変数に現在マウスイベントを処理中のセルの objectValue を保持するようにしておく。

CustomCell.h

@class CustomCellButton;
@interface CustomCell : NSCell {

 :
id handlingValue;
}

@property (retain) id handlingValue;

:


マウスがセル上に入ってきたら、その時のセルの値を取っておく。

-(void)handleMouseEntered:(NSEvent*)theEvent
{
self.handlingValue = [self objectValue];
}



セルの値は今回 NSManagedObjectID が入っているので handlingValue にはそれが入る。これを描画する時に使う。

こんな感じ。

- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
if (handlingValue == [self objectValue]) {
[[NSColor lightGrayColor] set];
NSRectFill(cellFrame);
} else {
[[NSColor whiteColor] set];
NSRectFill(cellFrame);
}

  :
}



今回は、-[NSCell drawWithFrame:inView:] の中で handlingValue と -[objectValue] を比較して一致する場合は背景をグレーにしてみた。これでマウスをセルの上に持っていくと背景がグレーとなる。もちろんマウスがセルの上から外れると元に(白)に戻る。

handlingValue にはセルを特定できるユニークな値が入っていることが条件になるが、通常の用途では 同じ NSManagedObjectID を別々の行で表示することは無いので問題ないだろう。


NSTableView の選択時ハイライト描画を無くす

NSTableViewは選択されている行の背景が自動的にグレーとなる。これを隠す設定メソッドは用意されていないが -[highlightSelectionInClipRect:] をオーバーライドすると描画内容を替えられるのでこれを使う。


- (void)highlightSelectionInClipRect:(NSRect)clipRect
{
// do nothing
}


何もやらなければ、選択行の描画(背景グレー)は行われない。


実行

さて実行してみよう。



いい感じだ。 静止画だとわからないがちゃんとマウスの移動に追随してセルが灰色になる。


NSManagedObjectContext と CustomCell(NSCell)を Nib に入れる

オブジェクト間の接続をコードでやるのが煩わしくなったので構成を見直した。具体的には、NSManagedObjectContext と CustomCell を IntefaceBuilder でインスタンス登録して、それを NSArrayController などとワイヤー接続するようにした。そのおかげでメインのコントローラの処理は1行で済むようになった。

CustomCellWithCoredata_AppDelegate.m

- (void)awakeFromNib
{
 [ui_moc setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
}


ui_moc は Nib 内に作った NSManagedObjectContext。CoreData では NSManagedObjectContext を用途によって複数使い分けることができる。唯一 NSPersistentStoreCoordinator との接続だけは Nib内ではできないのでコードで紐づけている。Interface Builder で NSPersistentStoreCoordinator が用意されれば、全部 GUIベースで設定ができるのでかなり便利だと思う(誰か作っていないだろうか?)。以下、Interface Builder の設定画像を載せておく。








ソースコード
GitHubからどうぞ

CustomCellWithCoredata at 20091216 from xcatsan's SampleCode - GitHub

- - - -

イベントがセルまで到達できた。次はボタンを作る。