まずは、もっとも基本となるNSViewのサブクラスを作ることによって、コントロールを作成する方法を紹介します。
コントロールとして機能させるために以下のものを実装します。
今回はコントロールの値として、トラックパッド上の位置を表すNSPoint型の値を実装します。また、関連するプロパティーとしてトラックパッドの領域を表すNSRect型のareaプロパティーも実装します。いずれもインスタンス変数にして、アクセッサーを書きます。
@interface MyView : NSView
{
NSPoint value;
NSRect area;
}
@property NSPoint value;
@property NSRect area;
@end
@implementation MyView
- (id)initWithFrame:(NSRect)frameRect
{
if(!(self = [super initWithFrame:frameRect])) return nil;
self.area = [self bounds];
return self;
}
- (NSPoint)value
{
return value;
}
- (void)setValue:(NSPoint)aPoint
{
if(aPoint.x < NSMinX(area)) aPoint.x = NSMinX(area);
if(aPoint.x > NSMaxX(area)) aPoint.x = NSMaxX(area);
if(aPoint.y < NSMinY(area)) aPoint.y = NSMinY(area);
if(aPoint.y > NSMaxY(area)) aPoint.y = NSMaxY(area);
if(NSEqualPoints(value, aPoint)) return;
value = aPoint;
}
- (NSRect)area
{
return area;
}
- (void)setArea:(NSRect)aRect
{
area = aRect;
self.value = NSMakePoint(NSMidX(area), NSMidY(area));
}
@end
-setValue:では、渡された値が正しい値なのか検証する処理を含めています。
-setArea:では、トラックパッドのエリア変更に伴って、コントロール値の再設定も行っています。
NSViewは-initWithFrame:によって初期化されるため、これをオーバーライドしてareaプロパティーの初期化をしています。
NSViewはビューの再描画要求を受けると描画環境を初期化した後に-drawRect:を呼び出すようになっているため、これをオーバーライドしてコントロールの値を参照しながらコントロールを描画するコードを書きます。
- (void)drawRect:(NSRect)aRect
{
NSDrawGrayBezel([self bounds], [self bounds]);
NSPoint point;
CGFloat viewWidth = [self bounds].size.width;
CGFloat viewHeight = [self bounds].size.height;
point.x = (value.x - area.origin.x) / area.size.width * viewWidth;
point.y = (value.y - area.origin.y) / area.size.height * viewHeight;
NSBezierPath* path = [NSBezierPath bezierPath];
[path appendBezierPathWithArcWithCenter:point
radius:10.0
startAngle:0.0
endAngle:360.0];
[[NSColor whiteColor] set];
[path stroke];
}
- (void)setValue:(NSPoint)aPoint
{
...
[self setNeedsDisplay:YES];
}
-drawRect:のパラメータであるaRectは、描画を要求されている領域です。多くの場合はビュー全体の描画を要求されるため、これはビューと同じサイズの矩形になります。
通常、コントロールは自身の値が更新されると、すぐにその結果が反映されるようになっています。そのため、-setValue:内に[self setNeedsDisplay:YES]と書いて、値が設定されるとともに描画が更新されるようにします。
ビュー内をマウスダウンやドラッグが発生すると、NSViewのスーパークラスであるNSResponderの-mouseDown:や-mousesDragged:が呼ばれるようになっているので、これらをオーバーライドしてマウスダウン時やドラッグ時の処理を実装します。
- (void)mouseDown:(NSEvent *)theEvent
{
[self updateValueWithEvent:theEvent];
}
- (void)mouseDragged:(NSEvent *)theEvent
{
[self updateValueWithEvent:theEvent];
}
- (void)updateValueWithEvent:(NSEvent*)theEvent
{
NSPoint locationInWindow = [theEvent locationInWindow];
NSPoint location = [self convertPoint:locationInWindow fromView:nil];
NSPoint newValue;
CGFloat viewWidth = [self bounds].size.width;
CGFloat viewHeight = [self bounds].size.height;
newValue.x = location.x / viewWidth * area.size.width + area.origin.x;
newValue.y = location.y / viewHeight * area.size.height + area.origin.y;
self.value = newValue;
}
-mouseDown:や-mouseDragged:のパラメータであるtheEventにはそのイベントに対する詳細な情報が格納されており、locationInWindowで得られるマウスダウンが発生した位置もその一つです。ただし、ここで得られるマウス位置はウインドウの原点座標を基点としているため、NSViewの-convertPoint:fromView:でビューの原点を基点とした座標に変換した上でその後の計算をしています。
今回は、トラックパッドコントロールということで、ビュー内のマウス位置からコントロール値を更新しています。そして、コントロール値を更新するときに-setValue:が呼ばれて、再描画が行われます。
コントロールにターゲットとアクションが設定されていれば、ユーザーによって操作されたときに、ターゲットオブジェクトに対してアクションが送信されます。例えば、ボタンならマウスでクリックされたとき、テキストフィールドなら文字入力が完了したときなどにアクションが送信されます。今回のトラックパッドでは、マウスダウンやドラッグでアクションを送信するようにします。
@interface MyView : NSView
{
...
id target;
SEL action;
}
...
@property(assign, readwrite) id target;
@property SEL action;
@end
@implementation MyView
@synthesize target;
@synthesize action;
- (void)updateValueWithEvent:(NSEvent*)theEvent
{
...
if(action != nil)
{
[NSApp sendAction:action to:target from:self];
}
}
@end
まず、targetとactionプロパティーとそのアクセッサーを書きます。これらは、単純に値を保持するだけでいいので、@synthesizeで実装コードを生成します。
つぎに、アクションを送信したい箇所でNSApplicationの-sendAction:to:from:を呼びだせば、指定されたtargetオブジェクトのactionメソッドが呼ばれます。もしここでtargetがnilだった場合は、actionメソッドを実装したオブジェクトがファーストレスポンダから順番に検索されるようになります。
バインディングを使うと、ビューとモデル間でプロパティーの同期を取れるようになります。たとえば、ModelオブジェクトのnameプロパティーとViewオブジェクトのtitleプロパティーをバインドすると、nameプロパティーに値が設定されたときに、同時にtitleプロパティーにも値が設定されることになります。
@implementation MyView
+ (void)initialize
{
[MyView exposeBinding:@"value"];
}
- (void)setValue:(NSPoint)aPoint
{
...
NSDictionary* bindInfo = [self infoForBinding:@"value"];
if(bindInfo)
{
id modelObject = [bindInfo valueForKey:NSObservedObjectKey];
NSString* modelKeyPath = [bindInfo valueForKey:NSObservedKeyPathKey];
id newValue = [NSValue valueWithPoint:aPoint];
[modelObject setValue:newValue forKeyPath:modelKeyPath];
}
}
@end
まず、クラスの初期化メソッドである+initializeメソッド内で、exposeBinding:を呼び出し、バインド項目としてクラス外部に公開したいプロパティー名を指定します。これで、指定したプロパティー名がそのままバインド項目名となり、bind:toObject:withKeyPath:options:メソッドを使ってバインドすることができるようになります。バインドすると、モデルのプロパティーが変更されたときに、ビュー側のプロパティーのセッターメソッドが自動的に呼ばれるようになります。
ただし、このままだとビューのプロパティーを変更しても、自動的にモデル側のプロパティーが更新されたりはしません。この部分は手動で実装する必要があります。
bind:toObject:withKeyPath:options:によってバインドされると、infoForBinding:でバインド情報を参照できるようになるので、ここからバインド元のmodelオブジェクトとプロパティー名を取り出し、setValue:ForKeyPath:で値の設定をします。