9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-10-20

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

インストール

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

sudo apt install libgtk-4-dev

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

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

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

#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」のデモを動かしてみるのだが、どの動画を選んでも再生されません。

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

sudo apt install libgtk-4-media-gstreamer

Pythonで使う

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

# 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へ移行する際の注意点は、マニュアルに書かれています。

…、読んでみると、結構変更点が大きいですね…。
後でしっかり把握しようと思いますが、軽く読んで、目についた変更点を挙げておきます。
(以下、面倒くさいので、サンプルコードは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_hexpandgtk_widget_set_halignを呼び出します。

# 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イベント、主にマウスやキーボードなどの入力など、低レイヤーのイベントにより発生するシグナルが無くなったようです。
代わりに、各イベントに沿ったイベントコントローラを使用します

# 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を呼んで、別途描画メソッドを登録します。

それ以外のウィジェットは、…、マニュアルには、「(従来のやり方を踏襲するのであれば)GtkWidgetClasssnapshotメソッドをオーバーライドしてgtk_snapshot_append_cairoを呼び出せ」と書かれているようなので(英語苦手)、やってみました。

# 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が無くなりました。

	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シグナルをハンドリングして処理しなければなりません。

	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でスコープとやらを使うようです。

	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.Buttonclickedシグナルにハンドラを設定すると、2度目の呼び出しでSegmentationFaultが発生してしまいます。

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

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

後日、使い方がわかりました。

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

でも、上の様子じゃこれもPythonで使えなさそうだなぁ…、と思ってやっぱり悩みましたが、どうやらGtk.Templateリンクという便利デコレータがあるようです。

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

'22/7/20 追記

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

メニュー

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

# 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ファイルを使う方が良いでしょう。

# 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のリファレンス、「Python GTK+3 TutorialのApplicationの項gtk4-demoの「Menu」サンプルを参考にすればいいでしょう。

ちなみに、GtkMenuBarも無くなりました。代わりに、GtkPopoverMenuBarというものが存在します。
でも、GTK4的(何となくGNOME的?)には、マニュアルの「Getting Started with GTK」に書かれているように「そんなもの使わずに、GtkHeaderBarを使おう!」というニュアンスを感じますが…。

破棄

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

GtkWindowにのみ、gtk_window_destroyという関数が用意されています。
それ以外の非toplevelなウィジェットは、マニュアルによれば、gtk_container_removeを使えだそうです。

…、いや、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や、それに関連するGtkTreeModelGtkCellLayoutなどがdeprecatedなんだから、同じようにそれらを使っているGtkComboBoxもdeprecatedなんですね。

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

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?