PyGTKで重い処理をしているときにGUIを固まらせないための手法をまとめる
これまでに書いたPyGTKのコードの中で、何か重い処理をさせているときに(メインループに処理が回らないことにより)GUIが固まってしまうのを避けるために幾つかの方法を使用してきた。ここではそれらをまとめておく。
スレッドを使用する
マルチスレッド化して重い処理を別のスレッドで実行するようにする。子プロセスの実行など、同時に複数の処理を行いたいことがあるときに使えるが、以下のデメリットがある。どうしてもスレッドを使用しなくてはならない、という場合以外はおすすめできない。
ジェネレータとGLibのidle_add()を使用する
「PyGTKでのプログレスバーについて(シンプルなGUIダウンローダを作成)」で使用している。
glib.idle_add()*3はメインループよりも低い優先度で処理を実行し、その戻り値がTrueであれば繰り返し実行される。
ジェネレータの中で重いループなどの処理を行い
- ループの中などで処理を続行するところ: yield True
- 処理終了もしくは停止するところ: yield False
を記述するようにして
g = [ジェネレータ名]([引数...]) glib.idle_add(g.next)
のようにすると、その中で重い処理が行われてもGUIが固まることはない。ただし、きちんと「yield True」が途中で繰り返し呼ばれるようになっていないとうまくいかない。
手動でメインループを回す
「PyGTKでダイアログを出しつつ親ウィンドウも操作できるようにする」でも使用しているが、gtk.main_iteration()はメインループを1回だけ回す。gtk.events_pending()がTrueのときにこれを繰り返し呼ぶようにすると、それまでにGUI部品に対して行った操作の内、まだ反映処理がされていないものを処理してくれる。
(GUI部品に対する操作を記述...)
while gtk.events_pending():
gtk.main_iteration()
これを使用することによっても、スレッドを使用せずにGUIが固まるのを防ぐことができる場合がある。例えば「ドメインの名前解決チェックを行うGUIツールをPyGTKで作成」のコードをシングルスレッドで記述すると下のようになる。一部スリープ時間などは調整している。
#! /usr/bin/python # -*- encoding: utf-8 -*- # Filter2 20090422(single-threaded version) (C) 2009 kakurasan # Licensed under GPL-3 import socket import time import sys try: import pygtk pygtk.require('2.0') except: pass try: import pango import gtk except: print >> sys.stderr, 'Error: PyGTK is not installed' sys.exit(1) try: from glib import timeout_add as glib_timeout_add from glib import source_remove as glib_source_remove except: try: from gobject import timeout_add as glib_timeout_add from gobject import source_remove as glib_source_remove except: print >> sys.stderr, 'Error: cannot import GLib functions' sys.exit(1) class MainWindow(gtk.Window): """ メインウィンドウ """ def __init__(self, *args, **kwargs): gtk.Window.__init__(self, *args, **kwargs) # ショートカットキー(アクセラレータ) self.accelgroup = gtk.AccelGroup() self.add_accel_group(self.accelgroup) # メニュー項目 self.item_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.accelgroup) self.menu_file = gtk.Menu() self.menu_file.add(self.item_quit) self.item_file = gtk.MenuItem('_File', True) self.item_file.set_submenu(self.menu_file) self.menubar = gtk.MenuBar() self.menubar.append(self.item_file) # 部品 self.textview_input = gtk.TextView() self.textview_found = gtk.TextView() self.textview_notfound = gtk.TextView() self.textview_skipped = gtk.TextView() for textview in (self.textview_input, self.textview_found, self.textview_notfound, self.textview_skipped): textview.set_wrap_mode(gtk.WRAP_CHAR) self.textbuf_input = self.textview_input.get_buffer() self.textbuf_found = self.textview_found.get_buffer() self.textbuf_notfound = self.textview_notfound.get_buffer() self.textbuf_skipped = self.textview_skipped.get_buffer() self.txttag_active = gtk.TextTag() self.txttag_active.set_property('foreground', 'blue') self.txttag_active.set_property('weight', pango.WEIGHT_BOLD) self.textbuf_input.get_tag_table().add(self.txttag_active) self.id_textbuf_input = self.textbuf_input.connect('changed', self.on_textbuf_changed) self.sw_input = gtk.ScrolledWindow() self.sw_found = gtk.ScrolledWindow() self.sw_notfound = gtk.ScrolledWindow() self.sw_skipped = gtk.ScrolledWindow() for sw in (self.sw_input, self.sw_found, self.sw_notfound, self.sw_skipped): sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.sw_input.add(self.textview_input) self.sw_found.add(self.textview_found) self.sw_notfound.add(self.textview_notfound) self.sw_skipped.add(self.textview_skipped) self.button_start = gtk.Button(label='_Check') self.button_start.set_image(gtk.image_new_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON)) self.button_stop = gtk.Button(stock=gtk.STOCK_STOP) self.button_stop.set_sensitive(False) self.pgbar = gtk.ProgressBar() self.statusbar = gtk.Statusbar() self.cxtid_linecnt = self.statusbar.get_context_id('linecount') self.cxtid_status = self.statusbar.get_context_id('status') self.msgid_linecnt = None self.msgid_status = None # フレーム(説明のために使用・中身はスクロールウィンドウ) self.frame_input = gtk.Frame('Input') self.frame_input.add(self.sw_input) self.frame_found = gtk.Frame('OK') self.frame_found.add(self.sw_found) self.frame_notfound = gtk.Frame('NG') self.frame_notfound.add(self.sw_notfound) self.frame_notfound.set_size_request(-1, 100) self.frame_skipped = gtk.Frame('Skipped') self.frame_skipped.add(self.sw_skipped) self.frame_skipped.set_size_request(-1, 60) # レイアウト用コンテナ self.vbox_output = gtk.VBox() # 中央右側の縦分割 self.vbox_output.pack_start(self.frame_found) self.vbox_output.pack_start(self.frame_notfound) self.vbox_output.pack_start(self.frame_skipped) self.hbox_btn = gtk.HBox() # 下の横分割 self.hbox_btn.pack_start(self.button_start, expand=False, fill=False) self.hbox_btn.pack_start(self.button_stop, expand=False, fill=False) self.hbox_btn.pack_start(self.pgbar) self.hbox_textview = gtk.HBox() # 中央の横分割 self.hbox_textview.pack_start(self.frame_input) self.hbox_textview.pack_start(self.vbox_output) self.vbox = gtk.VBox() # 全体の縦分割 self.vbox.pack_start(self.menubar, expand=False, fill=False) self.vbox.pack_start(self.hbox_textview) self.vbox.pack_start(self.hbox_btn, expand=False, fill=False) self.vbox.pack_start(self.statusbar, expand=False, fill=False) # シグナル self.connect('delete_event', gtk.main_quit) self.item_quit.connect('activate', gtk.main_quit) self.button_start.connect('clicked', self.on_button_start_clicked) self.button_stop.connect('clicked', self.on_button_stop_clicked) # ウィンドウ self.add(self.vbox) self.set_size_request(350, 400) self.resize(400, 450) def to_update_progressbar(self): """ 処理中にのみ定期的に実行される関数 プログレスバーを更新 """ fraction = float(self.domains_processed) / float(self.domains) self.pgbar.set_text('%d / %d' % (self.domains_processed, self.domains)) self.pgbar.set_fraction(fraction) return True def resolve(self): """ 指定ドメインの名前解決を試みて結果をテキストバッファに出力 処理の前後にはGUI部品の状態などを設定 """ # ステータスバー if self.msgid_status: self.statusbar.remove(self.cxtid_status, self.msgid_status) self.msgid_status = self.statusbar.push(self.cxtid_status, 'Processing...') # 行数カウントによるテキスト更新を一時的にブロック self.textbuf_input.handler_block(self.id_textbuf_input) # 出力先のテキストバッファを空にする for widget in (self.textbuf_found, self.textbuf_notfound, self.textbuf_notfound, self.textbuf_skipped): widget.set_text('') # 停止ボタンを押せるようにする self.button_stop.set_sensitive(True) # テキストビューの編集を禁止し、開始ボタンも押せないようにする for widget in (self.button_start, self.textview_input, self.textview_found, self.textview_notfound, self.textview_skipped): widget.set_sensitive(False) # プログレスバー更新のタイムアウト関数を設定 tag = glib_timeout_add(150, self.to_update_progressbar) # テキストビューの各行のドメイン名について名前解決を試みて # 処理した項目は強調し(スタイルを付け)ていく iter_start = self.textbuf_input.get_start_iter() self.domains = self.textbuf_input.get_line_count() self.domains_processed = 0 # 項目別にループで処理を行う while True: # メインループを回して表示反映 while gtk.events_pending(): gtk.main_iteration() iter_end = iter_start.copy() # 位置情報をコピーして iter_end.forward_to_line_end() # 行末(終点)に持っていく domain = self.textbuf_input.get_text(iter_start, iter_end) self.textbuf_input.place_cursor(iter_end) self.textbuf_input.apply_tag(self.txttag_active, iter_start, iter_end) self.textview_input.scroll_to_iter(iter_end, 0) if '/' in domain: # ドメインの後ろにディレクトリを含むものは飛ばす iter_end_skipped = self.textbuf_skipped.get_end_iter() self.textbuf_skipped.place_cursor(self.textbuf_skipped.get_end_iter()) self.textbuf_skipped.insert_at_cursor(domain + '\n') self.textview_skipped.scroll_to_mark(self.textbuf_skipped.get_insert(), 0) else: # 名前解決を試行し、結果に応じたテキストバッファに追加 try: socket.gethostbyname(domain) except socket.gaierror: # 失敗 iter_end_notfound = self.textbuf_notfound.get_end_iter() self.textbuf_notfound.place_cursor(self.textbuf_notfound.get_end_iter()) self.textbuf_notfound.insert_at_cursor(domain + '\n') self.textview_notfound.scroll_to_mark(self.textbuf_notfound.get_insert(), 0) else: # 成功 self.textbuf_found.place_cursor(self.textbuf_found.get_end_iter()) self.textbuf_found.insert_at_cursor(domain + '\n') self.textview_found.scroll_to_mark(self.textbuf_found.get_insert(), 0) self.domains_processed += 1 # 次の行の先頭へ移動し、途中に終わりがあれば抜ける forward_line = iter_start.forward_line() if not forward_line: break # 次の名前解決処理までに少し間隔を空ける for _ in range(20): # 0.1秒を20回、合計2秒ずつとする # メインループを回す while gtk.events_pending(): gtk.main_iteration() # 停止ボタンが押された場合はsleepを中断する if self.stop_requested: glib_source_remove(tag) # プログレスバーの更新を終了 self.statusbar.remove(self.cxtid_status, self.msgid_status) self.msgid_status = self.statusbar.push(self.cxtid_status, 'Stopped') self.button_stop.set_sensitive(False) for widget in (self.button_start, self.textview_input, self.textview_found, self.textview_notfound, self.textview_skipped): widget.set_sensitive(True) self.textbuf_input.handler_unblock(self.id_textbuf_input) self.pgbar.set_fraction(0.0) self.pgbar.set_text('') self.textbuf_input.remove_tag(self.txttag_active, self.textbuf_input.get_start_iter(), self.textbuf_input.get_end_iter()) return time.sleep(0.1) # 各種後始末 glib_source_remove(tag) self.statusbar.remove(self.cxtid_status, self.msgid_status) self.msgid_status = self.statusbar.push(self.cxtid_status, 'Done') self.pgbar.set_text('Total:%d OK:%d NG:%d Skipped:%d' % (self.domains, self.textbuf_found.get_line_count() - 1, self.textbuf_notfound.get_line_count() - 1, self.textbuf_skipped.get_line_count() - 1)) self.pgbar.set_fraction(1.0) self.button_stop.set_sensitive(False) for widget in (self.button_start, self.textview_input, self.textview_found, self.textview_notfound, self.textview_skipped): widget.set_sensitive(True) self.textbuf_input.handler_unblock(self.id_textbuf_input) self.textbuf_input.remove_tag(self.txttag_active, self.textbuf_input.get_start_iter(), self.textbuf_input.get_end_iter()) def on_button_start_clicked(self, widget): """ 開始ボタンを押したときの処理 """ if self.textbuf_input.get_char_count() > 0: # 完全に空のときは処理しない # stop_requestedは停止ボタンが押されたかどうかを示す真偽値で # 処理を行う関数側でこの値を監視しながらループする self.stop_requested = False self.resolve() def on_button_stop_clicked(self, widget): """ 停止ボタンを押したときの処理 """ self.stop_requested = True def on_textbuf_changed(self, widget): """ テキストバッファが変更されたときの処理 """ # ステータスバーにドメイン数(テキストバッファの行数)を表示 if self.msgid_linecnt: # 前回の文字列があれば消す self.statusbar.remove(self.cxtid_linecnt, self.msgid_linecnt) self.msgid_linecnt = self.statusbar.push(self.cxtid_linecnt, '%d Domain(s)' % self.textbuf_input.get_line_count()) class Filter2: """ フィルタをフィルタするツール """ def main(self): """ アプリケーションのメイン処理 """ win = MainWindow() win.show_all() gtk.main() if __name__ == '__main__': app = Filter2() app.main()
なお、「PyGTKでダイアログを出しつつ親ウィンドウも操作できるようにする」でも触れているように、gtk.main_iteration()の戻り値はメインループがgtk.main_quit()で終了するまではFalseで、その後はTrueとなる。
関連記事:
- Pythonのジェネレータについて
- PyGTKでのプログレスバーについて(シンプルなGUIダウンローダを作成)
- 外部プロセスをバックグラウンド実行しつつ、出力をGTK+のテキストビューにリアルタイム表示する
- ドメインの名前解決チェックを行うGUIツールをPyGTKで作成
- PyGTKでダイアログを出しつつ親ウィンドウも操作できるようにする
参考URL: