-
-
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 panes
3. 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 error
Connection:
# 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 pass
Why 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 = worker
Similar 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 tabs
Features:
- 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 pass
CppLab 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 terminal
Qt 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__.py
Includes:
- 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