-
-
Notifications
You must be signed in to change notification settings - Fork 0
UI Framework And PyQt6
CppLab IDE is built with PyQt6, a comprehensive Python binding for the Qt 6 application framework. I used it because I have worked with it before and I know my way around it a bit, and it's really nice. Apart from the pretty decision matrix below, the ease of the tool is its greatest advantage and what more would I want as a dev? Now, the irony of this is, that Qt is written in C++ and my initial idea was to develop this project in C++, which can be witnessed in the multiple repos I made earlier and then made private (:sweatdrop:). So, yeah. Bundling and developing with Qt in C++ is a bit of a headache if you ask me. Is it a skill issue? Most probably. Does that mean the project will forever be in python? Unlikely. But, for now, we have Python to thank for this project to have come together so nicely.
| Framework | Pros | Cons | Score |
|---|---|---|---|
| PyQt6 | Native look, rich widgets, cross-platform, mature | GPL/Commercial license, learning curve | ⭐⭐⭐⭐⭐ |
| Tkinter | Built-in, simple | Limited widgets, outdated look | ⭐⭐ |
| wxPython | Native widgets | Less documentation, smaller community | ⭐⭐⭐ |
| Kivy | Modern, touch-friendly | Not for desktop IDEs | ⭐⭐ |
| Dear ImGui | Game dev, fast | C++ binding, immediate mode | ⭐⭐ |
| Web (Electron) | Familiar (HTML/CSS/JS) | Heavy, memory hungry | ⭐⭐⭐ |
1. Native Performance
- Written in C++ (Qt framework)
- Hardware-accelerated rendering
- Low memory footprint (~50-80 MB)
- Fast startup time (~1-2 seconds)
2. Rich Widget Library
# Available out-of-the-box
QMainWindow # Main application window
QTextEdit # Code editor with syntax highlighting
QTreeView # File browser
QDockWidget # Dockable panels
QToolBar # Toolbar with actions
QStatusBar # Status bar with labels
QTabWidget # Tabbed panels
QComboBox # Dropdown selectors
QFileDialog # File/folder pickers
QMessageBox # Dialogs
QSplitter # Resizable panes3. Cross-Platform
- Windows (native)
- macOS (native)
- Linux (native)
- Same codebase, native look on each platform
4. Qt Designer Integration
- Visual UI design
-
.uifiles (XML-based) - Load at runtime with
uic.loadUi() - WYSIWYG editor
5. Signals & Slots
- Type-safe event system
- Decoupled components
- Thread-safe communication
6. Threading Support
-
QThreadfor background work -
QObject.moveToThread()pattern - Thread-safe signals
- No GIL issues for UI updates
7. Mature Ecosystem
- 20+ years of Qt development
- Extensive documentation
- Large community
- Proven in production (Autodesk Maya, Blender, etc.)
Application (QApplication)
└── MainWindow (QMainWindow)
├── MenuBar (QMenuBar)
│ ├── File Menu
│ ├── Edit Menu
│ ├── Build Menu
│ └── Run Menu
├── ToolBar (QToolBar)
│ ├── New File Action
│ ├── Open File Action
│ ├── Save Action
│ ├── Build Action
│ └── Run Action
├── Central Widget
│ ├── Splitter (QSplitter)
│ │ ├── File Tree (QTreeView) [Left]
│ │ └── Editor (QTextEdit) [Right]
├── Dock Widgets
│ └── Bottom Panel (QDockWidget)
│ └── Tab Widget (QTabWidget)
│ ├── Build Tab (QTextEdit)
│ ├── Problems Tab (QListWidget)
│ └── Console Tab (QTextEdit)
└── Status Bar (QStatusBar)
├── Mode Label
├── Build Status Label
├── Toolchain Label
└── Standard Label
File: src/cpplab/ui/MainWindow.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1200</width>
<height>800</height>
</rect>
</property>
<property name="windowTitle">
<string>CppLab IDE</string>
</property>
<!-- Central Widget -->
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout">
<item>
<widget class="QSplitter" name="mainSplitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<!-- File Tree and Editor -->
</widget>
</item>
</layout>
</widget>
<!-- Menu Bar -->
<widget class="QMenuBar" name="menubar">
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
</widget>
</widget>
<!-- Status Bar -->
<widget class="QStatusBar" name="statusbar"/>
<!-- Dock Widgets -->
<widget class="QDockWidget" name="bottomDock">
<property name="windowTitle">
<string>Output</string>
</property>
</widget>
</widget>
</ui>File: src/cpplab/app.py
from PyQt6 import uic
from PyQt6.QtWidgets import QMainWindow
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Load UI from .ui file
ui_path = Path(__file__).parent / "ui" / "MainWindow.ui"
uic.loadUi(ui_path, self)
# Setup additional components
self._setup_widgets()
self._connect_signals()Qt's Signal/Slot Mechanism:
- Type-safe callbacks
- One-to-many connections
- Automatic disconnection when objects deleted
- Thread-safe (with queued connections)
Signal Definition (built into QPushButton):
# QPushButton has a 'clicked' signal
button.clicked # Signal[bool]Connection:
def __init__(self):
# Connect signal to slot
self.buildButton.clicked.connect(self.on_build_clicked)
def on_build_clicked(self):
"""Slot called when build button clicked."""
print("Building...")
self.build_current()File: src/cpplab/app.py
from PyQt6.QtCore import QObject, pyqtSignal
class BuildWorker(QObject):
# Define custom signals
started = pyqtSignal() # No arguments
finished = pyqtSignal(object, int) # BuildResult, elapsed_ms
error = pyqtSignal(str) # Error message
def run(self):
"""Background build task."""
self.started.emit() # Emit started signal
try:
result = self.builder.build_project(...)
elapsed_ms = ...
self.finished.emit(result, elapsed_ms) # Emit finished
except Exception as e:
self.error.emit(str(e)) # Emit errorConnection:
# Create worker
worker = BuildWorker(...)
# Connect signals
worker.started.connect(self.on_build_started)
worker.finished.connect(self.on_build_finished)
worker.error.connect(self.on_build_error)
# Start thread
thread = QThread()
worker.moveToThread(thread)
thread.started.connect(worker.run)
thread.start()| Signal Type | Use Case | Example |
|---|---|---|
pyqtSignal() |
No data | started |
pyqtSignal(str) |
String data | error(message) |
pyqtSignal(int, int) |
Multiple args | progress(current, total) |
pyqtSignal(object) |
Complex data | finished(result) |
Modern Approach (used in CppLab):
class BuildWorker(QObject):
"""Worker object for background builds."""
finished = pyqtSignal(object, int)
def __init__(self, builder):
super().__init__()
self.builder = builder
def run(self):
"""Run in background thread."""
result = self.builder.build_project(...)
self.finished.emit(result, elapsed_ms)
# Usage
worker = BuildWorker(builder)
thread = QThread()
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.finished.connect(on_finished)
thread.start()Old Approach (don't use):
# ❌ Don't subclass QThread
class BuildThread(QThread):
def run(self):
# This is harder to maintain
passWhy Modern Approach is Better:
- Worker is reusable (not tied to thread)
- Easier to test (just call
worker.run()) - Cleaner separation of concerns
- Recommended by Qt documentation
UI Updates Must Be on Main Thread:
# [x] Safe: Use signal to update UI
class BuildWorker(QObject):
finished = pyqtSignal(str) # Signal emits to main thread
def run(self):
result = "Build succeeded"
self.finished.emit(result) # Thread-safe
# ❌ Unsafe: Direct UI update from thread
class BuildThread(QThread):
def run(self):
# This will crash or corrupt UI
self.text_edit.append("Build succeeded")def start_build_task(self, action):
"""Start build in background thread with proper cleanup."""
# Create worker and thread
worker = BuildWorker(...)
thread = QThread()
# Setup cleanup
worker.finished.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
thread.finished.connect(thread.deleteLater)
# Move worker to thread
worker.moveToThread(thread)
# Start
thread.started.connect(worker.run)
thread.start()
# Store references (prevent premature GC)
self.current_build_thread = thread
self.current_build_worker = workerSimilar to CSS:
# Apply stylesheet
self.setStyleSheet("""
QMainWindow {
background-color: #2b2b2b;
}
QTextEdit {
background-color: #1e1e1e;
color: #d4d4d4;
font-family: Consolas;
font-size: 10pt;
}
QPushButton {
background-color: #0e639c;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
}
QPushButton:hover {
background-color: #1177bb;
}
""")File: src/cpplab/settings.py
THEMES = {
"classic": """
QMainWindow {
background-color: #f0f0f0;
}
QTextEdit {
background-color: white;
color: black;
}
""",
"sky_blue": """
QMainWindow {
background-color: #e6f2ff;
}
QTextEdit {
background-color: #f0f8ff;
color: #003366;
}
"""
}Application:
def apply_settings(self):
"""Apply user settings."""
settings = load_settings()
# Apply theme
theme = settings.theme
if theme in THEMES:
self.setStyleSheet(THEMES[theme])
# Apply font
font = QFont("Consolas", settings.font_size)
if settings.bold_font:
font.setBold(True)
self.buildOutputEdit.setFont(font)Features:
- Multi-line text editing
- Syntax highlighting (via QSyntaxHighlighter)
- Line numbers (custom implementation)
- Find/replace
- Undo/redo
- Read-only mode
Usage:
from PyQt6.QtWidgets import QTextEdit
editor = QTextEdit()
editor.setPlainText("#include <iostream>\n\nint main() {\n return 0;\n}")
editor.setFont(QFont("Consolas", 10))
editor.setTabStopDistance(40) # 4-space tabsFeatures:
- Hierarchical data display
- File system model
- Icons
- Expand/collapse
- Selection
Usage:
from PyQt6.QtWidgets import QTreeView
from PyQt6.QtGui import QFileSystemModel
tree = QTreeView()
model = QFileSystemModel()
model.setRootPath("C:/Projects/MyApp")
tree.setModel(model)
tree.setRootIndex(model.index("C:/Projects/MyApp"))Features:
- Dockable/floating
- Resizable
- Closeable
- Multiple dock areas
Usage:
from PyQt6.QtWidgets import QDockWidget, QTextEdit
from PyQt6.QtCore import Qt
dock = QDockWidget("Output", self)
dock.setWidget(QTextEdit())
self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, dock)Features:
- Multiple tabs
- Tab switching
- Tab icons
- Closeable tabs
Usage:
from PyQt6.QtWidgets import QTabWidget, QTextEdit
tabs = QTabWidget()
tabs.addTab(QTextEdit(), "Build")
tabs.addTab(QTextEdit(), "Problems")
tabs.addTab(QTextEdit(), "Console")Features:
- Permanent and temporary messages
- Multiple widgets (labels, progress bars)
- Automatic layout
Usage:
from PyQt6.QtWidgets import QLabel
# Temporary message (disappears after 3 seconds)
self.statusBar().showMessage("File saved", 3000)
# Permanent widgets
mode_label = QLabel("Mode: Project")
self.statusBar().addPermanentWidget(mode_label)File Dialog:
from PyQt6.QtWidgets import QFileDialog
file_path, _ = QFileDialog.getOpenFileName(
self,
"Open File",
"",
"C++ Files (*.cpp *.cc);;C Files (*.c);;All Files (*)"
)Message Box:
from PyQt6.QtWidgets import QMessageBox
QMessageBox.information(self, "Success", "Build completed!")
QMessageBox.warning(self, "Warning", "Unsaved changes")
QMessageBox.critical(self, "Error", "Build failed")Input Dialog:
from PyQt6.QtWidgets import QInputDialog
text, ok = QInputDialog.getText(self, "Input", "Enter project name:")
if ok:
print(f"Project name: {text}")File: src/cpplab/settings_dialog.py
from PyQt6.QtWidgets import QDialog, QTabWidget, QVBoxLayout
class SettingsDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Settings")
self.resize(500, 400)
# Create layout
layout = QVBoxLayout()
# Add tabs
tabs = QTabWidget()
tabs.addTab(self._create_appearance_tab(), "Appearance")
tabs.addTab(self._create_build_tab(), "Build")
layout.addWidget(tabs)
self.setLayout(layout)
def _create_appearance_tab(self):
# Create appearance settings UI
passCppLab IDE (measured):
- Startup: ~50 MB
- With project open: ~80 MB
- During build: ~100 MB
Comparison:
- VS Code: ~300-500 MB
- CLion: ~800-1200 MB
- Visual Studio: ~1000-2000 MB
CppLab IDE (measured):
- Cold start: ~1.5 seconds
- Warm start: ~0.8 seconds
Comparison:
- VS Code: ~2-3 seconds
- CLion: ~5-8 seconds
- Visual Studio: ~10-15 seconds
PyQt6 Advantages:
- Hardware-accelerated (OpenGL/DirectX)
- Efficient text rendering
- Lazy loading of UI elements
- Virtual scrolling for large lists
1. Design in Qt Designer
Open Qt Designer → Create .ui file → Save in src/cpplab/ui/
2. Load in Python
from PyQt6 import uic
uic.loadUi("ui/MainWindow.ui", self)3. Access widgets
# Widgets are accessible as attributes
self.buildButton.clicked.connect(self.on_build)
self.editor.textChanged.connect(self.on_text_changed)import sys
from PyQt6.QtWidgets import QApplication
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()Restart to see changes (no hot reload by default)
Print statements:
print("Build clicked") # Shows in terminalQt debugging:
from PyQt6.QtCore import qDebug, qWarning, qCritical
qDebug("Debug message")
qWarning("Warning message")
qCritical("Critical error")Visual debugging:
# Show widget boundaries
self.setStyleSheet("* { border: 1px solid red; }")Command:
pyinstaller --onefile --windowed --icon=icon.ico ^
--add-data "ui;ui" ^
--add-data "compilers;compilers" ^
src/cpplab/__main__.pyIncludes:
- PyQt6 DLLs (~80 MB)
- Python runtime (~20 MB)
- UI files
- Resources
Output:
- Single
.exefile (~100-120 MB) - No dependencies required
Next: Settings and Configuration
Previous: Build System Details
💡 Found this wiki useful?
⭐ Star the repo
·
💖 Sponsor this project
·
📦 Latest release
·
🐞 Report an issue