Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

Building Professional Desktop Apps in Python with PySide6

Tech 1

PySide6 is the official Qt for Python binding for Qt 6. It delivers a modern, full‑featured GUI toolkit with a permissive LGPL license that allows closed‑source and commercial distribution. Compared with tkinter, PySide6 offers richer widgets, powerful styling, excellent cross‑platform support, and a mature event system.

Installation and version check

pip install -U pyside6
import sys
import PySide6

print(sys.version.split()[0])            # e.g. 3.11.6
print(PySide6.__version__)               # e.g. 6.x.y

Core Qt modules you’ll use most

from PySide6 import QtCore, QtGui, QtWidgets
  • QtCore: core types, object model, signals/slots, timers, threading
  • QtGui: fonts, colors, icons, images, events, painting
  • QtWidgets: all the visual widgets and layouts

Your first window

import sys
from PySide6 import QtWidgets, QtGui

app = QtWidgets.QApplication(sys.argv)

root = QtWidgets.QWidget()
root.setWindowTitle("Magic Forest")
root.resize(600, 400)
root.move(700, 500)

# A centered label
message = QtWidgets.QLabel("Marisa Kirisame", parent=root)
font = QtGui.QFont()
font.setFamily("STCaiyun")
font.setPointSize(20)
font.setBold(True)
font.setItalic(True)
message.setFont(font)

# center using geometry information
root_geom = root.geometry()
msg_size = message.sizeHint()
message.move(
    (root_geom.width() - msg_size.width()) // 2,
    (root_geom.height() - msg_size.height()) // 2,
)

root.show()
sys.exit(app.exec())

Typical application flow:

# 1) create QApplication
# 2) create top-level widget(s)
# 3) add child widgets, set properties/layouts
# 4) show top-level widget
# 5) enter event loop (app.exec())

Encapsulating a window in a class

import sys
from PySide6 import QtWidgets, QtGui

