A decorative animation can sit outside a scroll viewport while its widget still reports isVisible() == true. A timer that only checks that property can keep advancing the animation even when the user cannot see it.
Qt's visibility documentation distinguishes a widget's visible state from whether it is actually exposed on screen. That distinction matters when deciding whether to run a repeating timer.
This source walkthrough uses ParticleBackdrop from Fluent-Qt 1.8.3, a Qt Widgets library maintained by this account. The component paints decorative particles with QPainter. The useful part to examine here is how it decides to stop doing continuous work.
Compute the running state in one place
The implementation combines three conditions:
const bool run = motionAllowed() && inViewport() &&
(!pauseWhenInactive ||
qApp->applicationState() == Qt::ApplicationActive);
motionAllowed() checks the local animation switch, positive speed, the widget's enabled state, the effective high-contrast theme, and the library's policy for continuous motion. inViewport() checks visibility and rectangular clipping through the widget's ancestors. Application inactivity is a separate, configurable reason to pause.
The timer only changes state when the result changes:
if (run == timer.isActive())
return;
if (run) {
clock.start();
timer.start();
} else
timer.stop();
emit q->animatingChanged(run);
This avoids restarting the timer whenever an unrelated event causes the conditions to be checked again. It also distinguishes the requested setting, animationEnabled, from the effective state, isAnimating().
See the running-state implementation.
Check clipping through the parent chain
The visibility check begins with the component's local rectangle. For each parent, it translates the remaining rectangle into that parent's coordinates and intersects it with the parent's rectangle:
QRect clipped = q->rect();
const QWidget* child = q;
for (const QWidget* parent = q->parentWidget();
parent; parent = parent->parentWidget()) {
clipped.translate(child->pos());
clipped &= parent->rect();
if (clipped.isEmpty())
return false;
child = parent;
}
When an ancestor moves its contents entirely outside a viewport, the intersection becomes empty. Checking only the component's own move events would miss changes caused by that ancestor.
The component therefore installs event filters on its ancestors. Show, hide, move, resize, reparenting, and window-state events schedule another visibility check. A guarded QTimer::singleShot(0, ...) combines pending checks and rebuilds the ancestor watch list after reparenting. The callback has the widget as its context.
This is a rectangular clipping check. It does not detect every possible form of occlusion, such as another application covering the window or an overlapping sibling. Partially visible components still animate. That is the boundary of this implementation, and it should be kept explicit when adapting the approach.
Resume without advancing through the whole pause
The elapsed-time clock restarts when animation resumes. Each timer callback then measures its actual elapsed interval and caps the step at 0.1 seconds before applying the speed multiplier:
const double delta = std::min(clock.nsecsElapsed() / 1e9, .1);
clock.restart();
elapsed += delta * speed;
q->update();
The excerpt omits pointer and ripple bookkeeping. Resetting the clock excludes the paused interval; the cap limits the jump after a delayed callback. This choice fits a decorative effect, but a simulation that must track wall-clock time would need different behavior.
The default timer interval is qCeil(1000.0 / 30), or 34 milliseconds. Treat this as an animation scheduling budget. QTimer does not guarantee an exact frame rate, and window exposure can cause additional paints.
Test the transitions
The component test source exercises these transitions for all three particle presets:
- Move the component completely outside its parent, then move it back.
- Reparent it and move the new ancestor out of view.
- Hide the containing viewport.
- Disable local animation, enable reduced motion, or set speed to zero.
- Switch the effective theme to high contrast.
These checks inspect isAnimating(), so a static image alone cannot make them pass. They describe timer behavior; they are not a battery-use benchmark. This walkthrough was checked against the tagged source and existing tests, without rerunning the test suite for the article.
If you adapt this pattern, write down the exact conditions that stop your animation and the events that can change each condition. An otherwise correct predicate will still leave a timer running if nothing reevaluates it when an ancestor moves.
AI disclosure: This article was prepared by an AI assistant at the project maintainer's request and checked against the linked source and Qt documentation. It does not present generated anecdotes or measurements as the maintainer's experience.
Top comments (0)