15 KiB
PyQt6 Scaffold
A wrapper for PyQt6 designed for a more convenient workflow.
License
This project is licensed under the LGPLv3 license. See the LICENSE file for details.
Description
This library provides a set of ready-to-use patterns and tools that can be immediately integrated into development to avoid common pitfalls in complex areas (such as database management or window navigation).
The library is divided into two main modules:
- Core
- Contrib
Installation
Base installation (core only):
pip install pyqt6-scaffold
With MySQL support:
pip install pyqt6-scaffold[mysql]
With PostgreSQL support:
pip install pyqt6-scaffold[postgres]
All drivers at once:
pip install pyqt6-scaffold[all]
Quick Start
1. Configure Environment Variables
Create a start.sh (Linux/macOS) or start.cmd (Windows) file:
# 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. Implement the Database
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. Create Windows
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._password_input.setEchoMode(QLineEdit.EchoMode.Password)
self._submit_btn = QPushButton("Login")
def _tune_layouts(self):
layout = QVBoxLayout()
layout.addWidget(QLabel("Login:"))
layout.addWidget(self._login_input)
layout.addWidget(QLabel("Password:"))
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("Login")
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("Main Window")
4. Assemble the Application in 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()
Core Module
The module providing essential tools for development, usually in the form of abstract classes that implement a convenient pattern. For example, the BaseWindow class implements a minimally acceptable (in the author's opinion) pattern for an application window:
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
The main components of the Core module are the navigation and database interaction classes.
Database
The AbstractDatabase class is responsible for database management within the library.
The class structure looks approximately like this:
This is an abstract class that must be overridden in a subclass. Let's break it down in detail:
- Class Constructor:
- Contains a single argument: strict. This affects how the system reacts when a user opens a new connection, as the default assumption is that the user will use only one connection for database interaction. If strict = True and the class already holds a reference to an existing connection (_conn != None), the execution will fail with an error. If strict = False, the old connection will be closed and the class will create a new connection to the database.
- connect(): A context-independent method for creating a database connection. Internally, it calls the private method
_connect(), which is context-dependent on the SQL dialect and must be overridden by the user in the subclass.- In the
_connect()method, the user implements the specific database connection logic for their chosen SQL dialect. The method should return a connection object, typically resembling:your_sql_dialect_connection.connect(**DB_CONFIG)
- In the
- disconnect(): A context-independent method for manually closing the database connection.
- connection: A getter for the
_connfield, returning the current database connection. - execute(): A context-independent method for executing database code. It handles opening a connection cursor and executing the user's query. It consists of several components:
- CursorContext: A context-independent wrapper class for the cursor object. It is required for compatibility and using the cursor within a context manager, proxying all cursor methods to make it programmatically equivalent to a cursor object.
- placeholder: A context-dependent getter for the SQL dialect's specific placeholder used for value substitution; must be overridden by the user.
- Method Arguments:
execute()takes only 2 arguments:autocommit (bool)andparams (tuple). The first regulates database behavior during data changes—whether to apply them immediately or require the user to do so manually. The second argument passes the values for substitution. - If query execution fails, the method will automatically roll back any changes in the database.
Database Models
As is common knowledge, interaction between the database and the frontend in PyQt6 is implemented through a system of models and views. This library redefines models for tables, lists, and cards, implementing a compatible interface for the database management logic described above.
The model logic in the library is represented by three model classes and one auxiliary mixin class:
- DataMixin A mixin for Qt models (
QAbstractTableModel/QAbstractListModel). It contains shared logic for data storage, model updates, and row styling.DataMixinis used alongside Qt models and is not intended for standalone use. It simplifies the implementation of common model functions so that specific classes can focus on business logic.
Provides methods for:- data storage (
_data) - model reloading (
refresh) - row access (
row_data) - styling (
row_background,row_foreground,row_font)
- data storage (
- BaseTableModel A table model based on a list of row tuples. It supports displaying data in a table with column headers, access to raw data, and cell styling via mixin methods. Intended for scenarios where data is naturally represented as a table (e.g., lists of objects with fixed fields).
- BaseListModel A list model where each entry is represented as a list item. Text display is handled via the
row_display()method. Suitable for simple lists, comboboxes, and other views where a table structure is not required. - BaseCardModel A model for card-based or custom-drawn views.
Does not implementDisplayRole—visualization is delegated to aQStyledItemDelegate. This allows for building complex UI components (cards, tiles, custom lists) while maintaining data access viaUserRole.
Composer
The Composer class is the application's router and window lifecycle manager. It is responsible for window registration, navigation between them, and storing the active database connection. The Composer acts as the application's central hub, through which view switching and context passing occur.
Let's examine its structure:
- Class Constructor:
- Contains 2 arguments: app and db. app is a reference to the PyQt6 event loop object, and db is a reference to the database object.
- Internally initializes 5 fields:
- _db: Stores a reference to the database object.
- _current: Stores a reference to the active window.
- _app: Stores a reference to the PyQt6 event loop object.
- _registry: Stores a reference to the composer's registry, which is a "key-value" store (Python dict) containing mappings between a string name and a window class reference.
- The entry template in the composer registry is defined in the
register()method and looks like this:
{ "class": window, "instance": None, "lazy": lazy }- The class field is a reference to the window class.
- The instance field is a reference to the window object (if created).
- The lazy field is a setting for window object creation. If
lazy = True, the window object is created at the moment of the switch request. Iflazy = False, the object is created immediately during registration. The former is better if the window content is context-dependent.
- The entry template in the composer registry is defined in the
- _current_ctx: Stores the context of the active window as a "key-value" store. The context is unique to each window and is overwritten when switching. It can contain any key-value pairs used for window needs (e.g., if using RBAC, it is logical to store a user rights object here). The context is set during the
Maps()slot call. It is assumed the starting window should not contain a context.
- register(): A method for creating a new entry in the composer registry. If the class passed in the
windowargument is not a subclass ofBaseWindow, a Python exception will be raised. - navigate_request: The composer's only signal. It should be emitted when the user needs to switch windows, passing a reference to the
NavigationRequestdataclass. - navigate(): The method managing window switching. It is a PyQt6 slot and should not be called directly, as it is triggered automatically by the
Maps_requestsignal.- NavigationRequest is a dataclass consisting of
target: strandcontext: NavigationContextfields. It carries the switch request context, which consists of the target window name and the window class reference. In turn, the NavigationContext dataclass consists of a singledatafield which stores the window context.
- NavigationRequest is a dataclass consisting of
Switching between windows is performed via the Composer class's programmatic interface, a reference to which all windows should receive by default (see Window Template above).
Contrib Module
The contrib module contains ready-to-use implementations for common use cases. Unlike core, contrib is context-dependent and provides specific solutions that a student can use as-is or as a foundation.
Backends
Ready-made implementations of AbstractDatabase for three SQL dialects:
from pyqt6_scaffold.contrib import PostgresqlDatabase, MysqlDatabase, SqliteDatabase
Each backend reads configuration from environment variables:
| Class | Environment Variables |
|---|---|
| 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
Authorization system based on RBAC (Role-Based Access Control):
from pyqt6_scaffold.contrib.auth import RBACMixin, Role, RoleLevel
Role and RoleLevel — a dataclass and an enum for describing user roles:
from pyqt6_scaffold.contrib.auth import Role, RoleLevel
from pyqt6_scaffold import BaseUser
role = Role(name="Manager", level=RoleLevel.MANAGER.value)
user = BaseUser(id=1, name="Ivan", role=role)
RBACMixin — a mixin that adds the can() method to your database.
It assumes the existence of a permission_map table in the database:
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);
...
Usage:
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 if user.role.level >= 50
db.can(user, "delete.products") # False if user.role.level < 100
Table and column names can be overridden:
class AppDatabase(RBACMixin, MysqlDatabase):
permission_table = "rights"
permission_column = "action"
level_column = "required_level"