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
の使い方は、簡単に言えば、
-
GtkApplication
のオブジェクトを作成。 -
activate
シグナルを受け取って、ウィンドウを作成。
ウィンドウのapplication
プロパティに、GtkApplication
オブジェクトを設定。 -
g_application_run
メソッドを読んで、イベントループを回す。 -
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
を呼び出します。
# 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
を呼んで、別途描画メソッドを登録します。
それ以外のウィジェットは、…、マニュアルには、「(従来のやり方を踏襲するのであれば)GtkWidgetClass
のsnapshot
メソッドをオーバーライドして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.Button
のclicked
シグナルにハンドラを設定すると、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
や、それに関連するGtkTreeModel
やGtkCellLayout
などがdeprecatedなんだから、同じようにそれらを使っているGtkComboBox
もdeprecatedなんですね。
代わりに、GtkDropDown
を使いましょう。