はじめに

ここではキーボード入力に対してGUIツールキットがどういう処理を行っているかを読解します。ツールキットはgtk+-2.12.5 & glib-2.14.5を対象とします。また読解で注目するのは以下の点です。

  • 下位レイヤーから送られてきた情報をどう抽象化しているか
  • コントロールフォーカスの管理

なお、プラットフォームはx11とします。

サンプルコード

読解するにあたり、以下のコードについて各関数でどのような処理が行われているかを見ていくことにします。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 
 
 
 
 
-
|
|
!
 
 
 
-
|
|
|
!
 
 
 
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
#include <stdio.h>
#include <gtk/gtk.h>
 
static gboolean
window_delete_event(GtkWidget *widget, GdkEvent *event, gpointer data)
{
    gtk_main_quit();
    return FALSE;
}
 
static void
button_clicked(GtkWidget *widget, gpointer data)
{
    const gchar *entry_text;
    entry_text = gtk_entry_get_text(GTK_ENTRY(data));
    printf("Entry contents: %s\n", entry_text);
}
 
int
main(int argc, char *argv[])
{
    GtkWidget *window;
    GtkWidget *hbox;
    GtkWidget *entry;
    GtkWidget *button;
 
    gtk_init(&argc, &argv);
 
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_container_set_border_width(GTK_CONTAINER(window), 8);
    g_signal_connect(G_OBJECT(window), "delete_event",
             G_CALLBACK(window_delete_event), NULL);
 
    hbox = gtk_hbox_new(FALSE, 0);
    gtk_container_add(GTK_CONTAINER(window), hbox);
    gtk_widget_show(hbox);
 
    entry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(hbox), entry, TRUE, TRUE, 4);
    gtk_widget_show(entry);
 
    button = gtk_button_new_with_label("Button");
    g_signal_connect(G_OBJECT(button), "clicked",
             G_CALLBACK(button_clicked), entry);
    gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
    gtk_widget_show(button);
 
    gtk_widget_show(window);
 
    gtk_main();
 
    return 0;
}

gtk_init(gtk/gtkmain.c)

GTKではまずgtk_init関数を呼び出す必要があります。gtk_init関数を呼び出したことによるコールグラフを以下に示します。わかることとしては、

  • GDKのプラットフォーム依存な部分は個別のディレクトリにまとめられている
  • 先頭に_が付いているのはプラットフォーム依存関数かと思ったが全部がそうなっているわけではないようだ
  • GDKのイベントハンドラとして渡されているgtk_main_do_eventがかなり怪しい
gtk_init_check [gtk/gtkmain.c]
  gtk_parse_args [gtk/gtkmain.c]
    g_option_context_parse [glib/goption.c]
      pre_parse_hook [gtk/gtkmain.c]
        do_pre_parse_initialization [gtk/gtkmain.c]
          gdk_pre_parse_libgtk_only [gdk/gdk.c]
            g_type_init [gobject/gtype.c]
            _gdk_windowing_init [gdk/x11/gdkmain-x11.cなど]
          gdk_event_handler_set(gtk_main_do_event) [gdk/gdkevents.c]
      post_parse_hook [gtk/gtkmain.c]
        do_post_parse_initialization [gtk/gtkmain.c]
          gtk_type_init [gtk/gtktypeutils.c]
          _gtk_accel_map_init [gtk/gtkaccelmap.c]
          _gtk_rc_init [gtk/gtkrc.c]
  gdk_display_open_default_libgtk_only [gdk.c]
    gdk_display_open [gdk/x11/gdkdisplay-x11.cなど]
      _gdk_x11_screen_new [gdk/x11/gdkscreen-x11.c]
        _gdk_visual_init [gdk/x11/gdkvisual-x11.c]
        _gdk_windowing_window_init [gdk/x11/gdkwindow-x11.c]
      _gdk_x11_events_init_screen [gdk/x11/gdkevents-x11.c]
      gdk_window_new [gdk/x11/gdkwindow-x11.c]
        setup_toplevel_window [gdk/x11/gdkwindow-x11.c]
          create_focus_window [gdk/x11/gdkwindow-x11.c]
      _gdk_events_init [gdk/x11/gdkevents-x11.c]
      _gdk_input_init [gdk/x11/gdkinput-none.c]
      _gdk_dnd_init [gdk/x11/gdkdnd-x11.c]

