[戻る]

GtkTreeViewでdrag and drop

GtkTreeViewで普通のドラッグ&ドロップを行います。
gtk_tree_view_enable_model_drag_source()、gtk_tree_view_enable_model_drag_dest()等は使いません。

ドラッグ&ドロップ処理の概要とサンプルコード

普通のドラッグ&ドロップと同じ事を行います。
gtk_tree_view_enable_model_drag_source()gtk_tree_view_enable_model_drag_dest()を使用すれば通常とは違った実装のドラッグ&ドロップが行われますが、今回は使用しません。

ドラッグ側はgtk_drag_source_set()で登録すると簡単なのですが、そうするとアイテムの無い空白部分でもドラッグ開始できるようになってしまいます。
GtkTreeViewを使用する場合はアイテムが存在する場所でのみドラッグ開始させたいことが多いのではないかと思います。 そこでドラッグの開始はマウスカーソルの位置にアイテムが存在するかどうかをチェックしてgtk_drag_begin()でドラッグ開始させます。
ドラッグ&ドロップ処理が開始されればあとは通常の場合と変りありませんが、ドラッグ&ドロップのシグナルハンドラ内でデフォルトのシグナルハンドラを呼び出さないようにします。
デフォルトハンドラではGtkTreeDragSourceGtkTreeDragDestインターフェイス用の処理が行われますが、今回はgtk_tree_view_enable_model_drag_source()gtk_tree_view_enable_model_drag_dest()を使用しないので実際には事前チェックではねられて何も行われません。
とはいえ意図しないコードが実行されるのはあまり気持が良くないのでGtkTreeViewが何もしない事を期待するのではなく、きちんと自分でデフォルトハンドラ呼び出しを停止させます。
今回は使用しませんが、モデルデータにGtkTreeModelFilterを使用する場合、GtkTreeModelFilterGtkTreeDragDestインターフェイスを実装していないので、drag-data-receivedのデフォルトハンドラはコンソールに警告メッセージを何行も出力します。 その中に重要なメッセージが埋もれてしまわないよう、フィルタリングする場合は少なくともdrag-data-receivedデフォルトハンドラは停止させたほうが良いと思います。

