Russian Qt Forum

Qt => Пользовательский интерфейс (GUI) => Тема начата: kuzulis от Февраль 13, 2017, 22:01



Название: Граббить содержимое виджета по его изменению.
Отправлено: kuzulis от Февраль 13, 2017, 22:01
Всем доброго времени.

Допустим, есть некий тестовый виждет (чисто для примера), который имеет кнопочку
и на котором периодически по таймеру с периодом в 1 секунду рисуется некий текст.

Стоит задача поймать момент перерисовки этого виджета, сграббить все его содержимое
в пиксмапу и отправить на другой целевой виджет.

Вот простой примерчик:

Код
C++ (Qt)
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QPixmap>
#include <QTimer>
#include <QPainter>
 
class Widget : public QWidget
{
   Q_OBJECT
public:
   explicit Widget(QWidget *parent = 0)
       : QWidget(parent)
       , m_target(new QLabel)
       , m_timer(new QTimer(this))
   {
       m_target->show();
 
       auto pb = new QPushButton(tr("Hello"), this);
       Q_UNUSED(pb);
 
       connect(m_timer, &QTimer::timeout, [this]() {
           m_text = QString("%1").arg(qrand());
           update(); });
       m_timer->start(1000);
   }
 
   void paintEvent(QPaintEvent *event)
   {
       Q_UNUSED(event);
 
       {
           // Рисуем сначала все в нашу пиксмапу.
           QPainter p(&m_pixmap);
           p.drawText(rect(), Qt::AlignBottom, m_text);
       }
 
       // Дублируем пиксмапу на другом целевом виджете.
       m_target->setPixmap(m_pixmap);
 
       {
           // Потом рисуем пиксмапу на нашем главном виджете.
           QPainter p(this);
           p.drawPixmap(m_pixmap.rect(), m_pixmap);
       }
 
       // Потом очищаем пиксмапу.
       m_pixmap = QPixmap(size());
   }
 
   void resizeEvent(QResizeEvent *event)
   {
       QWidget::resizeEvent(event);
       m_pixmap = QPixmap(size());
   }
 
private:
   QLabel *m_target;
   QTimer *m_timer;
   QPixmap m_pixmap;
   QString m_text;
};
 
int main(int argc, char *argv[])
{
   QApplication a(argc, argv);
   Widget w;
   w.show();
   return a.exec();
}
 
#include "main.moc"
 

Проблема в том, что не получается получить пиксмапу кнопки, т.к. она не рисуется в этом виджете в paintEvent.
Если вызывать методы типа grab(), или render() внутри paintEvent(), то оно ругается на рекурсию.

Собственно вопрос: как сграббить все содержимое виждета только по его изменениям, без всякого
там поллинга и прочего? Облазил все что возможно, но так и не нашел вменяемого решения.


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: Racheengel от Февраль 13, 2017, 22:06
поскольку перерисовка может вызваться и без изменения контента, то только поллить...


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: twp от Февраль 13, 2017, 22:24
Как вариант, чтоб получить пиксмап, можно использовать QScreen::grabWindow(). В отличие от QWidget::grab() и QWidget::render() там используются системные вызова, а не QPainter.


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: kuzulis от Февраль 13, 2017, 22:44
Цитата: Racheengel
поскольку перерисовка может вызваться и без изменения контента, то только поллить...

Не, такой вариант не годится.

Цитата: twp
Как вариант, чтоб получить пиксмап, можно использовать QScreen::grabWindow().

Хм, да, спссибо. Но оно имеет недостатки:

1. Слишком медленно. Например, при размере ректа 1024х768 граббинг у
меня занимает ~13-14 миллисекунд, а на 1920х1080 - ~20 миллисекунд,
что при частоте обновления ~20 ФПС дает заметные подлагивания
(см. пример ниже).

UPD: Хотя, если упираемся в 20 мс, то это теоретически даст ~50 FPS..
Что вроде-как приемлемо.

