Finderからファイルやフォルダをドラッグ&ドロップ
してそのパスを表示するTextFieldを作ってみる


私がちょっとした目的で使うために作るツールには、テキストフィールド+FileOpenPanelを開く選択ボタンでファイルパスを指定させるケースが結構多いのですが、いちいちボタンクリック&選択操作をするのがかったるくなってきて、ファインダから直接ドラッグ&ドロップ一発でパスを入力できたらなと思うようになりました。

そこでドラッグ&ドロップの勉強もしたかったというのもあって、上記機能を果たすテキストフィールドを作って見ることにしました。

まずはどこから手を付けてよいのかわからないのでネットで検索したところ、makuriさんのホームページの「Makuri の Cocoa Tips」にある「 ファイルをドラッグしてパスを貼付ける」というページが見つかり、そこから必要な情報を得ることができました。
(ありがとうございました m(__)m >Makuriさん)

目的の機能を実現するTextFieldを作成する手順の概略は以下の通りです。

(1) NSTextFieldのサブクラスを作成する
(2) 上記サブクラスの初期化処理の中で、このView(NSTextFieldはNSViewを継承しています)にドロップ可能なデータの種類を登録する
(3) (1)で作成したクラスに、ドロップを受け付ける側で対応すべきメソッドを規定したNSDraggingDestinationプロトコルのうち、必要なメソッドを実装する
(4) IB上で、パスのドロップを行いたいテキストフィールドのクラスをNSTextFieldから(1)で作成したクラスに変更する

1.サンプルの概要

サンプルとして以下のような画面のプログラムを作ります。

テキストフィールドが3つあるのは、今回作成するテキストフィールドの拡張クラスでは、指定により

 ・ファイルのみ
 ・ディレクトリのみ
 ・ファイル/ディレクトリ両方

のドロップを可能にすることを考えているからです。

2.NSDraggingDestinationプロトコルメソッドについて

NSDraggingDestinationプロトコルでは以下の7つのメソッドが規定されています。

//
// ドラッグ中に呼び出されるメソッド
//
// ドロップ可能領域にドラッグ中のマウスポインタが入ったことの通知
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender

// ドロップ可能領域内でドラッグ中のマウスポインタが移動時に通知される
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender

// ???
- (void)draggingEnded:(id <NSDraggingInfo>)sender

// ドロップ可能領域からドラッグ中のマウスポインタが外れたことの通知
- (void)draggingExited:(id <NSDraggingInfo>)sender

//
// ドロップ時に呼び出されるメソッド
//
// ドロップの前準備要求通知
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender

// ドロップ処理の実行要求
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender

// ドラップ処理の完了通知
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender

これらメソッドはそれぞれどのようなタイミングで呼び出されるのか、また、今回実現する機能範囲でインプリメントが必要なものは何かを調べるために実験を行ってみました。

具体的には、

(1) NSDraggingDestinationプロトコルで規定されているメソッドをすべて実装し、メソッド内でNSLogによりメソッドが呼ばれたことをログへ出力する
(2) ビルド&実行を行い、以下の操作を行う
・Finderからファイルを目的のフィールド上にドラッグする
・ドラッグしたまま一旦フィールド外へ出る
・ドラッグしたまま再びフィールド内に入り、そこでドロップを行う

ということを行い、その結果出力されるログを解析してみました。

実行結果のログを以下に示します。

2004-05-19 09:51:47.833 TblViewTest[529] awakeFromNib            <= 初期化
2004-05-19 09:51:59.776 TblViewTest[529] draggingEntered:        <= ドラッグして中に入る
2004-05-19 09:51:59.779 TblViewTest[529] draggingUpdated:
2004-05-19 09:51:59.780 TblViewTest[529] draggingUpdated:
     ・
     ・
    中略
     ・
     ・
2004-05-19 09:52:00.242 TblViewTest[529] draggingUpdated:
2004-05-19 09:52:00.243 TblViewTest[529] draggingUpdated:
2004-05-19 09:52:00.259 TblViewTest[529] draggingExited:         <= 一旦外へ出る
2004-05-19 09:52:01.010 TblViewTest[529] draggingEntered:        <= 再び中に入る
2004-05-19 09:52:01.027 TblViewTest[529] draggingUpdated:
2004-05-19 09:52:01.027 TblViewTest[529] draggingUpdated:
     ・
     ・
    中略
     ・
     ・
2004-05-19 09:52:01.684 TblViewTest[529] draggingUpdated:
2004-05-19 09:52:01.685 TblViewTest[529] draggingUpdated:
2004-05-19 09:52:01.690 TblViewTest[529] prepareForDragOperation:    <= ドロップする
2004-05-19 09:52:01.691 TblViewTest[529] performDragOperation:
2004-05-19 09:52:01.691 TblViewTest[529] concludeDragOperation:

これを見ると、フィールド上をドラッグしながらマウスを移動させるとdraggingUpdated:が連続的に呼ばれるのですが、今回の目的ではドラッグ中のマウス位置などの情報が必要な訳ではないのでインプリメントは必要ありませんね。
また、ドロップ時に呼ばれるprepareForDragOperation:も、ドロップの実行時にたいしたことをするわけではなく、準備らしい準備も必要ではないのでこれも不要です。
なお、ログを見るとdraggingEnded:は呼ばれていませんが、仕様書を見るとこのメソッドは何やら複雑な仕掛けのために用意されているようですが、現在はまだインプリメントされてないとのことなのでこれも不要です。

以上から以下のメソッドのインプリメントを行うことにします。

 draggingEntered:
 draggingExited:
 performDragOperation:
 concludeDragOperation:

3.プロジェクトの作成とウィンドウの設計

「Cocoa Application」タイプのプロジェクトを作成し、「1.サンプルの概要」で示したような画面を作成してください。
なおこのとき、各テキストフィールドの編集可能属性を外しておいてください。理由は後で説明します。



4.NSTextFieldのサブクラスを作成する

IBのnibパネルの「Classes」タブの画面でNSTextFieldのサブクラスを作成します。

クラス名としてここでは「UCFilePathTextField」と名付けます。
クラスの作成が完了したらソースファイルへの書き出しも行っておいてください。



5.画面上のテキストフィールドのクラスを自作のクラスに変更する

先程IB上で作成した各テキストフィールドのクラスを「NSTextField」から先程作成した「UCFilePathTextField」に変更してください。



6.NSTextFieldのサブクラスに各種処理を実装する

6.1 実装するメソッドについて

NSTextFieldのサブクラスに実装するメソッドの一覧を以下に示します。

メソッド 機能概要
awakeFromNib 初期化処理
fileListInDraggingInfo: ドラッギング情報の中から現在選択されているファイル/ディレクトリの一覧を取得する
setAcceptableObjectType: ドロップ可能オブジェクト種別(ファイル or ディレクトリ)の設定
acceptableObject: 現在ドラッグ中のオブジェクトが自フィールドにドロップ可能か判定する
setInDragging: フィールド上をドロップ可能なオブジェクトがドラッグ中であるか否かの設定
draggingEntered: ドラッグしてフィールド内に入ったことの通知
draggingExited: フィールドから外れたことの通知
performDragOperation: ドロップ処理の実行要求
concludeDragOperation: ドロップ処理の完了通知
drawRect: ドラッグ中のフィールド枠の強調表示用にオーバーライド

このうち、以下のメソッドは設定により、ファイルのみ、ディレクトリのみ、または両方をドロップ可能にするために追加した独自のメソッドです。

 fileListInDraggingInfo:
 setAcceptableObjectType:
 acceptableObject:
 setInDragging:

以降、これらのメソッドの実装とインスタンス変数について記述します。

6.2 インスタンス変数

ヘッダファイルに以下のインスタンス変数を追加します。

    int     _nAcceptableObjectType;      // ドロップ可能オブジェクト種別
    BOOL    _bInDragging;                // ドラッグ中フラグ

_nAcceptableObjectTypeは、このフィールドがドロップ可能なオブジェクトの種別(ファイル/ディレクトリ)の種別を保持しておく領域で、値の定義は以下の通りとします。

// ドラッグ&ドロップで受付可能なオブジェクト種別
#define ACCEPT_BOTH         0
#define ACCEPT_FILE         1
#define ACCEPT_DIRECTORY    2

_bInDraggingは、フィールド上をドロップ可能なオブジェクトがドラッグ中、フィールド枠を強調表示するための制御に用いるフラグです。

6.3 初期化処理(awakeFromNib)

NSView系クラスの初期化メソッドにはもう一つinitWithRect:というのがあるのですが、IB上で配置したコントロールの場合、initWithRect:は呼ばれず、awakeFromNibが呼ばれます。

awakeFromNibに組み込むコードを以下に示します。

// 初期化処理
- (void)awakeFromNib
{
    // 親クラス呼び出し
    // 注)AppKitではawakeFromNibは定義のみでインプリメントはされていないので
    //    呼び出し可能か確認する必要があるとのこと
    if ( [[self superclass] instancesRespondToSelector:@selector(awakeFromNib)] ) {
        [super awakeFromNib];
    }

    // ドラッグ受付対象の登録(ファインダーからのファイルパス)
    NSArray *parrTypes = [NSArray arrayWithObject:NSFilenamesPboardType];
    [self registerForDraggedTypes:parrTypes];

    // インスタンス変数の初期化
    _bInDragging            = NO;
    _nAcceptableObjectType  = ACCEPT_BOTH;
}

親クラスの呼び出しに関しては、Appleドキュメントのどこに書いてあったのか失念しましたが、コメントに書いてあるように実装されているか確認してから呼び出してくれとのことでした。

なお、ここの処理で一番大事な処理は、中ほどにあるドラッグ受付対象の登録処理で、このメソッド自体はNSViewで定義されています。

// ドラッグ対象のペーストボード中のオブジェクト種別を登録する
- (void)registerForDraggedTypes:(NSArray *)pboardTypes

パラメータで指定するpboardTypesにはドラッグ可能なオブジェクトの種別(テキスト、イメージ etc...)をArrayで指定するのですが、Finderからのファイルのドラッグの場合はNSFilenamesPboardTypeのみを指定します。
なお、この定数はNSPasteboardクラスで定義されています。

どなたのサイトだったか本だったか失念しましたが、「ドラッグ&ドロップは見た目がど派手なコピー&ペーストだ!」と言われていた通り、ドラッグ&ドロップではコピー&ペーストで使用するペーストボードがかかわってきます。

6.4 ドラッギング情報の中からファイル一覧取得(fileListInDraggingInfo:)

このメソッドはNSDraggingDestinationプロトコルのメソッドが呼ばれるときに渡されるドラッギング情報の中からFinder上で選択されているファイル/ディレクトリのパスの一覧を取得するもので、処理内容は以下の通りです。

// ドラッグ情報からファイルリスト取得
- (NSArray *)fileListInDraggingInfo:(id <NSDraggingInfo>)info
{
    // ペーストボードオブジェクト取得
    NSPasteboard *poPasteBd = [info draggingPasteboard];

    // ドラッッグされたファイルの一覧取得
    NSArray *parrFiles = [poPasteBd propertyListForType:NSFilenamesPboardType];

    return parrFiles;
}

処理内容はコメントのとおり、ドラッギング情報の中からペーストボードオブジェクトを取得し、その中から指定した種別のオブジェクトを取得するというものです。

6.5 ドロップ可能オブジェクト種別の設定(setAcceptableObjectType:)

このメソッドは単にインスタンス変数にパラメータで指定された種別を設定するだけです。

// ドロップ可能オブジェクト種別(ファイル or ディレクトリ or BOTH)の設定
- (void)setAcceptableObjectType:(int)nType
{
    _nAcceptableObjectType = nType;
}

6.6 ドラッグ中オブジェクトが自フィールドにドロップ可能か判定(acceptableObject:)

このメソッドでは以下のチェックを行い、BOOL値で結果を返します。

複数のファイルが選択されていたらNG
ドラッグ中のファイルの種別をと、_nAcceptableObjectTypeに設定されている受付可能なオブジェクトの種別と比較し、OK/NGの判定を行う。

具体的なコードを以下に示します。

// 現在ドラッグ中のオブジェクトが自フィールドにドロップ可能か判定する
- (BOOL)acceptableObject:(id <NSDraggingInfo>)info
{
    NSArray         *parrFiles;
    NSString        *pstrPath;
    NSFileManager   *poFM = [NSFileManager defaultManager];
    BOOL            bRet, bDir;


    // ドラッッグされたファイルの一覧取得
    parrFiles = [self fileListInDraggingInfo:info];
    if ( [parrFiles count] > 1 ) {
        // 複数のドラッグは受付不可
        return NO;
    }

    // ドラッグされたパスが受付可能であるかチェック
    pstrPath = [parrFiles objectAtIndex:0];
    bRet = [poFM fileExistsAtPath: pstrPath isDirectory: &bDir];
    if ( bRet != YES ) {
        // あり得ないと思うがパスが存在しなかった場合
        return NO;
    }
    switch ( _nAcceptableObjectType ) {
    case ACCEPT_FILE:
        // ファイルのみ
        return (bDir == YES) ? NO : YES;
        break;

    case ACCEPT_DIRECTORY:
        // ディレクトリのみ
        return bDir;
        break;
    
    default:
        // 両方OK
        return YES;
    }
}

6.7 ドラッグ中であるか否かの設定(setInDragging:)

このメソッドは、フィールド上をドラッグ中にそのオブジェクトがドロップ可能であることを示すためにフィールドの強調表示を行う制御のためのもので、内部では以下の処理を行っています。

状態管理用インスタンス変数_bInDraggingに指定された値を設定
強調表示の状態を変更するためにNSView(このクラスの上位クラス)のsetNeedsDisplay:をYES指定で呼び出し、強制的に次のイベントループで再描画処理を起動する(実際のフィールドの強調表示の処理(描画)は引き続き呼び出されるdrawRect:で行います)

具体的なコードを以下に示します。

// フィールド上をドロップ可能なオブジェクトがドラッグ中であるか否かの設定
- (void)setInDragging:(BOOL)flag
{
    // 状態変更
    _bInDragging = flag;

    // 強調表示制御のために強制再描画要求
    [self setNeedsDisplay:YES];
}

6.8 ドラッグしてフィールド内に入ったことの通知(draggingEntered:)

このメソッドはドラッグ中のマウスポインタがこのテキストフィールドに入ってきたときに呼び出されるもので、渡されたパラメータや、自身の状態からドラッグを受付可能かどうかを復帰値として返します。
ここでは以下の処理を行います。

自身のacceptableObject:メソッドでドラッグされているものが受け入れ可能かチェックする
受け入れ可能であった場合は自身のsetInDragging:メソッドでドラッグ中状態に設定する

具体的なコードを以下に示します。

// ドラッグしてフィールド内に入ったことの通知
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
    // 受付可能なオブジェクトかチェックする
    BOOL bRet = [self acceptableObject:sender];
    if ( bRet != YES ) {
        return NSDragOperationNone;
    }

    // ドラッグ中状態に設定
    [self setInDragging: YES];

    return NSDragOperationGeneric;
}

復帰値の定義についてはNSDraggingInfoプロトコルを参照してください。

6.9 フィールドから外れたことの通知(draggingExited:)

このメソッドはドラッグ中のマウスポインタがこのテキストフィールドから外に出たときに呼び出されます。
ここでは以下の処理を行います。

自身のsetInDragging:メソッドでドラッグ中状態を解除する

具体的なコードを以下に示します。

// フィールドから外れたことの通知
- (void)draggingExited:(id <NSDraggingInfo>)sender
{
    // ドラッグ中状態解除
    [self setInDragging:NO];

    return;
}

6.10 ドロップ処理の実行要求(performDragOperation:)

このメソッドは以前呼び出されたdraggingEntered:やdraggingUpdated:でドロップ可能と返した状態でこのテキストフィールド上でマウスボタンを放した時に呼び出されます。
ここでは以下の処理を行います。

ドラッグング情報からドラッグされたファイルのパス取得
取得したパス文字列を自テキストフィールドに設定

具体的なコードを以下に示します。

// ドロップ処理の実行要求
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
    // ドラッッグされたファイルの一覧取得
    NSArray *parrFiles = [self fileListInDraggingInfo:sender];
    if ( [parrFiles count] > 1 ) {
        // 複数のドラッグは受付不可
        // (draggingEntered:の時点ではじいてるのでまずないとは思うが念のため)
        return NO;
    }

    // ドラッグされたファイルパス取得
    NSString *pstrPath = [parrFiles objectAtIndex:0];

    // パス文字列をテキストフィールドに設定
    [self setStringValue:pstrPath];

    return YES;
}

6.11 ドロップ処理の完了通知(concludeDragOperation:)

このメソッドは直前に呼び出されるperformDragOperation:がYESで正常終了した後に呼び出されます。
ここでは以下の処理を行います。

自身のsetInDragging:メソッドでドラッグ中状態を解除する

具体的なコードを以下に示します。

// ドロップ処理の完了通知
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender
{
    // ドラッグ中状態解除
    [self setInDragging:NO];

    return;
}

6.12 ドラッグ中のフィールド枠の強調表示(drawRect:)

受け入れ可能なオブジェクトがテキストフィールド上をドラッグ中に、ドロップ可能であることを示すためにフィールド枠を強調表示させるのですが、そのためにNSViewの描画メソッドであるdrawRect:をオーバーライドします。
ここでは以下の処理を行います。

親クラスNSTextFieldのdrawRect:を呼び出しNSTextFieldとしての基本的な描画を行わせる
_bInDraggingをチェックし、テキストフィールド上でドラッグ中であれば強調表示の枠線を描画する

具体的なコードを以下に示します。

// ドラッグ中のフィールド枠の強調表示用にオーバーライド
- (void)drawRect:(NSRect)aRect
{
    // まずはNSTextFieldとしての描画を実行させる
    [super drawRect:aRect];

    if ( _bInDragging == YES ) {
        // ドラッギング中はフレーム枠を強調表示
        [[NSColor selectedControlColor] set];
        NSFrameRectWithWidth( aRect, 2.0 );
    }
}

7.アプリケーションのdelegateとなるクラスの作成

このクラスはアプリケーション起動時に画面上の各テキストフィールドに受付可能なオブジェクトのタイプを設定するために作成します。
今回はUCAppControllerというクラス名で作成します。クラスの作成方法と、アプリケーションのdelegateとしての登録方法はここを参照してください。

UCAppControllerクラスを作成したら、以下のOutletを定義し、ウィンドウのそれぞれのテキストフィールドに接続します。

    // テキストフィールドへのOutlet
    IBOutlet UCFilePathTextField *_outPathBoth;
    IBOutlet UCFilePathTextField *_outPathDirOnly;
    IBOutlet UCFilePathTextField *_outPathFileOnly;

次に、UCAppControllerクラスのインプリメントファイルにアプリケーション起動時に呼び出される以下のメソッドを実装します。

// アプリケーション起動時
- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
    // 各テキストフィールドへドロップ可能なオブジェクト種別を設定
    [_outPathFileOnly   setAcceptableObjectType:ACCEPT_FILE];
    [_outPathDirOnly    setAcceptableObjectType:ACCEPT_DIRECTORY];
    [_outPathBoth       setAcceptableObjectType:ACCEPT_BOTH];
}

8.ビルド&実行

以上で作成作業は完了ですので、ビルド&実行してみてください。
ファイルのみ、フォルダのみのフィールドでは対象外のものをドラッグしても強調表示されず、ドラッグが受け付けられないことがわかると思います。

9.テキストフィールドの編集可能属性について

このサンプルの作成開始時に、テキストフィールドの編集可能属性を外すように指示しましたが、一通り動くようになったところで編集可能に変更して実行してみてください。

いかがでしょうか。入力フォーカスがドロップ対象のフィールドに無い場合はよいのですが、入力フォーカスがあるとドロップできなくは無いのですが、ドロップ可能として認識される領域がテキストフィールドの下側のかなり狭い範囲になってしまいます。

理由はわからないのですが、ちょっと困ったものです。(バグ? or 構造上の仕様?)

サンプルプログラムのダウンロード(24KB)

【作成・確認環境】
Mac OS X v10.3.4
Xcode v1.2


一覧に戻る