Рисование поверх дочерних виджетов

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск

Содержание

[править] Рисование поверх дочерних виджетов

Изображение:Stars.gif

[править] Теория

Бывают задачи, когда необходимо создать эффект падающего снега или при выводе модального диалога "приглушить" цвет у главного окна. По сути задача сводится к тому, чтобы дать виджетам себя отрисовать и поверх них нарисовать что-то своё. Казалось бы у Qt богатый выбор методов для рисования, но как выясняется все не так радужно:

  • Функционал Qt, представленный пользователям библиотеки, позволяет виджетам рисовать себя и только себя.
  • Нет возможности выбора рисовать перед тем как виджет себя нарисует или после этого
  • Qt очищает область виджета перед посылкой QPaintEvent'a независимо от того собираетесь ли вы рисовать что-то или нет
  • Невозможно изменить порядок рисования виджетов. Он всегда один и тот же - от родителя до последнего ребенка.

Для наших задач предлагаются следующие варианты: Widget Overlay и Fading Effects

Единое для обоих этих вариантов то, что создаётся дополнительный прозрачный виджет, который "прозрачен" для клавиатуры и кликов мышки, задача которого появляться поверх всех остальных виджетов. Рисование на таком виджете выглядит как рисование поверх остальных виджетов.

Меня не устроил такой вариант по идеологическим соображениям. Но могу сказать сразу, что это простейший вариант. В общем я отказался принять тот факт, что Qt со своими богатыми творческими возможностями не может выполнить простую задачу без использования костылей (к сожалению в дальнейшем без костылей тоже обойтись не удалось, но к правде мы стали ближе).

[править] Алгоритм

Руководствуясь тем, что Qt не позволяет рисовать поверх чужих виджетов нам остается только один вариант: переопределить paintEvent у всех дочерних виджетов и заставить рисовать каждый какую-то часть общей картинки. Первая неприятность с которой мы сталкиваемся - мы не можем переопределить paintEvent у дочерних составных виджетов, так как они создаются не нами, а библиотекой. К тому же это означает, что мы не сможем добавить наше решение в существующий проект без того, чтобы не обернуть в новый класс каждый из виджетов используемый в нем. Кроме того, это будет также означать, что нам придется отказаться от визуального дизайнера или, по крайней мере, сделать "Promote to..." для каждого из виджетов на форме.

Напрашивается другое решение - installEventFilter для каждого ребенка. Далее ловим событие Qt::Paint.

static void childsRecursive(QObject *object, QWidget *watcher, bool install)
{
    if (object->isWidgetType()) {
        if (install) object->installEventFilter(watcher);
        else object->removeEventFilter(watcher);
        QWidget *widget = qobject_cast<QWidget*>(object);
#if 0
//Тут надо как-то доработать, чтобы возвращать оригинальную настройку этого параметра при снятии фильтра
#endif
        widget->setAutoFillBackground(true);
    }
    QObjectList children = object->children();
    foreach(QObject *child, children) {
        childsRecursive(child, watcher, install);
    }
}

Тут мы сталкиваемся с еще одной проблемой. У нас есть 2 выбора:

  • полностью переопределить метод отрисовки виджета
  • дать виджету нарисовать себя

Но они нам не подходят, т.к. необходимо рисовать поверх виджета, а не рисовать его с нуля. Пришлось изрядно помучиться, прежде чем был найден трюк. Мы сгенерируем событие отрисовки еще раз, что заставит виджет нарисовать себя. Но, т.к. у нас идет перехват события, то мы должны как-то вызвать сначала оригинальный обработчик, а затем свой. В этом нам поможет QApplication::sendEvent:

bool form::eventFilter(QObject *obj, QEvent *event)
{
    if(event->type() == QEvent::Paint) {
        QWidget *widget = qobject_cast<QWidget*>(obj);
        widget->setAutoFillBackground(false);
//снимаем на время фильтр, чтобы при следующем вызове не пришло сообщение о перерисовки главного окна
        obj->removeEventFilter(this); 
        QApplication::sendEvent(obj, event); //заставляем ребенка себя нарисовать
        obj->installEventFilter(this);
        widget->setAutoFillBackground(true);
 
//начинаем рисовать поверх уже нарисованного ребенка. По сути этот код рисует некоторую часть общей картинки.
        QPoint point = widget->pos();
        while(widget && (widget->parentWidget() != this)) {
            widget = widget->parentWidget();
            point += widget->pos();
        }
 
        QRect r = qobject_cast<QWidget*>(obj)->rect().translated(point);
 
        QPainter p(qobject_cast<QWidget*>(obj));
        p.setWindow(r);
 
        QPixmap pix(size());
        if(!flag) {
            pix.load(QString("../png/%1.png").arg(count));
        } else {
            pix.fill(QColor(0,0,0,100));
        }
        p.drawPixmap(rect(), pix);
 
        return true;
    }
    return false;
}

Исходный код примера можно взять тут.

[править] Обсуждение

Обсудить на форуме...