ドラッグアウトで爆発アニメーションと共に消去する【実践的Macintoshプログラミング解説】

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

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

LinkIconホーム

更新日 2009-05-24

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

ドラッグアウトで爆発アニメーションと共に消去する

 NVKTableViewに「ドラッグアウトで爆発アニメーションと共に消去する」機能を追加してみます。これは要するにDockと同じユーザインタフェースにしようという事です。テーブルビューのアイテムをドラッグして、ドロップが受け入れられなかった場合は爆発アニメーションを表示し、そのアイテムを削除します。

 Dockの場合は元のファイルが削除されるわけではありませんが、こちらはモデルオブジェクトが管理対象オブジェクトコンテキストから削除されます。

 爆発アニメーションはNSShowAnimationEffect()を使えば実現できます。

NSDraggingSource非形式プロトコル

 ドラッグ元のオブジェクトがドラッグ操作中に受け取るメッセージはNSDraggingSource非形式プロトコルで定義されています。実装が必須とされているのはdraggingSourceOperationMaskForLocal:メソッドだけで、後はオプションです。

 実装が必須とされているdraggingSourceOperationMaskForLocal:メソッドを実装していなくてもうまくいっていたのですが、これはNSTableViewが実装してくれていたものと思われます。

 NVKTableViewではNSDraggingSource非形式プロトコルで規定されているメソッドのうち、以下のメソッドを実装します。

  • draggingSourceOperationMaskForLocal:
  • draggedImage:beganAt:
  • draggedImage:movedTo:
  • draggedImage:endedAt:operation:

 またドラッグ操作を始めるNSViewのメソッドdragImage:at:offset:event:pasteboard:source:slideBack:をオーバーライドします。このメソッドはNSTableViewが実装してくれているので本来は実装する必要はないのですが、ドラッグ開始時のマウスポインタの座標を知るためにオーバーライドしています。

インスタンス変数を追加する

 ドラッグ中に送られてくるメッセージの引数に座標データが含まれているのですが、この座標はドラッグされている画像の原点座標であって、マウスポインタの座標ではありません。処理上で必要なのはマウスポインタの座標なので、これを求めるためにドラッグ開始時の座標データを覚えておく必要があります。そこで、そのためのインスタンス変数startPointを追加します。またドラッグされている画像とマウスポインタの座標の差分を保持するインスタンス変数offsetを追加します。

 ドラッグ操作中にマウスポインタがテーブルビューの外へでているかどうかでカーソルの形状を変え、ユーザにこれから起こる現象を予告します。そこでマウスポインタがテーブルビューの外へでているかどうかを表すインスタンス変数draggingOutを追加します。

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

#import <Cocoa/Cocoa.h>


@interface NVKTableView : NSTableView
{
    BOOL                draggingOut;
    NSPoint             startPoint,offset;
    NSArrayController   *arrayController;
}

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

@end

ドラッグ操作をマスクする

 実装が必須とされているdraggingSourceOperationMaskForLocal:を実装します。これはどんな操作を許すのかというフラグを連結したものを返せばOKです。以下のような定数が定義されているので、必要な操作を論理和で連結して返します。(ただし最新情報はデベロッパドキュメントを参照する事)

  • NSDragOperationNone
  • NSDragOperationCopy
  • NSDragOperationLink
  • NSDragOperationGeneric
  • NSDragOperationPrivate
  • NSDragOperationAll_Obsolete(廃止予定。代わりにNSDragOperationEveryを使う)
  • NSDragOperationMove
  • NSDragOperationDelete
  • NSDragOperationEvery

 引数は自分のアプリケーション内のドラッグ操作かどうかを表すフラグです。

 NVKTableViewでは汎用性を考慮して、自分のアプリケーション内では全てのドラッグ操作を許可し、他のアプリケーションへのドラッグ操作はコピーのみ許可する事にします。

//
//  NVKTableView.m
//

//  自分のアプリケーション内におけるドラッグ操作は全て許可する
//  他のアプリケーションへのドラッグ操作はコピーのみ許可する
- (unsigned int)draggingSourceOperationMaskForLocal:(BOOL)isLocal
{
    return  isLocal ? NSDragOperationEvery : NSDragOperationCopy;
}

マウスクリックされた座標を覚えておく

 ドラッグ開始時のマウスポインタの座標を知るためにNSViewのメソッドdragImage:at:offset:event:pasteboard:source:slideBack:をオーバーライドします。引数のeventで渡されるのは、ドラッグ操作を引き起こした左クリックのマウスダウンイベントなので、locationInWindowメソッドでイベントの発生した座標(=クリックされた座標)が得られます。この座標はメソッド名の通りウィンドウ座標なので、スクリーン座標に直してから保存します。

 スーパークラスのメソッドを呼びだす際にslideBackをNOにしていますが、これはスライドバックする必要がないためです。ドロップを受け入れない場合にドラッグされている画像をもとの場所まで戻すアニメーションをするのがスライドバックですが、ドロップを受け入れない場合は爆発アニメーションと共にオブジェクトを削除するわけですから、スライドバックが不要になります。

//
//  NVKTableView.m
//

- (void)dragImage:(NSImage *)anImage
               at:(NSPoint)imageLoc
           offset:(NSSize)mouseOffset
            event:(NSEvent *)theEvent
       pasteboard:(NSPasteboard *)pboard
           source:(id)sourceObject
        slideBack:(BOOL)slideBack
{
    //  ドラッグ操作のメソッドで渡される座標はイメージの原点であって、マウスクリックされた座標ではない。
    //  そこでマウスクリックされた座標を覚えておく。
    startPoint = [[self window] convertBaseToScreen:[theEvent locationInWindow]];
    
    //  スライドバックする状況はないので、スライドバックを禁止する
    [super dragImage:anImage
                  at:imageLoc
              offset:mouseOffset
               event:theEvent
          pasteboard:pboard
              source:sourceObject
           slideBack:NO];
}

ドラッグ操作の初期化処理

 ドラッグされる画像が表示され、しかしまだ画像をマウスポインタに追随させる処理が始まっていない段階でdraggedImage:beganAt:メッセージが送られてきます。ここでドラッグ操作の初期化処理を行ないます。最初はドラッグアウトされている筈はありませんので、draggingOutをNOで初期化します。

 引数beganAt:で渡される座標は、ドラッグされる画像の原点で、これはスクリーン座標です。この座標と先ほど保存したstartPointとの差をoffsetとして保存しておきます。

//
//  NVKTableView.m
//

- (void)draggedImage:(NSImage *)anImage
             beganAt:(NSPoint)aPoint
{
    draggingOut = NO;

    //  マウスクリックされた座標とイメージの原点の差をオフセットとして持つ
    offset.x = startPoint.x - aPoint.x;
    offset.y = startPoint.y - aPoint.y;
}

ドラッグ中の処理

 ドラッグ操作中はドラッグ元にdraggedImage:movedTo:メッセージが送られてきます。ここではマウスカーソルを変える事でユーザにこれから起こる現象を予告します。

 まずmovedTo:引数で渡されるスクリーン座標をウィンドウ座標に変換します。これに先ほど計算したオフセットを足して、画像原点の座標をマウスポインタの座標に変換します。得られた座標を更に変換して、自分のビュー座標にします。この座標が自分自身のbounds内にあるかどうかで、マウスポインタがテーブルビューの中にあるか、外にあるかがわかります。

 マウスポインタが外から中に入ってきた場合は、カーソルを矢印カーソルにします。マウスポインタが中から外へでた場合はカーソルを爆発カーソル(というんでしょうか???)に変えます。

 最初はこれだけでよいかと思っていたのですが、マウスポインタが自分のウィンドウの外へでるとマウスカーソルが変えられてしまう問題が発生しました。ドロップを受け入れない場合は矢印カーソルに、受け入れる場合は状況に応じたカーソル(コピーの場合はプラスマークのついたカーソル)になります。問題はドロップを受け入れない場合で、この場合は爆発カーソルにしたいわけです。そこでマウスポインタがテーブルビューの外にあって、かつカーソルが矢印カーソルの場合は、カーソルを爆発カーソルに変える様にしました。

//
//  NVKTableView.m
//

- (void)draggedImage:(NSImage *)draggedImage
             movedTo:(NSPoint)screenPoint
{
    BOOL    pointInView;
    NSPoint windowPoint,viewPoint;
    
    windowPoint = [[self window] convertScreenToBase:screenPoint];
    //  オフセットを足す事でマウスカーソルの座標になる
    windowPoint.x += offset.x;
    windowPoint.y += offset.y;
    viewPoint = [self convertPoint:windowPoint fromView:nil];
    pointInView = NSPointInRect(viewPoint,[self bounds]);
    if(draggingOut && pointInView)
    {
        [[NSCursor arrowCursor] set];
        draggingOut = NO;
    }
    if(!draggingOut && !pointInView)
    {
        [[NSCursor disappearingItemCursor] set]; 
        draggingOut = YES;
    }
    if(!pointInView && [NSCursor currentCursor] == [NSCursor arrowCursor])
        [[NSCursor disappearingItemCursor] set]; 
}

ドラッグ終了時の処理

 ドラッグ先にデータ処理の機会が与えられた後で、ドラッグ元にdraggedImage:endedAt:operation:メッセージが送られてきます。爆発アニメーションと共にデータを削除するのは、ドラッグ操作がNSDragOperationNone(何もしない)でマウスポインタがテーブルビューの外にある場合です。この場合にNSShowAnimationEffect()で爆発アニメーションを実行し、マウスカーソルを矢印カーソルに戻します。データの削除処理はデータソースに任せます。

 NSShowAnimationEffectの引数は以下の様になっています。

void NSShowAnimationEffect (
   NSAnimationEffect animationEffect,
   NSPoint centerLocation,
   NSSize size,
   id animationDelegate,
   SEL didEndSelector,
   void *contextInfo
);

animationEffect
アニメーションの種類を指定する。NSAnimationEffectPoofで爆発アニメーションになる。
centerLocation
アニメーションの中心座標をスクリーン座標で指定する。引数の座標にオフセットを足して、マウスポインタの座標としています。
size
アニメーションのサイズ。NSZeroSizeにするとデフォルトのサイズになる。最初、ドラッグ画像のサイズとしましたが、複数の原図をドラッグアウトすると縦長になって見栄えが悪いので、画像の横幅を使った正方形にしました。
animationDelegate
アニメーション終了後にメッセージが送られるオブジェクト。データの削除処理をデータソースに任せたいので、データソースを指定しています。
didEndSelector
上記のデリゲートに送られるメッセージのセレクタ。デベロッパドキュメントで指定された通りのセレクタとしています。
contextInfo
上記セレクタの引数。何か情報を渡したい時に使えます。今回は特に渡したい情報もないのでnilとしています。

//
//  NVKTableView.m
//

- (void)draggedImage:(NSImage *)anImage
             endedAt:(NSPoint)aPoint
           operation:(NSDragOperation)operation
{
    if(operation == NSDragOperationNone && draggingOut)
    {
        NSShowAnimationEffect(NSAnimationEffectPoof,
                              NSMakePoint(aPoint.x + offset.x, aPoint.y + offset.y),
                              NSMakeSize([anImage size].width, [anImage size].width),
                              [self dataSource],
                              @selector(animationEffectDidEnd:),
                              nil);
        [[NSCursor arrowCursor] set];
    }
}

 データソースに実装するanimationEffectDidEnd:については次のページで説明します。