カスタム値変換を作る【実践的Macintoshプログラミング解説】

印刷用表示 |テキストサイズ 小 |中 |大 |

CoreData版 Repeating Motif Generator の開発 Repeating Motif Wonderland CoreData 実践的 Macintosh プログラミング解説

LinkIconホーム

更新日 2009-05-24

テーブルビューにパラメータではなくリサージュ図形を表示する

カスタム値変換を作る

valueTransformerImage.png 第二のプロジェクトではテーブルビューに発振器のパラメータを表示していましたが、単なる数値を眺めてもそれがどんなリサージュ図形であるのかはわかりません。テーブルビューに表示すべきなのはパラメータなどではなく、リサージュ図形そのものです。

 テーブルビューで図形を表示するには、テーブルカラムにイメージセルをセットすればよいのですが、これで表示できるのはNSImageクラスのオブジェクトです。
 一方Lissajousが図形の属性として持っているのはNSBezierPathであってNSImageではありません。したがって直接これを表示する事はできません。

 LissajousにNSImageの属性を追加する方法もありますが、別の方法もあります。値変換(Value Transformer)を使う方法です。右の図の様にNSBezierPathをバインドしても、それをカスタムの値変換でNSImageに変換してやれば問題ありません。

 こうするとデータとそれを表現するものを分離する事ができます。

カスタム値変換を使うための三つのステップ

 カスタムの値変換を使うには、以下の三つのステップが必要です。

  1. NSValueTransformerのサブクラスを作る
  2. そのサブクラスのインスタンスを生成して名前付きで登録する
  3. バインディングを設定してValue Transformerに登録した名前を入力する

1. NSValueTransformerのサブクラスを作る

 どのような変換を行うのかを決めます。具体的には、サブクラスが必ず実装しなければならないメソッドが決まっているので、それを実装するという事です。

2. そのサブクラスのインスタンスを生成して名前付きで登録する

 作った値変換を登録します。登録にはNSValueTransformerのクラスメソッドsetValueTransformer:forName:を使います。ここで登録するのはインスタンスなので、同じクラスから複数のインスタンスを生成して違う名前で登録する事ができます。

 例えば今回はNSBezierPathからNSImageに変換するわけですが、変換するサイズが異なる値変換を登録したりできるわけです。まず一つインスタンスを生成してサイズを100×100に設定し、RMGPathToImage100という名前で登録します。次に新たなインスタンスを生成してサイズを200×200に設定し、RMGPathToImage200という名前で登録します。こうしておけば一つのクラスで内容の異なる二つの値変換を使えます。

 いつ登録するのかですが、なるべく早い段階がよいと考えAppControllerのinitializeメソッドで登録する事にしました。AppControllerはアプリケーション全体の面倒を見るコントローラで、今回新たに作成します。

3. バインディングを設定してValue Transformerに登録した名前を入力する

 使う値変換を指定します。指定には登録名を使います。バインディングの設定でValue Transformerを指定する場所があるので、ここに登録名を入力します。

NSValueTransformerのサブクラスを作る

 Xcodeのファイルメニューから新規ファイル…を実行します。Xcode3.1にバージョンアップしたら、現れるウィンドウが変化しました。リストの項目が多すぎるので、グループ分けして整理する必要が生じたのでしょうね。
 CocoaグループのObjective-C classを選んで「次へ」をクリックします。

newFileAssistant31.png

 クラス名を入力します。NSBezierPathからNSImageへ変換するので、RMGPathToImageTransformerとしました。RMGはRepeating Motif Generatorの頭文字です。固有の接頭詞をつける事で名前の重複を避けています。

newFileAssistant31_2.png

 インターフェイスファイルの変更点ですが、まずスーパークラスをNSValueTransformerに変更します。インスタンス変数は追加しません。

 説明の時に例として挙げた様に、イメージサイズを変更できる様にするなら、サイズを保持するインスタンス変数を追加する所でしょう。今回はそこまで汎用性を高めなくてもよいと考えました。

 サブクラスで実装する事が必須とされている三つのメソッドの宣言を追加します。

//
//  RMGPathToImageTransformer.h
//  RepeatingMotifGenerator
//
//  Copyright NovemberKou 2008 . All rights reserved.
//

#import <Cocoa/Cocoa.h>


@interface RMGPathToImageTransformer : NSValueTransformer
{
}

+ (Class)transformedValueClass;
+ (BOOL)allowsReverseTransformation;

- (id)transformedValue:(id)value;

@end

 インプリメンテーションファイルでは実装必須の三つのメソッドを実装します。

 transformedValueClassは変換されたオブジェクトのクラスを指定するメソッドです。NSImageに変換して返すので[NSImage class]を返します。

 allowsReverseTransformationで逆変換ができるかどうかを表します。NSImageからNSBezierPathに変換する事はできないのでNOを返します。

 transformedValue:が変換を行うメソッドです。引数で渡されたNSBezierPathをアフィン変換を使ってイメージサイズに合わせる様に変形します。この辺りはOscillatorViewで説明したので、詳細説明は省略します。
 変形してしまうのでコピーを作って、それを操作しています。

//
//  RMGPathToImageTransformer.m
//  RepeatingMotifGenerator
//
//  Copyright NovemberKou 2008 . All rights reserved.
//

#import "RMGPathToImageTransformer.h"


@implementation RMGPathToImageTransformer

+ (Class)transformedValueClass      {return  [NSImage class];}
+ (BOOL)allowsReverseTransformation {return NO;}

- (id)transformedValue:(id)value
{
    NSSize              imageSize = NSMakeSize(100,100);
    NSImage             *image = [[[NSImage alloc] initWithSize:imageSize] autorelease];
    NSBezierPath        *bezierPath = [value copy];
    NSAffineTransform   *at = [NSAffineTransform transform];

    if(value == nil)
        return  nil;
    //  変換操作を逆順で登録する
    //      ベジエパスの中心をイメージの中心に移動
    [at translateXBy:imageSize.width/2
                 yBy:imageSize.height/2];
    //      サイズをフィットさせる
    [at scaleXBy:(imageSize.width-5)/[bezierPath bounds].size.width
             yBy:(imageSize.height-5)/[bezierPath bounds].size.height];
    //      ベジエパスの中心を原点に移動
    [at translateXBy:-NSMidX([bezierPath bounds])
                 yBy:-NSMidY([bezierPath bounds])];

    [bezierPath transformUsingAffineTransform:at];

    [image lockFocus];
    [bezierPath stroke];
    [image unlockFocus];

    [bezierPath release];

    return  image;
}

@end