Как стать автором
Обновить

Точим себе инструмент на PyQt

Время на прочтение7 мин
Количество просмотров76K
Мне нужен был инструмент. Острый, практичный, универсальный. Отвечающий всем моим требованиям и расширяемый по моему желанию.

image

Но простой и удобный. Тут надо отметить, что на основной работе я не разработчик, поэтому постоянной среды программирования на рабочем компе не имею и, когда это требуется, пишу на чем придется — bat, JScript, VBA в MSOffice (да, это Windows, корпоративные системы, тут нет bash и perl «из коробки»), макросы в разном ПО и т.д. Все это помогает решить текущую задачу, но уровень и возможности маленько не те, что хотелось бы иметь.

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

Вы скажете, что сейчас есть инструменты на любой вкус и цвет, только выбирай. Лягушка aka TOAD под Oracle, SoapUI для шины и продукты GNU и Apache для всего остального.
Но проблема в том, что все они они специализированы под одну какую-то деятельность, а с другой стороны слишком универсальны — можно сделать многое, но многими действиями. А если возможность в продукте отсутствует, то добавить ее нельзя. Либо продукт закрытый, либо нужно разрабатывать/покупать плагин, либо качать исходники и в них разбираться. А мне нужен был инструмент, в котором простые действия делаются просто, а на сложные сначала тратится немного времени и дальше опять все просто.

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



В качестве языка программирования нужно взять что-то не требующее компиляции, либо с минимальными затратами на неё, чтобы можно было легко перестроить под конкретную задачу.

Javascript хорош для небольших скриптов и подошел бы, но он не имеет оконного интерфейса, а локально поднимать NodeJS ради окошек и сражаться с браузером мне не интересно.
Perl, PHP — та же проблема.

Visual Basic и VBScript — ну, это под Windows. Да, большинство систем корпоративного ИТ, где я имею честь работать, это Windows. И на каждой есть Офис и, следовательно, VBA. Но уж если делать что-то, чем захочется постоянно пользоваться, то кроссплатформенное.

Выбор пал на Python+PyQt5. О существовании языка я узнал (помимо Хабра, конечно) от малинки Raspberry Pi, где Python был предустановлен. Пробой пера послужил бот для Telegram, ищущий синонимы фраз (на pymorphy2 и YARN, потом опишу, если интересно). А Qt я уже знал.

pip3 install pyqt5

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

Подключим PyQt. Именования в Qt строгие, поэтому импортируем все подряд, мешать не будет.

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

Чтобы сообщения об ошибках и предупреждения Qt не терялись, подключим модуль с message handler, как предложено здесь

import meshandler

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

import dbpool

Создадим класс на основе QDialog (QWidget тоже подойдет, но в нем default кнопки не работают)


class PyExecutor(QDialog):
  def __init__(self, iniFile, parent=None):
    super(PyExecutor, self).__init__(parent)
    self.setWindowFlags(self.windowFlags()
       | Qt.WindowMinimizeButtonHint
       | Qt.WindowMaximizeButtonHint
       )

Заполним окно, сверху вниз


 self.topLay = QVBoxLayout(self)
 self.topLay.setContentsMargins(6,6,6,6)

Макет с местом для ввода параметров и кнопками


 self.lay = QFormLayout()
 self.topLay.addLayout(self.lay)

Место для вывода результата


 self.resultLay = QVBoxLayout()
 self.topLay.addLayout(self.resultLay)

И статусная строка, чтобы было


 self.bar = QStatusBar(self)
 self.topLay.addWidget(self.bar)

Загрузим ini-файл. Загрузку вынесем в отдельный метод, чтобы потом можно было его перекрыть, если понадобится.


   self.loadIni(iniFile)

 def loadIni(self, iniFile):

Для работы с ini-файлами я пользуюсь средствами Qt просто потому, что знаю, как там это делается. В Python-е наверняка тоже есть способы, но я не рыл. Чтобы не было в будущем проблем с русским языком, будем работать в UTF-8 во всех файлах.


 ini = QSettings(iniFile, QSettings.IniFormat)
 ini.setIniCodec("utf-8")

Загружаем параметры запроса из раздела «Input»


 ini.beginGroup("Input")
 for key in sorted(ini.childKeys()):

Параметр определяется строкой «Name=Метка: значение по умолчанию»
Название можно опустить вместе с двоеточием, тогда в интерфейсе будет Name.


 v = ini.value(key).split(':')
 if len(v)>1:
   paramTitle = v[0]
   paramValue = v[1]
 else:
   paramTitle = key
   paramValue = v[0]

На каждый параметр создаем строку ввода, складываем себе в копилочку, вставляем вместе с меткой в интерфейс


 self.params.append([key, paramTitle, paramValue])
 if paramTitle != '':
   le = QLineEdit()
   self.inputs[key] = le
   le.setText(paramValue)
   le.paramTitle = paramTitle
   self.lay.addRow(paramTitle, le)

 ini.endGroup()

Начитываем параметры соединения с базой из раздела «DB»


 ini.beginGroup("DB")
 self.dbini = ini.value("DBConnect")
 if self.dbini == "this":
   self.dbini = iniFile
 ini.endGroup()

И, наконец, начитываем текст запроса SQL.

В разделе «Run» либо будет ключ «SQL» с самим текстом запроса (его лучше взять в кавычки), либо будет ключ «SQLScript», в котором прописан sql-файл с запросом — это дает возможность создавать многострочные запросы. К тому же запросы в файле удобнее редактировать в FAR с подсветкой Colorer.

Как и ini, считаем, что sql-файл находится в кодировке UTF-8, только для перекодировки будем пользоваться 'utf-8-sig', чтобы избавиться от BOM в начале файла.


  ini.beginGroup("Run")
  if ini.contains("SQL"):
    self.sql = ini.value("SQL")
  else:
    f = QFile(ini.value("SQLScript"))
    f.open(QIODevice.ReadOnly)
    self.sql = str(f.readAll(),'utf-8-sig')
  ini.endGroup()

Последние штрихи — добавить кнопку запуска, расположить красиво.


  self.runBtn = QPushButton("Run")
  self.runBtn.setDefault(True)
  self.btnLay = QHBoxLayout()
  self.btnLay.addStretch()
  self.btnLay.addWidget(self.runBtn)
  self.lay.addRow(self.btnLay)

Кнопке назначим наш метод, запускающий запрос на выполнение


  self.runBtn.clicked.connect(self.run)

Собственно метод запуска


def run(self):
  self.runBtn.setEnabled(False) #Отключим кнопку, что на нее не нажали второй раз
  self.clearResult() #Почистим предыдущие результаты, если были

Поехали работать с базой данных.

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


self.db = dbpool.openDatabase(self.dbini)
if self.db == None or not self.db.isValid() or not self.db.isOpen():
  print("No opened DB", self.dbini)
  self.endRun()
  return

В Qt по сути один способ работы с запросами БД — это QSqlQuery


self.query = QSqlQuery(self.db)

Парсим sql-запрос, заполняем его параметры значениями из строк ввода


self.query.prepare(self.sql)
for p in self.params:
  key = p[0]
  if key in self.inputs:
    le = self.inputs[key]
    par = ':'+key
    self.query.bindValue(par, le.text())

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


  self.tr = QueryRunner(self.query)
  self.tr.finished.connect(self.showQueryResult)
  self.tr.start();

После завершения потока выполнится этот метод


def showQueryResult(self):

Создадим табличку QTableView такую, как нам надо


  w = self.createTableView()

Но модель с результатом запроса передадим во view не сразу, а через прокси — это даст нам возможность сортировать табличку нажатием на столбец и сделать поиск, если понадобится.


  w.sqlModel = QSqlQueryModel(w)
  w.sqlModel.setQuery(self.query)
  w.proxyModel = QSortFilterProxyModel(w)
  w.proxyModel.setSourceModel(w.sqlModel)
  w.setModel(w.proxyModel)
  self.resultLay.addWidget(w)
  self.endRun()

Сделаем запуск того, что получилось, чтобы проверить без оболочки


