Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

UI Framework And PyQt6

techy4shri edited this page Nov 29, 2025 · 2 revisions

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.

Why PyQt6?

Decision Matrix

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 ⭐⭐⭐

Key Advantages

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
  • .ui files (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

  • QThread for 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.)

Architecture

Component Hierarchy

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

UI Definition

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>

Loading UI at Runtime

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()

Signals & Slots

Event System

Qt's Signal/Slot Mechanism:

  • Type-safe callbacks
  • One-to-many connections
  • Automatic disconnection when objects deleted
  • Thread-safe (with queued connections)

Example: Build Button

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()

Custom Signals

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 Types

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)

Threading

QThread Pattern

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

Thread Safety

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")

Thread Cleanup

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

Styling

Qt Style Sheets (QSS)

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;
 }
""")

Theme System

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)

Widgets in Detail

QTextEdit (Code Editor)

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

QTreeView (File Browser)

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"))

QDockWidget (Bottom Panel)

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)

QTabWidget (Tabbed Interface)

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")

QStatusBar (Status Bar)

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)

Dialogs

Standard Dialogs

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}")

Custom Dialogs

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

Performance Considerations

Memory Usage

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

Startup Time

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

Rendering Performance

PyQt6 Advantages:

  • Hardware-accelerated (OpenGL/DirectX)
  • Efficient text rendering
  • Lazy loading of UI elements
  • Virtual scrolling for large lists

Development Workflow

UI Design Process

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)

Hot Reload (Development)

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)

Debugging

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; }")

Packaging

PyInstaller

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 .exe file (~100-120 MB)
  • No dependencies required

Next: Settings and Configuration
Previous: Build System Details

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /