Разработка → Прогресбар и нити в PyGTK

Strange 17 октября 2009 в 22:55 1,6k
Недавно появилась необходимость и желание ознакомится с PyGTK. Литературы на русском по данному вопросу практически нет, а то, что находит гугл в разных блогах — бывает немного устаревшим. Также с удивлением обнаружил, что и на хабре тема PyGTK не особо популярна.

Итак, не буду рассказывать про компоновку элементов интерфейса, ибо такие статьи уже есть. Расскажу про следующий шаг: создание приложения, которое выполняет некую работу, в процессе отображая свой прогресс.

К примеру, напишем примитивный GUI для двух функций утилиты convert (пакет ImageMagick). Наша программа будет принимать четыре значения:
  • размер, к какому уменьшить изображение
  • качество
  • каталог с самими изображениями
  • каталог куда сохранять

Сам интерфейс создадим в glade. Важно, проект должен быть в формате GtkBuilder.

Glade GUI


Далее два примера кода, первый — обыкновенный, а второй, с созданием отдельной нити для обработки изображений. Примера два — чтобы наглядно убедится, есть ли смысл возится с нитями.

Скелет программы:

#!/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 помещаем то, что необходимо выполнять при старте — т.е. саму обработку.

Вот и все, как говорится «Результат на лицо».

Хочу заметить, в данном примере, расчет процента выполнения и его изменение для диалога, происходить непосредственно в рабочей нити. Это считается плохим тоном, и если бы нить была не одна — так бы не сработало (точнее работало бы с ошибками).

Немного расширенная версия нашей программы выглядит так

Simple Images Converter


Почитать про возможности и скачать можно тут.

Архив с исходниками.

UPD:
alex3d напомнил, что если соответствовать мануалу, в Worker.run строки
self.dialog['progressbar'].set_fraction(i/count)
self.dialog['progressbar_label'].set_text(file)

нужно обернуть в
gtk.threads_enter() / gtk.threads_leave().
Проголосовать:
+36
Сохранить: