Как стать автором
Обновить
VK
Технологии, которые объединяют

Blur Behind: История провалов на разных ОС

Время на прочтение21 мин
Количество просмотров12K


Привет, Хабр! Меня зовут Михаил Полукаров, я занимаюсь разработкой Desktop-версии корпоративного супераппа в команде VK Teams. Я уже писал на Хабр две статьи про использование масок, создание сложных многослойных и полупрозрачных окон и о своем опыте реализации красивых спецэффектов с использованием Qt Framework: вот первая и вторая. По просьбам читателей я решил закончить этот небольшой цикл статей описанием неудач, с которыми мне пришлось столкнуться при реализации спецэффекта Blur Behind для разных ОС.

Примечания


Примечание 1. Все, что приводится в статье, может работать, а может и не работать в зависимости от сочетания программного-аппаратного обеспечения, версии Qt Framework, версии и типа операционной системы и прочих факторов. Весь код, приведенный в статье, проверялся для Qt Framework версии 5.14.1 в ОС Windows 10, Ubuntu 20.1 LTS Linux и macOS Big Sur. Для упрощения понимания весь код реализации методов написан в классе, а в Production-коде настоятельно рекомендую этого избегать. Полный код примеров вы сможете найти на странице проекта в GitHub. Для сборки потребуются установленный Qt Framework и компилятор С++17.

Примечание 2. Все, что описывается в статье, вероятно, можно реализовать с использованием Qt QML или других языков программирования и фреймворков. Особо отмечу, что моя цель — показать, как использовать Qt Framework исключительно с применением классического QWidget-based-подхода.

Главное о Blur Behind


Blur Behind (или Back Drop) — это спецэффект, заключающийся в размытии фона под окном или его элементом.

Чтобы реализовать его для окна, нам в любом случае потребуется написать платформенно-зависимый код. Более того, этот эффект невозможно реализовать на некоторых платформах. Для подобного размытия требуется специальная композиция (регион под приложением, регион в самом окне, размытие и окончательная сборка этих частей), которая реализуется в самой ОС. В Linux, например, за это отвечает Composition Manager, в Windows — Desktop Window Manager (DWM). Для macOS такой эффект можно сделать штатным API, для Windows (начиная с Windows 8) — только используя недокументированные возможности, для Linux — только для рабочего стола KDE, для остальных (Xfce, GNOME и прочих) такое не поддерживается вовсе. Более подробное описание Blur Behind с использованием нативного API на разных платформах можно найти по ссылке.

VK Teams используют системообразующие компании, и поэтому использование недокументированных функций ОС невозможно, не говоря уже о неполной поддержке на всех платформах. Из-за этих факторов мы отказались использовать этот эффект на всем окне и сосредоточились на том, как можно использовать QGraphicsEffect для реализации этого эффекта между внутренними компонентами окна. Подробно этот подход я разобрал в предыдущей статье.

Но давайте попробуем отбросить все вышесказанное и реализовать этот эффект для всего окна. Для начала разберемся с Linux, поскольку в этой ОС решение задачи кажется наиболее трудным.

Попытка Blur Behind для Linux


Поскольку такой эффект поддерживает только KDE, сперва нам необходимо выяснить, какой именно оконный менеджер сейчас используется. Если KDE, то для окна будет необходимо выставить атом _KDE_NET_WM_BLUR_BEHIND_REGION, что несложно сделать несколькими системными вызовами. Если у нас другая оконная система, например GNOME, то придется делать композицию окон вручную.

UPD: пока готовилась эта статья, вышла новая версия рабочего стола — GNOME 4, где такой эффект поддерживается, однако подробное рассмотрение реализации выходит за рамки статьи.

Один из возможных путей решения этой задачи — через равные промежутки времени скрывать наше окно, считывать всю видеопамять монитора, вырезать участок окна, размывать его и затем снова показывать окно, отрисовав размытую часть. Однако неясно, на какой промежуток времени в таком случае выставить таймер. Если он будет срабатывать слишком часто, мы не успеем прочитать всю графическую память и попутно размыть изображение, поскольку эта процедура весьма трудоемкая. Если же интервал срабатывания таймера будет слишком большим, появится большое отставание между двумя тактами. Кроме того, такое решение очень сильно нагружает как процессор, так и подсистему видеопамяти, что делает его нерациональным.

Другой вариант, который приходит на ум, — фактически сделать свой небольшой локальный Composition Manager. Для этого будем хранить буфер в виде QPixmap размером в наше окно, а общий алгоритм создания такой композиции будет следующий:

  1. Необходимо запросить у X11 все видимые на экране окна и обойти их в порядке возрастания оси Z вплоть до нашего окна.
  2. Для каждого внешнего окна проверить, что идентификатор окна не равен нашему, иначе завершить цикл.
  3. Для каждого внешнего окна получить его геометрию и выяснить, пересекается ли она с нашим окном.
  4. В случае пересечения вычислить регион пересечения.
  5. Получить изображение внешнего окна в этом регионе.
  6. Отрисовать полученный участок в буфер в соответствии с координатами пересечения.

Далее при каждом такте отрисовки уже нашего окна в paintEvent() необходимо размыть полученный буфер и, наконец, отрисовать то, что получилось. 

