© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
J. M. WillmanBeginning PyQthttps://doi.org/10.1007/978-1-4842-7999-1_16

16. Extra Projects

Joshua M Willman1  
(1)
Sunnyvale, CA, USA
 

Throughout this book, we have aimed to take a practical approach for creating applications in order to help you learn the fundamentals of GUI development. As you continue to use PyQt6 and Python, you will find yourself learning about other modules and classes that will also prove useful.

In some cases, this book has only scratched the surface of what you can do with PyQt. With so many modules, classes, and possibilities for customization provided by Qt, the potential for building GUIs is endless. To expand your experience with PyQt, this chapter takes a look at some additional Qt classes that we couldn’t fit into earlier chapters.

In this chapter, you will create projects for
  1. 1.

    Displaying directories and files using QFileSystemModel and QTreeView

     
  2. 2.

    Making a GUI that takes photos using QCamera and demonstrates how to make custom dialogs using QDialog

     
  3. 3.

    Creating a simple clock GUI with QDate and QTime

     
  4. 4.

    Exploring the QCalendarWidget class

     
  5. 5.

    Building Hangman with QPainter and other PyQt classes

     
  6. 6.

    Building the framework for a web browser using the QtWebEngineWidgets module

     
  7. 7.

    Creating tri-state QComboBox widgets

     

The explanations for each project will not go into great lengths of detail. Rather, they will focus on explaining the key points of each program and leave it up to you to research the details that you are unsure about, either by finding the answers in a different chapter or by searching online for help.

Project 16.1 – Directory Viewer GUI

For every operating system, there needs to be some method for a user to access the data and files located within it. The drives, directories, and files are stored in a hierarchical file system and presented to the user so that they only view the files that they are interested in seeing.

Whether you use a command line interface or a graphical user interface, there needs to be some way to create, remove, and rename files and directories. However, if you are already interacting with one interface, it may be more convenient to locate files or directories that you need in the application’s main window rather than opening new windows or other programs.

This project shows you how to set up an interface for viewing the files on your local system. There are two key classes that will be introduced in this project: QFileSystemModel, which grants you access to the file system on your computer, and QTreeView, which provides a visual representation of data using a tree-like structure. The directory viewer application can be seen in Figure 16-1.
Figure 16-1

Directory viewer displaying the local system’s directories

Explanation for the Directory Viewer GUI

Begin by using the main_window_template.py script from Chapter 5, and then import the necessary modules for this GUI. For this project, we will need to use the Model/View paradigm to view the data on your computer. For more information about Model/View programming, refer to Chapter 10. The code for the directory viewer is found in Listing 16-1.
# directory_viewer.py
# Import necessary modules
import sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
    QTreeView, QFrame, QFileDialog, QVBoxLayout)
from PyQt6.QtGui import QFileSystemModel, QAction
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initializeUI()
    def initializeUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(500, 400)
        self.setWindowTitle("16.1 – View Directory GUI")
        self.setUpMainWindow()
        self.createActions()
        self.createMenu()
        self.show()
    def setUpMainWindow(self):
        """Set up the QTreeView in the main window to
        display the contents of the local filesystem."""
        self.model = QFileSystemModel()
        self.model.setRootPath("")
        self.tree = QTreeView()
        self.tree.setIndentation(10)
        self.tree.setModel(self.model)
        # Set up container and layout
        frame = QFrame()
        frame_v_box = QVBoxLayout()
        frame_v_box.addWidget(self.tree)
        frame.setLayout(frame_v_box)
        self.setCentralWidget(frame)
    def createActions(self):
        """Create the application's menu actions."""
        # Create actions for Directories menu
        self.open_dir_act = QAction("Open Directory...")
        self.open_dir_act.triggered.connect(
            self.chooseDirectory)
        self.root_act = QAction("Return to Root")
        self.root_act.triggered.connect(
            self.returnToRootDirectory)
    def createMenu(self):
        """Create the application's menu bar."""
        self.menuBar().setNativeMenuBar(False)
        # Create file menu and add actions
        dir_menu = self.menuBar().addMenu("Directories")
        dir_menu.addAction(self.open_dir_act)
        dir_menu.addAction(self.root_act)
    def chooseDirectory(self):
        """Slot for selecting a directory to display."""
        file_dialog = QFileDialog(self)
        file_dialog.setFileMode(
            QFileDialog.FileMode.Directory)
        directory = file_dialog.getExistingDirectory(
            self, "Open Directory",
            "", QFileDialog.Option.ShowDirsOnly)
        self.tree.setRootIndex(self.model.index(directory))
    def returnToRootDirectory(self):
        """Slot for redisplaying the contents of the root
        directory."""
        self.tree.setRootIndex(self.model.index(""))
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
Listing 16-1

Code for the directory viewer GUI

The QFileSystemModel class provides the model we need to access data on the local file system. For PyQt6, this class is now located in QtGui. While not included in this project, you could also use QFileSystemModel to rename or remove files and directories, create new directories, or use it with other display widgets as part of a browser.

The QTreeView class will be used to display the contents of the model in a hierarchical tree view.

For this GUI, we will create a Directories menu with actions that will either let the user view a specific directory or return back to the root directory. A screenshot of the menu bar can be seen in Figure 16-2.
Figure 16-2

The menu for the directory viewer GUI

Create an instance of the QFileSystemModel class, model, and set the directory to the root path by passing an empty string to setRootPath(). You can set a different directory by passing a different path to setRootPath().

Finally, let’s set the model for the tree object to show the contents of the file system using setModel(). To choose a different directory, the user can select Open Directory… from the menu, and a file dialog will appear. A new directory can then be selected in the chooseDirectory() slot and set as the new root path to be displayed in the tree object using the QTreeView method setRootIndex().

If a new directory has been selected, you can use the slot, returnToRootDirectory(), that is triggered by root_act to redisplay the root directory.

Project 16.2 – Camera GUI

PyQt can do more than handle images, as it also includes modules for working with videos, audio, and other kinds of media. For this GUI, we’ll create a simple window that opens your computer’s webcam and displays its contents in a window. If the user presses the space bar, a custom QDialog appears, displaying the screenshot and prompting the user to save or reject the video. The main window is shown in Figure 16-3.
Figure 16-3

The camera GUI

Before beginning this project, make sure your version of PyQt6 is version 6.2 or higher. The multimedia classes were not included in version 6.1 and earlier. To check your version of PyQt6, open the Python shell and enter the following commands:
>>> import PyQt6
>>> from PyQt6.QtCore import PYQT_VERSION_STR
>>> print(PYQT_VERSION_STR)
6.2.1
>>> help(PyQt6)
You should see at least 6.2.1 appear as output in the shell. If you don’t, you can upgrade your version of PyQt6 by running the following command:
$ pip3 install PyQt6 --upgrade

Use pip instead of pip3 on Windows. You can use the Python help() function to get a list of all of the PyQt6 modules. Look through them and you should see QtMultimedia listed among the different modules.

Another way to make sure that the multimedia classes were installed is to open your Python shell and run the following code:
>>> from PyQt6 import QtMultimedia
Note

For those readers using macOS, you may have issues running this application if you are using the Z shell, also known as zsh. The bash shell used to be the default on macOS until recently. If you run into problems due to zsh, you can switch to using bash by entering chsh -s /bin/bash/ in the command line. If you want to switch back to zsh when you are finished, you will need to enter the command chsh -s /bin/zsh. Just be aware that when you do switch between shells, you will also need to install PyQt6 from PyPI or edit the paths in bash to locate PyQt6 and your other Python packages.

Explanation for the Camera GUI

Let’s take a look at how to use the multimedia classes to create a GUI for taking photos in Listing 16-2. To begin, we’ll use basic_window.py from Chapter 1.

To build a custom dialog that will display the images taken using the webcam, we’ll need QDialog and QDialogButtonBox from QtWidgets. The widget QDialogButtonBox is used to easily create and arrange standard buttons in dialog boxes. Have a look in the Appendix at the “QDialog” subsection for more information about button types.

There have been numerous updates when it comes to the multimedia classes in PyQt6. The QtMultimedia module provides access to a number of multimedia tools that can handle audio, videos, and cameras. The QCamera class provides the interface to work with camera devices. We can use QImageCapture to record or take pictures of media objects, such as QCamera. QMediaDevices supplies information about available cameras or audio devices.

From the QtMultimediaWidgets module, the QVideoWidget class sets up and displays the camera or video object’s output.
# camera.py
# Import necessary modules
import os, sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
    QDialog, QDialogButtonBox, QVBoxLayout)
from PyQt6.QtCore import Qt, QDate
from PyQt6.QtGui import QPixmap
from PyQt6.QtMultimedia import (QCamera, QImageCapture,
    QMediaDevices, QMediaCaptureSession)
from PyQt6.QtMultimediaWidgets import QVideoWidget
class ImageDialog(QDialog):
    def __init__(self, id, image):
        """Custom QDialog that displays the image taken."""
        super().__init__()
        self.id = id
        self.setWindowTitle(f"Image #{id}")
        self.setMinimumSize(400, 300)
        self.pixmap = QPixmap().fromImage(image)
        image_label = QLabel()
        image_label.setPixmap(self.pixmap)
        # Create the buttons that appear in the dialog
        self.button_box = QDialogButtonBox(
            QDialogButtonBox.StandardButton.Save |
            QDialogButtonBox.StandardButton.Cancel)
        self.button_box.accepted.connect(self.accept)
        self.button_box.rejected.connect(self.reject)
        dialog_v_box = QVBoxLayout()
        dialog_v_box.addWidget(image_label)
        dialog_v_box.addWidget(self.button_box)
        self.setLayout(dialog_v_box)
    def accept(self):
        """Reimplement accept() method to save the image
        file in the images directory."""
        file_format = "png"
        today = QDate().currentDate().toString(
            Qt.DateFormat.ISODate)
        file_name = f"images/image{self.id}_{today}.png"
        self.pixmap.save(file_name, file_format)
        super().accept()
class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()
    def initializeUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(500, 400)
        self.setWindowTitle("16.2 - Camera GUI")
        self.setUpMainWindow()
        self.show()
    def setUpMainWindow(self):
        """Create and arrange widgets in the main window."""
        # Create the image output directory
        exists = os.path.exists("images")
        if not exists:
            os.makedirs("images")
        info_label = QLabel(
            "Press 'Spacebar' to take pictures.")
        info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        # Create the camera that uses the computer's
        # default camera
        self.camera = QCamera(
            QMediaDevices.defaultVideoInput())
        # Create an instance of the class used to capture
        # images
        self.image_capture = QImageCapture(self.camera)
        self.image_capture.imageCaptured.connect(
            self.viewImage)
        video_widget = QVideoWidget(self)
        # QMediaCaptureSession handles playing and capturing
        # video and audio
        self.media_capture_session = QMediaCaptureSession()
        self.media_capture_session.setCamera(self.camera)
        self.media_capture_session.setImageCapture(
            self.image_capture)
        self.media_capture_session.setVideoOutput(
            video_widget)
        self.camera.start()
        main_v_box = QVBoxLayout()
        main_v_box.addWidget(info_label)
        main_v_box.addWidget(video_widget, 1)
        self.setLayout(main_v_box)
    def viewImage(self, id, preview):
        """Open a dialog to preview the image."""
        self.image_dialog = ImageDialog(id, preview)
        self.image_dialog.open()
    def keyPressEvent(self, event):
        """Reimplement to capture the image when the space
        bar is pressed."""
        if event.key() == Qt.Key.Key_Space:
            self.image_capture.capture()
    def closeEvent(self, event):
        if self.camera.isActive():
            self.camera.stop()
        event.accept()
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
Listing 16-2

Example code that shows how to use the QCamera class and build custom dialogs

In order to create a customized dialog, you’ll need to create a new class that inherits QDialog. An instance of ImageDialog will display the image taken from the camera on a QLabel. The argument id refers to the id value returned by QImageCapture when a picture is taken. Since QImageCapture also returns a QImage object, we’ll need to change image to a pixmap using the QPixmap method fromImage() before setting the picture on the label.

The button_box instance is a QDialogButtonBox that contains Save and Cancel keys. The two buttons are added to button_box and separated by a pipe key, |. When a button is clicked, it emits a signal. Generally, those signals are accepted or rejected when working with dialog buttons. We’ll attach those signals to built-in slots, accept() and reject(). While these are standard slots, you could also connect the accepted or rejected signals to custom slots and perform other operations. We’ll do just that for accept() but use the default functionality for reject().

For this example, we’ll reimplement accept() to save the pixmap to a folder called images. The file extension, .png, still needs to be included to avoid issues with saving images (especially on Windows).

An example of ImageDialog along with its buttons can be seen in Figure 16-4.
Figure 16-4

A custom QDialog instance that displays the image taken by the camera

The last step is to arrange the widgets in a layout just like you normally do with other windows.

Moving on to setUpMainWindow() in the MainWindow class, let’s first create the images directory for saving images if it does not already exist. The window consists of a label for providing instructions and a QVideoWidget object for showing the camera’s contents.

QMediaDevices can be used to specify a camera to use or provide a list of possible devices to the user. For cameras, we’ll need to use QCameraDevice to detect available cameras. The QMediaDevices method defaultVideoInput() locates a computer’s default QCameraDevice. You can then pass that camera device to QCamera when creating the camera instance.

Using QImageCapture, the user is able to take pictures. The imageCaptured signal is used to detect when a picture is taken. Image capturing for this GUI is handled by keyPressEvent(). When the space bar is pressed, QImageCapture.capture() takes a picture, thereby emitting the imageCaptured signal. This calls ViewImage(), where the picture’s id and QImage object, preview, are passed to an ImageDialog instance. The open() method is used to open the dialog. If the user clicks the Save button, the image is converted to a pixmap and saved in the images folder (handled in the accept() slot of ImageDialog).

Back in setUpMainWindow(), QMediaCaptureSession will manage the capturing of the camera that is displayed in video_widget.

Project 16.3 – Simple Clock GUI

PyQt6 also provides classes for dealing with dates, QDate, or time, QTime. The QDateTime class supplies functions for working with both dates and time. All three of these classes include methods for handling time-related features.

Let’s take a brief look at the QDateTime class. The following snippet of code creates an instance of QDateTime that prints the current date and time using the currentDateTime() method:
now = QDateTime.currentDateTime()
print(now.toString("MMMM dd, yyyy hh:mm:ss AP”))
The current date and time is printed to the screen with the following format:
November 07, 2021 03:34:11 PM
This format is used to display the time in GUI in Figure 16-5.
Figure 16-5

The clock GUI displaying the current calendar date and clock time

In PyQt6, you can also use the enum Qt.DateFormat to utilize standard date and time format types. These include ISO 8601 format (using the flag ISODate) and RFC 2822 (using the flag RFC2822Date). The toString() method returns the date and time as a string. QDateTime also handles daylight saving time, different time zones, and the manipulation of times and dates such as adding or subtracting months, days, or hours.

If you only need to work with the individual dates and times, QDate and QTime also provide similar functions as you shall see in the following example.

Explanation for the Clock GUI

We’ll use the basic_window.py from Chapter 1 as the base for this program. Start by importing the necessary modules, including QDate, QTime, and QTimer from the QtCore module in Listing 16-3.

The QTimer class will be used to create a timer object to keep track of the time that has passed and update the labels that hold the date and time accordingly. The timer is set up in initializeUI(), and its timeout signal is connected to the updateDateTime() slot. The timeout signal is emitted every second.
# clock.py
# Import necessary modules
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
    QVBoxLayout)
from PyQt6.QtCore import Qt, QDate, QTime, QTimer
class DisplayTime(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()
    def initializeUI(self):
        """Set up the application's GUI."""
        self.setGeometry(100, 100, 250, 100)
        self.setWindowTitle("16.3 – QDateTime Example")
        self.setStyleSheet("background-color: black")
        self.setUpMainWindow()
        # Create timer object
        timer = QTimer(self)
        timer.timeout.connect(self.updateDateTime)
        timer.start(1000)
        self.show()
    def setUpMainWindow(self):
        """Create labels that will display current date and
        time in the main window."""
        current_date, current_time = self.getDateTime()
        self.date_label = QLabel(current_date)
        self.date_label.setStyleSheet(
            "color: white; font: 16px Courier")
        self.time_label = QLabel(current_time)
        self.time_label.setStyleSheet(
            """color: white;
            border-color: white;
            border-width: 2px;
            border-style: solid;
            border-radius: 4px;
            padding: 10px;
            font: bold 24px Courier""")
        # Create layout and add widgets
        v_box = QVBoxLayout()
        v_box.addWidget(self.date_label,
            alignment=Qt.AlignmentFlag.AlignCenter)
        v_box.addWidget(self.time_label,
            alignment=Qt.AlignmentFlag.AlignCenter)
        self.setLayout(v_box)
    def getDateTime(self):
        """Returns current date and time."""
        date = QDate.currentDate().toString("MMMM dd, yyyy")
        time = QTime.currentTime().toString("hh:mm:ss AP")
        return date, time
    def updateDateTime(self):
        """Slot that updates date and time values."""
        date = QDate.currentDate().toString("MMMM dd, yyyy")
        time = QTime.currentTime().toString("hh:mm:ss AP")
        self.date_label.setText(date)
        self.time_label.setText(time)
        return date, time
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = DisplayTime()
    sys.exit(app.exec())
Listing 16-3

Code for the clock GUI

In order to get the current date and time, the values are retrieved using the currentDate() and currentTime() methods in the getDateTime() method. These are then returned and set as the current_date and current_time.

The values for date and time are both set using a sequence of characters to create a format string. For date, we’ll present the full month’s name (MMMM), the day (dd), and the full year (yyyy). The time instance will display hours (hh), minutes (mm), seconds (ss), and AM or PM (AP).

The labels that will display the date and time are then instantiated, styled, and added to the layout in setUpMainWindow(). The values of the labels are updated using the updateDateTime() slot that is connected to timer.

Project 16.4 – Calendar GUI

This project takes a look at how to set up the QCalendarWidget class and use a few of its functions. PyQt makes adding a monthly calendar to your applications rather effortless. The calendar can be seen in Figure 16-6.
Figure 16-6

The calendar GUI that displays the calendar, the current date, and the widgets that allow the user to search for dates within a specified time range

The QCalendarWidget class provides a calendar that already has a number of other useful widgets and functions built-in. For example, the calendar already includes a horizontal header that includes widgets for changing the month and the year and a vertical header that displays the week number. The class also includes signals that are emitted whenever the dates, months, and years on the calendar are changed. The look of your calendar will vary depending upon the platform that you are using to run the application.

The QDateEdit widget is used in this application to restrict the date range a user can select, specified by minimum and maximum values.

Explanation for the Calendar GUI

We can start with basic_window.py script from Chapter 1. After importing the modules needed for the calendar GUI in Listing 16-4, the styles for the QLabel and QGroupBox widgets are prepared using style_sheet.
# calendar.py
# Import necessary modules
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
    QCalendarWidget, QDateEdit, QGroupBox, QHBoxLayout,
    QGridLayout)
from PyQt6.QtCore import Qt, QDate
style_sheet = """
    QLabel{
        padding: 5px;
        font: 18px
    }
    QLabel#DateSelected{
        font: 24px
    }
    QGroupBox{
        border: 2px solid gray;
        border-radius: 5px;
        margin-top: 1ex;
        font: 14px
    }
"""
class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()
    def initializeUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(500, 400)
        self.setWindowTitle("16.4 – Calendar GUI")
        self.setUpMainWindow()
        self.show()
    def setUpMainWindow(self):
        """Create and arrange widgets in the main window."""
        self.calendar = QCalendarWidget()
        self.calendar.setGridVisible(True)
        self.calendar.setMinimumDate(QDate(1900, 1, 1))
        self.calendar.setMaximumDate(QDate(2200, 1, 1))
        # Connect to newDateSelection() slot when currently
        # selected date is changed
        self.calendar.selectionChanged.connect(
            self.newDateSelection)
        current = QDate.currentDate().toString(
            "MMMM dd, yyyy")
        self.current_label = QLabel(current)
        self.current_label.setObjectName("DateSelected")
        # Create current, minimum, and maximum QDateEdit
        # widgets
        min_date_label = QLabel("Minimum Date:")
        self.min_date_edit = QDateEdit()
        self.min_date_edit.setDisplayFormat("MMM d yyyy")
        self.min_date_edit.setDateRange(
            self.calendar.minimumDate(),
            self.calendar.maximumDate())
        self.min_date_edit.setDate(
            self.calendar.minimumDate())
        self.min_date_edit.dateChanged.connect(
            self.minDatedChanged)
        current_date_label = QLabel("Current Date:")
        self.current_date_edit = QDateEdit()
        self.current_date_edit.setDisplayFormat("MMM d yyyy")
        self.current_date_edit.setDate(
            self.calendar.selectedDate())
        self.current_date_edit.setDateRange(
            self.calendar.minimumDate(),
            self.calendar.maximumDate())
        self.current_date_edit.dateChanged.connect(
            self.selectionDateChanged)
        max_date_label = QLabel("Maximum Date:")
        self.max_date_edit = QDateEdit()
        self.max_date_edit.setDisplayFormat("MMM d yyyy")
        self.max_date_edit.setDateRange(
            self.calendar.minimumDate(),
            self.calendar.maximumDate())
        self.max_date_edit.setDate(
            self.calendar.maximumDate())
        self.max_date_edit.dateChanged.connect(
            self.maxDatedChanged)
        # Add widgets to group box and add to grid layout
        dates_gb = QGroupBox("Set Dates")
        dates_grid = QGridLayout()
        dates_grid.addWidget(self.current_label, 0, 0, 1, 2,
            Qt.AlignmentFlag.AlignAbsolute)
        dates_grid.addWidget(min_date_label, 1, 0)
        dates_grid.addWidget(self.min_date_edit, 1, 1)
        dates_grid.addWidget(current_date_label, 2, 0)
        dates_grid.addWidget(self.current_date_edit, 2, 1)
        dates_grid.addWidget(max_date_label, 3, 0)
        dates_grid.addWidget(self.max_date_edit, 3, 1)
        dates_gb.setLayout(dates_grid)
        # Create and set main window's layout
        main_h_box = QHBoxLayout()
        main_h_box.addWidget(self.calendar)
        main_h_box.addWidget(dates_gb)
        self.setLayout(main_h_box)
    def selectionDateChanged(self, date):
        """Update current_date_edit when the calendar's
        selected date changes. """
        self.calendar.setSelectedDate(date)
    def minDatedChanged(self, date):
        """Update the calendar's minimum date.
        Update max_date_edit to avoid conflicts with
        maximum and minimum dates."""
        self.calendar.setMinimumDate(date)
        self.max_date_edit.setDate(
            self.calendar.maximumDate())
    def maxDatedChanged(self, date):
        """Update the calendar's maximum date.
        Update min_date_edit to avoid conflicts with
        minimum and maximum dates."""
        self.calendar.setMaximumDate(date)
        self.min_date_edit.setDate(
            self.calendar.minimumDate())
    def newDateSelection(self):
        """Update date in current_label and current_date_edit
        widgets when a new date is selected."""
        date = self.calendar.selectedDate().toString(
            "MMMM dd, yyyy")
        self.current_date_edit.setDate(
            self.calendar.selectedDate())
        self.current_label.setText(date)
if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setStyleSheet(style_sheet)
    window = MainWindow()
    sys.exit(app.exec())
Listing 16-4

The calendar GUI code

Creating an instance of QCalendarWidget is very simple.
        self.calendar = QCalendarWidget()

Next, we set a few of the calendar object’s parameters. Setting setGridVisible() to True will make the grid lines visible. In order to specify the date range that a user can select in the calendar, we set the minimum and maximum date values using the QCalendar methods setMinimumDate() and setMaximumDate().

Whenever a date is selected in the calendar widget, it emits a selectionChanged signal. This signal is connected to the newDateSelection() slot that updates the date on current_label and in current_date_edit. Selecting a value in current_date_edit widget will also change the other values.

The QCalendarWidget class also has a number of functions that allow you to configure its behaviors and appearance. For this project, we create three QDateEdit widgets that will allow the user to change the minimum and maximum values for the date range, as well as the current date selected in the calendar. These widgets can be seen on the right side of the GUI in Figure 16-6.

A displayed format for the date in the QDateEdit widget can be set using setDisplayFormat(). The date edit objects are also given a date range using setDateRange(). The following line of code is an example of how to set the min_date_edit widget’s date range by using ranges set earlier for the calendar object:
        self.min_date_edit.setDateRange(
            self.calendar.minimumDate(),
            self.calendar.maximumDate())

When a date is changed in a date edit widget, it generates a dateChanged signal. Each one of the QDateEdit widgets is connected to a corresponding slot that will update the calendar’s minimum, maximum, or current date values depending upon which date edit widget is changed. The method for changing the dates is adapted from the Qt document website.1

Finally, the label and date edit widgets are arranged in a QGroupBox, added to a QGridLayout instance, and nested into the main window’s layout in setUpMainWindow().

Project 16.5 – Hangman GUI

PyQt can be used to create a variety of different kinds of applications. Throughout this book, we have looked at quite a few ideas for building GUIs. For this next project, we will take a look at how to use QPainter and a few other classes to build a game – Hangman. While Hangman is a simple game to play, it can be used to teach a few of the fundamental concepts for using PyQt to create games. The Hangman interface can be seen in Figure 16-7.
Figure 16-7

The Hangman application. Can you save him?

For this application, the player can select from one of the 26 English letters to guess a letter in an unknown word. As each letter is chosen, they will become disabled in the window. If the letter is correct, it will be revealed to the player. Otherwise, a part of the Hangman figure’s body is drawn on the screen. If all of the letters are correctly guessed, then the player wins. There are a total of six turns. Whether or not the player wins or loses, a dialog will be displayed to inform the player and allow them to quit or to continue playing.

Be sure to download words.txt from the files folder in the GitHub repository before beginning this project.

Explanation for the Hangman GUI

A variety of classes are used in the Hangman GUI in Listing 16-5, including different widgets from QtWidgets, as well as classes used for drawing from QtCore and QtGui. The style sheet used is to change the style properties of the widgets and to handle the appearance of the buttons when they are pressed.
# hangman.py
# Import necessary modules
import sys, random
from PyQt6.QtWidgets import (QApplication, QMainWindow,
    QWidget, QPushButton, QLabel, QFrame, QButtonGroup,
    QHBoxLayout, QVBoxLayout, QMessageBox, QSizePolicy)
from PyQt6.QtCore import Qt, QRect, QLine
from PyQt6.QtGui import QPainter, QPen, QBrush, QColor
style_sheet = """
    QWidget{
        background-color: #FFFFFF
    }
    QLabel#Word{
        font: bold 20px;
        qproperty-alignment: AlignCenter
    }
    QPushButton#Letters{
        background-color: #1FAEDE;
        color: #D2DDE1;
        border-style: solid;
        border-radius: 3px;
        border-color: #38454A;
        font: 28px
    }
    QPushButton#Letters:pressed{
        background-color: #C86354;
        border-radius: 4px;
        padding: 6px;
        color: #DFD8D7
    }
    QPushButton#Letters:disabled{
        background-color: #BBC7CB
    }
"""
class DrawingLabel(QLabel):
    def __init__(self):
        """The hangman is drawn on a QLabel object, rather
        than on the main window. This class handles the
        drawing."""
        super().__init__()
        self.height = 200
        self.width = 300
        self.incorrect_letter = False
        self.incorrect_turns = 0
        self.wrong_parts_list = []
    def drawHangmanBackground(self, painter):
        """Draw the gallows for the GUI."""
        painter.setBrush(QBrush(QColor("#000000")))
        # drawRect(x, y, width, height)
        painter.drawRect(int(self.width / 2) - 40,
            self.height, 150, 4)
        painter.drawRect(int(self.width / 2), 0, 4, 200)
        painter.drawRect(int(self.width / 2), 0, 60, 4)
        painter.drawRect(int(self.width / 2) + 60, 0, 4, 40)
    def drawHangmanBody(self, painter):
        """Create and draw body parts for hangman."""
        if "head" in self.wrong_parts_list:
            head = QRect(int(self.width / 2) + 42, 40, 40, 40)
            painter.setPen(QPen(QColor("#000000"), 3))
            painter.setBrush(QBrush(QColor("#FFFFFF")))
            painter.drawEllipse(head)
        if "body" in self.wrong_parts_list:
            body = QRect(int(self.width / 2) + 60, 80, 2, 55)
            painter.setBrush(QBrush(QColor("#000000")))
            painter.drawRect(body)
        if "right_arm" in self.wrong_parts_list:
            right_arm = QLine(int(self.width / 2) + 60, 85,
                int(self.width / 2) + 50,
                int(self.height / 2) + 30)
            pen = QPen(Qt.GlobalColor.black, 3,
                Qt.PenStyle.SolidLine)
            painter.setPen(pen)
            painter.drawLine(right_arm)
        if "left_arm" in self.wrong_parts_list:
            left_arm = QLine(int(self.width / 2) + 62, 85,
                int(self.width / 2) + 72,
                int(self.height / 2) + 30)
            painter.drawLine(left_arm)
        if "right_leg" in self.wrong_parts_list:
            right_leg = QLine(int(self.width / 2) + 60, 135,
                int(self.width / 2) + 50,
                int(self.height / 2) + 75)
            painter.drawLine(right_leg)
        if "left_leg" in self.wrong_parts_list:
            left_leg = QLine(int(self.width / 2) + 62, 135,
                int(self.width / 2) + 72,
                int(self.height / 2) + 75)
            painter.drawLine(left_leg)
        # Reset variable
        self.incorrect_letter = False
    def paintEvent(self, event):
        """Create QPainter object and handle painting
        events."""
        painter = QPainter()
        painter.begin(self)
        self.drawHangmanBackground(painter)
        if self.incorrect_letter == True:
            self.drawHangmanBody(painter)
        painter.end()
class Hangman(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initializeUI()
    def initializeUI(self):
        """Set up the application's GUI."""
        self.setFixedSize(400, 500)
        self.setWindowTitle("16.5 - Hangman GUI")
        self.newGame()
        self.show()
    def newGame(self):
        """Create new Hangman game. Sets up the objects
        for the main window."""
        self.setUpHangmanBoard()
        self.setUpWord()
        self.setUpBoard()
    def setUpHangmanBoard(self):
        """Set up label object to display hangman."""
        self.hangman_label = DrawingLabel()
        self.hangman_label.setSizePolicy(
            QSizePolicy.Policy.Expanding,
            QSizePolicy.Policy.Expanding)
    def setUpWord(self):
        """Open words file and choose random word.
        Create labels that will display '_' depending
        upon length of word."""
        words = self.openFile()
        self.chosen_word = random.choice(words).upper()
        #print(self.chosen_word)
        # Keep track of correct guesses
        self.correct_counter = 0
        # Keep track of label objects.
        # Is used for updating the text on the labels
        self.labels = []
        word_h_box = QHBoxLayout()
        for letter in self.chosen_word:
            self.letter_label = QLabel("___")
            self.labels.append(self.letter_label)
            self.letter_label.setObjectName("Word")
            word_h_box.addWidget(self.letter_label)
        self.word_frame = QFrame()
        self.word_frame.setLayout(word_h_box)
    def setUpBoard(self):
        """Set up objects and layouts for keyboard and main
        window."""
        top_row_list = ["A", "B", "C", "D", "E",
            "F", "G", "H"]
        mid_row_list = ["I", "J", "K", "L", "M",
            "N", "O", "P", "Q"]
        bot_row_list = ["R", "S", "T", "U", "V",
            "W", "X", "Y", "Z"]
        # Create buttongroup to keep track of letters
        self.keyboard_bg = QButtonGroup()
        # Set up keys in the top row
        top_row_h_box = QHBoxLayout()
        for letter in top_row_list:
            button = QPushButton(letter)
            button.setObjectName("Letters")
            top_row_h_box.addWidget(button)
            self.keyboard_bg.addButton(button)
        top_frame = QFrame()
        top_frame.setLayout(top_row_h_box)
        # Set up keys in the middle row
        mid_row_h_box = QHBoxLayout()
        for letter in mid_row_list:
            button = QPushButton(letter)
            button.setObjectName("Letters")
            mid_row_h_box.addWidget(button)
            self.keyboard_bg.addButton(button)
        mid_frame = QFrame()
        mid_frame.setLayout(mid_row_h_box)
        # Set up keys in the bottom row
        bot_row_h_box = QHBoxLayout()
        for letter in bot_row_list:
            button = QPushButton(letter)
            button.setObjectName("Letters")
            bot_row_h_box.addWidget(button)
            self.keyboard_bg.addButton(button)
        bot_frame = QFrame()
        bot_frame.setLayout(bot_row_h_box)
        # Connect buttons in button group to slot
        self.keyboard_bg.buttonClicked.connect(
            self.buttonPushed)
        keyboard_v_box = QVBoxLayout()
        keyboard_v_box.addWidget(top_frame)
        keyboard_v_box.addWidget(mid_frame)
        keyboard_v_box.addWidget(bot_frame)
        keyboard_frame = QFrame()
        keyboard_frame.setLayout(keyboard_v_box)
        # Create main layout and add widgets
        main_v_box = QVBoxLayout()
        main_v_box.addWidget(self.hangman_label)
        main_v_box.addWidget(self.word_frame)
        main_v_box.addWidget(keyboard_frame)
        # Create central widget for main window
        central_widget = QWidget()
        central_widget.setLayout(main_v_box)
        self.setCentralWidget(central_widget)
    def buttonPushed(self, button):
        """Handle buttons from the button group and
        game logic."""
        button.setEnabled(False)
        body_parts_list = ["head", "body", "right_arm",
            "left_arm", "right_leg", "left_leg"]
        # When the user guesses incorrectly and the number of
        # incorrect turns is not equal to 6 (the number of
        # body parts)
        if button.text() not in self.chosen_word and
            self.hangman_label.incorrect_turns <= 5:
            self.hangman_label.incorrect_turns += 1
            index = self.hangman_label.incorrect_turns - 1
            self.hangman_label.wrong_parts_list.append(
                body_parts_list[index])
            self.hangman_label.incorrect_letter = True
        # When a correct letter is chosen, update labels and
        # correct counter
        elif button.text() in self.chosen_word and
            self.hangman_label.incorrect_turns <= 5:
            self.hangman_label.incorrect_letter = True
            for i in range(len(self.chosen_word)):
                if self.chosen_word[i] == button.text():
                    self.labels[i].setText(button.text())
                    self.correct_counter += 1
        # Call update before checking winning conditions
        self.update()
        # User wins when the number of correct letters equals
        # the length of the word
        if self.correct_counter == len(self.chosen_word):
            self.displayDialogs("win")
        # Game over if number of incorrect turns equals
        # the number of body parts. Reveal word to user
        if self.hangman_label.incorrect_turns == 6:
            for i in range(len(self.chosen_word)):
                self.labels[i].setText(self.chosen_word[i])
            self.displayDialogs("game_over")
    def openFile(self):
        """Open words.txt file."""
        try:
            with open("files/words.txt", 'r') as f:
                word_list = f.read().splitlines()
                return word_list
        except FileNotFoundError:
            print("File Not Found.")
            ex_list = ["nofile"]
            return ex_list
    def displayDialogs(self, text):
        """Display win and game over dialog boxes."""
        if text == "win":
            message = QMessageBox().question(self, "Win!",
                "You Win! NEW GAME?",
                QMessageBox.StandardButton.Yes |
                QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No)
        elif text == "game_over":
            message = QMessageBox().question(
                self, "Game Over",
                "Game Over NEW GAME?",
                QMessageBox.StandardButton.Yes |
                QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No)
        if message == QMessageBox.StandardButton.No:
            self.close()
        else:
            self.newGame()
if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setStyleSheet(style_sheet)
    window = Hangman()
    sys.exit(app.exec())
Listing 16-5

Code for the Hangman GUI

This program contains two classes: DrawingLabel and Hangman.

Creating the Drawing Class

The DrawingLabel class inherits from QLabel and handles the different paint events that will be drawn on the label object in the main window. The paintEvent() function is called in a class that inherits from QLabel so that way the paint events occur on the label and are not covered up by the main window.

In order to use the DrawingLabel() class, an instance is created in the Hangman class method setupHangmanBoard():
        self.hangman_label = DrawingLabel()

The paintEvent() function sets up QPainter and handles the two painting methods: drawHangmanBackground(), which draws the gallows of the Hangman game onto the label, and drawHangmanBody(), which only draws the body parts if they are contained in the part_list.

Creating the Main Window Class

The Hangman class starts by initializing the GUI window and calling the newGame() method. First, the Hangman board is created as an instance of the DrawingLabel class. Then, setUpBoard() selects a random word from the words.txt file. The labels that will represent the letters of the chosen word are replaced with underscore characters, appended to the labels list, and added to the horizontal layout of the word_frame object.

Finally, we need to set up the keyboard push buttons, layouts, and the game logic in setUpBoard(). Three rows of push buttons that represent the letters of the alphabet are controlled by one QButtonGroup object, keyboard_bg.

When one button is pushed, it generates a signal that calls the buttonPushed() slot. When a push button is pressed, it is disabled by passing False to setEnabled().

The list of body parts, body_parts_list, contains the six body part names. If the player guesses an incorrect letter, the name is appended to the wrong_parts_list and checked for in the DrawingLabel method drawHangmanBody() function. Using this method ensures that all necessary parts are drawn with their different styles when paintEvent() is called. Otherwise, the labels are updated to display the correct letters in the appropriate positions if the player guesses correctly.

If the player wins or loses, a QMessageBox will appear and allow the user to close the application or continue. If Yes is selected, newGame() is called.

Project 16.6 – Web Browser GUI

A web browser is a graphical user interface that allows access to information on the World Wide Web. A user can enter a Uniform Resource Locator (URL) into an address bar and request content for a website from a web server to be displayed on their local device, including text, image, and video data. URLs are generally prefixed with http, a protocol used for fetching and transmitting requested web pages, or https, for encrypted communication between browsers and websites.

Qt provides quite a few classes for network communication, WebSockets, support for accessing the World Wide Web, and more. This project introduces PyQt’s classes for adding web integration into GUIs.

For the following project, we will take a look at Qt’s WebEngine core classes, specifically the QtWebEngineWidgets module for creating widget-based web applications. The WebEngine core classes provide a web browser engine that can be used to embed web content into your applications. The QtWebEngineCore module uses Chromium as its back end. Chromium is an open source software from Google that can be used to create web browsers.

The web browser GUI that we will create in Figure 16-8 serves as a framework for creating your own web browser and includes the following features:
  • Ability to open multiple windows and tabs, either by using the application’s menu or shortcut hotkeys

  • A navigation bar that is made up of back, forward, refresh, stop and home buttons, and the address bar for entering URLs

  • The web engine view widget created using QWebEngineView

  • A status bar

  • A progress bar that relays feedback to the user about loading web pages

Figure 16-8

The web browser GUI displaying the menu bar, toolbar, different tabs, the logo for my blog, redhuli.io, and the progress bar at the bottom

Note

You will need to install the QtWebEngineWidgets module. To do so, enter the following command into the command line: pip3 install PyQt6-WebEngine (use pip for Windows).

In addition, make sure that you download the icons folder from this chapter’s GitHub repository.

Explanation for Web Browser GUI

You can begin with main_window_template.py from Chapter 5 as your foundation for this application. Two new classes are introduced in Listing 16-6: QUrl is used for managing and constructing URLs, and QWebEngineView is used for creating the main component for rendering content from the Web, the web engine view (denoted as web_view in the code).
# web_browser.py
# Import necessary modules
import os, sys
from PyQt6.QtWidgets import (QApplication, QMainWindow,
    QWidget, QLabel, QLineEdit, QTabWidget, QToolBar,
    QProgressBar, QStatusBar, QVBoxLayout)
from PyQt6.QtCore import QSize, QUrl
from PyQt6.QtGui import QIcon, QAction
from PyQt6.QtWebEngineWidgets import QWebEngineView
style_sheet = """
    QTabWidget:pane{
        border: none
    }
"""
class WebBrowser(QMainWindow):
    def __init__(self):
        super().__init__()
        # Create lists that will keep track of the new
        # windows, tabs and urls
        self.window_list = []
        self.list_of_web_pages = []
        self.list_of_urls = []
        self.initializeUI()
    def initializeUI(self):
        self.setMinimumSize(300, 200)
        self.setWindowTitle("16.6 – Web Browser")
        self.setWindowIcon(QIcon(os.path.join("icons",
            "pyqt_logo.png")))
        self.sizeMainWindow()
        self.createToolbar()
        self.setUpMainWindow()
        self.createActions()
        self.createMenu()
        self.show()
    def setUpMainWindow(self):
        """Create the QTabWidget object and the different
        pages for the main window. Handle when a tab is
        closed."""
        self.tab_bar = QTabWidget()
        # Add close buttons to tabs
        self.tab_bar.setTabsClosable(True)
        # Hides tab bar when less than 2 tabs
        self.tab_bar.setTabBarAutoHide(True)
        self.tab_bar.tabCloseRequested.connect(self.closeTab)
        # Create a tab
        self.main_tab = QWidget()
        self.tab_bar.addTab(self.main_tab, "New Tab")
        # Call method that sets up each page
        self.setUpTab(self.main_tab)
        self.setCentralWidget(self.tab_bar)
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
    def createActions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.new_window_act = QAction("New Window", self)
        self.new_window_act.setShortcut("Ctrl+N")
        self.new_window_act.triggered.connect(
            self.openNewWindow)
        self.new_tab_act = QAction("New Tab", self)
        self.new_tab_act.setShortcut("Ctrl+T")
        self.new_tab_act.triggered.connect(self.openNewTab)
        self.quit_act = QAction("Quit Browser", self)
        self.quit_act.setShortcut("Ctrl+Q")
        self.quit_act.triggered.connect(self.close)
    def createMenu(self):
        """Create the application"s menu bar."""
        self.menuBar().setNativeMenuBar(False)
        # Create File menu and add actions
        file_menu = self.menuBar().addMenu("File")
        file_menu.addAction(self.new_window_act)
        file_menu.addAction(self.new_tab_act)
        file_menu.addSeparator()
        file_menu.addAction(self.quit_act)
    def createToolbar(self):
        """Set up the navigation toolbar."""
        tool_bar = QToolBar("Address Bar")
        tool_bar.setIconSize(QSize(30, 30))
        self.addToolBar(tool_bar)
        # Create toolbar actions
        back_page_button = QAction(
            QIcon(os.path.join("icons", "back.png")),
            "Back", self)
        back_page_button.triggered.connect(
            self.backPageButton)
        forward_page_button = QAction(
            QIcon(os.path.join("icons", "forward.png")),
            "Forward", self)
        forward_page_button.triggered.connect(
            self.forwardPageButton)
        refresh_button = QAction(
            QIcon(os.path.join("icons", "refresh.png")),
            "Refresh", self)
        refresh_button.triggered.connect(self.refreshButton)
        home_button = QAction(
            QIcon(os.path.join("icons", "home.png")),
            "Home", self)
        home_button.triggered.connect(self.homeButton)
        stop_button = QAction(
            QIcon(os.path.join("icons", "stop.png")),
            "Stop", self)
        stop_button.triggered.connect(self.stopButton)
        # Set up the address bar
        self.address_line = QLineEdit()
        # addAction() is used here to merely display the icon
        # in the line edit
        self.address_line.addAction(
            QIcon("icons/search.png"),
            QLineEdit.ActionPosition.LeadingPosition)
        self.address_line.setPlaceholderText(
            "Enter website address")
        self.address_line.returnPressed.connect(
            self.searchForUrl)
        tool_bar.addAction(home_button)
        tool_bar.addAction(back_page_button)
        tool_bar.addAction(forward_page_button)
        tool_bar.addAction(refresh_button)
        tool_bar.addWidget(self.address_line)
        tool_bar.addAction(stop_button)
    def setUpWebView(self):
        """Create the QWebEngineView object that is used to
        view web docs. Set up the main page, and handle
        web_view signals."""
        web_view = QWebEngineView()
        web_view.setUrl(QUrl("https://google.com"))
        # Create page loading progress bar that is displayed
        # in the status bar.
        self.page_load_pb = QProgressBar()
        self.page_load_label = QLabel()
        web_view.loadProgress.connect(self.updateProgressBar)
        # Display url in address bar
        web_view.urlChanged.connect(self.updateUrl)
        ok = web_view.loadFinished.connect(
            self.updateTabTitle)
        if ok:
            # Web page loaded
            return web_view
        else:
            print("The request timed out.")
    def setUpTab(self, tab):
        """Create individual tabs and widgets. Add the
        tab"s url and web view to the appropriate list.
        Update the address bar if the user switches tabs."""
        # Create the web view that will be displayed on the
        # page
        self.web_page = self.setUpWebView()
        # Append new web_page and url to the appropriate lists
        self.list_of_web_pages.append(self.web_page)
        self.list_of_urls.append(self.address_line)
        self.tab_bar.setCurrentWidget(self.web_page)
        # If user switches pages, update the url in the
        # address to reflect the current page.
        self.tab_bar.currentChanged.connect(self.updateUrl)
        tab_v_box = QVBoxLayout()
        # Sets the left, top, right, and bottom margins to
        # use around the layout.
        tab_v_box.setContentsMargins(0,0,0,0)
        tab_v_box.addWidget(self.web_page)
        tab.setLayout(tab_v_box)
    def openNewWindow(self):
        """Create new instance of the WebBrowser class."""
        new_window = WebBrowser()
        new_window.show()
        self.window_list.append(new_window)
    def openNewTab(self):
        """Create a new web tab."""
        new_tab = QWidget()
        self.tab_bar.addTab(new_tab, "New Tab")
        self.setUpTab(new_tab)
        # Update the tab_bar index to keep track of the new
        # tab. Load the url for the new page
        tab_index = self.tab_bar.currentIndex()
        self.tab_bar.setCurrentIndex(tab_index + 1)
        self.list_of_web_pages[
            self.tab_bar.currentIndex()].load(
                QUrl("https://google.com"))
    def updateProgressBar(self, progress):
        """Update progress bar in status bar.
        This provides feedback to the user that page is
        still loading."""
        if progress < 100:
            self.page_load_pb.setVisible(progress)
            self.page_load_pb.setValue(progress)
            self.page_load_label.setVisible(progress)
            self.page_load_label.setText(
                f"Loading Page... ({str(progress)}/100)")
            self.status_bar.addWidget(self.page_load_pb)
            self.status_bar.addWidget(self.page_load_label)
        else:
            self.status_bar.removeWidget(self.page_load_pb)
            self.status_bar.removeWidget(self.page_load_label)
    def updateTabTitle(self):
        """Update the title of the tab to reflect the
        website."""
        tab_index = self.tab_bar.currentIndex()
        title = self.list_of_web_pages[
            self.tab_bar.currentIndex()].page().title()
        self.tab_bar.setTabText(tab_index, title)
    def updateUrl(self):
        """Update the url in the address to reflect the
        current page being displayed."""
        url = self.list_of_web_pages[
            self.tab_bar.currentIndex()].page().url()
        formatted_url = QUrl(url).toString()
        self.list_of_urls[
            self.tab_bar.currentIndex()].setText(
                formatted_url)
    def searchForUrl(self):
        """Make a request to load a url."""
        url_text = self.list_of_urls[
            self.tab_bar.currentIndex()].text()
        # Append http to url
        url = QUrl(url_text)
        if url.scheme() == "":
            url.setScheme("http")
        # Request url
        if url.isValid():
            self.list_of_web_pages[
                self.tab_bar.currentIndex()].page().load(url)
        else:
            url.clear()
    def backPageButton(self):
        tab_index = self.tab_bar.currentIndex()
        self.list_of_web_pages[tab_index].back()
    def forwardPageButton(self):
        tab_index = self.tab_bar.currentIndex()
        self.list_of_web_pages[tab_index].forward()
    def refreshButton(self):
        tab_index = self.tab_bar.currentIndex()
        self.list_of_web_pages[tab_index].reload()
    def homeButton(self):
        tab_index = self.tab_bar.currentIndex()
        self.list_of_web_pages[tab_index].setUrl(
            QUrl("https://google.com"))
    def stopButton(self):
        tab_index = self.tab_bar.currentIndex()
        self.list_of_web_pages[tab_index].stop()
    def closeTab(self, tab_index):
        """Slot is emitted when the close button on a tab is
        clicked. index refers to the tab that should be
        removed."""
        self.list_of_web_pages.pop(tab_index)
        self.list_of_urls.pop(tab_index)
        self.tab_bar.removeTab(tab_index)
    def sizeMainWindow(self):
        """Use QApplication.primaryScreen() to access
        information about the screen and use it to size the
        main window when starting a new application."""
        desktop = QApplication.primaryScreen()
        size = desktop.availableGeometry()
        screen_width = size.width()
        screen_height = size.height()
        self.setGeometry(0, 0, screen_width, screen_height)
if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setStyleSheet(style_sheet)
    window = WebBrowser()
    app.exec()
Listing 16-6

Code for the web browser GUI

Before calling initializeUI(), we need to instantiate a few lists that will contain the new windows, web pages viewed, and URLs for each tab. This project also calls setWindowIcon() to include an application icon, but it will not be displayed on macOS due to system guidelines.

There are several methods that are called in initializeUI(). The first one is sizeMainWindow(), which demonstrates how to use QApplication to access information about the computer’s screen size. The second, createToolbar(), sets up the toolbar for navigating web pages. Methods createActions() and createMenu() set up the main menu. The menu includes actions and shortcuts for creating new windows and new tabs and closing the application. The application’s status bar is created in setUpMainWindow(), along with the QTabWidget for managing the open web pages.

In the createToolbar() method, the tool_bar instance includes buttons for navigating between web pages and a QLineEdit widget for entering and displaying URLs. Each button emits a signal when triggered that is connected to an appropriate slot. For example, if the back_page_button is pressed, the backPageButton() slot will be called, which we can see in the following block of code:
    def backPageButton(self):
        tab_index = self.tab_bar.currentIndex()
        self.list_of_web_pages[tab_index].back()

The current index of the tab we are viewing is stored in tab_index. The back() method is then called on the web_view object for that current tab. If the tab_index is not 0, then the user can navigate back through previously viewed web pages. The back() method is but one of several functions included in the QWebEngineView class. Other methods for navigation include forward(), reload(), and stop(), and these are also utilized for the other tool_bar buttons.

When the user enters a web address in the QLineEdit widget and presses the return key, we check to see if the URL begins with the correct scheme (such as http, https, or file) in searchForUrl(). If a valid scheme is not present, http is appended to the beginning of the URL. If the URL conforms to standard encoding rules, a request is then sent to load() the website.

Creating Tabs for the Web Browser

The setUpMainWindow() is used to handle creating the tab widget and the web view objects. First, we need to create the QTabWidget that will display each individual tab’s web view. Refer back to Chapter 6 for more details on setting up tab widgets.

A few of the tab _bar widget’s parameters are changed so that each tab includes a close button, and if only one tab remains, then the tab bar will not be displayed. This helps to make sure that there is always at least one tab in the main window. If a tab is closed, the closeTab() slot is called. The corresponding URLs and web view items for that tab are also removed from the list_of_urls and list_of_web_pages lists.

The first tab, main_tab, is created, added to the tab_bar, and then passed to the setUpTab() method. The tab_bar widget is set as the central widget for the main window. To set up a tab to display a web page, we first need to create a web view object.

Creating the Web View
The setUpWebView() method creates an instance of the QWebEngineView class, web_view, and sets the web view’s URL to display the Google web page:
        web_view.setUrl(QUrl("https://google.com"))
To create a basic instance of a web view in an application, you only need to create a QWebEngineView object, use the load() method to load the web page onto the web view widget, and then call show(). The following code shows the process for setting up a simple web view widget.
web_view = QWebEngineView()
web_view.load(QUrl("https://google.com"))
web_view.show()

Once the web page has loaded, the urlChanged signal connected to updateUrl() changes the URL displayed in address_line. We can use the loadFinished() signal to tell the current tab to update its title using the updateTabTitle() slot and return the web_view widget.

Next, create the layout to hold the web view widget, append the current tab’s URL and web_page object to the list_of_urls and list_of_web_pages lists, and set the layout for the current tab’s page. The web_page object is the web_view widget that is returned from setUpWebView() and displayed in the page in setUpTab().

Finally, to handle when a user switches between tabs, QTabWidget has the currentChanged signal. If a different tab is selected, the connected slot, updateUrl(), will change the displayed URL in address_line.

Adding a QProgressBar to the Status Bar

In setUpWebView(), a progress bar and label are also created that will be used to display the loading progress of a web page in the browser’s status bar. When the loadProgress signal is generated, the updateProgressBar() slot is called.

The loadProgress slot includes integer information that we can use to track how much of the page has loaded. While progress is less than 100, the progress bar and the label are both displayed, and their values are set. The code for displaying the progress bar is shown in the following lines:
            self.page_load_pb.setVisible(progress)
            self.page_load_pb.setValue(progress)
The widgets are then added to the status bar:
            self.status_bar.addWidget(self.page_load_pb)

When a page is finished loading, we call removeWidget() to remove the progress bar and the label. An example of the progress bar can be seen at the bottom of Figure 16-8.

Note

Creating a web browser is a very extensive task. There are many topics that are not included in this project, such as accessing HTTP cookies with Qt WebEngine Core, working with the browser history with QWebEngineHistory, managing connections and client certificates, proxy support with QNetworkProxy, working with JavaScript, downloading content from websites, and others. You are definitely encouraged to research these topics if you need to use Qt WebEngine for more advanced projects.

Project 16.7 – Tri-state QComboBox

While you may typically work with check boxes that have two states, checked or unchecked, a third state also exists, partially checked. This type of condition is usually influenced by the combo box’s children widgets or by the group that the QComboBox is managing. Figure 16-9 shows a simple example of a tri-state combo box where not all of its children are selected.
Figure 16-9

A window that contains a partially checked QComboBox

If all of the child elements are selected, the parent check box is checked. If some of the children are selected, then the parent is partially checked.

Explanation for the Tri-state QComboBox

For this example, let’s begin with the basic_window.py script from Chapter 1. There are no new widgets or other classes introduced in this section. Here, we’ll focus on learning what we’ve learned before in order to learn a new skill. Take a look at the tristate_cb instance in setUpMainWindow() in Listing 16-7. You’ll notice that we want to wait for a signal whenever the state of the widget changes using stateChanged.
# tristate.py
# Import necessary modules
import sys
from PyQt6.QtWidgets import (QApplication, QWidget,
    QCheckBox, QGroupBox, QButtonGroup, QVBoxLayout)
from PyQt6.QtCore import Qt
class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()
    def initializeUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(300, 200)
        self.setWindowTitle("Tri-State Example")
        self.setUpMainWindow()
        self.show()
    def setUpMainWindow(self):
        """Create and arrange widgets in the main window."""
        self.tristate_cb = QCheckBox("Select all toppings")
        self.tristate_cb.stateChanged.connect(
            self.updateTristateCb)
        # Create the check boxes with an indentation
        # using style sheets
        topping1_cb = QCheckBox("Chocolate Chips")
        topping1_cb.setStyleSheet("padding-left: 20px")
        topping2_cb = QCheckBox("Gummy Bears")
        topping2_cb.setStyleSheet("padding-left: 20px")
        topping3_cb = QCheckBox("Oreos, Peanuts")
        topping3_cb.setStyleSheet("padding-left: 20px")
        # Create a non-exclusive group of check boxes
        self.button_group = QButtonGroup(self)
        self.button_group.setExclusive(False)
        self.button_group.addButton(topping1_cb)
        self.button_group.addButton(topping2_cb)
        self.button_group.addButton(topping3_cb)
        self.button_group.buttonToggled.connect(
            self.checkButtonState)
        gb_v_box = QVBoxLayout()
        gb_v_box.addWidget(self.tristate_cb)
        gb_v_box.addWidget(topping1_cb)
        gb_v_box.addWidget(topping2_cb)
        gb_v_box.addWidget(topping3_cb)
        gb_v_box.addStretch()
        group_box = QGroupBox(
            "Choose the toppings for your ice cream")
        group_box.setLayout(gb_v_box)
        main_v_box = QVBoxLayout()
        main_v_box.addWidget(group_box)
        self.setLayout(main_v_box)
    def updateTristateCb(self, state):
        """Use the QCheckBox to check or uncheck all boxes."""
        for button in self.button_group.buttons():
            if state == 2: # Qt.CheckState.Checked
                button.setChecked(True)
            elif state == 0: # Qt.CheckState.Unchecked
                button.setChecked(False)
    def checkButtonState(self, button, checked):
        """Determine which buttons are selected and set the
        state of the tri-state QCheckBox."""
        button_states = []
        for button in self.button_group.buttons():
            button_states.append(button.isChecked())
        if all(button_states):
            self.tristate_cb.setCheckState(
                Qt.CheckState.Checked)
            self.tristate_cb.setTristate(False)
        elif any(button_states) == False:
            self.tristate_cb.setCheckState(
                Qt.CheckState.Unchecked)
            self.tristate_cb.setTristate(False)
        else:
            self.tristate_cb.setCheckState(
                Qt.CheckState.PartiallyChecked)
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
Listing 16-7

Code for the tri-state QComboBox

If tristate_cb is checked, we’ll use the value of state that is passed with the stateChanged signal to check all of the check boxes in checkButtonState(). Otherwise, the widgets are all unchecked.

Next, we’ll create the rest of the window, instantiate the children QCheckBox objects, and arrange them in a QButtonGroup. The QButtonGroup signal buttonToggled is emitted whenever any of the widgets are checked or unchecked. If the state of one of the check boxes in the button group changes, the slot checkButtonState() is used to find out which buttons are checked or unchecked. We can access all of the buttons in QButtonGroup using the buttons() method.

Those values are then added to the button_states list. It is here that we take care of updating the parameters of tristate_cb. If all values are True, setCheckState() is used to ensure that tristate_cb only has two states. If all of the buttons are False, then tristate_cb is unchecked. Finally, if there is a mix of True and False values in button_states, tristate_cb is set to tri-state mode using setCheckState() and the PartiallyChecked flag.

Summary

In this chapter, you saw different GUI applications that build the structure for larger projects, such as the camera GUI or the web browser GUI. Other projects introduced components that you may be able to include in other programs, such as the directory viewer GUI, the clock GUI, and the calendar GUI. In the case of the Hangman GUI, we demonstrated how an understanding of QPainter is useful for drawing and customizing the look of widgets. Finally, tri-state QComboBox widgets are useful for managing child elements.

We have explored a variety of topics for designing graphical user interfaces using PyQt6 and Python throughout this book – different types of widgets, classes, and layouts. We saw how to stylize your interfaces, how to add menus, and how to make an application simpler with Qt Designer. Advanced topics such as working with the clipboard, SQL, and multithreaded applications were also covered.

The Appendix will fill in more details about some of the PyQt6 classes used in this book as well as a few other classes that were not included in previous chapters.

Your feedback and questions are always welcome. Thank you so much for joining me on this journey and allowing me to share my knowledge about GUI development with you.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset