Пример разработки небольшого python+PyQt4 приложения для учетной системы

    Часто приходится разрабатывать приложения для корпоративной системы которые должны были функционировать еще вчера, не требующие строго соответствия корпоративным стандартам. Такими приложениями могут представлять cms к сайтам, gui для сервисов под *nix системы просто приложением учетной системы. Разработка приложений подобного рода на скриптовых языках есть тема! обычно оптимальна с точки зрения скорости выполнения. Под катом пример реализации приложения на python+PyQt4, функции приложения парсинг и загрузка данных из xls файлов...
    Для того чтобы приготовить яичницу я использовал:
    1. Python2.5
    2. PyQt-4.4.3
    3. kinterbasdb3 (БД КИС — Firebird)
    4. xlrd-0.6
    В качестве редактора всем рекомендую emacs python-mode (это касается и
    виндопользователей):

    emsc python-mode

    1. Достаем посуду из шкафа (хранение настроек)


    Вообще для скриптовых языков такой вопрос не стоит так как, вы можете завести файл настроек типа settings.py и спокойно запускать его на уровне кода:
    # -*- coding: cp1251 -*-
    host='localhost/3051:intur'
    user='SYSDBA'
    path='d:\\dlogs\\scanner\\xls'

    oprts = (
    u'НАТАЛИ',
    u'ЭКСПРЕСС',
    u'ИНТАЕР',
    u'ЛАНТА',
    u'КОРАЛ',
    )

    Получение переменных в программе выглядит так:
    mod = __import__(modName)
    host = getattr(mod, 'host', 'horn:intur')
    user = getattr(mod, 'user', 'SYSDBA')

    2.Рисование форм


    Qt Software — вообще красавцы с точки зрения документации, или если сравнивать тулзы для рисования GUI — то здесь они тоже молодцы. Главное окошко программы я нарисовал в QtDesigner + отметил сигналы с виджетов, которые нужно обрабатывать.
    emsc python-mode
    Для того чтобы быстро собирать формы в дизайнере безусловно нужно немного поднатореть в понимание механизмов формирования Layout и Spacers. Существует два варианта поднятия окошка в приложения, либо вы генерируете файл необходимом для вас языке программирования, либо с помощью спец.модулей Qt строите классы из xml-файлов описания. Я обычно генерирую файл, не могу сказать что это чем-то лучше, просто мне это кажется более удобным. Для питона PyQt поставляет pyuic.bat который просто нужно скопировать в директорию вашего проекта и генерировать классы-обработчики окошек по мере вашей работы в дизайнере
    У меня это для главного окна выглядит так:

           pyuic.bat -o mwnd.py mwnd.ui

    В отличие от wx Qt генеруют не наследники классов главных окон, а классы- модификаторы которые получают в качестве аргумента базовый класс окна и «вешают» на него ваши контролы. При генерации на с++ Qt использует множественное наследование класс вашего приложения становится наследником класса базового окна и класса-модификатора. Интересно что при генерации файлов слоты pyuic вешаются на базовый класс, отсюда несоответствие — обработчики сигналов находятся в наследнике базового класса окна, а ссылки на контролы этого же окна находятся у класса-модификатора сгенерированного pyuic.

        def _initApp(self):
            self.mwnd, self.mwnd.ui = MainWnd(), Uwnd()
            self.mwnd.ui.setupUi(self.mwnd)
    

    Я поступил просто: связал класс-модификатор с наследником базового класса окна, получилось просто при необходимости обращения к контролам общаюсь к ним как — self.ui.«name». Вроде очевидная штука, но как часто это бывает пришла ко мне в голову не с первого раза.

    ! Еще интересно что в питон не переваривает ключевые слова в названиях атрибутов и методов классов, поэтому вы не можете завести переменную self.pass или self.exec(). Отсюда и смешная надпись в каждой PyQt приложении sys.exit(app.exec_())

    3. Ставим скороводку на плиту, или запуск приложения


    Вот код main.py — его задачи запуститься, загрузить файл настроек, запросить пароль и в случае удачного подключения запустить главное окно приложения

    main.py

    4. Не забудем положить яица в яичницу.


    Наследник главного окна приложения (MainWnd) выполняет функции отслеживания поведения пользователя, здесь кстати видны слоты которые мы сгенерировали в дизайнере, здесь основная задача кода максимально прозрачные и простые функции управления окном для основного класса приложения выполняющего загрузку файла в БД.
    Класс выполняющий основную функцию приложения — парсинг и загрузку данных (MainModel) не имеет смысла без главного окна поэтому сделаем его атрибутом окна. Для парсинга xls файлов в питоне есть тривиальная в использовании, и тем замечательная библиотека xlrd, работает с файлом напрямую не поднимая громоздких OLE машин, и поэтому еще и более быстродейственна. Приложение позволяет загружать в режиме только измененных файлов, поэтому часть когда MainModel, уведено для сравнения дат модификации и последней загрузки, которая хранится в БД. Так кроме главного рабочего класса приложения никто с БД не общается даем ему небольшой класс (dbConn), которые хранит инструкции MainModel в «терминах» sql.

    WndModel.py

    Вообще по науке, есть некая неуклюжесть программы из-за протягивания переменных (например app) с класса Container до классов 2 уровня иерархий. В больших программах таких переменных становится чуть более чем 9000 много, и эта задача решается с помощью синглтона (в следующий раз это я учту).

    В итоге мы получили шаблон приложения, который вы можете использовать в своих целях, выбрасываете WndModel.py, пишите свой наследник базового окна, свой класс реализующий ваши задачи — и вроде бы все теперь оно даже работает.

    P.S. Полный исходный код здесь

    UPD. вынес код на внешний кодохранитель, если кто подскажет как корректно подсветить python-код на Хабре буду благодарен.
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 24
    • НЛО прилетело и опубликовало эту надпись здесь
      • НЛО прилетело и опубликовало эту надпись здесь
        • –4
          (set-language-environment 'UTF-8)
          (setq default-input-method 'russian-computer)

          (set-selection-coding-system 'windows-1251)
          (set-default-coding-systems 'windows-1251)
          (prefer-coding-system 'windows-1251)

          (custom-set-faces
          ;; custom-set-faces was added by Custom.
          ;; If you edit it by hand, you could mess it up, so be careful.
          ;; Your init file should contain only one such instance.
          ;; If there is more than one, they won't work right.
          '(default ((t (:inherit nil :stipple nil :background «gray17» :foreground «snow» :inverse-video nil :box nil :strike-through nil :overline nil :underline nil :slant normal :weight normal :height 98 :width normal :foundry «outline» :family «Courier New»))))
          '(cursor ((t (:background «peach puff»))))
          '(font-lock-comment-delimiter-face ((default (:inherit font-lock-comment-face)) (((class color) (min-colors 16)) (:background «green»))))
          '(font-lock-comment-face ((nil nil)))
          '(font-lock-doc-face ((t (:inherit font-lock-string-face :background «black»)))))

          (setq auto-mode-alist
          (append
          '(
          ( "\\.el$". lisp-mode)
          ( "\\.py$". python-mode)
          ( "\\.html$". sgml-mode)
          ( "\\.xml$". sgml-mode)
          )))

          (global-font-lock-mode 1)

          (autoload 'javascript-mode «javascript» nil t)
          (add-to-list 'auto-mode-alist '("\\.js\\'". javascript-mode))

          ;; tool bar
          (tool-bar-mode -1)

          (set-background-color "#333333")
          (set-foreground-color "#ffffff")

          (setq scroll-step 1)
          (global-hl-line-mode 1)

          (windmove-default-keybindings 'meta)
          (fset 'yes-or-no-p 'y-or-n-p)

          (iswitchb-mode 1)
          (global-set-key [?\C-,] 'previous-buffer)
          (global-set-key [?\C-.] 'next-buffer)

          (put 'upcase-region 'disabled nil)
          (delete-selection-mode 1)

          ;;(setq inhibit-startup-message t)
          (setq default-tab-width 4)

          (desktop-save-mode t)
          (global-set-key [f5] 'call-last-kbd-macro)
          (global-set-key [f11] 'buffer-menu)
          (global-set-key [f10] 'bookmark-bmenu-list)

          (global-set-key [?\C-'] 'toggle-truncate-lines)
          (put 'narrow-to-page 'disabled nil)

          что не понравится (set-language-environment 'UTF-8)
          (setq default-input-method 'russian-computer)

          (set-selection-coding-system 'windows-1251)
          (set-default-coding-systems 'windows-1251)
          (prefer-coding-system 'windows-1251)

          (custom-set-faces
          ;; custom-set-faces was added by Custom.
          ;; If you edit it by hand, you could mess it up, so be careful.
          ;; Your init file should contain only one such instance.
          ;; If there is more than one, they won't work right.
          '(default ((t (:inherit nil :stipple nil :background «gray17» :foreground «snow» :inverse-video nil :box nil :strike-through nil :overline nil :underline nil :slant normal :weight normal :height 98 :width normal :foundry «outline» :family «Courier New»))))
          '(cursor ((t (:background «peach puff»))))
          '(font-lock-comment-delimiter-face ((default (:inherit font-lock-comment-face)) (((class color) (min-colors 16)) (:background «green»))))
          '(font-lock-comment-face ((nil nil)))
          '(font-lock-doc-face ((t (:inherit font-lock-string-face :background «black»)))))

          (setq auto-mode-alist
          (append
          '(
          ( "\\.el$". lisp-mode)
          ( "\\.py$". python-mode)
          ( "\\.html$". sgml-mode)
          ( "\\.xml$". sgml-mode)
          )))

          (global-font-lock-mode 1)

          (autoload 'javascript-mode «javascript» nil t)
          (add-to-list 'auto-mode-alist '("\\.js\\'". javascript-mode))

          ;; tool bar
          (tool-bar-mode -1)

          (set-background-color "#333333")
          (set-foreground-color "#ffffff")

          (setq scroll-step 1)
          (global-hl-line-mode 1)

          (windmove-default-keybindings 'meta)
          (fset 'yes-or-no-p 'y-or-n-p)

          (iswitchb-mode 1)
          (global-set-key [?\C-,] 'previous-buffer)
          (global-set-key [?\C-.] 'next-buffer)

          (put 'upcase-region 'disabled nil)
          (delete-selection-mode 1)

          ;;(setq inhibit-startup-message t)
          (setq default-tab-width 4)

          (desktop-save-mode t)
          (global-set-key [f5] 'call-last-kbd-macro)
          (global-set-key [f11] 'buffer-menu)
          (global-set-key [f10] 'bookmark-bmenu-list)

          (global-set-key [?\C-'] 'toggle-truncate-lines)
          (put 'narrow-to-page 'disabled nil)

          это часть .emacs которая для меня особо необходима. попробуйте.
      • +2
        а не расскажете про упаковку приложения в py2exe? а то с pyqt4 постоянно какие-то проблемы возникают
        • +1
          Обратите внимание на PyInstaller — малоизвестный инструмент, между тем превосходящий по фичам и удобству упомянутый py2exe.

          Один из ключевых моментов — автоматически распознает и подключает PyQt/lxml и прочие библиотеки. Т.е. не надо делать абсолютно никаких телодвижений — оно работает «из коробки».

          Впрочем, для объективности отмечу и минусы — для поддержки .manifest файлов требует патча (не знаю как с этим у py2exe), и лучше использовать SVN-ветку программы т.к. они почему-то уже год с лишним как не хотят выкладывать на публику версии своей утилиты, несмотря на то, что работает отлично и разработка идет, проект не мертв.
        • +7
          Хотелось бы видеть в начале статьи краткую аннотацию с описанием того, что конкретно делает ваша программа.

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

          Какая цель этой статьи? Кому должна быть полезна?

          Тот кто не знает PyQt от такого изложения не поймет ничего. А кто знает, скажет вам, что код ваш откровенно средненького качества.

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

          Если сейчас там есть какие-то достойные моменты — то их не видно в вашей куче. Если нет — то незачем писать статью.

          В данный момент все это похоже на «Я вот сделал за вечер на коленке программку, посмотрите какой я молодец, вот ее исходник».
          • 0
            Я бы такую статью бы с удовольствием прочитал ранее, статья не салат-копипаст. полезна для тех, кто часто пишет комменты типа «скоро буду изучать питон».

            >А кто знает, скажет вам, что код ваш откровенно средненького качества.
            Вы подскажите что не так, я подучусь.

            а скачивание кода сделайте архивом
            а разве я не так сделал.

            А вообще ваш коммент заменить на — «вы мне не нравитесь. я умнее.»
            • 0
              Уважаемый, ну зачем сразу же так агрессивно…

              Я приношу извинения, если тон или стиль моего изложения задел вас, и вовсе не хочу сказать, что имею что-то против вас лично. Напротив, выложить свой код на суд общественности — смелое решение и в полной мере достойно уважения.

              Лишь хочу сказать, что в данной статье не хватает (помимо подсветки кода) некоторой центральной идеи.

              Говорю о том, что читателю вряд ли нужна точно такая программа, как есть у вас. Читатель хочет научиться чему-то, каким-то принципам, каким-то приемам.

              Потому полезно ткнуть пальцем «вот тут сделано то-то так-то потому, что...». А учиться лишь по одному исходнику способны гики, которые вряд ли обучаются программированию на хабре.
              • +1
                На вас не обижаюсь, даже буду рад если дадите какое-то ценное указание.

                код ваш откровенно средненького качества.
                аргументируйте, хоть как-то.
                • +1
                  Не обижайтесь, пожалуйста, но код похож либо на то, что вы очень торопились, и главное было сделать программу с заданными функциями в заданный срок, либо вы еще изучаете инструментарий.

                  Например участок с конфигурационным файлом. Вместо
                  mod = __import__(modName)
                  host = getattr(mod, 'host', 'horn:intur')
                  user = getattr(mod, 'user', 'SYSDBA')

                  можно использовать простое и понятное
                  import settings
                  … settings.host…
                  … settings.user…

                  Кроме читаемости это будет соответствовать принципам «Simple is better than complex», «Namespaces are one honking great idea» и другим, включенным в The Zen of Python.

                  Так же, например, код:
                  def completeFile(self, i):
                  … widget = self.ui.listWidget
                  … text = widget.item(i).text()
                  … widget.item(i).setText('%s\t%s' % (text, u'готово'))
                  … self.ui.progressBar.setValue(0)


                  Читая его получаем:
                  функция «завершить i-й файл»:
                  … На форме у нас есть некоторый ListWidget, возьмем его
                  … Допишем к тексту i-го элемента в нем слово «готово»
                  … Возьмем некоторый прогрессбар на форме и установлим его прогресс в 0.


                  Отсюда сразу встают вопросы, что за ListWidget, что за элементы в нем хранятся, каково их назначение, и почему функция «завершить файл» вместо того, чтобы завершать этот файл, лишь констатирует факт что файл завершен.

                  Я не претендую на истинность своих слов, это лишь результат быстрой выкладки, но можно было бы переписать так:
                  def file_completed(self, file_index):
                  … file_item = self.ui.file_list.item(file_index)
                  … file_item.setText( '%s\t%s' % (file_item.text(), u'готово') )
                  … self.ui.current_file_progress.setValue(0)


                  Ваш код весьма запутан, а хорошая программа всегда имеет простой (не путать с примитивностью) код, даже если эта программа сама по себе крайне сложна.
                  • –2
                    То что, вы указали весьма спорно:

                    1. В моем случае скрипт запустится при незаполненных переменных host, user. В вашем случае он упадет, __import__(modName), позволяет вынести в начало файла название скрипта настроек, у вас он вбит внутри код, что не есть хорошо.

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

                    По-моему код прозрачен, вот сущности выполняющие основные действия:

                    Container — (пре запуск программы, проверка настроек)
                    -->Wnd — (класс окошка и отрисовка виджетов)
                    ----->MainModel — управление парсингом и загрузкой
                    --------->DbConn — контейнер sql, и подключение к БД.

                    • +2
                      1. Если меняется название скрипта настроек, то имеет смысл обратиться к модулю ConfigParser и вынести настройки в ini-файл, оставив при этом в покое сам модуль конфигурационного файла.

                      2. Идеи правильного именования вы найдете в таких трудах, как «Совершенный код» (глава 11), «Domain driven development» и многих других.

                      Позволю себе привести отрывок из первой упомянутой книги:
                      Имя переменной нельзя выбирать, как кличку собаке:… В отличие от собаки и ее клички, которые являются разными сущностями, переменная и ее имя формируют по идее одну сущность. Поэтому и адекватность переменной во многом определяется ее именем. Выбирайте имена переменных со всей тщательностью.


                      Попробуйте читать код после обфускатора, если считаете что стиль и имена переменных не имеют значения.
                      • –4
                        2. Найдите хоть одну здравую программу на питоне которая использует ini-файлы.

                        1. имена переменных и виджетов абсолютно разные вещи возьмите любой gui-туториал от qt, и посмотрите там имена виджетов. Это не принципиально потому что, в на форме может быть сотня виджетов и важдому присваивать имя это никому не нужная работа.
                        Имена переменных в моем коде все корректны.

                        • +1
                          1. Давайте не доводить до абсурда. Если вам не нравится формат INI — сделайте в XML, YAML или любом другом формате, который вам нравится.

                          Суть не в конкретном формате, а в том, что есть модуль Settings, в котором есть параметры.

                          Откуда они там берутся — это проблема модуля settings и никакого другого модуля программы.

                          2. Про переменные даже отвечать не буду. Я вам привел ссылки на литературу, считаю что этого достаточно.
                          • –4
                            абсурд — это ваши замечания, объективно почему получился код средненький, как написать лучше вы не сказали.
          • +8
            Статья содержит пару идеологических просчётов:
            1. Исходники в cp1251. Что не самое страшное, пока код не выпускается из ваших рук.
            2. Просчёт похуже: скриншоты текста в jpg

            это не xkcd, не ищете текста alt
            • +1
              Какая прелесть :)
              • +1
                эта прелесть — регулярный гость на хабре :-)
            • +2
              В принципе пост хороший, но уж больно «галопом по Европам». Пример написания гуевого приложения, работающего с БД, рассчитаный на новичковую аудиторию, имхо, лучше бы расписать подетальнее и пообширнее — и это задача не из простых. Не менее интересны и комментарии к тексту!
              • 0
                Ссылки с кодом не рабочие…
                • 0
                  на гитхабе выложить не можете? а то ссылки уже на рабочие

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

                  Самое читаемое