if __name__ == '__main__':
  # Немного магии для Windows
  import os
  import PyQt5
  import sys

  pyqt = os.path.dirname(PyQt5.__file__)
  QApplication.addLibraryPath(os.path.join(pyqt, "Qt", "plugins"))

И собственно запуск


  app = QApplication(sys.argv)
  ex = PyExecutor("artists.ini")
  ex.show()
  sys.exit(app.exec_())

Файл artists.ini

[Common]
Title=Поиск артиста

[Input]
Name=Имя артиста(маска):%r%

[DB]
DBConnect=sqlite.ini

[Run]
SQL="SELECT * FROM artists where :Name = '' or artist like :Name"

Проверили — работает



Теперь нам нужна собственно оболочка запуска.

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

Для простоты будем использовать MDI-окно, благо в Qt все для этого есть. Чтение дерева и его отображение взято полностью из примера PyQt, поэтому на нем останавливаться не будем.

Только определим, что в первом столбце у нас название функции, выводимое в строке дерева, во второй — описание функции, в третьей — ini-файл, передаваемый модулю

Тесты Тестовые папки
Поиск артистов Поиск артистов по маске artists.ini

Покажу, как создаем основное окно на QMainWindow


class MainWindow(QMainWindow):
 def __init__(self, parent=None):
   super(MainWindow, self).__init__(parent)

Основная часть MDI-окна — это специальный widget QMdiArea. В нем будут жить окна запускаемых модулей.


 self.mdiArea = QMdiArea(self)
 self.setCentralWidget(self.mdiArea)

Сделаем главное меню, пока с одним пунктом:


 self.mainMenu = QMenuBar(self)
 self.setMenuBar(self.mainMenu)
 m = self.mainMenu.addMenu("Window")
 a = m.addAction("Cascade windows")
 a.triggered.connect(self.mdiArea.cascadeSubWindows)

Дерево будет в dock-панели слева.


 self.treePanel = QDockWidget("Дерево задач", self)
 w = QWidget(self.treePanel)
 self.treePanel.setWidget(w)
 lay = QVBoxLayout(w)
 lay.setSpacing(1)
 lay.setContentsMargins(1,1,1,1)
 w.setLayout(lay)
 self.tree = TreeWidget(self.treePanel)
 lay.addWidget(self.tree)

В нижней части будет выводится описание функции (потом)


 edit = QTextEdit(w)
 lay.addWidget(edit)

На дабл-клик в дереве назначим обработчик и посадим панель в основное окно


 self.tree.activated.connect(self.handle_dblclick)
 self.addDockWidget(Qt.LeftDockWidgetArea, self.treePanel)

Обработчик дабл клика возьмет из модели дерева имя ini-файла, и создаст с ним класс из модуля. Наш класс является виджетом, его вставляем в клиентскую часть MDI-окна.


 def handle_dblclick(self, index):
   proc = index.data(Qt.UserRole)
   if proc != None:
     proc = proc.strip()
     ex = PyExecutor(proc)
     self.mdiArea.addSubWindow(ex)
     ex.show()

Проверяем — работает:



Исходники выложены на github под лицензией MIT. Ссылка ведет на исходники, использованные для статьи, а с корня можно взять последнюю версию.

Подсказки:
1. В PyQt, как и в Qt не входит бинарный драйвер QOCI, нужный для доступа к Oracle. Драйвер надо собрать из исходников (C++), поставляемых с Qt, и положить в PyQt5\Qt\plugins\sqldrivers. Для сборки вам понадобятся dll из Oracle Client.
2. Python лучше ставить так, что путь к нему не содержал кириллицы. Иначе у PyQt маленько съезжает крыша, и он не может найти свои файлы.

Продолжение:
Лепим тулбар на PyQt, экспортируем данные в Excel и HTML
Режем XML по разметке XQuery
Preview документов в программе на Python
Теги:
Хабы:
Всего голосов 28: ↑26 и ↓2+24
Комментарии25

Публикации

Истории

Работа

Python разработчик
121 вакансия
QT разработчик
4 вакансии
Data Scientist
78 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань