これは, Gtk 3を使うにあたって, よいチュートリアル的なものが見つからなかったので, 自分の試行錯誤を人柱として残しておくものです.
Gtk 3の入門的なwebページなどはたくさんありますし, 検索をすればすぐ見つかりますし, 初心者でもわかりやすく丁寧にまとめられているものもいっぱいあります. それにも関わらず, よいチュートリアル的なものが見つからなかったといったのは, 以下の意味です: 実はGtk 3はその初版が世に出てから10年くらいがたっています. 初期のapiの中には今は非推奨となっているものも少なくありません. 例えば, gtk_main() などは, 使わないのがモダンな書き方だと思われます. Gtk 3のチュートリアル的な文書の中には(もしくは, 多くは), その初期のapiの使い方を引きずっているものも多くあります. 最近書かれた文書であっても, 過去の文書をベースに書かれていたりするため, 古い書き方なのかどうかは書かれた時期を見ても判断できません. また, 非推奨なものでも, apiとしては残っているので, エラーが出るわけでもなく, 判断できません. Gtk 4という新しいバージョンのapiがでましたが, このバージョンアップで古いapiは捨てられたようです. 新しいバージョンがでたものの, この新しいバージョンが標準となるのは少し先になりそうなので, まだGtk 3を使う機会はあるかと思いますが, いずれGtk 4に移るときに備え, 移行が容易な書き癖を身につけておくのが得策かと思います.
基本的には私の試行錯誤を だらだらと載せていきます. 実行ファイルを徐々に書き足していく形をとります. 冗長かもしれません. 不定期に内容が増えていく予定です.
とりあえず,
pythonで書くので,
お決まりの文をかいて, test.py
という名前で保存.
#!/usr/bin/env python3
def main():
pass
if __name__ == "__main__":
main()
最初のは書かなくてもいいけど,
一応おきまりなので書いておく.
./test.py
とやるだけで,
python3 test.py
を実行してもらうためのおまじない(SHEBAN).
main()
という関数を定義しているが,
その中身は
pass
のみ.
pass
はなにもしないコマンド.
なにか行がないと文法エラーになるようなときに,
とりあえずいれるもの.
(moduleとしてimportされるという形で呼び出されたのではなく)
このファイルが直接実行されるたときには,
__name__
という変数に,
"__main__"
という文字列が代入されている.
それを見て,
moduleとして呼び出されたのか,
直接実行されたのかを判断して,
直接実行されたときには,
main()
という関数を呼んでいる.
とりあえず, 実行時のオプションなどを解析する必要があるだろうから, SHEBANの直後に次を追加し sys
を読み込む.
import sys
次に Gtkをimportする. バージョンは '3.0'
を指定する.
具体的には次を書く.
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
結果以下のようなものができる. 何も起こらないが, エラーも出ないはず. ここでエラーが出るようなら, ライブラリがちゃんと入っていないということなので, 適宜インストールする.
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
def main():
pass
if __name__ == "__main__":
main()
Gtk でアプリを書くときは,
Gtk.Application
というクラスを利用する.
Gtk.Application
の派生クラスを定義しておき,
main()
ではそのインスタンスを作り,
実行するというのが基本.
main()
では,
基本的にそれしかやらない.
コマンドラインのオプションを解析するとかいうのが必要だったりしたときも,
main
ではなく,
Gtk.Application
の派生クラス
に担当させる.
Gtk.Application
の派生クラス
を定義し,
起動時に実際にやること(や終了時にやること)は,
do_なんとか
というMethodを実際に書いていく.
色々と複雑なこともできるようにいろいろなものが用意されているけども,
すごく単純なアプリ
(コマンドラインオプションを解析しなくて良い, 実行のたびに新しいwindowが開く, etc.)
なら, とりあえず,
毎回の起動時に実行される do_activate
だけを実装しておけば良い.
do_なんとか
は,
状況に合わせて色々あり,
必要なのものを実装していけば,
それなりになる.
今は, とりあえず動かすことが重要なので, もう少し詳しいことは, 後日に回す.
とりあえず,
TestApplication
というクラスを
Gtk.Application
の派生クラスとして定義していく.
今回は, 起動時にdone do_actvate
とだけ標準出力に出力するアプリにすることにして,
以下を追加する.
class TestApplication(Gtk.Application):
def do_activate(self):
print("done do_activate")
main()
では,
今書いたTestApplication
のインスタンスを作り,
run
というメソッドを実行する.
具体的には, Main
の定義を以下のように書き換える.
def main():
app = TestApplication()
app.run(sys.argv)
ここまでの変更で, 以下のようになっているはずで,
python3 test.py
とやると
done do_activate
と標準出力に出力されるはずである.
当然ながらまだwindowなどが出るわけではない.
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class TestApplication(Gtk.Application):
def do_activate(self):
print("done do_activate")
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()
GUIのプログラムを組むならウインドウを開く必要があるが,
Gtk.Application
から呼び出すのは,
Gtk.Window
でも悪くないけど,
Gtk.ApplicationWindow
のほうが良いと
マニュアルに書いてある.
Gtk.ApplicationWindow
の派生クラスを用意し,
メインのウインドウとして使う.
とりあえず今回は, TestAppWindow
というクラスを用意する.
何もしないので, とりあえずコンストラクタだけ書くが,
親クラスのやつをただ呼び出すだけということにして,
以下を追加.
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
TestApplication
のdo_activate
では,
TestAppWindow
のインスタンスをつくり,
そのpresent
というメソッドを実行し,
windowを表示する.
TestAppWindow
のインスタンスを作る際には,
application=self
を与える.
具体的には
TestApplication
の
do_activate
の定義を以下のように変更.
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
ここまでの変更で以下のようになっているはずであるが, 実行すると, ウインドウが表示されると思う. 特に何もしていないが, ウィンドウの閉じるボタンを押すと終了するなどの 基本的な動作はするようになっている.
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class TestApplication(Gtk.Application):
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()
gtk_main()
を呼び出すのは古いやり方.
window.connect("destroy", Gtk.main_quit)
とかもやらなくていい.
ただウインドウを開くだけで何もするわけではないけども, とりあえず, メニューバーなんかがあると, それっぽい雰囲気にはなるので, メニューバーあたりを実装する.
LinuxやWindowsならウインドウの一番上, Macなら画面の一番上にあるような,
ファイル, 編集, 表示, ヘルプなどの文字列が並んでいて基本的な操作ができるるやつ(メニュー)をとりあえず作る.
メニューは, (ウィンドウにではなく) Applicationに対して, 設定をする.
(LinuxやWindowsの実装を見ていたらウィンドウに指定したくなるけども,
Macの実装をみると, ウィンドウではなくアプリがメニューを知っていると考えるのが, 自然なのかもしれない)
Gtk.Application
の持っているset_menubar
というメソッドを使えばよい.
このメソッドには, Gio.MenuModel
(GMenuModel
)
を渡してメニューの構造を指定する.
(渡すのはメニューの構造であって,
メニューのwidgetではない.
WidgetではないのでGtk
ではなくGio
のクラスであるので,
マニュアルなどを見るときは注意が必要.
ただ,
実際にGio
のクラス
を直接どうこうすることは殆どないので気にしなくて良い.)
Gio.MenuModel
は,
Bulidable
というものなので,
XMLで書いたデータを元にインスタンスを作成する方法が用意されている.
Menuの構造は単純な入れ子構造になっている(はずな)ので,
XMLと相性がよいから, XMLで書くほうが楽だと思う.
XMLをGtk.Builder.new_from_string
に放り込み, そこから実際のインスタンスを手に入れるということをする.
まずは, XMLを記述してMenuModelの構造を記述していく. 何か別ファイルを用意してそこにXMLを記述していくのが筋が良いとは思うんだけど, とりあえず今回は, ソースのグローバル変数にベタ書きすることにする. import などが終わった直後に以下を追加する.
MENUBAR_XML = """
"""
最初の行は単にxmlのお決まりの文句を書いているだけ.
多分, なくても動作するとは思うんだけど, 念の為書いておく.
interface
タグの中に,
メニューの構造を記述していく.
まず Menu
タグを追加.
get_obeject
でインスタンスを作る際に
要素を指定する必要があるので, id
を指定しておく.
ここでは, menubar
としておく.
MENUBAR_XML = """
"""
この中にメニューの構造を記述していくわけだが,
いまは, メニューバーには,
FileとHelpの2種類のサブメニューがあるので,
それぞれ記述していく.
サブメニューを作るにはsubmenu
タグを使う.
FileとHelpのために2つを用意する.
MENUBAR_XML = """
"""
このままでは, submenuのタイトル(ラベル)がないので, それを
attibute
タグをつかって指定する.
例えば, Fileの方なら
を入れる.
neme="label"
はこのタグがラベルを指定していることを表している.
translatable="yes"
はいずれ多国語対応するなら必要 (gettextがらみの指定. 自動で翻訳されるとか言うのではない.).
そんな本格的なものを作るわけじゃないなら必要ないけど,
まぁ書いておいても損はないので書いておいた.
_File
は ラベルがFile
であるという意味.
File
でもラベルはFile
になる.
_File
と書いているのは,
こう書いておけばAlt+F
というショートカットが作られるから.
_
の次の文字がショートカットになる. ラベルの途中でもよく,
大文字と小文字は区別しない. ラベルに_
自体を含めたい場合は,
__
のように二重に書くことでエスケープできる.
MENUBAR_XML = """
"""
次にFileの中身を書いていく.
Fileの中には, Quitという項目をおく予定だった.
このために, item
タグを使う.
項目のラベルは,
attibute
タグをつかって指定する.
今の場合だと,
をいれることになる.
これをsubmenu
に追記する.
MENUBAR_XML = """
"""
次にHelpの中身を書いていく. 同様の作業をして以下のようになる.
MENUBAR_XML = """
"""
Helpの中身のContent(ヘルプの目次)と,
About(バージョン情報等)
では, 仕切り線を入れて区切りたいかもしれない.
このような場合は,
section
タグを使ってグルーピングをする.
MENUBAR_XML = """
"""
これで, menubarの構造は記述できたので,
これを読み込みAppliactionについかするということをする必要がある.
直接どこかに書いてもいいが,
一旦
build_menu
というメソッドを作り,
それを然るべきところから呼び出す形にする.
やることは割と単純.
まず, Gtk.Builder.new_from_string
に引数として
XMLが入った文字列を与えGtk.Builder
のインスタンスを作る.
そのインスタンスのget_obect
に引数としてid
であるところのmenubar
を与えメニューバーの構造を得る.
それを, Gtk.Application
のset_menubar
に渡せば,
メニューができる.
具体的には次をTestApplication(Gtk.Application)
のところに追記すればよい.
def build_menubar(self):
builder = Gtk.Builder.new_from_string(MENUBAR_XML, -1)
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
これを実行してもエラーは出ないが, メニューも追加されていない.
それは今定義したbuild_menubar
は定義されたが,
どこからも実行されていないから.
これを実行するものを,
do_なんとか
のどこかに書く必要があるが,
どこに書くべきかについて少し考える.
コマンドラインオプションを与えたときに挙動変えたいということはよくありますが,
そういったことをやりやすいように,
Application
の
do_なんとか
はなっています.
コマンドラインオプションが何もないときには,
do_activate
が実行されますが,
ファイル名を指定したときなどは,
別のdo_なんとか
が実行されます.
いずれの場合も
do_startup
が実行されてから,
do_activate
などが実行されます.
メニューの設定のようなアプリケーションに共通な
設定は,
do_startup
で行うべきでしょう.
メニューの設定などは,
do_startup
に書くことにします.
以下のように書きます.
親クラスのdo_startup
を最初に呼び出していることに注意してください.
def do_startup(self):
Gtk.Application.do_startup(self)
self.build_menubar()
ここまでの変更でソースは以下のようになっています. ファイルとヘルプというメニューが追加されました. ただし, これらをクリックしても今は何も起こりません.
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,Gio
MENUBAR_XML = """
"""
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class TestApplication(Gtk.Application):
def do_startup(self):
Gtk.Application.do_startup(self)
self.build_menubar()
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
def build_menubar(self):
builder = Gtk.Builder.new_from_string(MENUBAR_XML, -1)
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()
昔は
Gtk.menubar
というwidgetを作って,
直接windowに追加していたが,
これは古いやり方.
メニューは各windowではなくapplicationがもつもの.
とりあえず, 文字列としてXMLを記述して
Gtk.Builder
を作った.
これは, ソースファイルが一つにまとまるという点でありがたいけど,
このままここに載せると, ハイライトがおかしくなるので,
xmlは別ファイルにして, そのファイルからGtk.Builder
を作る
ように変更する.
このようにしておいたほうがメンテナンスの観点からも良いかもしれない.
まずtest.py
と同じフォルダに
test.xml
という名前で以下の内容のファイルを保存する.
(MENUBAR_XML
の内容そのもの)
test.py
の方は,
まず, MENUBAR_XML
を削除し,
変わりに, MENUBAR_XML_FILENAME = "./test.xml"
を追加.
Gtk.Builder.new_from_string
になっていたのを,
Gtk.Builder.new_from_file
に変更し, 引数は MENUBAR_XML_FILENAME
のみを与える.
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,Gio
MENUBAR_XML_FILENAME = "./test.xml"
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class TestApplication(Gtk.Application):
def do_startup(self):
Gtk.Application.do_startup(self)
self.build_menubar()
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
def build_menubar(self):
builder = Gtk.Builder.new_from_file(MENUBAR_XML_FILENAME)
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()
メニューがあっても何もしないのであれば意味がないので,
とりあえず, Quitを選んだときの挙動を実装していきます.
Quitを選んだときの処理を行うメソッドを自分で作ってからそれを登録します.
名前は何でも良いですが, わかりやすいように on_quit
としてやることにします.
アプリを終了するには,
Gtk.Application
のquit
というメソッドを使います.
具体的にはTestApplication
に以下を追記します.
def on_quit(self,action,param):
self.quit()
このメソッドをQuitを選んだときの挙動として登録するには,
以下の手順を踏みます.
まずtest.xml
を編集します.
Quitに関するitem
の中に
というタグを追加します.
quit
は別の名前でもよいですが,
何をするのかがわかりやすいほうが良いと思います.
Applicationに,
ApplicationWindowに送られます.
次に, test.py
を修正し,
メニューからGtk.Application
に送られるイベントに対する処理として,
on_quit
を登録するということをします.
そのために, Gio.SimpleAction
というものを使う必要がありますので,
Gio
もimportするように変更します.
from gi.repository import Gtk,Gio
Gio.SimpleAction.new("quit", None)
でquit
に対応するものを作り, そのactivate
アクションの際に実行するメソッドとしてon_quit
メソッドを指定し,
それをTestApplication
に登録するということをします.
具体的には build_ui
のなかに,
以下を追加します.
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
結局以下のような状況になっています. これでメニューのQuitを選ぶとウィンドウが閉じるようになりました.
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,Gio
MENUBAR_XML_FILENAME = "./test.xml"
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class TestApplication(Gtk.Application):
def do_startup(self):
Gtk.Application.do_startup(self)
self.build_menubar()
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
def build_menubar(self):
builder = Gtk.Builder.new_from_file(MENUBAR_XML_FILENAME)
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
def on_quit(self,action,param):
self.quit()
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()
gtk.main_quit()
を使ってプログラムを止めるのは,
古いやり方.
次は, Aboutを選んだときの挙動を記述します.
TestApplication
にon_about
というメソッドを作成し,
このアプリについての情報を載せたダイアログを出すようにします.
ダイアログは一般には
Gtk.Dialog
や
Gtk.MessageDialog
を使って書きますが,
Aboutようのダイアログは特別なものが用意されています.
それを使って以下のように書きます.
(とりあえずmodalにしたりとかはしていません. 最低限だけです)
def on_about(self, action, param):
about_dialog = Gtk.AboutDialog()
about_dialog.set_program_name("TestApp")
about_dialog.present()
あとは, Quitのときと同じように, アクションと結びつければ終わりです.
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,Gio
MENUBAR_XML_FILENAME = "./test.xml"
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class TestApplication(Gtk.Application):
def do_startup(self):
Gtk.Application.do_startup(self)
self.build_menubar()
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
def build_menubar(self):
builder = Gtk.Builder.new_from_file(MENUBAR_XML_FILENAME)
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
action = Gio.SimpleAction.new("about", None)
action.connect("activate", self.on_about)
self.add_action(action)
def on_quit(self,action,param):
self.quit()
def on_about(self, action, param):
about_dialog = Gtk.AboutDialog()
about_dialog.set_program_name("TestApp")
about_dialog.present()
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()
最後に, Contentを選んだときの挙動を記述します. マニュアルを表示するのが適切ですが, マニュアルはwebにおくことにして, そこへのリンクがあるダイアログを表示させることにします.
外部へのリンクはGtk.LinkButton
を使います.
またメッセージを表示させるためのダイアログは,
Gtk.MessageDialog
をつかいます.
多分間違った使い方ですが,
メインのエリアに強引に外部リンクを追加する方法で実装します.
TestApplication
にon_help
という
メソッドを以下の内容で追記します.
def on_help(self, action, param):
help_dialog = Gtk.MessageDialog(flags=0,buttons=Gtk.ButtonsType.OK,text="Help")
help_dialog.format_secondary_text("Help for TestApp")
link=Gtk.LinkButton.new_with_label("https://google.co.jp/","link to help")
link.show()
contentarea=help_dialog.get_content_area()
contentarea.add(link)
help_dialog.run()
help_dialog.destroy()
それをイベントに接続したりするのは同様にやればよいので, 最終的には以下のようになります
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,Gio
MENUBAR_XML_FILENAME = "./test.xml"
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class TestApplication(Gtk.Application):
def do_startup(self):
Gtk.Application.do_startup(self)
self.build_menubar()
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
def build_menubar(self):
builder = Gtk.Builder.new_from_file(MENUBAR_XML_FILENAME)
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
action = Gio.SimpleAction.new("about", None)
action.connect("activate", self.on_about)
self.add_action(action)
action = Gio.SimpleAction.new("help", None)
action.connect("activate", self.on_help)
self.add_action(action)
def on_quit(self,action,param):
self.quit()
def on_about(self, action, param):
about_dialog = Gtk.AboutDialog()
about_dialog.set_program_name("TestApp")
about_dialog.present()
def on_help(self, action, param):
help_dialog = Gtk.MessageDialog(flags=0,buttons=Gtk.ButtonsType.OK,text="Help")
help_dialog.format_secondary_text("Help for TestApp")
link=Gtk.LinkButton.new_with_label("https://google.co.jp/","link to help")
link.show()
contentarea=help_dialog.get_content_area()
contentarea.add(link)
help_dialog.run()
help_dialog.destroy()
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()
set_menubar
でメニューを追加すると,
Fileなどの文字列が並んだ(昔ながらの)メニューが作成されます.
一方最近では, メニューをまとめて, ハンバーガアイコン(三本線のメニューアイコン)
で表示するアプリもよくあります.
このようなアプリケーションメニューを作成するには,
set_app_menu
を使います.
環境によっては,
set_app_menu
で作られるものは,
ハンバーガアイコンかもしれませんし,
もしかしたら,
単にアプリケーションというのがメニューバーに追加されるだけかもしれません.
やることは, set_menubarと同じなので, 割愛します.
python3 test.py filename
などとやって,
起動時にオプションを渡すことを考えます.
オプションの解析や仕様に合わない入力があったときのエラー処理などは
基本的にやってくれるので,
自分で書く必要はありません.
デフォルトでは何もオプションを受け付けません.
ですので, もし何か引数をつけて実行すると,
-f は不明なオプションです
とか
This application can not open files.
などといって終了します.
引数としてファイル名を渡し実行するという形式はよくあります.
このようなものは簡単にかけるようになっています.
まず,
Application
に, Gio.ApplicationFlags.HANDLES_OPEN
を指定します.
これは, インスタンスを作成するときに設定されるべきなのでコンストラクタのなかで
行います. 基底クラスのコンストラクタを呼び出す際にオプション引数で渡しても良いですが,
ここでは, set_flags
というメソッドを使います.
TestApplication(Gtk.Application)
に以下を追記します:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_flags(Gio.ApplicationFlags.HANDLES_OPEN)
実行時に引数が渡されなかった場合は,
今までどおり,
do_startup
のあとにdo_activate
が実行されます.
実行時に引数が渡された場合は,
do_startup
のあとにdo_open
が実行されます.
この場合do_activate
は実行されません.
引数として複数のファイル名を与えることもできます.
do_open
を定義していきます.
do_open(self,files,n_files,hint)
で,
n_files
は指定されたファイルの総数,
files
はGio.File
のリストです.
hint
は文字列です.
(通常は""
のようですが,
なんのために開かれたのかという情報を表す文字列(view
, edit
)を渡して挙動を変えることができるようです.)
今は何もしません.
とりあえず, 与えられたファイル名を表示するように,
以下をTestApplication(Gtk.Application)
に追記します.
def do_open(self,files,n_files,hint):
for gfile in files:
print(gfile.get_path())
print(hint)
ここまでの変更で以下のようになっています.
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,Gio
MENUBAR_XML_FILENAME = "./test.xml"
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class TestApplication(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_flags(Gio.ApplicationFlags.HANDLES_OPEN)
def do_startup(self):
Gtk.Application.do_startup(self)
self.build_menubar()
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
def do_open(self,files,n_files,hint):
for gfile in files:
print(gfile.get_path())
print(hint)
def build_menubar(self):
builder = Gtk.Builder.new_from_file(MENUBAR_XML_FILENAME)
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
action = Gio.SimpleAction.new("about", None)
action.connect("activate", self.on_about)
self.add_action(action)
action = Gio.SimpleAction.new("help", None)
action.connect("activate", self.on_help)
self.add_action(action)
def on_quit(self,action,param):
self.quit()
def on_about(self, action, param):
about_dialog = Gtk.AboutDialog()
about_dialog.set_program_name("TestApp")
about_dialog.present()
def on_help(self, action, param):
help_dialog = Gtk.MessageDialog(flags=0,buttons=Gtk.ButtonsType.OK,text="Help")
help_dialog.format_secondary_text("Help for TestApp")
link=Gtk.LinkButton.new_with_label("https://google.co.jp/","link to help")
link.show()
contentarea=help_dialog.get_content_area()
contentarea.add(link)
help_dialog.run()
help_dialog.destroy()
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()
コマンドラインで起動する際にファイル名だけではなく,
複雑な設定をすることをも行いたいときがあります.
例えば, --version
とか-v
などとやってバージョン情報を表示させるような挙動を実装していこうと思います.
コマンドラインオプションを細かく設定するには,
まず,
Gio.ApplicationFlags.HANDLES_COMMAND_LINE
をセットします.
そのために, 以下のように変更をします:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_flags(Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
このフラグをセットすると,
do_setup
のあとにdo_command_line
が実行されるようになります.
オプションを指定していなくても
do_command_line
が実行され,
do_activate
は実行されなくなります.
また, HANDLES_OPEN
を追加で指定していても,
do_open
は実行されません.
do_command_line(self,command_line)
を定義します.
command_line
には, コマンドラインオプションを解析したものが入っています.
とりあえず, 渡されたコマンドラインオプションを表示するように,
実装してみます.
return 0
のように整数を返す必要があります.
def do_command_line(self,command_line):
options_dict = command_line.get_options_dict()
options = options_dict.end().unpack()
print(options)
return 0
このままでは, 何も表示されません.
理由は, 有効なオプションを登録していないため,
どんな引数も無向なオプションとして捨てられているからです.
コンストラクタで, add_main_option
というメソッドを使い,
解釈するオプションを追加します.
オプションの種類などを指定するフラグが,
GLib
で定義されているので,
importします.
from gi.repository import Gtk,Gio,GLib
--version
または -v
で指定し,
追加の文字列を伴わないものであれば,
以下のように書きます. --help
をつけて実行すると, --version
の説明としてversion infoとでてきます.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_flags(Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
self.add_main_option("version",ord("v"),GLib.OptionFlags.IN_MAIN,GLib.OptionArg.NONE,"version info",None)
do_command_line
を,
version
オプションなら
バージョン情報を表示しそこで終了,
そうでなければactivate
するように変更します.
def do_command_line(self,command_line):
options_dict = command_line.get_options_dict()
options = options_dict.end().unpack()
if "version" in options:
if options["version"]:
print("version:",0)
return 0
self.activate()
return 0
なお, オプションとして処理されなかった引数たちは,
command_line.get_arguments()
で文字列の配列として得ることができます.
ここまでの変更で, 以下のようになっています:
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,Gio,GLib
MENUBAR_XML_FILENAME = "./test.xml"
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class TestApplication(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_flags(Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
self.add_main_option("version",ord("v"),GLib.OptionFlags.IN_MAIN,GLib.OptionArg.NONE,"version info",None)
def do_startup(self):
Gtk.Application.do_startup(self)
self.build_menubar()
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
def do_open(self,files,n_files,hint):
for gfile in files:
print(gfile.get_path())
print(hint)
def do_command_line(self,command_line):
options_dict = command_line.get_options_dict()
options = options_dict.end().unpack()
if "version" in options:
if options["version"]:
print("version:",0)
return 0
self.activate()
return 0
def build_menubar(self):
builder = Gtk.Builder.new_from_file(MENUBAR_XML_FILENAME)
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
action = Gio.SimpleAction.new("about", None)
action.connect("activate", self.on_about)
self.add_action(action)
action = Gio.SimpleAction.new("help", None)
action.connect("activate", self.on_help)
self.add_action(action)
def on_quit(self,action,param):
self.quit()
def on_about(self, action, param):
about_dialog = Gtk.AboutDialog()
about_dialog.set_program_name("TestApp")
about_dialog.present()
def on_help(self, action, param):
help_dialog = Gtk.MessageDialog(flags=0,buttons=Gtk.ButtonsType.OK,text="Help")
help_dialog.format_secondary_text("Help for TestApp")
link=Gtk.LinkButton.new_with_label("https://google.co.jp/","link to help")
link.show()
contentarea=help_dialog.get_content_area()
contentarea.add(link)
help_dialog.run()
help_dialog.destroy()
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()
メニューバーにViewという項目を用意し,
その中にMaximizeというウィンドウを最大化するための項目を用意します.
Maximizeは,
Applicationではなく, ウィンドウに行われるべきものなので,
actionはapp.
ではなくwin.miximize
とします.
test.xml
のFileとHelpのsubmenuの間に以下を追加します.
_View
-
_Maximize
win.miximize
次にMiximizeが選ばれたときに実行するメソッドを,
TestAppWindow(Gtk.ApplicationWindow)
に実装していきます.
実行した際に, 最大化されていなかったら最大化し,
最大化されていたら最大化を解除する, というように,
状況によって切り替えるようにします.
そのために, 対応するactionをstateful
にし,
windowに加えます.
具体的にはまず以下を, TestAppWindow
の__init__
の末尾に追加します.
maximize_action = Gio.SimpleAction.new_stateful("maximize", None, GLib.Variant.new_boolean(False))
self.add_action(maximize_action)
ここで実行をすると, メニューバーにはViewという項目があり, さらにそこにはMaximizeという項目があります. Maximizeにはチェックボックスがあり, 選択するたびに切り替わります. 起動時の状態は, Falseを指定しているので, 選択されていません.
この項目を選択するとchange-state
というイベントが発生するので,
そのときに実行されるメソッドを定義します.
on_maximize_toggle(self, action, value)
を定義しますが,
変更前の値がvalueに代入されて実行されるので,
その値に応じて処理を変えます.
def on_maximize_toggle(self, action, value):
if value.get_boolean():
self.maximize()
else:
self.unmaximize()
次にこのメソッドをアクションと接続します.
__init__
に以下を追加します.
maximize_action.connect("change-state", self.on_maximize_toggle)
これでほぼ終わりですが,
メニューバーを使わずウィンドウの最大化をした場合に,
メニューバーの状態が正しくなくなってしまうので,
このままではよくありません.
そこで,
最大化されたときに, actionの状態を追従させるよう
__init__
に以下を追加します.
self.connect("notify::is-maximized",lambda obj, pspec: maximize_action.set_state(GLib.Variant.new_boolean(obj.props.is_maximized)),)
ここまでの修正で以下のようになっていると思います.
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,Gio,GLib
MENUBAR_XML_FILENAME = "./test.xml"
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
maximize_action = Gio.SimpleAction.new_stateful("maximize", None, GLib.Variant.new_boolean(False))
maximize_action.connect("change-state", self.on_maximize_toggle)
self.add_action(maximize_action)
self.connect("notify::is-maximized",lambda obj, pspec: maximize_action.set_state(GLib.Variant.new_boolean(obj.props.is_maximized)),)
def on_maximize_toggle(self, action, value):
if value.get_boolean():
self.maximize()
else:
self.unmaximize()
class TestApplication(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_flags(Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
self.add_main_option("version",ord("v"),GLib.OptionFlags.IN_MAIN,GLib.OptionArg.NONE,"version info",None)
def do_startup(self):
Gtk.Application.do_startup(self)
self.build_menubar()
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
def do_open(self,files,n_files,hint):
for gfile in files:
print(gfile.get_path())
print(hint)
def do_command_line(self,command_line):
options_dict = command_line.get_options_dict()
options = options_dict.end().unpack()
if "version" in options:
if options["version"]:
print("version:",0)
return 0
self.activate()
return 0
def build_menubar(self):
builder = Gtk.Builder.new_from_file(MENUBAR_XML_FILENAME)
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
action = Gio.SimpleAction.new("about", None)
action.connect("activate", self.on_about)
self.add_action(action)
action = Gio.SimpleAction.new("help", None)
action.connect("activate", self.on_help)
self.add_action(action)
def on_quit(self,action,param):
self.quit()
def on_about(self, action, param):
about_dialog = Gtk.AboutDialog()
about_dialog.set_program_name("TestApp")
about_dialog.present()
def on_help(self, action, param):
help_dialog = Gtk.MessageDialog(flags=0,buttons=Gtk.ButtonsType.OK,text="Help")
help_dialog.format_secondary_text("Help for TestApp")
link=Gtk.LinkButton.new_with_label("https://google.co.jp/","link to help")
link.show()
contentarea=help_dialog.get_content_area()
contentarea.add(link)
help_dialog.run()
help_dialog.destroy()
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()
FileにNewとOpenを追加します.
test.xml
の_Fileを表すsubmenu
の中を以下のように変更します.
section
でグループ化しますが,
グループ化しているものとされているものが混ざっていると,
区切られないので,
_Close
の含まれるグループはそれのみですがsection
でくくられていることに中が必要です.
_File
-
_New
app.new
-
_Open
app.open
-
_Close
app.quit
Newの方は, 対応するon_new
を作成しますが,
そのなかでやることはself.activate
を実行するだけです.
具体的には, 以下をTestApplication
に追加するだけです.
def on_new(self,action,param):
self.activate()
これをbuild_menubar
で
アクションと結びつけます.
具体的には以下を追加します>
action = Gio.SimpleAction.new("new", None)
action.connect("activate", self.on_new)
self.add_action(action)
次にOpenの方を実装しますが,
対応するon_open
を作成します.
一旦なにもしないメソッドとしてon_open
を以下のように作成します.
def on_open(self,action,param):
pass
そしてbuild_menubar
に以下を追加します.
action = Gio.SimpleAction.new("open", None)
action.connect("activate", self.on_open)
self.add_action(action)
これでエラーが出ないことを確認したら,
do_open
を実装していきます.
ファイルを選ぶには, Gtk.FileChooserDialog
を使います.
add_button
をつかって, ダイアログに用意するボタンと,
ボタンが押されたときに帰ってくる値を指定します.
そのあとrun()
を呼び出すとダイアログが開かれ,
ボタンが押されると値が返ってきます.
返ってきた値に応じた処理をしたあと,
destroy()
を実行すると,
ダイアログが消えます.
これが基本的な流れです.
とりあえず値に応じた処理は後で書くことにして雛形を作ってしまいます.
def on_open(self,action,param):
dialog = Gtk.FileChooserDialog(action=Gtk.FileChooserAction.OPEN)
dialog.add_buttons(Gtk.STOCK_CANCEL,Gtk.ResponseType.CANCEL,Gtk.STOCK_OPEN,Gtk.ResponseType.OK)
response = dialog.run()
dialog.destroy()
とりあえず, ダイアログが開かれることと, ボタンを押すとダイアログが消えることを確認したら, 値に応じた処理を実装していきます.
なお,
Openに関するものだったので,
action=Gtk.FileChooserAction.OPEN
としましたが,
SaveAs(名前を付けて保存)の場合には,
action=Gtk.FileChooserAction.SAVE
とします.
まだタイトルがついていないようなものをSave(上書き保存)を選んだときには,
action=Gtk.FileChooserAction.SAVE
をつかい, set_current_name
でデフォルトの名前を"Untitled"
にでも設定しておくと良いでしょう.
また, ファイルではなくフォルダを選択させる場合は,
Gtk.FileChooserAction.SELECT_FOLDER
にします.
run()
をした結果に応じての挙動を実装します.
開くを選んだ場合は, ファイルの情報をダイアログから取り出し,
そのファイルを開き,
そのあとダイアログをdestroyします.
ファイルの情報は,
Gtk.FileChooserDialog
の
get_file
というメソッドを使います. Gio.File
が返ってきます.
これを引数に Application
の
open
というメソッドを実行します.
open
にはFileのリスト他,
"view"
や"edit"
といった挙動の付加的な情報を表す文字列を与えます.
付加情報は必要なければ""
を与えます(None
ではない).
ファイルを開く際に行う動作は実際には,
do_open
に実装しました/すが,
open
を実行するとそれが実行されます.
response = dialog.run()
のあと (dialog.destroy()
の前) に
以下を追加します.
if response == Gtk.ResponseType.OK:
file=dialog.get_file()
self.open([file],"")
elif response == Gtk.ResponseType.CANCEL:
print("Cancel clicked")
open
を呼ぶためには,
flagにGio.ApplicationFlags.HANDLES_OPEN
が指定されていないといけません.
そのため
TestApplication
の
__init__
でset_flags
をするのを以下のように変更します.
self.set_flags(Gio.ApplicationFlags.HANDLES_OPEN|Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
ここまでの変更で, 以下のようになっていると思います.
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,Gio,GLib
MENUBAR_XML_FILENAME = "./test.xml"
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
maximize_action = Gio.SimpleAction.new_stateful("maximize", None, GLib.Variant.new_boolean(False))
maximize_action.connect("change-state", self.on_maximize_toggle)
self.add_action(maximize_action)
self.connect("notify::is-maximized",lambda obj, pspec: maximize_action.set_state(GLib.Variant.new_boolean(obj.props.is_maximized)),)
def on_maximize_toggle(self, action, value):
if value.get_boolean():
self.maximize()
else:
self.unmaximize()
class TestApplication(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_flags(Gio.ApplicationFlags.HANDLES_OPEN|Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
self.add_main_option("version",ord("v"),GLib.OptionFlags.IN_MAIN,GLib.OptionArg.NONE,"version info",None)
def do_startup(self):
Gtk.Application.do_startup(self)
self.build_menubar()
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
def do_open(self,files,n_files,hint):
for gfile in files:
print(gfile.get_path())
print(hint)
def do_command_line(self,command_line):
options_dict = command_line.get_options_dict()
options = options_dict.end().unpack()
if "version" in options:
if options["version"]:
print("version:",0)
return 0
self.activate()
return 0
def build_menubar(self):
builder = Gtk.Builder.new_from_file(MENUBAR_XML_FILENAME)
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
action = Gio.SimpleAction.new("new", None)
action.connect("activate", self.on_new)
self.add_action(action)
action = Gio.SimpleAction.new("open", None)
action.connect("activate", self.on_open)
self.add_action(action)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
action = Gio.SimpleAction.new("about", None)
action.connect("activate", self.on_about)
self.add_action(action)
action = Gio.SimpleAction.new("help", None)
action.connect("activate", self.on_help)
self.add_action(action)
def on_quit(self,action,param):
self.quit()
def on_about(self, action, param):
about_dialog = Gtk.AboutDialog()
about_dialog.set_program_name("TestApp")
about_dialog.present()
def on_help(self, action, param):
help_dialog = Gtk.MessageDialog(flags=0,buttons=Gtk.ButtonsType.OK,text="Help")
help_dialog.format_secondary_text("Help for TestApp")
link=Gtk.LinkButton.new_with_label("https://google.co.jp/","link to help")
link.show()
contentarea=help_dialog.get_content_area()
contentarea.add(link)
help_dialog.run()
help_dialog.destroy()
def on_new(self,action,param):
self.activate()
def on_open(self,action,param):
dialog = Gtk.FileChooserDialog(action=Gtk.FileChooserAction.OPEN)
dialog.add_buttons(Gtk.STOCK_CANCEL,Gtk.ResponseType.CANCEL,Gtk.STOCK_OPEN,Gtk.ResponseType.OK)
response = dialog.run()
if response == Gtk.ResponseType.OK:
file=dialog.get_file()
self.open([file],"")
elif response == Gtk.ResponseType.CANCEL:
print("Cancel clicked")
dialog.destroy()
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()
ファイルの一覧に関連のないファイルまで表示されると選択が大変なので, 拡張子で絞る機能を追加します.
Gtk.FileFilter
を, ダイアログにadd_filter
で追加することでやりたいことができます.
Gtk.FileFilter
には,
関連するmime typeもしくはワイルドカードを使ったファイル名のパターンを指定します.
add_mime_type
や
add_pattern
を使って指定します.
複数の条件(unionになる)を指定することができます.
また, ダイアログに表示するときの名前をset_name
で指定します.
Zipファイルに制限する(本質的にzipファイルであるdocxなどもふくまれるかもしれません)フィルターと,
すべてのファイルを表示するフィルターを用意するためには,
以下を, dialog.run()
をする前に追加します.
filter = Gtk.FileFilter()
filter.set_name("Zip files")
filter.add_pattern("*.zip")
filter.add_mime_type("application/zip")
dialog.add_filter(filter)
filter = Gtk.FileFilter()
filter.set_name("All files")
filter.add_pattern("*")
dialog.add_filter(filter)
ここまでの変更で, 以下のようになっていると思います.
#!/usr/bin/env python3
import sys
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk,Gio,GLib
MENUBAR_XML_FILENAME = "./test.xml"
class TestAppWindow(Gtk.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
maximize_action = Gio.SimpleAction.new_stateful("maximize", None, GLib.Variant.new_boolean(False))
maximize_action.connect("change-state", self.on_maximize_toggle)
self.add_action(maximize_action)
self.connect("notify::is-maximized",lambda obj, pspec: maximize_action.set_state(GLib.Variant.new_boolean(obj.props.is_maximized)),)
def on_maximize_toggle(self, action, value):
if value.get_boolean():
self.maximize()
else:
self.unmaximize()
class TestApplication(Gtk.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_flags(Gio.ApplicationFlags.HANDLES_OPEN|Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
self.add_main_option("version",ord("v"),GLib.OptionFlags.IN_MAIN,GLib.OptionArg.NONE,"version info",None)
def do_startup(self):
Gtk.Application.do_startup(self)
self.build_menubar()
def do_activate(self):
window=TestAppWindow(application=self)
window.present()
def do_open(self,files,n_files,hint):
for gfile in files:
print(gfile.get_path())
print(hint)
def do_command_line(self,command_line):
options_dict = command_line.get_options_dict()
options = options_dict.end().unpack()
if "version" in options:
if options["version"]:
print("version:",0)
return 0
self.activate()
return 0
def build_menubar(self):
builder = Gtk.Builder.new_from_file(MENUBAR_XML_FILENAME)
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
action = Gio.SimpleAction.new("new", None)
action.connect("activate", self.on_new)
self.add_action(action)
action = Gio.SimpleAction.new("open", None)
action.connect("activate", self.on_open)
self.add_action(action)
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
action = Gio.SimpleAction.new("about", None)
action.connect("activate", self.on_about)
self.add_action(action)
action = Gio.SimpleAction.new("help", None)
action.connect("activate", self.on_help)
self.add_action(action)
def on_quit(self,action,param):
self.quit()
def on_about(self, action, param):
about_dialog = Gtk.AboutDialog()
about_dialog.set_program_name("TestApp")
about_dialog.present()
def on_help(self, action, param):
help_dialog = Gtk.MessageDialog(flags=0,buttons=Gtk.ButtonsType.OK,text="Help")
help_dialog.format_secondary_text("Help for TestApp")
link=Gtk.LinkButton.new_with_label("https://google.co.jp/","link to help")
link.show()
contentarea=help_dialog.get_content_area()
contentarea.add(link)
help_dialog.run()
help_dialog.destroy()
def on_new(self,action,param):
self.activate()
def on_open(self,action,param):
dialog = Gtk.FileChooserDialog(action=Gtk.FileChooserAction.OPEN)
dialog.add_buttons(Gtk.STOCK_CANCEL,Gtk.ResponseType.CANCEL,Gtk.STOCK_OPEN,Gtk.ResponseType.OK)
filter = Gtk.FileFilter()
filter.set_name("Zip files")
filter.add_pattern("*.zip")
filter.add_mime_type("application/zip")
dialog.add_filter(filter)
filter = Gtk.FileFilter()
filter.set_name("All files")
filter.add_pattern("*")
dialog.add_filter(filter)
response = dialog.run()
if response == Gtk.ResponseType.OK:
file=dialog.get_file()
self.open([file],"")
elif response == Gtk.ResponseType.CANCEL:
print("Cancel clicked")
dialog.destroy()
def main():
app = TestApplication()
app.run(sys.argv)
if __name__ == "__main__":
main()