Cropped Image
1 概要
このサンプルは、ある画像の一部から別の画像を作る。画像の一部を指定するために、選択範囲を使う。このサンプルによって、次のことが学べる。
- 選択範囲の表示や取り扱い
- 画像の一部から別の画像を作成する方法
このサンプルを実際に動かして確かめてみよう。
起動すると、上図のように左右2つのビューが並んだウインドウが表示される。右下の「Mandrill」ボタンを押すと、マンドリル猿の画像が表示される。
上図のように、左のビュー内でマウスドラッグを行なうと選択範囲が形成される。そして、右のビューにその部分の画像が切りとられて表示される。左下のボタンの「Update Continously」チェックすると、ドラッグ中でも右のビューに選択範囲内の画像が表示される。チェックが外れると、マウスが上がってドラッグが終了し選択範囲が確定されたときに、右のビューに画像が表示される。上の図は、下のポップアップメニューから、「Finder Style」を選んだ場合の選択範囲である。「plain」なら、単に色がついた長方形が選択範囲を表す。「iPoto Style」では、選択範囲外が色づけされる。「Lasso Style」では、マウスの動きに応じてパスが作られていき、不定形の選択範囲を作ることができる。これを下の図に示します。
このとき、「Smooth Edges」をチェックすることで角をまるめられる。また、ポップアップメニューの左のカラーウェルで、選択範囲の色を変えることもできる。
注意:この内容は 2004 年 2 月に、個人的な覚え書きとして作成されました。その後一部修正しているものの、古い内容が残っている可能性があることに注意してください。
2 プロジェクトのファイル構成
では、プロジェクトを構成しているファイルを見ていく。まず、Xcode でプロジェクトファイル(Cropped Image.pbproj)を開く。
CropImageController.h と .m は、このサンプルの中心となるコントローラー。CroppingImageView.h と .m は、選択範囲を処理できる NSImageView のサブクラス。CropMarker、FinderStyleCropMarker、IPhotoStyleCropMarker、LassoStyleCropMarker は、選択範囲の表示などを受け持つクラス。NSBezierPathExtensions.h と .m は、NSBezierPath に新しいカテゴリを追加する。
Resources のところには、「mandrill.jpg」という画像があるが、これが右下のボタンによって、ウィンドウの中に表示されるもの。「MainMenu.nib」には、ユーザーインターフェイス等の定義がなされている。
3 MainMenu.nibの構成
次に、MainMenu.nib をダブルクリックして、Interface Builder でその中身を見てみる。
MainMenu.nib のウィンドウを見ると、先程出てきた CropImageController クラスのインスタンスがある。これを作るには、NSObject のサブクラスを作ってインスタンス化する。まず最初にアウトレットの状態を見てみよう。
まずウインドウ全体が window に、左側のビューが imageView に、右側のビューは croppedImageView に接続されている。そして、左下のチェックボックスは左から、continuousModeCheckBox、antiAliasCheckBox に接続されている。右下はカラーウェルが colorWellに、スタイルを選ぶポップアップメニューが popUp に接続されている。次にアクションの接続を見る。
アクションはコントローラーへ接続されているものと、左側のビューに接続されているものに大別できる。
コントローラーへは、左側と右側のビューが同じ imageChanged メソッドに、madrill ボタンが showTheApe: メソッドへ接続されている。
ビューに対しては、左下のチェックボックスから、takeContinuousModeFrom、takeAntiAliasModeFrom メソッドへ、カラーウェルから takeSelectionColorFrom へ、ポップアップメニューから takeSelectionMarkerStyleFrom へ接続されている。これらは、ビュー内の選択範囲の表示の仕方を変更するためのメソッドである。
4 CroppingImageView クラス
このクラスは NSImageView を表示している画像の一部を選択できるように拡張したものである。通常の NSImageView の機能とべつに、CroppingImageView はイベントを CropMarker に送り、選択された画像をその croppedImage: メソッドから得ることができる。クラスのインターフェイスは次のようになっている。
CroppingImageView.h
@interface CroppingImageView : NSImageView
{
id selectionMarker;
BOOL shouldAntiAlias;
}
- (IBAction) takeSelectionMarkerStyleFrom:sender;
- (IBAction) takeSelectionColorFrom:sender;
- (IBAction) takeAntiAliasModeFrom:sender;
- (IBAction) takeContinuousModeFrom:sender;
- (NSImage *) croppedImage;
- (void) selectionChanged;
- (void) postSelectionChangedNotification;
- (void) setSelectionMarker:(CropMarker *) marker;
@end
extern NSString *selectionChangedNotification;
このクラスは追加のインスタンス変数を定義している。selectionMarker は CropMarker クラスへの参照を保持する。shouldAntiAlias はアンチエイリアスをオンにするかどうかを示す。
また、このクラスのインプリメンテーションファイルでは、次のような定数を定義している。
CroppingImageView.m
enum
{
plainMarkerStyle, // Simple rectangle
finderMarkerStyle, // Rectangle bordered with a solid color, and its interior is tinted.
iPhotoMarkerStyle, // Area outside the rectangle is tinted.
lassoMarkerStyle // Path is stroked, and filled with a tint.
};
NSString *selectionChangedNotification = @"ImageSelectionChanged";
@implementation NSImageCell (CroppingImageView)
- (NSRect) rectCoveredByImageInBounds:(NSRect) bounds
// This is a work-around to deal with the fact that NSImageCell won't tell me the rectangle *actually* covered by its image, but NSCell will.
{
return [super imageRectForBounds:bounds];
}
@end
最初の列挙定数は、CropMarker の種類を表す定数である。次の NSString は選択範囲が変更されたことを伝える通知に使われる。そして、その次にこのクラスでは、NSImageCell にカテゴリを追加し、rectCoveredByImageInBonuds: というメソッドを追加している。
このクラスにおける -mouseDown:、-mouseDragged:、-mouseUp: の実装に注意しよう。マウストラッキングコードはイベントループを乗っとる必要がない。これが Cocoa におけるマウストラッキングを実装するための好ましいやり方である。
CroppingImageView.m : mouseDown:
- (void) mouseDown:(NSEvent *) theEvent
{
[selectionMarker mouseDown:theEvent];
}
mouseDown: メソッドは、単に selectionMarker にマウスが押されたことを伝える。
CroppingImageView.m : mouseDragged:
- (void) mouseDragged:(NSEvent *) theEvent
{
[selectionMarker mouseDragged:theEvent];
if ([self isContinuous])
[self postSelectionChangedNotification];
[self selectionChanged];
}
mouseDragged: メソッドは、selectionMarker にマウスがドラッグされたことを伝え、もし isContinuous が YES を返せば、選択範囲が変更されたことを通知する。そのあと自分自身の selectionChanged メソッドを実行する。
CroppingImageView.m : postSelectionChangedNotification
- (void) postSelectionChangedNotification
{
[[NSNotificationCenter defaultCenter] postNotificationName:selectionChangedNotification object:self];
}
このメソッドは、単に選択範囲が変更されたことを通知するだけである。これを使って、もう1つのビューを再表示できる。
CroppingImageView.m : selectionChanged
- (void) selectionChanged
{
[self setNeedsDisplay:YES];
}
このメソッドは単にこのビューが再表示される必要があることを指定するだけである。次に mouseUp: メソッドを見てみる。
CroppingImageView.m : mouseUp:
- (void) mouseUp:(NSEvent *) theEvent
{
[selectionMarker mouseUp:theEvent];
[self selectionChanged];
[self postSelectionChangedNotification];
// This is how the controller knows to redraw the second NSImageView.
}
まず selectionMarker にマウスが離されたことを伝える。そして、自分自身の selectionChanged メソッドを実行し再表示をするようにする。そして、選択範囲が変更されことを通知し、もう1つのビューの再表示を行えるようにする。
選択範囲の表示は、このクラスの drawRect のなかで行われている。
CroppingImageView.m : drawRect:
- (void)drawRect:(NSRect)rect
{
[super drawRect:rect];
[selectionMarker drawCropMarker];
}
このように、上位クラスの描画のあとに選択範囲の描画を行っているだけである。
-croppedImage メソッドは Tinted Image サンプルのなかの -tintedImage メソッドに似ています。それは新しく作成された自動解放される NSImage を返す。
CroppingImageView.m : croppedImage
- (NSImage *) croppedImage
// Returns an autoreleased NSImage,
// consisting of the selected portion of the reciever's image.
// If there's no selection, this method will return the original image.
{
NSRect sourceImageRect
= [[self cell] rectCoveredByImageInBounds:[self bounds]],
newImageBounds
= NSIntersectionRect([selectionMarker selectedRect], sourceImageRect);
if (!NSIsEmptyRect(newImageBounds))
{
NSImage *newImage
= [[NSImage alloc] initWithSize:sourceImageRect.size];
NSAffineTransform *pathAdjustment
= [NSAffineTransform transform];
NSBezierPath *croppingPath
= [selectionMarker selectedPath];
[pathAdjustment translateXBy: -NSMinX(sourceImageRect) yBy: -NSMinY(sourceImageRect)];
croppingPath = [pathAdjustment transformBezierPath:[selectionMarker selectedPath]];
[newImage lockFocus];
if (!shouldAntiAlias)
[[NSGraphicsContext currentContext] setShouldAntialias:NO];
[[NSColor blackColor] set];
[croppingPath fill];
[[self image] compositeToPoint:NSZeroPoint operation:NSCompositeSourceIn];
[newImage unlockFocus];
return [newImage autorelease];
}
return [self image];
}
まず sourceImageRect に、このビューのバウンズのなかで画像が存在する部分の NSRect が入る。そして、それと selectionMarker の selectedRect によって返される NSRect との共通部分が newImageBounds に入る。
この newImageBonuds が空でなければ、新しく作られた選択範囲の画像が返される。空なら、画像全体が返される。
空でない場合、まず sourceImageRect と同じサイズの NSImage が作られます。NSAffineTransform として pathAdjustment が作られます。また、selectionMarker の selectedPath メソッドによって返される選択範囲のパスが croppingPath に入る。
次に NSAffineTransform を使用して、CroppingImageView と新しい NSImage の間の座標空間の違いによって、パスを調節する。
そして、 -lockfocus します。アンチエイリアスがオフなら、グラフィックコンテクトをそのように設定する。
そして描画色に黒を設定して、調節したパスを塗りつぶす。それから、NSCompositeSourceIn 演算子を使って、オリジナルの画像が、新しい画像のうえに合成される。ここで、演算子によって、黒に塗りつぶされた部分だけが新しい NSImage に描画されることになる。
もしアンチエイリアスが新しい画像の描画コンテクストでオンになっていれば、パスを満すことによって描かれるマスクはなめらかなエッジを持つことに注意する。チェックボックスでこれをオフにしてその影響を見る。マスクを見るためには、新しい画像を読み込む前に lasso style で領域を選択する。
ほかにも、take... というメソットがいくつかあり、これらは色や選択範囲のスタイルなどを設定するのに使われる。これについては、説明は不用であろう。
CroppingImageView.m : take...
- (IBAction) takeSelectionMarkerStyleFrom:sender
// I should probably check to see if this call is a No-Op. As it is, whenever this is called, I discard the existing CropMarker, and create a new one.
{
NSRect stash = [selectionMarker selectedRect]; // If we change styles, it's nice to keep the same area selected.
switch ([[sender selectedCell] tag])
{
case plainMarkerStyle:
[self setSelectionMarker:[CropMarker cropMarkerForView:self]];
[selectionMarker setSelectedRect:stash];
break;
case finderMarkerStyle:
[self setSelectionMarker:[FinderStyleCropMarker cropMarkerForView:self]];
[selectionMarker setSelectedRect:stash];
break;
case iPhotoMarkerStyle:
[self setSelectionMarker:[IPhotoStyleCropMarker cropMarkerForView:self]];
[selectionMarker setSelectedRect:stash];
break;
case lassoMarkerStyle:
[self setSelectionMarker:[LassoStyleCropMarker cropMarkerForView:self]];
break;
}
[self setNeedsDisplay:YES];
}
- (void) setSelectionMarker: marker // Should be a CropMarker or a subclass thereof, but I'm not in the mood for strong typing..
{
[marker retain];
[selectionMarker release];
selectionMarker = marker;
}
- (IBAction) takeSelectionColorFrom:sender
{
[selectionMarker setColor:[sender color]];
[self setNeedsDisplay:YES];
}
- (IBAction) takeAntiAliasModeFrom:sender
{
shouldAntiAlias = [sender intValue];
[self postSelectionChangedNotification];
}
- (IBAction) takeContinuousModeFrom:sender
{
[self setContinuous:[sender intValue]];
}
5 CropMarker クラス
この CropMarker クラスは単純な長方形の選択範囲を実装する。そして、他の選択範囲スタイルを担当するクラスの上位クラスとして機能する。長方形は、選択領域の内側でクリックしてドラッグすることによって移動できる。このクラスのインターフェイスは以下のようになる。
typedef enum _SelectionTrackingMode
{
trackNone,
trackSelecting,
trackMoving,
trackResizing // Not implemented!
// (Currently left as an exercise for the reader,
// but I may fill this in someday.)
} SelectionTrackingMode;
@interface CropMarker : NSObject
{
NSView *target;
BOOL selecting, dragging, resizing;
NSRect selectedRect;
NSPoint lastLocation;
NSColor *fillColor, *strokeColor;
SelectionTrackingMode trackingMode;
NSBezierPath *selectedPath;
}
// Convenience constructor. Use this in most cases.
+ cropMarkerForView:aView;
// Designated Intiailizer
- initWithView:(NSView *) aView;
- (void) drawCropMarker;
// The mouse-tracking methods
- (void) startSelectingAtPoint:(NSPoint) where;
- (void) continueSelectingAtPoint: (NSPoint) where;
- (void) stopSelectingAtPoint:(NSPoint) where;
- (void) startMovingAtPoint:(NSPoint) where;
- (void) continueMovingAtPoint: (NSPoint) where;
- (void) stopMovingAtPoint:(NSPoint) where;
- (void) mouseDown:(NSEvent *) theEvent;
- (void) mouseUp:(NSEvent *) theEvent;
- (void) mouseDragged:(NSEvent *) theEvent;
// Simple Accessors
- (void) setColor:(NSColor *) color;
- (void) setFillColor:(NSColor *) color;
- (void) setStrokeColor:(NSColor *) color;
- (NSBezierPath *) selectedPath;
- (NSRect) selectedRect;
- (void) setSelectedRect:(NSRect) rect;
- (void) setSelectedRectOrigin:(NSPoint) where;
- (void) setSelectedRectSize:(NSSize) size;
- (void) moveSelectedRectBy:(NSSize) delta;
@end
NSRect rectFromPoints(NSPoint p1, NSPoint p2);
// Given two corners, make an NSRect.
_SelectionTrackingMode は、マウストラックキングのモードを表す列挙定数である。rectFromPoints は、左上と右下の2つの点を与えることによって NSRect を作成して返すユーティリィティ関数である。
このクラスは、インスタンス変数として、ターゲットとなる NSView を参照する target、現在行なわれているマウス操作を示す BOOL 型の selecting、dragging、resizing、選択範囲を表す NSRect の selectedRect、最後の位置を表す NSPoint の lastLocation、選択範囲の表示時の色を格納する NSColor を参照する fillColor、strokeColor、選択範囲のトラッキングモードを表す SelectionTrackingMode 型の trackingMode、選択範囲を表す NSBezierPath を参照する selectedPath を定義する。
まず初期化メソッドを見てみよう。初期化には、クラスメソッドの cropMarkerForView: と initWithView: があるが、前者は後者を使って自動解放に設定しているだけである。
CropMarker.m : 初期化メソッド
+ cropMarkerForView:aView
{
return [[[self alloc] initWithView:aView] autorelease];
}
- initWithView:(NSView *) aView
{
if (self = [super init])
{
target = aView;
[self setColor:[NSColor blueColor]];
selectedPath = [[NSBezierPath bezierPath] retain];
[self setSelectedRect:NSZeroRect];
}
return self;
}
target にビューを設定したあと、デフォルトとして青色をセットし、新しく作成した空のパスを selectedPath に格納し、選択範囲の長方形に空の NSRect をセットする。ただし、このクラスでは実際には selectedPath は使用されていない。
次にマウスメソッドを見ていく。まずは、mouseDown: である。
CropMarker.m : mouseDown:
- (void) mouseDown:(NSEvent *) theEvent
{
lastLocation = WHERE;
if (NSPointInRect(lastLocation, selectedRect))
{
[self startMovingAtPoint:lastLocation];
return;
}
[self startSelectingAtPoint:lastLocation];
}
ここで WHERE は次のマクロで定義されている。
#define WHERE [target convertPoint:[theEvent locationInWindow] fromView:nil]
すなわち、マウスイベントの起こった場所をビューの位置に変換したものである。それを lastLocation にセットする。そして、選択範囲内にあれば、移動を開始する。そうでなければ、選択範囲の指定を開始する。start... メソッドは単純である。単にトラッキングモードを設定し、点の位置を保存しておくだけである。
CropMarker.m : start...
- (void) startMovingAtPoint:(NSPoint) where
{
trackingMode = trackMoving; lastLocation = where;
}
- (void) startSelectingAtPoint:(NSPoint) where
{
trackingMode = trackSelecting; lastLocation = where;
}
次は、マウスドラッグである。
CropMarker.m : mouseDragged:
- (void) mouseDragged:(NSEvent *) theEvent
{
switch (trackingMode)
{
case trackSelecting:
[self continueSelectingAtPoint:WHERE];
break;
case trackMoving:
[self continueMovingAtPoint:WHERE];
break;
default:
NSLog (@"Bad tracking mode in [CropMarker mouseDragged]");
}
}
トラッキングモードによって、実行される内容が変わる。選択範囲設定中ならば、選択範囲の決定を継続する。移動中なら、移動を継続する。それ以外の場合は NSLog で警告を行う。この continue... メソッドも単純である。移動中なら、選択範囲の新しい原点をセットし、選択中ならば、新しく選択範囲の長方形を計算する。
CropMarker.m : continue...
- (void) continueMovingAtPoint:(NSPoint) where
{
selectedRect.origin.x += where.x - lastLocation.x;
selectedRect.origin.y += where.y - lastLocation.y;
lastLocation = where;
}
- (void) continueSelectingAtPoint:(NSPoint) where
{
selectedRect = rectFromPoints(lastLocation,where);
}
次にマウスが離されたときの処理です。
CropMarker.m : mouseUp:
- (void) mouseUp:(NSEvent *) theEvent
{
switch (trackingMode)
{
case trackSelecting:
[self stopSelectingAtPoint:WHERE];
break;
case trackMoving:
[self stopMovingAtPoint:WHERE];
break;
default:
NSLog (@"Bad tracking mode in [CropMarker mouseUp]");
}
}
これも、trakingMode によって、実行される内容が変わる。選択範囲設定中ならば、選択範囲設定を終了する。移動中なら、移動を終了する。それ以外ならば、NSLog で警告を行う。stop.. メソッドは以下のようになる。
CropMarker.m : stop...
- (void) stopMovingAtPoint:(NSPoint) where
{
[self continueMovingAtPoint:where];
trackingMode = trackNone;
}
- (void) stopSelectingAtPoint:(NSPoint) where
{
selectedRect = rectFromPoints(lastLocation,where);
trackingMode = trackNone;
}
どちらも継続中と同じだが、最後にトラッキングモードをリセットすることだけが違っている。
このように単純な構造になっているのは、マウストラッキングの部分と、選択範囲の描画の部分が切り離されているからである。マウストラッキングにおいては、その位置によって選択範囲とモードを調節するだけになる。実際の描画は、ビューの drawRect: から呼び出される drawCropMarker が行う。
CropMarker.m : drawCropMarker
- (void) drawCropMarker
{
[strokeColor set]; NSFrameRect(selectedRect);
}
このクラスでは、単純に選択範囲の長方形を描画しているだけである。
他に、色などのさまざまな属性をセットしたり、選択範囲の数値を設定しやすくするためのメソッドなどがあるが、単純なのでここでは省略する。
6 FinderStyleCropMarker クラス
上に説明したように、選択範囲の表示は、drawCropMarker で実行される。このクラスでは、このメソッドのみをオーバーライドしている。
FinderStyleCropMarker.m : drawCropMarker
- (void) drawCropMarker
{
if (!NSIsEmptyRect(selectedRect))
{
[fillColor set];
NSRectFillUsingOperation(selectedRect, NSCompositeSourceOver);
[strokeColor set];
NSFrameRect(selectedRect);
}
}
選択範囲が空でなければ、塗りつぶしの色をセットした後で、NSCompositeSourceOver を使って塗りつぶす。この塗りつぶしの色は上位クラスでセットされ、透明度が20%にセットされている。その後で、輪郭の色をセットし、長方形を描画する。
7 IPhotoStyleCropMarker クラス
このクラスも drawCropMarker をオーバーライドしているだけである。
IPhotoStyleCropMarker.m : drawCropMarker
- (void) drawCropMarker
{
if (!NSIsEmptyRect(selectedRect))
{
NSBezierPath
*cutout = [NSBezierPath bezierPathWithRect:NSInsetRect([target bounds], 5.0, 5.0)];
[cutout appendBezierPathWithRect:selectedRect];
[cutout setWindingRule:NSEvenOddWindingRule];
[fillColor set];
[cutout fill];
[strokeColor set];
NSFrameRect(selectedRect);
}
}
iPhoto スタイルは、選択領域の外の領域を色づけする。これは、2つの長方形からなる NSBezierPath を作成することによってなされる。ビューを囲む長方形と、選択された長方形である。選択された長方形の内側を色づけしないために、ワィンディングの奇数/偶数ルールを使って、このパスをぬりつぶしている。
まず cutout にターゲットとなるビューのバウンズから内側に5ピクセルだけ小さい長方形がセットされ。その後で、選択範囲の長方形がパスに追加される。そして、塗りつぶしの色をセットしたあと、塗りつぶしを実行している。その後、輪郭の色をセットして、輪郭を描いている。
8 LassoStyleCropMarker
このクラスでは、選択範囲をパスとして扱う。そのため、drawCropMarker 以外のメソッドもオーバーライドされている。まず mouseDown: メソッドは次のようになる。
LassoStyleCropMarker.m : mouseDown:
- (void) mouseDown:(NSEvent *) theEvent
{
lastLocation = [target convertPoint:[theEvent locationInWindow] fromView:nil];
if ([[selectedPath closedPath] containsPoint: lastLocation])
{
[self startMovingAtPoint:lastLocation];
return;
}
[self startSelectingAtPoint:lastLocation];
}
選択範囲がパスの内側かどうかをチェックして、内側なら移動を開始し、外側なら選択を開始する。startSelectingAtPoint: メソッドのみオーバーライドされている。移動は一緒である。
LassoStyleCropMarker.m : startSelectingAtPoint:
- (void) startSelectingAtPoint:(NSPoint) where
{
lastLocation = where;
trackingMode = trackSelecting;
[selectedPath removeAllPoints];
[selectedPath moveToPoint:where];
}
位置とモードをセットした後で、選択パスをすべて削除し、新しい点へと動く。
マウスドラッグのメソッドはそのままだが、実際に移動と選択を行なうメソッドはオーバーライドされている。
LassoStyleCropMarker.m : continue...
- (void) continueMovingAtPoint:(NSPoint) where
{
NSAffineTransform
*transform = [NSAffineTransform transform];
[transform translateXBy: where.x - lastLocation.x yBy:where.y - lastLocation.y];
[selectedPath transformUsingAffineTransform:transform];
lastLocation = where;
}
- (void) continueSelectingAtPoint:(NSPoint) where
{
[selectedPath lineToPoint:where];
}
移動は NSAffinTransform を使っている。選択はたんに新しい点へと移っているだけである。
ドラッグ終了も、選択メソッドをオーバーライドしているだけである。
LassoStyleCropMarker.m : stopSelectingAtPoint:
- (void) stopSelectingAtPoint:(NSPoint) where
{
[selectedPath lineToPoint:where];
[selectedPath closePath];
trackingMode = trackNone;
}
新しい点へ移動してパスを閉じる。そしてトラッキングモードをリセットする。
選択範囲を描く drawCropMarker は、次のようになる。
LassoStyleCropMarker.m : drawCropMarker
- (void) drawCropMarker
{
if (selectedPath)
{
NSBezierPath
*path = trackingMode == trackSelecting ? [selectedPath closedPath] : selectedPath;
[fillColor set];
[path fill];
[strokeColor set];
[path stroke];
}
}
選択パスが存在したら、選択中ならば、closedPath メソッドで一時的に閉じられたパスを、そうでなけば選択パスを使って、塗りつぶしと輪郭の描画を行う。
管理人:神吉 秀典 E-mail: