これまでは、ビューをベースとしてコントロールを作成してきましたが、これからセルをベースとした本格的なコントロールを作成して行きたいと思います。
セルを作る前に、まず土台となるNSControlのサブクラスを作ります。
@interface MyControl : NSControl @end
@implementation MyControl
+ (Class)cellClass
{
return [MyCell class];
}
@end
実装が必須となっているのは、+cellClassだけです。ここでコントロールの上に乗せるセルのクラスを指定をします。そして、その他にセル固有のプロパティーなどがあれば、それも実装します。
- (NSRect)area
{
return [[self cell] area];
}
- (void)setArea:(NSRect)anArea
{
[[self cell] setArea:anArea];
}
この場合、アクセッサによるプロパティーへのアクセスは、すべてセルに中継します。
それでは土台ができたところで、コントロールの本体であるセルを実装します。まず、値とプロパティーの実装コードは以下のようになります。
@interface MyCell : NSActionCell
{
NSRect area;
}
@property NSRect area;
@end
@implementation MyCell
- (id)init
{
if(!(self = [super init])) return nil;
self.area = NSMakeRect(0.0, 0.0, 100.0, 100.0);
return self;
}
- (NSRect)area
{
return area;
}
- (void)setArea:(NSRect)aRect
{
area = aRect;
NSPoint value = NSMakePoint(NSMidX(area), NSMidY(area));
[self setObjectValue:[NSValue valueWithPoint:value]];
}
@end
セルの値であるvalueプロパティーがどこにも定義されていないのは、これがobjectValueとして既に実装済みだからです。ただし、これはid型なので、NSPoint型などid以外の型を使いたい場合は、NSValueを使ってラップする必要があります。
セルの描画には、ビューのうえにセルを乗せたときに、drawRect:から呼び出したdrawWithFrame:inView:をオーバーライドします。
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
[self setControlView:controlView];
[NSGraphicsContext saveGraphicsState];
NSRectClip(cellFrame);
NSDrawGrayBezel(cellFrame, cellFrame);
NSPoint value = [[self objectValue] pointValue];
CGFloat ratioX = (value.x - area.origin.x) / area.size.width;
CGFloat ratioY = (value.y - area.origin.y) / area.size.height;
NSPoint point;
point.x = ratioX * cellFrame.size.width + cellFrame.origin.x;
point.y = ratioY * cellFrame.size.height + cellFrame.origin.y;
NSBezierPath* path = [NSBezierPath bezierPath];
[path appendBezierPathWithArcWithCenter:point
radius:10.0
startAngle:0.0
endAngle:360.0];
[[NSColor whiteColor] set];
[path stroke];
[NSGraphicsContext restoreGraphicsState];
}
cellFrameがセル全体の矩形を表すので、その範囲に収まるようにセル画像を描画します。
[self setControlView:controlView]で自身が置かれているビューを保存しておけば、セルの値が変化したときに、自動的にビューを再描画するようになります。
セルのマウスイベント処理は、ドラッグ処理を踏まえた形になっています。セルの領域内でマウスダウンが発生すると、まず、-trackMouse:inRect:ofView:untilMouseUp:が呼ばれます。そして次に、このメソッドの内部から-startTrackingAt:inView:が呼ばれます。マウスダウンだけに反応するセルなら、-startTrackingAt:inView:をオーバーライトして処理を追加し、NOを返します。
もし、セルでドラッグを処理するなら、-startTrackingAt:inView:をオーバーライドして、YESを返すと、次に-continueTracking:at:inView:が呼ばれます。以降ドラッグ中はずっと連続して-continueTracking:at:inView:が呼ばれるのでここにドラッグの本体処理を書きます。そして、このメソッドでYESを返し続けている限りドラック処理は継続されます。
マウスアップまたはマウスカーソルがセル領域から外れてドラッグ処理が終了すると、最後に-stopTracking:at:inView:mouseIsUp:が呼ばれます。これをオーバーライドしてドラッグの後処理を書く事ができます。
- (BOOL)startTrackingAt:(NSPoint)startPoint inView:(NSView *)controlView
{
[self updateValueWithLocation:startPoint];
return YES;
}
- (BOOL)continueTracking:(NSPoint)lastPoint
at:(NSPoint)currentPoint
inView:(NSView *)controlView
{
[self updateValueWithLocation:currentPoint];
return YES;
}
-continueTracking:at:inView:のパラメータのうち、lastPointがセルを基点とした前回のマウスの位置、currentPointが現在のマウス位置となっています。これによって、マウスの移動量を計算することはできますが、今回のトラックパッド・コントロールではセル全体のサイズが分からないとセルの値を算出できないので、-trackMouse:inRect:ofView:untilMouseUp:をオーバーライドしてセルの矩形を保存するようにしています。
@interface MyCell : NSActionCell
{
...
NSRect trackRect;
}
...
@end
- (BOOL)trackMouse:(NSEvent *)theEvent
inRect:(NSRect)cellFrame
ofView:(NSView *)controlView
untilMouseUp:(BOOL)untilMouseUp
{
trackRect = cellFrame;
return [super trackMouse:theEvent
inRect:cellFrame
ofView:controlView
untilMouseUp:untilMouseUp];
}
- (void)updateValueWithLocation:(NSPoint)location
{
CGFloat ratioX = (location.x - trackRect.origin.x) / trackRect.size.width;
CGFloat ratioY = (location.y - trackRect.origin.y) / trackRect.size.height;
NSPoint newValue;
newValue.x = ratioX * area.size.width + area.origin.x;
newValue.y = ratioY * area.size.height + area.origin.y;
[self setObjectValue:[NSValue valueWithPoint:newValue]];
}
ターゲット&アクションとバインディングは、すでに実装済みなので、特にコードを書く必要はありません。
ターゲット&アクションについては、NSControlに-setTarget:や-setAction:などのメソッドがあり、デフォルトでNSCellの-setTarget:や-setAction:に中継されるような実装になっています。しかし、実を言うとNSCellの-setTarget:や-setAction:の実装は空っぽで、もし呼び出すと例外を出力するようになっています。ターゲット&アクションはNSCellのサブクラス側で実装しなければならないのです。そして、これを既に実装しているのがNSActionCellで、ターゲット&アクションが必要なセルを作る時は通常NSActionCellからサブクラスを作ります。
バインディングについては、valueというバインド項目名でセルの値にバインドができます。