Привет, Хабр! Меня зовут Михаил Полукаров, я занимаюсь разработкой 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 размером в наше окно, а общий алгоритм создания такой композиции будет следующий:
- Необходимо запросить у X11 все видимые на экране окна и обойти их в порядке возрастания оси Z вплоть до нашего окна.
- Для каждого внешнего окна проверить, что идентификатор окна не равен нашему, иначе завершить цикл.
- Для каждого внешнего окна получить его геометрию и выяснить, пересекается ли она с нашим окном.
- В случае пересечения вычислить регион пересечения.
- Получить изображение внешнего окна в этом регионе.
- Отрисовать полученный участок в буфер в соответствии с координатами пересечения.
Далее при каждом такте отрисовки уже нашего окна в
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;
}
}
В этом методе мы:
- Запрашиваем идентификатор корневого окна всего рабочего стола.
- Запрашиваем дерево всех окон, дочерних для корневого, вызовом
xcb_query_tree_reply()
.
- Получаем количество окон вызовом
xcb_query_tree_children_length()
.
- Получаем список всех окон вызовом
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 | Неизвестно. По всей видимости, фон полностью прозрачен. |
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 реализуется наследник NSView
— QNSView
. Мы можем получить указатель на 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, то можно считать, что цель статьи достигнута.
Комментарии (3)
sepuka
24.05.2023 05:05Как же всё-таки хорошо, что пока ещё можно отключать все эти тени, прозрачности, анимации да фейды интерфейсов и просто оставлять только реально нужные show window contents while dragging да прочие smooth edges of screen fonts...
ArtZilla
Мысли вслух: какие же приятные прозрачные окна были в Vista/7, хоть в 10 попытались некое подобие этого сделать, но окна всё равно топорные, на границах окон прозрачность и размытие работают криво. Всё же иногда надо бить палкой по рукам за желание всё переписать и переделать.
nafikovr
У меня ностальгия по 2007 году началась (Beryl с его прозрачными и "аморфными" окнами, а не то что вы подумали)