GTK+3プログラマがubuntu21.10でGTK4を触ってみた

ubuntu21.10もリリースされたので、GTK4を触ってみたいと思います。
…いや、21.04で既にパッケージが用意されていたのは知ってましたけど、ほったらかしにしてしまいまして、ちょっと出遅れた感がしますが、まぁ気にしない。

## インストール

~~デスクトップ版であれば動作に必要なパッケージはインストールされていますが~~(さも当然のように書いたけど、確認をしていないので、取り消します)、ビルドに必要な開発パッケージは自分でインストールしなければなりません。

```
sudo apt install libgtk-4-dev
```

パッケージ名は、「3」から「4」に変わっただけですね。

## 簡単なコードを書いてみる。

とりあえず、簡単なコードを書いてみましょう。

```c
#include <gtk/gtk.h>

static void on_activate(GApplication *app, gpointer user_data)
{
	GtkWidget *window;

#if	GTK_MAJOR_VERSION >= 4
	window = gtk_window_new();
#else
	window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
#endif
	gtk_window_set_application(GTK_WINDOW(window), GTK_APPLICATION(app));
	gtk_widget_show(window);
}

int main(int argc, char *argv[])
{
	GtkApplication *app;

	app = gtk_application_new(NULL, G_APPLICATION_FLAGS_NONE);
	g_signal_connect(G_OBJECT(app), "activate", G_CALLBACK(on_activate), NULL);
	return g_application_run(G_APPLICATION(app), argc, argv);
}
```

もしかすると人によっては見慣れないコードが出てきたかもしれません。
(とはいえ、GTK+3でも1行以外はそのまま通るコードなんですが)
入門のソースコードでよく描かれていた`gtk_main`ですが、GTK4ではとうとう無くなりました。
代わりに、`GtkApplication`のオブジェクトを作成し、`run`メソッドを呼ぶようになります。

## ビルドする(pkg-config)

次に、ビルドしてみます。
とりあえず手っ取り早く、`pkg-config`を使ってコンパイルオプションを指定します。

```
gcc main.c `pkg-config --cflags --libs gtk4`
```

`pkg-config`のパッケージ名が、「`gtk+-3.0`」から「`gtk4`」に変更されているので、ちょっと注意です。

## ドキュメントを読む

ubuntuのパッケージには、ドキュメントも存在します。

```
sudo apt install libgtk-4-doc
```

これも、パッケージ名が「3」から「4」に変わっただけ…と思いきや、注意点が一つ。
GTK+3の時には、パッケージは「`/usr/share/gtk-doc/`」以下にインストールされましたが、GTK4は「`/usr/share/doc/libgtk-4-doc`」以下にインストールされます。
まぁ、「`/usr/share/doc`」にドキュメントがインストールされるのがubuntuでは一般的なので、「一般的になった」とも言えますが。

## デモを動かす

GTK+3でもありましたが、GTK4のデモプログラムもあります。

```
sudo apt install gtk-4-examples
gtk4-demo
```

## 動画を扱う

どうも動画周りのプログラムが動作しない。エラーは起きないのだけど、どうも動画フォーマットがサポートされていないかのごとく、無効な動画として処理されてしまいます。

例えば、上記の`gtk4-demo`の中の「Video Player」のデモを動かしてみるのだが、どの動画を選んでも再生されません。