サンプルはGtkListStoreでデータを作成して表形式にしています。gtktreeview_dnd.c
ちょっと欲張りすぎていろいろ詰め込みすぎになってしまいました。
表示データは本棚にあるものから適当に作成しました。追求しないでください。(^_^;
文字コードはUTF-8です。
[TOP]

ドラッグ側の処理

ドラッグ開始を自分でチェックしてgtk_drag_begin()を呼び出します。
また、アイテムのある場所のみドラッグ可能とし、何もない空白部分ではドラッグ開始しないようにします。
ドラッグ開始したあとはデフォルトのシグナルハンドラを呼び出さないようにします。
[TOP]

ドラッグ開始の判定処理

基本は通常のドラッグ&ドロップの開始チェックと同じです。
自分でドラッグ開始を判定するために下記シグナルのハンドラを作成して、その中でマウスボタンの押下状態、カーソルの移動状態をチェックします。
この中で少し注意しなければならないのはbutton-press-eventシグナルハンドラでのマウスカーソル位置にアイテムが存在するかのチェックです。
カーソル位置にあるアイテムはgtk_tree_view_get_path_at_pos()で調べます。
gboolean gtk_tree_view_get_path_at_pos ( GtkTreeView        *tree_view,
                                         gint                x,
                                         gint                y,
                                         GtkTreePath       **path,
                                         GtkTreeViewColumn **column,
                                         gint               *cell_x,
                                         gint               *cell_y );
xyにイベントの発生場所event->xevent->yを渡すと、pathにマウスカーソルの位置にあるアイテムのパス、columnに列、さらにcell_xcell_yにセル内の位置がセットされます。
必要ない項目にはNULLを渡します。

しかし、その前にbutton-press-eventシグナルがちゃんとGtkTreeViewの描画ウィンドウ内で発行されたのかを確認しなければなりません。
これは少し変だと思うでしょう。指定したウィジェット上でのイベントを処理する為にシグナルハンドラを登録しているので、そのイベントがどこで発生したかなんてわざわざ確認する必要なんて無いはずです。
普通であればそうなのでしょうが、GtkTreeViewでは少し事情が違うようです。
GtkTreeViewのヘッダ部分で発生したイベントはGtkTreeViewColumnに渡してもらうのが自然だと思うのですが、ヘッダの区切り部分でボタンを押すとGtkTreeViewbutton-press-eventシグナルが発行されます。
APIリファレンスにもgtk_tree_view_get_path_at_pos()の説明でevent->windowを確認するように記述されています。
その為に以下の関数を使用します。
GdkWindow* gtk_tree_view_get_bin_window ( GtkTreeView *tree_view );
この関数の戻り値とbutton-press-eventシグナルに渡されたevent->windowを比較して、同じときだけgtk_tree_view_get_path_at_pos()でアイテムチェックを行います。
この判定を怠ると意図しないパスを取得してしまい、その結果プログラムは期待した動作を行わないかもしれません。

またbutton-press-eventハンドラではボタン2または3が押された場合に自分でアイテムの選択処理を行っていますが、これはデフォルトの動作に違和感があったのでそのようにしているだけです。
自分で選択処理を行った場合はデフォルトハンドラが動作しないようTRUEを返しています。

button-release-eventハンドラでg_source_remove()を呼び出しているのは自動スクロールの為の処理です。後で説明します。
ボタン2または3で何か実行したりポップアップメニューを表示する場合は、ボタンを押したときではなく離したときのbutton-release-eventシグナルハンドラで行います。
サンプルでは#if 0 〜 #endifで囲って無効にしている箇所です。
[TOP]

デフォルトハンドラの停止とその他の処理

drag-begindrag-data-getdrag-data-deletedrag-endの各シグナルハンドラの処理も通常の場合と同じです。
ただ1つの違いはg_signal_stop_emission_by_name()でデフォルトのシグナルハンドラ呼び出しを停止している事です。
void g_signal_stop_emission_by_name ( gpointer     instance,
                                      const gchar *detailed_signal );
instanceにハンドラに渡されたウィジェットを、detailed_signalにハンドラと同じシグナルを文字列で渡します。
この呼び出し停止処理は毎回行う必要があります。

ドラッグ&ドロップとは関係無いですが、drag-data-deleteで選択中のアイテムを削除する時にGtkTreeRowReferenceを使用しています。
選択中アイテムのパスはgtk_tree_selection_get_selected_rows()で取得できますが、取得したパスをそのまま使用して複数のアイテムを削除しようとすると、削除により事前に取得したパスが無効になり削除できなくなります。
そのため、取得したパスGtkTreePathをリファレンスGtkTreeRowReferenceに変更し、削除する毎にGtkTreePathに戻しています。
変換は以下の関数で行います。
GtkTreeRowReference* gtk_tree_row_reference_new ( GtkTreeModel *model,
                                                  GtkTreePath  *path );

GtkTreePath* gtk_tree_row_reference_get_path ( GtkTreeRowReference *reference );
GtkTreeRowReferenceから直接GtkTreeIterに変換できると便利なのですが今のところそういう機能は無いですね。

GtkTreeRowReferenceも使用し終ったらGtkTreePathと同様にリソースを開放します。
void gtk_tree_row_reference_free ( GtkTreeRowReference *reference );
[TOP]

ドロップ側の処理

ドロップ側もドラッグ側と同様にg_signal_stop_emission_by_name()でデフォルトのシグナルハンドラを停止させている点以外は通常の処理と基本的に同じです。
drag-motiondrag-dropハンドラでは同じウィジェット内でのドラッグ&ドロップのとき、選択中の(ドラッグしている)アイテム上ではドロップ不可にしています。
また、drag-motionで自動スクロールを設定し、drag-leavedrag-dropbutton-release-eventでその登録解除を行います。
[TOP]

マウスカーソル下のアイテムチェック

同じウィジェット内でのドラッグ&ドロップのとき、選択中の(ドラッグしている)アイテム上ではドロップ不可にしています。
ドラッグを開始したウィジェットは次の関数で確認できます。
GtkWidget* gtk_drag_get_source_widget ( GdkDragContext *context );
contextにはシグナルハンドラに渡された値をそのまま渡します。
この戻り値がハンドラのウィジェットと同じであれば同一ウィジェット内でドラッグ&ドロップが行われていることになります。

その後、マウスカーソルの位置にあるアイテムを調べ、アイテムが存在していたら選択中かどうかを確認します。
このときのアイテムチェックはbutton-press-eventシグナルハンドラで使用したものとは別の関数を使用します。
gboolean gtk_tree_view_get_dest_row_at_pos ( GtkTreeView             *tree_view,
                                             gint                     drag_x,
                                             gint                     drag_y,
                                             GtkTreePath            **path,
                                             GtkTreeViewDropPosition *pos );
ドラッグ&ドロップ中にマウスカーソルの位置にあるアイテムを調べる場合はこの関数を使用します。
drag_xdrag_yにはハンドラに渡されたxyを渡します。
TRUEが返されたときにpathにマウスカーソルの位置にあるアイテムのパスが、posにはドロップした時にデータが挿入される位置が入ります。
posgtk_tree_view_enable_model_drag_dest()を使用したときに挿入される位置なので今回は無視します。
gtk_tree_view_get_path_at_pos()でも同じことが出来そうに思えますがこちらでは正しい結果を得られません。 ヘッダを表示しているとその分ずれた結果が返されるみたいです。

パスを取得したら選択中かどうか判定します。
GtkTreeSelection* gtk_tree_view_get_selection ( GtkTreeView *tree_view );

gboolean gtk_tree_selection_path_is_selected ( GtkTreeSelection *selection,
                                               GtkTreePath *path );
gtk_tree_view_get_selection()で得たGtkTreeSelectionと先に調べたパスをgtk_tree_selection_path_is_selected()に渡します。
選択中ならgtk_tree_selection_path_is_selected()TRUEを返します。
戻り値がTRUEならgdk_drag_status()でステータス変更して、ハンドラをFALSEで終了すればその場所ではドロップ不可となります。
[TOP]

ウィジェットの自動スクロール

drag-motionハンドラではタイマを使用してウィジェットの端にカーソルがあるときに自動でスクロールするようにしています。
ドロップ先のアイテムを選択したい場合には自動でスクロールしてくれると便利です。

guint g_timeout_add ( guint       interval,
                      GSourceFunc function,
                      gpointer    data );
intervalにミリ秒単位の待ち時間、functionに待ち時間経過後に呼び出す関数、dataにはfunctionに渡すパラメータをセットします。
戻り値はイベントIDとして0より大きな値が返されます。このIDはタイマを解除するときに使用します。
functionTRUEを返している間interval毎に繰り返し呼び出されます。FALSEを返すとタイマ登録は自動で解除されます。
functionは何もする事がない時に実行され、他のイベント処理により実行が遅延することもあります。

サンプルではこの関数に下記の自動スクロール用の関数を登録しています。
gboolean scroll_tree_view ( gpointer user_data )
この中でマウスカーソルの位置を調べ、その場所がウィジェットの端だった場合にウィジェットを格納しているGtkScrolledWindowからGtkAdjustmentを取得してその値を変更することにより上下左右にスクロールさせています。
上下または左右どちらかスクロールしたときにはまだスクロールできるかもしれないのでTRUEを返して繰り返し呼び出されるようにし、スクロールしなかった時は何度呼ばれても同じ場所ではスクロールできないのでFALSEを返してタイマ解除するようにしています。



自動スクロール用関数ではGtkTreeViewのスクロール処理を行う前にGTK+メインループとの排他処理を行います。

GTK+の基礎となるGDKやGLibはマルチスレッドセーフになっていますが、GTK+はマルチスレッドセーフになっていません。 また、GDKやGLib内のグローバル変数は自動的にロックされて保護されますが、それ以外の個別の変数はパフォーマンス上の理由で自動的にロックされません。 これらの変数への読み書きやGTK+関数の呼び出しを正しく行う為に排他制御を行います。

GTK+のシグナルハンドラはメインループから排他処理を行って呼び出されるのでマルチスレッドを意識する必要は無いのですが、今回使用したg_timeout_add()等から呼び出されるハンドラはGTK+の関知しないところで動作します。その為、その中からGTK+関数を使用するときには排他処理が必要となります。
void gdk_threads_enter (void);

void gdk_threads_leave (void);
スクロール用関数でGTK+ウィジェットや関数を使用している箇所の前後をgdk_threads_enter()gdk_threads_leave()で囲めば安全に操作が出来ます。

GTK+バージョン2.12以降の場合はスクロール用関数の登録をgdk_threads_add_timeout()で行なえばgdk_threads_enter()等の呼び出しをGDK内で行ってくれるので少しだけ楽ができます。
guint gdk_threads_add_timeout ( guint       interval,
                                GSourceFunc function,
                                gpointer    data );
戻り値、引数はg_timeout_add()と同じです。
スレッドの詳細やマルチスレッドアプリケーションの作成方法はGDKやGLib APIリファレンスのThreads項目を参照してください。

また今回は使用していませんが、GDK内に溜ったコマンドを処理させるためはgdk_flush()を呼び出します。
void gdk_flush (void);

と、ここまでマルチスレッドの考慮を行いましたが、今回のサンプルはマルチスレッドアプリケーションではないのでgdk_threads_enter()gdk_threads_leave()があっても無くても変りありません。
コードの流用や参考にしたときに忘れない為だけにマルチスレッドの考慮を加えています。



スクロール処理に戻ります。

マウスカーソルの位置を調べる為に以下の関数を使用します。
GdkWindow* gdk_window_get_pointer ( GdkWindow       *window,
                                    gint            *x,
                                    gint            *y,
                                    GdkModifierType *mask );

void gdk_window_get_geometry ( GdkWindow *window,
                               gint      *x,
                               gint      *y,
                               gint      *width,
                               gint      *height,
                               gint      *depth );
gdk_window_get_pointer()window上のマウスカーソル位置とシフトキーの状態を取得し、マウスカーソルのある描画ウィンドウを返します。
gdk_window_get_geometry()windowの表示位置、サイズ、表示色のビット数を取得します。
ともに不要な値にはNULLを渡すことができます。
これらの関数でマウスカーソルの位置とウィジェットの大きさを取得し、それぞれの値を比較して端にあるかどうかを調べます。
サンプルでは手を抜いてウィジェットの端32ドットにマウスカーソルがあるかをチェックしていますが、32ドットに特に根拠は無く、フォントサイズやDPI値を使用して環境毎に適切な値を使用すべきでしょう。

カーソルが端にあると判定されたら上下左右にスクロールさせます。
GtkAdjustment* gtk_scrolled_window_get_vadjustment ( GtkScrolledWindow *scrolled_window );

GtkAdjustment* gtk_scrolled_window_get_hadjustment ( GtkScrolledWindow *scrolled_window );

void gtk_adjustment_set_value ( GtkAdjustment *adjustment,
                                gdouble        value );
上下スルロールの場合はgtk_scrolled_window_get_vadjustment()、左右の場合はgtk_scrolled_window_get_hadjustment()GtkAdjustmentを取得し、その値をgtk_adjustment_set_value()で変更することでスクロールが行われます。
valueには上または左スクロールさせるときはadjustment->value - adjustment->step_incrementを、下または右スクロールのときはadjustment->value + adjustment->step_incrementを渡します。
実際には少し複雑な判定を行ってスクロールするかしないかを決定しています。

タイマの解除はg_source_remove()で行います。
gboolean g_source_remove ( guint tag );
tagにはg_timeout_add()が返したIDを渡します。
drag-leavedrag-dropbutton-release-eventハンドラでタイマ解除を行っています。
マウスカーソルがウィジェット外へ移動した時はdrag-leaveシグナル、ドロップした時はdrag-dropシグナルでタイマ解除を行えば良いのですが、ドロップ不可の場所でドロップするとそこでドラッグ&ドロップ処理は終了し、drag-leavedrag-dropのどちらも発行されません。 そこで苦肉の策としてbutton-release-eventシグナルハンドラでもタイマー解除を行うようにしています。
button-release-eventハンドラでタイマ解除を行っているので、drag-dropハンドラでの解除処理は実際には無駄になっています。

自動スクロールの方法はGtkTreeViewのソースを参考にしました。
バージョン2.12以降はgdk_threads_add_timeout()を使用しています。こちらの方がタイマの精度が良いらしいです。
[TOP]
[戻る]