21 KiB
PyQt6 Scaffold
Обёртка для PyQt6, делающая работу удобнее
Лицензия
Этот проект лицензирован под лицензией LGPLv3. Для подробностей ознакомьтесь с файлом LICENSE.
Описание
Эта библиотека предоставляет набор готовых паттернов и инструментов, которые можно сразу использовать для разработки и избежать ошибок в сложных местах (вроде базы данных или навигации между окнами)
Библиотека делится на 2 больших модуля:
- Core
- Contrib
Установка
Базовая установка (только ядро):
pip install pyqt6-scaffold
С поддержкой MySQL:
pip install pyqt6-scaffold[mysql]
С поддержкой PostgreSQL:
pip install pyqt6-scaffold[postgres]
Все драйверы сразу:
pip install pyqt6-scaffold[all]
Быстрый старт
1. Настройте переменные окружения
Создайте файл start.sh (Linux/macOS) или start.cmd (Windows):
# start.sh
export MYSQL_HOST=localhost
export MYSQL_PORT=3306
export MYSQL_USER=root
export MYSQL_DATABASE=mydb
export MYSQL_PASSWORD=secret
python main.py
:: start.cmd
set MYSQL_HOST=localhost
set MYSQL_PORT=3306
set MYSQL_USER=root
set MYSQL_DATABASE=mydb
set MYSQL_PASSWORD=secret
python main.py
2. Реализуйте базу данных
from pyqt6_scaffold import BaseUser
from pyqt6_scaffold.contrib import MysqlDatabase
class AppDatabase(MysqlDatabase):
def auth(self, login: str, password: str):
with self.execute(
f"SELECT id, name FROM users WHERE login = {self.placeholder} AND password = {self.placeholder}",
(login, password)
) as cursor:
row = cursor.fetchone()
if not row:
return None
return BaseUser(id=row[0], name=row[1], role=None)
3. Создайте окна
from pyqt6_scaffold import BaseWindow
from PyQt6.QtWidgets import QLabel, QLineEdit, QPushButton, QVBoxLayout, QWidget
class LoginWindow(BaseWindow):
def _define_widgets(self):
self._login_input = QLineEdit()
self._password_input = QLineEdit()
self._pas3. Create Windowssword_input.setEchoMode(QLineEdit.EchoMode.Password)
self._submit_btn = QPushButton("Войти")
def _tune_layouts(self):
layout = QVBoxLayout()
layout.addWidget(QLabel("Логин:"))
layout.addWidget(self._login_input)
layout.addWidget(QLabel("Пароль:"))
layout.addWidget(self._password_input)
layout.addWidget(self._submit_btn)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
def _connect_slots(self):
self._submit_btn.clicked.connect(self._on_submit)
def _apply_windows_settings(self):
self.setWindowTitle("Вход")
def _on_submit(self):
user = self._db.auth(
self._login_input.text(),
self._password_input.text()
)
if user is None:
return
from pyqt6_scaffold import NavigateRequest, NavigationContext
self._composer.navigate_request.emit(
NavigateRequest(
target="main",
context=NavigationContext(data={"user": user})
)
)
class MainWindow(BaseWindow):
def _apply_windows_settings(self):
self.setWindowTitle("Главное окно")
4. Соберите приложение в main.py
import sys
from PyQt6.QtWidgets import QApplication
from pyqt6_scaffold import Composer
def main():
app = QApplication(sys.argv)
db = AppDatabase()
db.connect()
composer = Composer(app=app, db=db)
composer.register("login", LoginWindow)
composer.register("main", MainWindow)
sys.exit(composer.run(start="login"))
if __name__ == "__main__":
main()
Модуль Сore
Модуль, предоставляющий основные инструменты для работы, как правило это абстрактные классы, реализующие удобный шаблон для разработки. Например, класс BaseWindow реализующий минимально приемлемый (по мнению автора) шаблон для окна приложения:
from PyQt6.QtWidgets import QMainWindow
class BaseWindow(QMainWindow):
def __init__(self, composer, db):
super().__init__()
self._db = db
self._composer = composer
self._define_widgets()
self._tune_layouts()
self._connect_slots()
self._apply_windows_settings()
def _define_widgets(self): pass
def _tune_layouts(self): pass
def _connect_slots(self): pass
def _apply_windows_settings(self): pass
Главные составляюшие модуля Core это классы навигации и взаимодействия с базой данных.
База Данных
За управление базой данных в библиотеке отвечает класс AbstractDatabase.
Этот класс выглядит примерно так:
Это абстрактный класс который необходимо переопределить в классе наследнике. Разберём его подробнее:
- Конструктор класса:
- Содержит всего один аргумент: strict, он влияет на то, как реагировать на открытие пользователем нового соединения, так как по умолчанию предполагается что пользователь будет использовать только одно соедниение для взаимодействия с базой данных. Если strict = True, то в случае если класс уже хранит ссылку на существующее соедниение (_conn != None), то выполнение кода завершится с ошибкой. Если strict = False, то старое соединение закроется и класс создаст новое соедниение с базой данных.
- connect() это контекстно независимый метод для создания соединения с базой данных. Внутри себя он вызывает приватный метод _connect(), который является контекстно зависимым от диалекта SQL и должен быть переопределён пользователем в классе наследнике
- В методе _connect() пользователь реализует специфичную для используемого им диалекта SQL логику подключения к базе данных. Метод должен возвращать объект содениения, как правило он выглядит как:
your_sql_dialect_connection.connect(**DB_CONFIG)
- В методе _connect() пользователь реализует специфичную для используемого им диалекта SQL логику подключения к базе данных. Метод должен возвращать объект содениения, как правило он выглядит как:
- disconnect() это контекстно независимый метод для ручного закрытия соединения с базой данных
- connection это геттер для поля _conn, возвращает текущее соедниение с базой данных.
- execute() это контекстно независимый метод для выполнения кода базы данных, состоит из открытия курсора соединения и выполнения запроса пользователя через курсор, состоит из нескольких компонентов:
- CursorContext - контекстно независимый класс-обёртка над объектом курсора, необходим для совместимости и использовании курсора в контекстном менеджере, проксирует все методы курсора делая его программно эквивалентом объекта курсора
- placeholder - контекстно зависимый геттер для placeholder'а специфичного диалекта SQL для подстановки значений, должен быть переопределён пользователем.
- Аргументы метода: execute() принимает всего 2 аргумента: autocommit(bool) и params(tuple). Первый регулирует поведение базы данных при изменении данных - применять ли их сразу или просить пользтвателя сделать это вручную. Второй аргумент передаёт значения для подстановки.
- В случае если выполнение запроса завершится с ошибкой, метод автоматически откатит изменения в базе данных (если они были)
Модели базы данных
Как известно, взаимодействия между базой данных и фронтендом в PyQt6 реализовано через систему моделей (model) и представлений (view), в данной библиотеке заного определены модели для таблиц, списков и карточек, реализующие совместимый интерфейс для логики организации управления базами данных данной библиотеки, описанной выше.
Логика моделей в библиотеке представлена 3-мя классами модели и одним вспомогательным классом миксином:
- DataMixin
Миксин для моделей Qt (QAbstractTableModel / QAbstractListModel). Содержит общую логику хранения данных, обновления модели и стилизации строк. Миксин DataMixin используется совместно с Qt-моделями и не предназначен для самостоятельного использования. Он упрощает реализацию общих функций модели, чтобы конкретные классы сосредоточились на бизнес-логике. Предоставляет методы для:- хранения данных (_data)
- перезагрузки модели (refresh)
- доступа к строкам (row_data)
- стилизации (row_background, row_foreground, row_font)
- BaseTableModel
Табличная модель, основанная на списке кортежей (row tuples). Поддерживает отображение данных в таблице с заголовками колонок, доступ к сырым данным и стилизацию ячеек через методы миксина. Предназначена для сценариев, где данные естественно представимы в виде таблицы (например, списки объектов с фиксированными полями). - BaseListModel
Листовая модель, в которой каждая запись представлена как элемент списка. Отображение текста выполняется через метод row_display(). Подходит для простых списков, комбобоксов и других представлений, где не требуется табличная структура. - BaseCardModel
Модель для карточных или кастомно отрисованных представлений.
Не реализует DisplayRole — визуализация делегируется QStyledItemDelegate. Позволяет строить сложные UI-компоненты (карточки, плитки, кастомные списки), сохраняя доступ к данным через UserRole.
Composer
Класс-композитор — это маршрутизатор приложения и менеджер жизненного цикла окон. Он отвечает за регистрацию окон, навигацию между ними и хранение активного соединения с базой данных. Composer выступает центральным хабом приложения, через который происходит переключение представлений и передача контекста.
Разберём его устройство:
- Конструктор класса:
- Содержит 2 аргумента: app и db. app это ссылка на объект цикла событий PyQt6, db это ссылка на объект базы данных.
- Внутри себя инициализирует 5 полей:
- _db - хранит ссылку на объект базы данных
- _current - хранит ссылку на активное окно
- _app - хранит ссылку на объект цикла событий PyQt6
- _registry - хранит ссылку регистр композитора, который в свою очередь хранит базу данных формата "ключ-значение" (словарь/dict python), содержащую сопоставление строкового названия окна и ссылку на класс окна.
- Шаблон записи в регистре композитора определён в методе register() и выглядит так:
{ "class": window, "instance": None, "lazy": lazy }- Поле class это ссылка на класс окна
- Поле instance это ссылка на объект окна (если он создан)
- Поле lazy это настройка создания объекта окна. Если lazy = True, тогда объект окна создаётся в момент запроса на переключение на это окно. Если lazy = False, тогда объект окна создаётся сразу при регистрации окна в регистре композитора. Первый вариант больше подходит если содержимое окна контекстно зависимо.
- _current_ctx - хранит контекст активного окна, представляет собой базу данных формата "ключ-значение" (словарь/dict python). Контекст уникален для каждого окна и перезаписывается при переключении окон. Может содержать любые записи "ключ-значение", информация из контекста используется для нужд окна (например, если пользователь использует RBAC как систему аутентификации, логично хранить объект с правами пользователя в контексте окна). Контекст задаётся во время вызова слота navigate(). Предполагается что стартовое окно контекст содержать не должно
- register() - метод создающий новую запись в регистре композитора. Если класс передающийся в аргумент window не является наследником BaseWindow, произойдёт исключение Python.
- navigate_request - единственный сигнал класса композитора, эмит сигнала должен происходить в случае если пользователю требуется переключить окно, передаёт ссылку на датакласс NavigationRequest.
- navigate() - метод управляющий переключением окон. Является слотом PyQt6 и напрямую не должен вызываться, так как вызывается автоматически при эмитте сигнала navigate_request.
- NavigationRequest это датакласс, который состоит из полей target: str и context: NavigationContext, и передаёт контекст запроса на переключение окон, состоящий из строкового названия окна и ссылки на класс окна (и возможно объекта класса окна). В свою очередь, датакласс NavigationContext состоит из одного поля data, которое хранит контекст окна.
Переключение между окнами осуществляется через программный интерфейс класса композитора, ссылку на объект которого по умолчанию додлжны получить все окна (Шаблон окна выше).
Модуль Contrib
Модуль contrib содержит готовые реализации для типовых случаев. В отличие от core, contrib контекстно зависим и предполагает конкретные решения которые пользователь может использовать как есть или взять за основу.
Backends
Готовые реализации AbstractDatabase для трёх диалектов SQL:
from pyqt6_scaffold.contrib import PostgresqlDatabase, MysqlDatabase, SqliteDatabase
Каждый backend читает конфигурацию из переменных окружения:
| Класс | Переменные окружения |
|---|---|
| PostgresqlDatabase | PG_HOST, PG_PORT, PG_USER, PG_DATABASE, PG_PASSWORD |
| MysqlDatabase | MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_DATABASE, MYSQL_PASSWORD |
| SqliteDatabase | SQLITE_PATH |
Auth
Система авторизации на основе RBAC (Role-Based Access Control):
from pyqt6_scaffold.contrib.auth import RBACMixin, Role, RoleLevel
Role и RoleLevel — датакласс и enum для описания ролей пользователя:
from pyqt6_scaffold.contrib.auth import Role, RoleLevel
from pyqt6_scaffold import BaseUser
role = Role(name="Менеджер", level=RoleLevel.MANAGER.value)
user = BaseUser(id=1, name="Иван", role=role)
RBACMixin — миксин добавляющий метод can() в вашу базу данных.
Предполагает наличие таблицы permission_map в базе данных:
CREATE TABLE permission_map (
perm VARCHAR(50),
min_level INT
);
INSERT INTO permission_map VALUES ('edit.products', 50);
INSERT INTO permission_map VALUES ('delete.products', 100);
INSERT INTO permission_map VALUES ('view.products', 0);
...
Использование:
from pyqt6_scaffold.contrib import MysqlDatabase
from pyqt6_scaffold.contrib.auth import RBACMixin
class AppDatabase(RBACMixin, MysqlDatabase):
def auth(self, login, password): ...
db = AppDatabase()
db.connect()
db.can(user, "edit.products") # True если user.role.level >= 50
db.can(user, "delete.products") # False если user.role.level < 100
Названия таблицы и полей можно переопределить:
class AppDatabase(RBACMixin, MysqlDatabase):
permission_table = "rights"
permission_column = "action"
level_column = "required_level"