如何从两侧裁剪 Qt window?

How can I crop Qt window from both sides?

我有一个 Qt window,其中所有小部件都已插入中央小部件,因此当 window 调整大小时,小部件大小不会改变(原始 Window):

有时,我不需要在右侧或左侧看到一些小部件,所以我尝试裁剪 Windows 因为那些小部件不可见(Right-cropped window):

但是,这仅适用于 Qt window 右侧的小部件,当我尝试从左侧裁剪 window 时,左侧的小部件未隐藏( Right-cropped window):

由于 PushButton1 不可见,是否有任何 way/trick 能够从左边缘裁剪?

我必须承认我不能完全确定我是否完全理解了OP的意图。不过, 的评论引起了我的注意:

there's no direct and safe way to know "what" border of the widget has actually caused the resize

我相信这并没有那么复杂。为了证明我是对还是错,我刚刚启动了一个 MCVE 来检查一下。

因此,我从假设开始,即通过拖动左 and/or 顶部 window 边框来调整大小也应该导致更改 window 坐标。所以,应该可以确定这一点,并保持 resp 的位置。 window children 相对于桌面的常量(而不是 Qt 中常见的 top/left window 角)。

这可以通过简单地通过 resp 的负运动来补偿 window 的运动来完成。 windowchild仁。这可能(或可能不)仅限于 window 调整大小。 (如果它也应用于 window 运动,这会导致另一个有趣的效果,使 window 成为 peephole-like。)

child windows 的放置在 Qt 中通常是布局管理器的主题(源自 QLayout)。虽然有多种布局管理器 built-in(涵盖日常业务的需要),但也可以选择制作 Custom Layout Manager。另一种选择是不使用任何布局管理器,而是直接放置和调整 child 小部件。为了保持 MCVE 简单和简短,我使用了后一个选项。

当我摆弄这个时,我注意到一些我以前没有意识到的影响,并尝试分别处理它。

  • 当在 resizeEvent() 中检索到当前 window 位置时,它会提供旧位置。无法从调用数据(QResizeEvent 类型)中检索当前位置。解决方案是推迟补偿 child 小部件移动,直到调整大小的处理完成。这可以通过 single-shot 计时器(延迟为 0)方便地实现。

  • 拖动左边框或上边框 window 时,resizeEvent() 伴随着 moveEvent()。 (在某种程度上对我来说有意义。)因此,我必须区分 window 移动导致的 moveEvents() 和 window 调整大小导致的 moveEvents() 。我注意到(在我的例子中)resizeEvent()s 总是在 moveEvent()s 之前发送。满怀希望,这可能是可靠的,我使用了一个标志来忽略 resizeEvent() 中的 moveEvent()s,直到我延迟处理 child 移动。

(我必须承认 Python 只是我的第二(或第三)语言,而我的第一语言是 C++。所以,我用 C++ 写了一个 proof-of-concept,将端口延迟到 Python/PyQt 直到稍后。)

这就是我最终得到的结果:

#include <QtWidgets>

class Window: public QWidget {
  private:
    QVector<QWidget*> _pQWidgetsMove;
    int _x, _y;
    bool _updateXY = true;

  public:
    Window(QWidget *pQParent = nullptr): QWidget(pQParent) { }
    virtual ~Window() = default;

    Window(const Window&) = delete;
    Window& operator=(const Window&) = delete;

    void setMoveWidgets(QVector<QWidget*> pQWidgetsMove)
    {
      _pQWidgetsMove = pQWidgetsMove;
    }

  protected:
    virtual void showEvent(QShowEvent *pQEvent) override;
    virtual void resizeEvent(QResizeEvent *pQEvent) override;
    virtual void moveEvent(QMoveEvent *pQEvent) override;
};

void Window::showEvent(QShowEvent *pQEvent)
{
  QWidget::showEvent(pQEvent);
  _x = x(); _y = y();
}

void Window::moveEvent(QMoveEvent* pQEvent)
{
  QWidget::moveEvent(pQEvent);
  if (_updateXY) {
    _x += pQEvent->pos().x() - pQEvent->oldPos().x();
    _y += pQEvent->pos().y() - pQEvent->oldPos().y();
  }
}

void Window::resizeEvent(QResizeEvent* pQEvent)
{
  QWidget::resizeEvent(pQEvent);
  _updateXY = false;
  QTimer::singleShot(0, // 0 ms -> idle callback
    [this, x = x(), y = y()]() {
      for (QWidget* pQWidget : _pQWidgetsMove) {
        pQWidget->move(pQWidget->x() + _x - x, pQWidget->y() + _y - y);
      }
      _x = x; _y = y;
      _updateXY = true;
    });
}

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  QApplication app(argc, argv);
  // config GUI
  const int wMain = 320, hMain = 240; // initial size of main window
  const int nBtns = 4; // number of buttons
  // setup GUI
  Window qWinMain;
  qWinMain.setWindowTitle("Fun with Resize");
  qWinMain.resize(wMain, hMain);
  QPushButton qBtns[nBtns];
  { int i = 0;
    for (QPushButton& qBtn : qBtns) {
      const int xBtn = i * wMain / nBtns, wBtn = wMain / nBtns;
      qBtn.setParent(&qWinMain);
      qBtn.move(xBtn, 0);
      qBtn.resize(wBtn, hMain);
      qBtn.setText(QString("Button %1").arg(i + 1));
      ++i;
    }
  }
  QCheckBox qTglStick("Stick Buttons to Desktop", &qWinMain);
  qTglStick.move(2, 2);
  qWinMain.show();
  // install signal handlers
  QObject::connect(&qTglStick, &QCheckBox::toggled,
    [&](bool checked) {
      QVector<QWidget*> pQWidgetsMove;
      if (checked) {
        for (QPushButton& qBtn : qBtns) pQWidgetsMove.push_back(&qBtn);
      }
      qWinMain.setMoveWidgets(pQWidgetsMove);
    });
  // runtime loop
  return app.exec();
}

输出(Windows,Visual Studio 2019):

这看起来还不错。

在拖动 and/or 左上边框调整 window 大小时,按钮会轻微晃动。我假设这是由 QWidget::move() 的调用导致的事件的顺序处理造成的。我真的不知道如何 work-around 这个。 (抑制 paintEvent()s 一段时间?)所以,我决定暂时忍受这个。

在 Windows 上工作时,Linux 怎么样?

所以,我在 Debian 中(在 VM 中)重建并测试了。

输出(Debian,g++ 8.3.0):


最后把上面MCVE的端口给Python3 / PyQt5:

#!/usr/bin/python3

import sys
from PyQt5.QtCore import QT_VERSION_STR
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QCheckBox

class Window(QWidget):

  def __init__(self, parent = None):
    QWidget.__init__(self, parent)
    self.widgetsMove = list()
    self.x, self.y = (0, 0)
    self.updateXY = True

  def setMoveWidgets(self, widgets):
    self.widgetsMove = widgets
    self.x, self.y = QWidget.x(self), QWidget.y(self)

  def moveEvent(self, event):
    QWidget.moveEvent(self, event)
    if self.updateXY:
      self.x += event.pos().x() - event.oldPos().x()
      self.y += event.pos().y() - event.oldPos().y()

  def moveWidgets(self, xOld, yOld):
    for widget in self.widgetsMove:
      widget.move( \
        widget.x() + self.x - xOld, \
        widget.y() + self.y - yOld)
    self.x, self.y = xOld, yOld
    self.updateXY = True

  def resizeEvent(self, event):
    QWidget.resizeEvent(self, event)
    self.updateXY = False
    QTimer.singleShot(0, \
      lambda x = QWidget.x(self), y = QWidget.y(self): \
        Window.moveWidgets(self, x, y))

if __name__ == '__main__':
  print("Qt Version: {}".format(QT_VERSION_STR))
  app = QApplication(sys.argv)
  # config GUI
  wMain, hMain = 320, 240
  nBtns = 4
  # setup GUI
  qWinMain = Window()
  qWinMain.setWindowTitle("Fun with Resize (PyQt5)")
  qWinMain.resize(wMain, hMain)
  qBtns = list()
  for i in range(0, nBtns):
    xBtn = i * wMain / nBtns
    wBtn = wMain / nBtns
    qBtn = QPushButton(qWinMain)
    qBtn.move(xBtn, 0)
    qBtn.resize(wBtn, hMain)
    qBtn.setText("Button {}".format(i + 1))
    qBtns.append(qBtn)
  qTglStick = QCheckBox("Stick Buttons to Desktop", qWinMain)
  qTglStick.move(2, 2)
  qWinMain.show()
  # install signal handlers
  qTglStick.toggled.connect(lambda checked: \
    qWinMain.setMoveWidgets(qBtns if checked else list()))
  # runtime loop
  sys.exit(app.exec_())

输出(Debian、Python3、PyQt5):