Прогресбар и нити в PyGTK

    Недавно появилась необходимость и желание ознакомится с 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().
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 12
    • +2
      Как только добрые люди добавят Вам достаточно кармы, можете перенести в блог PyGTK :-)
      • +1
        Как только, так сразу)
      • +2
        вот отличная статья! Как раз тема с которой я сейчас начал разбираться.
        Странно, питон вроде достаточно популярен, а вот PyGTK (в рунете) не особо. По крайней мере материалов на русском действительно очень мало.
        • 0
          Многие предпочитают wxPython или вообще PyQT, но кажется мне, с литературой там ситуация та же.
          • 0
            wxPython не смотрел, а вот по pyQT все так-же плохо. Причем из-за этого pyGTK освоить все-таки проще.
            Кстати, назрел вопрос. Использование формата GtkBuilder дает какие-то преимущества по сравнению с форматом glade или это упомянуто просто потому что пример использует формат GtkBuilder?
            • +1
              В данном случае, только потому что пример в GtkBuilder.

              Хотя GtkBuilder позиционируется как замена старому libglade и новые проекты рекомендуют начинать на нем. Более широкие функциональные возможности есть, это GtkTreeView и зависимые виджеты (думаю есть еще что-то, с чем я пока не столкнулся).
              • 0
                а вот про функциональные различия я и не знал. Буду иметь в виду теперь.
                Glade мне понравился тем, как там можно описывать события. Удобно передать структуру, вместо нудного перечисления для билдера.
        • +2
          Спасибо всем за карму, перенес в PyGTK.
          • +1
            Мммм, очень сочная статья, спасибо! С нетерпением жду завтрашнего вечера, когда начну ее читать и применять.
            Я как раз пишу программу на PyGTK и мне очень нужны примеры средней сложности. В них проще вникать, чем в монстров типа gajim.

            Карма +1.
            • 0
              Спасибо!
              • +1
                На счёт расчёта процента выполнения и его изменение для диалога в нити — самый простой вариант по-моему, такой чтоб не испортить простоту примера и избавиться от «плохого тона» — передать в конструктор вместо диалога ссылку на callback-функцию, которая в качестве параметра будет принимать текущее значение прогресса и вызываться нитью. Ну а в классе граф. интерфейса соответственно такую создать — которая уже будет обновлять прогресс-бар. По крайней мере это позволит отделить мух от котлет (гуи от обработки). Или такие трюки в нитях с гуи не рекомендуются?
                • 0
                  Это уже правильнее, но если две нити обработки — могу быть неточности.
                  1 нить берет на обработку файл, и в конце должна установить значение 15%
                  2 нить берет на обработку файл, и в конце должна установить значение 20%
                  Если вторая нить отработает быстрее (а такое вполне возможно, в зависимости от размера файла, в данном примере) — сначала установит 20%, а потом 15%.

                  У себя в siconverter я использую callback-функцию, которая запускает метод класса GUI, а он уже с помощью других классов высчитывает значение у устанавливает его.
                  Но я не ручаюсь, что это истинно верный путь :)

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.