PyQt6 Evaluation

PyQt6 Evaluation

After some discussion at work, I couldn't help but dive into Python GUI frameworks just to see what it is like getting started.

I have past experience with Visual C++, Visual Basic, C#, various web frameworks, but haven't done all that much with Python GUIs professionally.

For this evaluation, I'm basically asking ChatGPT to generate some basic single file examples. I may look into recommended file structures, but starting simple for now.

Installation

pip install PyQt6 matplotlib numpy

Example 1 - Hello World

import sys
from PyQt6.QtWidgets import QApplication, QMainWindow

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt6 Basic Application")
        self.setGeometry(100, 100, 600, 400)  # x, y, width, height

def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

Example 2 - Basic Chart Demo

import sys
import matplotlib

matplotlib.use('QT5Agg')  # Use QT5Agg backend for compatibility with PyQt6
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QLabel
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure


class MplCanvas(FigureCanvas):
    def __init__(self, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super().__init__(fig)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt6 Application with Buttons, Labels, and Matplotlib Chart")

        # Create a central widget
        self.central_widget = QWidget()
        self.setCentralWidget(self.central_widget)

        # Create a vertical layout
        self.layout = QVBoxLayout(self.central_widget)

        # Add a button
        self.button = QPushButton("Click Me")
        self.layout.addWidget(self.button)

        # Add a label
        self.label = QLabel("Hello, PyQt6!")
        self.layout.addWidget(self.label)

        # Add a Matplotlib chart
        self.canvas = MplCanvas(width=5, height=4, dpi=100)
        self.canvas.axes.plot([0, 1, 2, 3, 4], [10, 1, 20, 3, 40])
        self.layout.addWidget(self.canvas)

        # Connect button click to the method
        self.button.clicked.connect(self.on_button_clicked)

    def on_button_clicked(self):
        self.label.setText("Button clicked!")


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

Example 3 - Chart with Controls Demo

This example comprises of dials for setting the frequency and amplitude of a sine wave displayed within a matplotlib chart.

import sys
import numpy as np
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel, QDial
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure

class SineWavePlotter(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Sine Wave Plotter")
        self.setGeometry(100, 100, 800, 600)  # x, y, width, height

        # Central Widget and Layout
        self.central_widget = QWidget()
        self.layout = QVBoxLayout(self.central_widget)
        self.setCentralWidget(self.central_widget)

        # Matplotlib Figure
        self.canvas = FigureCanvas(Figure(figsize=(5, 3)))
        self.ax = self.canvas.figure.subplots()
        self.layout.addWidget(self.canvas)

        # Frequency Dial
        self.freqDial = QDial()
        self.freqDial.setMinimum(1)
        self.freqDial.setMaximum(20)
        self.freqDial.setValue(5)
        self.freqDial.valueChanged.connect(self.update_plot)
        self.layout.addWidget(self.freqDial)

        # Amplitude Dial
        self.ampDial = QDial()
        self.ampDial.setMinimum(1)
        self.ampDial.setMaximum(10)
        self.ampDial.setValue(1)
        self.ampDial.valueChanged.connect(self.update_plot)
        self.layout.addWidget(self.ampDial)

        # Frequency Label
        self.freqLabel = QLabel("Frequency: 5 Hz")
        self.freqLabel.setFont(QFont("Arial", 16))
        self.freqLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.layout.addWidget(self.freqLabel)

        # Amplitude Label
        self.ampLabel = QLabel("Amplitude: 1")
        self.ampLabel.setFont(QFont("Arial", 16))
        self.ampLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.layout.addWidget(self.ampLabel)

        # Initial Plot
        self.update_plot()

    def update_plot(self):
        freq = self.freqDial.value()
        amp = self.ampDial.value()
        self.freqLabel.setText(f"Frequency: {freq} Hz")
        self.ampLabel.setText(f"Amplitude: {amp}")

        t = np.linspace(0, 1, 1000)
        y = amp * np.sin(2 * np.pi * freq * t)
        self.ax.clear()
        self.ax.plot(t, y)
        self.ax.set(xlabel='Time (s)', ylabel='Amplitude',
                    title='Real-time Sine Wave Plot')
        self.canvas.draw()

def main():
    app = QApplication(sys.argv)
    window = SineWavePlotter()
    window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

Example 4 - Standard Menus / Toolbar

import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow, QMessageBox, QToolBar, QStyle)
from PyQt6.QtGui import QAction
from PyQt6.QtCore import Qt


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.statusBar().showMessage('Ready')

        # Create actions
        exitAct = QAction('Exit', self)
        exitAct.setShortcut('Ctrl+Q')
        exitAct.triggered.connect(QApplication.instance().quit)

        aboutAct = QAction('About', self)
        aboutAct.triggered.connect(self.about)

        # Menu Bar
        menubar = self.menuBar()
        fileMenu = menubar.addMenu('&File')
        fileMenu.addAction(exitAct)

        helpMenu = menubar.addMenu('&Help')
        helpMenu.addAction(aboutAct)

        # Toolbar
        toolbar = QToolBar("Main Toolbar")
        self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar)

        # Create actions with standard icons
        action_names = ["New", "Open", "Save", "About"]
        tooltips = ["Create something new", "Open a file", "Save the file", "Show the application's About box"]
        standard_icons = [QStyle.StandardPixmap.SP_FileIcon, QStyle.StandardPixmap.SP_DirOpenIcon,
                          QStyle.StandardPixmap.SP_DialogSaveButton, QStyle.StandardPixmap.SP_DialogHelpButton]

        for name, tooltip, icon in zip(action_names, tooltips, standard_icons):
            action = QAction(self.style().standardIcon(icon), name, self)
            action.setToolTip(tooltip)
            if name == "About":
                action.triggered.connect(self.about)
            toolbar.addAction(action)

        self.setGeometry(300, 300, 350, 250)
        self.setWindowTitle('Main window')

    def about(self):
        QMessageBox.about(self, "About Application",
                          "Version 1.0\nThis is a simple PyQt6 application example with a toolbar using standard icons.")

