Cocoa Break Logo Top Soft Develop  脱力空間 Logo
Apple Web Badge
made by mi

概要 翻訳 ソース リンク がらくた

概要 Examples ADC Samples 3rd Parties etc CBOriginals

CompositeLab

以下は、Mac OS X 10.4.11 上の Xcode 2.5 で説明しています。

目次:
1 README.rtf の日本語訳
2 アプリケーション
3 ファイル構成
4 CompositeLab.nib
5 CompositeView クラス
5.1 継承とインスタンス変数
5.2 初期化と割り当て解除
5.3 描画
5.4 アクション
5.5 ドラッグ&ドロップ
6 まとめと機能追加

1 README.rtf の日本語訳

CompositeLab は、Quartz 表示システムにおける合成モードを対話的に実演するのに役立つ単純なアプリケーションです。

NSColorWell と the NSColorPanel を使って選ばれた色(と不透明度)をもつ 2 つの別々な画像、元 (source) と対象 (destination) を、ユーザーによって指定された合成モードを使って、合成します。画像は NSImage のインスタンスに保持されます。NSView のサブクラスである CompositeView がターゲット・アクションメソッドと描画のすべてを管理します。

ユーザーは 3 つの色を選択できます。背色と元と対象です。背景色は、3 つのペインすべてのビューに塗られる色です。元画像は、最初に透明に消去され、それから元の色 (source color) を使って塗られます(元画像が画像ファイルからもたらされないかぎり、その場合元の色は無視されます)。対象画像は、最初に画像を消去し、それから対象色で塗りつぶすことで、同じ方法で作られます。それから対象画像が結果画像に Copy モードを使って合成され、そして元がユーザーが指定した合成モードを使って一番上に合成されます。最後に、3 つの画像すべてが SourceOver モードを使ってビューに表示されます。したがって、指定された背景色の上に見えるものとして、3 つの画像を見ることになります。

NSImage は、drawMethod:inObject: メソッドにおいて使われます。3 つのメソッド(drawSource:drawDestination:drawResult:)は、3 つの画像を描画するグラフィックコマンドを提供します。NSImage は、これらのメソッドを呼び出し、必要な時はいつでも描画をキャッシュします。最初に合成される時と、(recache メソッドの呼び出しによって示される)内容が変更された後で合成される時はいつでも、これが起こります。CompositeLab がプリントを許容するなら、これらのメソッドは、ウインドウサーバーにおいてキャッシュされた解像ではなく、プリンタの完全な解像度で画像を作り出ため、プリントの間にも呼ばれることになるでしょう。

CompositeLab は、アプリケーション間のドラッグメカニズムを使って、ドラッグされた画像や色を受けとる方法を実演します。最小限必要なメソッド(draggingEntered:performDragOperation:)に加えて、CompositeView は、ドロップすることなく、色をビューの上にドラッグした時のユーザーに対する反応を与えるため、draggingUpdated:draggingExited: もオーバーライドします。

2 アプリケーション

アプリケーションを起動すると、次のような画面が表示されます。

右側上の「Source(元)」、「Destination(対象)」、「Result(結果)」というタイトルが付けられた 3 つの部分をもつビューが中心になります。あとで nib ファイルを見ればわかりますが、この 3 つで 1 つのビューとなっています。元と対象の合成結果が結果に表示されます。左側の「Mode」というタイトルがついたボックス内に収められたマトリックス(ラジオボタン)は、合成するときのモードを表していて、選択を変更すると、右側の結果の表示が変わります。

右側中段の「Colors」というタイトルのついたボックス内のカラーウェルは、元と対象の塗りつぶし色と背景を選択するようになっています。このボックスからドラッグし、上のビューにドロップしてみてください。そこの色が変化します。また、ドロップしなくても、ドラッグしたままビューの上にきた時、ビューの表示が変わります。

右側下の「Source Picture」ボックス内のマトリックスは、元画像の形を選択できるようになっていて、選択を変えると元の塗りつぶしの形が変わります。また、右側の「Set Custom...」ボタンをクリックすると、画像を選べるようになっています。これらは元画像だけに適用されます。また、Finder から直接元画像内に画像をドラッグ&ドロップできるようになっています。

3 ファイル構成

プロジェクトファイルの構成は以下のようになっています。

CompositeLab.h と .m は、ウインドウの右側上の画像表示を行っているビュークラスを実装するファイルです。CompositeLab.nib は、ウインドウやその内部などのユーザーインターフェイスを定義しているファイルです。さらに、このプロジェクトには、BBall.icns という名前のアプリケーションのアイコンファイルと、DefaultCustomImage.tiff という「Source Picture」で「Custom」が選ばれたとき、ユーザーがファイルを設定したりドロップしていない場合に表示されるデフォルト画像の 2 つの画像ファイルがあります。また、Credits.rtf という、メニューから「About CompositeLab...」を選んだときに表示される内容を収めるリッチテキストファイルもあります。

4 CompositeLab.nib

CompositeLab.nib を開くと次のようになっています。

まず、ウインドウ右上の CompositeView ですが、これには backColorWell(背景カラーウェル)、destColorWell(対象カラーウェル)、sorceColorWell(元カラーウェル)、sourcePictureMatrix(元画像の形状選択マトリックス)というアウトレットがあり、カッコ内の説明のように接続されています。

次に、左側のボックス内のマトリックスですが、ターゲット CompositeView にアクション setOperator:(演算子を設定する)が接続されています。

右側中段のボックス内の左側のカラーウェルからは changeSourceColor:(元の色を変更する)、真ん中のカラーウェルからは changeDestColor:(対象色を変更する)、右側のカラーウェルからは changeBackgroundColor:(背景色を変更する)というアクションが CompositeView をターゲットにして接続されています。

右側下のボックス内の左側のマトリックスからは setSourcePicture:(元画像を設定する)、右側のボタンからは changeCustomImage:(独自画像の変更)というアクションが CompositeView をターゲットにして接続されています。

File's Owner の delegate は設定されていません。この nib はウインドウとメニューだけで、コントローラーにあたるものはありません。すべてのアクションは CompositeView へと接続されていて、このビューがサンプルの中心になっていることがわかります。

5 CompositeView クラス

5.1 継承とインスタンス変数

まず、継承とインスタンス変数から見てみます。