G_DEFINE_TYPE(gobject/gtype.h)

キーボード入力とは関係ないのですがGTK、というかGLibのオブジェクトについて。オブジェクトを作るときは以下のようにするようです。

display = g_object_new (GDK_TYPE_DISPLAY_X11, NULL);

GDK_TYPE_DISPLAY_X11の正体が何者かというと以下の者です。

#define GDK_TYPE_DISPLAY_X11              (_gdk_display_x11_get_type())
GType      _gdk_display_x11_get_type            (void);

問題なのはgrepをかけても_gdk_display_x11_get_typeという関数の定義が見つからないことです。きっとマクロで自動的に作られているに違いないと調べてみると

G_DEFINE_TYPE (GdkDisplayX11, _gdk_display_x11, GDK_TYPE_DISPLAY)

となっていました。G_DEFINE_TYPEマクロを展開すると型の登録およびget_type関数*1が定義されるようです。

GdkWindow

gtk_init関数呼び出しで実行されるコードの中でGdkWindow周りの部分が少しややこしいので書いておきます。

まず、GdkWindowの定義です。

gdk/gdktypes.h

typedef struct _GdkDrawable           GdkWindow;

で、_GdkDrawable構造体の定義。

struct _GdkDrawable
{
  GObject parent_instance;
};

次にGdkWindowを使っている所としてgdk_window_new関数です。

  GdkWindow *window;
  GdkWindowObject *private;
  GdkWindowImplX11 *impl;
  ...
  window = g_object_new (GDK_TYPE_WINDOW, NULL);
  private = (GdkWindowObject *)window;
  impl = GDK_WINDOW_IMPL_X11 (private->impl);

GdkWindowをGdkWindowObjectにキャストしています。何故そんなことをして問題ないかと言うと、

gdk/gdkwindow.h

#define GDK_TYPE_WINDOW              (gdk_window_object_get_type ())

gdk/gdkwindow.c

GType
gdk_window_object_get_type (void)
{
  static GType object_type = 0;

  if (!object_type)
    object_type = g_type_register_static_simple (GDK_TYPE_DRAWABLE,
						 "GdkWindow",
						 sizeof (GdkWindowObjectClass),
						 (GClassInitFunc) gdk_window_class_init,
						 sizeof (GdkWindowObject),
						 (GInstanceInitFunc) gdk_window_init,
						 0);
  
  return object_type;
}

というわけで先ほどget_type関数はマクロから作られると書きましたが自分で書くことも可能でGDK_TYPE_WINDOWに対して実際にはGdkWindowObjectが作成されるため、キャストしても問題ないということになっています。

ついでにimplですが、

gdk/gdkwindow.c

static void
gdk_window_init (GdkWindowObject *window)
{
  /* 0-initialization is good for all other fields. */

  window->window_type = GDK_WINDOW_CHILD;

  window->state = GDK_WINDOW_STATE_WITHDRAWN;
  
  window->impl = g_object_new (_gdk_window_impl_get_type (), NULL);
}

gdk/x11/gdkwindow-x11.c

G_DEFINE_TYPE (GdkWindowImplX11, gdk_window_impl_x11, GDK_TYPE_DRAWABLE_IMPL_X11)

GType
_gdk_window_impl_get_type (void)
{
  return gdk_window_impl_x11_get_type ();
}

のようになっています。

g_signal_emit(gobject/gsignal.c)