def main():
    app = QApplication(sys.argv)
    mainWin = MainWindow()
    mainWin.show()
    sys.exit(app.exec())

if __name__ == '__main__':
    main()

Example 5 - Real-Time Chart Demo 2

import sys
import numpy as np
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from PyQt6.QtCore import QTimer
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure

class RealTimePlotter(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        # Set the window title and size
        self.setWindowTitle("Real-Time Signal Plotter")
        self.setGeometry(100, 100, 800, 600)

        # Create a central widget and set layout
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)

        # Create a matplotlib figure for plotting the signals
        self.figure = Figure()
        self.canvas = FigureCanvas(self.figure)
        layout.addWidget(self.canvas)
        self.ax = self.figure.add_subplot(111)

        # Initialize data storage for voltage, current, and power
        self.time_window = 30  # seconds
        self.update_interval = 500  # milliseconds
        self.time_data = np.linspace(-self.time_window, 0, num=int(self.time_window*1000/self.update_interval))
        self.voltage_data = np.zeros_like(self.time_data)
        self.current_data = np.zeros_like(self.time_data)
        self.power_data = np.zeros_like(self.time_data)

        # Set up the plot
        self.ax.set_xlim(-self.time_window, 0)
        self.ax.set_ylim(-10, 10)
        self.voltage_line, = self.ax.plot(self.time_data, self.voltage_data, label="Voltage (V)")
        self.current_line, = self.ax.plot(self.time_data, self.current_data, label="Current (A)")
        self.power_line, = self.ax.plot(self.time_data, self.power_data, label="Power (W)")
        self.ax.legend(loc="upper left")
        self.ax.set_xlabel("Time (s)")
        self.ax.set_ylabel("Signal")

        # Set up a timer to update the plot
        self.timer = QTimer()
        self.timer.setInterval(self.update_interval)
        self.timer.timeout.connect(self.update_plot)
        self.timer.start()

    def update_plot(self):
        # Generate pseudo-random data for voltage, current, and power
        self.voltage_data = np.roll(self.voltage_data, -1)
        self.current_data = np.roll(self.current_data, -1)
        self.power_data = np.roll(self.power_data, -1)

        self.voltage_data[-1] = np.sin(np.pi * np.random.rand()) * 8
        self.current_data[-1] = np.cos(np.pi * np.random.rand()) * 5
        self.power_data[-1] = self.voltage_data[-1] * self.current_data[-1] * 0.1  # Simplified calculation

        # Update the plot lines
        self.voltage_line.set_ydata(self.voltage_data)
        self.current_line.set_ydata(self.current_data)
        self.power_line.set_ydata(self.power_data)

        # Redraw the canvas
        self.canvas.draw()

def main():
    app = QApplication(sys.argv)
    main_window = RealTimePlotter()
    main_window.show()
    sys.exit(app.exec())

if __name__ == "__main__":
    main()

Example 6 - Utilization Monitor

You'll need to install psutil for this one. pip install psutil

import sys
import numpy as np
import psutil
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel
from PyQt6.QtCore import QTimer
from PyQt6.QtGui import QFont
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure


class SystemMonitor(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle("System Performance Monitor")
        self.setGeometry(100, 100, 800, 600)

        # Create central widget and layout
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)

        # Labels for instantaneous and average values
        self.cpu_label = QLabel("CPU: 0%")
        self.mem_label = QLabel("Memory: 0%")
        self.avg_cpu_label = QLabel("Average CPU: 0%")
        self.avg_mem_label = QLabel("Average Memory: 0%")

        # Set large font for labels
        font = QFont("Arial", 16)
        self.cpu_label.setFont(font)
        self.mem_label.setFont(font)
        self.avg_cpu_label.setFont(font)
        self.avg_mem_label.setFont(font)

        # Add labels to layout
        layout.addWidget(self.cpu_label)
        layout.addWidget(self.mem_label)
        layout.addWidget(self.avg_cpu_label)
        layout.addWidget(self.avg_mem_label)

        # Matplotlib Figure for plotting
        self.figure = Figure()
        self.canvas = FigureCanvas(self.figure)
        layout.addWidget(self.canvas)
        self.ax = self.figure.add_subplot(111)

        # Data storage
        self.time_window = 20  # seconds
        self.update_interval = 1000  # milliseconds
        self.time_data = np.linspace(-self.time_window, 0, num=int(self.time_window * 1000 / self.update_interval))
        self.cpu_data = np.zeros_like(self.time_data)
        self.mem_data = np.zeros_like(self.time_data)

        # Set up plot
        self.cpu_line, = self.ax.plot(self.time_data, self.cpu_data, label="CPU (%)")
        self.mem_line, = self.ax.plot(self.time_data, self.mem_data, label="Memory (%)")
        self.ax.legend(loc="upper left")
        self.ax.set_ylim(0, 100)
        self.ax.set_title("CPU and Memory Utilization")
        self.ax.set_xlabel("Time (s)")
        self.ax.set_ylabel("Utilization (%)")

        # Timer to update data
        self.timer = QTimer()
        self.timer.setInterval(self.update_interval)
        self.timer.timeout.connect(self.update_data)
        self.timer.start()

    def update_data(self):
        # Fetch new data
        cpu = psutil.cpu_percent()
        mem = psutil.virtual_memory().percent

        # Update data arrays
        self.cpu_data = np.roll(self.cpu_data, -1)
        self.mem_data = np.roll(self.mem_data, -1)
        self.cpu_data[-1] = cpu
        self.mem_data[-1] = mem

        # Update plot lines
        self.cpu_line.set_ydata(self.cpu_data)
        self.mem_line.set_ydata(self.mem_data)

        # Update labels
        self.cpu_label.setText(f"CPU: {cpu}%")
        self.mem_label.setText(f"Memory: {mem}%")
        self.avg_cpu_label.setText(f"Average CPU: {np.mean(self.cpu_data):.2f}%")
        self.avg_mem_label.setText(f"Average Memory: {np.mean(self.mem_data):.2f}%")

        # Redraw the canvas
        self.canvas.draw()


def main():
    app = QApplication(sys.argv)
    ex = SystemMonitor()
    ex.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

Conclusion

Overall, a positive experience.

  • Most simple examples generated by ChatGPT worked out of the box with minimal dependencies

  • Unsure how well it scales for more comprehensive applications.

Resources

Source: https://github.com/ericjameszimmerman/pyqt6-examples