PyGTKでのプログレスバーについて(シンプルなGUIダウンローダを作成)
「urllibのダウンロードで進行状況を表示、キャッシュをせずに直接ファイルに保存」のコードをもとに、PyGTKで入力欄に入れたURLをダウンロードしてファイルに保存するシンプルなダウンローダを作成した。
コード
#! /usr/bin/python # -*- encoding: utf-8 -*- import urllib import locale import time import sys import os try: import pygtk pygtk.require('2.0') except: pass try: import gtk import gobject except: sys.exit(1) if gtk.pygtk_version < (2,4,0): print >> sys.stderr, 'Error: PyGTK 2.4.0 or later required' sys.exit(1) locale.setlocale(locale.LC_TIME, 'C') # 最終更新日時(英語)解析のため def run_errordialog(message): """ 渡されたメッセージを使用してエラーダイアログを表示 PyGTK 2.4.0以上が必要 """ errdlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK, message_format=message) errdlg.set_title('Error') errdlg.set_markup(message) errdlg.run() errdlg.destroy() def run_warningdialog(message): """ 渡されたメッセージを使用して警告ダイアログを表示 PyGTK 2.4.0以上が必要 """ errdlg = gtk.MessageDialog(type=gtk.MESSAGE_WARNING, buttons=gtk.BUTTONS_OK) errdlg.set_title('Warning') errdlg.set_markup(message) errdlg.run() errdlg.destroy() class MyURLopener(urllib.FancyURLopener): """ 進行状況を表示しながらURLをファイルに直接ダウンロードして タイムスタンプも処理する """ version = 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)' mtime_remote = None # 「Last-Modified: 」ヘッダが無い場合の値とする length = -1 # 同様にヘッダの無い場合のための値とする downloaded = 0 # ダウンロード済みバイト数 download_done = False # ダウンロード完了フラグ cancelled = False # 停止ボタンが押されたかどうか def set_localfile_timestamp(self): """ ダウンロードしたファイルの最終更新日時を サーバの「Last-Modified」ヘッダに合わせる """ try: # 「Sun, 29 Jun 2008 12:34:56 GMT」の形式 new_timestamp = time.mktime(time.strptime(self.mtime_remote, u'%a, %d %b %Y %H:%M:%S GMT')) - time.timezone except ValueError: run_warningdialog('Failed to parse "Last-Modified" header for URL "%s"' % self.url) return try: os.utime(self.filename, (new_timestamp, new_timestamp)) # 場所, (アクセス, 更新) except OSError: run_warningdialog('Failed to set timestamp: %s' % localfile) def retrieve(self, url, filename, pgbar, btn_start, btn_stop, entry, bufsiz=4096): """ 進行状況を表示しながらURLをファイルに直接ダウンロード データのキャッシュは行わない 上書きされたメンバ関数 """ self.url = url self.filename = filename try: f_in = self.open(self.url) except IOError: run_errordialog('Cannot open URL "%s"' % self.url) yield False # 長さとタイムスタンプをヘッダから取得 for l in str(f_in.info()).splitlines(): if 'Content-Length: ' in l: self.length = float(l[len('Content-Length: '):]) # 都合によりfloat化 elif 'Last-Modified: ' in l: self.mtime_remote = l[len('Last-Modified: '):] # 出力ファイルを開く try: f_out = open(self.filename, 'wb') except IOError: run_errordialog('Cannot write file "%s"' % self.filename) yield False # ボタンの状態を変更し、停止ボタンだけが押せるようにする btn_start.set_sensitive(False) btn_stop.set_sensitive(True) entry.set_sensitive(False) # 一定時間ごとにプログレスバーを更新するタイマーを開始 gobject.timeout_add(150, self.update_progressbar, pgbar) # PyGTK FAQ 23.20. # How do I update a progress bar and do some work at the same time # http://faq.pygtk.org/index.py?req=show&file=faq23.020.htp # データを溜めながら少しずつファイルに書き出していく while True: try: data = f_in.read(bufsiz) # データをバッファ分までダウンロード if not data: break f_out.write(data) # 読んだ分だけファイルに書き込む self.downloaded += len(data) if self.cancelled == True: # キャンセルされた場合 raise IOError else: yield True # 停止ボタンが押されていなければループ続行 except IOError: f_in.close() f_out.close() try: os.remove(self.filename) # 途中までのファイルを消す except OSError: pass yield False # その後の処理を止める f_in.close() # (全てダウンロードしたら)接続を切断 f_out.close() # 出力ファイルを閉じる self.download_done = True # Last-Modifiedヘッダがある場合にローカルファイルのタイムスタンプを変更 if self.mtime_remote: self.set_localfile_timestamp() # ボタンの状態を戻す btn_start.set_sensitive(True) btn_stop.set_sensitive(False) entry.set_sensitive(True) yield False def update_progressbar(self, pgbar): if self.cancelled == True: return False if self.length != -1: fraction = self.downloaded / self.length pgbar.set_text('%d / %d' % (self.downloaded, self.length)) pgbar.set_fraction(fraction) else: pgbar.set_text('%d' % self.downloaded) pgbar.pulse() # 左右に動かすとき、動かす度に呼ぶ if self.download_done == True: pgbar.set_fraction(1.0) return False else: return True class MainWindow(gtk.Window): """ シンプルなダウンローダ(プログレスバーのテスト) """ def __init__(self, *args, **kwargs): gtk.Window.__init__(self, *args, **kwargs) # ウィンドウ self.set_title('PyGTK Simple Downloader') # ショートカットキー(アクセラレータ) self.accelgroup = gtk.AccelGroup() self.add_accel_group(self.accelgroup) # メニューバー項目 self.menuitem = gtk.MenuItem('_File', True) self.imagemenuitem = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.accelgroup) self.gtkmenu = gtk.Menu() self.gtkmenu.add(self.imagemenuitem) self.menuitem.set_submenu(self.gtkmenu) self.menubar = gtk.MenuBar() self.menubar.append(self.menuitem) # URL用テキストエントリ self.entry = gtk.Entry() # URL用テキストラベル self.label_url = gtk.Label('_URL:') self.label_url.set_use_underline(True) self.label_url.set_mnemonic_widget(self.entry) # 上の2つを横に並べる self.hbox_url = gtk.HBox(False, 0) self.hbox_url.pack_start(self.label_url, False, False, 0) self.hbox_url.pack_start(self.entry, True, True, 0) # 保存先指定 self.chooserbtn = gtk.FileChooserButton('save') self.chooserbtn.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) # 保存先用テキストラベル self.label_save = gtk.Label('_Save in:') self.label_save.set_use_underline(True) self.label_save.set_mnemonic_widget(self.chooserbtn) # 上の2つを横に並べる self.hbox_save = gtk.HBox(False, 0) self.hbox_save.pack_start(self.label_save, False, False, 0) self.hbox_save.pack_start(self.chooserbtn, True, True, 0) # ダウンロード開始ボタン self.btn_start = gtk.Button() self.btn_start.set_label('St_art') self.btn_start.set_use_underline(True) # ダウンロード停止ボタン self.btn_stop = gtk.Button() self.btn_stop.set_label('S_top') self.btn_stop.set_use_underline(True) self.btn_stop.set_sensitive(False) # 押せない状態 # 上の2つを横に並べる self.hbox_dl = gtk.HBox(False, 0) self.hbox_dl.pack_start(self.btn_start, True, True, 0) self.hbox_dl.pack_start(self.btn_stop, True, True, 0) # プログレスバー self.pgbar = gtk.ProgressBar() self.pgbar.set_text('Ready') self.pgbar.set_pulse_step(0.1) #self.pgbar.set_orientation(gtk.PROGRESS_RIGHT_TO_LEFT) # 右から左へ # メニュー、水平ボックス、プログレスバーを縦に並べる self.vbox = gtk.VBox(False, 0) self.vbox.pack_start(self.menubar, False, False, 0) self.vbox.pack_start(self.hbox_url, False, False, 0) self.vbox.pack_start(self.hbox_save, False, False, 0) self.vbox.pack_start(self.hbox_dl, False, False, 0) self.vbox.pack_start(self.pgbar, False, False, 0) self.add(self.vbox) # シグナルを手動で接続 self.entry.connect('activate', self.on_entry_activated) self.btn_start.connect('clicked', self.on_btn_start_clicked) self.btn_stop.connect('clicked', self.on_btn_stop_clicked) self.imagemenuitem.connect('activate', gtk.main_quit) self.connect('delete_event', gtk.main_quit) def on_entry_activated(self, widget): """ URLのテキストエントリでEnterを押したときに 「開始ボタンがクリックされた」と通知する """ self.btn_start.emit('clicked') def on_btn_start_clicked(self, widget): """ ダウンロード開始ボタンが押されたときの処理 """ # URL文字列を取得 url = self.entry.get_text() # URLが空の場合は処理しない if url == '' or url.isspace(): return # 保存ファイル名 name = os.path.basename(url) if name == '': # 「/」の後ろの名前がない場合はその前の文字列を使用 name = url[len('http://'):] file = os.path.join(os.getcwd(), name.replace('/', '')) self.opener = MyURLopener({}) # PyGTK FAQ 23.20. # How do I update a progress bar and do some work at the same time # http://faq.pygtk.org/index.py?req=show&file=faq23.020.htp g = self.opener.retrieve(url, file, self.pgbar, widget, self.btn_stop, self.entry) gobject.idle_add(g.next) # メインループより優先度を下げて実行 def on_btn_stop_clicked(self, widget): """ ダウンロード停止ボタンが押されたときの処理 """ widget.set_sensitive(False) self.btn_start.set_sensitive(True) self.opener.cancelled = True self.entry.set_sensitive(True) class PyGTKSimpleDownloader: """ シンプルなダウンローダ(プログレスバーのテスト) """ def main(self): """ アプリケーションのメイン処理 """ win = MainWindow() win.show_all() gtk.main() if __name__ == '__main__': app = PyGTKSimpleDownloader() app.main()
(2009/1/13)Windowsでデータがファイル書き出し時に破損する不具合を修正(ファイルへの書き込みモードをバイナリモードに修正・えす氏に感謝)
(2009/4/22)メインウィンドウのクラス継承や例外処理の微調整
使い方
入力欄にURLを入力してEnterキーもしくは「Start」のボタンを押すと、ファイルがダウンロードされて指定ディレクトリに保存される。
進行状況は約0.15秒ごとに更新されるようにしている。
ループごとに進行状況を更新するようにした場合...
下は作成途中の段階でのコードだが、これを実行すると、プログレスバーの更新が滑らかで重たくなる。上のコードとの違いは、少しずつ「ダウンロード + 書き込み」の処理をループで行っているところで、毎回のループごとに更新するようにしているところ。こうなってしまってもやはりまずい。
def retrieve(self, url, filename, pgbar, btn_start, btn_stop, entry, bufsiz=4096): """ 進行状況を表示しながらURLをファイルに直接ダウンロード データのキャッシュは行わない 上書きされたメンバ関数 """ self.url = url self.filename = filename try: f_in = self.open(self.url) except IOError: run_error_dialog('Cannot open URL "%s"' % self.url) yield False # 長さとタイムスタンプをヘッダから取得 for l in str(f_in.info()).splitlines(): if "Content-Length: " in l: self.length = float(l[len("Content-Length: "):]) # 都合によりfloat化 elif "Last-Modified: " in l: self.mtime_remote = l[len("Last-Modified: "):] # 出力ファイルを開く try: f_out = open(self.filename, "wb") except IOError: run_error_dialog('Cannot write file "%s"' % self.filename) yield False # ボタンの状態を変更し、停止ボタンだけが押せるようにする btn_start.set_sensitive(False) btn_stop.set_sensitive(True) entry.set_sensitive(False) # PyGTK FAQ 23.20. # How do I update a progress bar and do some work at the same time # http://faq.pygtk.org/index.py?req=show&file=faq23.020.htp # データを溜めながら少しずつファイルに書き出していく while True: data = f_in.read(bufsiz) # データをバッファ分までダウンロード if not data: break f_out.write(data) # 読んだ分だけファイルに書き込む self.downloaded += len(data) # 直接ここにプログレスバー更新のコードを書くと # ループごとに更新を行うことになるため # 滑らかになる一方、CPUの使用率が非常に高くなる if self.length != -1: fraction = self.downloaded / self.length pgbar.set_text("%d / %d" % (self.downloaded, self.length)) pgbar.set_fraction(fraction) else: pgbar.set_text("%d" % self.downloaded) pgbar.pulse() # 左右に動かすとき、動かす度に呼ぶ if self.cancelled == True: # キャンセルされた場合 f_in.close() f_out.close() os.remove(self.filename) # 途中までのファイルを消す yield False # その後の処理を止める else: yield True # 停止ボタンが押されていなければループ続行 f_in.close() # (全てダウンロードしたら)接続を切断 f_out.close() # 出力ファイルを閉じる self.download_done = True # Last-Modifiedヘッダがある場合にローカルファイルのタイムスタンプを変更 if self.mtime_remote: self.set_localfile_timestamp() # ボタンの状態を戻す btn_start.set_sensitive(True) btn_stop.set_sensitive(False) entry.set_sensitive(True) yield False
関連記事: