Sunteți pe pagina 1din 32

In this lecture we will try to cover the theoretical aspects of the QObject class, which makes Qt what Qt is.

Understanding this is key to the rest of the lectures. The first half of the lecture will be devoted to the QObject class, meta-information and the possibilities given by these classes. The second half of the lecture will be devoted to signals and slots which is the single most important concept of Qt. Recommended reading before the lecture: http://doc.trolltech.com/4.6/object.html

The QObject class is essential to Qt It is the base class of most active classes in Qt, including all widgets (the image shows only a subset of the classes derived from QObject, it also shows that those subclasses are themselves inherited by other classes) The QObject implements many of the features that makes Qt what it is, such as: signals slots properties simplified memory management

The QObject is the base class of many of Qt's classes, but there are some exceptions. Graphics view items are not QObjects, as they have been designed to be lean (there can be tens of thousands of them, so every byte is important). QString, QList, QChar, all represent values, and cannot be QObjects because of that (continues)

So, why cannot QChar be a QObject. QObjects are individuals! This means that you cannot write obj1 = obj2, copy is not a valid operation. Why? QObjects have names, for instance addButton, deleteButton, etc (from the first lecture's demo). How do you copy this? You don't want two addButtons? QObjects are structured in hierarchies. How do you copy that context? Do you want them at the same level? Leaf level? Root level? QObjects can be interconnected (add calls a slot) How do you copy this? Do you want to duplicate connections?
5

QObjects carry meta-data, that is data about the object itself. This makes it possible to add introspection to Qt, for instance to ask a class which methods it has Every QObject has a meta object which can be retreived using the metaObject method. The meta object knows about the class, its name, base class, properties, methods, slots and signals. Continues

While providing C++ developers with meta data, Qt is still based 100% on C++. There is no other language involved. Instead, moc, the meta object compiler, parses the C++ code and generates even more C++ code. The figure shows an ordinary C++ build process, headers are included, sources compiled, object files linked and the end result is executable code. (Even libraries are, more or less, executable code, so this holds true in all cases.) Continues

So, Qt adds a new step to the build process. Moc parses class declarations and generates C++ code that implements the specific meta object. Notice that it is possible to have the moc work on a source file directly, and then include the result into the same source file, but that is only useful in the special case of a QObject derived class only used from within one single file. All this is handled by QtCreator, so you do not need to worry about it. There are solutions taking care of this step for all other build environments as well (command line builds, Visual Studio, Eclipse, Xcode, etc) Continues

What does moc look for in your class declarations?


First of all, you need to inherit QObject directly or indirectly. If you are inheriting multiple classes, your QObject (decendant) must appear first. You cannot inherit from QObject twice (directly or indirectly). You then need to put the Q_OBJECT macro in your class declaration, in the private section. By convention, and historical limitations, this is usually placed first. You can then add class info, and more, through special macros. For instance, in this case the key author is given the value John Doe. Qt also adds some special keywords (just macros, from the compiler's point of view) which will discuss later on.

Here, the slides will walk through the different features that the QObject class and meta data enable. First example, the inherits keyword lets you check if a class inherits, i.e. is-a, class. A class inherits itself, so it can be used as a check when deciding how to cast. This shows that meta data knows of the class hierarchy. Another example is that the meta data contains information about your enums (have you passed the enum through the Q_ENUM macro). That way, you can convert enum values from and to text easily. This shows that the meta data has detailed knowledge about each class. All this information makes it quite easy to integrate Qt with scripting languages and other dynamic environments (PyQt, Ruby, JavaScript, etc)

10

One commonly used feature is the Qt property system. You have probably used it without noticing if you have looked at the Designer part of QtCreator. There you have a list of properties for each class easily available. Properties are implemented through getter and setter methods, so the actual value is stored as a private member. Convention is that: Getters are named after the property (no get-prefix) or with the is-prefix if a boolean. Setters are named with a set-prefix. The pair of functions is then made into a Qt property using the Q_PROPERTY macro. (arguments: type, name, READ, getter, WRITE, setter) Continues
11

Why do you want to use setters instead of public variables? Setters makes it possible to validate settings before you store them. Asserting all input is a good habit and improves code quality. Setters also make it possible to react to changes. For instance, if someone alters the text property of a label, the label makes sure that it is repainted as needed. Continues

12

Getter methods are also useful, even though not as useful as setters. (reading public variables is somewhat ok) What they do is that they separate the storage of the data from the reading. The read is indirect. Example: size contains both width and height. Width contains the width of the size. Example: a set of flags can be stored in a 32-bit word instead of in separate booleans. Continues

Properties as specified using the Q_PROPERTY macro. What can we read from it? All properties must have a type, a name and be readable. WRITE: They can be written to (optional) RESET: They can be reset (optional), this means to write no value to it NOTIFY: A notify signal is sent when the property is changed (more on signals later). An example would be, textChanged, in the previous class (text/setText) DESIGNABLE/SCRIPTABLE: Properties can be made available in designer and through scripts. STORED: Most properties are stored, meaning that they are a part of the object's state. However, some properties take their values from other properties (e.g. width is a part of size) and are not stored. USER: A user property is the property modified by the user, e.g. isChecked of a QCheckBox) CONSTANT: Constant properties do not change for an instance of a class. (i.e. it can change between instances) FINAL: Final properties are not overridden. This is not enforced,

13

14

but relied upon for performance optimizations. Continues

What does a property look like when using a class? You can use it directly, simply call the getter and setter. You can use the property/setProperty calls. They work with QVariants (a type that contains all types), so you have to cast it to strings using toString, etc. You can also ask the meta object about properties. Whether they can be read/written, their name, etc. This can then be used when calling property/setProperty. Continues

14

15

Using the property/setProperty approach, you can also create dynamic properties. This makes it possible to tag objects. For instance, to add a required boolean property to certain QLineEdits. The line editors do not need to know about this, but they can still keep track of it as a dynamic property. When validating the form, you can check this property and see if they are empty or not. Continues

When adding custom properties, it is good to follow the standard template. This makes it more intuitive to use your code with Qt code. The parts that you need are a getter and a setter. In addition, you need to store the state somewhere (private state). Optionally, you can allow the initialization of common properties (i.e. text of a label, but not window title of it as it isn't a top level widget most of the time). When all parts are there, you can use the Q_PROPERTY macro. From left to right: type, name, getter, setter. Continues

16

17

When implementing the custom properties, you simply implement the functions as always. Ensure that the constructor calls QObject with the parent pointer. In the getter, you could have returned a calculated value. For instance, with width, as a part of the size (shown earlier). In the setter, first update the state, then react to the change. This way, you can use the getter throughout the class. This makes it easier to make changes later on. Continues

In the special case of using an enumerated type as the type of a property, the Q_ENUMS macro can be used to inform Qt of the enumeration. This allows you to convert enumerations to and from strings (as shown earlier). For enums with bitwise values (i.e. that can be combined using OR), the Q_FLAGS macro can be used instead.

18

19

QObjects can be used in a way that takes care of all the dirty parts of memory management. It becomes almost as easy as working with a garbage collector, but you are still in full control. The trick is that all QObjects have a parent, or an owner. When the parent is deleted, it deletes all its children. This reduces the number of objects that you need to keep track of to one, in the ideal case. Continues

The very same parent-child relationship is used to represent visual hierarchies. Refer to the tree structure, the box contains the radio buttons (option1/2). The parent contains the box and the button. Compare to the previous slide. Continues

20

21

So, how does this make memory management easy. I still need to keep track of an object and make sure that I delete it? No, not if you use the stack cleverly. First of all, the example from the previous slide would probably have been implemented in the parent's constructor, i.e. this is the top-level parent. Second, when using the dialog, you allocate it on the stack. This means that the dialog, along with all its children, will be deleted when the scope ends. Continues

These slides intend to jog the students' memory, not explain the stack vs heap decision in full. The heap is used when you allocate memory dynamically. In C++, that means new/delete. In C you have used malloc and free. Heap memory must be explicitly freed, i.e. you must call delete on everything that you allocate. If you do not do so, you will leak memory. This will, eventually, lead to memory shortage and a crash. Dynamically allocated objects live until you delete them, so you have full control of when something is constructed or destructed. Continues
22 23

The stack is used for automatic memory allocations (as opposed to dynamic memory). The stack grows and shrinks when you make function calls. It is used for local variables, function arguments and return addresses. Objects allocated on the stack are destructed when they go out of scope. The scope ends with a }, or return, or for single-line scopes, at the end of the line. Continues

To get almost automatic memory management using Qt, the trick is to allocate the outermost parents on the stack, and the rest on the heap. For instance, the main function scope will be valid for as long as the application is running, so we allocate the application and window on the stack. The window, in turn, creates a bunch of child widgets in its constructor. To avoid destruction when the scope of the constructor ends, they are allocated dynamically. But, they are given the window as their parent object and are thus also destructed when the main function scope ends.

24

25

Continues

QObjects are structured into parent-child-relationships, but these hierarchies are not rigid. Instead, it is possible to move items around. To change a parent, simply call setParent with a new parent pointer as argument. This will notify both the old and new parent about the change. As the parent is notified, you can simply delete items from lists and such to remove them. If you want to move objects around, you can look for methods that take items. This means that the parent releases the child, and you get a pointer to the child. This can then be used for further operations (such as adding it to another parent). Unsafe/safe examples: the first does not work if item zero does not exist, and you cannot check for it and them delete it in one atomic operation. In the second alternative, item is null if item zero does not exist, otherwise you have removed it from the list and are free to work with it.
26 26

Continues

As QObjects all have parents, most QObject-related constructors take a parent pointer. Exceptions: QCoreApplication (QApplication) are QObjects w/o parents The parent usually appears as the left-most argument with a default value. However, as there can be multiple constructors, this can be compensated. It is common to use the explicit keyword to avoid unwanted automatic conversion of types. Example: QPushButton, the parent ends up last in all c'tor versions as there are no other default arguments Example: QLabel, the window flags and parent have default values. This means that the parent is the first of them, but after the text as the text property has no default value.

27

27

As these are recommended guidelines and making exceptions is always an option. However, following them, makes your code more Qt-esque. Allow parent to be null (take that into consideration in your code). Have one constructor taking only parent (lets you use your widgets from Designer). This forces you to handle the case where all properties are set to default values or not set at all. Placing parent in the right place helps reuse. Multiple constructors avoids the need to pass dummy settings to unused properties.

28

29

One of the key factors of Qt is the signals and slots mechanism. It is a mechanism to dynamically and loosely tie together events and changes with reactions Dynamically = at run-time Loosely = sender and receiver do not know each other events = timer event, clicks, etc. Not to be confused with actual events (QEvent). state changes = size changed, text changed, value changed, etc reactions = source code that actually does something This is what makes a Qt application tick Continues

Looking at an example from the first lecture. The three buttons all emit the clicked signal when they are clicked by the user (or activated by other means). They do this regardless whether something is connected to them or not, and regardless of what is connected to them. Continues

30

31

We choose to connect the buttons to the list widget's clear slot, and two custom slots in our custom code. As said earlier, the buttons do not care what they are connected to, and the slots are equally independent of what triggers them. You can even call them programatically as ordinary functions. Continues

So, the add button emits clicked, ending up in the on_addButton_clicked slot resulting in the following code being run. It simply asks for input and then adds it to the list. The delete button emits clicked, ending up in the on_deleteButton_clicked slot, where all selected items are deleted. The clear button emits clicked, which ends up in the clear slot of the QListWidget, which to us is a black box that does what its documentation says.

32

33

Continues

Signals and slots implement a pattern that is quite common. Usually, the mechanism is implemented as callback functions. It is important to recognize that signals/slots not are callbacks. For instance, a callback is simply a pointer. There is not actual check of signature compatibility. They also always work as direct calls, making them tricky to use across threads, etc. Signals and slots are more dynamic. For instance, the mechanism is 100% generic, so any QObject can be connected to any other QObject. The sender and receiver do not have to know of each others' implementations. Signature compatibility checks provide safety, but also flexibility, as they allow you to ignore arguments. All in all, signals and slots are easier, safer and more flexible. (for the advanced: compare to functors, which are clumsier from an implementation point and less flexible when it comes to skipping arguments, etc).
34 34

A slot is an ordinary function, just that it can be connected to signals. They do not have to be connected, you can call a slot like any other function, and you implement it as usual. Slots are declared in one of the sections public, protected and private slots. These access restrictions work as intended when calling the function, but a private or protected slot can be connected to any other signal, so they can be triggered from outside the class. Slots can return values, but connections cannot carry return arguments. Any number of signals can be connected to a single slot. This means that a single slot can serve several sources of events think keyboard shortcut, button, etc. Continues
35

Signals are defined in the signals section. This section can be considered protected, as a signal can only be emitted from within a class or its decendants. Signals always return void, and must not be implemented. Instead, moc provides function bodies that trigger the actual slot-activation-code. A signal can be connected to any number of slots, so a single event can trigger multiple reactions. It is fully possible to connect signals and slots across threads. Third party libraries such as Qxt (http://doc.libqxt.org/0.5.0/classQxtRPCPeer.html). Inside a signal emitting class, you use the emit keyword to emit a signal. Continues
36

You can make signals to slots connections between any two QObjects. Qt verifies that the signatures of the signal and slot match. The signature consists of the name of the signal or slot followed by the argument types. There must be no values nor variable names in the signature. It is also recommended to stick to using standard types, e.g. the ItemClass custom type reduces the reusability and should thus be avoided. Continues

When matching signatures, Qt is very forgiving. The basic rule is that Qt cannot create or convert values, but apart from that anything is allowed (i.e. skipping arguments). The examples on the slide demonstrate this. The errors are (from the top): missing the last int (cannot create) QString does not match int (cannot convert) missing the only int (cannot create) Continues

37

38

When making connections from Designer to your own source, Qt uses the automatic connection mechanism. It lets signals automatically connect to slots with the corresponding name (structure and examples on the slide). The automatic connections are made when connectSlotsByName is called. That is done at the end of the setupUi function generated by Designer. When using this mechanism, think about reusability. Sometimes handwriting a couple of connect statements can greatly improve readability of the code. Continues

A common scenario for signals and slots is to synchronize a pair of widgets. This is implemented by interconnecting two objects. (Refer to the example). If the value of dial1 changes, it emits valueChanged, which changes the value of dial2, that emits valueChanged, which changes the value of dial1, that emits... To avoid an infinite loop (which results in endless recursion, which will make the stack grow until you run out of memory and then crash miserably) the setValue function ignores attempts to set the current value.

39

40

Adding custom signals and slots to a class is really quite easy. You can add slots for numerous reasons. For instance, for each and every possible user action (file open, save, close, copy, cut, paste, help, about). Setter functions also make natural slots. Adding slots is just a matter of putting your functions in the right section of the declaration. Adding signals is just as easy, simply declare them in the signals section. If you have properties, it is convention to inform Qt of signals using the Q_PROPERTY macro. Continues

Implementing slots is just as implementing common methods. However, you must not forget the infinite loop protection. Emitting signals is as easy as calling emit signalName(arguments). When emitting signals, make sure to update the internal state first, so that your object is updated before it is queried.

41

42

Let's look at a real example that adds slightly more complexity to the situation. We will use two dial LCD pairs and interconnect them using our custom TempConverter class that converts between Celsius and Fahrenheit. It does not only convert, it monitors and emits signals when changes take place. Continues

The dialog contains a TempConverter object and the user interface. The user interface consists of two halves one for Celsius and one for Fahrenheit. Each consisting of a QGroupBox. The group boxes each contains a QDial and a QLCDNumber. Continues

43

44

The TempConverter class declaration. QObject, parent and Q_OBJECT macro are needed before we can add signals and slots. The setters are slots. There are signals for changes in either temperature. To avoid infinite loops we must have a current temperature. In this example we have decided to keep it in celsius. As we use integers throughout, this will not be very accurate, from a temperature conversion point of view. Continues
45

Looking at the slot implementations, the set temp Celsius slot contains the recursion lock, as the current temperature is kept in Celsius. It then updates the internal state and emits both signals. Notice that the argument of the Fahrenheit signal is retrieved from the getter function, which does the actual conversion C to F. The set Fahrenheit slot converts the temperature (F to C) and then uses the set Celsius method. Continues

46

The window making up the application then contains four widgets (dial + LCD for Celsius and Fahrenheit) and a TempConverter object. The connections between the dials are set up to go through the temperature converter object, while the LCDs are directly driven by the dials. Continues

The next slides will show how a signal propagates through the system. Everything starts with a user event the celsiusDial is moved. This results in it emitting the valueChanged signal Continues

47

48

The valueChanged signal is connected to display of the celsiusLcd and setTempCelsius of the temperature converter. The display call simply changes the value shown in the LCD, while the setTempCelsius call changes the value of the temperature converter. Continues

Changing the temperature of the temp converter results in two signals being emitted: tempCelsiusChanged and tempFahrenheitChanged Continues

49

50

The tempCelsiusChanged signal is connected to the celsiusDial's setValue slot. This slot detects that the value of the dial isn't changed thus halting there. The tempFahrenheitChanged signal connects to setValue of the fahrenheitDial, causing the dial's value to change Continues

As the dial's value changes, it emits the valueChanged signal with the new value Continues

51

52

This causes the fahrenheitLcd to be updated (through the display slot) The setTempFahrenheit slot is also called. This slot detects that the temperature isn't changed and stops there. Continues

At this point, all signals have propagated through the system and the TempConverter objects, and all the widgets are again in sync. Notice how the slots take responsibility to stop infinite propagation through the system. Also notice the importance of picking a scale to use for the current temperature. A rounding error causing a mismatch between the Celsius and Fahrenheit values could have caused the system to swing forth and back indefinitely.

53

54

When using signals and slots, a common scenario is to need to pass a value with a signal where the actual signal does not carry the needed value. For instance, the keyboard built from QPushButtons. It is not valid due to two issues: slot signature isn't valid (cannot hold a number) connections cannot hold argument values Continues

The brute force solution would be to add a slot for every key. This is quite tedious, and it is easy to get code repetition inside these slots unless they simply call another function right away. Code repetition is bad since changes (e.g. bug fixes) to that code has to be carried out in multiple locations. Continues

55

56

Another option would be to create a new button class emitting the signal with a value. This requires us to add a new class to the project for a specific case. This class cannot really be re-used in other scenarios. Continues

Comparing the two. Solution number one means loads of extra code to maintain. Solution number two adds another class that we probably won't reuse. (There is a third way, to let the receiving slot look at the sender of the signal and use a dynamic property to hold the value, but that is really not something to show to the non-advanced students) Continues

57

58

To solve this problem, the signal mapper class enters the picure. It allows us to associate each sender with a specific value. I.e. it converts a signal without arguments to a signal with one argument, where the argument value depends on the sender to the original signal. The trick is to put the signal mapper between the buttons and the keyPressed slot. Using setMapping, each button is mapped to a value (can be int, QString, a QWidget pointer or a QObject pointer). The clicked signal of each button is connected to the map slot of the signal mapper. The mapped signal of the mapper is then connected to the keyPressed slot. Continues
59

A graphical way to look at the connection is to see that by passing the signal through the signal mapper, a value (integer) is added to the signal.

60

S-ar putea să vă placă și