Ошибка Windows Aero Rendering
Итак, я наткнулся на интересную ошибку в Windows API, и мне интересно, есть ли у кого-нибудь представление о том, как ее обойти. Кажется, что даже Google боролся с этим. Следует отметить, что, хотя я буду исправлять это в самом источнике Qt, проблема связана с обработкой сообщений по умолчанию Windows, а не с Qt. Все файлы, о которых я расскажу, можно найти в Интернете, так как все они являются библиотеками с открытым исходным кодом. Ниже приведена сложная проблема, и я постараюсь дать как можно больше контекста. Я потратил много времени и сил на то, чтобы исправить это сам, но, будучи тем, что я был инженером всего около 8 месяцев, я все еще довольно неопытен и вполне мог пропустить что-то очевидное.
Контекст:
Я написал программу, которая использует Qt для обложки моих окон с помощью пользовательских скинов. Эти скины обрабатывают скины пользовательского интерфейса, не связанные с клиентом по умолчанию. Другими словами, я использую пользовательские раскрашенные кадры (поддерживаемые Qt). Начиная с Qt5 у меня возникли проблемы с моей программой, когда она запускается на любой предварительной Windows Aero OS (меньше XP и выше, чем Vista с отключенным Windows Aero). К сожалению, разработчики Qt почти подтвердили, что они больше не поддерживают XP, поэтому я не буду полагаться на них, чтобы исправить ошибку.
Ошибка:
Щелчок в любом месте неклиентской области при запуске машины с отключенной композицией (отключенный или не существующий Windows Aero) приведет к тому, что Windows перекрасит свой клиентский интерфейс, не используемый клиентом по умолчанию, поверх моего пользовательского интерфейса кожа.
Мои исследования
Немного отладки и расследования привели меня к qWindowsProc в qwindowscontext.cpp. Я смог определить, что последнее сообщение Windows, которое должно быть обработано до того, как был очерчен мой скин, был WM_NCLBUTTONDOWN. Это показалось странным, поэтому я пошел в интернет.
Конечно, я нашел файл hwnd_message_Handler.cc, который поставляется с Google Chromium Embedded Framework (CEF). В этом файле много комментариев о том, как различные сообщения Windows, по какой-то безумной причине, вызывают рецензию системных по умолчанию неклиентских кадров по пользовательским фреймам. Ниже приведен один такой комментарий.
// A scoping class that prevents a window from being able to redraw in response
// to invalidations that may occur within it for the lifetime of the object.
//
// Why would we want such a thing? Well, it turns out Windows has some
// "unorthodox" behavior when it comes to painting its non-client areas.
// Occasionally, Windows will paint portions of the default non-client area
// right over the top of the custom frame. This is not simply fixed by handling
// WM_NCPAINT/WM_PAINT, with some investigation it turns out that this
// rendering is being done *inside* the default implementation of some message
// handlers and functions:
// . **WM_SETTEXT**
// . **WM_SETICON**
// . **WM_NCLBUTTONDOWN**
// . EnableMenuItem, called from our WM_INITMENU handler
// The solution is to handle these messages and **call DefWindowProc ourselves**,
// but prevent the window from being able to update itself for the duration of
// the call. We do this with this class, which automatically calls its
// associated Window lock and unlock functions as it is created and destroyed.
// See documentation in those methods for the technique used.
//
// The lock only has an effect if the window was visible upon lock creation, as
// it doesn't guard against direct visiblility changes, and multiple locks may
// exist simultaneously to handle certain nested Windows messages.
//
// IMPORTANT: Do not use this scoping object for large scopes or periods of
// time! IT WILL PREVENT THE WINDOW FROM BEING REDRAWN! (duh).
//
// I would love to hear Raymond Chen explanation for all this. And maybe a
// list of other messages that this applies to ;-)
Также в этом файле существует несколько специальных обработчиков сообщений, чтобы предотвратить появление этой ошибки. Например, появилось другое сообщение, которое вызывает эту ошибку: WM_SETCURSOR. Разумеется, у них есть обработчик для того, что при переносе в мою программу отлично работало.
Один из распространенных способов обработки этих сообщений - это ScopedRedrawLock. По сути, это просто блокирует перерисовку в начале обработки по умолчанию неактивного сообщения (через DefWindowProc) и остается заблокированным на время вызова, разблокируя себя, когда выходит из области действия (следовательно, Scoped RedrawLock), Этот не работает для WM_NCLBUTTONDOWN по следующей причине:
Пройдя через qWindowsWndProc во время обработки по умолчанию WM_NCLBUTTONDOWN, я увидел, что WM_SYSCOMMAND обрабатывается в том же стеке вызовов непосредственно после WM_NCLBUTTONDOWN. WParam для данного WM_SYSCOMMAND 0xf012 - другое официально недокументированное значение **. К счастью, в разделе замечаний на странице MSDN WM_SYSCOMMAND кто-то прокомментировал это. Оказывается, это код SC_DRAGMOVE.
По причинам, которые могут показаться очевидными, мы не можем просто заблокировать перерисовку для обработки WM_NCLBUTTONDOWN, потому что Windows автоматически предполагает, что пользователь пытается перетащить окно, если он нажимает на неклиентскую область (в данном случае - HTCAPTION). Блокировка здесь заставит окно никогда не перерисовываться на время перетаскивания, пока Windows не получит сообщение с кнопкой (WM_NCLBUTTONUP или WM_LBUTTONUP).
И конечно, я нахожу этот комментарий в своем коде,
if (!handled && message == WM_NCLBUTTONDOWN && w_param != HTSYSMENU &&
delegate_->IsUsingCustomFrame()) {
// TODO(msw): Eliminate undesired painting, or re-evaluate this workaround.
// DefWindowProc for WM_NCLBUTTONDOWN does weird non-client painting, so we
// need to call it inside a ScopedRedrawLock. This may cause other negative
// side-effects (ex/ stifling non-client mouse releases).
DefWindowProcWithRedrawLock(message, w_param, l_param);
handled = true;
}
Это похоже на то, что у них была такая же проблема, но не совсем понял, как это решить.
Единственное другое место CEF обрабатывает WM_NCLBUTTONDOWN в той же области, что и эта проблема:
else if (message == WM_NCLBUTTONDOWN && delegate_->IsUsingCustomFrame()) {
switch (w_param) {
case HTCLOSE:
case HTMINBUTTON:
case HTMAXBUTTON: {
// When the mouse is pressed down in these specific non-client areas,
// we need to tell the RootView to send the mouse pressed event (which
// sets capture, allowing subsequent WM_LBUTTONUP (note, _not_
// WM_NCLBUTTONUP) to fire so that the appropriate WM_SYSCOMMAND can be
// sent by the applicable button ButtonListener. We _have_ to do this
// way rather than letting Windows just send the syscommand itself (as
// would happen if we never did this dance) because for some insane
// reason DefWindowProc for WM_NCLBUTTONDOWN also renders the pressed
// window control button appearance, in the Windows classic style, over
// our view! Ick! By handling this message we prevent Windows from
// doing this undesirable thing, but that means we need to roll the
// sys-command handling ourselves.
// Combine |w_param| with common key state message flags.
w_param |= base::win::IsCtrlPressed() ? MK_CONTROL : 0;
w_param |= base::win::IsShiftPressed() ? MK_SHIFT : 0;
}
}
И хотя этот обработчик обращается к аналогичной проблеме, это не совсем то же самое.
Вопрос
Итак, в этот момент я застрял. Я не совсем уверен, где искать. Возможно, я неправильно читаю код? Может быть, ответ есть в CEF, и я просто его не замечаю?Похоже, что инженеры CEF столкнулись с этой проблемой и еще не придумали решение, учитывая комментарий TODO:. Кто-нибудь знает, что еще я могу сделать? Куда мне идти дальше? Не решить эту ошибку не вариант. Я готов копать глубже, но на данный момент я размышляю о том, как управлять обработкой событий Windows самостоятельно, а не с помощью DefWindowProc. Хотя это может все еще вызвать ошибку в случае, когда пользователь фактически перетаскивает окно.
Ссылки
Я включил список ссылок, которые я использовал в своих исследованиях. Лично я сам загрузил источник CEF, чтобы я мог лучше ориентироваться в коде. Если вы действительно заинтересованы в решении этой проблемы, вам может понадобиться сделать то же самое.
WM_NCLBUTTONDOWN
WM_NCHITTEST
WM_SYSCOMMAND
DefWindowProc
hwnd_message_handler.cc
hwnd_message_handler.h
qwindowscontext.cpp
Касательная
Чтобы проверить правильность кода CEF, если вы посмотрите в заголовок hwnd_message_handler, вы также заметите, что есть два недокументированных сообщения Windows со значением 0xAE и 0xAF. Я видел 0xAE во время обработки WM_SETICON по умолчанию, что вызывало проблемы, и этот код помог подтвердить, что то, что я видел, действительно реально.
Ответы
Ответ 1
Таким образом, фактическим способом достижения этого исправления было удаление флага WS_CAPTION во время NC_LBUTTONDOWN и добавление его обратно во время обработки сообщений NC_LBUTTONUP. Однако из-за того, как Windows вычисляет свой размер перед рендерингом, он может просчитать, поскольку он удаляет область заголовка из рассмотрения. Таким образом, вам придется компенсировать это при обработке сообщения WM_NCCALCSIZE.
Имейте в виду, что количество пикселей, которое вам нужно будет компенсировать, будет зависеть от того, какие темы или ОС Windows вы находитесь. То есть у Vista есть другая тема, кроме XP. Поэтому вам нужно будет определить масштабный коэффициент, чтобы поддерживать его чистоту.
Ответ 2
Я нашел эту страницу, которая предлагает скрывать ваше окно, удалив WS_VISIBLE
непосредственно перед вызовом DefWindowProc()
, а затем сразу же показывая его. Я не пробовал, но это на что посмотреть.