普通のドラッグ&ドロップと同じ事を行います。
gtk_tree_view_enable_model_drag_source()と
gtk_tree_view_enable_model_drag_dest()を使用すれば通常とは違った実装のドラッグ&ドロップが行われますが、今回は使用しません。
ドラッグ側は
gtk_drag_source_set()で登録すると簡単なのですが、そうするとアイテムの無い空白部分でもドラッグ開始できるようになってしまいます。
GtkTreeViewを使用する場合はアイテムが存在する場所でのみドラッグ開始させたいことが多いのではないかと思います。
そこでドラッグの開始はマウスカーソルの位置にアイテムが存在するかどうかをチェックして
gtk_drag_begin()でドラッグ開始させます。
ドラッグ&ドロップ処理が開始されればあとは通常の場合と変りありませんが、ドラッグ&ドロップのシグナルハンドラ内でデフォルトのシグナルハンドラを呼び出さないようにします。
デフォルトハンドラでは
GtkTreeDragSource、
GtkTreeDragDestインターフェイス用の処理が行われますが、今回は
gtk_tree_view_enable_model_drag_source()、
gtk_tree_view_enable_model_drag_dest()を使用しないので実際には事前チェックではねられて何も行われません。
とはいえ意図しないコードが実行されるのはあまり気持が良くないので
GtkTreeViewが何もしない事を期待するのではなく、きちんと自分でデフォルトハンドラ呼び出しを停止させます。
今回は使用しませんが、モデルデータに
GtkTreeModelFilterを使用する場合、
GtkTreeModelFilterは
GtkTreeDragDestインターフェイスを実装していないので、
drag-data-receivedのデフォルトハンドラはコンソールに警告メッセージを何行も出力します。
その中に重要なメッセージが埋もれてしまわないよう、フィルタリングする場合は少なくとも
drag-data-receivedデフォルトハンドラは停止させたほうが良いと思います。
サンプルは
GtkListStoreでデータを作成して表形式にしています。
gtktreeview_dnd.c
ちょっと欲張りすぎていろいろ詰め込みすぎになってしまいました。
表示データは本棚にあるものから適当に作成しました。追求しないでください。
(^_^;
文字コードはUTF-8です。
基本は
通常のドラッグ&ドロップの開始チェックと同じです。
自分でドラッグ開始を判定するために下記シグナルのハンドラを作成して、その中でマウスボタンの押下状態、カーソルの移動状態をチェックします。
- button-press-event
マウスカーソルの位置にアイテムが存在する場合のみ、押したマウスボタンの番号を保存する。
ボタン2または3を押した場合は自分でアイテムの選択処理を行い、他のハンドラを実行しないようにする。
- button-release-event
ボタン押下情報をクリアする。
サンプルでは実装していないですが、ポップアップメニューの表示を行う場合はここで行う。
- motion-notify-event
ボタンが押されたまま一定量以上マウスカーソルが移動されていたらgtk_drag_begin()を呼び出してドラッグ&ドロップ処理を開始する。
- leave-notify-event
ボタン押下情報をクリアする。
この中で少し注意しなければならないのは
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 );
x、
yにイベントの発生場所
event->x、
event->yを渡すと、
pathにマウスカーソルの位置にあるアイテムのパス、
columnに列、さらに
cell_xと
cell_yにセル内の位置がセットされます。
必要ない項目には
NULLを渡します。
しかし、その前に
button-press-eventシグナルがちゃんと
GtkTreeViewの描画ウィンドウ内で発行されたのかを確認しなければなりません。
これは少し変だと思うでしょう。指定したウィジェット上でのイベントを処理する為にシグナルハンドラを登録しているので、そのイベントがどこで発生したかなんてわざわざ確認する必要なんて無いはずです。
普通であればそうなのでしょうが、
GtkTreeViewでは少し事情が違うようです。
GtkTreeViewのヘッダ部分で発生したイベントは
GtkTreeViewColumnに渡してもらうのが自然だと思うのですが、ヘッダの区切り部分でボタンを押すと
GtkTreeViewで
button-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で囲って無効にしている箇所です。
drag-begin、
drag-data-get、
drag-data-delete、
drag-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 );
同じウィジェット内でのドラッグ&ドロップのとき、選択中の(ドラッグしている)アイテム上ではドロップ不可にしています。
ドラッグを開始したウィジェットは次の関数で確認できます。
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_xと
drag_yにはハンドラに渡された
xと
yを渡します。
TRUEが返されたときに
pathにマウスカーソルの位置にあるアイテムのパスが、
posにはドロップした時にデータが挿入される位置が入ります。
posは
gtk_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で終了すればその場所ではドロップ不可となります。
drag-motionハンドラではタイマを使用してウィジェットの端にカーソルがあるときに自動でスクロールするようにしています。
ドロップ先のアイテムを選択したい場合には自動でスクロールしてくれると便利です。
guint g_timeout_add ( guint interval,
GSourceFunc function,
gpointer data );
intervalにミリ秒単位の待ち時間、
functionに待ち時間経過後に呼び出す関数、
dataには
functionに渡すパラメータをセットします。
戻り値はイベントIDとして0より大きな値が返されます。このIDはタイマを解除するときに使用します。
functionは
TRUEを返している間
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()を呼び出します。
と、ここまでマルチスレッドの考慮を行いましたが、今回のサンプルはマルチスレッドアプリケーションではないので
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-leave、
drag-drop、
button-release-eventハンドラでタイマ解除を行っています。
マウスカーソルがウィジェット外へ移動した時は
drag-leaveシグナル、ドロップした時は
drag-dropシグナルでタイマ解除を行えば良いのですが、ドロップ不可の場所でドロップするとそこでドラッグ&ドロップ処理は終了し、
drag-leave、
drag-dropのどちらも発行されません。
そこで苦肉の策として
button-release-eventシグナルハンドラでもタイマー解除を行うようにしています。
button-release-eventハンドラでタイマ解除を行っているので、
drag-dropハンドラでの解除処理は実際には無駄になっています。
自動スクロールの方法は
GtkTreeViewのソースを参考にしました。
バージョン2.12以降は
gdk_threads_add_timeout()を使用しています。こちらの方がタイマの精度が良いらしいです。