gtk_init関数を呼んだ後、ごそごそとウィジェットを作成していきます。その中でGTKを理解するために大事であろう要素があったので紹介します。gtk_container_add関数の実装です。

gtk_container_add (GtkContainer *container,
                   GtkWidget    *widget)
{
  ...
  g_signal_emit (container, container_signals[ADD], 0, widget);
}

というわけで、子ウィジットのリストに引数のウィジットを追加するみたいなコードが書いてあるわけではなくシグナルが送られています。何でこんなことがされているかというとGtkContainer*が指すものはサンプルコードで言うとGtkWindowです。指しているものによってaddと言われても振る舞いが異なるというのはオブジェクト指向の基本でそれを実現するためにシグナルが用いられているようです。

次に呼び出される関数がどうやって登録されているかを見てみましょう。

container_signals[ADD] =
  g_signal_new (I_("add"),
		 G_OBJECT_CLASS_TYPE (object_class),
		 G_SIGNAL_RUN_FIRST,
		 G_STRUCT_OFFSET (GtkContainerClass, add),
		 NULL, NULL,
		 _gtk_marshal_VOID__OBJECT,
		 G_TYPE_NONE, 1,
		 GTK_TYPE_WIDGET);

雰囲気から察するにaddシグナルが送られるとGtkContainerClassのaddに設定されている関数が呼び出されそうです。というわけでGtkBinを眺めてみる*2

