Ошибка 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(), а затем сразу же показывая его. Я не пробовал, но это на что посмотреть.