Давайте попробуем реализовать эти идеи в коде. Для начала нам потребуются обертки над системными функциями работы с окнами:

struct WindowFunctions
{
    static WId leaderWindow(WId w);
    static bool isVisible(WId w);
    static QRect windowGeometry(WId w);
    static QImage windowImage(WId w, const QRect& rect);
    static QImage grabRootImage(const QRect& r = {});
};

Для удобства создадим также делегат обработки отдельного окна:

class ExternalWindowHandler : public WindowFunctions
{
public:
    virtual ~ExternalWindowHandler() {}
    virtual bool processWindow(WId w) = 0;
};

Поскольку мы предполагаем работу с X11, хорошим тоном является использование обертки Xcb. Очевидно, что оконных менеджеров не может быть больше одного, поэтому класс XcbWindowManager — синглтон, который маскирует способ получения и обхода внешних окон. При обходе для каждого окна вызывается делегат ExternalWindowHandler, позволяющий обработать отдельное внешнее окно. Поскольку ExternalWindowHandler наследуется от WindowFunctions, мы можем прозрачно работать с системными функциями оконного менеджера.

Теперь займемся реализацией класса XcbWindowManager, вот его объявление:

class XcbWindowManager
{
    Q_DISABLE_COPY(XcbWindowManager)
    XcbWindowManager();
public:
    struct cdeleter
    {
        void operator()(void* _ptr) const { free(_ptr); }
    };
    template<class T>
    using cmem_ptr = std::unique_ptr<T, cdeleter>;
    static XcbWindowManager& instance();
    xcb_connection_t* xcb() const;
    xcb_window_t leaderWindow(xcb_window_t _w);
    bool isVisible(xcb_window_t _w);
    QRect geometry(xcb_window_t _w) const;
    QImage image(xcb_window_t _w, const QRect& rect) const;
    void traverseWindows(ExternalWindowHandler* handler);
private:
    bool processWindow(xcb_window_t w, ExternalWindowHandler *handler);
private:
    xcb_connection_t* mConn_;
};

Здесь собраны все необходимые для реализации методы и умный указатель-обертка, освобождающий память через free(). Эта обертка весьма удобна для работы с функциями XCB, поскольку они требуют удаления данных именно через вызов free(). Теперь перейдем к реализации. Сперва напишем конструктор, позволяющий получить соединение с сервером X11.

XcbWindowManager::XcbWindowManager()
{
    // Получаем XCB-соединение с небольшой помощью Qt
    mConn_ = QX11Info::connection();
    if (mConn_ == nullptr) 
    {
        qCritical("xcb connection failed");
        return;
    }
}

Далее займемся реализацией основной функциональности:

xcb_window_t XcbWindowManager::leaderWindow(xcb_window_t _w)
{
    for (;;)
    {
        xcb_query_tree_cookie_t cookie = xcb_query_tree(mConn_, _w);
        cmem_ptr<xcb_query_tree_reply_t> reply(xcb_query_tree_reply(mConn_, cookie, NULL));
        if (!reply)
            break;
        if (reply->parent == XCB_WINDOW_NONE || reply->parent == reply->root)
            break;
        _w = reply->parent;
    }
    return _w;
}
bool XcbWindowManager::isVisible(xcb_window_t _w)
{
    if (!mConn_)
        return false;
    cmem_ptr<xcb_get_window_attributes_reply_t> attribs(xcb_get_window_attributes_reply(mConn_, xcb_get_window_attributes(mConn_, _w), nullptr));
    if (!attribs)
        return false;
    return attribs->_class == XCB_WINDOW_CLASS_INPUT_OUTPUT &&
           attribs->map_state == XCB_MAP_STATE_VIEWABLE;
}
QRect XcbWindowManager::geometry(xcb_window_t _w) const
{
    if (!mConn_)
        return QRect{};
    QRect windowRect;
    cmem_ptr<xcb_get_geometry_reply_t> geometry(xcb_get_geometry_reply(mConn_, xcb_get_geometry(mConn_, _w), nullptr));
    if (geometry)
        windowRect.setRect(geometry->x, geometry->y, geometry->width, geometry->height);
    return windowRect;
}
QImage XcbWindowManager::image(xcb_window_t _w, const QRect &rect) const
{
    auto img_cookie = xcb_get_image(
        mConn_, XCB_IMAGE_FORMAT_Z_PIXMAP,
        _w, rect.x(), rect.y(), rect.width(), rect.height(), static_cast<uint32_t>(~0)
    );
    cmem_ptr<xcb_get_image_reply_t> xcbimg(xcb_get_image_reply(mConn_,img_cookie, nullptr));
    if (xcbimg)
    {
        const uchar* imgData = xcb_get_image_data(xcbimg.get());
        QImage image(imgData, rect.width(), rect.height(), QImage::Format_ARGB32);
        image.detach();
        return image;
    }
    return QImage{};
}

В основном реализация довольно тривиальна, но пришлось повозиться с поиском документации к XCB. Отдельно остановлюсь на методах XcbWindowManager::leaderWindow(), XcbWindowManager::isVisible() и XcbWindowManager ::image().

Метод leaderWindow() необходим для поиска нашего окна. Дело в особенностях реализации окон верхнего уровня в Qt. Если у Qt-окна установлена рамка (а это поведение по умолчанию), то при обходе всех окон мы не сможем найти наше. Проблема заключается в методах QWidget::winId() и QWidget::effectiveWinId(): они возвращают WId контента окна, а не рамки, в то время как при обходе окон извлекаются WId именно рамки. Чтобы при обходе мы не пропустили наше окно, необходимо получить WId его рамки. Для этого можно запросить через XCB дерево окон, начиная от хендла, полученного от QWidget::effectiveWinId(), и пойти вверх по дереву, а не вниз. Так мы либо найдем рамку, либо, если ее нет, упремся в XCB_WINDOW_NONE или корневое окно всего рабочего стола. Интересная особенность: если лишить окно рамки вызовом setWindowFlags(windowFlags() | Qt::FramelessWindowHint), то QWidget::effectiveWinId() будет теперь возвращать идентификатор окна, совпадающий с системным.

Метод isVisible() необходим, чтобы отфильтровать невидимые окна. Но и здесь все непросто: мне так и не удалось полностью отфильтровать невидимые окна: либо пропускаются видимые, либо, наоборот, пропускаются невидимые.

В методе image() мы получаем указатель на сырые данные в части внешнего окна, который используется для конструирования QImage. Следует обратить внимание на вызов detach() для QImage. Это важно, поскольку если этого не сделать, то сырые данные будут удалены при выходе из функции и QImage останется без данных. Точнее, с Dangling Pointer на сырые данные, что вызовет Segfault при первом же обращении к QImage.

Перейдем теперь к реализации обхода всех окон:

void XcbWindowManager::traverseWindows(ExternalWindowHandler *handler)
{
    xcb_window_t root = xcb_setup_roots_iterator(xcb_get_setup(mConn_)).data->root;
    xcb_window_t* children;
    /* Получаем все окна, чьим родителем является корневое окно рабочего стола */
    cmem_ptr<xcb_query_tree_reply_t> reply(xcb_query_tree_reply(mConn_, xcb_query_tree(mConn_, root), nullptr));
    if (!reply)
        return;
    /* Запрашиваем число дочерних */
    const int len = xcb_query_tree_children_length(reply.get());
    if (len < 1)
        return;
    /* Запрашиваем дерево внешних окон */
    children = xcb_query_tree_children(reply.get());
    if (!children)
        return;
    for (int i = 0; i < len; ++i)
    {
        auto w = children[i];
        if (processWindow(w, handler))
            break;
    }
}

В этом методе мы:

  1. Запрашиваем идентификатор корневого окна всего рабочего стола. 
  2. Запрашиваем дерево всех окон, дочерних для корневого, вызовом xcb_query_tree_reply()
  3. Получаем количество окон вызовом xcb_query_tree_children_length().
  4. Получаем список всех окон вызовом xcb_query_tree_children()

Интересно, что полученный массив уже отсортирован по возрастанию оси Z. Таким образом, если нам необходимо пройти от самого нижнего окна к самому верхнему, достаточно обойти массив в прямом порядке. Если же нужно обойти окна по убыванию оси Z, то достаточно просто сменить порядок обхода на обратный. Дальнейшие действия тривиальны: обходим массив и для каждого элемента вызываем processWindow(), которая, в свою очередь, вызовет обработчик для внешнего окна. Если обработчик вернул true, то прерываем цикл.

Дальнейшая стратегия заключается в том, чтобы через равные промежутки времени запускать процедуру обхода всех внешних окон и создавать композицию из тех частей окон, которые пересекаются с нашим. Поскольку обход будет идти по возрастанию оси Z, мы получим правильно скомпонованное изображение. Кроме того, мы можем сразу остановить обход, как только достигнем идентификатора, совпадающего с нашим окном.

Однако во всех этих рассуждениях пропущен важный момент. Дело в том, что мы полностью продумали, как обрабатывать внешние окна, но совершенно забыли про сам рабочий стол. Оказалось, что получить изображение рабочего стола — весьма непростая задача. Подробное описание мучений можно найти здесь. Приведу код, адаптированный для использования с Qt:

QImage XcbWindowManager::grabRootImage(const QRect &r) const
{
    if (!mConn_)
        return QImage{};
    xcb_window_t root = xcb_setup_roots_iterator(xcb_get_setup(mConn_)).data->root;
    xcb_intern_atom_cookie_t cookie = xcb_intern_atom(mConn_, 0, strlen("_XROOTPMAP_ID"), "_XROOTPMAP_ID");
    cmem_ptr<xcb_intern_atom_reply_t> reply(xcb_intern_atom_reply(mConn_, cookie, 0));
    if (!reply)
        return QImage{};
     const xcb_atom_t atom = reply->atom;
     xcb_get_property_cookie_t image_cookie = xcb_get_property(mConn_, 0, root, atom, XCB_ATOM_PIXMAP, 0, 1);
     cmem_ptr<xcb_get_property_reply_t> image_reply(xcb_get_property_reply(mConn_, image_cookie, nullptr));
     if (!image_reply)
         return QImage{};
     if (xcb_get_property_value_length(image_reply.get()) == 0)
         return QImage{};
     const QRect area = r.isValid() ? r : geometry(root);
     xcb_drawable_t drawable = *(xcb_drawable_t*)xcb_get_property_value(image_reply.get());
     auto img_cookie = xcb_get_image(
         mConn_, XCB_IMAGE_FORMAT_Z_PIXMAP,
         drawable, area.x(), area.y(), area.width(), area.height(), static_cast<uint32_t>(~0)
     );
     cmem_ptr<xcb_get_image_reply_t> xcbimg(xcb_get_image_reply(mConn_, img_cookie, nullptr));
     if (xcbimg)
     {
         const uchar* imgData = xcb_get_image_data(xcbimg.get());
         QImage image(imgData, area.width(), area.height(), QImage::Format_RGB32);
         image.detach();
         return image;
     }
     return QImage{};
}

Теперь мы полностью готовы реализовать наше окно с эффектом Blur Behind и обработчик внешних окон.

class WindowCompositionHandler;
class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = 0);
    ~Widget();

private Q_SLOTS:
    void compose();

protected:
    void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE;
    void showEvent(QHideEvent *event) Q_DECL_OVERRIDE;
    void hideEvent(QHideEvent *event) Q_DECL_OVERRIDE;

private:
    QPixmap pixmap;
    QTimer* timer;
    WindowCompositionHandler* compositor;
};

Здесь нам опять не обойтись без таймера, поскольку узнать, изменилось ли что-то под нашим окном, мы не можем. Поэтому будем просто опрашивать оконный менеджер через равные интервалы времени. Отключим у виджета автозаполнение фона и включим атрибут Qt::WA_TranslucentBackground, чтобы сделать окно полупрозрачным. Далее создадим наш обработчик и таймер. При возникновении showEvent() включим таймер, а на hideEvent() отключим, это позволит сэкономить вычислительные ресурсы, если окно в текущий момент невидимо. Вот код, реализующий вышеописанное:

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    setAttribute(Qt::WA_TranslucentBackground);
    setAutoFillBackground(false);
    compositor = new WindowCompositionHandler(this);
    timer = new QTimer(this);
    timer->setInterval(10);
    connect(timer, &QTimer::timeout, this, &Widget::compose);
}

Widget::~Widget()
{
    delete compositor;
}

void Widget::hideEvent(QHideEvent *event)
{
    timer->stop();
    QWidget::hideEvent(event);
}
void Widget::showEvent(QShowEvent *event)
{
    QWidget::showEvent(event);
    timer->start();
}

На каждый такт таймера будем обходить все окна и выполнять композицию — это позволит сделать метод Widget::compose():

void Widget::compose()
{
    compositor->reset();
    XcbWindowManager::instance().traverse(compositor);
    pixmap = compositor->result();
}

void Widget::paintEvent(QPaintEvent*)
{
    QPainter painter(this);
    painter.setOpacity(0.8);
    if (!pixmap.isNull())
        painter.drawPixmap(0, 0, pixmap);
}

В методе Widget::compose() мы сбрасываем текущее состояние обработчика, обходим все окна и получившуюся композицию сохраняем в QPixmap. В обработчике paintEvent() мы просто отрисовываем pixmap на контексте нашего виджета. В итоге вся основная магия содержится в классе WindowCompositionHandler. Без лишних слов — вот его реализация:

