How I Use PySide6 in A Pythonic And Comprehensive Way | by Gust Van Mierlo | Medium

Source: How I Use PySide6 in A Pythonic And Comprehensive Way | by Gust Van Mierlo | Medium

6 min read

Aug 28, 2024

Press enter or click to view image in full size

I’m using Python for about 4 years now and I dare say I pretty know how to write ‘pythonic’ and ‘comprehensive’ programs. The greatest guideline is of course PEP 8.

PySide6, Excellent,

I used to work with PySimpleGui, but since the latest version you had to get a license, so I desided to look for another GUI library and found PySide6. It’s very powerfull if you know how to use it, and fortunately it has a very elaborate documentation. I’m using it for about half a year now and I’m getting to feel quite comfortable with it.

But…

What I don’t like about it: although it’s a Python library, it’s far from ‘pythonic’ and not very ‘comprehensive’. By the former I mean it doesn’t follow PEP 8 recommendations (e.g. camelCase methods). By the latter I mean it takes some effort to understand what is being created, when after some time you look at the code you wrote yourself.

Example

from PySide6.QtWidgets import (
    QApplication, QMainWindow, QVBoxLayout, QWidget, QHBoxLayout, QPushButton, QLabel,
    QComboBox, QFrame,
)
from PySide6.QtCore import Qt

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        button_column = QVBoxLayout()
        self.button = QPushButton('Push me')
        self.button.setStyleSheet('color: blue')
        self.button.clicked.connect(self.button_pushed)
        button_column.addWidget(self.button)

        self.button_label = QLabel(self.button_label_text())
        button_column.addWidget(self.button_label)

        combo_column = QVBoxLayout()
        self.combo = QComboBox()
        self.combo.addItems(['1' , '2', '3'])
        self.combo.setCurrentText('1')
        self.combo.currentTextChanged.connect(self.combo_changed)
        combo_column.addWidget(self.combo)

        self.combo_label = QLabel(self.combo_label_text())
        combo_column.addWidget(self.combo_label)

        summary_frame = QFrame()
        summary_frame.setFrameStyle(QFrame.Box | QFrame.Plain)
        title = QLabel('Summary')
        title.setAlignment(Qt.AlignHCenter)
        title.setStyleSheet(f'font-weight:bold; font-size: 12pt')
        summary_frame_layout = QVBoxLayout()
        summary_frame_layout.addWidget(title)
        summary_line = QHBoxLayout()
        self.summary_button_state = QLabel(self.button_label_text())
        self.summary_combo_state = QLabel(self.combo_label_text())
        summary_line.addWidget(self.summary_button_state)
        summary_line.addWidget(self.summary_combo_state)
        summary_frame_layout.addLayout(summary_line)
        summary_frame.setLayout(summary_frame_layout)

        top_layout = QHBoxLayout()
        top_layout.addLayout(button_column)
        top_layout.addLayout(combo_column)

        main_layout = QVBoxLayout()
        main_layout.addLayout(top_layout)
        main_layout.addWidget(summary_frame)
        widget = QWidget()
        # widget.setFixedSize(500, 300)
        widget.setLayout(main_layout)
        self.setCentralWidget(widget)

    def button_label_text(self):
        if self.button.isFlat():
            return "The button is flat"
        return "The button is not flat"

    def combo_label_text(self):
        return f"The combo choise is {self.combo.currentText()}"

    def button_pushed(self):
        self.button.setFlat(not self.button.isFlat())
        self.button_label.setText(self.button_label_text())
        self.summary_button_state.setText(self.button_label_text())

    def combo_changed(self):
        self.combo_label.setText(self.combo_label_text())
        self.summary_combo_state.setText(self.combo_label_text())


app = QApplication()
app.setStyle ('Fusion')
main_window = MainWindow()
main_window.show()
app.exec()

If you look at this code with pythonic glasses, you’ll notice the following:

  • The class definition of the main window is a series of individual statements with no indentations. You have to read them one by one and stay focused to understand how the main window is constructed and how it will look like.
  • It contains a lot of camelCase method calls.

Alternative

Now look at this script:

from PySide6.QtCore import Qt

from py_qt_classes import (
    PQApplication, PQVBoxLayout, PQHBoxLayout, PQWidget, PQMainWindow,
    PQPushButton, PQLabel, PQTitledFrame, PQComboBox,
)


