How to monitor changes to an arbitrary widget?

限于喜欢 提交于 2019-12-09 23:14:45

问题


I am starting a QT5 application with a rather complex design based on Qt Widgets. It runs on Beagleboard with a touchscreen. I will have a rather weird local invention instead of the LCD display. It's a laser painting on acrylic plate. It has no driver yet. To actually update a screen I must create a screenshot of the window as bitmap, turn it to grayscale and feed to a proprietary library, which will handle the laser. It should look cute, when ready. Unfortunately, the laser blinks on update, so I cannot just make screenshots on timer, or it will be jerky like hell.

I need to run a function every time a meaningful update of GUI happens, while preferably ignore things like button being pressed and released. Is there some way to create a hook without subclassing every single Qt Widget I will use? The only way to do this I know is to override paintEvent of everything. I want a simpler solution.

Possible assumptions are: the application will be running under X server with dummy display, will be the only GUI app running. Some updates happen without user input.


回答1:


The code below does it. It doesn't dig too deeply into the internals of Qt, it merely leverages the fact that backing store devices are usually QImages. It could be modified to accommodate OpenGL-based backing stores as well.

The WidgetMonitor class is used to monitor the widgets for content changes. An entire top-level window is monitored no matter which particular widget is passed to the monitor(QWidget*) method. You only need to call the monitor method for one widget in the window you intend to monitor - any widget will do. The changes are sent out as a QImage of window contents.

The implementation installs itself as an event filter in the target window widget and all of its children, and monitors the repaint events. It attempts to coalesce the repaint notifications by using the zero-length timer. The additions and removals of children are tracked automagically.

When you run the example, it creates two windows: a source window, and a destination window. They may be overlapped so you need to separate them. As you resize the source window, the size of the destination's rendition of it will also change appropriately. Any changes to the source children (time label, button state) propagate automatically to the destination.

In your application, the destination could be an object that takes the QImage contents, converts them to grayscale, resizes appropriately, and passes them to your device.

I do not quite understand how your laser device works if it can't gracefully handle updates. I presume that it is a raster-scanning laser that runs continuously in a loop that looks roughly like this:

while (1) {
  for (line = 0; line < nLines; ++line) {
    drawLine();
  }
}

You need to modify this loop so that it works as follows:

newImage = true;
QImage localImage;
while (1) {
  if (newImage) localImage = newImage;
  for (line = 0; line < localImage.height(); ++line) {
    drawLine(line, localImage);
  }
}

You'd be flipping the newImage flag from the notification slot connected to the WidgetMonitor. You may well find out that leveraging QImage, and Qt's functionality in general, in your device driver code, will make it much easier to develop. Qt provides portable timers, threads, collections, etc. I presume that your "driver" is completely userspace, and communicates via a serial port or ethernet to the micro controller that actually controls the laser device.

If you will be writing a kernel driver for the laser device, then the interface would be probably very similar, except that you end up writing the image bitmap to an open device handle.

// https://github.com/KubaO/stackoverflown/tree/master/questions/surface-20737882
#include <QtWidgets>
#include <array>

const char kFiltered[] = "WidgetMonitor_filtered";

class WidgetMonitor : public QObject {
   Q_OBJECT
   QVector<QPointer<QWidget>> m_awake;
   QBasicTimer m_timer;
   int m_counter = 0;
   void queue(QWidget *window) {
      Q_ASSERT(window && window->isWindow());
      if (!m_awake.contains(window)) m_awake << window;
      if (!m_timer.isActive()) m_timer.start(0, this);
   }
   void filter(QObject *obj) {
      if (obj->isWidgetType() && !obj->property(kFiltered).toBool()) {
         obj->installEventFilter(this);
         obj->setProperty(kFiltered, true);
      }
   }
   void unfilter(QObject *obj) {
      if (obj->isWidgetType() && obj->property(kFiltered).toBool()) {
         obj->removeEventFilter(this);
         obj->setProperty(kFiltered, false);
      }
   }
   bool eventFilter(QObject *obj, QEvent *ev) override {
      switch (ev->type()) {
         case QEvent::Paint: {
            if (!obj->isWidgetType()) break;
            if (auto *window = static_cast<QWidget *>(obj)->window()) queue(window);
            break;
         }
         case QEvent::ChildAdded: {
            auto *cev = static_cast<QChildEvent *>(ev);
            if (auto *child = qobject_cast<QWidget *>(cev->child())) monitor(child);
            break;
         }
         default:
            break;
      }
      return false;
   }
   void timerEvent(QTimerEvent *ev) override {
      if (ev->timerId() != m_timer.timerId()) return;
      qDebug() << "painting: " << m_counter++ << m_awake;
      for (auto w : m_awake)
         if (auto *img = dynamic_cast<QImage *>(w->backingStore()->paintDevice()))
            emit newContents(*img, w);
      m_awake.clear();
      m_timer.stop();
   }

  public:
   explicit WidgetMonitor(QObject *parent = nullptr) : QObject{parent} {}
   explicit WidgetMonitor(QWidget *w, QObject *parent = nullptr) : QObject{parent} {
      monitor(w);
   }
   Q_SLOT void monitor(QWidget *w) {
      w = w->window();
      if (!w) return;
      filter(w);
      for (auto *obj : w->findChildren<QWidget *>()) filter(obj);
      queue(w);
   }
   Q_SLOT void unMonitor(QWidget *w) {
      w = w->window();
      if (!w) return;
      unfilter(w);
      for (auto *obj : w->findChildren<QWidget *>()) unfilter(obj);
      m_awake.removeAll(w);
   }
   Q_SIGNAL void newContents(const QImage &, QWidget *w);
};

class TestWidget : public QWidget {
   QVBoxLayout m_layout{this};
   QLabel m_time;
   QBasicTimer m_timer;
   void timerEvent(QTimerEvent *ev) override {
      if (ev->timerId() != m_timer.timerId()) return;
      m_time.setText(QTime::currentTime().toString());
   }

  public:
   explicit TestWidget(QWidget *parent = nullptr) : QWidget{parent} {
      m_layout.addWidget(&m_time);
      m_layout.addWidget(new QLabel{"Static Label"});
      m_layout.addWidget(new QPushButton{"A Button"});
      m_timer.start(1000, this);
   }
};

int main(int argc, char **argv) {
   QApplication app{argc, argv};
   TestWidget src;
   QLabel dst;
   dst.setFrameShape(QFrame::Box);
   for (auto *w : std::array<QWidget *, 2>{&dst, &src}) {
      w->show();
      w->raise();
   }
   QMetaObject::invokeMethod(&dst, [&] { dst.move(src.frameGeometry().topRight()); },
                             Qt::QueuedConnection);

   WidgetMonitor mon(&src);
   src.setWindowTitle("Source");
   dst.setWindowTitle("Destination");
   QObject::connect(&mon, &WidgetMonitor::newContents, [&](const QImage &img) {
      dst.resize(img.size());
      dst.setPixmap(QPixmap::fromImage(img));
   });
   return app.exec();
}

#include "main.moc"


来源:https://stackoverflow.com/questions/20737882/how-to-monitor-changes-to-an-arbitrary-widget

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!