CompositeView.h > 継承とインスタンス変数
@interface CompositeView:NSView { id source, destination, result, customImage; // 画像への参照 NSRect sRect, dRect, rRect; // 3 つの画像の範囲 NSCompositingOperation operator; // 合成に使う演算 CompositeViewPicture sourcePicture; // 元画像の選択 NSColor *sourceColor, *destColor, *backgroundColor; // 色 // アウトレット... id sourceColorWell; // 右真ん中の左のカラーウェル id destColorWell; // 右真ん中の中のカラーウェル id backColorWell; // 右真ん中の右のカラーウェル id sourcePictureMatrix; // 元画像選択マトリックス }

構成は単純なので、付けたコメントでわかると思います。最初のインスタンス変数は id となっていますが、初期化メソッドを見ればわかるように、実際には NSImage* が入れられることなります。

5.2 初期化と割り当て解除

このサンプルが起動され、nib ファイルが読み込まれ、CompositeView がインスタンス作成されるときに、initWithFrame: が呼ばれることになります。

CompositeView.m > initWithFrame:
- (id)initWithFrame:(NSRect)rect { // ビューを初期化する if (!(self = [super initWithFrame:rect])) return nil; // 元、対象、結果に対する長方形を作る sRect = [self bounds]; sRect.size.width /= 3.0; dRect = sRect; dRect.origin.x = sRect.size.width; rRect = dRect; rRect.origin.x = dRect.origin.x + dRect.size.width; // 元、対象、結果画像を作る 1 [(source = [[NSImage allocWithZone:[self zone]] initWithSize:sRect.size]) addRepresentation:[[[NSCustomImageRep alloc] initWithDrawSelector:@selector(drawSource:) delegate:self] autorelease]]; [source setBackgroundColor:[NSColor clearColor]]; [(destination = [[NSImage allocWithZone:[self zone]] initWithSize:dRect.size]) addRepresentation:[[[NSCustomImageRep alloc] initWithDrawSelector:@selector(drawDestination:) delegate:self] autorelease]]; [destination setBackgroundColor:[NSColor clearColor]]; [(result = [[NSImage allocWithZone:[self zone]] initWithSize:dRect.size]) addRepresentation:[[[NSCustomImageRep alloc] initWithDrawSelector:@selector(drawResult:) delegate:self] autorelease]]; [result setBackgroundColor:[NSColor clearColor]]; // デフォルトの演算と元画像選択を設定する 2 // デフォルト色を設定する必要はない // ウェルに対するアウトレットが確立されたとき、nib から読み込まれる operator = NSCompositeCopy; sourcePicture = TrianglePicture; // アプリケーションに、カラーパネルとドラッグされた色で // 不透明度値が許容されたほうがいいことを知らせる // たいていのアプリケーションはこれで手間をかけたくない [NSColor setIgnoresAlpha:NO]; // 最後に、色とファイルのドラッグを登録 [self registerForDraggedTypes: [NSArray arrayWithObjects:NSColorPboardType, NSFilenamesPboardType, nil]]; return self; }

それほど複雑なことは行われていないので十分わかると思います。最初にスーパークラスの初期化を呼び出し、失敗して nil が返ってきたら、そのまま nil を返して終了します。成功したら、以降を続けます。

次に、右側上の3つの画像領域の長方形を作っています。バウンズを設定し、幅を 1/3 にします。この時点で、原点は左下なので、一番左側の長方形が設定できたことになります。あとは、この長方形をコピーして、原点の x 位置だけをずらしています。

そして、1 で、画像を設定します。[[NSImage allocWithZone:[self zone]] の部分は、それほど気にしなくて OK です。これはメモリ内のゾーンを指定して割り当てを行っています。くわしいことを知りたい方は Performance ドキュメントの『メモリ使用パフォーマンスガイドライン』を読んでください。一般にアプリケーションで最初に malloc が呼ばれた時にデフォルトのゾーンが作られ、そのゾーンが以後使用されます。このゾーンは仮想メモリ上の範囲となります。ゾーンを利用することで、たとえばたくさんのオブジェクトをゾーン内に作り、個別に解放するのではなく、ゾーンそのものを破壊することでメモリ消費をなくすことも可能です。メモリ操作に精通していて、メモリの扱いに細心の注意をはらわないかぎり、ゾーンの使用は実際には速度向上ではなく、むしろ問題を起こすこともあるので、メモリ消費が問題にならないかぎり、このあたりについては無視して大丈夫です。アプリケーションで複数のゾーンを利用するためには、malloc_create_zone 関数で明示的に新しい独自ゾーンを作ったり、他の関数を使う必要がありますが、このサンプルではそういう事が行われていないので、結局、この部分は [[NSImage alloc] と同じことです。

メモリを割り当てた後で、直前に計算した長方形のサイズで NSImage を初期化しています。この後にカッコが閉じていますが、これは先頭と対応し、こうして作られた NSImagesource へ代入しています。さらに文は続きますが、次のメソッドは代入された NSImage をレシーバーとするものです。

次に addRepresentation:NSImage に表現を追加しています。ここでは、引数の部分で作られた NSCustomImageRep を渡しています。この NSCustomImageRep 内は、NSImage の画像内容を独自描画にしたい場合に使うものです。initWithDrawSelector: delegate: を使って、委任とセレクタを指定してやれば、画像内容が必要なときに、設定された委任のセレクタ(ここでは、[self drawSource:])が呼ばれます。渡した NSImage が保持するので autorelease されています。

同様に、対象と結果画像も作成しています。それぞれ渡すセレクタが違うだけです。また、それぞれの作成の後で、各画像に対して背景を透明色に設定しています。

2 以降では、さまざまな設定が行われています。まずデフォルトの合成演算 NSCompositeCopy を設定しています。この演算については他のものも含め、『Cocoa 描画ガイド』>「グラフィックコンテキスト」>「現在のグラフィックコンテキストの修正」>「合成オプションの設定」を見てください。NSCompositeCopy は元を対象にそのままコピーし、結果が元画像になるだけです。最初の形を三角形に設定しています。TrianglePicture は、CompositeView.h の最初で CompositeViewPicture 型の列挙定数のひとつとして定義されているものです。次に不透明度値も扱うことを通知しています。最後に、ドラッグを受けつける型を登録します。ここでは色とファイルのドラッグを登録します。

また、初期化と対応して、このクラスでは割り当て解除メソッドも定義されていますが、保持しているオブジェクトを解放するだけの単純なものです。

5.3 描画

さて、初期化メソッドが呼ばれ、インスタンスが作成された後、それはウインドウに配置され、そのウインドウが表示されます。これは nib ファイル内で「visible at launch time(起動時に表示)」がチェックされていたためです。このとき、drawRect: メソッドが呼ばれることになります。

CompositeView.m > drawRect:
- (void)drawRect:(NSRect)rect { // ビュー全体を消去 [backgroundColor set]; // 背景色を設定 NSRectFill([self bounds]); // 全体を背景色で塗りつぶし // 3 つの部分の枠に対する色 [[NSColor blackColor] set]; // 元画像を描画し、枠を黒で描く [source compositeToPoint:sRect.origin operation:NSCompositeSourceOver]; NSFrameRect(sRect); // 対象画像を描画し、枠を黒で描く [destination compositeToPoint:dRect.origin operation:NSCompositeSourceOver]; NSFrameRect(dRect); // 結果画像を扱う、同様に枠を黒で描く [result compositeToPoint:rRect.origin operation:NSCompositeSourceOver]; NSFrameRect(rRect); }

非常に簡単な構造になっています。最初に背景全体を背景色で塗りつぶし、それから黒色を設定し、各画像を描画した後で、それぞれの部分のまわりに黒色で境界線を描いています。NSImage のメソッド compositeToPoint: operation: を呼び出し、各長方形の原点から NSCompositeSourceOver を使って描画しているだけです。NSCompositeSourceOver は、対象の上に元をかぶせるもので、この場合、背景色の上に描画されることになります。NSFrameRect は、AppKit 関数で、現在の設定で指定した長方形を線画するものです。

これで描画は完了です。とはいうものの、これで画面上の表示が理解できるわけではありません。compositeToPoint: operation: を呼び出した時、各 NSImage はその内容を必要とします。手順的には、まず表現を選択して、それからその表現の内容をとりだし、それを描画することになります。ここでは独自の画像表現を設定しましたが、ファイルからの読み込みだと、PDF なら NSPDFImageRep というぐあいに、他の表現もあります。また、最初に設定した表現とは別に、解像度が違う場合に備えて別の表現を持たせたいこともあるでしょう。そういう場合、複数の表現から現在のグラフィックコンテキストの解像度等によって最適な表現を選ぶようになっています。このサンプルでは、独自画像表現が選ばれたので、その画像表現が使われることになります。

それから画像内容を取り出します。このサンプルでは、独自画像表現なので、ここで設定したセレクタが呼ばれることになります。元画像に対しては、[self drawSource:] が呼ばれることになります。

CompositeView.m > drawSource:
- (void)drawSource:(NSCustomImageRep *)imageRep { NSBezierPath *path = [NSBezierPath bezierPath]; switch (sourcePicture) { case TrianglePicture: { 三角形の処理 } break; case CirclePicture: { 円の処理 } break; case DiamondPicture: { ダイヤ形の処理 } break; case HeartPicture: { ハート形の処理 } break; case FlowerPicture: { 花形の処理 } break; case CustomPicture: 独自画像の処理 break; default: break; } [path closePath]; [sourceColor set]; [path fill]; }

空のパスを作成し、それから switch 文で場合分けをして、場合ごとにパスを追加します。switch 文の後でバスを閉じ、それから元の色を設定して、パスを塗りつぶしているだけです。独自画像の場合、パスは追加されず、空のままなので、何も塗りつぶされないことになります。以下で各場合の処理を見ていきます。

CompositeView.m > drawSource: > 三角形の処理
[path moveToPoint:NSMakePoint(0.0, 0.0)]; [path lineToPoint:NSMakePoint(0.0, sRect.size.height)]; [path lineToPoint:NSMakePoint(sRect.size.width, sRect.size.height)];

まず (0.0, 0.0) に移動し、それから長方形の高さ分上に線を引き、そこから長方形の幅の分右へ線を引いています。長方形の左上に直角三角形を描くことになります。

CompositeView.m > drawSource: > 円の処理
// 領域の 80% の長方形内に楕円を描く [path appendBezierPathWithOvalInRect: NSInsetRect(sRect, floor(sRect.size.width * 0.1), floor(sRect.size.height * 0.1))];

appendBezierPathWithOvalInRect: は、パスに楕円を追加します。長方形に内接する楕円が作られます。正方形を渡せば円が描かれることになります。ここでは NSInsetRect を使って、10 % だけ内側に小さくなった長方形を作り、それを渡しています。結果として、幅も高さも 80 % になります。

CompositeView.m > drawSource: > ダイヤ形の処理
[path moveToPoint:NSMakePoint(0.0, sRect.size.height / 2.0)]; [path lineToPoint:NSMakePoint(sRect.size.width / 2.0, 0.0)]; [path lineToPoint:NSMakePoint(sRect.size.width, sRect.size.height / 2.0)]; [path lineToPoint:NSMakePoint(sRect.size.width / 2.0, sRect.size.height)];

次はダイヤ形です。まず長方形の下の真ん中に移動し、そこからダイヤ形に各辺の中心に線を引いているだけです。

CompositeView.m > drawSource: > ハート形の処理
NSAffineTransform *transform = [NSAffineTransform transform]; [transform scaleXBy:sRect.size.width yBy:sRect.size.height]; [transform concat]; [path moveToPoint:NSMakePoint(0.5, 0.6)]; [path curveToPoint:NSMakePoint(0.5, 0.1) controlPoint1:NSMakePoint(0.3, 1.0) controlPoint2:NSMakePoint(0.0, 0.5)]; [path moveToPoint:NSMakePoint(0.5, 0.6)]; [path curveToPoint:NSMakePoint(0.5, 0.1) controlPoint1:NSMakePoint(0.7, 1.0) controlPoint2:NSMakePoint(1.0, 0.5)];

まず、空のアフィン変換を作り、それを長方形の幅と高さ分拡大縮小するように設定し、その変換を適用しています。これは、幅 1.0、高さ 1.0 の単位を基準として形を描画し、それを拡大していると考えればいいです。そのため、以下のパスの座標は、この 1.0 を基準にして設定されています。パスの開始点へと移動し、それから曲線を描き、再び元の開始点へと移動し、そこから曲線を描いています。こうしているのは、対称形であるため、2 回の曲線の設定に逆の数値(0.3 と 0.7、0.0 と 1.0)を設定すればいいからです。

CompositeView.m > drawSource: > 花形の処理
int cnt; NSAffineTransform *transform = [NSAffineTransform transform]; [transform scaleXBy:sRect.size.width yBy:sRect.size.height]; [transform translateXBy:0.5 yBy:0.5]; [transform concat]; [path moveToPoint:NSZeroPoint]; transform = [NSAffineTransform transform]; [transform rotateByDegrees:60.0]; for (cnt = 0; cnt < 6; cnt++) { [path transformUsingAffineTransform:transform]; // 毎回 60 度回転 [path curveToPoint:NSZeroPoint controlPoint1:NSMakePoint(0.4, 0.5) controlPoint2:NSMakePoint(-0.4, 0.5)]; }

まず、空のアフィン変換を作り、それに拡大縮小を設定します。これでハート形で行ったのと同様に 1.0 を基準として形を描ければいいことになります。次に 0.5 ずつ平行移動しています。これで原点が中心へと移動することになります。そのため、中心を原点として描画すればいいので、まず原点へと移動しています。そしてアフィン変換を作り 60 度の回転を設定しています。これは毎回適用することで、60 度ずつ回転させるためのものです。そして 360 度割る 60 度の 6 回繰り返しを行います。繰り返しごとに、まず変換を適用して 60 度回転させ、そこから花の一部である曲線を描画しています。

CompositeView.m > drawSource: > 独自画像の処理
if (!customImage) { // ファイル設定画像があるか NSString *fileName = [[NSBundle mainBundle] pathForImageResource:@"DefaultCustomImage"]; // デフォルト画像を探す if (fileName) { // 見つかったら customImage = [[NSImage allocWithZone:[self zone]] initByReferencingFile:fileName]; // デフォルト画像で画像設定 [customImage setScalesWhenResized:YES]; [customImage setSize:rRect.size]; } } [customImage compositeToPoint:NSZeroPoint operation:NSCompositeSourceOver];

最後に独自画像です。右下のボタンから設定するか、ドロップによってファイルを置かないかぎり、元画像はデフォルトの画像となります。これはプロジェクト内に DefaultCustomImage.tiff として含まれているものです。ビルドされた時に、これはアプリケーションバンドル内の「Resource」フォルダ内にコピーされます。最初にすでに独自画像を設定しているかを調べ、まだ何も設定されなければ、このデフォルト画像を設定しています。それから、設定された画像を NSCompositeSourceOver で描画しています。

これで元画像の画像内容の取得は終了です。残りの 2 つの画像内容は単純です。次に対象画像を見てみましょう。

CompositeView.m > drawDestination:
- (void)drawDestination:(NSCustomImageRep *)imageRep { NSBezierPath *path = [NSBezierPath bezierPath]; [destColor set]; [path moveToPoint:NSMakePoint(dRect.size.width, 0.0)]; [path lineToPoint:NSMakePoint(dRect.size.width, dRect.size.height)]; [path lineToPoint:NSMakePoint(0.0, dRect.size.height)]; [path closePath]; [destColor set]; [path fill]; }

サンプルを起動させて見ればわかるように、対象画像はつねに右上の直角三角形となります。そのため、元画像内容の三角形の場合と一部の数値が違うだけでほとんど同じになります。次は、結果画像の内容です。

CompositeView.m > drawResult:
- (void)drawResult:(NSCustomImageRep *)imageRep { [destination compositeToPoint:NSZeroPoint operation:NSCompositeCopy]; [source compositeToPoint:NSZeroPoint operation:operator]; }

たった 2 行です。まず対象をそのままコピーしています。次に、その上に元画像を設定されている合成演算を使って描画しています。

5.4 アクション

描画が終了すれば、アプリケーションはユーザーの行動を待つ状態になります。カラーウェルやマトリックスを選択すれば、それに応じて対応することになります。まずは左側のマトリックスから呼ばれるメソッドを見てみます。

CompositeView.m > drawResult:
- (void)setOperator:(id)sender { switch ([sender selectedRow]) { case 0: operator = NSCompositeCopy; break; ... 中略 ... case 12: operator = NSCompositePlusLighter; break; default: break; } [result recache]; [self setNeedsDisplayInRect:rRect]; }

選択された行にしたがって、演算を設定しています。その次に [result recache] が呼ばれてから、setNeedsDisplayInRect: が呼ばれていることに注意してください。

このサンプルでは独自描画をする画像内容を設定しましたが、画像内容は最初に取得されたときにキャッシュされます。そのため、setNeedsDisplayInRect: だけだと以前にキャッシュされた内容がそのまま使われ、結果として表示は変わらないことになってしまいます。ここで合成演算の変更で影響を受けるのは結果画像だけなので、[result recache] とすることで、画像内容をもう一度作り直させています。それから結果画像が表示される部分を再表示が必要だと知らせています。次のイベントサイクルで(10.4 以降ならスクリーンのリフレッシュレートにあうような形で)描画メソッドが呼ばれることになります。

ここでさらに注意すべきは、これによって結果として drawRect:rRect が呼ばれることになることです。しかし、前に見たように、drawRect: 内では渡された NSRect をチェックしていませんでした。その結果、結局ビュー全体が描きなおされることになります。もし結果画像の部分だけを描画させたいなら、drawRect: メソッド内で渡された長方形をチェックしなければなりません。このサンプルの場合だと、渡された NSRectrRect などと重なるかを(NSIntersectsRect などを使って)チェックするだけでも高速化できるでしょう。

右側中段のカラーウェルのアクションは、どれも似ています。元画像の分だけを見てみます。

CompositeView.m > changeSourceColor:
- (void)changeSourceColor:(id)sender { [self changeSourceColorTo:[sender color] andDisplay:YES]; }

このように、独自メソッドをさらに呼んでいるだけです。これはドラッグ等で色が変更される場合などにも同じメソッドで対応するためです。ここで色の設定をやっていれば、ドラッグ対応メソッドではまた同じことを行わなければなりません。

CompositeView.m > changeSourceColorTo:andDisplay:
- (void)changeSourceColorTo:(NSColor *)color andDisplay:(BOOL)flag { if (![sourceColor isEqual:color]) { [sourceColor release]; sourceColor = [color copyWithZone:[self zone]]; [source recache]; [result recache]; if (flag) [self setNeedsDisplay:YES]; } }

設定されるのが同じ色なら何もしません。そうでないなら、元々保持していた色をまず解放します。それから新しい色をコピーしてそれをインスタンス変数に設定します。

copyWithZone: は、Foundation の NSCopying プロトコルのメソッドですが、NSObject で採用されているため、通常すべてのオブジェクトで利用できると思っていいでしょう。ただし、独自クラスで利用するなら、このプロトコルに準拠して、copyWithZone: を実装するのを忘れないでください。NSObject は宣言はしていますが、実装は提供していません。初期化メソッドの説明に書いたように、ゾーンは気にせずとも大丈夫です。NSObject にはデフォルトゾーンでコピーする copy メソッドも実装されているので、[color copy] で十分です。

ここで元と結果のどちらにも影響するので、両方を recache しています。あとは表示するように指定されていれば、再表示をマークしているだけです。

対象と結果も、設定したり、再キャッシュするものが違うだけでほとんど同じです。次は右下の左側のマトリックスから呼ばれるアクションを見てみます。

CompositeView.m > setSourcePicture:
- (void)setSourcePicture:(id)sender { sourcePicture = [sender selectedTag]; [source recache]; [result recache]; [self setNeedsDisplay:YES]; }

タグを設定し、元画像と結果画像の内容を再キャッシュさせ、ビュー全体を再表示が必要と指示しているだけです。独自画像の場合のデフォルト画像の設定は、上で見たように、画像内容を作成する時に行われいたので、ここで設定する必要はありません。次は、右下右側のボタンのアクションを見てみます。

CompositeView.m > changeCustomImage:
- (void)changeCustomImage:(id)sender { NSOpenPanel *openPanel = [NSOpenPanel openPanel]; if ([openPanel runModalForTypes:[NSImage imageFileTypes]]) { (void)[self changeCustomImageTo:[[NSImage allocWithZone:[self zone]] initByReferencingFile:[openPanel filename]]]; } }

オープンパネルを提示してファイルを選択させます。[NSImage imageFileTypes] は、NSImage がサポートしている形式を返してくれるものです。runModalForTypes: はキャンセルだと NO が返るため、そのまま終了となります。場合によっては、これを呼び出す前に、最初に表示するディレクトリなどの場所を設定したり、ディレクトリを受けとるかどうかを設定したり、アクセサリビューを追加したりなど、まずオープンパネルの動作を設定してから、runModal... などのメソッドを呼び出します。

ファイルが指定されたら、このクラスの別メソッドを呼び出して画像ファイルを設定しています。(void) は戻り値を明示的型変換するものです。この独自メソッドは BOOL を返すことになっているので、ここでは (void) を使って戻り値を使わないということを明言しています。色と同様に、これはドラッグ操作に対しても、同じメソッドを使って設定を行うためです。[openPanel filename] は、単にファイル名ではなくパスを返すことに注意してください。この initByReferencingFile: というメソッドは、initWithContentsOfFile: とは違って、遅延読み込みを行います。このメソッドが呼ばれた時点ではファイルは開かれず、後でファイル内容が必要になったとき(画像内容が要求されたとき)、ファイルを読み込んで表現を作ります。

CompositeView.m > changeCustomImage:
- (BOOL)changeCustomImageTo:(NSImage *)newImage { if (newImage && (newImage != customImage)) { [newImage setSize:rRect.size]; [newImage setScalesWhenResized:YES]; if ([newImage isValid]) { [customImage release]; customImage = newImage; if (sourcePicture != CustomPicture) { sourcePicture = CustomPicture; [sourcePictureMatrix selectCellWithTag:CustomPicture]; } [source recache]; [result recache]; [self setNeedsDisplay:YES]; return YES; } } return NO; }

まず設定画像が存在し、現在のものと違うかどうかを調べています。そうなら、新しい画像にサイズ変更時の動作を設定しています。それから新しい画像が有効かを調べ、有効なら、今のものと置き換えています。また、独自画像が設定されたため、右側下の左側のマトリックスも「Custom」が選択されている必要があります。そこで、「Custom」になっていないなら、それを選択させています。インスタンス変数で、このマトリックスがアウトレット宣言されていましたが、このために必要だったわけです。左側のマトリックスはユーザーが設定した値をプログラム内から変更する必要がなかったため、アウトレット宣言されていません。また、カラーウェルも、ドラッグの時にプログラム内から色を変更する必要があるため、同様にアウトレットとして保持されています。最後に、画像内容を再キャッシュさせ、再表示を行うよう指示して終了です。

これでアクションメソッドは終了で、これでサンプルの動作がほとんどわかったことでしょう。後は、ドラッグ&ドロップの対応だけです。

5.5 ドラッグ&ドロップ

ドラッグ&ドロップについては、『Cocoa のためのドラッグ&ドロッププログラミングトピック』で解説されています。まず、初期化メソッドにおいて、ドラッグを受けとりを登録したことを思い出してください。

CompositeView.m > initWithFrame: > ドラッグ登録
- (id)initWithFrame:(NSRect)rect { ... // 最後に、色とファイルのドラッグを登録 [self registerForDraggedTypes: [NSArray arrayWithObjects:NSColorPboardType, NSFilenamesPboardType, nil]]; ... }

この registerForDraggedTypes: メソッドは、ウインドウとビューで定義されています。登録している型がペーストボード型であることに注意してください。arrayWithObjects: は、最後が nil で終わる不定数の引数リストから配列を直接作るものです。

ドラッグを受けとるには、NSDraggingDestination 簡易プロトコルを実装しなければなりません。ReadMe.rtf の日本語訳にもあるように、このクラスでは最小限必要なメソッド(draggingEntered:performDragOperation:)以外にも、draggingUpdated:draggingExited: を実装しています。draggingEntered: は、ドラッグが進入した時に呼ばれ、その内容を調べて、どういう操作をするのかを返します。コピー操作などを指定でき、またドラッグ元と直接やりとりすることで、もっと複雑な連携も行えるようになっています。performDragOperation: は、ドロップされた時に、実際の操作を行います。draggingUpdated: は、ドラッグ画像が内部にある時に定期的に呼び出されます。draggingExited: は、画像が出た時に呼ばれます。このサンプルでは、ドラッグ中に色を変えているので、このメソッドで元に戻します。また、ReadMe.rtf では、触れられていませんが、このクラスではさらに concludeDragOperation: メソッドも実装しています。これは performDragOperation: がYES を返し、操作が成功したことを伝えた時に呼ばれます。ドロップ時の最後の操作になります。

まず、進入メソッドから見ていきます。

CompositeView.m > draggingEntered:
- (unsigned int)draggingEntered:(id )sender { return [self draggingUpdated:sender]; }

このサンプルでは、draggingUpdated: を実装しているため、単純にそこからの戻り値を返しています。それを実装しないなら、ここで操作を返すことになります。次に更新メソッドを見てみます。

CompositeView.m > draggingUpdated:
- (unsigned int)draggingUpdated:(id )sender { if ([sender draggingSourceOperationMask] & NSDragOperationGeneric) { 1 NSPasteboard *pboard = [sender draggingPasteboard]; if ([[pboard types] containsObject:NSColorPboardType]) { // 色 2 NSColor *sourceColorSave = [[sourceColor retain] autorelease]; NSColor *destColorSave = [[destColor retain] autorelease]; NSColor *backgroundColorSave = [[backgroundColor retain] autorelease]; [self doColorDrag:sender]; [self displayIfNeeded]; /* 今すぐ表示を強制 */ [self changeSourceColorTo:sourceColorSave andDisplay:NO]; /* 表示なしで復元 */ [self changeDestColorTo:destColorSave andDisplay:NO]; [self changeBackgroundColorTo:backgroundColorSave andDisplay:NO]; return NSDragOperationGeneric; } else if ([NSImage canInitWithPasteboard:pboard]) { // 画像か?3 return NSDragOperationGeneric; } } return NSDragOperationNone; }

まず、1[sender draggingSourceOperationMask] & NSDragOperationGeneric に注目してください。この NSDragOperationGeneric は、操作の種類を終点で決めるものですが、[sender draggingSourceOperationMask] によって、それがドラッグ元によって許可されているかどうかを調べています。忘れがちですが、どういう操作をするかをドラッグ元が制限している場合があります。なので、このチェックは必要です。これが許可されていた場合だけ、続く操作を行うことになります。許可されていたら、ドラッグ元のドラッグ用ペーストボードを取得します。

次に、2if 文で、ドラッグされているものが色かどうかをチェックしています。色なら以降が実行され、ファイルなら 3 へと移ります。ファイルの場合、ドラッグ中における変更を行わないので、単に操作の種類を返しているだけです。色なら、まず現在の色をローカル変数に代入しています。これは、ドラッグ操作により一時的に色を変えるので、最後に復元できるようこうしています。それから独自メソッド doColorDrag: を呼び、現在のドラッグ位置によって、色を変えています。それから返ってきた後で色を復元しています。このとき、再表示をしないように設定していることに注意してください。再表示されれば、このメソッドが呼ばれるたびに、色が変化し、パフォーマンスが低下し、画面がちらつくことになります。表示を強制しているので同じだと思うかもしれませんが、display ではなく、displayIfNeeded なので、doColorDrag: 内で再表示が必要だとマークされた場合だけ、今すぐ表示を行うことになります。display だと表示される内容は変わらなくても描画が強制されるので、パフォーマンスが低下します。次に実際の色変更処理を行っているメソッドを見てみます。

CompositeView.m > doColorDrag:
- (void)doColorDrag:(id )sender { NSPoint point = [sender draggingLocation]; NSColor *color = [NSColor colorFromPasteboard:[sender draggingPasteboard]]; point = [self convertPoint:point fromView:nil]; switch ((int)(3 * point.x / NSWidth([self bounds]))) { case 0: [self changeSourceColorTo:color andDisplay:YES]; break; case 1: [self changeDestColorTo:color andDisplay:YES]; break; case 2: [self changeBackgroundColorTo:color andDisplay:YES]; break; default: break; // 実際には起こらないはず } }

まず、ドラッグされている現在位置を取得しています。そして色をペーストボードから取り出します。次に、現在位置がウインドウ基準座標なので、それをビュー内の独自座標へと変換しています。convertPoint: fromView: の後の変数は、ビューが指定されればそのビューの独自座標から、nil が指定されればウインドウの基準座標系から点を変換します。次に点の x 座標がバウンズの 3 分割幅で割っています。これは 0 から 2 の値をもつはずです。そこで switch 文で場合分けをしています。それぞれの場合で色を変更するメソッドを呼び出し、再表示させているだけです。呼び出されたメソッド内の色設定メソッドで、同じ色かどうかを調べているので、同じ色の場合何も行われません。よって、ドラッグ画像が同じ範囲にとどまっている間は、再表示は起こらないことになります。

これで、更新メソッドが理解できました。次に、ドラッグが出た時の処理を見てみます。

CompositeView.m > draggingExited:
- (void)draggingExited:(id )sender { if ([[[sender draggingPasteboard] types] containsObject:NSColorPboardType]) { // ビューを調整する必要あり [self setNeedsDisplay:YES]; } }

これはオプションのメソッドですが、このサンプルでは、更新メソッドで色を変更したため、必要になっています。色自体は毎回復元されているので問題ありませんが、再表示させる必要があります。次に、ドロップされた時の処理を見てみます。これは必須メソッドです。

CompositeView.m > performDragOperation:
- (BOOL)performDragOperation:(id )sender { return ([self draggingUpdated:sender] == NSDragOperationNone) ? NO : YES; }

さて、このメソッドは、ドラッグ更新メソッドが操作なしを返すかどうかを調べて、操作なしなら NO、操作するなら YES を返しているだけです。通常は、このメソッドで実際のドロップ時の処理を行います。このサンプルでは、concludeDragOperation: を実装しているので、YES が返った場合は、それが呼び出されることになります。

CompositeView.m > concludeDragOperation:
- (void)concludeDragOperation:(id )sender { NSPasteboard *pboard = [sender draggingPasteboard]; if ([[pboard types] containsObject:NSColorPboardType]) { [self doColorDrag:sender]; [sourceColorWell setColor:sourceColor]; [destColorWell setColor:destColor]; [backColorWell setColor:backgroundColor]; } else { (void)[self changeCustomImageTo: [[NSImage allocWithZone:[self zone]] initWithPasteboard:pboard]]; } }

まずペーストボードを取得しています。次に色かファイルかをチェックします。色なら doColorDrag: で設定しています。これは、どこにドロップされたかで変更される色が違うため、その判断をこのメソッド内で行っていたからです。次に、 右側中段のカラーウェルに設定された色を渡しています。少数なので、いちいちチェックせず、3 つとも渡しています。ファイルの場合、渡されたファイルに設定画像を渡しています。そのまま渡しているので、サポートされない形式の場合、実際には画像は作られませんが、呼び出しているメソッドで画像があるかどうか、有効かどうかをチェックしているので大丈夫です。

6 まとめと機能追加

このサンプルは、合成演算について学ぶためのものです。また、簡単なドロップ対応の例としても参考になります。

合成演算に慣れないうちは、特別な効果がほしい場合、このサンプルでチェックをしてみればいいかもしれません。このサンプルを作り替えて、対象画像も独自画像などに設定できるようにしてもいいでしょう。


管理人:神吉 秀典 E-mail:puer@ba.wakwak.com