class MainWindow(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.setup()

    def setup(self):
        self.setGeometry(400, 300, 600, 500)
        self.setWindowTitle("Subterranean Animism")

        title = QtWidgets.QLabel("Ancient Song of the Underworld", self)
        f = QtGui.QFont("STKaiti", 25)
        f.setBold(True)
        title.setFont(f)
        # center using sizeHint (preferred size)
        w, h = self.width(), self.height()
        s = title.sizeHint()
        title.move((w - s.width()) // 2, (h - s.height()) // 2)

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    win = MainWindow()
    win.show()
    sys.exit(app.exec())

QObject essentials: names, dynamic properties, and stylesheets

Every widget is a QObject. You can assign an object name and custom dynamic properties, then target them from Qt stylesheets.

from PySide6.QtCore import QObject

obj = QObject()
obj.setObjectName("summer_festival")
print(obj.objectName())                 # summer_festival

obj.setProperty("author", "Sasha")
print(obj.property("author"))          # Sasha
print(obj.property("missing"))         # None

for p in obj.dynamicPropertyNames():
    print(p.data().decode())            # property keys as strings

Application‑wide stylesheet with selectors

import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel

class Demo(QWidget):
    def __init__(self):
        super().__init__()
        # Apply once at app level
        QApplication.instance().setStyleSheet(
            """
            QLabel#emphasis { font-size: 20px; color: seagreen; font-weight: 700; }
            QLabel[level="warn"] { font-style: italic; }
            """
        )
        self.init_ui()

    def init_ui(self):
        self.setGeometry(400, 400, 300, 200)
        self.setWindowTitle("Summer Festival")

        lbl1 = QLabel("Sasha", self)
        lbl1.setObjectName("emphasis")
        lbl1.move(40, 50)

        lbl2 = QLabel("Breathing World", self)
        lbl2.setProperty("level", "warn")
        lbl2.move(40, 100)

app = QApplication(sys.argv)
w = Demo()
w.show()
sys.exit(app.exec())
  • Use QLabel#name to match an object by objectName
  • Use attribute selectors like QLabel[level="warn"] to match dynamic properties
  • Combine selectors: QLabel#name[level="warn"]

Parent–child relationships and object tree

Widgets form a tree. If a widget has no parent, it’s a top‑level window. Children are clipped by the parent’s bounds and destroyed with the parent.

import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel
from PySide6.QtCore import QObject

app = QApplication(sys.argv)

root = QWidget()
root.setObjectName("root_window")

c1 = QLabel("one", root);  c1.setObjectName("c1")
c2 = QLabel("two", root);  c2.setObjectName("c2")
c3 = QLabel("three", c2);  c3.setObjectName("c3")

print(c1.parent().objectName())                 # root_window
print(c3.parent().objectName())                 # c2
print([ch.objectName() for ch in root.children()])     # ['c1', 'c2']
print([ch.objectName() for ch in root.findChildren(QObject)])  # ['c1','c2','c3']

Binding an existing widget to a new parent:

lbl = QLabel("reparent me")
lbl.setParent(root)

Children obey parent geometry and lifetime:

p = QWidget(); p.resize(300, 200); p.setStyleSheet("background: cyan")
q = QWidget(); q.resize(1200, 50); q.setStyleSheet("background: green")
q.setParent(p)   # clipped by p
p.show()
app.exec()

Signals and slots

Qt uses signals for state changes and slots for handlers. Any callable can be a slot.

import sys
from PySide6.QtCore import QObject
from PySide6.QtWidgets import QApplication

app = QApplication(sys.argv)

obj = QObject()
obj.setObjectName("demo")

# destroyed signal
obj.destroyed.connect(lambda o=None: print("object destroyed"))
del obj

x = QObject(); x.setObjectName("x")
x.destroyed.connect(lambda o: print(f"destroyed: {o.objectName()}"))
del x

Another builtin signal:

z = QObject()
z.objectNameChanged.connect(lambda name: print(f"renamed to: {name}"))
z.setObjectName("Hey")

Disconnect and blocking:

n = QObject()
slot = lambda s: print("name:", s)
n.objectNameChanged.connect(slot)
n.setObjectName("A")

n.blockSignals(True)      # connection kept, emission blocked
n.setObjectName("B")

n.blockSignals(False)
n.setObjectName("C")      # prints again

n.objectNameChanged.disconnect()   # remove all
n.setObjectName("D")               # no output

Object deletion: deleteLater

deleteLater() posts a deferred deletion event; the object is cleaned up on the next event loop iteration. Children are also destroyed.

import sys
from PySide6.QtCore import QObject
from PySide6.QtWidgets import QApplication

app = QApplication(sys.argv)

p = QObject(); p.setObjectName("p"); p.destroyed.connect(lambda: print("p gone"))
c = QObject(p); c.setObjectName("c"); c.destroyed.connect(lambda: print("c gone"))

c.deleteLater()           # queued for deletion
print(p.children()[0].objectName())  # still 'c' before loop processes events

app.exec()                # now prints: c gone

Timers on QObject

Override timerEvent and start a basic timer.

import sys, time
from PySide6.QtCore import QObject
from PySide6.QtWidgets import QApplication

class T(QObject):
    def timerEvent(self, ev):
        print("tick", ev.timerId())

app = QApplication(sys.argv)
obj = T()
ident = obj.startTimer(500)

# simulate some delay, then stop
time.sleep(2)
obj.killTimer(ident)
app.exec()

QWidget basics: top‑level vs children

import sys
from PySide6.QtWidgets import QApplication, QWidget

app = QApplication(sys.argv)

w1 = QWidget(); w1.resize(400, 400); w1.setStyleSheet("background: cyan")
w2 = QWidget(w1); w2.resize(200, 200); w2.setStyleSheet("background: orange")

w1.show()
app.exec()

Geometry and coordinates

  • Top‑level widgets use screen coordinates
  • Child widgets use parent coordinates
  • Origin is at the top‑left; +x to the right, +y downward
import sys
from PySide6.QtWidgets import QApplication, QWidget

app = QApplication(sys.argv)
parent = QWidget(); parent.resize(400, 300); parent.move(200, 100)
child = QWidget(parent); child.resize(100, 80); child.move(60, 70)

print(child.x(), child.y())            # 60, 70 (relative to parent)
print(child.pos())                     # QPoint
print(parent.size().toTuple())         # (400, 300)

parent.show()
app.exec()

Note: move() for top‑level widgets positions the window frame. resize() sets the client area (not including the title bar).

Fixed sizes and content‑based sizing

import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel, QPushButton

app = QApplication(sys.argv)
root = QWidget(); root.setFixedSize(500, 300)

lbl = QLabel("Hello", root)
lbl.move(200, 100)

btn = QPushButton("Append", root)
btn.move(200, 150)

def grow():
    lbl.setText(lbl.text() + " — PySide6")
    lbl.adjustSize()   # adapt to new text

btn.clicked.connect(grow)

root.show()
app.exec()

Min/max constraints

w = QWidget()
w.setMinimumSize(320, 200)
w.setMaximumSize(800, 600)

You can also constrain width or height using setMinimumWidth, setMaximumHeight, etc.

Mouse cursors

Set cursor shapes per widget.

import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel
from PySide6.QtCore import Qt

app = QApplication(sys.argv)

box = QWidget(); box.resize(400, 300)
area = QLabel("Hover me", box)
area.resize(200, 100)
area.setStyleSheet("background: lightcyan")
area.setCursor(Qt.BusyCursor)

box.show()
app.exec()

Custom cursor from an image:

import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel
from PySide6.QtGui import QPixmap, QCursor

app = QApplication(sys.argv)
root = QWidget(); root.resize(400, 300)
zone = QLabel("Custom cursor", root); zone.resize(220, 120)
zone.setStyleSheet("background: #cff")

pm = QPixmap("cursor.png").scaled(48, 48)
zone.setCursor(QCursor(pm, 0, 0))   # hotspot at (0,0)

root.show()
app.exec()

Reset/get cursor and move pointer:

cur = zone.cursor()
cur.setPos(0, 0)  # screen coordinates
zone.unsetCursor()

Event system highlights

Override event handlers on any widget. Common handllers include:

  • showEvent, closeEvent, resizeEvent
  • enterEvent, leaveEvent, mousePressEvent, mouseReleaseEvent, mouseMoveEvent, mouseDoubleClickEvent
  • keyPressEvent, keyReleaseEvent
  • focusInEvent, focusOutEvent
  • dragEnterEvent, dragLeaveEvent, dragMoveEvent
  • paintEvent, changeEvent, contextMenuEvent

Event propagation: if a child accepts an event, it won’t bubble to the parent; call event.ignore() to continue propagation.

import sys
from PySide6 import QtWidgets, QtGui

class Panel(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.setFixedSize(300, 200)
        self.label = Tile(self)
        self.label.setText("Click inside me")
        self.label.resize(120, 60)
        self.label.setStyleSheet("background: palegreen")

    def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
        print("panel pressed")

class Tile(QtWidgets.QLabel):
    def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
        print("label pressed; forwarding to parent…")
        e.ignore()   # let parent handle too

app = QtWidgets.QApplication(sys.argv)
win = Panel(); win.show()
sys.exit(app.exec())

Keyboard events and modifiers:

import sys
from PySide6 import QtWidgets, QtGui, QtCore

class KeyDemo(QtWidgets.QWidget):
    def keyPressEvent(self, ev: QtGui.QKeyEvent) -> None:
        mods = ev.modifiers()
        if (mods & QtCore.Qt.ControlModifier) and (mods & QtCore.Qt.ShiftModifier) and ev.key() == QtCore.Qt.Key_S:
            print("Ctrl+Shift+S pressed")

app = QtWidgets.QApplication(sys.argv)
win = KeyDemo(); win.resize(400, 200); win.show()
sys.exit(app.exec())

A QLabel does not accept keyboard focus by default. Either grab the keyboard or set a focus policy and give it focus:

import sys
from PySide6 import QtWidgets, QtCore, QtGui

class FocusLabel(QtWidgets.QLabel):
    def __init__(self, *a, **k):
        super().__init__(*a, **k)
        self.setText("Focus me and press Ctrl+K")
        self.setStyleSheet("background: #eef")
        self.setFocusPolicy(QtCore.Qt.StrongFocus)

    def keyPressEvent(self, ev: QtGui.QKeyEvent) -> None:
        if (ev.modifiers() & QtCore.Qt.ControlModifier) and ev.key() == QtCore.Qt.Key_K:
            print("Ctrl+K in label")

app = QtWidgets.QApplication(sys.argv)
root = QtWidgets.QWidget(); root.resize(400, 200)
lbl = FocusLabel(root); lbl.resize(250, 80); lbl.setFocus()  # give focus
root.show()
sys.exit(app.exec())

Z‑order (stacking) control

Control which widget appears on top when they overlap.

import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel

app = QApplication(sys.argv)
root = QWidget(); root.resize(480, 320)

bottom = QLabel("Bottom", root); bottom.resize(200, 200); bottom.move(100, 100)
bottom.setStyleSheet("background: orange")

top = QLabel("Top", root); top.resize(200, 200); top.move(100, 160)
top.setStyleSheet("background: lightblue")

# place 'top' under 'bottom'
top.stackUnder(bottom)
# alternatively: bottom.raise_(); top.lower()

root.show()
app.exec()

Top‑level window features

Window icon

import sys
from PySide6.QtWidgets import QApplication, QWidget
from PySide6.QtGui import QIcon

app = QApplication(sys.argv)
w = QWidget()
w.setWindowIcon(QIcon("app_icon.png"))
w.show()
app.exec()

Opacity

w = QWidget()
w.setWindowOpacity(0.7)  # 0.0 transparent … 1.0 opaque

Window state

from PySide6.QtCore import Qt

w = QWidget()
w.setWindowState(Qt.WindowMaximized)

Window flags (frame/buttons behaviors)

from PySide6.QtCore import Qt

w = QWidget()
# borderless window
w.setWindowFlags(Qt.FramelessWindowHint)

Compose flags as needed, e.g., keep on top (Qt.WindowStaysOnTopHint), disable resize, or show only specific titlebar buttons.

Custom close button on a frameless window:

import sys
from PySide6.QtWidgets import QApplication, QWidget, QPushButton
from PySide6.QtCore import Qt

app = QApplication(sys.argv)
win = QWidget(); win.resize(300, 200)
win.setWindowFlags(Qt.FramelessWindowHint)

close_btn = QPushButton("Close", win)
close_btn.move(20, 20)
close_btn.clicked.connect(win.close)

win.show()
app.exec()

Enable/disable and visibility

btn = QtWidgets.QPushButton("Disabled")
btn.setEnabled(False)

# visibility
btn.setVisible(True)   # or btn.show()
btn.setVisible(False)  # or btn.hide()

print(btn.isHidden(), btn.isVisible())

Visibility depends on parent; a visible child is not shown if its parent is hidden.

"Modified" marker in window title

Place [*] in the title and toggle the marker with setWindowModified(True/False).

import sys
from PySide6 import QtWidgets

app = QtWidgets.QApplication(sys.argv)
win = QtWidgets.QWidget()
win.setWindowTitle("Document[*]")

mark = QtWidgets.QPushButton("Modify", win)
mark.clicked.connect(lambda: win.setWindowModified(True))
mark.move(20, 20)

win.show()
app.exec()

Tooltips

import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel

app = QApplication(sys.argv)
w = QWidget(); w.resize(300, 200)
w.setToolTip("This is the window tooltip")

lbl = QLabel("Hover here", w)
lbl.setToolTip("Label tooltip")
lbl.setToolTipDuration(3000)  # ms

w.show()
app.exec()

Focus management

import sys
from PySide6 import QtWidgets

app = QtWidgets.QApplication(sys.argv)
win = QtWidgets.QWidget(); win.resize(300, 200)

u = QtWidgets.QLineEdit(win); u.move(40, 40)
p = QtWidgets.QLineEdit(win); p.move(40, 70)

p.setFocus()   # programmatically focus second field

win.show()
app.exec()

Control when widgets can receive focus:

from PySide6.QtCore import Qt

p.setFocusPolicy(Qt.TabFocus)     # only via Tab
# Qt.ClickFocus, Qt.StrongFocus, Qt.NoFocus are also available

To query the current focus widget inside event handling or timers:

fw = win.focusWidget()            # returns a widget or None
win.focusNextChild()              # move focus forward
win.focusPreviousChild()          # move focus backward

Buttons overview (QAbstractButton family)

  • QPushButton: standard push button
  • QToolButton: small toolbar‑style button
  • QRadioButton: mutually exclusive selection
  • QCheckBox: independent toggles
  • QCommandLinkButton: command link style

Updating button text on press

import sys
from PySide6 import QtWidgets, QtGui

class CounterButton(QtWidgets.QPushButton):
    def __init__(self, *a, **k):
        super().__init__(*a, **k)
        self.setText("0")

    def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None:
        super().mousePressEvent(ev)
        try:
            val = int(self.text())
        except ValueError:
            val = 0
        self.setText(str(val + 1))

app = QtWidgets.QApplication(sys.argv)
root = QtWidgets.QWidget(); root.resize(240, 160)
btn = CounterButton("0", root); btn.setGeometry(60, 50, 60, 40)
root.show()
sys.exit(app.exec())

Button icon

import sys
from PySide6 import QtWidgets, QtGui, QtCore

app = QtWidgets.QApplication(sys.argv)
root = QtWidgets.QWidget(); root.resize(200, 140)

b = QtWidgets.QPushButton(root)
b.setGeometry(60, 40, 80, 60)
b.setIcon(QtGui.QIcon("1.png"))
b.setIconSize(QtCore.QSize(64, 64))

root.show()
app.exec()

Keyboard shortcuts and auto‑repeat

import sys
from PySide6 import QtWidgets

app = QtWidgets.QApplication(sys.argv)
root = QtWidgets.QWidget(); root.resize(240, 160)

b = QtWidgets.QPushButton("Press or Alt+G", root)
b.setGeometry(40, 40, 160, 40)
b.pressed.connect(lambda: print("pressed"))

b.setShortcut("Alt+G")
b.setAutoRepeat(True)
b.setAutoRepeatDelay(1000)      # start repeating after 1s
b.setAutoRepeatInterval(2000)   # repeat every 2s

root.show()
app.exec()

Pressed state via stylesheet

btn = QtWidgets.QPushButton("Styled")
btn.setStyleSheet("QPushButton:pressed { background-color: cornflowerblue; }")

Checkable and exclusive behavior

import sys
from PySide6 import QtWidgets

app = QtWidgets.QApplication(sys.argv)
root = QtWidgets.QWidget(); root.resize(300, 220)

b1 = QtWidgets.QPushButton("One", root)
b2 = QtWidgets.QPushButton("Two", root)
b3 = QtWidgets.QPushButton("Three", root)
b4 = QtWidgets.QPushButton("Four", root)

for i, b in enumerate((b1,b2,b3,b4)):
    b.setGeometry(40, 30 + i*40, 120, 32)
    b.setCheckable(True)

# Make first three mutually exclusive (same parent)
for b in (b1, b2, b3):
    b.setAutoExclusive(True)

root.show()
app.exec()

A QCheckBox can be made exclusive in the same way, effectively acting like a radio button.

Programmatic clicks and signals

  • btn.click() emits a click immediately
  • btn.animateClick(ms) presses and releases after the delay
  • Signals: pressed, released, clicked
btn.clicked.connect(lambda: print("clicked"))
btn.animateClick(500)
Tags: pyside6

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.