テーブルビューをカット&ペーストに対応させる【実践的Macintoshプログラミング解説】

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

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

LinkIconホーム

更新日 2009-05-24

原図をカット&ペーストできる様にする

テーブルビューをカット&ペーストに対応させる

MasterMotifTableView2.png 第三のプロジェクトではテーブルビューのサブクラスLissajousTableViewを作ってcut:やpaste:といったメソッドを実装しました。

 これをMasterMotif対応に変更するわけですが、単にLissajousをMasterMotifCDに書き換えるだけでは別のクラスに対応させる時にまた同じ作業が発生します。将来RepeatingMotifやLayerクラスに対応したテーブルビューが必要になる予定ですので、二度手間・三度手間は避けたいところです。

 調べたところ、配列コントローラに設定されているエンティティから、エンティティに対応するクラスのクラスオブジェクトを得る事ができる事がわかりました。これによりソースコード上でLissajousやMasterMotifCDといったクラス名を書く必要がなくなるため自動的にMasterMotif対応になると共に、将来RepeatingMotifやLayerクラスに対応する際も、変更なしですむメリットがあります。これでかなり汎用性が高まりました。

 残る問題はアクション名を設定する部分だけですが、アクション名はモデルクラス固有の文字列なので、モデルクラスのクラスメソッドで定義する事にしました。

 このテーブルビューの名前は、当初RMGTableViewとする予定でしたが、他のアプリケーションでも使えそうなくらい汎用性が高まったので、接頭詞をRMGからNVKに変えてNVKTableViewとする事にします。NVKはNovemberKouからとりました。(NKだとNSとまぎらわしいですからね)

プロトコルを定義する

newHeader.png 汎用性を高めるための工夫として、対象となるモデルクラスのオブジェクトが採用(adopt)するプロトコルを定義する事にします。プロトコルを定義しておけば"RMGRoot.h"をインポートする必要がなくなり独立性が高まるメリットがあります。NVKTableViewは自分が使うメソッドの宣言だけを取り込みたいわけですから、プロトコルを使うのが適しています。

 プロトコル名はNVKPasteboardSupportとします。このプロトコルでは、NVKTableViewが使うメソッドの宣言だけを定義します。

 プロトコルはヘッダファイルで定義します。新規ヘッダファイルはCocoaグループではなく、C and C++グループに入っています。

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

@protocol NVKPasteboardSupport

+ (void)copyObjects:(NSArray *)objects
       toPasteboard:(NSPasteboard *)pboard;
+ (void)pasteFromPasteboard:(NSPasteboard *)pboard
                 controller:(NSArrayController *)controller;

+ (NSString *)dataType;
+ (NSString *)orderKey;

+ (NSString *)addActionName;
+ (NSString *)deleteActionName;
+ (NSString *)cutActionName;
+ (NSString *)pasteActionName;
+ (NSString *)dragCopyActionName;
+ (NSString *)dragOperationActionName;

@end

NVKTableViewを作る

インターフェイスファイルの説明

 新規ファイルを作成する手順はこれまでに何度もでてきたので省略し、インターフェイスファイルの説明に入ります。

インスタンス変数

 配列コントローラを参照するインスタンス変数arrayControllerを追加します。配列コントローラはアウトレットではなく単なるインスタンス変数にしました。これはバインディング情報から配列コントローラを得る事ができる事がわかったので、入手を自動化した事による変更です。これでアウトレットを接続する手間が省けました。

メソッドの宣言

 サブクラスで使いそうなメソッドの宣言を追加します。

  • targetClassは配列コントローラに指定されたエンティティに対応するクラスのクラスオブジェクトを返すメソッドです。
  • pasteBoardHas:は汎用ペーストボードに引数で指定された型のデータがあるかどうかを返すユーティリティメソッドです。
  • setActionName:はアンドゥマネージャにアクション名を設定するメソッドです。

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

#import <Cocoa/Cocoa.h>


@interface NVKTableView : NSTableView
{
    NSArrayController   *arrayController;
}

- (Class)targetClass;
- (BOOL)pasteboardHas:(NSString *)theType;
- (void)setActionName:(NSString *)actionName;

@end

配列コントローラからエンティティに対応するクラスのクラスオブジェクトを得る

 LissajousTableViewでは[Lissajous dataType]の様にクラス名を直接ソースコードに記述しており、このために汎用性が低下していました。このLissajousの部分をクラスオブジェクトに置き換えられれば、汎用性を高める事ができます。

 以下の手順で、配列コントローラからエンティティに対応するクラスのクラスオブジェクトを入手できる事がわかりました。

  1. 配列コントローラに設定されているエンティティ名をentityNameメソッドで得る。
  2. エンティティ名からエンティティオブジェクト(NSEntityDescriptionクラスのオブジェクト)を作る。それにはentityForName:inManagedObjectContext:メソッドを使う。
  3. エンティティに対応するクラス(カスタムクラスが設定されていればそのカスタムクラス)の名前をエンティティオブジェクトのmanagedObjectClassNameメソッドで得る。
  4. NSClassFromString()を使って、クラス名からクラスオブジェクトを得る。

 3.まではデベロッパドキュメントをたどっていく事で比較的容易に分かるのですが、4.がなかなかわかりませんでした。この手順を実装したのが以下のtargetClassメソッドです。

//
//  NVKTableView.m
//

//  配列コントローラに設定されているエンティティ名に対応するクラスのクラスオブジェクトを返す
- (Class)targetClass
{
    NSEntityDescription *entity = [NSEntityDescription entityForName:[arrayController entityName]
                                              inManagedObjectContext:[arrayController managedObjectContext]];

    return  NSClassFromString([entity managedObjectClassName]);
}

初期化メソッド

 LissajousTableViewとの違いを赤字で示します。

 配列コントローラをバインディング情報から得るコードを追加しています。これはAppleのサンプルプログラムDragAppを参考にしました。エラーチェックを省いているので、バインディングを設定してから実行して下さい。

 ドラッグを受け入れるデータタイプを登録する部分を変更しています。Lissajousと直接クラス名を書いていましたが、これを[self targetClass]と書き換えます。

//
//  NVKTableView.m
//

#pragma mark -
#pragma mark Initializer

//  配列コントローラをバインディング情報から得るので、バインディングを設定してから実行する事
- (void)awakeFromNib
{
    NSSortDescriptor        *orderSort;

    arrayController = [[self infoForBinding:NSContentBinding] objectForKey:NSObservedObjectKey];

    orderSort = [[NSSortDescriptor alloc] initWithKey:@"order" ascending:YES];
    [arrayController setSortDescriptors:[NSArray arrayWithObject:orderSort]];
    [orderSort release];

    [self registerForDraggedTypes:[NSArray arrayWithObject:[[self targetClass] dataType]]];
}

ユーティリティメソッド

 pasteBoardHas:はLissajousTableViewと変わりません。

 setActionName:メソッドは、引数がnilかどうかをチェックするコードを追加しました。

//
//  NVKTableView.m
//

#pragma mark -
#pragma mark Utility methods

//  汎用ペーストボード上に指定されたタイプのデータがあるかどうかを返す
- (BOOL)pasteboardHas:(NSString *)theType
{
    NSPasteboard    *pasteboard = [NSPasteboard generalPasteboard];
    NSArray         *types = [NSArray arrayWithObject:theType];
    
    return  ([pasteboard availableTypeFromArray:types] != nil);
}

- (void)setActionName:(NSString *)actionName
{
    NSUndoManager   *undoManager = [[arrayController managedObjectContext] undoManager];
    
    if(![undoManager isUndoing] && ![undoManager isRedoing] && actionName != nil)
        [undoManager setActionName:NSLocalizedString(actionName,nil)];
}

メニューアイテムの検証

 ペーストメニューを検証する際に、対象となるクラスのデータタイプがペーストボード上に存在するかどうかを見る必要があります。ここもLissajousと直接クラス名を書いていた部分を[self targetClass]と書き換えます。

//
//  NVKTableView.m
//

#pragma mark -
#pragma mark PasteBoard methods

- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{    
    if([menuItem action] == @selector(cut:) ||
       [menuItem action] == @selector(copy:) ||
       [menuItem action] == @selector(delete:) )
    {
        return  [[arrayController selectedObjects] count] > 0;
    }

    if([menuItem action] == @selector(paste:))
        return  [self pasteboardHas:[[self targetClass] dataType]];

    return  YES;
}

コピーメソッド

 同様の書き換えがcopy:メソッドでも発生します。

//
//  NVKTableView.m
//

- (IBAction)copy:(id)sender
{
    [[self targetClass] copyObjects:[arrayController selectedObjects]
                       toPasteboard:[NSPasteboard generalPasteboard]];
}

削除メソッド

 delete:メソッドでは、アクション名を設定するメソッドの引数を書き換えます。

//
//  NVKTableView.m
//

- (IBAction)delete:(id)sender
{
    NSManagedObject *selectedObject;
    NSEnumerator    *enumerator = [[arrayController selectedObjects] objectEnumerator];

    while(selectedObject = [enumerator nextObject])
        [[arrayController managedObjectContext] deleteObject:selectedObject];
    [self setActionName:[[self targetClass] deleteActionName]];
}

カットメソッド

 cut:メソッドでも同様に、アクション名を設定する部分を書き換えます。

//
//  NVKTableView.m
//

- (IBAction)cut:(id)sender
{
    [self copy:sender];
    [self delete:sender];
    [self setActionName:[[self targetClass] cutActionName]];
}

ペーストメソッド

 paste:メソッドでは対象クラスとアクション名の両方を書き換えます。

//
//  NVKTableView.m
//

- (IBAction)paste:(id)sender
{
    [[self targetClass] pasteFromPasteboard:[NSPasteboard generalPasteboard]
                                 controller:arrayController];
    [self setActionName:[[self targetClass] pasteActionName]];
}