[戻る]

GTK+でdrag and drop

ドラッグ&ドロップの基本です。
GTK+ 2.x系を使用します。

drop側の処理とサンプルコード

ドロップ側として動作させるにはgtk_drag_dest_set()でウィジェットを登録します。
そして、ドラッグ&ドロップで使用するシグナルハンドラを登録します。
ドロップ側で発生するシグナルには以下のものがあります。
特別なことをしないならば、これらシグナルのうちdrag-dara-receivedだけシグナルハンドラを用意し、残りはgtk_drag_dest_set()でデフォルト動作のハンドラを設定できます。

あまり綺麗なソースではありませんがサンプルです。drop.c
文字コードはUTF-8です。
サンプルでは全てのハンドラを自前で用意しています。
[TOP]

gtk_drag_dest_set()

サンプルでは受け側のラベルだけのウィンドウを作りgtk_signal_connect()でハンドラを登録してからgtk_drag_dest_set()でドロップ可能なウィジェットとしています。
void gtk_drag_dest_set ( GtkWidget            *widget,
                         GtkDestDefaults       flags,
                         const GtkTargetEntry *targets,
                         gint                  n_targets,
                         GdkDragAction         actions );
widgetで指定したウィジェットでドロップが出来るようになります。
flagsはデフォルトでの動作をGtkDestDefaultsの論理和で指定します。
typedef enum {
    GTK_DEST_DEFAULT_MOTION    = 1 << 0,
    GTK_DEST_DEFAULT_HIGHLIGHT = 1 << 1,
    GTK_DEST_DEFAULT_DROP      = 1 << 2,
    GTK_DEST_DEFAULT_ALL       = 0x07
} GtkDestDefaults;
GTK_DEST_DEFAULT_MOTIONはドロップ可否の判定を自動で行わせる場合に指定します。複雑な事をしないのであれば指定しておいた方が便利でしょう。 drag-motionシグナルハンドラを用意する必要が無くなります。
GTK_DEST_DEFAULT_HIGHLIGHTをセットするとドロップ可能なときにウィジェットがハイライト描画されます。ハイライトといってもささやかな変化の場合が多いようです。
GTK_DEST_DEFAULT_DROPはドロップ可否の判定とgtk_drag_get_data()の呼び出しを自動で行わせる場合に指定します。このフラグも指定しておくと便利な場合が多いと思います。drag-dropシグナルハンドラを用意する必要が無くなります。
GTK_DEST_DEFAULT_ALLは上記フラグを全てセットしたときと同じです。

targetsに受け入れ可能なデータ型、n_targetsにその要素数をセットします。
サンプルではファイルの初めのほうで配列を定義しています。
struct GtkTargetEntry {
    gchar *target;
    guint  flags;
    guint  info;
};
targetに受け入れ可能なmime型を設定します。
flagsGtkTargetFlagsのいづれか、あるいは0を設定します。
GTK_TARGET_SAME_APPをセットしたときは同じアプリケーションからのドラッグ&ドロップのみ可能になります。
GTK_TARGET_SAME_WIDGETをセットしたときは同じウィジェットからのドラッグ&ドロップのみ可能になります。
infoには任意の値を設定します。drag-data-receivedハンドラにこの値が渡されるので、mime型毎に別々の値を設定しておくとデータ形式の判別が楽になります。

actionsはドロップ操作で実行できる動作をGdkDragActionの論理和で指定します。
いろいろ変更してみてください。
typedef enum {
    GDK_ACTION_DEFAULT = 1 << 0,
    GDK_ACTION_COPY    = 1 << 1,
    GDK_ACTION_MOVE    = 1 << 2,
    GDK_ACTION_LINK    = 1 << 3,
    GDK_ACTION_PRIVATE = 1 << 4,
    GDK_ACTION_ASK     = 1 << 5
} GdkDragAction;
GDK_ACTION_PRIVATEはドロップ側で何をするかドラッグ側には判別不能な動作の場合に使用するらしいです。GTK_TARGET_SAME_APPGTK_TARGET_SAME_WIDGETで何か特殊な動作をさせる場合に利用するんでしょうか?
[TOP]

drag-motion シグナルハンドラ

gboolean dest_drag_motion ( GtkWidget      *widget,
                            GdkDragContext *context,
                            gint            x,
                            gint            y,
                            guint           time_,
                            gpointer        user_data );
ドラッグ中のマウスカーソルがウィジェット内に移動したときに呼び出されます。
ドロップ可否の判別等を行います。
サンプルでは左側にマウスを移動した場合はアクションを質問、右側の場合はコピーを推奨の動作とし、質問またはコピーができない場合はドロップ不可としています。
推奨する動作はgdk_drag_status()で設定します。

void gdk_drag_status ( GdkDragContext *context,
                       GdkDragAction   action,
                       guint32         time_ );
ドロップ可否はdrag-motionハンドラの戻り値で通知します。
TRUEを返せばドロップ可能、FALSEなら現在のマウスカーソル位置ではドロップ不可能となります。
[TOP]

drag-leave シグナルハンドラ

void dest_drag_leave ( GtkWidget      *widget,
                       GdkDragContext *context,
                       gint            x,
                       gint            y,
                       guint           time_,
                       gpointer        user_data );
マウスカーソルがウィジェット外へ移動したときと、ウィジェット内にドロップした直後、drag-dropシグナルの前に呼び出されます。
drag-motionハンドラで行った変更を元に戻すといった事に使用できます。
例えば、drag-motionハンドラでウィジェットの描画を変更したりステータスバーに情報表示して、drag-leaveで元に戻す等です。
[TOP]

drag-drop シグナルハンドラ

gboolean dest_drag_drop ( GtkWidget      *widget,
                          GdkDragContext *context,
                          gint            x,
                          gint            y,
                          guint           time_,
                          gpointer        user_data );
ドロップしたときに呼び出されます。
この場所でドロップ可能か判定し、ドロップ可能であればgtk_drag_get_data()を呼びだしてTRUEを返します。
ドロップ不可の場合はFALSEを返します。

void gtk_drag_get_data ( GtkWidget      *widget,
                         GdkDragContext *context,
                         GdkAtom         target,
                         guint32         time_ );
targetに受け取るデータ形式に対応したGdkAtom値を指定します。

ドロップ可能なデータ形式とGdkAtom値を見つけるために以下の関数を使用できます。
GtkTargetList* gtk_drag_dest_get_target_list ( GtkWidget *widget );
ハンドラに渡されたwidgetをそのまま渡すとドロップ可能なデータ形式のリストを得られます。
そのリストを次の関数に渡すと受け取り可能なデータ形式を決定してくれます。

GdkAtom gtk_drag_dest_find_target ( GtkWidget      *widget,
                                    GdkDragContext *context,
                                    GtkTargetList  *target_list );
受け取り可能なデータ形式がない場合はGDK_NONEが返されます。

受け取るデータ形式に優先順位がある場合など自分で決定したい場合はこの関数を使用せず、context->targetsの中身を調べて適当なデータ形式を決定します。
context->targetsGdkAtom値を格納したGList型で、後述するgdk_atom_intern()gdk_atom_name()を使用してGdkAtomとmime型の変換を行い受け取り可能なデータ形式を調べることができます。
サンプルのdrag-motionハンドラではcontext->targetsの中身をmime型文字列へ変換してコンソール出力を行っています。
[TOP]

drag-data-received シグナルハンドラ

void dest_drag_data_received ( GtkWidget        *widget,
                               GdkDragContext   *context,
                               gint              x,
                               gint              y,
                               GtkSelectionData *data,
                               guint             info,
                               guint             time_,
                               gpointer          user_data );
ドラッグ側からデータを受け取ったときに呼び出されます。
infoにはgtk_drag_dest_set()で指定したtargetsの値がはいるので、データ形式ごとに別々の値を設定してあれば受け取ったデータ形式を簡単に判別することができます。
dataに受け取ったデータ等の情報がはいっています。
struct GtkSelectionData {
    GdkAtom     selection;
    GdkAtom     target;
    GdkAtom     type;
    gint        format;
    guchar     *data;
    gint        length;
    GdkDisplay *display;
};
gdk_atom_intern()gdk_atom_name()を使用するとmime型の文字列からGdkAtomGdkAtomからmime型の文字列を得ることができます。
g_print("%s\n", gdk_atom_name(data->type));

if (data->type == gdk_atom_intern("text/uri-list",FALSE)) {
    /* なにかする */
}
といった感じです。

最後にドロップ処理が完了したかどうかをドラッグ側に通知します。
void gtk_drag_finish ( GdkDragContext *context,
                       gboolean        success,
                       gboolean        del,
                       guint32         time_);
成功したときはsuccessTRUE、失敗したときはFALSE
データ移動してdrag側でデータを削除させる場合にdelTRUEにして呼び出します。
サンプルでは推奨動作が質問のときdrag-data-receivedハンドラで質問用のダイアログを表示していますが、drag-dropハンドラ内で行えばキャンセルしたときにgtk_drag_get_data()を呼びださずにすむのでそちらのほうが良いかもしれません。
[TOP]

drag側の処理とサンプルコード

ドラッグ側として動作させるには2つの方法があります。
1つはgtk_drag_source_set()でウィジェットを登録し、GTK+に自動でドラッグ処理を開始させる方法。
もう1つは、自分でドラッグ開始を検知してgtk_drag_begin()でドラッグ処理を開始させる方法です。
大抵の場合はgtk_drag_source_setを使用する方法で良いと思います。
そして、ドラッグ側でもシグナルハンドラを登録します。 ドラッグ側で発生するシグナルには以下のものがあります。 どんなデータをドラッグ&ドロップするかによりますが、最低限drag-data-getシグナルハンドラは必要です。
残りはデータや処理内容により必要に応じてハンドラを登録します。

ドラッグ側として設定可能なウィジェットには制限があり、GDKウィンドウを持っていなければなりません。
GtkLabelのように自分自身のウィンドウを持たないものはドラッグ側として使用できません。

あまり上手くないサンプルです。drag.c
文字コードはUTF-8です。
サンプルではマクロUSE_GTK_DRAG_BEGINが未定義の場合はgtk_drag_source_set()を使用し、定義済みの場合はgtk_drag_begin()でドラッグ処理を自分で開始します。
[TOP]

gtk_drag_source_set()

void gtk_drag_source_set ( GtkWidget            *widget,
                           GdkModifierType       start_button_mask,
                           const GtkTargetEntry *targets,
                           gint                  n_targets,
                           GdkDragAction         actions );
gtk_drag_dest_set()と似ていますが、こちらは第2パラメータがドラッグ開始のボタン指定になっています。
ドラッグ開始に使用するボタンをGDK_BUTTON1_MASKGDK_BUTTON5_MASKの論理和で指定します。
[TOP]

gtk_drag_begin()

自分でドラッグ&ドロップを開始する方法です。
gtk_drag_source_set()を使用する場合と比べ、しなければいけないことが沢山あります。
ドラッグ開始するにはボタンが押されたまま移動した事を検出する必要があります。その為に以下のシグナルのハンドラを用意します。
マウスボタンがウィジェット内で押されたときにそのボタンと位置を保存し、マウスボタンが離されたらその情報をクリアします。
マウスボタンが押されたままマウスカーソルを移動し、その移動量が一定以上になった場合にドラッグ&ドロップ処理を開始します。
移動量を開始条件に含めるのは、ボタンを押したあと僅かでも動かしたらドラッグ開始となってしまうのは不便な事が多いからです。

また、ボタンを押した後、ドラッグ&ドロップ処理を始める前にマウスカーソルがウィジェット外へ移動した場合にも保存した情報をクリアします。
これをしないとウィジェットの隅でボタンを押し、そのままウィジェット外へ移動してボタンを離し、またウィジェット内へマウスカーソルを移動した瞬間にドラッグ処理が始まってしまいます。

またこれら全てのハンドラではFALSEを返して他のハンドラの動作を妨げないようにします。

gboolean button_press_event ( GtkWidget      *widget,
                              GdkEventButton *event,
                              gpointer        user_data );
ドラッグ&ドロップ処理中であれば何もしません。
マウスボタンが押されたら、押されたボタンとその位置を保存します。
サンプルではボタンに優先順位をつけています。ボタン3よりボタン2、ボタン2よりボタン1を優先しています。

gboolean button_release_event ( GtkWidget      *widget,
                                GdkEventButton *event,
                                gpointer        user_data );
ドラッグ&ドロップ処理中でなければボタン情報をクリアします。

gboolean motion_notify_event ( GtkWidget      *widget,
                               GdkEventMotion *event,
                               gpointer        user_data );
ドラッグ&ドロップ処理中であれば何もしません。
ボタンが押されたままであればマウスカーソルの移動量を調べて、一定以上であればgtk_drag_begin()でドラッグ開始します。
移動量チェックはgtk_drag_check_threshold()を使用します。

gboolean leave_notify_event ( GtkWidget        *widget,
                              GdkEventCrossing *event,
                              gpointer          user_data );
ドラッグ&ドロップ処理中でなければマウスボタンの情報をクリアします。



次はマウスカーソルの移動量を知らべる関数です。
上記ハンドラであつめた情報を使用して移動量が閾値を越えているかをチェックします。
gboolean gtk_drag_check_threshold ( GtkWidget *widget,
                                    gint       start_x,
                                    gint       start_y,
                                    gint       current_x,
                                    gint       current_y );
start_xstart_yにはマウスボタンを押したときの位置を、current_xcurrent_yには現在のマウスカーソルの位置を渡します。
motion-notify-eventシグナルハンドラ内で使用するので現在の位置は(gint)event->x(gint)event->yです。
この関数の結果がTRUEになったらドラッグ&ドロップ処理中フラグをセットして、gtk_drag_begin()でドラッグ処理を開始します。

