テーブルビューのサブクラスを作る【実践的Macintoshプログラミング解説】

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

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

LinkIconホーム

更新日 2009-05-24

リサージュ図形をカット&ペーストできる様にする

テーブルビューのサブクラスを作る

 まず管理対象オブジェクトのルートクラスにユーティリティメソッドを実装し、次にそのサブクラスをカット&ペーストに対応させました。最後にcut:やpaste:といったメソッドを実装すれば、アプリケーションがカット&ペーストできる様になります。

 これらのメソッドを実装する適切なオブジェクトはリサージュ図形を表示しているテーブルビューです。そこでテーブルビューのサブクラスを作って、そこにカット&ペースト関係のメソッドを実装します。

カット&ペーストのメソッドを実装する適切なオブジェクト

 リサージュ図形をカット&ペーストするメソッドを実装するのはどのオブジェクトがよいでしょうか?適しているオブジェクトの条件を挙げてみます。

  1. レスポンダチェーンに入っている
  2. 管理対象オブジェクトコンテキストへアクセスできる
  3. アンドゥマネージャにアクセスできる
  4. リサージュ図形の配列コントローラにアクセスできる

レスポンダチェーンに入っている

  •  これはcut:やpaste:といったメッセージを受け取るために必要な条件です。最初からレスポンダチェーンに入っているオブジェクトであれば手間がかかりません。そうでないオブジェクトをレスポンダチェーンに割り込ませる事もできるので、必須の条件というわけではないのですが。
  •  レスポンダチェーンに入っているオブジェクトがcut:メソッドを実装しているとカットメニューが自動的に有効になります。この仕組みを有効に生かすには、テーブルビューにリサージュ図形をカット&ペーストするメソッドを実装するのがよいでしょう。テーブルビューが選択されている時にメニューが自動的に有効になります。

管理対象オブジェクトコンテキストへアクセスできる

  •  カット&ペースト時は管理対象オブジェクトを追加・削除することになるので、管理対象オブジェクトコンテキストへのアクセスが必要になります。

アンドゥマネージャにアクセスできる

  •  アンドゥ自体はCoreDataによって自動化されていますが、アクション名を設定する際にアンドゥマネージャへのアクセスが必要になります。

リサージュ図形の配列コントローラにアクセスできる

  •  現在選択されているオブジェクトを知るために必要です。カット、コピー、削除は現在選択されているオブジェクトが対象なので、それを知らないと機能を実現できません。


 幾つか条件を挙げましたが、4を満足していれば2と3は満足されます。配列コントローラ経由で管理対象オブジェクトコンテキストへアクセスできますし、管理対象オブジェクトコンテキスト経由でアンドゥマネージャにアクセスできるからです。したがって結局1と4を満足していればOKです。そこでテーブルビューのサブクラスを作って、配列コントローラのアウトレットを追加する事にします。

 Appleのサンプルプログラム"DragApp"を見ると、テーブルビューにinfoForBinding:メッセージを送って返される辞書の中に、配列コントローラが入っているようです。バインディングの設定さえしておけば、実はアウトレットすら必要ないのかもしれません。
 ただしコードが複雑になるので採用しませんでした。

LissajousTableViewを作る

 Xcodeのファイルメニューから新規ファイルを実行します。CocoaグループのObjective-C NSView subclassを選んで「次へ」をクリックします。

newNSViewSubclass31.png

 LissajousTableViewと入力して完了です。

newFileAssistant31_4.png

LissajousTableViewのインターフェイスファイル

 インターフェイスファイルを以下に示します。スーパークラスをNSTableViewとします。
 上述した通り、リサージュ図形の配列コントローラにアクセスする必要がありますので、アウトレットを追加します。

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

#import <Cocoa/Cocoa.h>


@interface LissajousTableView : NSTableView
{
    IBOutlet    NSArrayController   *arrayController;
}

@end

カット&ペーストのメソッドを追加する

 カットアンドペーストを実現するにはNSPasteboardクラスを使用します。ペーストボードには汎用のもの、フォント用、ドラッグ&ドロップ用など何種類かのものがありますが、ここでは汎用のペーストボードを使います。汎用のペーストボードはクラスメソッドgeneralPasteboardで得ることができます。

