0

I am trying to bounce a QWidget around the screen. This is the code i tried.

class Window : public QMainWindow {
  public:
    void moveEvent(QMoveEvent* aEvent) override;
};

void Window::moveEvent(QMoveEvent* aEvent) {
  QSizeF screenSize = QGuiApplication::primaryScreen()->screenSize();
  QRect oldRect = this->geometry();
  QRect newRect = oldRect;
  QPoint offset;

  if (newRect.left() == 0) {
    offset.setX(1);
  }
  else if (newRect.right() == screenSize.width()) {
    offset.setX(-1);
  }
  if (newRect.top() == 0) {
    offset.setX(1);
  }
  else if (newRect.bottom() == screenSize.height()) {
    offset.setX(-1);
  }
  newRect.setTopLeft(newRect.topLeft() + offset);
  newRect.setBottomRight(newRect.bottomRight() + offset);

  QTimer::singleShot(1, [this, newRect]() {
    setGeometry(newRect);
  });
}

int main(int argc, char** argv) {
  QApplication app{argc, argv};

  Window* w = new Window();
  w->show();
  w->setGeometry(w->geometry());

  return app.exec();
}

However, the window does not move around the screen, but somewhat jitters in place. When i move the window with the mouse and let go. It moves sporadically around the desktop, which is also not what i want.

Does anyone know if this is possible? If so, does anyone know the right way to do this?

1 Answers1

1

There are several problems with the posted code, including:

  • The Window class doesn't have any member-variable to keep track of its current direction of motion. Without keeping that state, it's impossible to correctly calculate the next position along that direction of motion.

  • Driving the animation from within moveEvent() is a bit tricky, since moveEvent() gets called in response to setGeometry() as well as in response to the user actually moving the window with the mouse; that makes unexpected feedback loops possible, resulting in unexpected behavior.

  • The code assumes that the screen's usable surface area starts at (0,0) and ends at (screenSize.width(),screenSize.height()), which isn't necessarily a valid assumption. The actual usable area of the screen is a rectangle given by availableGeometry().

  • When calling setGeometry(), you are setting the new location of the area of the window that the Qt program can actually draw into. However that's only a 99% subset of the actual on-screen area taken up by the window, because the window also includes the non-Qt-controlled regions like the title bar and the window-borders. Those parts need to fit into the availableGeometry() also, otherwise the window won't be positioned quite where you wanted it to be, which can lead to anomalies (like the window getting "stuck" on the top-edge of the screen)

In any case, here's my attempt at rewriting the code to implement a closer-to-correct "bouncing window". Note that it's still a bit glitchy if you try to mouse-drag the window around while the window is also trying to move itself around; ideally the Qt program could detect the mouse-down-event on the title bar and use that to disable its self-animation until after the corresponding mouse-up-event occurs, but AFAICT that isn't possible without resorting to OS-specific hackery, because the window-title-bar-dragging is handled by the OS, not by Qt. Therefore, I'm leaving that logic unimplemented here.

#include <QApplication>
#include <QMainWindow>
#include <QMoveEvent>
#include <QShowEvent>
#include <QScreen>
#include <QTimer>

class Window : public QMainWindow {
public:
    Window() : pixelsPerStep(5), moveDelta(pixelsPerStep, pixelsPerStep)
    {
       updatePosition();  // this will get the QTimer-loop started
    }

private:
    void updatePosition()
    {
       const QRect windowFrameRect = frameGeometry();  // our on-screen area including window manager's decorations
       const QRect windowRect      = geometry();       // our on-screen area including ONLY the Qt-drawable sub-area

       // Since setGeometry() sets the area not including the window manager's window-decorations, it
       // can end up trying to set the window (including the window-decorations) slightly "out of bounds",
       // causing the window to "stick to the top of the screen".  To avoid that, we'll adjust (screenRect)
       // to be slightly smaller than it really is.
       QRect screenRect = QGuiApplication::primaryScreen()->availableGeometry();
       screenRect.setTop(    screenRect.top()    + windowRect.top()    - windowFrameRect.top());
       screenRect.setBottom( screenRect.bottom() + windowRect.bottom() - windowFrameRect.bottom());
       screenRect.setLeft(   screenRect.left()   + windowRect.left()   - windowFrameRect.left());
       screenRect.setRight(  screenRect.right()  + windowRect.right()  - windowFrameRect.right());

       // Calculate where our window should be positioned next, assuming it continues in a straight line
       QRect nextRect = geometry().translated(moveDelta);

       // If the window is going to be "off the edge", set it to be exactly on the edge, and reverse our direction
       if (nextRect.left()   <= screenRect.left())    {nextRect.moveLeft(  screenRect.left());   moveDelta.setX( pixelsPerStep);}
       if (nextRect.right()  >= screenRect.right())   {nextRect.moveRight( screenRect.right());  moveDelta.setX(-pixelsPerStep);}
       if (nextRect.top()    <= screenRect.top())     {nextRect.moveTop(   screenRect.top());    moveDelta.setY( pixelsPerStep);}
       if (nextRect.bottom() >= screenRect.bottom())  {nextRect.moveBottom(screenRect.bottom()); moveDelta.setY(-pixelsPerStep);}
       setGeometry(nextRect);

       QTimer::singleShot(20, [this]() {updatePosition();});
    }

    const int pixelsPerStep;
    QPoint moveDelta;  // our current positional-offset-per-step in both X and Y direction
};

int main(int argc, char** argv) {
  QApplication app{argc, argv};

  Window* w = new Window();
  w->show();

  return app.exec();
}
Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234