GTKのリソース管理(できればPythonで)


最近のGTKでは、UI定義ファイルなど、プログラムのソースコード以外のデータファイルを扱うことがあります。
従来からのunixでは、`/usr/share`以下のディレクトリに置くのが通例なのですが、Windowsなどにはそのようなディレクトリはありませんし、プラットフォームでファイルの場所が違っているとプログラムで扱いにくい点があります。

そこで、そのようなデータファイルを実行ファイルに埋め込んでしまおう、というのがGTKのリソースです。(実際にはGioなのですが)

なお、以下の話はGTK4を想定しています。
(リソースはGTK3でも使用できますが、スタイルシートなどはGTK3では使い方が違っていた気がします)

## 主なリソースデータ

どのようなデータをリソースとして扱えるかを挙げてみます。

### UI定義ファイル

`GtkBuilder`で扱うUI定義ファイルです。
`gtk_builder_new_from_resource`や`gtk_builder_add_from_resource`でアクセスします。

### スタイルシート

昨今のGTKは、見栄えをスタイルシートで指定します。
そのスタイルシートを定義したファイルも、リソースで扱えます。

```c
	GtkCssProvider *provider;

	provider = gtk_css_provider_new();
	gtk_css_provider_load_from_resource(provider, "/com/example/sample1/hoge.css");
	gtk_style_provider_add_provider_for_display(gdk_display_get_default(), provider, GTK_STYLE_PROVIDER_PRIORITY_USER);
```

### アイコン

アプリケーションで使用するアプリケーション固有のアイコンもリソースで扱います。
(アイコンは、ディレクトリ単位で管理します)

```c
	GtkIconTheme *icon_theme;

	icon_theme = gtk_icon_theme_get_for_display(gdk_display_get_default());
	gtk_icon_theme_add_resource_path(icon_theme, "/com/example/sample1/icons");
```

### その他のファイル

ぶっちゃけ、何のファイルでもいいので、リソースに組み込んでおき、プログラムで読むことができます。
その場合には、`g_resource_lookup_data`や`g_resource_open_stream`を使用します。
もしくは、「`resource`」というスキーマをもつURLを指定して`GFile`でアクセスします。

```c
	GFile *file_obj;
	char *contents;

	file_obj = g_file_new_for_uri("resource:/com/example/sample1/hoge.txt");
	g_file_load_contents(file_obj, NULL, &contents, NULL, NULL, NULL);
```

## C言語でのリソースの扱い

まずは、C言語でリソースを扱ってみます。
UI定義ファイルを作成します。(ファイル名を`hoge.ui`とします。)

```xml
<?xml version="1.0" encoding="utf-8"?>
<interface>
	<object class="GtkWindow" id="window">
	</object>
</interface>
```

これを読み込むソースコードです。(ファイル名を`main.c`とします。)

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

static void on_activate(GApplication *app)
{
	GtkBuilder *builder;
	GtkWidget *window;

	builder = gtk_builder_new();
	gtk_builder_add_from_resource(builder, "/com/example/sample1/hoge.ui");

	window = GTK_WIDGET(gtk_builder_get_object(builder, "window"));
	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("com.example.sample1", 0);
	g_signal_connect(G_OBJECT(app), "activate", G_CALLBACK(on_activate), NULL);
	return g_application_run(G_APPLICATION(app), argc, argv);
}
```

`gtk_builder_add_from_resource`を`gtk_builder_add_from_file`に変えれば、UI定義ファイルを実行時に直接読み込むことになりますが、今回はリソースから読み込むという話ですので、`gtk_builder_add_from_resource`を使用します。
引数に、リソースパスを指定します。これに関しては、後述します。

次に、リソース定義ファイルを作成します。(ファイル名を`gresource.xml`とします。)

```xml
<?xml version="1.0" encoding="utf-8"?>
<gresources>
	<gresource prefix="/com/example/sample1">
		<file preprocess="xml-stripblanks">hoge.ui</file>
	</gresource>
</gresources>
```

「`file`」要素に、リソースに組み込むファイルを指定します。
UI定義ファイルのようなXMLファイルの場合、「`preprocess="xml-stripblanks"`」属性を指定して、余分なスペースをカットします。

「`gresource`」要素の「`prefix`」に、リソースパスの接頭語を指定します。
通常は、アプリケーションIDをスラッシュで区切ったものを指定し、指定したファイルがアプリケーション固有のものだと示すのが慣例のようです。
上記のように指定した場合、「`hoge.ui`」のリソースパスは「`/com/example/sample1/hoge.ui`」になります。

このリソース定義ファイルを元に、リソースデータをC言語のソースコードに変換します。

```
glib-compile-resources gresource.xml --target resources.c --generate-source
```

これにより、「`resources.c`」というソースコードが作成されるので、これと一緒にビルドします。

```
gcc main.c resources.c $(pkg-config --cflags --libs gtk4)
```

これで、リソースを使用することができます。

### 追記

ビルド環境について、別記事にしてみました。
https://qiita.com/katsuko0303/items/4dbb1567ae3b0ac921aa

## Pythonでのリソースの扱い

さて、Pythonでのリソースの扱い方ですが、ハッキリ言って「これがベストだ!」というものは決めづらいところがあります。
ですので、いくつか例を挙げて、それぞれのメリット・デメリットを説明していきたいと思います。

その前に、Pythonの場合のプログラムのディレクトリ構成を確認しておきます。

自分は、Pythonのスクリプトが複数ある場合には、最初に起動するスクリプト以外はディレクトリを作成してその下の置き、最初の起動スクリプトはそのディレクトリと同じディレクトリに置くべきと考えています。
これは、Pythonのパッケージの考え方に基づくものです。これに関しては、いずれ話すことにしておいて…。
それを前提としてお話します。

### スクリプトに組み込む

上記のリソースは全部スクリプトの中に書いてしまえ、という考え方です。
ある意味、C言語のリソース管理と考え方は一緒ですね。

```python
builder = Gtk.Builder()
builder.add_from_string('''
<interface>
	<object class="GtkWindow" id="window" />
</interface>
''')

