Tại sao chọn PySide6 thay vì Tkinter hay PyQt5?
Dự án automation của mình ban đầu chỉ 200 dòng — một script nhỏ chạy trên terminal là đủ. Nhưng sau 6 tháng, nó phình lên 2000 dòng, khách hàng muốn có giao diện, muốn click button thay vì gõ lệnh. Mình thử Tkinter trước — nhanh nhưng giao diện trông như Windows 98. Sau đó chuyển sang PySide6 và không nhìn lại nữa.
PySide6 là binding chính thức của Qt 6 cho Python, được The Qt Company phát triển và cấp phép LGPL — nghĩa là bạn có thể dùng cho dự án thương mại miễn phí. So với PyQt5 (PyQt6), PySide6 có license thoải mái hơn và API tương đương. So với Tkinter, nó có widget hiện đại, hỗ trợ CSS-like styling, và quan trọng nhất: có Qt Designer — công cụ kéo thả thiết kế UI trực quan.
Sau 6 tháng dùng thực tế, đây là những gì mình học được về cách tổ chức một ứng dụng PySide6 đúng cách từ đầu.
Cài đặt môi trường
Bắt đầu với virtualenv để tránh conflict với các project khác:
python -m venv venv
source venv/bin/activate # Linux/macOS
# hoặc: venv\Scripts\activate # Windows
pip install PySide6
Lệnh trên cài toàn bộ Qt 6 binding bao gồm Qt Designer, Qt Quick, và các module cần thiết. Dung lượng khoảng 150MB — lần đầu hơi lâu, nhưng chỉ cần làm một lần.
Kiểm tra cài đặt thành công:
python -c "import PySide6; print(PySide6.__version__)"
Khởi động Qt Designer ngay từ terminal:
pyside6-designer
Thiết kế giao diện với Qt Designer
Qt Designer là điểm mình thấy PySide6 vượt trội hẳn các thư viện GUI Python khác. Thay vì viết code layout thủ công, bạn kéo thả widget, set property, rồi save thành file .ui (thực chất là XML). Mình mất khoảng 2 buổi làm quen với Designer — sau đó năng suất tăng hẳn.
Tạo file .ui đầu tiên
Trong Designer, chọn File → New → Main Window. Kéo các widget từ panel bên trái vào canvas:
- QLabel — hiển thị text tĩnh
- QLineEdit — input field
- QPushButton — nút bấm
- QTextEdit — vùng text nhiều dòng
Đặt tên objectName cho từng widget trong panel Properties — đây là tên bạn sẽ dùng trong code Python sau này. Đặt tên có nghĩa: btn_submit, input_username, label_status.
Save file thành main_window.ui.
Convert .ui sang Python
Có hai cách load file .ui trong code:
Cách 1: Load trực tiếp lúc runtime (tiện cho development):
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtUiTools import QUiLoader
from PySide6.QtCore import QFile
import sys
loader = QUiLoader()
file = QFile("main_window.ui")
file.open(QFile.ReadOnly)
window = loader.load(file)
file.close()
window.show()
Cách 2: Convert sang Python class (khuyến nghị cho production):
pyside6-uic main_window.ui -o ui_main_window.py
File ui_main_window.py sinh ra chứa class Ui_MainWindow mà bạn có thể import và kế thừa. Mình dùng cách 2 vì nó cho phép IDE autocomplete tên widget, giảm bug do gõ sai tên.
Cấu trúc code production-ready
Đây là bài học đắt giá nhất sau 6 tháng: đừng nhồi hết logic vào một class. Khi project từ 200 lên 2000 dòng, cấu trúc flat sẽ biến thành cơn ác mộng. Mình tổ chức theo pattern Model-View-Controller đơn giản hóa:
myapp/
├── main.py # Entry point
├── ui/
│ ├── main_window.ui # Qt Designer file
│ └── ui_main_window.py # Auto-generated, KHÔNG sửa tay
├── views/
│ └── main_window.py # Controller: kết nối UI với logic
└── core/
└── processor.py # Business logic thuần Python
File views/main_window.py — nơi kết nối mọi thứ
from PySide6.QtWidgets import QMainWindow, QMessageBox
from PySide6.QtCore import Qt, QThread, Signal
from ui.ui_main_window import Ui_MainWindow
from core.processor import DataProcessor
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.processor = DataProcessor()
self._connect_signals()
def _connect_signals(self):
# Kết nối signal với slot — đây là trái tim của Qt
self.ui.btn_submit.clicked.connect(self._on_submit_clicked)
self.ui.input_username.returnPressed.connect(self._on_submit_clicked)
def _on_submit_clicked(self):
username = self.ui.input_username.text().strip()
if not username:
QMessageBox.warning(self, "Lỗi", "Vui lòng nhập tên người dùng")
return
result = self.processor.process(username)
self.ui.label_status.setText(f"Kết quả: {result}")
Xử lý tác vụ nặng với QThread
Vấn đề lớn nhất khi mới dùng Qt: chạy tác vụ nặng (gọi API, đọc file lớn) trực tiếp trong slot sẽ freeze toàn bộ UI. Giải pháp là QThread:
from PySide6.QtCore import QThread, Signal
class WorkerThread(QThread):
# Signal để gửi kết quả về main thread
result_ready = Signal(str)
error_occurred = Signal(str)
def __init__(self, username: str):
super().__init__()
self.username = username
def run(self):
# Chạy trong thread riêng — KHÔNG được cập nhật UI ở đây
try:
import time
time.sleep(2) # Giả lập tác vụ nặng
result = f"Processed: {self.username.upper()}"
self.result_ready.emit(result)
except Exception as e:
self.error_occurred.emit(str(e))
# Trong MainWindow._on_submit_clicked():
def _on_submit_clicked(self):
username = self.ui.input_username.text().strip()
self.ui.btn_submit.setEnabled(False) # Disable để tránh click đúp
self.ui.label_status.setText("Đang xử lý...")
self.worker = WorkerThread(username)
self.worker.result_ready.connect(self._on_result_ready)
self.worker.error_occurred.connect(self._on_error)
self.worker.finished.connect(lambda: self.ui.btn_submit.setEnabled(True))
self.worker.start()
def _on_result_ready(self, result: str):
self.ui.label_status.setText(result)
def _on_error(self, error: str):
QMessageBox.critical(self, "Lỗi", error)
Lưu ý quan trọng: luôn giữ reference đến worker thread (self.worker) — nếu để là local variable, Python garbage collector sẽ destroy thread trước khi nó chạy xong.
Styling với QSS — CSS của Qt
Qt hỗ trợ stylesheet tương tự CSS, gọi là QSS. Mình thường load từ file riêng để dễ chỉnh sửa:
# Trong main.py
app = QApplication(sys.argv)
with open("style.qss", "r") as f:
app.setStyleSheet(f.read())
/* style.qss */
QPushButton {
background-color: #2563eb;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
}
QPushButton:hover {
background-color: #1d4ed8;
}
QPushButton:disabled {
background-color: #9ca3af;
}
QLineEdit {
border: 1px solid #d1d5db;
border-radius: 4px;
padding: 6px 10px;
font-size: 14px;
}
Kiểm tra và đóng gói ứng dụng
Chạy ứng dụng
# main.py
import sys
from PySide6.QtWidgets import QApplication
from views.main_window import MainWindow
def main():
app = QApplication(sys.argv)
app.setApplicationName("MyApp")
app.setApplicationVersion("1.0.0")
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
Đóng gói thành file .exe hoặc binary
Dùng pyinstaller để đóng gói:
pip install pyinstaller
# Đóng gói thành một file duy nhất
pyinstaller --onefile --windowed \
--add-data "ui/*.ui:ui" \
--add-data "style.qss:." \
main.py
File output nằm trong thư mục dist/. Trên Windows ra .exe, Linux ra binary không extension, macOS ra .app.
Một số vấn đề thường gặp
- UI freeze: Tác vụ nặng trong main thread → dùng QThread như mình hướng dẫn ở trên
- Widget không cập nhật: Quên emit signal hoặc cập nhật UI từ thread phụ → chỉ cập nhật UI từ main thread qua signal
- File .ui không tìm thấy sau khi đóng gói: Phải thêm
--add-datavào lệnh pyinstaller, hoặc convert sang Python class (cách 2 ở trên) để tránh vấn đề này hoàn toàn - Memory leak: Tạo widget không có parent → Qt không tự cleanup. Luôn truyền
parent=selfkhi tạo widget con
Tổng kết
Sau 6 tháng dùng PySide6, mình đúc kết lại: điểm mạnh nhất của nó là hệ thống Signal/Slot — một khi đã hiểu cơ chế này, mọi tương tác UI đều trở nên rõ ràng và có thể test được. Qt Designer giúp tiết kiệm thời gian thiết kế layout đáng kể so với viết code thủ công.
Nếu project của bạn cần GUI chạy cross-platform, có styling đẹp, và xử lý được tác vụ nặng mà không freeze — PySide6 là lựa chọn solid. Cấu trúc code theo pattern tách biệt UI/Logic/Core từ đầu sẽ giúp bạn không phải refactor lại toàn bộ khi project lớn dần.

