Russian Qt Forum

Qt => 2D и 3D графика => Тема начата: jasf от Ноябрь 05, 2009, 23:51



Название: Отрисовка поверх виджета
Отправлено: jasf от Ноябрь 05, 2009, 23:51
Здравствуйте. Хочется как-нибудь отрисоваться поверх виджета, при этом не вызывая функцию update() (repaint()) данного виджета. Т.е. вот у меня есть виджет с неким содержимым. Содержимое довольно тяжело прорисовывается. Но у меня есть координаты квадрата в этом виджете, который я хочу обновить.
Если отнаследоваться от этого виджета, переопределить paintEvent, сделать код типа
QMyWidget::needUpdateRect()
{
   redrawingRect = true;
   redraw();
   redrawingRect = false;
}
QMyWidget::paintEvent(QPaintEvent* event)
{
 if(redrawingRect) {
   redrawRect();
   return;
 }

 QTextEdit::paintEvent(event);
}

 - на некоторых платформах в QTextEdit начинаются артефакты  Если я всегда вызываю
 QTextEdit::paintEvent(event); в QTopMyWidget::paintEvent - никаких артефактов. Поэтому хотелось бы вообще обойти paintEvent. заранее спасибо за любые идеи.

P.S. этот способ с отнаследованием равносилен installEventFilter и перехват QEvent::Paint.
P.P.S. я думаю, что если бы я мог в начале функции needUpdateRect()  узнать, что виджет нуждается в перерисовке, и вызывать repaint().. артефакты бы прекратились. но мне неизвестен способ, как узнать, вызван ли для виджета update() и с какой областью (все эти данные расположены в d_func() ).