GdkDragContext* gtk_drag_begin ( GtkWidget     *widget,
                                 GtkTargetList *targets,
                                 GdkDragAction  actions,
                                 gint           button,
                                 GdkEvent      *event );
targetsgtk_target_list_new()で作成します。
buttonはドラッグ開始時に押されているマウスボタンです。ボタン1なら1、ボタン2なら2というように指定します。
eventmotion-notify-eventシグナルハンドラに渡されたeventを渡します。

GtkTargetList* gtk_target_list_new ( const GtkTargetEntry *targets,
                                     guint                 ntargets );
targetsntargetsgtk_drag_source_set()に渡すtargetsn_targetsと同じです。
作成したリストはリンクカウントを持ち、この値が1になっています。
リンクカウントはgtk_target_list_ref()で1増加し、gtk_target_list_unref()で1減少します。
リンクカウントが0になったときに自動的にリソースが開放されます。
ここではgtk_drag_begin()で使用したあと、gtk_target_list_unref()に渡します。
void gtk_target_list_unref ( GtkTargetList *list );
[TOP]

drag-begin シグナルハンドラ

void source_drag_begin( GtkWidget      *widget,
                        GdkDragContext *context,
                        gpointer        user_data );
ドラッグ処理が開始されたときに呼ばれます。
サンプルでは何もしていません。
[TOP]

drag-data-get シグナルハンドラ

void source_drag_data_get ( GtkWidget        *widget,
                            GdkDragContext   *context,
                            GtkSelectionData *data,
                            guint             info,
                            guint             time_,
                            gpointer          user_data );
ドロップ側からgtk_drag_get_data()でデータを要求されたときに呼ばれます。
ドラッグ&ドロップするデータをdataにセットします。
infoにはドロップ側がgtk_drag_source_set()で指定した値が入っているので、 その値にあわせてデータを用意します。

データのセットにはgtk_selection_data_set()関数やそのバリエーションを使用します。
void gtk_selection_data_set ( GtkSelectionData *selection_data,
                              GdkAtom           type,
                              gint              format,
                              const guchar     *data,
                              gint              length );
selection_dataにハンドラに渡されたdata
typeにセットするデータの型data->target
formatはセットするデータのビット長(バイト単位のときは8)、
dataにはセットするデータへのポインタ、
lengthはその長さを指定します。

データをセットしたくないとき、出来無いときは何もしないでハンドラの処理を終了します。

サンプルでは"text/uri-list"のとき、カレントディレクトリにある*.c *.txt *.htm *.htmlの全て、"text/plain"のとき、hello, world!の文字をセットしています。
[TOP]

drag-data-delete シグナルハンドラ

void source_drag_data_delete ( GtkWidget      *widget,
                               GdkDragContext *context,
                               gpointer        user_data );
ドロップ側でgtk_drag_finish()の第3パラメータがTRUEで呼び出された場合に呼び出されます。
ドラッグ&ドロップでデータを移動し、ドラッグ側でデータの削除が必要な場合はここで行います。
サンプルでは何もしていません。
[TOP]

drag-end シグナルハンドラ

void source_drag_end ( GtkWidget      *widget,
                       GdkDragContext *context,
                       gpointer        user_data );
ドラッグ&ドロップ処理が全て完了したときに呼びだされます。
表示の更新等に利用できます。
サンプルではgtk_drag_begin()でドラッグ開始した場合にドラッグ&ドロップ処理中フラグのクリアをしています。
それ以外は何もしていません。
[TOP]

drag and dropのシグナルと発生順

ドラッグ&ドロップを行ったときに発生するシグナルとその順序は以下のようになります。

ユーザ操作 drag側 drop側
(1) ドラッグ開始    
  (2) drag-beginシグナル  
(3) マウスカーソルをdrop側に移動    
    (4) drag-motionシグナル
(5) ドロップ    
    (6) drag-leaveシグナル
    (7) drag-dropシグナル
→ gtk_drag_get_data()呼び出し
  (8) drag-data-getシグナル
→ gtk_selection_data_set()呼び出し
 
    (9) drag-data-receivedシグナル
→ gtk_drag_finish()呼び出し
  (10) drag-data-deleteシグナル
(gtk_drag_finish()の第3パラメータがTRUEで呼び出された場合)
 
  (11) drag-endシグナル  
(7)のgtk_drag_get_data()呼び出しによって(8)のdrag-data-getシグナルが発生し、
(9)のgtk_drag_finish()呼び出しによって(10)のdrag-data-deleteシグナル、(11)のdrag-endシグナルが発生します。
[TOP]
[戻る]