如何在桌面上弹跳 QWidget

How to bounce a QWidget around the desktop

我试图在屏幕上弹跳 QWidget。这是我试过的代码。

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();
}

然而,window 并没有在屏幕上四处移动,而是在原地有些抖动。当我用鼠标移动 window 并松开时。它偶尔会在桌面上移动,这也不是我想要的。

有人知道这是否可行吗?如果是这样,有谁知道正确的方法吗?

发布的代码有几个问题,包括:

  • Window class 没有任何 member-variable 来跟踪其当前的运动方向。如果不保持该状态,则无法正确计算沿该运动方向的下一个位置。

  • moveEvent() 中驱动动画有点棘手,因为 moveEvent() 被调用以响应 setGeometry() 以及响应用户实际用鼠标移动 window;这使得意想不到的反馈循环成为可能,从而导致意想不到的行为。

  • 代码假设屏幕的可用表面积从 (0,0) 开始到 (screenSize.width(),screenSize.height()) 结束,这不一定是有效的假设。屏幕的实际可用区域是availableGeometry().

    给定的一个矩形
  • 当调用setGeometry()时,您正在设置Qt程序实际可以绘制到的window区域的新位置。然而,这只是 window 实际 on-screen 区域的 99% 子集,因为 window 还包括 non-Qt-controlled 区域,如标题栏和 window-borders。这些部分也需要适合 availableGeometry(),否则 window 将无法完全定位到您想要的位置,这可能导致异常(例如 window 卡住" 在屏幕的 top-edge 上)

无论如何,这是我重写代码以实现 closer-to-correct“弹跳 window”的尝试。请注意,如果您尝试 mouse-drag 周围的 window 而 window 也在尝试移动自己,它仍然有点小故障;理想情况下,Qt 程序可以检测到标题栏上的 mouse-down-event 并使用它来禁用其 self-animation 直到相应的 mouse-up-event 发生之后,但是 AFAICT 如果不求助于 OS-specific hackery,因为 window-title-bar-dragging 由 OS 处理,而不是由 Qt 处理。因此,我在此保留未实现的逻辑。

#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();
}