provider = Gtk.CssProvider()
provider.load_from_data(b'''
.color_label {
	background-color: #ff0000;
}
''')
```

見ての通り、ソースコードが見づらくなりますし、あまりいいやり方とは言えません。
とは言え、一つのスクリプトに絞り込む場合には、このような手段を取るのもありでしょう。

あと、アイコンの指定はできません。

### スクリプトと同じディレクトリにリソースを配置する

Pythonは「`__file__`」という変数にスクリプトのパスが格納されています。
なので、リソースファイルを同じディレクトリ(もしくは相対的に見れる場所)に配置しておけば、それを元にパスを求めることができます。

```python
# スクリプトと同じディレクトリに、「hoge.ui」「hoge.css」「icons」というファイル・ディレクトリがあるとする

import os

builder = Gtk.Builder()
builder.add_from_file(os.path.join(os.path.dirname(__file__), 'hoge.ui'))

provider = Gtk.CssProvider()
provider.load_from_file(Gio.File.new_for_path(__file__).get_parent().get_child('hoge.css'))

Gtk.IconTheme.get_for_display(Gdk.Display.get_default()).add_search_path(os.path.join(os.path.dirname(__file__), 'icons'))
```

おおよそいい手だと思います。
あえて欠点を上げるとすれば、パッケージングした際に、パッケージされたスクリプトやこれらのデータは必ずしもファイルとして存在しているとは限らない(zipで圧縮することもできる)ので、その場合の対処ができないという点ですね。
Pyinstallerとか使ったことがありませんが、それらを使った場合にも問題が出てくるんじゃないでしょうか。
まぁ稀なケースだと思うので、それほど重要視する欠点ではないと思います。

### スクリプトと同じディレクトリにリソースを配置した上で、パッケージデータとして扱う

上記の例の応用です。

Pythonの標準モジュールであるpkgutilモジュールに、`get_data`という関数があります。
パッケージ内のデータファイルを取得する関数です。
マニュアルにもありますが、以下の処理と同等です。

```python
d = os.path.dirname(sys.modules[package].__file__)
data = open(os.path.join(d, resource), 'rb').read()
```

これを見ればピンとくるかもしれませんが、上の処理を以下のように置き換えることができます。

```python
import pkgutil

builder = Gtk.Builder()
# パッケージデータはバイナリ文字列なので、strに変換する。
builder.add_from_string(pkgutil.get_data(__name__, 'hoge.ui').decode('utf-8'))

provider = Gtk.CssProvider()
provider.load_from_data(pkgutil.get_data(__name__, 'hoge.css'))
```

pkgutilというからには、パッケージがどのような形であれ読み込むことができるでしょうし、上記の問題は解決できると思います。(試していませんが)
ただし、「パッケージのユーティリティ」ですから、これらは全てパッケージの中にあることが前提です。(最初にディレクトリ構成について話したのは、このためです)

また、ディレクトリ指定はできませんので、アイコンには使用できません。

### `Gio.Resource`を使う

ここで、やっとGResourceを使うやり方です。

とは言え、C言語で使用する時には、リソースデータをC言語のソースファイルにコンバートしていましたが、それを使うのは面倒です。
そこで、ソースファイルではなくバイナリデータにコンバートします。(「`--generate-source`」オプションをなくせば、バイナリデータにコンバートされます)

```
glib-compile-resources gresource.xml --target resources.bin
```

このバイナリデータを上記のようにスクリプトと同じディレクトリに置き、同様に読み込んで登録します。

```python
import os, pkgutil
from gi.repository import Gio, GLib

# ファイルとして読み込む
# Gio.Resource.loadというクラスメソッドもあるが、ちょっと古いバージョンだと使えないかも
with open(os.path.join(os.path.dirname(__file__), 'resources.bin'), 'rb') as f:
	res = Gio.Resource.new_from_data(GLib.Bytes.new(f.read()))

# パッケージデータとして読み込む
res = Gio.Resource.new_from_data(GLib.Bytes.new(pkgutil.get_data(__name__, 'resources.bin')))

# リソースを登録
Gio.resources_register(res)
```

あとは、各処理でリソースとして扱えばいいでしょう。

```python
builder = Gtk.Builder()
builder.add_from_resource('/com/example/sample1/hoge.ui')

provider = Gtk.CssProvider()
provider.load_from_resource('/com/example/sample1/hoge.css')

Gtk.IconTheme.get_for_display(Gdk.Display.get_default()).add_resource_path('/com/example/sample1/icons')
```

今までの例の欠点も全て解決できるでしょう。
ただし、各リソースファイルを更新したらその都度コンバートしなければならない、という手間が生まれますが…。

## 終わりに

というわけで、GTKのリソースの扱いを説明しました。
改めて言いますが、Pythonの場合、どれがベストかは決めかねるので、メリット・デメリットを把握した上で判断しましょう。