Название: Re: Отрисовка поверх виджета
Отправлено: lit-uriy от Ноябрь 06, 2009, 00:22
Рисование поверх дочерних виджетов (http://www.wiki.crossplatform.ru/index.php/Рисование_поверх_дочерних_виджетов) подойдёт?

Gif-рисунки наложенные на окно:
Мужик (http://www.crossplatform.ru/node/887) и его подружка (http://www.forum.crossplatform.ru/index.php?showtopic=2839&view=findpost&p=25898)


Название: Re: Отрисовка поверх виджета
Отправлено: jasf от Ноябрь 06, 2009, 01:12
Вот как раз-таки нет :( оно делает:
QApplication::sendEvent(obj, event); //заставляем ребенка себя нарисовать
а в QTextEdit без вызова paintEvent начинаются артефакты.. а вызывать его всегда, когда нужно обновить rect - слишком накладко.


Название: Re: Отрисовка поверх виджета
Отправлено: jasf от Ноябрь 06, 2009, 01:46
Я СЧАСТЛИВ!!!!  :D :D :D Проблема решена.
Ещё раз постановлю задачу: мне нужно отрисовывать нечто (например кадр gif файла) поверх QTextEdit. Для максимально-быстрого отображения при отрисовке не следует вызывать QTextEdit::paintEvent, а только лишь обновить фреймы гифок поверх отрисованного виджета. Но может такое случиться, что при вызове таймера обновления фрейма gifа - QTextEdit так же может нуждаться в перерисовке (ввод символа генерирует update(), но timerEvent получит первенство ), и т.к. при отображении фрейма .gifа QTextEdit::paintEvent не вызывается (для оптимизации) - как следствие получаем артефакт (QTextEdit должным образом не отрисовался).

проблема решается следующим способом:

class QWidgetBackingStorePublic
{
public:
    QWidget *tlw;
    QRegion dirtyOnScreen; // needsFlush
    QRegion dirty; // needsRepaint
    QRegion dirtyFromPreviousSync;
    QVector<QWidget *> dirtyWidgets;
    QVector<QWidget *> *dirtyOnScreenWidgets;
    QList<QWidget *> staticWidgets;
    QWindowSurface *windowSurface;
};


void QMyTextEdit::timerEvent(QTimerEvent* event) // частый таймер, в котором происходит отрисовка rect()ов на поверхность виджета
{
   QMyTextEdit* ppp = (QMyTextEdit*)viewport()->window(); // через window() можно получить доступ к QWidgetBackingStore
   void** pp = ((void**)&ppp->d_ptr); // получаем доступ к protected переменной QWidget
   QWidgetPrivate* p = (QWidgetPrivate*)*pp; /

   QTLWExtra* extra = p->maybeTopData();
   if(extra) {
      QWidgetBackingStorePublic* bs = (QWidgetBackingStorePublic*) extra->backingStore; // что-бы иметь доступ к private переменным класса QWidgetBackingStore
      if(bs) {
         if(bs->dirtyWidgets.contains(viewport())) { // в список dirtyWidgets помещаются указатели на виджеты, которые нуждаются в перерисовке. тем самым я узнаю, нужно ли перерисовать виджет, и если необходимо - перерисовываю.
            viewport()->repaint();
         }
      }
   }

   drawingFromTimer_ = true;
   viewport()->repaint();
   drawingFromTimer_ = false;
}

void QMyTextEdit::paintEvent(QPaintEvent *event)
{
   QPainter painter(viewport());
   
    if(drawingFromTimer_) {
       drawAnimatedIcons(painter);
       return;
    }
        QTextEdit::paintEvent(event);
}


Название: Re: Отрисовка поверх виджета
Отправлено: SABROG от Ноябрь 06, 2009, 10:02
А можно подробней как это работает? Судя по коду весь этот хак нужен, чтобы получить доступ к backingStore, затем идет проверка на наличие в списке виджета и вызывается насильная перерисовка вьюпорта. А каким образом тогда идет отрисовка поверх виджета, часть кода отсутствует?


Название: Re: Отрисовка поверх виджета
Отправлено: jasf от Ноябрь 06, 2009, 12:42
нет нет. на viewport() внимание обращать не нужно. Пока я копал, обнаружил, что отрисовка текста (QTextObject) происходит как раз не в QTextEdit, а в QTextEdit::viewport(); т.е. хоть я и работаю с QTextEdit, но любую прорисовку нужно проводить в viewport(). видимо это особенность прорисовки в классах, отнаследованных от QAbstractScrollArea.

А код нужен для следющего:
я хочу отрисовываться поверх виджета по таймеру. Для этого есть код

void QMyTextEdit::timerEvent(QTimerEvent* event)
{
....
   drawingFromTimer_ = true;
   viewport()->repaint();
   drawingFromTimer_ = false;
}

void QMyTextEdit::paintEvent(QPaintEvent *event)
{
   QPainter painter(viewport());
  
    if(drawingFromTimer_) {
       drawAnimatedIcons(painter);
       return;
    }
}

в этом коде мы видим, что при отрисовке через таймер не вызывается QTextEdit::paintEvent(); Тем самым мы получаем чистый доступ к содержимому QTextEdit (ну нужно ещё установить viewport()->setAttrebute( Qt::OpaquePainterEvent), viewport()->setautoFillBackground(false); тогда уж точно ничего перерисовываться при отрисовке из таймера не будет.)

но, если в момент захода в функцию void QMyTextEdit::timerEvent(QTimerEvent* event)  QTextEdit нуждается в перерисовке, без проверки мы бы просто её (прорисовку) не произвели (при отрисовке из таймера QTextEdit::paintEvent не вызывается, но очищаются флаги, ответственные за информирование движка о надобности перерисовки).

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

прорисовка поверх виджета осуществляется посредством невызова QTextEdit::paintEvent в QMyTextEdit::paintEvent. :)


Название: Re: Отрисовка поверх виджета
Отправлено: SABROG от Ноябрь 06, 2009, 14:20
А можно компилябельный пример?


Название: Re: Отрисовка поверх виджета
Отправлено: jasf от Ноябрь 06, 2009, 15:12
Ну раз пример.. вот пример на основе QLabel. Правда здесь практически невозможно показать артефакт, о котором я говорил, (который по сути не возникает даже в QTextEdit под виндовс), но суть переносит.


#include <QtGui/QApplication>
#include <QObject>
#include <QLabel>
#include <QPainter>
#include <QEvent>
#include <..\src\gui\kernel\qwidget_p.h>
#include <..\src\gui\kernel\qevent.h>


class QWidgetBackingStorePublic
{
public:
   QWidget *tlw;
   QRegion dirtyOnScreen; // needsFlush
   QRegion dirty; // needsRepaint
   QRegion dirtyFromPreviousSync;
   QVector<QWidget *> dirtyWidgets;
   QVector<QWidget *> *dirtyOnScreenWidgets;
   QList<QWidget *> staticWidgets;
   QWindowSurface *windowSurface;
};

class QGetDPtr : public QObject
{
   Q_OBJECT;
public:
   void** getD_PtrAddr() { return(void**)&d_ptr; }
};

bool needUpdate(QWidget* widget)
{
   if(!widget) return false;
   QGetDPtr* ppp = (QGetDPtr*)widget->window();
   void** pp = ppp->getD_PtrAddr();
   QWidgetPrivate* p = (QWidgetPrivate*)*pp;

   QTLWExtra* extra = p->maybeTopData();
   if(extra) {
      QWidgetBackingStorePublic* bs = (QWidgetBackingStorePublic*) extra->backingStore;
      if(bs) {
         if(bs->dirtyWidgets.contains(widget)) {
            return true;
         }
      }
   }
   return false;
}

class QDrawer : public QObject
{
   Q_OBJECT;
public:
   QDrawer(QWidget* widget):QObject(0),widget_(widget)
   { widget_->installEventFilter(this); startTimer(50); drawOnWidget_ = false; counter = 0; }
   ~QDrawer(){}

public:
   virtual bool eventFilter(QObject *object, QEvent *event);
   virtual void timerEvent(QTimerEvent *e);
   void drawOnWidget();

private:
   QWidget*   widget_;
   bool      drawOnWidget_;
   int         counter;
};

void QDrawer::timerEvent(QTimerEvent *e)
{
   counter++;
   if(needUpdate(widget_)) {
      widget_->repaint();
   }
   drawOnWidget_ = true;


   bool isOpaquePainterEvent = widget_->testAttribute(Qt::WA_OpaquePaintEvent);
   bool isAutoFillBackground = widget_->autoFillBackground();
   widget_->setAttribute(Qt::WA_OpaquePaintEvent);
   widget_->setAutoFillBackground(false);
   widget_->repaint();
   if(!isOpaquePainterEvent)widget_->setAttribute(Qt::WA_OpaquePaintEvent,false);
   if(isAutoFillBackground)widget_->setAutoFillBackground(true);

   drawOnWidget_ = false;
}

bool QDrawer::eventFilter(QObject *object, QEvent *event)
{
   if(event->type()==QEvent::Paint) {
      if(drawOnWidget_) {
         drawOnWidget();
         return true;
      }
   }
   return QObject::eventFilter(object,event);
}


void QDrawer::drawOnWidget()
{
   QPainter painter(widget_);
   while(counter>=255/8)counter-=255/8;
   painter.fillRect( QRect(50,50,50,50), QColor(255,0+counter*8,0+counter* 8 ) );
}

int main(int argc, char *argv[])
{
   QApplication a(argc, argv);
   QLabel label("Hello qt!");
   label.resize(240,320);
   label.show();

   QDrawer handler(&label);

   return a.exec();
}

#include "main.moc"

QDrawer::drawOnWidget - сюда по таймеру попадает управление для отрисовки квадратика. Если в этот же момент QLabel будет нуждаться в прорисовке,  код
   if(needUpdate(widget_)) {
      widget_->repaint();
   }
выполнит это.

[edit] - Убрал QMyLabel


Название: Re: Отрисовка поверх виджета
Отправлено: SABROG от Ноябрь 06, 2009, 20:35
Поправь меня если я не прав. Таймер с периодичностью 20 раз в секунду сканирует приватный список виджетов, которые требуют перерисовки, при этом срабатывает таймер быстрей, чем приходит эвент QEvent::Paint. Вероятно потому, что никто не инициирует это событие, а виджеты сами не перерисовываются. Т.е. тут либо пользователь должен вызывать сам repaint(), либо это событие должна генерить ОС. Получается в последнем случае таймер может сработать, когда ОС уже спровоцировала обновление, но это не страшно потому, что отсутствие фрейма при 20 кадрах в секунду может быть не заметно, т.к. следующий же фрейм затрет его, если ОС не будет "издеваться" над окном постоянно его перерисовывая раньше таймера. При изменении размера главного окна этот недостаток виден.

Далее, чтобы изображение не затерлось при перерисовке устанавливается аттрибут Qt::WA_OpaquePaintEvent. Это позволяет нарисовать на готовом изображении что-то другое. Честно сказать я удивлен, что тебе удалось заставить работать этот аттрибут. Как я ни пытался его заставить работать раньше в моих первых опытах по оверпеинтингу, ничего не выходило.

Соответственно widget_->repaint(); тут играет роль QApplication::sendEvent(), т.к. пораждает QEvent::Paint, который мы удачно ловим в eventFilter и рисуем на не затертом виджете уже что-то своё.

Я правильно понял?

И главный вопрос. Возможно ли нарисовать этим методом что-то на родительском виджете таким образом, чтобы картинка перекрывала все дочерние виджеты?

Судя по всему нужно еще допиливать:

(http://img514.imageshack.us/img514/6513/over2.gif)


Название: Re: Отрисовка поверх виджета
Отправлено: jasf от Ноябрь 06, 2009, 21:55
Qt::WA_OpaquePaintEvent - я с ним познакомился ещё в первые дни изучения Qt :) работает без проблем.

Поповоду
  if(needUpdate(widget_)) {
      widget_->repaint();
   }
- впринципе этот код можно удалить! т.к. он срабатывает крайне редко. Нельзя сказать, что я в таймере сканирую приватный список виджетов, т.к. это делается для исключения артефактов, а не для корректной работы отрисовки поверх виджета.

"Вероятно потому, что никто не инициирует это событие, а виджеты сами не перерисовываются." - нет. Постараюсь обьяснить. Рассмотрим код без if(needUpdate(widget_)){...

Код:
void QDrawer::timerEvent(QTimerEvent *e)
{
   counter++;

   drawOnWidget_ = true; /// очень важная переменная


   bool isOpaquePainterEvent = widget_->testAttribute(Qt::WA_OpaquePaintEvent);
   bool isAutoFillBackground = widget_->autoFillBackground();
   widget_->setAttribute(Qt::WA_OpaquePaintEvent);
   widget_->setAutoFillBackground(false);
   widget_->repaint();
   if(!isOpaquePainterEvent)widget_->setAttribute(Qt::WA_OpaquePaintEvent,false);
   if(isAutoFillBackground)widget_->setAutoFillBackground(true);

   drawOnWidget_ = false;   /// очень важная переменная
}

 К примеру обработка нажатия клавиши Enter в QTextEdit ведёт к вызову функции update(), которая помечает виджет как dirty. Для наглядности представим QTextEdit размерами 1280х1024 и весь заполненный текстом. Курсор вверху. Так же Enter смещает все строки на одну вниз, соответственно меняется содержание почти всего виджета. Но что будет, если сразу после нажатия на энтер сработает таймер (до того, как разошлётся эвент QEvent::Paint)? виджет нуждается в перерисовке, но прорисовка из таймера не вызывает QTextEdit::paintEvent(). Обработка таймера устанавливает аттрибуты, устанавливает drawOnWidget_ = true; , что говорит eventFilterу, что нужно просто отрисовать маленький (50х50) квадратик, и сделать return true; Если drawOnWidget_ == false, тогда не происходит ничего, а эвент рисования просто передаётся дальше. Переменная drawOnWidget_ разделяет просто прорисовку от прорисовки поверх виджета. Т.е. только что мы получили, что при нажатии на Энтер у нас никакие строки не сьехали вниз! И тем самым получился артефакт. Данный артефакт и решается проверкой if(needUpdate(widget_))...

"И главный вопрос. Возможно ли нарисовать этим методом что-то на родительском виджете таким образом, чтобы картинка перекрывала все дочерние виджеты?" - да думаю можно. где-то встречал флаг, говорящий painterу, что он может рисовать поверх всего и вся (разграничить заклиппинность региона).

для твоего применения нужно допиливать. У меня же применение ограничивается лишь одним виджетом (а не окном :) ). Может что-нибудь и найду.



Название: Re: Отрисовка поверх виджета [решено]
Отправлено: jasf от Ноябрь 06, 2009, 22:36
Ну вот к примеру рисовать совсем на экран можно вот так:

QPaintDevice* device = bs->windowSurface->paintDevice(); // bs - QWidgetBackingStore
if(device) {
   QPainter painter(device);
   painter.fillRect(QRect(0,0,30,30),Qt::yellow);
}
bs->windowSurface->flush(window(),QRegion(rect),QPoint(0,0));

но помоиму это неочень элегантный способ решения проблем. и любой QEvent::Paint может перерисовать нарисованный данным образом контент. Правда их можно перехватить :)


Название: Re: Отрисовка поверх виджета
Отправлено: Igors от Ноябрь 06, 2009, 23:43
С интересом прочитал этот топик. С креативностью все нормально  :) Но, на мой взгляд, это слишком сложно и тонко, а "где тонко - там и рвется". Как насчет более "дубовой" реализации, пусть медленной но проще и универсальнее:

- виджет имеет свойство "прозрачность", его метод paint перекрыт - и только
- в перекрытом paint виджет сначала рендерит в QPixmap все что под ним и затем рисует себя, используя созданную QPixmap

Z-порядок известен, нужно добраться до MainWindow (через parent), взять список его children и рисовать в QPixmap каждого кто пересекается с нашим. Разумеется, это экспериментальная идея  :) 


Название: Re: Отрисовка поверх виджета
Отправлено: jasf от Ноябрь 06, 2009, 23:54
2Igors: Не могу согласиться. Вот как раз моя реализация очень даже не тонкая. Там не используется прорисовка непосредственно на экран. Используется прорисовка в widget. При этом widget один. Нет необходимости отслеживать родителей или потомков. И всё очень даже чётко работает :)
А более медленно, но совсём чётко - нет. 50 аним. обьектов в QTextEdit, вызывающие по 20раз в секунду перерисовку этого виджета - заставляют его тормазить даже под windows., а 50 отображений этих обьектов поверх виджета, не затрагивая QTextEdit::paintEvent - выдают и 100 фпс.


Название: Re: Отрисовка поверх виджета
Отправлено: SABROG от Ноябрь 07, 2009, 11:55
Через windowSurface действительно рисует, но точно также как и обычным образом, то есть картинку перекрывают дочерние виджеты.


Название: Re: Отрисовка поверх виджета
Отправлено: jasf от Ноябрь 07, 2009, 12:11
2SABROG: вот точно нет. У меня есть окно, в нём виджет, в нём лайоут, в нём верхний и нижний виджет. Так вот через сурфейс я спокойно отрисовался на весь экран, перекрыв все виджеты. Да и тут логика немного другая. Мы получаем указатель на window() - самый нижний виджет (к которому присвоено окно). Поэтому тут логики widgetов впринципе нет. Всё ограничено лишь окном.
и я видимо понял в чём проблема. изиняюсь. моя ошибка :)

bs->windowSurface->flush(window(),QRegion(window()->rect()),QPoint(0,0));

нужно ведь обновить содержимое QPaintDevice окна на экран по всему контуру окна, а не только по ректанглу виджета.


Название: Re: Отрисовка поверх виджета
Отправлено: SABROG от Ноябрь 07, 2009, 12:15
изиняюсь. моя ошибка :)

Код
C++ (Qt)
bs->windowSurface->flush(window(),QRegion(window()->rect()),QPoint(0,0));

Не ну проблему с компиляцией я точно также решил еще вчера, но у меня квадратик за кнопочкой рисуется, причем в некоторых редких случаях он все таки поверх рисуется. То ли я чего-то не убрал, то ли чего-то не добавил...