gtk_bin_class_init (GtkBinClass *class)
{
  GtkContainerClass *container_class;

  container_class = (GtkContainerClass*) class;

  container_class->add = gtk_bin_add;

となっており、gtk_bin_add関数が設定されています。

gtk_widget_show(gtk/gtkwidget.c)

GTKのウィジェットを表示する手順は以下のようになります。

  1. *_new関数を使ってオブジェクトを作成
  2. コンテナオブジェクトに設定(親ウィジェットの設定)
  3. gtk_widget_show関数を呼ぶ

で、最後のgtk_widget_show関数ですが以下のようになっています。

gtk_widget_show (GtkWidget *widget)
{
  g_return_if_fail (GTK_IS_WIDGET (widget));

  if (!GTK_WIDGET_VISIBLE (widget))
    {
      g_object_ref (widget);
      if (!GTK_WIDGET_TOPLEVEL (widget))
        gtk_widget_queue_resize (widget);
      g_signal_emit (widget, widget_signals[SHOW], 0);
      g_object_notify (G_OBJECT (widget), "visible");
      g_object_unref (widget);
    }
}

先ほど説明したg_signal_emit関数が使われています。今回サンプルコードで使ったウィジェットではGtkWindowのみgtk_window_show関数、他はgtk_widget_real_show関数が呼ばれるようになっていました。gtk_widget_real_show関数を見てみましょう。

gtk_widget_real_show (GtkWidget *widget)
{
  if (!GTK_WIDGET_VISIBLE (widget))
    {
      GTK_WIDGET_SET_FLAGS (widget, GTK_VISIBLE);

      if (widget->parent &&
          GTK_WIDGET_MAPPED (widget->parent) &&
          GTK_WIDGET_CHILD_VISIBLE (widget) &&
          !GTK_WIDGET_MAPPED (widget))
        gtk_widget_map (widget);
    }
}

何となく条件は成り立たない気がする(親をshowする前に子をshowしているため)のでgtk_window_show関数の方に行きましょう。

gtk_window_show(gtk/gtkwindow.c)

ちょっと長めなので順を追って見ていきます。まず、まだREALIZEDされていないのでif(need_resize)の中に入ることになります。

gtk_window_compute_configure_request関数ではウインドウのサイズや位置を計算しています。面倒くさいので詳細は飛ばします。

次にgtk_widget_size_allocate関数が呼ばれていますがこの関数は初めにshowされたときはあまり重要なことを行っていないようなので飛ばします。

次に、REALIZEされていないのでgtk_widget_realize関数が呼び出されます。この関数ではrealizeシグナルを送ることでオブジェクトのクラスに設定されている関数、GtkWindowの場合はgtk_window_realize関数を呼び出しています。gtk_window_realize関数ではgdk_window_new関数が呼ばれ、さらに内部でXの関数が呼び出されることで画面に表示されるウインドウが作られています。

gtk_window_show関数に戻って、gtk_decorated_window_calculate_frame_size関数ではウインドウの装飾(最大化ボタンなど)のあるなしに応じていろいろやっています。例によって飛ばします。

以上でif(need_resize)のブロックは終了です。次にgtk_widget_map関数が呼び出されています。mapシグナルが送られ、gtk_window_map関数が実行されます。gtk_window_map関数ではまず子ウィジェットに対してgtk_widget_map関数を実行し、ごそごそ設定を行った後にgdk_window_show関数を呼んでウインドウの表示を行っています。

GtkWindowの子ウィジェットに対して実行されたgtk_widget_map関数は以下のような呼び出しによって末端のGtkEntryまで届きます。

GtkHBox: gtk_container_mapを実行
  gtk_container_forall
    GtkContainerClass.forallとして設定されているgtk_box_forallを実行
      GtkBoxの子要素に対してgtk_widget_mapを実行
        GtkEntryの場合、gtk_entry_realizeを実行

gtk_entry_realize関数ではgdk_window_new関数を使ってエディットボックスを作成しています。紛らわしいので説明しておくとGtkWindowは一般的に言うウインドウです。一方、GdkWindowは一般的に言うウインドウの他、ボタンやエディットボックスが描画される領域です。どうやらGdkEntryでは2つGdkWindowを作っているようですがここで重要なのは以下の部分です。これが何を表しているかは後ほどわかります。

gdk_window_set_user_data (entry->text_area, entry);

gtk_window_show関数に戻ってくるとgtk_window_move_focus関数を呼び出してウィジェットにフォーカスが当たるようにしています。サンプルコードではエディットボックスにフォーカスが当たるわけですが呼び出しは以下のようになります。

gtk_window_move_focus
  gtk_widget_child_focus(GtkWindow)
    gtk_window_focus(focusシグナル)
      gtk_widget_child_focus(GtkHBox)
        gtk_container_focus(focusシグナル)
          gtk_container_focus_move
             gtk_widget_child_focus(GtkEntry)
               gtk_widget_real_focus(focusシグナル)
                 gtk_widget_grab_focus
                   gtk_entry_grab_focus(grab_focusシグナル)
                     gtk_widget_real_grab_focus
                       _gtk_window_internal_set_focus
                         gtk_window_real_set_focus(set_focusシグナル)

gtk_main(gtk/gtkmain.c)

ウインドウと表示するウィジェットを設定後、gtk_main関数を呼び出すことでGTKのイベント処理が開始されます。gtk_main関数を呼び出したことによるコールツリーです。処理の大部分はGLibのgmain.cが行うようです。なお、G_THREADS_ENABLEDは普通有効なのですがめんどくさいので読み飛ばします:-)

g_main_loop_new
  g_main_context_default
    g_main_context_new
g_main_loop_run
  g_main_context_iterate
    g_main_context_prepare
    g_main_context_query
    g_main_context_poll
    g_main_context_check
    g_main_context_dispatch
      g_main_dispatch

処理は大雑把に言うと、

  1. GMainContext.source_listを調べ
  2. 準備ができていたらGMainContext.pending_dispatchesに詰め
  3. 各処理関数にディスパッチ

ということを行っています。 次に調べないといけないのは、source_listがいつ設定されているかです。

_gdk_events_init(gdk/x11/gdkevents-x11.c)

source_listに要素を追加している関数はg_source_list_add関数です。ただこの関数はstaticで外部から呼ばれる可能性のある関数はg_source_attach関数です。 今回注目対象としているキーボード入力を処理するsourceは_gdk_events_init関数(gtk_init関数の呼び出し中で呼び出されています)で設定されているものなようなので詳しく見てみましょう。

まずgdk_display_source_new関数を呼び出してGdkDisplaySourceオブジェクトを作成しています。その際、source_funcsとして以下の関数が設定されています。source_funcsは前述の

  • g_main_context_prepare
  • g_main_context_check
  • g_main_dispatch

時にそれぞれ呼び出されます。

static GSourceFuncs event_funcs = {
  gdk_event_prepare,
  gdk_event_check,
  gdk_event_dispatch,
  NULL
};

次にディスプレイのコネクションナンバーをポーリング対象として登録しています。Xの知識はあまりないのですが、ここら辺を参照するとXクライアントはXサーバからの情報をネットワーク経由で受け取っているようです。というわけでそのディスクリプタをポーリング対象に加えているわけですね、

gdk_event_prepare(gdk/x11/gdkevents-x11.c)

それではまずgdk_event_prepare関数を見てみましょう。コアは以下の部分です。

retval = (_gdk_event_queue_find_first (display) != NULL || 
          gdk_check_xpending (display));

_gdk_event_queue_find_first関数はキューにあるイベントからGDK_EVENT_PENDINGフラグが立っていないものを探しています。この関数はgdk/gdkevents.cに書かれておりgenericなものです。

イベントが見つからなかった場合、gdk_check_xpending関数が呼ばれています。こちらはX特有のことをしており、単純にXlibのXPending関数を呼び出しています。

gdk_event_check関数はgdk_event_prepare関数とほぼ同じなので飛ばします。

gdk_event_dispatch(gdk/x11/gdkevents-x11.c)

gdk_event_dispatch関数ではまず_gdk_events_queue関数を呼んだ後、_gdk_event_funcに設定されている関数を呼び出しています。ちなみに、_gdk_event_unqueue関数はX特有のことをしていないのでgdk/gdkevents.cに書かれています。

_gdk_events_queue関数に進みましょう。Xのイベントを取り出してGDKのイベントの作成を行っているようです。GdkEventの定義はgdk/gdkevents.hに書かれています。GdkEventは各種イベントパラメータのstructをunionでまとめたもののようです。これはXのイベント表現とほぼ等価なようです。

変換の詳細はgdk_event_translate関数に続く。淡々と変換が行われています。KeyPressイベントに対してどのような関数が呼び出されるか示します。

gdk_event_translate [gdk/x11/gdkevents-x11.c]
  translate_key_event [gdk/x11/gdkevents-x11.c]
    _gdk_x11_get_group_for_state [gdk/x11/gdkkey-x11.c]
    gdk_keymap_translate_keyboard_state [gdk/x11/gdkkey-x11.c]
    _gdk_keymap_add_virtual_modifiers [gdk/x11/gdkkey-x11.c]
    _gdk_keymap_key_is_modifiers [gdk/x11/gdkkey-x11.c]

gtk_main_do_event(gtk/gtkmain.c)

_gdk_event_funcはgtk_init関数の呼び出し中でgtk_main_do_event関数に設定されておりGTKのレイヤーにイベント処理が移っています。

gtk_main_do_event関数ではまずgtk_get_event_widget関数を呼び出してイベントが起きたウィジェットを取得しています。gtk_get_event_widget関数は以下のようになっています。

GtkWidget*
gtk_get_event_widget (GdkEvent *event)
{
  GtkWidget *widget;

  widget = NULL;
  if (event && event->any.window &&
      (event->type == GDK_DESTROY || !GDK_WINDOW_DESTROYED (event->any.window)))
    gdk_window_get_user_data (event->any.window, (void**) &widget);

  return widget;
}

というわけで、イベントが起きたGdkWindowのuser_dataにイベントが起きたウィジェットが格納されています。

その後、grabされている場合はイベントウィジェットを切り替えるということが行われてますが無視します。で、イベントのタイプはGDK_KEY_PRESSなのでgtk_propagate_event関数に続きます。