class WindowCompositionHandler : public ExternalWindowHandler
{
public:
    WindowCompositionHandler(QWidget* w)
        : widget(w)
    {
        // получим изображение рабочего стола
        rootImage = grabRootImage();
        reset();
    }
    void reset()
    {
        wId = leaderWindow(widget->effectiveWinId()); // получаем реальный WId
        pixmap = QImage(widget->size(), Qimage::Format_ARGB32); // пересоздадим графический буфер
        QPainter painter(&pixmap);
        painter.drawImage(widget->rect(), rootImage, widget->geometry());
    }
    bool processWindow(WId w) Q_DECL_OVERRIDE
    {
        if (w == wId) // нашли свое же окно — останавливаем цикл обхода
            return true;
        if (!isVisible(w)) // окно невидимо 
            return false;
        const QRect wRect = windowGeometry(w); // получаем геометрию окна
        QRect intersected = wRect.intersected(widget->geometry()); // получаем прямоугольник пересечения
        if (intersected.isValid())
        {
            // для правильной отрисовки необходимо будет переместить
            // прямоугольник так, чтобы он оказался в локальных
            // координатах виджета, для этого получим глобальные 
            // координаты верхнего левого угла пересекающейся области
            QPoint offset = widget->mapFromGlobal(intersected.topLeft());
            // переместим область пересечения в 
            // локальные координаты внешнего окна
            intersected.moveTo(intersected.topLeft() - wRect.topLeft());
            // получаем графический буфер части окна
            QImage image = windowImage(w, intersected);
            // теперь переместим область пересечения в локальные 
            // координаты нашего виджета...
            intersected.moveTo(offset);
            QPainter painter(&pixmap);
            // ... и отрисуем часть внешнего виджета в нужной позиции
            painter.drawImage(intersected,image);
        }
        return false;
    }
    QPixmap result() const
    {
        // размываем изображение, используя downsampling, и возвращаем результат
        const QSize s = pixmap.size();
        QPixmap p = QPixmap::fromImage(stackBlurImage(pixmap.scaled((QSizeF(s) * 0.25).toSize(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation), 3, 4));
        return p.scaled(s, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
    }
    QImage rootImage; // изображение рабочего стола
    QImage pixmap;    // графический буфер
    QWidget* widget;  // указатель на основное окно
    WId wId;          // идентификатор основного окна
};


Эффект Blur Behind для окна нестандартной формы под Ubuntu Linux

Этот вариант страдает от серьезных недостатков: из-за постоянного опроса оконной системы и выполнения вычислительно тяжелого алгоритма размытия повышается потребление ресурсов CPU и памяти. К тому же нельзя точно определить, является ли внешнее окно видимым. Из-за использования таймера велика вероятность получить лаг при отрисовке.


Лаг и артефакты при отрисовке: алгоритм размытия не успевает за изменениями в оконной системе

Кроме того, если на рабочем столе находятся значки, получить их изображение также не получится. В итоге попытка интересная, но неудачная.

Попытка Blur Behind для Windows


Начиная с Windows Vista благодаря новой технологии Windows Aero появилась возможность использовать эффект Blur Behind. Для этого существует функция DwmEnableBlurBehindWindow. Однако в документации можно увидеть важное примечание:

Beginning with Windows 8, calling this function doesn't result in the blur effect, due to a style change in the way windows are rendered.

То есть начиная с Windows 8 использовать эту функцию невозможно. Однако существует недокументированная функция SetWindowCompositionAttribute, которая позволяет включить этот эффект. Чтобы ею воспользоваться, придется вручную загрузить библиотеку user32.dll для получения адреса SetWindowCompositionAttribute. К сожалению, найти хоть какую-то информацию об аргументах этой функции невероятно трудно. Вот описание функций, структур и перечислений, которое мне удалось найти:

typedef enum _WINDOWCOMPOSITIONATTRIB
{
    WCA_UNDEFINED = 0,
    WCA_NCRENDERING_ENABLED = 1,
    WCA_NCRENDERING_POLICY = 2,
    WCA_TRANSITIONS_FORCEDISABLED = 3,
    WCA_ALLOW_NCPAINT = 4,
    WCA_CAPTION_BUTTON_BOUNDS = 5,
    WCA_NONCLIENT_RTL_LAYOUT = 6,
    WCA_FORCE_ICONIC_REPRESENTATION = 7,
    WCA_EXTENDED_FRAME_BOUNDS = 8,
    WCA_HAS_ICONIC_BITMAP = 9,
    WCA_THEME_ATTRIBUTES = 10,
    WCA_NCRENDERING_EXILED = 11,
    WCA_NCADORNMENTINFO = 12,
    WCA_EXCLUDED_FROM_LIVEPREVIEW = 13,
    WCA_VIDEO_OVERLAY_ACTIVE = 14,
    WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15,
    WCA_DISALLOW_PEEK = 16,
    WCA_CLOAK = 17,
    WCA_CLOAKED = 18,
    WCA_ACCENT_POLICY = 19,
    WCA_FREEZE_REPRESENTATION = 20,
    WCA_EVER_UNCLOAKED = 21,
    WCA_VISUAL_OWNER = 22,
    WCA_LAST = 23
} WINDOWCOMPOSITIONATTRIB;

typedef struct _WINDOWCOMPOSITIONATTRIBDATA
{
    WINDOWCOMPOSITIONATTRIB Attrib;
    PVOID pvData;
    SIZE_T cbData;
} WINDOWCOMPOSITIONATTRIBDATA;

typedef enum _ACCENT_STATE
{
    ACCENT_DISABLED = 0,
    ACCENT_ENABLE_GRADIENT = 1,
    ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
    ACCENT_ENABLE_BLURBEHIND = 3,
    ACCENT_ENABLE_ACRYLICBLURBEHIND = 4,
    ACCENT_INVALID_STATE = 5
} ACCENT_STATE;

typedef struct _ACCENT_POLICY
{
    ACCENT_STATE AccentState;
    DWORD AccentFlags;
    COLORREF GradientColor;
    DWORD AnimationId;
} ACCENT_POLICY;

WINUSERAPI
BOOL
WINAPI
GetWindowCompositionAttribute(
    _In_ HWND hWnd,
    _Inout_ WINDOWCOMPOSITIONATTRIBDATA* pAttrData);

WINUSERAPI
BOOL
WINAPI
SetWindowCompositionAttribute(
    _In_ HWND hWnd,
    _Inout_ WINDOWCOMPOSITIONATTRIBDATA* pAttrData);

По всей видимости, некоторые элементы перечисления _WINDOWCOMPOSITIONATTRIB отвечают за то же, что и элементы DWMWINDOWATTRIBUTE. Описание перечисления _WINDOWCOMPOSITIONATTRIB:
Имя Значение Описание
WCA_UNDEFINED 0 Неизвестно.
WCA_NCRENDERING_ENABLED 1 По всей видимости, то же, что и DWMWA_NCRENDERING_ENABLED
WCA_NCRENDERING_POLICY 2 По всей видимости, то же, что и DWMWA_NCRENDERING_POLICY
WCA_TRANSITIONS_FORCEDISABLED 3 По всей видимости, то же, что и DWMWA_TRANSITIONS_FORCEDISABLED
WCA_ALLOW_NCPAINT 4 По всей видимости, то же, что и DWMWA_ALLOW_NCPAINT
WCA_CAPTION_BUTTON_BOUNDS 5 По всей видимости, то же, что и DWMWA_CAPTION_BUTTON_BOUNDS
WCA_NONCLIENT_RTL_LAYOUT 6 По всей видимости, то же, что и DWMWA_NONCLIENT_RTL_LAYOUT
WCA_FORCE_ICONIC_REPRESENTATION 7 По всей видимости, то же, что и DWMWA_FORCE_ICONIC_REPRESENTATION
WCA_EXTENDED_FRAME_BOUNDS 8 По всей видимости, то же, что и DWMWA_EXTENDED_FRAME_BOUNDS
WCA_HAS_ICONIC_BITMAP 9 По всей видимости, то же, что и DWMWA_HAS_ICONIC_BITMAP
WCA_THEME_ATTRIBUTES 10 Неизвестно.
WCA_NCRENDERING_EXILED 11 Неизвестно
WCA_NCADORNMENTINFO 12 Неизвестно.
WCA_EXCLUDED_FROM_LIVEPREVIEW 13 Неизвестно.
WCA_VIDEO_OVERLAY_ACTIVE 14 Неизвестно.
WCA_FORCE_ACTIVEWINDOW_APPEARANCE 15 Неизвестно.
WCA_DISALLOW_PEEK 16 По всей видимости, то же, что и DWMWA_EXCLUDED_FROM_PEEK
WCA_CLOAK 17 По всей видимости, то же, что и DWMWA_CLOAK
WCA_CLOAKED 18 По всей видимости, то же, что и DWMWA_CLOAKED
WCA_ACCENT_POLICY 19 Включает эффект Blur Behind.
WCA_FREEZE_REPRESENTATION 20 По всей видимости, то же, что и DWMWA_FREEZE_REPRESENTATION
WCA_EVER_UNCLOAKED 21 Неизвестно.
WCA_VISUAL_OWNER 22 Неизвестно.
WCA_LAST 23 Неизвестно, по всей видимости, счетчик всех элементов перечисления.

Описание перечисления _ACCENT_STATE:
Имя Значение Описание
ACCENT_DISABLED 0 Значение по умолчанию. Фон черный.
ACCENT_ENABLE_GRADIENT 1 Фон GradientColor, альфа-канал игнорируется.
ACCENT_ENABLE_TRANSPARENTGRADIENT 2 Фон GradientColor.
ACCENT_ENABLE_BLURBEHIND
3 Фон GradientColor с эффектом Blur Behind.
ACCENT_ENABLE_ACRYLICBLURBEHIND
4 Фон GradientColor с эффектом Acrylic Blur Behind.
ACCENT_ENABLE_HOSTBACKDROP 5 Неизвестно.
ACCENT_INVALID_STATE 6 Неизвестно. По всей видимости, фон полностью прозрачен.
Важно отметить, что Qt предоставляет модуль QtWinExtra с функцией QtWinExtra::enableBlurBehindWindow(), которая вызывает DwmEnableBlurBehindWindow(). Ею стоит воспользоваться, если есть необходимость в поддержке Windows 7.


Эффект Blur Behind на Windows 7

Для более поздних версий ОС Windows она работать уже не будет, и такой случай — как раз предмет демонстрируемой реализации.


Эффект Blur Behind на Windows 10

Ниже приведу полный код функции, включающий эффект Blur Behind для Top-level-виджета Qt для версий Windows 8 и выше:

typedef enum 
{
    ACCENT_DISABLED = 0,
    ACCENT_ENABLE_GRADIENT = 1,
    ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
    ACCENT_ENABLE_BLURBEHIND = 3,
    ACCENT_ENABLE_ACRYLICBLURBEHIND = 4,
    ACCENT_ENABLE_HOSTBACKDROP = 5,
    ACCENT_INVALID_STATE = 6
} ACCENT_STATE;
 
typedef struct {
    ACCENT_STATE accentState; // Background effect.
    DWORD accentFlags; // Effect Flags.
    COLORREF gradientColor; // Background color or gradient
    DWORD animationId; // Unknown (seems to be some animation)
} DWMACCENTPOLICY; // Determines how a window's background is rendered.
 
typedef struct _WINCOMPATTR_DATA {
    DWMACCENTPOLICY AccentPolicy; // The attribute being get or set is an accent policy.
} WINCOMPATTR_DATA;
 
typedef struct tagWINCOMPATTR
{
    DWORD attribute; // The attribute to query
    WINCOMPATTR_DATA* pData; // Buffer to store the result
    ULONG dataSize; // Size of the pData buffer
} WINCOMPATTR;

void enableBlurBehind(QWidget* widget)
{
    typedef HRESULT (WINAPI* pfnSetWindowCompositionAttribute)(HWND, WINCOMPATTR*);
    pfnSetWindowCompositionAttribute pSetWindowCompositionAttribute = 0;
    HMODULE user32 = ::LoadLibraryA("user32.dll");
    if (user32)
    {
        pSetWindowCompositionAttribute =
            (pfnSetWindowCompositionAttribute)::GetProcAddress(user32, "SetWindowCompositionAttribute");
    }
    DWORD dwFlags =  0x10;
    DWMACCENTPOLICY policy = { ACCENT_ENABLE_BLURBEHIND, dwFlags, 0, 0};
    WINCOMPATTR data = { WCA_ACCENT_POLICY, (WINCOMPATTR_DATA*)&policy, sizeof(DWMACCENTPOLICY) };
    if (pSetWindowCompositionAttribute)
        pSetWindowCompositionAttribute((HWND)widget->winId(), &data);
    ::FreeLibrary(user32);
}

Стоит особо отметить использование флага accentFlags в структуре DWMACCENTPOLICY, значения его элементов пришлось подбирать методом проб и ошибок:
Значение флага Описание
0x1
Неизвестно.
0x2
Включает перемешивание с цветом, указанным в переменной gradientColor. Для лучшего отображения цвет должен иметь альфа-канал, но в сочетании с ACCENT_ENABLE_GRADIENT альфа-канал игнорируется.
0x3
Неизвестно.
0x4
По всей видимости, увеличивает размер внешней части окна до размера всего экрана.
0x8
Неизвестно.
0x10
Включает использование масок с Blur Behind.
0x20
Тень по левой границе окна.
0x40
Тень по верхней границе окна.
0x80
Тень по правой границе окна.
0x100
Тень по нижней границе окна.
Используя переменную gradientColor из той же структуры, можно получить не только Blur Behind, но еще и полупрозрачную заливку цветом.


Эффект Blur Behind на Windows 10 с зеленой заливкой

Интересен также флаг 0x10: он позволяет использовать эффект совместно с окнами произвольной непрямоугольной формы.


Эффект Blur Behind для окна эллиптической формы на Windows 10

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


Эффект Blur Behind c ACCENT_ENABLE_ACRYLICBLURBEHIND на Windows 10

Интересного эффекта можно добиться, используя полупрозрачный градиент в paintEvent(): в таком случае этот градиент также будет использован при композиции фона окна, но на стороне Qt Framework. В конечном итоге окно может выглядеть весьма эффектно.



Blur Behind на macOS


Для реализации на macOS доступен API Cocoa, который позволяет полностью реализовать требуемый эффект. Класс NSVisualEffectView из стандартной поставки AppKit предназначен как раз для таких случаев. Однако, как всегда, мы сталкиваемся со сложностями: для использования класса придется придумать, как совместить реализацию с Qt Framework. Одно из решений, которое мне удалось найти, заключается в использовании наследования от класса QWindow:

class EffectWindow : public QWindow
{
    Q_OBJECT
public:
    EffectWindow();
protected:
    void resizeEvent(QResizeEvent* ev) Q_DECL_OVERRIDE;
private:
    QWindow* m_effectWindow;
};


EffectWindow::EffectWindow() : QWindow()
{
    // Создадим нативное окно эффектов
    NSVisualEffectView *effectsView = [[NSVisualEffectView alloc] init];
    effectsView.material = NSVisualEffectMaterialHUDWindow;
    effectsView.blendingMode = NSVisualEffectBlendingModeBehindWindow;
    effectsView.state = NSVisualEffectStateActive;

    // Обернем окно в QWindow, для того чтобы контролировать его
    m_effectsWindow = QWindow::fromWinId(reinterpret_cast<WId>(effectsView));

    // Добавим окно эффектов как дочернее к нашему
    m_effectsWindow->setParent(this);
    m_effectsWindow->setGeometry(this->geometry());
    m_effectsWindow->show();
}

void EffectWindow::resizeEvent(QResizeEvent* ev)
{
    QWindow::resizeEvent(ev);
    // Поддерживаем синхронность изменения размеров окна эффектов и нашего окна
    m_effectsWindow->setGeometry(0, 0, ev->size().width(), ev->size().height());
}

Такое решение работает, но теперь нет возможности использовать полученный класс совместно с наследниками QLayout или другими QWidget
-классами. Чтобы преодолеть эти трудности, придется нырнуть поглубже и попытаться разобраться, как можно использовать QWidget вместо QWindow.

Сперва разберемся, как связан QWidget c NSView. Достаточно беглого взгляда на исходные коды Qt, чтобы понять, что для macOS реализуется наследник NSViewQNSView. Мы можем получить указатель на NSView следующим образом:

NSView *view = (NSView *)widget->window()->effectiveWinId();

Из полученного NSView легко получить само окно NSWindow:

NSWindow *wnd = [view window];

Кажется, что теперь все просто: достаточно описать класс-наследник QWidget, создать в его конструкторе эффект и далее работать как обычно, создавая QLayout и добавляя в него другие необходимые нам виджеты. Однако такой подход работать не будет. Дело в том, что из-за вызова

m_effectsWindow = QWindow::fromWinId(reinterpret_cast<WId>(effectsView));

эффект будет поверх всех остальных элементов QWidget. Как же решить проблему? Для этого необходимо разобраться с тем, как располагаются NSView внутри NSWindow. Как можно догадаться, вызов addSubview у NSWindow позволяет напрямую добавить NSView как дочерний элемент NSWindow. Этим можно воспользоваться. Попробуем пойти следующим путем: создадим эффект, как в коде ранее, но добавим его к окну вручную. Затем создадим ложный QWidget, который будет служить контейнером для всех необходимых нам элементов UI. Настроим для него QLayout и добавим туда все необходимые нам элементы интерфейса пользователя.

Далее вызовом winId() принудительно создадим NSView
для виджета-контейнера. Теперь полученный указатель можно вручную добавить к нашему окну. Поскольку мы добавляем NSView виджета-контейнера к окну после эффекта, то контейнер окажется выше по оси Z и все элементы интерфейса будут видимы.

Чтобы эффект правильно отображался при изменении размеров виджета, необходимо синхронизировать его размер. Для этого добавим в класс переменную, хранящую указатель на NSView. Здесь нам придется использовать void*, поскольку NS-типы не поддерживаются в заголовочных файлах С++. Далее в обработчике resizeEvent() необходимо любым способом выставить правильный размер для NSVisualEffectView. Например, преобразовав QRect в CGRect или получив NSWindow и его размеры и передав тот же CGRect напрямую. Я выбрал второй вариант, но тут дело вкуса.

Теперь остался последний штрих. Наш виджет-контейнер имеет непрозрачный фон, и из-за этого не виден эффект BlurBehind. Можно попробовать установить атрибут Qt::WA_TranslucentBackground для нашего контейнера, но, как ни странно, эффекта это не даст. Дело в том, что этот атрибут применим только для окон верхнего уровня. Поэтому мы поступим неочевидным способом: установим атрибут Qt::WA_TranslucentBackground на весь корневой виджет. Теперь все работает как задумывалось.


Эффект Blur Behind на macOS BigSur

Ниже приведу код, реализующий все описанное выше:

class EffectWidget : public QWidget
{
    Q_OBJECT
public:
    EffectWidget();
protected:
    void resizeEvent(QResizeEvent* ev) Q_DECL_OVERRIDE;
private:
    void* m_effectView;
};


EffectWidget::EffectWidget() : QWidget()
{
    // Выставим атрибут, позволяющий виджету иметь прозрачный фон
    setAttribute(Qt::WA_TranslucentBackground);

    // получим указатели на NSView и NSWindow
    NSView *view = (NSView *)window()->effectiveWinId();
    NSWindow *wnd = [view window];

    // Создадим и настроим эффект
    auto v = [[[NSVisualEffectView alloc] init] autorelease];
    v.material = NSVisualEffectMaterialHUDWindow;
    v.blendingMode = NSVisualEffectBlendingModeBehindWindow;
    v.state = NSVisualEffectStateActive;

    // добавим эффект к нашему окну
    [wnd.contentView addSubview:v];
    [v setFrameSize:[ [ wnd contentView ] frame ].size];

    m_effectView = v;

    // Теперь создадим виджет контейнер
    QWidget* content = new QWidget(this);
    QPushButton* button = new QPushButton("Push Me!", content);
    QVBoxLayout* layout = new QVBoxLayout(content);
    layout->addWidget(button);

    QVBoxLayout* mainLayout = new QVBoxLayout(this);
    mainLayout->addWidget(content);

    // получим указатель на NSView виджета контейнера
    NSView *contentView = (NSView *)content->winId();

    // вручную добавим NSView контейнера к нашему окну
    [wnd.contentView addSubview:contentView];

    // поскольку контейнер теперь нативное окно, необходимо явно вызвать show()
    content->show();
}

void EffectWidget::resizeEvent(QResizeEvent *event)
{
    QWidget::resizeEvent(event);
    NSView *view = (NSView *)window()->effectiveWinId();
    NSWindow *wnd = [view window];
    NSView* effect = (NSView*)m_effectView;
    // синхронизируем размеры окна и эффекта
    [effect setFrame:[ [ wnd contentView ] frame ]];
}

Заключение


Для реальных задач далеко не всегда удается найти удачное решение. Часто приходится признавать и неудачи. Но не стоит опусать руки: неудачи лишь подстегнут вас к более глубокому изучению и в итоге приведут к еще более впечатляющим результатам, чем могло показаться на первый взгляд.

После всех изысканий в полной мере реализовать эффект Blur Behind окна верхнего уровня удалось только для ОС Windows и macOS, но полученный опыт может быть применен для успешного решения других задач. Если этот материал помог вам глубже понять внутренние механизмы оконных менеджеров и их применение совместно с библиотекой Qt, то можно считать, что цель статьи достигнута.

Познакомиться с VK Teams и узнать больше о приложении можно на сайте.
Теги:
Хабы:
Всего голосов 27: ↑26 и ↓1+25
Комментарии5

Публикации

Информация

Сайт
team.vk.company
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия
Представитель
Руслан Дзасохов