ペーストボード上に指定した型のデータが存在するかどうかを調べる

 ペーストボード上にどんな種類のデータがあるのかはNSPasteboardクラスのtypesメソッドで知ることができますが、availableTypeFromArray:メソッドを使う方法もあります。これは引数のNSArrayに自分が読み込むことのできるデータ型を入れて渡すと、ペーストボードから読み込める型をNSArrayの中から探し、見つかった最初の型を返してくれるメソッドです。見つからなかった場合はnilを返します。

 したがって引数で渡したデータ型が汎用ペーストボード上にあるかどうかを返すpasteboardHas:メソッドは以下の様になります。

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

#import "LissajousTableView.h"

#import "Lissajous.h"

@implementation LissajousTableView

#pragma mark -
#pragma mark PasteBoard methods

- (BOOL)pasteboardHas:(NSString *)theType
{
    NSPasteboard    *pasteboard = [NSPasteboard generalPasteboard];
    NSArray         *types = [NSArray arrayWithObject:theType];

    return  ([pasteboard availableTypeFromArray:types] != nil);
}

メニューの有効・無効をコントロールする

 LissajousTableViewはレスポンダチェーンの中にあるので、例えばcopy:メソッドを定義すると、copyメニューが有効になります。コピーできる状況であろうがなかろうが、とにかく有効になってしまいます。これは望ましい動作ではありません。

 メニューの有効・無効をコントロールしたい場合はvalidateMenuItem:メソッドを実装して有効にしたい場合YES、無効にしたい場合NOを返します。

 カット・コピー・デリートメニューの場合、リサージュ図形が選択されていればメニューを有効にします。ペーストメニューの場合ペーストボード上に該当するデータが存在する場合、メニューを有効にします。ここで先ほど用意したメソッドを使います。

 メニュー項目を判定するのにラベルではなくアクションを使っていますが、こうするとメニュー項目がローカライズされているかどうかを意識する必要がなくなるメリットがあります。

- (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:[Lissajous dataType]];

    return  YES;
}

アクション名をセットする

 アンドゥメニューに現れるメニュー項目が適切なものになる様にしたいので、アクション名を設定します。これはRMGRootのsetActionName:と同じ事をやっているのですが、アンドゥマネージャへのアクセス方法が異なります。

- (void)setActionName:(NSString *)actionName
{
    NSUndoManager   *undoManager = [[arrayController managedObjectContext] undoManager];

    if(![undoManager isUndoing] && ![undoManager isRedoing])
        [undoManager setActionName:NSLocalizedString(actionName,nil)];
}

コピーメソッド

 準備が終わって、ここからようやくカット&ペーストを実行するメソッドの実装に入ります。まずはコピーメソッドですが、これは用意したユーティリティメソッドcopyObjects:を呼び出して終了です。

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

削除メソッド

 管理対象オブジェクトを削除するには、管理対象オブジェクトコンテキストにdeleteObject:メッセージを送ります。引数は削除するオブジェクトです。
 したがってdelete:メソッドでは、現在選択されているオブジェクトを対象にループを回し、管理対象オブジェクトコンテキストにdeleteObject:メッセージを送ればOKです。

 削除はアンドゥの対象なので、アクション名をセットします。Localizable.stringsにdeleteLissajousをキーとするキー値ペアを追加する必要があるのですが、これについては後でまとめて書きます。

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

    while(selectedObject = [enumerator nextObject])
        [[arrayController managedObjectContext] deleteObject:selectedObject];
    [self setActionName:@"deleteLissajous"];
}

カットメソッド

 カットとはコピーして削除する事です。その通りにメソッドを呼びだした後にアクション名をセットして終了です。

- (IBAction)cut:(id)sender
{
    [self copy:sender];
    [self delete:sender];
    [self setActionName:@"cutLissajous"];
}

ペーストメソッド

 用意したユーティリティメソッドpasteFromPasteboard:controller:を呼び出し、アクション名をセットして終了です。

- (IBAction)paste:(id)sender
{
    [Lissajous pasteFromPasteboard:[NSPasteboard generalPasteboard]
                        controller:arrayController];
    [self setActionName:@"pasteLissajous"];
}

@end

Localizable.stringsにキー値ペアを追加する

 新たなアクション名を追加したので、それに対応するキー値ペアをLocalizable.stringsに追加します。これでアンドゥ時のメニュー項目が適切なものになります。

"setAmplitude" = "Change Amplitude";
"setFrequency" = "Change Frequency";
"setPhaseLag" = "Change PhaseLag";
"setResolution" = "Change Resolution";

"cutLissajous" = "Cut Lissajous Figure";
"deleteLissajous" = "Delete Lissajous Figure";
"pasteLissajous" = "Paste Lissajous Figure";