gtk_propagate_event関数ではKEYイベントの場合、特別処理が行われています。gtk_widget_event関数が実行されると思って問題ないでしょう。

gtk_entry_key_press(gtk/gtkentry.c)

gtk_widget_event関数の実際の処理はgtk_widget_event_internal関数で行われています。gtk_widget_event_internal関数ではまずeventシグナルを送り反応がないならイベント別のシグナルを送るということを行っています。GtkEntryの場合、gtk_entry_key_press関数が呼び出されます。

gtk_entry_key_press関数ではまずgtk_im_context_filter_keypress関数が呼び出されています。なお、im_contextは以下のように初期化されています。

gtk_entry_init (GtkEntry *entry)
{
  ...
  entry->im_context = gtk_im_multicontext_new ();
  
  g_signal_connect (entry->im_context, "commit",
		    G_CALLBACK (gtk_entry_commit_cb), entry);

で、gtk_im_context_filter_keypress関数。

gboolean
gtk_im_context_filter_keypress (GtkIMContext *context,
				GdkEventKey  *key)
{
  GtkIMContextClass *klass;
  
  g_return_val_if_fail (GTK_IS_IM_CONTEXT (context), FALSE);
  g_return_val_if_fail (key != NULL, FALSE);

  klass = GTK_IM_CONTEXT_GET_CLASS (context);
  return klass->filter_keypress (context, key);
}

GtkIMContext*が指す実際のオブジェクト(GtkIMMulticontext)のfilter_keypressを呼び出しています。というわけで呼ばれるgtk_im_multicontext_filter_keypress関数。

static gboolean
gtk_im_multicontext_filter_keypress (GtkIMContext *context,
				     GdkEventKey  *event)
{
  GtkIMMulticontext *multicontext = GTK_IM_MULTICONTEXT (context);
  GtkIMContext *slave = gtk_im_multicontext_get_slave (multicontext);

  if (slave)
    return gtk_im_context_filter_keypress (slave, event);
  else
    return FALSE;
}

gtk_im_multicontext_get_slave関数は現在のロケールやインストールされているimmoduleから適切なGtkIMContextの実装を決定しているようですが、デフォルトのGtkIMContextSimpleが作られるということにします。

というわけでgtk_im_context_simple_filter_keypress関数が呼ばれます。simpleと言っておきながらあまりsimpleじゃありませんがどうやら特殊入力を処理しているようなのでバッサリ無視して、最後に呼んでいるno_sequence_matches関数に進みます。no_sequence_matches関数を眺めると普通の入力の場合はgtk_im_context_simple_commit_char関数に進みそうです。というわけでgtk_im_context_simple_commit_char関数に進むとcommitシグナルが送られ、gtk_entry_commit_cb関数が呼び出されます。

gtk_entry_commit_cb関数が呼び出されると中心はGtkEntryに戻ります。gtk_entry_enter_text関数が呼び出され、gtk_editable_insert_text → gtk_entry_insert_text → gtk_entry_real_insert_textという呼び出しによって入力したキーボードの内容が格納されます。

おわりに

今回はキーボード入力に対してGTKがどのように処理を行っているかを見てきました。わかったこととしては以下のことがあります。

  • GTKはシグナルなどにより実行時に呼び出される関数を変えている
  • GDKのウインドウ(プラットフォームとのインターフェース部分)にウィジェットの情報を入れ、イベント時に利用している

ん?そういえばマウスでクリックしてフォーカスを切り替えるとこは読んでない、けどまあここまで読み通した知識があれば得に問題はないでしょう。それではみなさんもよいコードリーディングを。


*1 実際には初めて呼ばれたときに登録するスタイルなのでひとつです
*2 GtkWindowがaddを処理するのではなく親クラスのGtkBinが処理しています

トップ   編集 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2008-04-14 (月) 01:31:32 (5856d)