Andreas
Qt
Graphics View
Posted by Andreas
 in Qt, Graphics View
 on Friday, May 23, 2008 @ 07:47

Here’s another brain dump… I’m working on adding opacity support to QGraphicsItem and possibly QWidget, and have run into a common inside pattern, with a common problem to solve. Figured I’d share it with those interested.

Many of Qt’s properties propagate, some do in different ways, but the general pattern is that you set or modify a property on a widget or an item, and that property has some effect up and/or down its hierarchy. This is a good API pattern. Propagation is most often what you want, and you’re often surprised when a certain property doesn’t propagate (like QWidget::style).

  • Q{Graphics,}Widget::visible - if you explicitly show a widget, its implicitly hidden descendants are shown, explicitly hide a widget, and visible children are all implicitly hidden
  • Q{Graphics,}Widget::enabled - if you explicitly enable a widget, its implicitly disabled descendants are enabled, explicitly disable a widget, and its enabled descendants are implicitly disabled
  • Q{Graphics,}Widget::palette - you set a mask [*] of modified palette entries, the palette is resolved against its ancestors, and propagates to children
  • Q{Graphics,}Widget::font - you set a mask of modified font entries, the font (or rather -request) is resolved against the widget’s ancestors, and also propagates to children
  • Q{Graphics,}Widget::pos - if you move a widget, its children move with it
  • QGraphicsItem::transform - if you transform an item, its children transform

There are plenty of other properties that propagate. How does this actually work on the inside? Let’s take a look at visibility, as an example. QWidget::visible’s setter, QWidget::setVisible() (which is called by show() and hide()), sets local state only, and doesn’t touch other parts of the hierarchy - so the setter is “cheap”. Checking isVisible() is also cheap, but what you get from isVisible() is local state only; determining whether the widget will actually be visible to you when its window is shown, requires some more work. It’s relatively expensive to resolve “true” or “effective” visibility for a widget (compared to the setter anyway). Luckily though, QWidget’s visibility property is cleverly designed so you very seldom need to call show() or hide() at all, and I don’t know of any case where I’ve ever had to check my widget’s “effective” visibility. This is all done inside Qt. And that’s good, since we can do clever stuff to avoid the resolving expense. Because visibility rarely changes, we could even cache it.

For the record, if you want to know your widget’s effective visibility, there’s a handy QWidget::isVisibleTo() [**] that you can use:

if (widget->isVisible()) {
    // Whether this widget's local state is visible. But if the
    // parent is hidden, this widget is also effectively hidden.
}
QWidget *window = widget->window();
if (window->isVisible() && widget->isVisibleTo(window)) {
   // Yup, unless the window is outside your desktop, and unless
   // you've turned off your monitor, or perhaps you don't even
   // have a monitor at all, it should be visible. ;-)
}

OK, so let’s give an example of the opposite, a propagating property with a cheap effective getter and an expensive setter: QWidget::palette. When you set a palette, this palette is first resolved against the parent palette, and then explicitly propagated to all descendants. This operation can be expensive, depending on how deep your descendant hierarchy is. Why is the setter expensive? Because unlike QWidget::visible, which changes quite frequently during the runtime of an application (think how many times you open and close menus, dialogs, switch tabs, show and hide docks, minimize and maximize windows), palettes are very rarely changed at all, and mostly assigned once only, right before the widget is shown for the first time. So every time the widget is drawn and you need to access the palette, it’s very cheap and easy to access.

Expensive assignment, cheap resolve

If you think your property will be assigned only once-ish, and you need it to propagate, you go for the cheap resolve and expensive assignment. Typically this involves storing local state in every single child, so it’s also a memory vs. speed trade-off. This implicitly makes the individual child widget’s resolve step (”effective property resolve”) cheap. Using the example above, when you set a palette with a blue color entry for QPalette::Link, QWidget::setPalette will immediately walk the child tree and resolve all child widgets’ palettes against this new entry. So if the deepest descendant needs to redraw itself (or paint using QWidget::render), it has a fully resolved copy of the palette in its private section.

Laziness can help a lot here; you can speed up the expensive assignment by setting a dirty state on all non-dirty descendants - now the resolve is will only happen on access. The setter is the same order of complexity though, it’s still expensive. There’s still room to be clever :-).

