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.
Download
postcode.zipand unzip it, thencdinto thepostcodesubdirectory and examine the filespostcode.hpp,postcode.cppandmain.cpp. The first two files define a specialized version ofQLineEditthat 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.
Now create a new directory called
examgrade, alongside (not inside) thepostcodedirectory. In this directory, create a new widget namedExamGradeInput, along with a small program to test it. Use the postcode example as a guide.Your widget class should inherit from
QSpinBoxand 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 methodgetGrade()that returns the exam grade as an integer.TipThis method will need to invoke the
text()method to retrieve the entered text. This is returned as aQStringobject, on which you can call thetoInt()method to get the corresponding integer value.Create a
CMakeLists.txtfile for theexamgradeproject, using the file frompostcodeas a guide. Build yout test program and run it. Verify that the widget does, indeed, prevent input outside of the range 0–100.Modify your
ExamGradeInputwidget 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.ImportantRemember also that you will need to add the
Q_OBJECTmacro 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.
Download
stereo.zipand unzip it, thencdinto thestereosubdirectory. In here, you will find two files that define and implement aVolumeControlwidget. This is very similar to the one from the previous exercise; the only real difference is that it inherits fromQGroupBox, which provides a title and visual grouping for the contents of the widget. (We could have stuck withQWidgetas 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
titleandparentparameters. The latter represents the parent widget ofController—i.e., the widget that contains it. The default ofnullptrappearing incontrol.hppmeans that there is no parent by default—i.e., that theVolumeControlwill be treated as the top-level widget of the application, unless another widget has been specified. The value ofparentis passed on to the superclass constructor so that it can be used to establish the containment hierarchy of the UI.CautionIf you want compose a UI from custom widgets, you must ensure that those widgets can specify their parent in this manner.
You don’t need to make any changes to
VolumeControl. Instead, your task is to implement a ‘Stereo Volume Controller’ application inmain.cpp. This should create two instances of theVolumeControlwidget, 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.
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.
Start by adding the
Q_OBJECTmacro to the definition ofVolumeControlincontrol.hpp. Remember that this is needed in every class that defines custom signals or slots.Now declare the signal by adding the following to the class definition, between the
publicandprivatesections:signals: void valueChanged(int);NoteUnlike 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:, notpublic signals:, because custom signals are implicitly public.Next, in the
makeConnections()method, connect thevalueChangedof 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
valueChangedsignal emitted by the dial, theVolumeControlwidget itself will emit an equivalent signal, communicating the change in value to the outside world.Now create the custom slot. Add a
public slots:section tocontrol.hpp, just after thepublic: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
setValueslot 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.)Finally, edit
main.cppand 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
valueChangedsignal of theVolumeControlon the left to thesetValueslot of theVolumeControlon the right, and then establishes the equivalent connection in the opposite direction.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!