© 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_12

12. Creating Custom Widgets

Joshua M Willman1  
(1)
Sunnyvale, CA, USA
 

While most development tasks can be solved with buttons, text editing widgets, and other components provided by PyQt, you may at some point find yourself in a situation where no single widget provides you with the tools or functionality that you need. You might even find yourself needing to use a widget you made in other GUIs and therefore need a way to easily import your custom-made widget into other applications. Thankfully, PyQt allows developers to build and import their own widgets for solving new and unforeseen tasks.

In this chapter, you will
  • Find out about creating your own custom widgets in PyQt

  • See how to apply the custom widget built in a small example GUI

  • Learn about Qt’s four image handling classes

  • Use a new widget, QSlider, for selecting values in a bounded range

Let’s learn about the custom widget we’ll build in the following sections.

Project 12.1 – RGB Slider Custom Widget

For this chapter’s project, we are going to take a look at making a custom, functional widget in PyQt. While PyQt offers a variety of widgets for building GUIs, every once in a while you may find yourself needing to design and build your own. One of the benefits of creating a customized widget is that you can either create a general widget that can be used by many different applications or make an application-specific widget that allows you to solve a specific problem.

There are quite a few techniques that you can use to create your own widgets, most of which we have already seen in previous examples.
  • Modifying the properties of PyQt’s widgets by using built-in methods, such as setAlignment(), setTextColor(), and setRange()

  • Creating style sheets to change a widget’s existing behavior and appearances

  • Subclassing widgets and reimplementing event handlers, or adding properties dynamically to QWidget classes

  • Creating composite widgets that are made up of two more types of widgets and arranged together using a layout

  • Designing a completely new widget that subclasses QWidget and has its own unique properties and appearance

The RGB slider, shown in Figure 12-1, actually is created by combining a few of the techniques listed previously. The widget uses Qt’s QSlider and QSpinBox widgets for selecting RGB values and displays the color on QLabel widgets. The look of the sliders is modified by using style sheets. All of the widgets are then assembled into a parent widget, which we can then import into other PyQt applications.
Figure 12-1

A custom widget used to select colors using sliders and spin boxes

Before finding out how to make the RGB slider widget, we’ll need to learn a little more about some of the classes we will need to build the application.

PyQt’s Image Handling Classes

In previous examples, we worked with QPixmap to handle image data. Qt actually provides four different classes for working with images, each with their own special purpose.

QPixmap is the go-to choice for displaying images on the screen. Pixmaps can be presented on a variety of widgets that can display icons, including QLabel and QPushButton. QImage is optimized for reading, writing, and manipulating images and is very useful if you need to directly access and modify an image’s pixel data. QImage can also act as a paint device. A paint device (created by the QPaintDevice class) is a two-dimensional surface that can be drawn on using QPainter. It is also worth noting that QImage inherits QPaintDevice.

Conversion between QImage and QPixmap is also possible. One possibility for using the two classes together is to load an image file with QImage, manipulate the image data, and then convert the image to a pixmap before displaying it on the screen. The RGB slider widget gives an example for converting between the two classes.

QBitmap is a subclass of QPixmap and provides monochrome (1-bit depth) pixmaps. QPicture is a paint device that replays QPainter commands, meaning you can create a picture from whatever image format you are painting on. Pictures created with QPicture are resolution independent, appearing the same no matter what image format you use, such as png, svg, or pdf.

The RGB slider uses two types of widgets for selecting RGB values: QSpinBox, which was introduced in Chapter 4, and a new widget.

The QSlider Widget

The QSlider class provides a developer with a tool for selecting integer values within a bounded range. Sliders provide users with a convenient means for quickly selecting values or changing settings with only the slide of a simple handle. By default, sliders are arranged vertically (specified by Qt.Orientation.Vertical), but that can be changed by passing the flag Qt.Orientation.Horizontal to the constructor.

The following block of code demonstrates how to create an instance of QSlider, set the slider’s maximum range value, and connect to the signal valueChanged that is emitted whenever the slider’s value has changed.
slider = QSlider(Qt.Horizontal, self)
# Default values are from 0 to 99
slider.setMaximum(200)
slider.valueChanged.connect(self.printSliderValue)
def printSliderValue(self, value):
    print(value)

Here, the slider’s maximum range is 200, and its value is printed to the shell whenever the slider’s position changes.

Explanation for the RGB Slider Widget

The RGB slider is a custom widget created by combining a few of Qt’s built-in widgets: QLabel, QSlider, and QSpinBox. The appearance of the sliders is adjusted using style sheets so that they give visual feedback to the user about which RGB value they are adjusting. The sliders and spin boxes are connected together so that their values are in sync and so that the user can see the integer value on the RGB scale. The RGB values are also converted to hexadecimal format and displayed on the widget.

The sliders and spin boxes can be used to either find out the RGB or hexadecimal values for a color or use the reimplemented mousePressEvent() method so that a user can click on a pixel in an image to find out the pixel’s RGB value. An example of this is shown in the “RGB Slider Demo” section, where you will also see how to import the RGB slider in a demo application.

We need to import quite a few classes in Listing 12-1. The classes for working with images in PyQt are found in the QtGui module. Another class worth mentioning, qRgb, is actually a typedef that creates an unsigned int representing the RGB value triplet (r, g, b). A typedef in C++ is a keyword that is used to create a new name for a data type, in this case to represent the RGB value.
# rgb_slider.py
# Import necessary modules
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
    QSlider, QSpinBox, QHBoxLayout, QVBoxLayout, QGridLayout)
from PyQt6.QtGui import QImage, QPixmap, QColor, qRgb, QFont
from PyQt6.QtCore import Qt
Listing 12-1

The imports for the RGB slider

The style sheet that follows in Listing 12-2 is used to change the appearance of the sliders. We want to modify their appearance so that they give the user more feedback about which RGB values are being changed. Each slider is given an ID Selector using the setObjectName() method in the setUpMainWindow() method. If no ID Selector is used in the style sheet, then that style is applied to all of the QSlider objects.
# rgb_slider.py
style_sheet = """
    QSlider:groove:horizontal{
        border: 1px solid #000000;
        background: white;
        height: 10 px;
        border-radius: 4px
    }
    QSlider#Red:sub-page:horizontal{
        background: qlineargradient(x1:1, y1:0, x2:0, y2:1,
            stop: 0 #FF4242, stop: 1 #1C1C1C);
        background: qlineargradient(x1:0, y1:1, x2:1, y2:1,
            stop: 0 #1C1C1C, stop: 1 #FF0000);
        border: 1px solid #4C4B4B;
        height: 10px;
        border-radius: 4px;
    }
    QSlider::add-page:horizontal {
        background: #FFFFFF;
        border: 1px solid #4C4B4B;
        height: 10px;
        border-radius: 4px;
    }
    QSlider::handle:horizontal {
        background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
            stop: 0 #EEEEEE, stop: 1 #CCCCCC);
        border: 1px solid #4C4B4B;
        width: 13px;
        margin-top: -3px;
        margin-bottom: -3px;
        border-radius: 4px;
    }
    QSlider::handle:horizontal:hover {
        background: qlineargradient(x1:0, y1:0, x2:1, y2:1,
            stop: 0 #FFFFFF, stop: 1 #DDDDDD);
        border: 1px solid #393838;
        border-radius: 4px;
    }
Listing 12-2

The style sheet for the RGB slider, part 1

The sliders use linear gradients so that users can get a visual representation of how much of the red, green, and blue colors are being used. With linear gradients, the color is interpolated from x1, y1 to x2, y2. The pseudostate horizontal is used to specify that the styles will be applied to horizontal QSlider objects.

The groove subcontrol refers to the long, rectangular part of the slider, which is solid white before moving the handle of the slider. The add-page subcontrol denotes the color of the slider parts before the handle, and sub-page denotes the color after. For the handle, the color will change whenever the mouse hovers over it.

The only changes that need to be made for the Green and Blue sliders are to the sub-page subcontrols. These changes are handled in Listing 12-3. You can also refer back to Chapter 6 for a refresher about style sheets.
# rgb_slider.py
    QSlider#Green:sub-page:horizontal{
        background: qlineargradient(x1:1, y1:0, x2:0, y2:1,
            stop: 0 #FF4242, stop: 1 #1C1C1C);
        background: qlineargradient(x1:0, y1:1, x2:1, y2:1,
            stop: 0 #1C1C1C, stop: 1 #00FF00);
        border: 1px solid #4C4B4B;
        height: 10px;
        border-radius: 4px;
    }
    QSlider#Blue:sub-page:horizontal{
        background: qlineargradient(x1:1, y1:0, x2:0, y2:1,
            stop: 0 #FF4242, stop: 1 #1C1C1C);
        background: qlineargradient(x1:0, y1:1, x2:1, y2:1,
            stop: 0 #1C1C1C, stop: 1 #0000FF);
        border: 1px solid #4C4B4B;
        height: 10px;
        border-radius: 4px;
    }
"""
Listing 12-3

The style sheet for the RGB slider, part 2

The RGBSlider class inherits QWidget in Listing 12-4. For this class, the user can pass an image and other arguments as parameters to the constructor.
# rgb_slider.py
class RGBSlider(QWidget):
    def __init__(self, _image=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._image = _image
        self.initializeUI()
    def initializeUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(225, 300)
        self.setWindowTitle("12.1 - RGB Slider")
        # Store the current pixel value
        self.current_val = QColor()
        self.setUpMainWindow()
        self.show()
if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setStyleSheet(style_sheet)
    window = RGBSlider()
    sys.exit(app.exec())
Listing 12-4

Code to start building the RGBSlider class

The current_val instance variable will be used to keep track of the current RGB color value. The color, of course, will be composed by the slider and spin box values.

In setUpMainWindow() in Listing 12-5, a QImage object is created that will display the color created from the RGB values. Using the QImage method fill(), the first color that will show when the application is run is black. To display the image in the widget, first convert the QImage to a QPixmap using the QPixmap method fromImage() and pass it a QImage instance. Then use setPixmap() to set the QLabel widget’s pixmap.
# rgb_slider.py
    def setUpMainWindow(self):
        """Create and arrange widgets in the main window."""
        # Image that will display the current color set by
        # slider/spin_box values
        self.color_display = QImage(
            100, 100, QImage.Format.Format_RGBX64)
        self.color_display.fill(Qt.GlobalColor.black)
        self.cd_label = QLabel()
        self.cd_label.setPixmap(
            QPixmap.fromImage(self.color_display))
        self.cd_label.setScaledContents(True)
Listing 12-5

Code for the setUpMainWindow() method in the RGBSlider class, part 1

The contents of cd_label are then scaled to fit the window’s size.

Updating the Sliders and Spin Boxes

Next, we create the red, green, and blue QSlider and QSpinBox widgets in Listing 12-6. The sliders’ maximum values are set to 255, since RGB values are in the range of 0255. Each slider is also given an object name that is used to identify it in the style sheet.
# rgb_slider.py
        # Create RGB sliders and spin boxes
        red_label = QLabel("Red")
        red_label.setFont(QFont("Helvetica", 14))
        self.red_slider = QSlider(Qt.Orientation.Horizontal)
        self.red_slider.setObjectName("Red")
        self.red_slider.setMaximum(255)
        self.red_spinbox = QSpinBox()
        self.red_spinbox.setMaximum(255)
        green_label = QLabel("Green")
        green_label.setFont(QFont("Helvetica", 14))
        self.green_slider = QSlider(Qt.Orientation.Horizontal)
        self.green_slider.setObjectName("Green")
        self.green_slider.setMaximum(255)
        self.green_spinbox = QSpinBox()
        self.green_spinbox.setMaximum(255)
        blue_label = QLabel("Blue")
        blue_label.setFont(QFont("Helvetica", 14))
        self.blue_slider = QSlider(Qt.Orientation.Horizontal)
        self.blue_slider.setObjectName("Blue")
        self.blue_slider.setMaximum(255)
        self.blue_spinbox = QSpinBox()
        self.blue_spinbox.setMaximum(255)
Listing 12-6

Code for the setUpMainWindow() method in the RGBSlider class, part 2

The two labels instantiated in Listing 12-7 will display the hexadecimal value of the color. They are then arranged in a QHBoxLayout, which is set as the layout for hex_container.
# rgb_slider.py
        # Use the hex labels to display color values in hex
        # format
        hex_label = QLabel("Hex Color ")
        self.hex_values_label = QLabel()
        hex_h_box = QHBoxLayout()
        hex_h_box.addWidget(
            hex_label, Qt.AlignmentFlag.AlignRight)
        hex_h_box.addWidget(self.hex_values_label,
            Qt.AlignmentFlag.AlignRight)
        hex_container = QWidget()
        hex_container.setLayout(hex_h_box)
        # Create grid layout for sliders and spin boxes
        grid = QGridLayout()
        grid.addWidget(
            red_label, 0, 0, Qt.AlignmentFlag.AlignLeft)
        grid.addWidget(self.red_slider, 1, 0)
        grid.addWidget(self.red_spinbox, 1, 1)
        grid.addWidget(
            green_label, 2, 0, Qt.AlignmentFlag.AlignLeft)
        grid.addWidget(self.green_slider, 3, 0)
        grid.addWidget(self.green_spinbox, 3, 1)
        grid.addWidget(
            blue_label, 4, 0, Qt.AlignmentFlag.AlignLeft)
        grid.addWidget(self.blue_slider, 5, 0)
        grid.addWidget(self.blue_spinbox, 5, 1)
        grid.addWidget(hex_container, 6, 0, 1, 0)
Listing 12-7

Code for the setUpMainWindow() method in the RGBSlider class, part 3

From there, the sliders, spin boxes, and container for the labels are organized in a QGridLayout.

Updating the Colors

QSlider and QSpinBox can both emit the valueChanged signal. We can connect the sliders and spin boxes so that their values change relative to each other. For example, when red_slider emits a signal, it triggers the updateRedSpinBox() slot, which then updates the red_spinbox value using setValue(). A similar process happens for the red_spinbox. This process also happens for the sliders and spin boxes that control the blue and green values.

Take a look at the valueChanged signals in Listing 12-8 for a slider and its corresponding spin box and you will notice that they trigger slots that update each other.
# rgb_slider.py
        # The sliders and spin boxes for each color should
        # display the same values and be updated at the same
        # time
        self.red_slider.valueChanged.connect(
            self.updateRedSpinBox)
        self.red_spinbox.valueChanged.connect(
            self.updateRedSlider)
        self.green_slider.valueChanged.connect(
            self.updateGreenSpinBox)
        self.green_spinbox.valueChanged.connect(
            self.updateGreenSlider)
        self.blue_slider.valueChanged.connect(
            self.updateBlueSpinBox)
        self.blue_spinbox.valueChanged.connect(
            self.updateBlueSlider)
        # Create container for rgb widgets
        rgb_widgets = QWidget()
        rgb_widgets.setLayout(grid)
        main_v_box = QVBoxLayout()
        main_v_box.addWidget(self.cd_label)
        main_v_box.addWidget(rgb_widgets)
        self.setLayout(main_v_box)
Listing 12-8

Code for the setUpMainWindow() method in the RGBSlider class, part 4

All of the widgets along with cd_label from Listing 12-5 are contained in rgb_widgets and arranged in the main layout.

Let’s take a look at the slots in Listing 12-9 for updating the widget values.
# rgb_slider.py
    # The following slots update the red, green and blue
    # sliders and spin boxes
    def updateRedSpinBox(self, value):
        self.red_spinbox.setValue(value)
        self.redValue(value)
    def updateRedSlider(self, value):
        self.red_slider.setValue(value)
        self.redValue(value)
    def updateGreenSpinBox(self, value):
        self.green_spinbox.setValue(value)
        self.greenValue(value)
    def updateGreenSlider(self, value):
        self.green_slider.setValue(value)
        self.greenValue(value)
    def updateBlueSpinBox(self, value):
        self.blue_spinbox.setValue(value)
        self.blueValue(value)
    def updateBlueSlider(self, value):
        self.blue_slider.setValue(value)
        self.blueValue(value)
Listing 12-9

Code for the slots that update the slider and spin box values

When a valueChanged signal triggers a slot, it uses value to update the corresponding slider or spin box and then calls a function that will create a new color from the red, green, or blue values.

We’ll take a look at one example since the others are organized in a similar manner. If the value of red_slider is changed, the updateRedSpinBox() slot will be called, and the value of red_spinbox set to value. From there, let’s move to Listing 12-10 to handle the creation of new colors.
# rgb_slider.py
    # Create new colors based upon the changes to the RGB
    # values
    def redValue(self, value):
        new_color = qRgb(value,
            self.current_val.green(), self.current_val.blue())
        self.updateColorInfo(new_color)
    def greenValue(self, value):
        new_color = qRgb(self.current_val.red(),
            value, self.current_val.blue())
        self.updateColorInfo(new_color)
    def blueValue(self, value):
        new_color = qRgb(self.current_val.red(),
            self.current_val.green(), value)
        self.updateColorInfo(new_color)
    def updateColorInfo(self, color):
        """Update color displayed in image and set the hex
        values accordingly."""
        self.current_val = QColor(color)
        self.color_display.fill(color)
        self.cd_label.setPixmap(QPixmap.fromImage(
            self.color_display))
        self.hex_values_label.setText(
            f"{self.current_val.name()}")
Listing 12-10

Code for methods that create and update a color

Continuing with red, the redValue() function creates a new qRgb color, using the new red value and the current_val’s green() and blue() colors. The variable current_val is an instance of QColor. The QColor class has functions that we can use to access an image’s RGB (or other color format) values.

The new_color is then passed to updateColorInfo(). Green and blue colors are handled in a similar fashion. Next, we have to create a QColor from the qRgb value and store it in current_val. The QImage color_display is updated with fill(), which is then converted to a QPixmap, and displayed on the cd_label.

The last thing to do is to update the hexadecimal labels using QColor.name(). (Remember that current_val is a QColor object.) This function returns the name of the color in the format “#RRGGBB”.

Adding Methods to a Custom Widget

The options for methods that you could create for a custom widget are numerous. One option is to create methods that allow the user to modify the behavior or appearance of your custom widget. Another option is to use the event handlers to check for keyboard or mouse events that could be used to interact with your GUI.

The getPixelValue() method in Listing 12-11 is a reimplementation of the mousePressEvent() event handler. If an image is passed into the RGBSlider constructor, then _image is not None, and the user can click on points in the image to get their corresponding pixel values. QColor.pixel() gets a pixel’s RGB values. Then, the value for current_val is updated in order to use the selected pixel’s red, blue, and green values. These values are then passed back into the functions that will update the sliders, spin boxes, labels, and QImage.
# rgb_slider.py
    def getPixelValues(self, event):
        """The method reimplements the mousePressEvent method.
        To use, set an widget's mousePressEvent equal to
        getPixelValues, like so:
            image_label.mousePressEvent = rgbslider.getPixelValues
        If an _image != None, then the user can select pixels
        in the images, and update the sliders to get view the color,
        and get the rgb and hex values."""
        x = int(event.position().x())
        y = int(event.position().y())
        # valid() returns true if the point selected is a valid
        # coordinate pair within the image
        if self._image.valid(x, y):
            self.current_val = QColor(self._image.pixel(x, y))
            red_val = self.current_val.red()
            green_val = self.current_val.green()
            blue_val = self.current_val.blue()
            self.updateRedSpinBox(red_val)
            self.updateRedSlider(red_val)
            self.updateGreenSpinBox(green_val)
            self.updateGreenSlider(green_val)
            self.updateBlueSpinBox(blue_val)
            self.updateBlueSlider(blue_val)
Listing 12-11

Code for the getPixelValues() method

Go ahead and run the script now and see how it operates. Right now, the application is simply its own small GUI. Let’s see how to use the custom widget class in another application to utilize the color selecting feature.

RGB Slider Demo

One reason for creating a custom widget is so that it can be used in other applications. The following program is a short example of how to import and set up the RGB slider built in Project 12.1. For this example, an image is displayed in the window alongside the RGB slider. Users can click on points within the image and see the RGB and hexadecimal values change in real time.

This short program’s GUI can be seen in Figure 12-2.
Figure 12-2

An example GUI with the custom RGB slider. Image from www.​pixilart.​com/​

Explanation for the RGB Slider Demo

Be sure to download the image from the images folder in the GitHub repository.

You can use the basic_window.py class from Chapter 1 to get your started with this program. Begin by importing a few classes, including the RGB slider and the style sheet from rgb_slider.py, in Listing 12-12.
# rgb_demo.py
# Import necessary modules
import sys
from PyQt6.QtWidgets import (QApplication, QWidget, QLabel,
    QHBoxLayout)
from PyQt6.QtGui import QPixmap, QImage
from PyQt6.QtCore import Qt
from rgb_slider import RGBSlider, style_sheet
class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()
    def initializeUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(225, 300)
        self.setWindowTitle("12.2 - Custom Widget Example")
        # Load image
        image = QImage("images/duck_pic.png")
        # Create instance of RGB slider widget
        rgbslider = RGBSlider(image)
        image_label = QLabel()
        image_label.setAlignment(Qt.AlignmentFlag.AlignTop)
        image_label.setPixmap(QPixmap().fromImage(image))
        # Reimplement the label's mousePressEvent
        image_label.mousePressEvent = rgbslider.getPixelValues
        h_box = QHBoxLayout()
        h_box.addWidget(rgbslider)
        h_box.addWidget(image_label)
        self.setLayout(h_box)
        self.show()
if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setStyleSheet(style_sheet)
    window = MainWindow()
    sys.exit(app.exec())
Listing 12-12

Code that shows an example for using the RGB slider widget

In the MainWindow class, set up the window in initializeUI(), load an image, and create an instance of the RGB slider. The widgets are then arranged in the window.

For this application, we are still creating the image as an instance of QImage and then converting it to a QPixmap. QImage is used so that we have access to the image’s pixel information.

To use the getPixelValues() method in the RGBSlider class, we’ll need to reimplement the QLabel object’s mouse event handler. When the user clicks on a pixel in the image, the x and y coordinates from the event are used to update the values in the RGB slider widget using the getPixelValues() method.

If you only want to use the slider to get different RGB or hexadecimal values, then the application is finished. But you could continue to add other functionalities to the RGB slider to use in your own projects.

Summary

Not every problem can be solved by the widgets that Qt provides. In situations where ingenuity is needed, PyQt is great because it allows developers to design, build, and customize their own widgets. This can be handled in a variety of ways, perhaps by building a new widget from preexisting widgets or by creating completely new widgets from scratch. From there, a new widget can be seamlessly imported into other applications.

In Chapter 13, you will find out how to create modern-looking GUIs using Qt Quick.

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

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