Недавно появилась необходимость и желание ознакомится с PyGTK. Литературы на русском по данному вопросу практически нет, а то, что находит гугл в разных блогах — бывает немного устаревшим. Также с удивлением обнаружил, что и на хабре тема PyGTK не особо популярна.
Итак, не буду рассказывать про компоновку элементов интерфейса, ибо такие статьи уже есть. Расскажу про следующий шаг: создание приложения, которое выполняет некую работу, в процессе отображая свой прогресс.
К примеру, напишем примитивный GUI для двух функций утилиты convert (пакет ImageMagick). Наша программа будет принимать четыре значения:
Сам интерфейс создадим в glade. Важно, проект должен быть в формате GtkBuilder.
Далее два примера кода, первый — обыкновенный, а второй, с созданием отдельной нити для обработки изображений. Примера два — чтобы наглядно убедится, есть ли смысл возится с нитями.
Скелет программы:
Добавим основной метод on_start, который отображает диалог прогресбара, получает указание пользователем значения, генерирует список файлов (каталоги исключаем) и непосредственно занимается обработкой.
Отображаем наше диалоговое окно, загружаем значения с GUI и генерируем список файлов. Далее запускаем цикл с перебором списка files.
Первым делом мы проверяем, не стоит ли стоп флаг указывающий на прекращение обработки if self.stop (устанавливается методом on_progressdialog_close по кнопке Отмена или закрытию диалогового окна). Далее, в процессе меняем текст и процент обработки в прогресбаре, а также запускаем саму утилиту convert с нужными параметрами.
Важный кусок кода
Без него, наше диалоговое окно просто не отобразится, а сам интерфейс зависнет до завершения обработки. Это происходит потому, что наш цикл блокирует перерисовку интерфейса (главный цикл) до своего завершения. Вышеуказанный код, на небольшое время возвращает управление главному циклу.
Также, я специально добавил time.sleep(5), а то если у кого-то быстрый компьютер, может и не заметить, что во время непосредственно обработки (или sleep) интерфейс не реагирует на события.
С нитями в PyGTK, по незнанию пришлось повозится. В начале надо вызвать gtk.gdk.threads_init(), а все дальнейшие вызовы gtk надо обрамлять gtk.threads_enter() и gtk.threads_leave(), такая вот специфика.
Наш код немного изменился. Мы создали словарь self.dialog, который нам пригодится ниже, а метод on_start, теперь только подготавливает данные и запускает новую нить, в которой и происходит непосредственная обработка не блокирующая интерфейс нашей программы.
Создадим класс Worker:
Нам пришлось переопределить конструктор (__init__) дабы он принимал параметры. Это словари dialog (я говорил, он пригодится), data (размер, качество, и два каталоги) и список файлов.
В метод run помещаем то, что необходимо выполнять при старте — т.е. саму обработку.
Вот и все, как говорится «Результат на лицо».
Хочу заметить, в данном примере, расчет процента выполнения и его изменение для диалога, происходить непосредственно в рабочей нити. Это считается плохим тоном, и если бы нить была не одна — так бы не сработало (точнее работало бы с ошибками).
Немного расширенная версия нашей программы выглядит так
Почитать про возможности и скачать можно тут.
Архив с исходниками.
UPD:
alex3d напомнил, что если соответствовать мануалу, в Worker.run строки
нужно обернуть в
gtk.threads_enter() / gtk.threads_leave().
Итак, не буду рассказывать про компоновку элементов интерфейса, ибо такие статьи уже есть. Расскажу про следующий шаг: создание приложения, которое выполняет некую работу, в процессе отображая свой прогресс.
К примеру, напишем примитивный GUI для двух функций утилиты convert (пакет ImageMagick). Наша программа будет принимать четыре значения:
- размер, к какому уменьшить изображение
- качество
- каталог с самими изображениями
- каталог куда сохранять
Сам интерфейс создадим в glade. Важно, проект должен быть в формате GtkBuilder.
Далее два примера кода, первый — обыкновенный, а второй, с созданием отдельной нити для обработки изображений. Примера два — чтобы наглядно убедится, есть ли смысл возится с нитями.
Скелет программы:
#!/usr/bin/python
# coding: utf-8
try:
import sys, pygtk
pygtk.require('2.0')
except:
print 'Не удалось импортировать модуль PyGTK'
sys.exit(1)
import gtk, os, time
class GUI(object):
def __init__(self):
self.wTree = gtk.Builder()
# Загружаем наш интерфейс
self.wTree.add_from_file("convert.glade")
# присоединяем сигналы
self.wTree.connect_signals(self)
self.window1 = self.wTree.get_object("window1")
self.progressdialog = self.wTree.get_object("progressdialog")
self.progressbar_label = self.wTree.get_object("progressbar_label")
self.window1.show()
# Значеия по умолчанию
self.wTree.get_object("size").set_value(100)
self.wTree.get_object("quality").set_value(95)
self.progressbar = self.wTree.get_object("progressbar")
def on_cancel(self,widget):
gtk.main_quit()
def on_progressdialog_close(self, *args):
self.stop = True
self.progressdialog.hide()
return True
if __name__ == "__main__":
app = GUI()
gtk.main()
Добавим основной метод on_start, который отображает диалог прогресбара, получает указание пользователем значения, генерирует список файлов (каталоги исключаем) и непосредственно занимается обработкой.
def on_start(self,widget):
self.progressdialog.show()
self.stop = False
# Значения с GUI
self.size = int(self.wTree.get_object("size").get_value())
self.quality = int(self.wTree.get_object("quality").get_value())
self.from_dir = self.wTree.get_object("from_dir").get_current_folder()
self.to_dir = self.wTree.get_object("to_dir").get_current_folder()
files = []
all_files = os.listdir(self.from_dir)
for f in all_files:
fullname = os.path.join(self.from_dir, f)
if os.path.isfile(fullname):
files.append(f)
count = len(files)
i = 1.0
for file in files:
# Остановка
if self.stop:
break
self.progressbar_label.set_text(file)
self.progressbar.set_fraction(i/count)
os.popen('convert -resize ' + str(self.size) + ' -quality ' + str(self.quality) + ' ' + os.path.join(self.from_dir, file) + ' ' + os.path.join(self.to_dir, file))
# Обновляем диалоговое окно
while gtk.events_pending():
gtk.main_iteration()
time.sleep(5)
i += 1
self.progressdialog.hide()
Отображаем наше диалоговое окно, загружаем значения с GUI и генерируем список файлов. Далее запускаем цикл с перебором списка files.
Первым делом мы проверяем, не стоит ли стоп флаг указывающий на прекращение обработки if self.stop (устанавливается методом on_progressdialog_close по кнопке Отмена или закрытию диалогового окна). Далее, в процессе меняем текст и процент обработки в прогресбаре, а также запускаем саму утилиту convert с нужными параметрами.
Важный кусок кода
while gtk.events_pending():
gtk.main_iteration()
Без него, наше диалоговое окно просто не отобразится, а сам интерфейс зависнет до завершения обработки. Это происходит потому, что наш цикл блокирует перерисовку интерфейса (главный цикл) до своего завершения. Вышеуказанный код, на небольшое время возвращает управление главному циклу.
Также, я специально добавил time.sleep(5), а то если у кого-то быстрый компьютер, может и не заметить, что во время непосредственно обработки (или sleep) интерфейс не реагирует на события.
Нити
С нитями в PyGTK, по незнанию пришлось повозится. В начале надо вызвать gtk.gdk.threads_init(), а все дальнейшие вызовы gtk надо обрамлять gtk.threads_enter() и gtk.threads_leave(), такая вот специфика.
#!/usr/bin/python
# coding: utf-8
try:
import sys, pygtk
pygtk.require('2.0')
except:
print 'Не удалось импортировать модуль PyGTK'
sys.exit(1)
import threading, gtk, os, time
class GUI(object):
def __init__(self):
self.wTree = gtk.Builder()
# Загружаем наш интерфейс
self.wTree.add_from_file("convert.glade")
# присоединяем сигналы
self.wTree.connect_signals(self)
self.window1 = self.wTree.get_object("window1")
self.dialog = {
"progressdialog" : self.wTree.get_object("progressdialog"),
"progressbar_label" : self.wTree.get_object("progressbar_label"),
"progressbar" : self.wTree.get_object("progressbar")
}
self.window1.show()
# Значеия по умолчанию
self.wTree.get_object("size").set_value(100)
self.wTree.get_object("quality").set_value(95)
def on_cancel(self,widget):
gtk.main_quit()
def on_progressdialog_close(self, *args):
self.work.stop()
self.dialog['progressdialog'].hide()
return True
def on_start(self,widget):
self.dialog['progressdialog'].show()
# Значения
self.data = {
'size' : int(self.wTree.get_object("size").get_value()),
'quality': int(self.wTree.get_object("quality").get_value()),
'from_dir' : self.wTree.get_object("from_dir").get_current_folder(),
'to_dir' : self.wTree.get_object("to_dir").get_current_folder()
}
files = []
all_files = os.listdir(self.data['from_dir'])
for f in all_files:
fullname = os.path.join(self.data['from_dir'], f)
if os.path.isfile(fullname):
files.append(f)
self.work = Worker(self.dialog, self.data, files)
self.work.start()
if __name__ == "__main__":
gtk.gdk.threads_init()
app = GUI()
gtk.gdk.threads_enter()
gtk.main()
gtk.gdk.threads_leave()
Наш код немного изменился. Мы создали словарь self.dialog, который нам пригодится ниже, а метод on_start, теперь только подготавливает данные и запускает новую нить, в которой и происходит непосредственная обработка не блокирующая интерфейс нашей программы.
Создадим класс Worker:
class Worker(threading.Thread):
# События нити
stopthread = threading.Event()
def __init__ (self, dialog, data, files):
threading.Thread.__init__(self)
self.dialog = dialog
self.data = data
self.files = files
def run(self):
count = len(self.files)
i = 1.0
for file in self.files:
# Остановка по событию
if ( self.stopthread.isSet() ):
self.stopthread.clear()
break
self.dialog['progressbar'].set_fraction(i/count)
self.dialog['progressbar_label'].set_text(file)
os.popen('convert -resize ' + str(self.data['size']) + ' -quality ' + str(self.data['quality']) + ' ' + os.path.join(self.data['from_dir'], file) + ' ' + os.path.join(self.data['to_dir'], file))
time.sleep(2)
i += 1
# Очищаем события
self.stopthread.clear()
# Скрываем диалоговое окно
self.dialog['progressdialog'].hide()
def stop(self):
self.stopthread.set()
Нам пришлось переопределить конструктор (__init__) дабы он принимал параметры. Это словари dialog (я говорил, он пригодится), data (размер, качество, и два каталоги) и список файлов.
В метод run помещаем то, что необходимо выполнять при старте — т.е. саму обработку.
Вот и все, как говорится «Результат на лицо».
Хочу заметить, в данном примере, расчет процента выполнения и его изменение для диалога, происходить непосредственно в рабочей нити. Это считается плохим тоном, и если бы нить была не одна — так бы не сработало (точнее работало бы с ошибками).
Немного расширенная версия нашей программы выглядит так
Почитать про возможности и скачать можно тут.
Архив с исходниками.
UPD:
alex3d напомнил, что если соответствовать мануалу, в Worker.run строки
self.dialog['progressbar'].set_fraction(i/count)
self.dialog['progressbar_label'].set_text(file)
нужно обернуть в
gtk.threads_enter() / gtk.threads_leave().