2. Оно не поддерживается на iOS, судя по доке (http://doc.qt.io/qt-5/qscreen.html#grabWindow).
Что уже является весьма опасным.

3. Не граббит, если виджет скрыт.

Код
C++ (Qt)
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QPixmap>
#include <QTimer>
#include <QPainter>
#include <QScreen>
#include <QElapsedTimer>
#include <QDebug>
 
class Widget : public QWidget
{
   Q_OBJECT
public:
   explicit Widget(QWidget *parent = 0)
       : QWidget(parent)
       , m_target(new QLabel)
       , m_timer(new QTimer(this))
       , m_screen(QGuiApplication::primaryScreen())
   {
 
       m_target->show();
 
       auto pb = new QPushButton(tr("Hello"), this);
       Q_UNUSED(pb);
 
       connect(m_timer, &QTimer::timeout, [this]() {
           m_text = QString("%1").arg(qrand());
           update(); });
       m_timer->start(50); // ~20 FPS
   }
 
   void paintEvent(QPaintEvent *event)
   {
       Q_UNUSED(event);
 
       QPainter p(this);
       p.drawText(rect(), Qt::AlignBottom, m_text);
 
       QElapsedTimer et;
       et.start();
       const auto pixmap = m_screen->grabWindow(winId());
       qDebug() << "elapsed:" << et.elapsed();
 
       m_target->setPixmap(pixmap);
   }
 
private:
   QLabel *m_target;
   QTimer *m_timer;
   QScreen *m_screen;
   QString m_text;
};
 
int main(int argc, char *argv[])
{
   QApplication a(argc, argv);
   Widget w;
   w.resize(1920, 1080);
   w.show();
   return a.exec();
}
 
#include "main.moc"
 

Есть еще какие-нибудь варианты?


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: twp от Февраль 13, 2017, 23:15
А кнопка видима? Если так, то надо скрыть и тогда можно спокойно граббить. Но понятно что кнопку нельзя схайдить - ибо на нее надо кликать. А вот с QLabel проблем не должно быть.


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: kuzulis от Февраль 13, 2017, 23:19
Эмм... Не понял.. Да, кнопка видима. Она должна также граббится. Т.е. при наведении на нее,
при клике по ней, все изменения ее состояния также должны отображаться и на целевом виджете.


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: twp от Февраль 13, 2017, 23:22
Да, я вверху подправил свой текст. Просто такой подход я видел, но там используется QLabel, которая скрыта. Но вот с кнопкой очевидно уже так не получится.


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: twp от Февраль 13, 2017, 23:35
Я более внимательно глянул код, и первое что пришло в голову - почему бы не перенести граббинг и установку в целевой виджет за пределы paintEvent? например в самом конце paintEvent поставить QTimer::singleShot(0, [=] {....});


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: kuzulis от Февраль 14, 2017, 00:19
В таком случае оно срабатывает вечно:

Код
C++ (Qt)
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QPixmap>
#include <QTimer>
#include <QPainter>
#include <QPaintEvent>
#include <QScreen>
#include <QElapsedTimer>
#include <QDebug>
 
class Widget : public QWidget
{
   Q_OBJECT
public:
   explicit Widget(QWidget *parent = 0)
       : QWidget(parent)
       , m_target(new QLabel)
       , m_timer(new QTimer(this))
       , m_screen(QGuiApplication::primaryScreen())
   {
       //setAttribute(Qt::WA_DontShowOnScreen);
 
       m_target->show();
 
       auto pb = new QPushButton(tr("Hello"), this);
       Q_UNUSED(pb);
 
       connect(m_timer, &QTimer::timeout, [this]() {
           //m_text = QString("%1").arg(qrand());
           //update();
       });
       m_timer->start(5000); // ~20 FPS
   }
 
   void paintEvent(QPaintEvent *event)
   {
       Q_UNUSED(event);
 
       QPainter p(this);
       p.drawText(rect(), Qt::AlignBottom, m_text);
 
       QTimer::singleShot(0, [=](){
           QElapsedTimer et;
           et.start();
           QPixmap pixmap = grab();
           qDebug() << "elapsed:" << et.elapsed();
           m_target->setPixmap(pixmap);
       });
   }
 
private:
   QLabel *m_target;
   QTimer *m_timer;
   QScreen *m_screen;
   QString m_text;
};
 
int main(int argc, char *argv[])
{
   QApplication a(argc, argv);
   Widget w;
   w.resize(800, 600);
   w.show();
   return a.exec();
}
#include "main.moc"
 

Таймер singleShot запускает grab(), который вызывает repaint() который вызывает paintEvent(?) который запускает таймер singleShot.

Проблема в том, блин, что grab(), что render() вызывают paintEvent()... вот если бы как-то заблокировать рекурсию..


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: twp от Февраль 14, 2017, 01:07
Ну да, тут только граббить через QScreen::grabWindow(). Главное отрисовка будет происходить быстро, а тормознутый вызов QScreen::grabWindow() вынести за ее пределы. Если конечно работа в iOS не предвидится. Вообще, что-то уж очень закручено получается, может стоит пересмотреть саму архитектуру приложения.


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: twp от Февраль 14, 2017, 01:29
Но если все таки хочется использовать grab(), то очевидно надо это делать по событию таймера и фактически отрисовка будет происходить дважды: самого виджета и в пиксмап:
Код
C++ (Qt)
connect(m_timer, &QTimer::timeout, [this]() {
   m_text = QString("%1").arg(qrand());
   update();
   QPixmap pixmap = grab();
   m_target->setPixmap(pixmap);
});
 


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: kuzulis от Февраль 14, 2017, 09:30
Цитировать
а тормознутый вызов QScreen::grabWindow() вынести за ее пределы. Если конечно работа в iOS не предвидится.

Не, QScreen::grabWindow() не вариант также, т.к. не работает для скрытого виджета или у виджета с аттрибутами Qt::WA_DontShowOnScreen.
Мне нужно чтобы работало у таких виджетов (я не дописал это в начале).

Цитировать
Вообще, что-то уж очень закручено получается, может стоит пересмотреть саму архитектуру приложения.

Куда тут еще что пересматривать? Нужно отправлять/дублировать/перенаправлять новые фреймы из текущего виджета в нужный целевой виджет.. всЁ.

Единственный "вменяемый" вариант - это все самому рисовать в paintEvent(), включая кнопочки и прочее, например в пиксмапу, которую потом
отрисовывать в виджет. Тогда не нужен grab() вообще и достаточно просто эту пиксмапу также рисовать на целевом виджете..
Блин, но это все гимор какой-то..  :-\


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: GreatSnake от Февраль 14, 2017, 11:42
Проблема в том, блин, что grab(), что render() вызывают paintEvent()... вот если бы как-то заблокировать рекурсию..
Попробуй запускать таймер только в случае, когда на виджете не выставлен атрибут Qt::WA_WState_InPaintEvent.


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: Igors от Февраль 14, 2017, 12:01
Проблема в том, что не получается получить пиксмапу кнопки, т.к. она не рисуется в этом виджете в paintEvent.
Если вызывать методы типа grab(), или render() внутри paintEvent(), то оно ругается на рекурсию.
1) В коде рисования послать сигнал с QueuedConnection, а в нем уже вызвать grab() или render()
2) Отловить QEvent::UpdateRequest, после его выполнения должен быть готов буфер QImage, содрать оттуда


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: kuzulis от Февраль 14, 2017, 12:17
Цитата: GreatSnake
Попробуй запускать таймер только в случае, когда на виджете не выставлен атрибут Qt::WA_WState_InPaintEvent.

Имхо, не выход все-равно, т.к. рекурсия будет все-равно.

Цитата: Igors
1) В коде рисования послать сигнал с QueuedConnection, а в нем уже вызвать grab() или render()

А сами то пробовали?

Цитата: Igors
2) Отловить QEvent::UpdateRequest, после его выполнения должен быть готов буфер QImage, содрать оттуда

Каким же образом содрать?


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: GreatSnake от Февраль 14, 2017, 13:10
Да, с атрибутом я погорячился.
Достаточно, как посоветовал Igors всего-лишь поймать UpdateRequest:
Код
C++ (Qt)
bool event( QEvent* e )
{
if( e->type() == QEvent::UpdateRequest )
{
QElapsedTimer et;
et.start();
m_target->setPixmap(grab());
qDebug() << "elapsed:" << et.elapsed();
}
return QWidget::event( e );
}
 
Т.е. никакие таймеры и сигналы не нужны.


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: Igors от Февраль 14, 2017, 13:57
А сами то пробовали?
А что Вас смущает в этом стандартном/банальном приеме?

Каким же образом содрать?
В Qt 5 вроде можно и так
Код
C++ (Qt)
QImage * GetImage( QWidget * widget )
{
QBackingStore * store = widget->backingStore();
Q_ASSERT(store);
 
QPaintDevice * pdev = store->paintDevice();
return dynamic_cast<QImage *> (pdev);
}
 
У меня то же самое но через приватные хедеры (для более ранних Qt), этот код рабочий


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: kuzulis от Февраль 14, 2017, 14:55
Всем спасибо (я пока не пробовал то что вы предложили),

остановился пока на своем варианте с прорисовкой всех контролов вручную через
стили (благо там у меня всего две кнопочки надо отрисовать).. :)

Сначала рисую в пиксмапу все что нужно, а потом рисую из этой пиксмапы
в виджет, а также передаю эту пиксмапу в целевой виджет.


 


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: kuzulis от Февраль 14, 2017, 15:32
UPD: Попробовал с QEvent::UpdateRequest и всроде оно работает (по крайней мере под windows), спасибо!  :)

Но немного медленнее чем с "прямым" рисованием контролов ручками.


Название: Re: Граббить содержимое виджета по его изменению.
Отправлено: GreatSnake от Февраль 14, 2017, 16:57
Но немного медленнее чем с "прямым" рисованием контролов ручками.
Дык за всё нужно платить. Особенно за пиксмапы, которые хранятся на стороне граф.подсистемы.
Попробуй вместо QPixmap задействовать QPicture:
Код
C++ (Qt)
QPicture pic;
render( &pic );
m_target->setPicture( pic );