Building Next Generation UIs Across Platforms with Qt

来源:互联网 发布:龙腾管家淘宝 编辑:程序博客网 时间:2024/06/12 11:28

Building Next Generation UIs Across Platforms with Qt

With Qt 4.6, a multitude of new classes have been added. All classes point in one direction – modern, animated, slick user interfaces.

Looking at user interfaces from a user perspective, today's mobile devices have started employing user interfaces emulating the real world. This has brought advanced graphical effects such as three dimensional transformations, blur, opacity – but also smooth transitions and physics simulations. In this type of application, using standard user interface building blocks is not always desired. Instead, the user interface must feel like a part of the physical device, its feeling and its brand.

Qt makes it possible to approach this situation from two directions. One half is the user interaction methods. Through Qt, multi-touch and gestures can be used to complement keys and mice. The other half is the graphics, transitions and effects. That is the half that we will focus on in this article.

The new graphics and animation classes of Qt 4.6 can be divided into a three groups: graphics effects, property animations and the state machine framework. These three together form the basis of a modern, animated user interface. Before we can apply these tricks, the application needs to have the right look.

The Look

Traditionally, desktop applications are built from square widgets. Widgets clip their own contents as well as their children's contents to their allotted rectangle. They are also built to be placed in relation to each other and stay roughly in the same position for most of the time. The result is a more or less pedantic interface built on a structure of rectangles. Desktop environments sometimes play with rounded corners of buttons and adding some color. Today's users expect more.

A classic user interface.
A modern user interface.
ClassicModern

To circumvent the limitations of widgets, it is possible to use the Graphics Viewframework as the basis of your user interface. This framework replaced the old canvas API, but also extended it. With Graphics View, it is possible to rotate, scale, shear and move items, but also to apply pseudo-three-dimensional transformations such as rotating an object around the x or y axes.

Through the versions of Qt, more and more features have been added, including the ability to integrate widgets into graphics scenes. Among the additions in Qt 4.6 are a framework of classes for animation, graphics effects, user interface states and transitions. Being able to mix graphics together with widgets and animating the result enables modern user interfaces to be created.

When creating a modern user interface, the designers' job is to get the mix of effects right. The developer implementing the interface must decide which tool to use for each part of the design. The tool at hand is an Animation Framework . These classes define, group and order animations. In addition to animations, a set of effects, such as blurring and adding shadows, are also available.

To drive such a user experience, some sort of state machine is needed. This is addressed by Qt's State Machine Framework . Here, a set of states can be defined, as well as transitions with animations and effects. There is no need for a list of booleans and a general state variable coupled with timers and custom slots. A single state machine configuration encapsulates the entire user interface with transitions and coupled animations. With this administrative problem solved, the developers can focus on adding actual functionality to the application. The user experience is delivered as expected by Qt.

Graphics Effects

One of the completely new features of Qt 4.6 is the set of new graphics effects classes. The base class, QGraphicsEffect , forms the foundation of a framework providing effects such as blur and drop shadow. These effects can be applied to user interface elements such as widgets and graphics items. It is possible to create custom effects that can be used in the same circumstances, meaning that you can do anything to anything.

All graphics effects are controlled by a number of properties. This means, not only, that the effects can be tuned for each application, but also that they can be exposed to the animation system which we will look at later on.

Using a blur effect.
Using a drop shadow effect.
BlurDrop shadow

One of the available effects is the QGraphicsBlurEffect . It blurs its source element depending on the blurRadius property. A small radius gives little blurring while a large radius leads to more blur. To apply the graphics effect to a graphics item, use the QGraphicsItem::setGraphicsEffect() method as shown below.

    QGraphicsBlurEffect  *effect = new QGraphicsBlurEffect (this);    effect->setBlurRadius(0);    QGraphicsPixmapItem  *item = new QGraphicsPixmapItem ();    // ...    item->setGraphicsEffect(effect);

For even more effect it is possible to combine effects. This is achieved by implementing a hierarchy of graphics items and then apply one effect to each level. For instance, both blur and opacity effects are applied in the example below. The QGraphicsRectItem is the item we want to apply the effects to while the QGraphicsItemGroup is used for the second effect.

    // Create the items    QGraphicsRectItem  *innerItem = new QGraphicsRectItem (-40,-40,80,80);    QGraphicsItemGroup  *outerItem = new QGraphicsItemGroup ();    // Apply the effects    innerItem->setGraphicsEffect(new QGraphicsOpacityEffect (this));    outerItem->setGraphicsEffect(new QGraphicsBlurEffect (this));    // Place the rect in a group and the group in the scene    innerItem->setParentItem(outerItem);    scene->addItem(outerItem);
Combining effects with graphics items.
A rectangle with blur and opacity effects

Animation Classes

One key element of a modern user interface is smooth, animated transitions instead of immediate and sudden changes. This can help the user understand how elements are connected, makes the user interface feel more real and mechanical. Last but not least important it also helps improving the visual appeal of the whole system.

Qt 4.6 comes with a brand new Animation Framework . Animations have been possible to implement using earlier versions of Qt, but this has been limited to providing time lines for animations and classes for animating transformations of graphics items. The new framework makes it possible to animate any property of any QObject .

The animation framework consists of a number of classes based on theQAbstractAnimation class. Basically, the classes used either facilitate synchronization of animations or animation of properties. The synchronization available lets animations be run either in series or in parallel. TheQPropertyAnimation class is probably what you will use the most. It takes care of animating individual properties of QObject s.

When animating a property, start and end values need to be specified. This is achieved by setting the QPropertyAnimation::setStartValue() andQPropertyAnimation::setEndValue() properties. It is also possible to set intermediate key values using the QPropertyAnimation::setKeyValueAt()function. When setting a key value, the animation is assumed to run between 0 and 1. This is not entirely true, as animations can use different easing curves. These curves can sometimes move slightly outside the 0 to 1 range as illustrated below.

Linear easing curve.In-out-quad easing curve.In-out-elastic easing curve.In-back easing curve.

Illustrating all these techniques, the snippet below shows a simple animation that lets a QPushButton appear within a container widget. As this animation will only be used once, the argumentQAbstractAnimation::DeleteWhenStopped is passed to the start method. This ensures that the animation object is deleted when it has been stopped.

    QPropertyAnimation  *animation = new QPropertyAnimation (button, "geometry");    animation->setStartValue(startRect);    animation->setEndValue(endRect);    animation->setEasingCurve(QEasingCurve::OutBounce);    animation->start(QAbstractAnimation::DeleteWhenStopped);
The same technique shown here for widgets can be used for any QObject . However, the commonly appearing QAbstactGraphicsItem class does not inherit from QObject . To resolve this, a custom graphics item must be created for these cases. This is not as cumbersome as it might sound. All the new class has to do is inherit from both QObject and the required QGraphicsItem class and then expose the required properies. For instance, the CustomRectItem shown below exposes the rotation property of a QGraphicsRectItem to the animation system. No additional code except a short constructor and the Q_PROPERTY line is needed.
class CustomRectItem : public QObject , public QGraphicsRectItem {    Q_OBJECT    Q_PROPERTY(qreal rotation READ rotation WRITE setRotation)public:    CustomRectItem(const QRectF  &rect)        : QObject (0),QGraphicsRectItem (rect)    { }};

Given the CustomRectItem class, rotating the object is as easy as creating aQPropertyAnimation object for that class and starting it.

    QPropertyAnimation  *animation = new QPropertyAnimation (item, "rotation", this);    animation->setStartValue(0);    animation->setEndValue(90);    animation->start();

Adding States

A common use case for animations is to move between different states of the user interface such as list, detail and edit. Most developers have heard of, and perhaps even used, state machines. These are great for defining states and transitions between states.

Qt 4.6 comes with a brand new State Machine Framework letting you do just this. Using this framework, objects' property values can be defined for a set of states. Transitions can then be added between the different states and the whole machine can be started.

A simple example showing this can be built from our earlier examples. We take a graphics scene with two rectangles embedded in groups (for double effects). We then define two states, one where the green rectangle is shown 100% opaque without blur while the yellow is 50% opaque and blurred, and then one where these effects are applied to the green rectangle instead of the yellow one.

The yellow rectangle blur state.The green rectangle blur state.

The first part of creating this is to set up the scene – you can see this in the source code available for download alongside this article. The results are rectangle items and groups that are called greenRectyellowRectgreenGroup andyellowGroup. We then apply effects to these items, add them to the scene and such.

    QGraphicsOpacityEffect  *greenOpacity = new QGraphicsOpacityEffect ();    QGraphicsOpacityEffect  *yellowOpacity = new QGraphicsOpacityEffect ();    QGraphicsBlurEffect  *greenBlur = new QGraphicsBlurEffect ();    QGraphicsBlurEffect  *yellowBlur = new QGraphicsBlurEffect ();    greenRect->setGraphicsEffect(greenOpacity);    yellowRect->setGraphicsEffect(yellowOpacity);    greenGroup->setGraphicsEffect(greenBlur);    yellowGroup->setGraphicsEffect(yellowBlur);

When all this is in place, the state machine framework enters the picture. We create two states, the greenState and the yellowState. The initialization of the green state is shown below. Notice that properties for the state are set using theassignProperty() method that takes an object pointer, a property name and a value. The value is handled as a QVariant , so you can put almost anything there. You can even add support for your own types using the Qt type system, but I'm digressing.

    QState  *greenState = new QState ();    greenState->assignProperty(greenOpacity, "opacity", 1);    greenState->assignProperty(greenBlur, "blurRadius", 0);    greenState->assignProperty(yellowOpacity, "opacity", 0.5);    greenState->assignProperty(yellowBlur, "blurRadius", 5);

For each state, a set of transitions must be added. These transitions make it possible to move from the current state to another state. As we only have two states, that leaves us with one transition per state.

Before we look at the code, let's discuss transitions. All transitions inherit from the QAbstractTransition class. At this level, source and target states can be specified. There are then specializations of transitions depending on how the transition is triggered. For instance, there are event transitions (QEventTransition ) that can intercept any event, a specific key press (QKeyEventTransition ) or mouse action (QMouseEventTransition ). Then there is the QSignalTransition class that implements a transition triggered by a specific signal. This is the class that we use in the code below.

    QSignalTransition  *t = new QSignalTransition (this, SIGNAL(yellowClicked()));    t->setTargetState(yellowState);    greenState->addTransition(t);

With all states in place, they must be added to the QStateMachine object that handles the big picture. As you can see, we add all states to the machine, but set one as the initial state. This is needed to give the state machine a starting point (remember that all transitions have a source state).

    QStateMachine  *machine = new QStateMachine (this);    machine->addState(greenState);    machine->addState(yellowState);    machine->setInitialState(greenState);

Now, it is time to tell the machine how to transition from one state to another. This is where the animation classes you saw earlier enter the picture. It is possible to specify exactly which animation to use for each object, property and transition. But to get things going you can also specify default animations for each object and property at a state machine level.

    machine->addDefaultAnimation(new QPropertyAnimation (greenBlur, "blurRadius"));    machine->addDefaultAnimation(new QPropertyAnimation (yellowBlur, "blurRadius"));    machine->addDefaultAnimation(new QPropertyAnimation (greenOpacity, "opacity"));    machine->addDefaultAnimation(new QPropertyAnimation (yellowOpacity, "opacity"));

Before you can start the application and enjoy smooth transitions, one final step is left: starting the state machine.

    machine->start();

What About Widgets?

Until now, we've focused on graphics views and items, but almost everybody is sitting with a widget-based legacy user interface. The prospect of replacing all that work seems like a rather large task. The good news is that almost all the effects can be used in a widget-based world as well.

Let's start with an example. Look at the two dialog states below. Here, the unused widgets are placed outside the containing widget. Another option would have been to alter the visibility of the items.

Different states used by an editor dialog.

These states are expressed in code through the viewState and editState variables shown below. The states specific to each state are listed through theassignProperty() calls. As you can tell, the sacrifice that you need to make to use these effects is to not use layouts. This is unfortunate, but by exploring the possibilities you will find the right mix of bling and flexible layouts.

    QState  *viewState = new QState ();    viewState->assignProperty(editButton, "geometry", QRect(180, 5, 70, 30));    viewState->assignProperty(okButton, "geometry", QRect(110, -35, 70, 30));    viewState->assignProperty(cancelButton, "geometry", QRect(180, -35, 70, 30));    viewState->assignProperty(m_textLabel, "geometry", QRect(5, 5, 175, 30));    viewState->assignProperty(m_textEdit, "geometry", QRect(-110,5, 105, 30));    QState  *editState = new QState ();    editState->assignProperty(editButton, "geometry", QRect(180, 55, 70, 30));    editState->assignProperty(okButton, "geometry", QRect(110, 5, 70, 30));    editState->assignProperty(cancelButton, "geometry", QRect(180, 5, 70, 30));    editState->assignProperty(m_textLabel, "geometry", QRect(-200, 5, 175, 30));    editState->assignProperty(m_textEdit, "geometry", QRect(5,5, 105, 30));

When the states have been defined, it is time to add transitions.

    viewState->addTransition(editButton, SIGNAL(clicked()), editState);    editState->addTransition(okButton, SIGNAL(clicked()), viewState);    editState->addTransition(cancelButton, SIGNAL(clicked()), viewState);

We then add the states to the machine before adding the default animations. The label and editor widget are moved using the standard easing curve while the buttons move in a more "bouncy" way, using the QEasingCurve::InOutBackcurve.

    QStateMachine  *machine = new QStateMachine (this);    machine->addState(viewState);    machine->addState(editState);    machine->setInitialState(viewState);    QPropertyAnimation  *pa;    machine->addDefaultAnimation(pa = new QPropertyAnimation (editButton, "geometry"));    pa->setEasingCurve(QEasingCurve::InOutBack);    // ...    machine->addDefaultAnimation(new QPropertyAnimation (m_textEdit, "geometry"));

Having defined all states and transitions, the whole user interface comes to life as soon as the QStateMachine instance is started. Hopefully, your users will have a pleasant surprise when clicking the Edit button for the first time.

The source code for the example discussed in this article is available for download: qq32-blurstates.zip

Johan Thelin is the author of the Foundations of Qt Development book available from Apress and has a soft spot for embedded systems. He is also involved in the QtCentre as well as working with software development and technical writing.