Expensive resolve, cheap assignment

If you think your property will be assigned several times, and relatively rarely used, you go for the cheap assignment and expensive resolve. QWidget::pos, for example. Changing the position of a widget is something you might not do very many times yourself, but you can be sure that the layout system does this very many times for many different widgets in the same form (although only once per widget). Making the setter propagate its state immediately, like QWidget::palette, would mean form resizing would become a quadratic operation (for each widget, set the position and adjust all descendants’ positions). So instead, you set local state only. Now, when the widget is rerendered, Qt walks the tree to calculate each widget’s effective position, but it only needs to do this only once per paint event.

Caching can help here, tremendously in fact. When you resolve the state of a leaf node, you cache all resolved states for its parents all the way up to the ancestor. Now, if you need to resolve another leaf, there’s a fair chance you’ll hit a node that already has a resolved state for you. The complexity is still O(N lg N), but the cut-off makes the common case O(N).

So the question is - which approach to choose for widget opacity? :-)

— Andreas

[*] From the outside, QFont and QPalette look like actual value classes, but a little-known secret is that they actually represent a request set. When you assign a palette or a font to a widget, you’re not actually overriding any other entries than the ones you explicitly set, and even those might be modified to match what’s available. This is why neither QWidget::font nor QWidget::palette have symmetric setters and getters (the getters return the resolved palette).

[**] Qt 3 has QWidget::isVisibleToTLW(), which is obsolete (but present) in Qt 4. This does the same as widget->isVisibleTo(widget->window()) < = which is much clearer and doesn't use ugly abbreviations. :-)

12 Responses to “The benefit and cost of a good API pattern: Property Propagation”

» Posted by Norman Rasmussen
 on Friday, May 23, 2008 @ 08:06

I’d imagine Opacity isn’t something that would change very often (in current UI designs), so you could probably use the ‘Expensive assignment, cheap resolve’ mode. With modern UI’s however you find that opacity fading in/out as you mouse-over or focus something is fairly common, so that might be fairly expensive. I noticed you explicitly didn’t mention which method you used for GraphicsItem::transform, as I’m pretty sure that transport and opacity would use the same model.

» Reply from Andreas
 on Friday, May 23, 2008 @ 08:42
Andreas

Norman: Hm, nod, opacity doesn’t change often in traditional UIs, true. At most once per paint event, at least. When you assign opacity you usually do it to one or two items (fade in and out), and the children follow. So yeah, expensive setters may very well be the way to go…

» Posted by Enrico Ros
 on Friday, May 23, 2008 @ 09:16

Wow! This is the great news ;-) I know you’ll choose the best method for propagating the property so I won’t give my opinion on that! :D

Since now I managed opacity either with painter->setOpacity or glColor4f( .., .., .., a ). However, I found that the output of non-opaque-painting is different from what I expected:
- I expect that if I got a complex widget and set opacity on an inner (non leaf) widget, the contents of that widget were faded out (the whole ‘block’) to the wanted opacity.
- What I got was every widget getting non opaquely painted and accumulating (using SourceOver Porter-Duff) on the framebuffer.
I think that the second case is right in the meaning of property propagation, but the first one is what people will expect with QWidget::setOpacity() (also difficult to achieve in the ‘propagation’ implementation).

Thanks for your work, and for this big news ;-)

» Posted by AAk
 on Friday, May 23, 2008 @ 09:44

What about going the middle way : cheap setter, expensive getter first, but then cached ?
Then, if you set the opacity for multiple widgets, it won’t needlessly propagate, but then, when the getter is first used, new uses will be cheap.
Well if setting the opacity has just the same cost as invalidating cache (just change a single value, no computation), then this is a stupid approach and expensive setters would fit best I guess.

» Posted by Carina Denkmann
 on Friday, May 23, 2008 @ 11:58

If you care about performance, the simplest way would be to use the QPainter’s opacity attribute, and require that paintEvent handlers respect that setting, i.e. multiply with previous value for changes. Something like the matrix setter has both a “replace” and a “multiply with old” variant. Then you only have to change opacity once in the paint event, and have all children use that value. Children need to share the painter with the parent though.

