Мне нужен был инструмент. Острый, практичный, универсальный. Отвечающий всем моим требованиям и расширяемый по моему желанию.
Но простой и удобный. Тут надо отметить, что на основной работе я не разработчик, поэтому постоянной среды программирования на рабочем компе не имею и, когда это требуется, пишу на чем придется — 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-е наверняка тоже есть способы, но я не рыл.
ini = QSettings(iniFile, QSettings.IniFormat)
Чтобы не было в будущем проблем с русским языком, будем работать в UTF-8 во всех файлах.
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()
И, наконец, начитываем запрос.
В разделе «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-файла, и запустит с ним модуль
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.
Автор: sshmakov