Custom Widgets in Qt

This exercise shows how subclassing can be used to create new widgets that specialize, extend or combine the capabilities of those already provided with Qt.

A Custom Widget For Text Input

Subclassing of existing Qt widgets allows us to create variants with additional or different behaviour.

  1. Download postcode.zip and unzip it, then cd into the postcode subdirectory and examine the files postcode.hpp, postcode.cpp and main.cpp. The first two files define a specialized version of QLineEdit that handles input of a UK postcode. The third file is a program that tests this widget.

    Use CMake to build the program, in the usual way, then run it. You should find that the widget prevents combinations of characters that are not permitted in a postcode1.

  2. Now create a new directory called examgrade, alongside (not inside) the postcode directory. In this directory, create a new widget named ExamGradeInput, along with a small program to test it. Use the postcode example as a guide.

    Your widget class should inherit from QSpinBox and it should include code in its constructor that constrains input to be an integer in the range 0 to 100. In addition to a constructor, give your class a method getGrade() that returns the exam grade as an integer.

    Tip

    This method will need to invoke the text() method to retrieve the entered text. This is returned as a QString object, on which you can call the toInt() method to get the corresponding integer value.

    Create a CMakeLists.txt file for the examgrade project, using the file from postcode as a guide. Build yout test program and run it. Verify that the widget does, indeed, prevent input outside of the range 0–100.

  3. Modify your ExamGradeInput widget so that its background colour is red for failing grades (less than 40) and green for passing grades.

    You will need to add a custom slot to your widget, following a similar approach to that used in the dice roller application from the previous exercise. You can use code like

    setStyleSheet("background-color: red");

    to set the background colour.

    Remember to connect your widget’s valueChanged(int) signal to the new slot. You can do this in the constructor, or in a method that you call from the constructor.

    Important

    Remember also that you will need to add the Q_OBJECT macro to the widget class definition.

Composite Widgets

We can also use subclassing to create new widgets that bundle together sets of existing widgets, giving them a specific layout and allowing them to be manipulated as a single widget.

Consider, for example, the volume control implemented in the previous exercise. In that exercise, we combined a QDial widget with a QLCDNumber widget, and treated the combination as if it were an application window. But we could have just as easily treated the combination as a single widget that can form part of a larger UI. Let’s explore this idea now.

  1. Download stereo.zip and unzip it, then cd into the stereo subdirectory. In here, you will find two files that define and implement a VolumeControl widget. This is very similar to the one from the previous exercise; the only real difference is that it inherits from QGroupBox, which provides a title and visual grouping for the contents of the widget. (We could have stuck with QWidget as the superclass if we didn’t need the title or visual grouping.)

    Study the code. Note, in particular, how the constructor has been specified in control.hpp:

    class VolumeControl: public QGroupBox
    {
      public:
        VolumeControl(QString = "", QWidget* = nullptr);
      ...
    }

    Note also how it has been implemented in control.cpp:

    VolumeControl::VolumeControl(QString title, QWidget* parent):
      QGroupBox(title, parent)
    {
      ...
    }

    The constructor has title and parent parameters. The latter represents the parent widget of Controller—i.e., the widget that contains it. The default of nullptr appearing in control.hpp means that there is no parent by default—i.e., that the VolumeControl will be treated as the top-level widget of the application, unless another widget has been specified. The value of parent is passed on to the superclass constructor so that it can be used to establish the containment hierarchy of the UI.

    Caution

    If you want compose a UI from custom widgets, you must ensure that those widgets can specify their parent in this manner.

  2. You don’t need to make any changes to VolumeControl. Instead, your task is to implement a ‘Stereo Volume Controller’ application in main.cpp. This should create two instances of the VolumeControl widget, with titles “Left Channel” and “Right Channel”, respectively. It should arrange these side-by-side before making the UI visible.

    The final UI, which you can run with ./stereo, should look something like this:

Adding Custom Signals & Slots

How might we link the two VolumeControl widgets so that adjusting the dial of either one of them makes an identical adjustment to the other?2

One approach might be to access the internal widgets of each VolumeControl widget directly, connecting the valueChanged(int) signal of the dial widget on the left to the setValue(int) slot of the dial on the right, and vice versa. This isn’t currently possible because the internal widgets are declared as private. You could make them public, but this is bad practice as it would expose implementation details of VolumeControl unnecessarily.

A better solution would be to give the VolumeControl widget its own custom signal and slot to handle changes in value.

Note

The key point to note here is that custom signals and slots like this provide the means to ‘wire up’ VolumeControl widgets to each other and to other Qt widgets. Applications can use these custom signals and slots and do not need to know anything about what is happening inside a VolumeControl. This makes it easier to change the implementation of VolumeControl at a later date without breaking any code that uses the widget.

  1. Start by adding the Q_OBJECT macro to the definition of VolumeControl in control.hpp. Remember that this is needed in every class that defines custom signals or slots.

  2. Now declare the signal by adding the following to the class definition, between the public and private sections:

    signals:
      void valueChanged(int);
    Note

    Unlike custom slots, custom signals don’t need to be implemented explicitly; the meta-object compiler will generate the necessary supporting code from the declaration given in the class definition.

    Note also that the section heading is signals:, not public signals:, because custom signals are implicitly public.

  3. Next, in the makeConnections() method, connect the valueChanged of signal of the dial to your newly defined custom signal:

    connect(dial, SIGNAL(valueChanged(int)), this, SIGNAL(valueChanged(int)));

    Qt allows signals to be connected to other signals. In this case, it means that, for every valueChanged signal emitted by the dial, the VolumeControl widget itself will emit an equivalent signal, communicating the change in value to the outside world.

  4. Now create the custom slot. Add a public slots: section to control.hpp, just after the public: section, containing a prototype for the slot:

    public slots:
      void setValue(int);

    Then add the following implementation to control.cpp:

    void VolumeControl::setValue(int value)
    {
      dial->setValue(value);
    }

    This simply delegates to the setValue slot of the dial. (If you hadn’t realised, slots are just regular methods that you are free to invoke yourself; they are not limited to being connection targets.)

  5. Finally, edit main.cpp and add the following two lines:

    QObject::connect(left, SIGNAL(valueChanged(int)), right, SLOT(setValue(int)));
    QObject::connect(right, SIGNAL(valueChanged(int)), left, SLOT(setValue(int)));

    This code connects the valueChanged signal of the VolumeControl on the left to the setValue slot of the VolumeControl on the right, and then establishes the equivalent connection in the opposite direction.

  6. Rebuild the application. You should now find that adjusting the dial for the left channel triggers an identical adjustment in the right channel, and vice versa.

Challenge

Ganging of the two volume controls should really be optional. How might you modify the application to support this?

Sketch out a solution and then try implementing it!

Footnotes

  1. We don’t claim that this is a good solution to the problem of validating postcode entry! For one thing, it provides no feedback to the user that they are attempting to input invalid characters…↩︎

  2. This is known as ‘ganging’.↩︎