class MainWindow(PQMainWindow):
    def __init__(self):
        super().__init__()
        self.button = PQPushButton(
            'Push me',
            style_sheet='color: blue',
            clicked = self.button_pushed,
        )
        self.button_label = PQLabel(self.button_label_text())
        button_column = PQVBoxLayout(
            [self.button],
            [self.button_label]
        )

        self.combo = PQComboBox(
            items=['1' , '2', '3'],
            current_text='1',
            changed=self.combo_changed
        )
        self.combo_label = PQLabel(self.combo_label_text())
        combo_column = PQVBoxLayout(
            [self.combo],
            [self.combo_label]
        )

        self.summary_button_state = PQLabel(self.button_label_text())
        self.summary_combo_state = PQLabel(self.combo_label_text())
        summary_frame = PQTitledFrame(
            title = PQLabel('Summary'),
            layout=PQHBoxLayout(
                [self.summary_button_state], [self.summary_combo_state],
            )
        )

        main_layout = PQVBoxLayout(
            [button_column, combo_column],
            [summary_frame]
        )
        widget = PQWidget(
            layout=main_layout
        )
        self.set_central_widget(widget)

    def button_label_text(self):
        if self.button.flat:
            return "The button is flat"
        return "The button is not flat"

    def combo_label_text(self):
        return f"The combo choise is {self.combo.currentText()}"

    def button_pushed(self):
        self.button.flat = not self.button.flat
        self.button_label.set_text(self.button_label_text())
        self.summary_button_state.set_text(self.button_label_text())

    def combo_changed(self):
        self.combo_label.set_text(self.combo_label_text())
        self.summary_combo_state.set_text(self.combo_label_text())


