Qt Academy has now launched! See how we aim to teach the next generation of developers. Get started
最新版Qt 6.3已正式发布。 了解更多。
最新バージョンQt6.5がご利用いただけます。 詳細はこちら

Flippin’ Widgets (Medium Rare, Please)

In Qt 4.6, QGraphicsWidget is gaining some new properties for transforming items: {x,y,z}Rotation, {x,y}Scale, {horizontal,vertical}Shear and transformOrigin, to be specific. How does this fit with the 4.6 master plan, the almighty Qt Kinetic project? Did someone just say "Like a glove! (Ka-bling!)"?! Yes, that sounds about right. Naturally I set out to use and abuse that shiny new stuff by means of states and animations. Seeing as I'm born flippy, a basic use case that came to mind was to create a widget that can be flipped between a "front" side and "back" side. Such a "two-faced" widget can (for example) be implemented as a QStackedWidget that contains the "front" widget on page 0 and the "back" widget on page 1. Flipping to the other side is then a matter of toggling the QStackedWidget's currentIndex property. But obviously we want to smoothly animate this change, as failure to do so would kinda take the "flip" out of "flippin'" (ya know?). One way of doing it is to first animate the item's yRotation from 0 to 90 (at which point the item will effectively be invisible), then toggle the currentIndex property, and finally animate yRotation from -90 to 0. The visual effect is that the "two-faced" item appears to rotate 180 degrees, exposing its Good (Evil) side.

Today's post is brough to you courtesy of programming languages of yore (either you're with them, or you're against them). Alas, here's the C++ code that sets up the stacked widget and such:

FlipWidget::FlipWidget(QGraphicsItem *parent)
: QGraphicsWidget(parent)
{
// The front.
QWidget *front = new QWidget();
QPushButton *toBackButton = new QPushButton("To Back");
QObject::connect(toBackButton, SIGNAL(clicked()), this, SIGNAL(flipRequest()));
toBackButton->setStyleSheet("background: pink");
{
QVBoxLayout *vbox = new QVBoxLayout(front);
vbox->addWidget(toBackButton);
}

// The back.
QWidget *back = new QWidget();
QPushButton *toFrontButton = new QPushButton("To Front");
QObject::connect(toFrontButton, SIGNAL(clicked()), this, SIGNAL(flipRequest()));
toFrontButton->setStyleSheet("background: yellow");
{
QVBoxLayout *vbox = new QVBoxLayout(back);
vbox->addWidget(toFrontButton);
}

// The stacked widget.
m_stack = new QStackedWidget();
m_stack->addWidget(front);
m_stack->addWidget(back);

QGraphicsProxyWidget *stackProxy = new QGraphicsProxyWidget();
stackProxy->setWidget(m_stack);
QGraphicsLinearLayout *vbox = new QGraphicsLinearLayout(this);
vbox->addItem(stackProxy);

The "front" and "back" widgets don't contain anything interesting in this example, just buttons that flip the item. Clicking both the "front" and "back" button will cause the item's own flipRequest() signal to be emitted; this is so that the same state logic can be used to handle flips from both "sides". Which brings us to the interesting bit, which is the code that sets up the states and animations for doing the flip:

    QStateMachine *machine = new QStateMachine(this);
QState *s0 = new QState(machine->rootState());
s0->assignProperty(this, "yRotation", 0);

QState *s1 = new QState(machine->rootState());
s1->assignProperty(this, "yRotation", 90);

QAbstractTransition *t1 = s0->addTransition(this, SIGNAL(flipRequest()), s1);
QPropertyAnimation *yRotationAnim = new QPropertyAnimation(this, "yRotation");
yRotationAnim->setDuration(250);
t1->addAnimation(yRotationAnim);

QState *s2 = new QState(machine->rootState());
QObject::connect(s2, SIGNAL(entered()), this, SLOT(togglePage()));
s2->assignProperty(this, "yRotation", -90);
s1->addTransition(s1, SIGNAL(polished()), s2);

QAbstractTransition *t2 = s2->addTransition(s0);
t2->addAnimation(yRotationAnim);

machine->setInitialState(s0);
machine->start();

Note the use of the QState::polished() signal (name still subject to change); this signal is emitted when all properties associated with a state change have reached their target values (effectively waiting for property animations, if any, to finish). When that happens, the machine transitions to a state that toggles the currentIndex (togglePage()) and inverts the rotation, and then unconditionally transitions back to the initial state (yRotation = 0), again animating the change. So, just three states to handle the flip both ways. Maybe it's the bias talking, but isn't it nice?

Finally, let's create a number of independently flippable widgets and put them in a grid:

class Window : public QWidget
{
public:
Window(QWidget *parent = 0)
: QWidget(parent) {
QGraphicsScene *scene = new QGraphicsScene(this);
QGraphicsWidget *widget = new QGraphicsWidget();
QGraphicsGridLayout *grid = new QGraphicsGridLayout(widget);
for (int i = 0; i < 6; ++i) {
for (int j = 0; j < 6; ++j)
grid->addItem(new FlipWidget(), i, j);
}
scene->addItem(widget);
QGraphicsView *view = new QGraphicsView(scene);
QVBoxLayout *vbox = new QVBoxLayout(this);
vbox->addWidget(view);
}
};

EDIT: Added video:

Man, those widgets they be flippin' all over! Best not show the Widget Pimp, lest he be pimpin' out!

The source code for this example can be grabbed here. You'll need to build it against qt/master. Something that might be fun to do is try to make the animated transition more "dramatic", for example by animating the scale of the item as well, experimenting with the duration ("How slow can it go?"), and setting some whacky easing curves (did I declare my love for InOutElastic already? Why yes, yes I did). Oh yeah, if you really want to differentiate yourself you can create a different animation for the back-to-front flip.

The FlipWidget class could be made more general-purpose by allowing you to pass in any widgets to use as the two sides, and adding a flip() slot (so an item can be flipped programmatically) and a flipped() signal. I'll leave that as a small exercise for the reader (hint 1: the flip() slot is a one-liner due to the presence of the flipRequest() signal; hint 2: the flipped() signal can be provided based on the QState::entered() signal, but you'll need to insert an extra state). Once you have flip() and flipped(), you should be able to add some logic (Qt State Machine-based, of course) to the basic example and turn it into a "Find-two-of-a-kind" memory game. (As I've forgotten to mention on numerous occasions, the criteria by which a Qt API will ultimately be judged is its ability to sustain the healthy development of silly games. Lots and lots of silly little games.)

You know, I'm starting to get the same "It's all coming together"-feeling as I did back with 4.4. It's a good feeling. Anyway, happy hacking, and don't be afraid to share your ideas and code!


Blog Topics:

Comments