Разговариваем про PyQt4 — Посиделка вторая

    image

    Добро пожаловать!


        В прошлый раз мы обсуждали, как можно писать свое PyQt4-приложение, опираясь на логику сгенерированного программкой pyuic4 файла. Как это часто бывает — после написания топик получил много интересных и, что самое главное, содержательных комментариев, объясняющих, почему в отдельных случаях я прав, а в других неправ.
        Самое любопытное состоит еще и в том, что обсуждение интересно как для питонистов, так и для приверженцев C++, ибо в данном случае разница невелика, в основном только незначительные вещи в синтаксисе. Все это потому, что PyQt4, по своей сути, является простой оберткой вокруг сишных Qt-классов, сохраняющей все названия и методы. Итак, вот вам чашечка чая или кофе, устраивайтесь поудобнее, давайте начнем нашу беседу.

    С места в карьер


        Итак, мы пишем оконное приложение. Акцент здесь на слове "оконное", а это значит, что у нас есть user-friendly интерфейс, позволяющий конечному пользователю оценить всю прелесть щелканья по кнопочкам. Таким образом, консолью мы пользуемся для отладки и отлова ошибок. Но зачем консоль юзеру, у которого уже есть красивое окошко? Тут можно выбирать, что делать для удобства — вести лог работы (этот способ как раз любит Microsoft, собирая в логи все самое интересное и спрашивая, отправлять ли отчет о работе приложения) или перенаправить вывод консоли на виджет на форме, чтобы видеть, с чем имеешь дело. И если первое делается довольно легко:
    Copy Source | Copy HTML
    1. import sys
    2. # открываем файлы для записи с возможностью аппендинга нужной информации
    3. sys.stdout = open("log.txt", "a+")
    4. sys.stderr = open("errors.txt", "a+")

    то со вторым у многих возникают вопросы. На самом деле отгадка скрыта как раз в этом примере. Что мы имеем? File-like-объект, позволяющий добавление строк в конец. Это делается, насколько мы помним из работы с файлами в Python, с помощью метода write(). Так давайте сделаем объект с методом write() и передадим ему в качестве параметра выводной виджет!
    Copy Source | Copy HTML
    1. class Logger(object):
    2.     def __init__(self, output):
    3.         self.output = output
    4.  
    5.     def write(self, string):
    6.         if not (string == "\n"):
    7.             trstring = QtGui.QApplication.translate("MainWindow", string.strip(), None, QtGui.QApplication.UnicodeUTF8)
    8.             self.output.append(trstring)

    при этом зачастую приходится отрезать от строки все ненужное и делать проверку на символ переноса каретки, чтобы половину лога не занимали пустые строки.
        Теперь, чтобы перенаправить вывод консоли в виджет, нам надо прописать его в классе окна и привязать через класс логгера к стандартному выводу:
    Copy Source | Copy HTML
    1. self.logText = QtGui.QTextEdit(MainWindow)
    2. # даем виджету свойство read-only
    3. self.logText.setReadOnly(True)
    4. # делаем полосу вертикальной прокрутки видимой всегда, на мой взгляд, так удобнее
    5. self.logText.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
    6. # начальное сообщение
    7. self.logText.append("Log started")
    8. # можно как перенаправлять все в один виджет, так и разделить ошибки и 
    9. # стандартный вывод
    10. self.logger = Logger(self.logText)
    11. self.errors = Logger(self.logText)
    12. sys.stdout = self.logger
    13. sys.stderr = self.errors

        Теперь мы можем писать в лог двумя способами — либо (в классе окна) с помощью self.logger.write(), либо просто с помощью print. Таким образом, сделав красивый вывод ошибок при try-except'ах, мы видим все прямо в окне. Тут еще большим плюсом является то самое разделение Python-логики с логикой Qt — форма висит в отдельном потоке и при ошибках во внешних функциях приложение не завершается, а вот ошибка сразу падает в лог. И да, об этом стоит поговорить немного подробнее.

    Чуть-чуть о потоках


        Думаю, вы не раз замечали, что если прописать функцию, требующую много времени и ресурсов на выполнение, то форма «подвиснет» и никаких данных о состоянии функции вы не получите. Я сам столкнулся с этим, когда хотел сделать последовательный вывод возвращаемых из функций значений в список на форме. Решение же очевидное — использовать потоки. Причем перед нами будет стоять выбор — использовать потоки с Python-логикой или же потоки Qt? Лично я в данном случае предпочитаю второе — меньше мороки в некоторых аспектах.
        Для того, чтобы нам было красиво, надо опять создать очередной класс с наследованием от Qt-шного. А что поделать, такая уж «классовая логика» :) Тут свои плюсы и минусы, но сейчас не об этом.
    Copy Source | Copy HTML
    1. class myQThread(QtCore.QThread):
    2.     def __init__(self, output):
    3.         QtCore.QThread.__init__(self)
    4.         self.output = output
    5.  
    6.     def run(self):
    7.         some_big_function_with_output()

        Как видите, сначала мы инициализируем класс Qt-шного потока (я еще добавил в параметры конструктора объект вывода — это как раз может быть тот самый виджет на форме). Затем переопределяем стандартный метод run(), в котором как раз и содержится то, что должен делать поток. Эту функцию или функции можно подключать даже из внешних модулей — в данном случае это не играет роли. Ее вывод в данном случае можно и нужно осуществить через передаваемый функции output с помощью все того же метода append(). Вы можете для пущей красоты добавить прогрессбар или другой визуализатор, чтобы пользователь видел, что поток работает.
        А как мы узнаем, что поток завершил свою работу? Для этого намутим один небольшой хак — мы создадим слот обработчика сигнала от потока, но не будем применять connect, а используем декоратор:
    Copy Source | Copy HTML
    1. @QtCore.pyqtSignature("")
    2. def threadFinished(self):
    3.     self.logger.write("Поток завершен!")

        Да-да, вы правильно поняли, это пишется именно в класс формы, а когда генерируется сигнал threadFinished() — программа сообщает нам об этом. Вот такие пироги.

        Собственно, работу с потоками, слотами и сигналами можно было бы рассмотреть поподробнее — но для начала вполне хватит. Надеюсь, вам понравилось наше маленькое чаепитие, до связи! Пишите комментарии :)
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 18
    • –1
      Давно хотел попробовать что-нибудь отличное от Tk. Спасибо за (чае|кофе)питие :)
      Надеюсь планируются продолжения, например, про аналоги того, что в Tk называется geometry managers?

      А еще в Tk есть отличная коллекция примеров собранных в приложение «widget». Там код на все случаи жизни. В Qt под python есть что-нибудь аналогичное?
      • 0
        Честно — Tk еще как-то попробовать не удалось. Если geometry managers — это схемы расстановки элементов — то да, такое в Qt есть :) А насчет примеров — у PyQt4 там не одно приложение, но в них реально куча полезных вещей. Об этом я еще, думаю, расскажу.
        • 0
          Буду ждать продолжений :)

          P.S. sys.stderr = open(«errors.txt, „a+“) немного странно подсветилось
          • 0
            Потому что не хватило кавычки
      • 0
        В пакеты для разработки обычно включают программу QtDemo — это сборник примеров различной сложности. Там можно посмотреть исходники(C++) и увидеть как они работают.
      • 0
        Если будете писать топик про потоки в Qt, то советую особо выделить и рассмотреть QtConcurent, который является абстракцией над потоками и примитивами синхронизации.
        • 0
          отлично. но вопрос в том, какие потоки лучше использовать при смеси двух тенологий, остается открытым.
          • 0
            Ну, мне кажется, что GUI'шные потоки в основном как раз и предназначены для работы в тех случаях, когда интерфейс может «заморозиться», и они в данном случае более родные, нежели threading.Thread. А вообще работа с потоками в питоне — это темный лес во многих отношениях, причем я имею в виду не конкретно себя, а вообще среднестатистического программиста :) В данном случае каждый выбирает подход сам.
            • 0
              pyobject.ru/blog/2008/05/07/pyqt-unpythonic-gui/
              Вот еще одна статья, автор при всей его склонности к Qt-стилю, тоже использует QThread. Так что вопрос и правда спорный :)
            • 0
              Создавать отдельные потоки, чтобы в цикле не зависал GUI — это несколько странно, ведь нужно всего лишь обработать накопившуюся очередь событий, а для этого есть processEvents()
              • +1
                Позвольте добавить свои пять копеек:
                1. Писать собственный Logger непрактично. Лучше использовать встроенные возможности Qt (qDebug) или Python (logging). И для того и для другого можно определить обработчики сообщений (Qt — qInstallMsgHandler, Python — logging.handlers).

                2. Для потоков лучше написать декоратор, который будет производить всю работу по инициализации и очищению ресурсов. Это вам сильно упростит жизнь — фактически, вы сможете превращать функции из однопоточных в многопоточные, просто приписав к ним этот декоратор. Конечно, при этом они станут асинхронными, но для GUI-приложений зачастую это именно то, что нужно.
                • +3
                  Спасибо за вторую статью цикла.

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

                  Приведенная довольно страшненькая конструкция для перевода выводимого текста будет бесполезна в 99% случаев.
                  trstring = QtGui.QApplication.translate("MainWindow", string.strip(), None, QtGui.QApplication.UnicodeUTF8)

                  Согласен, что от нее может быть иногда толк, но она явно не относится к необходимому для логирования на форму, а перегрузка ненужными деталями затрудняет восприятие примера. К тому же неопытный читатель может так бездумно и вставить все это дело себе в код, так и не поняв что это и зачем оно на самом деле надо.

                  Второе, что хотел бы я отметить: то, что вы пишете — это хорошо и правильно, но только для малых (и с большой натяжкой — для средних) приложений.

                  Для средних же и крупных необходимо отделение логики от представления. В идеале, вся работа с Qt должна быть изолирована либо в модуле gui, либо в package с таким или подобным именем.

                  Ядро же программы желательно должно оставаться девственно чистым от Qt-кода (в том числе Qt-потоков):
                  — Это облегчит тестирование (для того, чтобы провести юнит-тестирование Qt-потока, надо инициализировать QApplication, а для некоторых других вещей — создавать главное окно).
                  — Это позволит снизить сложность ядра программы: разработчику ядра не обязательно держать в голове все требования и особенности Qt-реализаций.
                  — Это позволит легко тестировать программу в консольном режиме.
                  — И, наконец, это позволит держать код в едином стиле, не перемешивая методы с подчеркрутыми_именами (питон-стиль) с теми, что написалы в camelCase, не перемешивая u'Строки в юникоде' с QString-ами.
                  • –1
                    почему использовали QThread, а не родные питоновские thread.Thread?
                    • 0
                      я не питонист и не знаю что умеет питоновский поток, но я почему-то уверен что при их использовании вы поимеете большой гемор, когда вам будут нужны сигналы и слоты в классе потока с исполнением евентЛупа внутри этого потока.
                      • 0
                        сомнительно, доверять потоки gui библиотеке.
                        • 0
                          Qt это не гуй-библиотека, вернее нестолько гуй-библиотека. По сути это фреймворк.
                          Да и как уже было проверено неоднократно мной, потоки кутешные работают адекватно (проверялось в высоконагруженных распределенных приложениях еще на версии Qt3, с тех пор их работа только улучшилась).
                        • +1
                          Нет, не поимеете. Сигналы и слоты это не просто вызов функций, а целый механизм, включающий в себя как прямой вызов слотов, так и отложенный.

                          Вот что написано в этом в документации Qt:
                          enum Qt::ConnectionType

                          This enum describes the types of connection that can be used between signals and slots. In particular, it determines whether a particular signal is delivered to a slot immediately or queued for delivery at a later time.

                          Qt::DirectConnection — When emitted, the signal is immediately delivered to the slot.
                          Qt::QueuedConnection — When emitted, the signal is queued until the event loop is able to deliver it to the slot.
                          Qt::AutoConnection — If the signal is emitted from the thread in which the receiving object lives, the slot is invoked directly, as with Qt::DirectConnection; otherwise the signal is queued, as with Qt::QueuedConnection.


                          Последний вариант используется в Qt по умолчанию. Работоспособность этого дела с нативными питон-нитями была мною уже не раз проверена и активно используется в разработке.

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