app = PQApplication(W

Well, that looks a bit better, doesn’t it? And you don’t even have to examine the imported module py_qt_classes to see what it’s doing.

Get Gust Van Mierlo’s stories in your inbox

Join Medium for free to get updates from this writer.

But let me tell you about this module.

  • The first class I wrote was PQVBoxLayout. It accepts a list of lists as arguments. Each inner list represents a QHBoxLayout, with widgets and/or layouts as elements. So when you see:
main_layout = PQVBoxLayout(
    [button_column, combo_column],
    [summary_frame]
)

it’s kind of a visualisation of the layout.

  • The counterpart PQHBoxLayout is analog, although the visualisation effect is less obvious.
  • The next thing I tackled was the problem that instantiation of a QtWidget doesn’t accept other than the most obvious arguments (such as text for a QLabel). So when you want to define a width, style, callback function…, it takes seperate lines of code, something like widget.setAttribute(…) or widget.signal.connect(…). This also doesn’t visually shows its relationship with the created widget.
  • So for each QWidget class that I used, I wrote a custum class PQWidget(QWidget) with specific arguments for widget-specific attributes and **kwargs for ‘general’ attributes. Example:
class PQComboBox(QComboBox):
    def __init__(
        self,
        items=None,
        current_text=None,
        editable=None,
        changed=None,
        **kwargs
    ):
        super().__init__()
        if items:
            self.addItems(items)
        if current_text:
            self.setCurrentText(current_text)
        if editable:
            self.setEditable(editable)
        if changed:
            self.currentTextChanged.connect(changed)

        set_attributes(self, **kwargs)
  • The last line of the __init__ method is: ‘set_attributes(self, **kwargs)’. This function handles the arguments that are not specific for the widget:
def set_attributes(widget, **attributes):
    for attribute, value in attributes.items():
        match attribute:
            case 'enabled':
                widget.setEnabled(value)
            case 'style_sheet':
                widget.setStyleSheet(value)
            case 'tooltip':
                widget.setToolTip(value)
            case 'width':
                widget.setFixedWidth(int(value))
            case 'height':
                widget.setFixedHeight(int(value))
            case 'maximum_width':
                widget.setMaximumWidth(int(value))
            case 'maximum_height':
                widget.setMaximumHeight(int(value))
            case 'alignment':
                widget.setAlignment(value)
            case _:
                message = (f"Method for {attribute} of {widget} not yet defined.")
                raise AttributeError(message)
  • So now it’s possible to create a widget with it’s attributes as a one-liner, e.g. :
self.combo = PQComboBox(
    items=['1' , '2', '3'],
    current_text='1',
    changed=self.combo_changed
 )

Notice that by doing this you can get rid of the camelCase in your code (except in the module py_qt_classes itself of course).

  • Final note: All this is an evolving process: I created the PQWidgets when I first needed them, with the arguments that I needed at that moment, and also set_attributes contains olny those arguments that I’ve needed so far.
  • So this is the module at this moment:
from PySide6.QtWidgets import (
    QApplication, QLabel, QFrame, QTableWidget, QTableWidgetItem, QPushButton,
    QWidget, QVBoxLayout, QHBoxLayout, QProgressBar, QComboBox, QScrollArea,
    QLayout, QToolBar, QCheckBox, QListWidget, QLineEdit, QTabWidget, QMenu,
    QDialog, QCalendarWidget, QMainWindow
)
from PySide6.QtGui import QIcon, QColor
from PySide6.QtCore import Qt, QRect
from typing import Unpack

def set_attributes(widget, **attributes):
    for attribute, value in attributes.items():
        match attribute:
            case 'enabled':
                widget.setEnabled(value)
            case 'style_sheet':
                widget.setStyleSheet(value)
            case 'tooltip':
                widget.setToolTip(value)
            case 'width':
                widget.setFixedWidth(int(value))
            case 'height':
                widget.setFixedHeight(int(value))
            case 'maximum_width':
                widget.setMaximumWidth(int(value))
            case 'maximum_height':
                widget.setMaximumHeight(int(value))
            case 'alignment':
                widget.setAlignment(value)
            case _:
                message = (f"Method for {attribute} of {widget} not yet defined.")
                raise AttributeError(message)


class PQApplication(QApplication):
    def __init__(self, style=None):
        super().__init__()
        if style:
            self.setStyle(style)


class PQWidget(QWidget):
    def __init__(self, title=None, layout=None, **kwargs):
        super().__init__()
        if title:
            self.setWindowTitle(title)
        if layout:
            self.setLayout(layout)
        set_attributes(self, **kwargs)


class PQMainWindow(QMainWindow):
    def __init__(self, central_widget=None):
        super().__init__()
        if central_widget:
            self.setCentralWidget(central_widget)

    def set_central_widget(self, widget):
        self.setCentralWidget(widget)


class PQToolBar(QToolBar):
    def __init__(self, actions=None, **kwargs):
        super().__init__()
        actions = actions or []
        for action in actions:
            if action == 'separator':
                self.addSeparator()
            else:
                self.addAction(action)
        set_attributes(self, **kwargs)


class PQMenu(QMenu):
    def __init__(self, label, font_size=None, actions=None, **kwargs):
        super().__init__(label)
        if font_size :
            font = self.font()
            font.setPointSize(int(font_size))
            self.setFont(font)
        actions = actions or []
        for action in actions:
            if action == 'separator':
                self.addSeparator()
            else:
                self.addAction(action)
        set_attributes(self, **kwargs)

class PQTabWidget(QTabWidget):
    def __init__(self, tabs=None, **kwargs):
        super().__init__()
        tabs = tabs or {}
        for widget, label in tabs.items():
            self.addTab(widget, label)
        set_attributes(self, **kwargs)


class PQTitledFrame(QFrame):
    def __init__(
        self, title, layout,
        width=None, height=None,
        font_size='10pt',
        **kwargs
    ):
        super().__init__()
        self.title = title
        self.layout = layout
        self.width = width
        self.height = height
        if width:
            self.setFixedWidth(width)
        if height:
            self.setFixedHeight(height)

        self.setFrameStyle(QFrame.Box | QFrame.Plain)
        self.label = PQLabel(
            title,
            alignment=Qt.AlignHCenter,
            style_sheet=f'font-weight:bold; font-size: {font_size}'
        )
        layout.insertWidget(0, self.label)
        self.setLayout(layout)
        set_attributes(self, **kwargs)

class PQTable(QTableWidget):
    def __init__(
            self, rows, columns,
            headers=None,
            items=None,
            item_clicked=None
    ):
        super().__init__(rows, columns)
        if headers:
            self.setHorizontalHeaderLabels(headers)
        self.headers = headers
        items = items or {}
        for row_col, item in items.items():
            row, col = row_col
            self.setItem(row, col, item)
        if item_clicked:
            self.itemClicked.connect(item_clicked)
        self.resizeColumnsToContents()

    def add_row_items(self, row_items, alignment=None):
        assert len(row_items) == self.columnCount()
        self.insertRow(self.rowCount())
        for ind, item in enumerate(row_items):
            if isinstance(item, list):
                table_item = TableItem(
                    ','.join(item),
                    alignment=alignment)
            elif str(item).endswith('png'):
                table_item = TableItem(
                    '',
                    icon=QIcon(item),
                    alignment=alignment
                )
            else:
                table_item = TableItem(
                    str(item),
                    alignment=alignment,
                )
            self.setItem(self.rowCount() -1, ind, table_item)
        self.resizeColumnsToContents()

    def table_width(self):
        return sum(self.columnWidth(col) for col in range(self.columnCount()))


class PQTableItem(QTableWidgetItem):
    def __init__(self, text, alignment=None, icon=None, **kwargs):
        if isinstance(text, list):
            text = ','.join(text)
        super().__init__(text)
        if alignment:
            self.setTextAlignment(alignment)
        if icon:
            self.setIcon(icon)
        set_attributes(super(), **kwargs)

class PQPushButton(QPushButton):
    def __init__(
        self,
        label='',
        icon=None,
        clicked=None,
        **kwargs
    ):
        super().__init__(label)
        if icon:
            self.setIcon(QIcon(icon))
        if clicked:
            self.clicked.connect(clicked)
        set_attributes(self, **kwargs)

    @property
    def flat(self):
        return self.isFlat()

    @flat.setter
    def flat(self, value):
        self.setFlat(value)


class PQCheckBox(QCheckBox):
    def __init__(
            self,
            label='',
            checked=None,
            changed=None,
            **kwargs
    ):
        super().__init__(label)
        if checked:
            self.setChecked(checked)
        if changed:
            self.stateChanged.connect(changed)
        set_attributes(self, **kwargs)



class PQVBoxLayout(QVBoxLayout):
    def __init__(self, *args: Unpack[QWidget | QLayout]):
        super().__init__()

        for row in args:
            layout = QHBoxLayout()
            for element in row:
                if isinstance(element, QHBoxLayout):
                    self.addLayout(element)
                elif isinstance(element, QWidget):
                     layout.addWidget(element)
                else:
                    layout.addLayout(element)
            self.addLayout(layout)


class PQHBoxLayout(QHBoxLayout):
    def __init__(self, *args: Unpack[QWidget | QLayout]):
        super().__init__()

        for row in args:
            layout = QVBoxLayout()
            for element in row:
                if isinstance(element, QVBoxLayout):
                    self.addLayout(element)
                elif isinstance(element, QWidget):
                     layout.addWidget(element)
                else:
                    layout.addLayout(element)
            self.addLayout(layout)


class PQLabel(QLabel):
    def __init__(
        self,
        text,
        **kwargs
    ):
        super().__init__(text)
        set_attributes(self, **kwargs)

    def set_text(self, text):
        self.setText(text)


class PQComboBox(QComboBox):
    def __init__(
        self,
        items=None,
        current_text=None,
        editable=None,
        changed=None,
        **kwargs
    ):
        super().__init__()
        if items:
            self.addItems(items)
        if current_text:
            self.setCurrentText(current_text)
        if editable:
            self.setEditable(editable)
        if changed:
            self.currentTextChanged.connect(changed)

        set_attributes(self, **kwargs)


class PQListWidget(QListWidget):
    def __init__(self, **kwargs):
        super().__init__()

        set_attributes(self, **kwargs)

class PQLineEdit(QLineEdit):
    def __init__(self, text='', changed=None, **kwargs):
        super().__init__()
        if text:
            self.setText(text)
        if changed:
            self.textChanged.connect(changed)

        set_attributes(self, **kwargs)


class PQScrollArea(QScrollArea):
    def __init__(self, widget):
        super().__init__()
        self.setWidget(widget)


class PQDialog(QDialog):
    def __init__(self, title='', **kwargs):
        super().__init__()
        if title:
            self.setWindowTitle(title)
        set_attributes(self, **kwargs)


class PQCalendarWidget(QCalendarWidget):
    def __init__(
            self,
            minimum_date=None,
            maximum_date=None,
            selected_date=None,
            selection_changed=None,
            **kwargs):
        super().__init__()
        if minimum_date:
            self.setMinimumDate(minimum_date)
        if maximum_date:
            self.setMaximumDate(maximum_date)
        if selected_date:
            self.setSelectedDate(selected_date)
        if selection_changed:
            self.selectionChanged.connect(selection_changed)
        set_attributes(self, **kwargs)

Python
Pyside6
Pythonic
GUI

Leave a Reply

The maximum upload file size: 500 MB. You can upload: image, audio, video, document, spreadsheet, interactive, other. Links to YouTube, Facebook, Twitter and other services inserted in the comment text will be automatically embedded. Drop file here