Хотя код дома остался, а я подзабыл что использовал widget->rect() или window()->rect()...


Название: Re: Отрисовка поверх виджета
Отправлено: jasf от Ноябрь 07, 2009, 13:42
Да нет.. у меня нормально рисуется. но, естесственно, как только виджет перерисуется - прорисовка через windowSurface будет "потёрта" контентом кнопочки. Потом смогу скинуть исходный код.


Название: Re: Отрисовка поверх виджета
Отправлено: igor_bogomolov от Ноябрь 07, 2009, 15:39
У меня же применение ограничивается лишь одним виджетом (а не окном :) ).

Всё таки ваше решение мне кажется слишком навороченным. Тем более, что рисовать нужно поверх одного элемента. Ещё один пример использования (http://www.forum.crossplatform.ru/index.php?s=&showtopic=3462&view=findpost&p=24503), предлагаемого выше примера.

Если же для вас дорого перерисовывать сам виджет,  поверх которого  рисуете, лучше разместить поверх него прозрачный виджет, на котором и рисовать. ИМХО.


Название: Re: Отрисовка поверх виджета
Отправлено: jasf от Ноябрь 07, 2009, 18:22
Если же для вас дорого перерисовывать сам виджет,  поверх которого  рисуете, лучше разместить поверх него прозрачный виджет, на котором и рисовать. ИМХО.

Дорого. Поэтому я и поднял тему :) В вашем примере ничего нового не нашёл

Код:
bool MyLabel::eventFilter(QObject *obj, QEvent *event)
{
    if( (event->type() == QEvent::Paint) && (obj == this) ) {
        removeEventFilter(this);
        QApplication::sendEvent(obj, event);
        installEventFilter(this);

        QPainter p(qobject_cast<QWidget*>(obj));
        p.drawImage(rect().center() - movie->frameRect().bottomRight(), image);
        return true;
    }
    return false;
}

т.е. опять же отрисовка содержимого виджета, а потом отрисовка поверх.

А Ваше предложение трудновыполнимо. У виджета может быть флаг Qt::WA_OpaquePaintEvent. Значит у нашего прозрачного виджета этот флаг не должен быть установлен по определению (т.к. если установлен данный флаг, все виджеты, находящиеся rect'ально под виджетом с этим флагом, не получают paintEvent ). А если мы будем отрисовываться на прозрачный виджет без этого флага, QEvent::Paint будет перенаправляться на низлижащие виджеты (для восстановления прозрачности), что по сути только утяжеляет прорисовку (из Вашего примера мы лишь отлавливаем эвенты.. а так ещё создаётся один лишний виджет). :)