Gtkでマウスカーソルに追従するツールチップの作り方

もしかしたらGtk3でも同じ方法で実装できるかもしれませんが、サンプルコードはGtk2で書いています。

コードは`main()`から読むとわかりやすいと思います。

カレンダーみたいなウィジェットを自力で実装することを考えます。カレンダーには日付やスケジュールなどが書かれているとします。
このときスケジュールが長いと、実装によっては、対象のセルに収まりきらず表示が切れてしまいます。

そんなとき、ツールチップにスケジュールを省略せずに表示できると便利です。

ポイントはいくつかあるのですが、箇条書きで示します。

* `query-tooltip`イベントとカスタムツールチップでやる
    * どちらか片方だけだとツールチップの表示位置がずれたり、ツールチップのサイズが制御できなかったりで、中途半端になってしまう
* `has-tooltip`を`TRUE`にしてから`query-tooltip`に`g_signal_connect()`する
* `gtk_widget_set_tooltip_window()`でカスタムツールチップを使うようにする
* `gtk_widget_set_tooltip_window()`の第2引数のGtkWindowはサイズ変更不可にしておく
    * これでツールチップのサイズがツールチップに表示するテキストの長さによって自動的に調整される

### コード

```c:main.c
#include <gtk/gtk.h>
#include <cairo/cairo.h>
#include <stdlib.h>

static void destroy(GtkWidget *widget, gpointer data)
{
  gtk_main_quit();
}

// 格子状の模様を書く
static gboolean expose_event(GtkWidget *widget,
                             GdkEventExpose *event,
                             gpointer data)
{
  cairo_t *cr = gdk_cairo_create(widget->window);
  cairo_rectangle(cr,
                  event->area.x, event->area.y,
                  event->area.width, event->area.height);
  cairo_clip(cr);

  cairo_set_source_rgba (cr, 1, 0.2, 0.2, 0.6);
  cairo_set_line_width (cr, 2.0);

  for (int x = 0; x <= 500; x += 25) {
    cairo_move_to(cr, x, 0);
    cairo_line_to(cr, x, 500);
  }

  for (int y = 0; y <= 500; y += 25) {
    cairo_move_to(cr, 0, y);
    cairo_line_to(cr, 500, y);
  }

  cairo_stroke(cr);

  cairo_destroy(cr);
  return TRUE;
}

static gchar *prev_text = NULL;

static gboolean query_tooltip(GtkWidget  *widget,
                              gint        x,
                              gint        y,
                              gboolean    keyboard_mode,
                              GtkTooltip *tooltip,
                              gpointer    user_data)
{
  gint x_cell = x / 25;
  gint y_cell = y / 25;
  gint root_x = 0;
  gint root_y = 0;
  GtkWidget *label = GTK_WIDGET(user_data);

  gdk_window_get_root_coords(widget->window,
                             x, y,
                             &root_x, &root_y);

  gchar *text;
  // ツールチップに表示するテキストの長さを場所によって変える
  if (x % 5 == 0 || x % 3 == 0) {
    text = g_strdup_printf("(%05d,%05d)", x_cell, y_cell);
  } else {
    text = g_strdup_printf("(%d,%d)", x_cell, y_cell);
  }
  // 前の query-tooltip イベントと同じマスにいるなら何もしない
  if (!g_strcmp0(prev_text, text)) {
    g_message("%s, %s", prev_text, text);
    g_free(text);
    return FALSE;
  }
  // gtk_widget_set_tooltip_window() 経由で
  // カスタムツールチップを使うときはこれだとダメ
  // gtk_tooltip_set_text(tooltip, text);
  // ↓は両方呼ばないとツールチップの内容が変更できない
  gtk_widget_set_tooltip_text(widget, text);
  gtk_label_set_text(GTK_LABEL(label), text);

  GtkWindow *tooltip_window = gtk_widget_get_tooltip_window(widget);
  if (tooltip_window) {
    gtk_window_move(tooltip_window, root_x, root_y);
    g_debug("move");
  }

  g_free(prev_text);
  prev_text = text;

  return FALSE;
}

int main(int argc, char* argv[])
{
  GtkWidget *window;
  GtkWidget *drawing_area;
  GtkWidget *tooltip_window;
  GtkWidget *tooltip;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  drawing_area = gtk_drawing_area_new();

  gtk_container_add(GTK_CONTAINER(window), drawing_area);

  gtk_widget_set_size_request(window, 500, 500);

  // カスタムツールチップ用のGtkWidgetを構築する
  tooltip_window = gtk_window_new(GTK_WINDOW_POPUP);
  tooltip = gtk_label_new("");
  gtk_label_set_line_wrap(GTK_LABEL(tooltip), TRUE);
  // サイズ変更不可にしないと、ツールチップのテキストを変更したとき
  // ツールチップのサイズが追従しない
  gtk_window_set_resizable(GTK_WINDOW(tooltip_window), FALSE);
  gtk_widget_set_name(tooltip_window, "gtk-tooltip");
  gtk_container_add(GTK_CONTAINER(tooltip_window), tooltip);
  gtk_widget_show(tooltip);

  // has-tooltip を TRUE にしてから query-tooltip に g_signal_connect() する
  gtk_widget_set_has_tooltip(drawing_area, TRUE);

  g_signal_connect(window,
                   "destroy",
                   G_CALLBACK(destroy),
                   NULL);
  g_signal_connect(drawing_area,
                   "expose-event",
                   G_CALLBACK(expose_event),
                   NULL);
  g_signal_connect(drawing_area,
                   "query-tooltip",
                   G_CALLBACK(query_tooltip),
                   tooltip);

  gtk_widget_set_tooltip_window(drawing_area,
                                GTK_WINDOW(tooltip_window));

  gtk_widget_show(window);
  gtk_widget_show(drawing_area);

  gtk_main();

  return EXIT_SUCCESS;
}
```

```mf:Makefile
build: main.c
	gcc $< -Wall -o main `pkg-config --cflags --libs gtk+-2.0 cairo`
```

### 参考

* [GtkWidget: GTK+ 2 Reference Manual](https://developer.gnome.org/gtk2/stable/GtkWidget.html "GtkWidget: GTK+ 2 Reference Manual")
* [GtkLabel: GTK+ 2 Reference Manual](https://developer.gnome.org/gtk2/stable/GtkLabel.html "GtkLabel: GTK+ 2 Reference Manual")
* [GtkTooltip: GTK+ 2 Reference Manual](https://developer.gnome.org/gtk2/stable/GtkTooltip.html "GtkTooltip: GTK+ 2 Reference Manual")
* [GtkWindow: GTK+ 2 Reference Manual](https://developer.gnome.org/gtk2/stable/GtkWindow.html "GtkWindow: GTK+ 2 Reference Manual")
* Gtk+-2.0のソースコード