とりあえずググってみても、(例えば[ここ](https://palepoli.skr.jp/wp/2021/05/05/gtkvideo-and-gtkpicture/)とか)特に特別な事もせずにmp4ファイルを再生しているので、もしかするとubuntuのパッケージを追加でインストールしなければならないのかなぁ、と思ってsearchしてみると、「`libgtk-4-media-gstreamer`」というパッケージがあったので、これをインストールしてみると、再生されるようになりました。

```
sudo apt install libgtk-4-media-gstreamer
```

## Pythonで使う

Pythonで使う場合は、`Gtk`モジュールをインポートする前に、「`gi.require_version('Gtk', '4.0')`」を実行し、GTK4のモジュールを読むようにします。

```python
# coding: utf-8

import sys
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk

def on_activate(app):
	window = Gtk.Window(application=app)
	window.set_child(Gtk.Label(label='Hello, world!'))
	window.show()

if __name__ == '__main__':
	app = Gtk.Application()
	app.connect('activate', on_activate)
	app.run(sys.argv)
```

## GTK+3からGTK4へ…

さて、GTK+3からGTK4へ移行する際の注意点は、マニュアルに書かれています。

* [Migrating from GTK 3.x to GTK 4](https://docs.gtk.org/gtk4/migrating-3to4.html)

…、読んでみると、結構変更点が大きいですね…。
後でしっかり把握しようと思いますが、軽く読んで、目についた変更点を挙げておきます。
(以下、面倒くさいので、サンプルコードはPythonで書きます)

### イベントループ

先に話したとおりなんですが、もう少し。

昔からGTK+は、`gtk_init`を呼んで、`gtk_main`でイベントループを回す、というのがプログラムの流れでしたが、これが`GtkApplication`クラスに置き換わります。
`GtkApplication`の使い方は、簡単に言えば、

1. `GtkApplication`のオブジェクトを作成。
2. `activate`シグナルを受け取って、ウィンドウを作成。
   ウィンドウの`application`プロパティに、`GtkApplication`オブジェクトを設定。
3. `g_application_run`メソッドを読んで、イベントループを回す。
4. `application`プロパティが設定されたウィンドウが全て破棄されれば、イベントループが終了する。

といった流れになります。
何か、Qtみたいですね。

### `GtkContainer`/`GtkBin`

`GtkContainer`/`GtkBin`クラスがなくなったようです。
どちらも抽象クラスで、子ウィジェットを持つウィジェットは`GtkContainer`クラスを継承し、その中でも一つのみ子ウィジェットを持つものは`GtkBin`クラスを継承していました。

よくウィンドウを作成した後に、子ウィジェットを追加するために`gtk_container_add`を呼んでいたと思いますが、当然これも無くなり、代わりに`gtk_window_set_child`を呼び出します。

### `GtkBox`の子ウィジェットの追加

`GtkBox`で子ウィジェットを追加する`gtk_box_pack_start`/`gtk_box_pack_end`メソッドが無くなったようです。
代わりに、`gtk_box_append`/`gtk_box_prepend`を使用します。

ただし、これらには以前のようなexpand(子ウィジェットの配置領域を拡大する)やfill(配置領域にあわせて子ウィジェットのサイズを変更する)の指定がありません。
指定をするには、**子ウィジェットに対して**`gtk_widget_set_hexpand`や`gtk_widget_set_halign`を呼び出します。

```python
# coding: utf-8

import sys
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, Gdk

def on_activate(app):
	provider = Gtk.CssProvider()
	# 子ウィジェットのサイズがわかりやすいように、背景に色を付けます。
	n = provider.load_from_data(b'''
.label_red {
	background: #ff0000;
}
.label_green {
	background: #00ff00;
}
.label_blue {
	background: #0000ff;
}
''')
	Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)

	window = Gtk.Window(application=app, default_width=400, default_height=300)

	box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
	window.set_child(box)

	label = Gtk.Label(label='expand/fill')
	label.set_halign(Gtk.Align.FILL)
	label.set_hexpand(True)
	label.get_style_context().add_class('label_red')
	box.append(label)

	label = Gtk.Label(label='expand')
	label.set_halign(Gtk.Align.CENTER)
	label.set_hexpand(True)
	label.get_style_context().add_class('label_green')
	box.append(label)

	label = Gtk.Label(label='no expand')
	label.get_style_context().add_class('label_blue')
	box.append(label)

	window.show()

if __name__ == '__main__':
	app = Gtk.Application()
	app.connect('activate', on_activate)
	app.run(sys.argv)

```

### GDKイベントシグナル

GDKイベント、主にマウスやキーボードなどの入力など、低レイヤーのイベントにより発生するシグナルが無くなったようです。
代わりに、[各イベントに沿ったイベントコントローラを使用します](https://docs.gtk.org/gtk4/migrating-3to4.html#stop-using-gtkwidget-event-signals)

```python
# coding: utf-8

import sys
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk

class App(Gtk.Application):
	def __init__(self, **kwargs):
		super().__init__(**kwargs)
		self.points = []

	def do_activate(self):
		window = Gtk.Window(application=app, default_width=400, default_height=300)

		drawing_area = Gtk.DrawingArea()
		drawing_area.set_draw_func(self.on_draw)
		window.set_child(drawing_area)

		event_controller = Gtk.GestureClick()
		event_controller.set_button(1)
		event_controller.connect('pressed', self.on_pressed_primary, drawing_area)
		drawing_area.add_controller(event_controller)

		event_controller = Gtk.GestureClick()
		event_controller.set_button(3)
		event_controller.connect('pressed', self.on_pressed_secondary, drawing_area)
		drawing_area.add_controller(event_controller)

		window.show()

	def on_draw(self, drawing_area, cr, width, height):
		for x, y in self.points:
			cr.set_source_rgb(0, 1, 0)
			cr.rectangle(x, y, 8, 8)
			cr.fill()

	def on_pressed_primary(self, gesture, n_press, x, y, drawing_area):
		print('on_pressed_primary(n_press = %s).' % repr(n_press), flush=True)
		self.points += [(x, y)]
		drawing_area.queue_draw()

	def on_pressed_secondary(self, gesture, n_press, x, y, drawing_area):
		print('on_pressed_secondary(n_press = %s).' % repr(n_press), flush=True)
		self.points = []
		drawing_area.queue_draw()

if __name__ == '__main__':
	app = App()
	app.run(sys.argv)

```

### drawシグナル

drawシグナルも無くなったようです。
`GtkDrawingArea`ウィジェットで描画するには、上記のように`gtk_drawing_area_set_draw_func`を呼んで、別途描画メソッドを登録します。

それ以外のウィジェットは、…、[マニュアル](https://docs.gtk.org/gtk4/migrating-3to4.html#adapt-to-drawing-model-changes)には、「(従来のやり方を踏襲するのであれば)`GtkWidgetClass`の`snapshot`メソッドをオーバーライドして`gtk_snapshot_append_cairo`を呼び出せ」と書かれているようなので(英語苦手)、やってみました。

```python
# coding: utf-8

import sys
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, Graphene

class MyWidget(Gtk.Widget):
	def do_snapshot(self, snapshot):
		width = self.get_allocated_width()
		height = self.get_allocated_height()
		cr = snapshot.append_cairo(Graphene.Rect().init(0, 0, width, height))
		cr.set_source_rgb(0, 0, 1)
		cr.rectangle(width / 4, height / 4, width / 2, height / 2)
		cr.fill()

def on_activate(app):
	window = Gtk.Window(application=app, default_width=400, default_height=300)

	child = MyWidget()
	window.set_child(child)

	window.show()

if __name__ == '__main__':
	app = Gtk.Application()
	app.connect('activate', on_activate)
	app.run(sys.argv)

```

Grapheneとやらがよくわからんのだけど、一応できているっぽいです。 

## 最後に

ホントは、ビルドできるところまで軽く書いて終わり、というつもりだったのですが、3からの違いをちょっと付け足そうと思って書いたら長くなってしまいました。それでは。

## '22/7/13 追記

そろそろ自分が使っているGTK+3のプログラムをGTK4に置き換えようかな、と思ってちょっとだけやってみましたら、他にも大きな変更点がありました。
ちょっと付け足そうかと思います。

### `gtk_dialog_run`

`gtk_dialog_run`が無くなりました。

```python
	dialog = Gtk.MessageDialog(modal=True, destroy_with_parent=True, message_type=Gtk.MessageType.INFO,
			buttons=Gtk.ButtonsType.OK, text='Hello, world!')
	dialog.run()
	dialog.destroy()
```

GTK4では、上記の書き方はエラーになります。
`response`シグナルをハンドリングして処理しなければなりません。

```python
	dialog = Gtk.MessageDialog(modal=True, destroy_with_parent=True, message_type=Gtk.MessageType.INFO,
			buttons=Gtk.ButtonsType.OK, text='Hello, world!')
	def _on_response(dlg, response):
		dlg.destroy()
	dialog.connect('response', _on_response)
	dialog.show()
```

### `GtkBuilder`のシグナル処理

`GtkBuilder`のシグナル処理も結構変わったようです。

`gtk_builder_connect_signals`が無くなったようです。
C言語であれば、gmoduleを使えば勝手にシグナルハンドラが設定されますが、もしくは`gtk_builder_connect_signals`のようにプログラムでシグナルハンドラを設定するには、`gtk_builder_set_scope`でスコープとやらを使うようです。

```c
	GtkBuilder *builder;
	GtkBuilderScope *scope;

	builder = gtk_builder_new();
	scope = gtk_builder_cscope_new();
	gtk_builder_cscope_add_callback_symbol(GTK_BUILDER_CSCOPE(scope), "on_clicked", G_CALLBACK(on_clicked));
	gtk_builder_set_scope(builder, scope);
	/* set_scopeしてからuiファイルを読み込まないと、認識しない。*/
	gtk_builder_add_from_file(builder, "./main.ui", NULL);
```

さて、Pythonの場合ですが…。

gmoduleなんてPythonじゃ使えませんし、`gtk_builder_connect_signals`が無くなったとなれば、スコープとやらを使うしかないのですが、上記のCのコードをPythonに置き換えて試してみましたが、`Gtk.Button`の`clicked`シグナルにハンドラを設定すると、2度目の呼び出しでSegmentationFaultが発生してしまいます。

どうすればいいんだ…。と悩んでいましたが、どうやら`Gtk.Builder`のコンストラクタの引数に、以前の`Gtk.Builder.connect_signals`のようにオブジェクトかマップを指定すればいいようです。

```python
	builder = Gtk.Builder({ 'on_clicked': on_clicked })
	builder.add_from_file('./main.ui')
```

→ [後日、使い方がわかりました。](https://qiita.com/katsuko0303/items/9c7bd0813d28473d9771)

なお、GTKには「template」という機能があります。
`GtkBuilder`で使用するuiファイルを、自分で定義したウィジェットに適用させるものです。
まぁ詳細は置いといて、その際にGTK+3では`gtk_widget_class_set_connect_func`を言う関数でシグナルハンドラを設定していたのですが、これも無くなり、代わりに`gtk_widget_class_set_template_scope`という`GtkBuilderScope`を使う関数ができました。

でも、上の様子じゃこれもPythonで使えなさそうだなぁ…、と思ってやっぱり悩みましたが、どうやら`Gtk.Template`[リンク](https://pygobject.readthedocs.io/en/latest/guide/gtk_template.html)という便利デコレータがあるようです。

ちなみに、GTK+3の頃からあったようです。
…、知らなかったよ。似たようなデコレータ作っちまった…。

## '22/7/20 追記

いい加減大きなのは出尽くしただろう、と思ったら、まだまだありました。

### メニュー

`GtkMenu`が無くなりました。代わりに、`GtkPopoverMenu`を使用します。
…、`GtkPopoverMenu`の存在は前から知っていたし、そうなると`GtkMenu`は無くなるんだろうなぁ、とは思ってましたけどね。

```python
# coding: utf-8

import sys
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, Gio, Gdk

def on_hoge(action, param):
	print('hoge.')

def on_pressed(controller, n_press, x, y, menu):
	rect = Gdk.Rectangle()
	rect.x, rect.y, rect.width, rect.height = x, y, 1, 1
	menu.set_pointing_to(rect)
	menu.popup()

def on_destroy(widget, menu):
	menu.unparent()

def on_activate(app):
	window = Gtk.ApplicationWindow(application=app)

	menu_model = Gio.Menu()
	menu_model.append('Hoge', 'win.hoge')
	menu = Gtk.PopoverMenu(menu_model=menu_model, has_arrow=0)
	menu.set_parent(window)

	controller = Gtk.GestureClick(button=Gdk.BUTTON_SECONDARY)
	controller.connect('pressed', on_pressed, menu)
	window.add_controller(controller)

	action = Gio.SimpleAction.new('hoge', None)
	action.connect('activate', on_hoge)
	window.add_action(action)

	window.connect('destroy', on_destroy, menu)

	window.show()

if __name__ == '__main__':
	app = Gtk.Application()
	app.connect('activate', on_activate)
	sys.exit(app.run(sys.argv))

```

…と書くと、このように項目が一つしかなければまだいいけど、構成が複雑になると分かりづらいので、uiファイルを使う方が良いでしょう。

```python
# coding: utf-8

import sys
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, Gdk, Gio

menu = None

def on_hoge(action, param):
	print('hoge.')

def on_pressed(controller, n_press, x, y):
	rect = Gdk.Rectangle()
	rect.x, rect.y, rect.width, rect.height = x, y, 1, 1
	menu.set_pointing_to(rect)
	menu.popup()

def on_activate(app):
	builder = Gtk.Builder(globals())
	builder.add_from_string('''
<interface>
	<object class="GtkApplicationWindow" id="window">
		<child>
			<object class="GtkGestureClick" id="gesture_click">
				<property name="button">3</property>
				<signal name="pressed" handler="on_pressed" />
			</object>
		</child>
		<child>
			<object class="GtkPopoverMenu" id="menu">
				<property name="has-arrow">false</property>
				<property name="menu-model">menu_model</property>
			</object>
		</child>
	</object>

	<menu id="menu_model">
		<item>
			<attribute name="label">Hoge</attribute>
			<attribute name="action">win.hoge</attribute>
		</item>
	</menu>
</interface>
''')

	window = builder.get_object('window')
	global menu
	menu = builder.get_object('menu')

	action = Gio.SimpleAction.new('hoge', None)
	action.connect('activate', on_hoge)
	window.add_action(action)

	window.set_application(app)
	window.show()

if __name__ == '__main__':
	app = Gtk.Application()
	app.connect('activate', on_activate)
	sys.exit(app.run(sys.argv))

```

メニューの書き方は、[`GtkPopoverMenu`のリファレンス](https://docs.gtk.org/gtk4/class.PopoverMenu.html#menu-models)、「[Python GTK+3 TutorialのApplicationの項](https://python-gtk-3-tutorial.readthedocs.io/en/latest/application.html)、`gtk4-demo`の「Menu」サンプルを参考にすればいいでしょう。

ちなみに、`GtkMenuBar`も無くなりました。代わりに、`GtkPopoverMenuBar`というものが存在します。
でも、GTK4的(何となくGNOME的？)には、[マニュアルの「Getting Started with GTK」](https://docs.gtk.org/gtk4/getting_started.html)に書かれているように「そんなもの使わずに、`GtkHeaderBar`を使おう！」というニュアンスを感じますが…。

### 破棄

で、上記の話を書いていて気付いたのですが、`gtk_widget_destroy`が無くなりました。

`GtkWindow`にのみ、`gtk_window_destroy`という関数が用意されています。
それ以外の非toplevelなウィジェットは、[マニュアルによれば、`gtk_container_remove`を使え](https://docs.gtk.org/gtk4/migrating-3to4.html#reduce-the-use-of-gtk_widget_destroy)だそうです。

…、いや、`GtkContainer`無くなったじゃん。
要は、親ウィジェットから切り離すことによって、破棄しろということでしょう。(C言語であれば、`g_object_unref`で参照も破棄すること)
例えば、`GtkBox`であれば、`gtk_box_remove`を使います。


## '22/11/10 追記

別にGTK4じゃ使えないというわけではないのですが、何となくこの続きに書いた方がいい気がしたので…

GTK4のマニュアルを見ていたら、「deprecated 4.10」の文字がそこら中に…。
主に見つけたものを、書いておきます。(全部、よく使っているよ…)

### `GtkTreeView` / `GtkIconView`

もちろん、`GtkTreeModel`などの関連クラスもdeprecatedです。
代わりに、`GtkColumnView` / `GtkGridView` などを使いましょう。
(まぁ、GTK3の頃から `GListModel` とか出てきていた時点で、無くす気なんだろうなぁ、と想定してましたが）

### `GtkFileChooserDialog` / `GtkFontChooserDialog` / `GtkColorChooserDialog`

代わりに、`GtkFileDialog` / `GtkFontDialog` / `GtkColorDialog` を使いましょう。(ただし、使えるのは4.10からです)

### `GtkComboBox`

あぁ、`GtkTreeView`や、それに関連する`GtkTreeModel`や`GtkCellLayout`などがdeprecatedなんだから、同じようにそれらを使っている`GtkComboBox`もdeprecatedなんですね。

代わりに、`GtkDropDown`を使いましょう。