Skip to content

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