» Reply from Andreas
 on Friday, May 23, 2008 @ 15:04
Andreas

AAk, yeah, if invalidating the cache is faster then I agree / if you have memory to spare for the dirty bit. For opacity it’s usually a float multiplication, which is fast (except when you don’t have an FPU) and you approach does work well when changing opacity on many nodes in the graph at a time.

Carina, what value does the opacity property give if every paint event needs to add explicit support for it? :-)

» Reply from Andreas
 on Friday, May 23, 2008 @ 15:06
Andreas

Enrico, if you embed a form, and set the window opacity on it, then you’ll get what you want. Otherwise, yes, opacity will be applied in source-over manner for each child independently, which means overlapping siblings will blend on top of one another instead of.. not doing so. Unfortunately it’s hard to do this kind of blending without an off screen buffer that covers all items and subitems (actually one for each level of opacity), and that’s very expensive, slow, and hard to maintain. But yeah, that would have been ideal…

» Posted by Carina Denkmann
 on Friday, May 23, 2008 @ 15:39

Andreas, I wrote “respect that setting”. I would like a child to show up transparent when the parent is transparent, but if the child has a different opacity, it needs to use the “effective” opacity by multiplying with that of the parent. If the child does not respect it, a simple painter->setOpacity(0.5) could make a child more opaque than the parent. Not something that you want. So you need to use

qreal opacity = painter->opactity();
painter->setOpacity(opacity * item->opacity());
/* paint item and all its children here with the same painter */
painter->setOpacity(opacity);

» Posted by Marc
 on Saturday, May 24, 2008 @ 07:01

Actually, when I read “Property Propagation”, my first thought was: Gosh, does Designer finally get the ability to define a form property as a forward to a child widget’s property, so I don’t have to write all those trivial forwarders:

void MyDialog::setFoo( bool foo ) { ui->fooCB->setChecked( foo ); }
bool MyDialog::foo() const { return ui->fooCB->isChecked(); }

But then you went on to talk about something completely different :)

» Reply from Andreas
 on Wednesday, May 28, 2008 @ 07:42
Andreas

Carina, hm, well since parents don’t paint their children in Graphics View we can’t just initialize the painter and pass the value. Each item is painted individually, and so each item must be able to set the correct opacity. If the painter’s opacity is initialized to the item’s effective opacity, and you can still override that by calling painter->setOpacity(0.5), I would argue that’s a good thing - you have full control. You can always multiply it in, or use QGraphicsItem::effectiveOpacity(), which gives you the right initial value in case you need it. I think it’s Qt’s job to make sure that it performs well.

» Posted by Scorp1us
 on Wednesday, May 28, 2008 @ 17:52

As someone who animated opacity for QGraphicsItems, I have this to say:

Well, opacity is a common property to be animated, therefore, it should be set easily, as it is likely set 10-30 times a second. This is particularly true for QGraphicsItems. The QWidgets probably won’t animate much, perhaps opacity is a replacement for disabled controls (allowing a ghost effect, rather than etched for disabled) (where one the condition for enabling is satisfied they become enabled and animate to full opacity).

QGraphicsItems though, can share a painter (painter.save(); painter.setOpacity(); drawChildren(painter); restore(); — just like scale(), so children know nothing of thier alpha status. They draw ignorant. If they modify their own opacity, it is relative to the parent opacity where you can call setOpacity, which will automatically propagate down to children. this is a fast setter and a fast getter. (Note this then can create a situation where opacity can be 1.5, where you can have a more opaque control than its siblings. (Assume parent opacity is .5, this control is 1.5, giving a final opacity of .75))

QWidgets still use a painter, so I don’t know why that solution would be any different.

And don’t forget that opacity > 0 means isVisible()==true, 0==false, and can be optimized out of paintings. (though the object must still have a geometry, which may be part of the visible area) (I’m sure you already know that though)

» Reply from Andreas
 on Wednesday, May 28, 2008 @ 20:39
Andreas

Scorp1us, if setOpacity() propagates immediately like you say, wouldn’t the setter then be slow?



© 2008 Nokia Corporation and/or its subsidiaries. Nokia, Qt and their respective logos are trademarks of Nokia Corporation in Finland and/or other countries worldwide.
All other trademarks are property of their respective owners.