26 апреля 2013 г.

WM_PAINT во время ожидания вызова COM-сервера

В новых версиях Windows, начиная с Vista, приложение получает сообщение WM_PAINT во время ожидания вызова COM-сервера. Как написано в статье «Do you receive WM_PAINT when waiting for a COM call to return?», основной причиной для этого стала необходимость в правильной работе UAC (контроль учётных записей пользователей). Если приложение не рассчитано на такое поведение, то это может привести к ошибкам.


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



Есть несколько вариантов решения проблемы:
  1. Не обращаться к серверу из обработчика WM_PAINT. Помечать флагами узлы дерева, которые необходимо загрузить и отправлять специальное сообщение с помощью PostMessage, чтобы данные были загружены позже.
  2. Официальный способ. Используя Application Compatibility Toolkit создать для приложения SDB-файл с флагом DisableNewWMPAINTDispatchInOLE и при установке приложения регистрировать SDB-файл в системе. Как это сделать написано в упомянутой выше статье «Do you receive WM_PAINT when waiting for a COM call to return?».
  3. Приложение может самостоятельно при запуске установить флаг DisableNewWMPAINTDispatchInOLE. Проблема здесь только в том, что это недокументированная возможность и она может измениться в следующих версиях Winodws.
Первый вариант не подошел из-за высокой трудоемкости для встраивания в готовое приложение. Второй вариант неудобен тем, что нужно устанавливать SDB-файл, а очень хотелось, чтобы приложение можно было просто скопировать, без установки. После рассмотрения всех вариантов я для себя решил использовать третий вариант, как наиболее простой для реализации в моих условиях. В других условиях, наверное, правильным будет использовать второй вариант.

Скачав и установив Windows Symbol Packages, загрузил приложение в отладчик. В недрах ole32.dll был найден код функции _DisableNewWM_PAINTDispatch.
function _DisableNewWM_PAINTDispatch: LongBool;
asm
  mov eax,dword ptr fs:[00000018h]// Thread Environment Block
  mov eax,dword ptr [eax+30h]     // Process Environment Block
  mov eax,dword ptr [eax+1D8h]    // AppCompatFlags
  and eax,100000h
end;
Немного изменив код, получаем необходимую процедуру.
procedure DisableNewWMPaintDispatch;
asm
  mov eax,dword ptr fs:[00000018h]
  mov eax,dword ptr [eax+30h]
  mov edx,dword ptr [eax+1D8h]
  or  edx,100000h
  mov dword ptr [eax+1D8h],edx
end;

initialization
  if CheckWin32Version(6) then
    DisableNewWMPaintDispatch();

Для получения указателя на информацию о потоке можно было бы использовать функцию NtCurrentTeb вместо mov eax,dword ptr fs:[18h], но это вряд ли добавит совместимости с будущими версиями Windows, так как структуры TEB и PEB не документированы, и в Windows SDK про них явно написано: «The PEB and TEB structures are subject to changes between Windows releases, thus the fields offsets may change as well as the Reserved fields».

Код проверялся в Windows 7 и Windows 8. Дополнительно можно почитать комментарии к статье MSDN «IMessageFilter::MessagePending method (COM)» и топик «DCOM in Vista specifically processing WM_PAINT messages» в форумах MSDN.

1 комментарий:

  1. Как вариант - проверка рекурсивности вызова обработчика WM_PAINT.

    ОтветитьУдалить