Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

Эталон ТП за рулем

Поворот с ул. Набежено-Крещатицкой на ул Нижний Вал. Всегда найдутся уроды, поворачивающие со второго ряда направо, где только прямо; уроды, которые проезжают перекресток и сдают задом чтобы стать перед всеми. А иногда и вот такое хуйло попадается, но чаще мужского пола. А здесь - эталон ТП за рулем. ТП - тупая пизда, за рулем женщина.

Cruel InvokeRequired

Всем давно известно, что в WinForms начиная с версии 2 появилась защита от многопоточного использования элементов управления. При попытке выполнить опасные операции библиотечный код выбрасывает InvalidOperationException с текстом Cross-thread operation not valid: Control 'xxx' accessed from a thread other than the thread it was created on. Дальше я объясню как выполняется эта проверка и о некоторых подводных камнях этого механизма.

Свойство Handle класса Control имеет нетривиальную логику, часть которой содержит проверку на безопасность использования даного кода из другого потока:

[
Browsable(false), 
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
DispId(NativeMethods.ActiveX.DISPID_HWND), 
SRDescription(SR.ControlHandleDescr) 
]
public IntPtr Handle { 
   get {
       if (checkForIllegalCrossThreadCalls &&
           !inCrossThreadSafeCall &&
           InvokeRequired) { 
           throw new InvalidOperationException(SR.GetString(SR.IllegalCrossThreadCall,
                                                            Name)); 
       } 

       if (!IsHandleCreated) 
       {
           CreateHandle();
       }

       return HandleInternal;
   } 
}

Основная часть условия - свойство InvokeRequired:

[
Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced), 
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), 
SRDescription(SR.ControlInvokeRequiredDescr)
] 
public bool InvokeRequired {
   get {

       using (new MultithreadSafeCallScope()) 
       {
           HandleRef hwnd; 
           if (IsHandleCreated) { 
               hwnd = new HandleRef(this, Handle);
           } 
           else {
               Control marshalingControl = FindMarshalingControl();

               if (!marshalingControl.IsHandleCreated) { 
                   return false;
               } 

               hwnd = new HandleRef(marshalingControl, marshalingControl.Handle);
           } 

           int pid;
           int hwndThread = SafeNativeMethods.GetWindowThreadProcessId(hwnd, out pid);
           int currentThread = SafeNativeMethods.GetCurrentThreadId(); 
           return(hwndThread != currentThread);
       } 
   } 
}

Что здесь происходит? В локальную переменную hwnd записывается дескриптор текущего окна (если оно создано), иначе - дескриптор первого созданного окна в иерархии child-parent (метод FindMarshalingControl). Если ни один родитель не создан (нет дескриптора), метод InvokeRequired возвращает false. Далее используются функции GetWindowThreadProcessId и GetCurrentThreadId чтобы определить принадлежность созданного окна текущему потому. ОС Windows запоминает идентификаторы потоков в контексте которых произошли вызовы CreateWindow для создания окон.

Из этого можно сделать следующие выводы:

  1. WinForms не изобретает колесо - лишь использует доступную информация для выполнения нужных проверок
  2. Если окно не было создано (нет дексриптора), а также не были созданы все его родители - InvokeRequired возвращает false, что весьма логично - объект CLR может быть создан в любом потоке, но получит привязку к конкретному потоку лишь после создания

В результате следующий код содержит потенциальную проблему:

void handleNotificationFromOtherThread(...)
{
   if (someControl.InvokeRequired)
   {
      someControl.BeginInvoke(handleNotificationFromOtherThread, ...);
   }
   // Thread safe code here
   ...
}

Если уведомления прийдут до того, как хоть одно окно из иерархии будет создано, InvokeRequired вернет false и код выполнится в неправильном контексте. И здесь даже механизм защиты WinForms не поможет. В результате получаем многопоточный доступ к ресурсам без блокировок.

Как избежать подобного сценария? Создавая окно специально для целей синхронизации доступа:

Control sync = new Control();
sync.CreateControl();
_syncInvoke = (ISynchronizeInvoke)sync;

...

void handleNotificationFromOtherThread(...)
{
   if (_syncInvoke.InvokeRequired)
   {
      _syncInvoke.BeginInvoke(handleNotificationFromOtherThread, ...);
   }
   // Thread safe code here
   ...
}

Код выше принудительно создает окно в нужном контексте и использует его интерфейс ISynchronizeInvoke.

Но есть и более изящное решение - сохранить System.Threading.SynchronizationContext.Current как член класса и использовать для синхронизации. WinForms сам создаст по одному окну специально для маршаллинга вызовов в каждом потоке. Этот подход лучше всего подходит для простых сценариев, когда нужно отмаршаллить все выховы в один главный поток.

iPhone 4s jailbreak

Пока что я жмотюсь платить по 10+$ за нужные мне программы. Это не долбанные игрушки по 99 центов, а необходимые в жизни вещи типа Wallet и iGO. Ну, просто психологически не готов вывалить даже 10 у.е. за программу, которая просто хранит текст в шифрованном виде. А 100 у.е. за навигацию - перебор. Но мучения мои скоро закончатся, грядет A5 jailbreak.

iPhone 4s case

Решил, что чехол для роботизированной сучки нужен. Прошлый телефон изрядно пострадал из-за моего обращения с ним. Бросал в рюкзак, карманы, где ключи лежали и прочая чепуха. В результате на экране куча царапин и все такое. Бамперы и прочие подобные вещи считаю жлобством, поэтому выбор пал на мешочек (pouch по-буржуйски). Выбор пал на Sena SARACH ULTRASLIM White-Red.

Стоимость там - 40$. У нас - 60$. Заказал, привезли. На радостях отдал деньги курьеру, а лишь после заметил, что швы с одной стороны сильно перетянуты и в результате два слоя кожи не состыкованы, а слеплены. Ну, похоже на результат удара двух тектонических плит.

На упаковке - made in Turkey. Сука, как это все надоело! Кусок кожи стоимостью 60 у.е. сшит тяп-ляп в Турции. Вовремя позвонил курьеру и забрал обратно деньги, заебали.

Вывод? Просить чтобы привозили несколько экземпляров на выбор или не покупать вещи по маразматичным ценам.

iPhone 4s and SIRI

Получил я наконец свой iPhone 4S White 32Gb. Первое впечатление - говно. Но после пары часов использования, подключения к iCould и пр. - я понял, что сделано для людей. Очень удобный телефон.

  • На фотках белый выглядит гораздо лучше, чем в жизни. Черный на мой вкус смотрится более шикарно. Наверное из-за отражающего эффекта покрытия.
  • SIRI требует подключение к интернету и жрет трафик. Падла отсылает сжатый звук на сервер Apple, где происходит его анализ и генерация результатов. Транслитерацию не поддерживает, поэтому контакты должны использовать английский алфавит. С СМС-ками та же проблема.
  • После подключения симки происходило 2-3 пропадания сети Киевстар, что заставило злиться и спровоцировало поток матов. Симка то испорчена. Но после все образовалось. Не знаю почему. Возможно из-за отключения авто поиска сети и установки свежей прошивки. Перед этим я начитался о множестве проблем с пропаданием сети оператора Киевстар.
  • Батарею не сжирает. За ночь простоя ушло 3%. Если не пользоваться активно (как задрот или жлоб в метро)- жить будет 3-4 дня, полагаю. Геолокационные сервисы включены ВСЕ.
  • Камера понравилась. Хоть и сравнить не с чем, я никогда не пользовался телефоном как фотоаппаратом.
  • Работает шустро, тормоза не замечены нигде.

.NET Framework 4 Platform Update 2

А вы в курсе, что появился уже .NET Framework 4 Platform Update 2? Многие, наверное, не знают даже, что уже полгода как у многих установлен .NET Framework 4 Platform Update 1.

"Unsupported" C++ namespace names

Замечали такие вот интересные идентификаторы пространств имен: Win32Hosts.dll!<CrtImplementationDetails>::LanguageSupport::Initialize()? А вот как это делается:

namespace __identifier("<CrtImplementationDetails>")
{
...
}

Unable to step into .NET v4.0.30319 source

У меня длительное время пункт меню Go To Source Code недоступен, отлаживать исходный код .NET Framework невозможно. В окне Modules почти все сборки имеют Symbol Status = Symbols loaded. Проблема лишь в том, что pdb файлы маленького размера, а это значит, что информации об исходном коде в них нет.

Ответ нашелся в разделе Reference Source Server Discussion.

Решение состоит в использовании следующих версий сборок:

Name Version
mscorlib.dll 4.0.30319.1 (RTMRel.030319-0100)
PresentationFramework.dll 4.0.30319.1
WindowsBase.dll 4.0.30319.1 built by: RTMRel

Поэтому нужен .NET Framework 4 RTM, без патчей, которые накатывает операционная система. Следующие версии сборок не имеют соответствий в базе pdb файлов хранилища Reference Source:

Name Version
mscorlib.dll 4.0.30319.488 (RTMLDR.030319-4800)
PresentationFramework.dll 4.0.30319.450
WindowsBase.dll 4.0.30319.450 built by: RTMLDR

iPhone 4S around the world

Китайцы наконец собрали мой iPhone 4S 32Gb White и отдали в службу доставки. Теперь он отправится в Торонто, затем в Нью-Йорк, а уж после приедет в Киев :-).

Location Date Local Time Activity
Concord, ON, Canada 11/14/2011 18:45 Delivered
11/14/2011 16:11 The customer was not available on the 1st attempt. A 2nd delivery attempt will be made
11/14/2011 7:55 Out for Delivery
11/14/2011 5:49 Arrival Scan
Mount Hope, ON, Canada 11/14/2011 4:50 Departure Scan
Buffalo, NY, United States 11/13/2011 23:33 Departure Scan
11/13/2011 23:32 Arrival Scan
Mount Hope, ON, Canada 11/13/2011 10:07 Arrival Scan
Louisville, KY, United States 11/12/2011 11:25 Departure Scan
Anchorage, AK, United States 11/11/2011 4:56 Arrival Scan
Chek Lap Kok, Hong Kong 11/11/2011 12:59 Departure Scan
Chek Lap Kok, Hong Kong 11/09/2011 16:30 Arrival Scan
Shenzhen, China 11/09/2011 15:15 Departure Scan
11/09/2011 13:30 Origin Scan
China 11/09/2011 0:15 Order Processed: Ready for UPS

Oakley Polarized Juliet

Заказал себе Oakley Polarized Juliet. 400$ в США, без примерки, естественно. Я идиот?

Вот так нужно с тупыми бабами за рулем!


ICU quick starter

Часто бывает необходимо выполнить преобразование текста из одной кодировки в другую. Скажем, отослать на HTTP сервер строку в Windows-1251, а после декодировать ответ.

WinAPI как всегда выручает - функции MultiByteToWideChar и WideCharToMultiByte делают преобразование из UTF-16 в необходимую кодировку и обратно. Список поддерживаемых кодовых страниц велик, но нет механизма для получения идентификатора кодировки по имени. Поэтому я задался целью найти альтернативу этим функциям. И я ее нашел - библиотека ICU. Есть еще iconv, но меня она оттолкнула всей этой гнушной чепухой, не захотелось даже возиться. А вот ICU вполне прилежно поставляется с проектными файлами для свежайшей версии Visual Studio.

Собрать ICU проще простого:

  1. Скачиваем архив с исходными кодами со странички проекта, под колонкой ICU4C;
  2. Разворачиваем содержимое и открываем source\allinone\allinone.sln;
  3. Собираем обе конфигурации - Debug и Release;
  4. Запускаем bin\icuinfo.exe, проверяем, что ICU Initialization returned: U_ZERO_ERROR;
  5. Файл bin\icudtXY.dll должен быть внушительного размера - около 20 мегабайт.

    В случае получения ошибок U_FILE_ACCESS_ERROR или U_MISSING_RESOURCE_ERROR - проверяем размер файла. Если слишком маленький - пересобираем проект makedata, у меня один раз случилось подобное, долго не мог понять в чем дело. Нашел на сайте следующее:

    Why am I seeing a small ( only a few K ) instead of a large ( several megabytes ) data shared library (icudt)? Opening ICU services fails with U_MISSING_RESOURCE_ERROR and u_init() returns failure.

    ICU libraries always must link with the ICU data library. However, so that ICU can bootstrap itself, it first builds a 'stub' data library, in icu\source\stubdata, so that the tools can function. You should only use this in production if you are NOT using DLL-mode data access, in which case you are accessing ICU data as individual files, as an archive (.dat) file, or some other means. Normally, you should be using the larger library built from icu\source\data. If you see this issue after ICU has completed building, re-run 'make' in icu\source\data, or the 'makedata' project in Visual Studio.

Размер bin\icudtXY.dll можно уменьшить с помощью ICU Data Library Customizer. Файл icudt48l.dat из скачанного архива необходимо раскрыть в папку source\data\in (а может и source\data\out, точно не помню) и пересобрать проект makedata.

Рассмотрим пример использования этой замечательной библиотеки. Попытаемся получить строку Приветик, ICU! в кодировке Windows 1251, затем обратно в UTF-16 и сравнить с оригиналом:

#include "stdafx.h"
 
#include <cassert>
 
#include <unicode/ucnv.h>
#include <unicode/ucsdet.h>
 
int _tmain(int argc, _TCHAR* argv[])
{
   const wchar_t * const sourceUTF16 = L"Приветик, ICU!";
   const size_t len = wcslen(sourceUTF16);
 
   UErrorCode uError(U_ZERO_ERROR);
 
   /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
   UConverter * const pConverter = ucnv_open("windows-1251", &uError);
   if (U_FAILURE(uError)) {
      return -1;
   }
 
   /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
   const char * const pszCharsetInternalName = ucnv_getName(pConverter, &uError);
   assert(U_SUCCESS(uError));
   assert(pszCharsetInternalName);
   const char * const pszCharsetIANAName = ucnv_getStandardName(pszCharsetInternalName, "IANA", &uError);
   assert(U_SUCCESS(uError));
   assert(pszCharsetIANAName);
 
   const size_t minCharSize = ucnv_getMinCharSize(pConverter);
   const size_t maxCharSize = ucnv_getMaxCharSize(pConverter);
 
   /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
   const wchar_t *pSourceW = &sourceUTF16[0];
   const wchar_t * const pSourceLimitW = &sourceUTF16[len];
   
   char *targetEncoding = reinterpret_cast<char *>(_alloca(maxCharSize * (len + 1)));
   char *pTarget = &targetEncoding[0];
   const char * const pTargetLimit = &targetEncoding[len];
   ucnv_fromUnicode(pConverter, &pTarget, pTargetLimit, &pSourceW, pSourceLimitW, NULL, true, &uError);
   assert(U_SUCCESS(uError));
   *pTarget = '\0';
 
   /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
   const char *pSource = &targetEncoding[0];
   const char * const pSourceLimit = &targetEncoding[len];
 
   wchar_t * const targetUTF16 = reinterpret_cast<wchar_t *>(_alloca(sizeof(wchar_t) * (len + 1)));
   wchar_t *pTargetW = &targetUTF16[0];
   const wchar_t * const pTargetLimitW = &targetUTF16[len];
   ucnv_toUnicode(pConverter, &pTargetW, pTargetLimitW, &pSource, pSourceLimit, NULL, true, &uError);
   assert(U_SUCCESS(uError));
   *pTargetW = L'\0';
 
   /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
   ucnv_close(pConverter);
 
   return 0;
}

Разберем по порядку:

  1. Включение заголовочных файлов unicode/ucnv.h и unicode/ucsdet.h из папки include;
  2. Открытие конвертера функцией ucnv_open.

    В качестве параметра передаем имя кодовой страницы или ее аналог, один из многих альтернативных имен, поддерживаемых ICU.

  3. В отладочных целях получаем внутреннее и стандартное имена открытого конвертера функциями ucnv_getName и ucnv_getStandardName;
  4. Основная часть - аллокация памяти под результат и использование конвертера по назначению (функции ucnv_fromUnicode и ucnv_toUnicode);
  5. Освобождение ресурсов, закрытие конвертера функцией ucnv_close.

Линкеру понадобятся icuuc.lib и icuin.lib для успешного создания исполняемого файла. А ему, в свою очередь, - библиотеки icuuc48.dll, icudt48.dll и icuin48.dll для успешного запуска.

В результате выполнения получаем следующий результат:

ICU proof of concept

CEdit with an ellipsis

Sometimes developers are devoted to absolutely rediculous tasks. Like having an established conception broken for some reason. You spend a lot of time cracking things up to acomplish your mission. Recently I was working on an edit control with an ellipsis if the text does not fit.

I came up with painting the edit in case it's not focused:

class CEllipsisEdit : public CEdit
{
   DECLARE_DYNAMIC(CEllipsisEdit)
 
protected:
   COLORREF GetBkColor() const;
 
protected:
   CBrush m_bkBrush;
 
protected:
   DECLARE_MESSAGE_MAP()
   afx_msg void OnPaint();
 
};
#include "EllipsisEdit.h"
 
IMPLEMENT_DYNAMIC(CEllipsisEdit, CEdit)
 
// CEllipsisEdit message map
 
BEGIN_MESSAGE_MAP(CEllipsisEdit, CEdit)
   ON_WM_PAINT()
END_MESSAGE_MAP()
 
// CEllipsisEdit message handlers
 
COLORREF CEllipsisEdit::GetBkColor() const
{
   if (m_hWnd) {
      const HWND hParentWnd = GetParent()->GetSafeHwnd();
      if (::GetWindowThreadProcessId(hParentWnd, NULL) == ::GetWindowThreadProcessId(m_hWnd, NULL)) {
         CDC dc;
         dc.CreateCompatibleDC(NULL);
         const bool enabled = (GetStyle() & WS_DISABLED) == 0;
         const UINT colorMsg = enabled ? WM_CTLCOLOREDIT : WM_CTLCOLORSTATIC;
 
         ::SendMessage(hParentWnd, colorMsg, reinterpret_cast<WPARAM>(dc.m_hDC), reinterpret_cast<LPARAM>(m_hWnd));
 
         return dc.GetBkColor();
      }
   }
 
   return ::GetSysColor(COLOR_WINDOW);
}
 
void CEllipsisEdit::OnPaint()
{
   if (::GetFocus() == m_hWnd) {
      __super::OnPaint();
      return;
   }
 
   CString text;
   GetWindowText(text);
 
   if (!m_bkBrush.m_hObject)
      m_bkBrush.CreateSolidBrush(GetBkColor());
   
   CPaintDC dc(this);
   
   RECT rc = { 0, 0, 0, 0 };   
   GetClientRect(&rc);
   dc.FillRect(&rc, &m_bkBrush);
   
   GetRect(&rc);
   dc.SetBkMode(TRANSPARENT);
   dc.SelectObject(GetFont());
   dc.DrawText(text, text.GetLength(), &rc, DT_SINGLELINE | DT_NOPREFIX | DT_WORD_ELLIPSIS);
}

There were several problems:

  1. Edit control would paint itself synchronously. No WM_PAINT message is sent, just GetDC(hwnd) and the paint code, no way to sabstitute painting.
  2. Edit control doesn't have non-client area, borders are drawn in client area.
  3. Have to somehow determine text background color.

Synchronous paint seems to be an issue only with focused control. Borders are drawn as usual for unknown reason. Background color shall be determined by sending WM_CTLCOLOREDIT or WM_CTLCOLORSTATIC to parent or just a COLOR_WINDOW system color as a fallback.

Please note that WM_CTLCOLOR-related messages are not sent to parent widows in different threads! Also don't forget to add DT_NOPREFIX flag when invoking DrawText method.

Implementing popups in MFC (part 1)

Создание всплывающих окон - задача не из легких. Особенно если это не банальная контекстная подсказка, а, скажем, окно с элементами управления или необходимостью пользовательского взаимодействия (мышь, клавиатура). Самый распостраненный случай всплывающего окна - Combo Box.

Великий и могучий Combo Box.

Combo Box - это сложный элемент управления, как правило состоящий из списка (List Box), поля ввода (Text Box) и несколько кастрированной кнопки (Button).

Особенности работы Combo Box:

  • При нажатии на кнопку (это не единственный способ) появляется выпадающий список
  • Это окно по своей сути является стандартным List Box-ом, но на стероидах (с именем класса ComboLBox)
  • Фокус ввода остается на самом Combo Box-е, всплывшее окно со списком не активируется
  • Выпадающее окно производит захват мыши вызовом SetCapture, в результате чего все мышиные сообщения для всех окон процесса попадают исключительно в оконную процедуру всплывшего окна
  • При щелчке мышью вне выпадающего окна - оно скрывается сразу же при обработке WM_?BUTTONDOWN, при этом захват мыши отменяется вызовом ReleaseCapture, что приводит к получению WM_?BUTTONUP окном, которое находилось под курсором в момент отжатия кнопки
  • При щелчке внутри выпадающего окна, оно реагирует закрытием во время обработки WM_?BUTTONUP
  • Список реагирует на нажатия стрелочных кнопок (вверх, вниз); очевидно, что сообщения WM_KEYDOWN приходят в сам Combo Box, а он их передает в выпадающий список
  • При потере Combo Box-ом фокуса ввода - список прячется

Важные выводы:

  1. ComboBox работает правильно вне зависимости от наличия окон в различных UI потоках
  2. Во время отображения окна выпадающего списка лишь оно из всех окон процесса получает мышиные сообщения - соответственно не должно происходить никаких реакций на движение курсором мыши вне этого окна (всплывающие подсказки, подсветка кнопок, изменение картинки курсора и т.д.)
  3. Щелчок мышью вне окна съедается

А если не Combo Box?

В ОС существует множество выпадающих окон кроме Combo Box. Среди них - Date and Time Picker, меню (стандартное меню ОС, навороченное меню типа Ribbon или другие реализации, например WPF) и множество других. И я вас уверяю - единой концепции взаимодействия с пользователем у них нет...

Window menu

Стандартное меню, которое предоставляет операционная система, работает безотказно, но весьма ограничено в отображаемом контенте и функциональности. Я остановил процесс по таймеру во время трекинга меню функцией TrackPopupMenu. Эта функция блокирует выполнение программы и реализована ядром. Но при этом цикл сообщений крутится внутри нее, поэтому программа остается отзывчивой:

> Win32App.exe!CTopLevelWnd::OnTimer(unsigned int nIDEvent)  Line 211 C++
  Win32App.exe!CWnd::OnWndMsg(unsigned int message, unsigned int wParam, long lParam, long * pResult)  Line 2411 C++
  Win32App.exe!CWnd::WindowProc(unsigned int message, unsigned int wParam, long lParam)  Line 2087 + 0x20 bytes C++
  Win32App.exe!AfxCallWndProc(CWnd * pWnd, HWND__ * hWnd, unsigned int nMsg, unsigned int wParam, long lParam)  Line 257 + 0x1c bytes C++
  Win32App.exe!AfxWndProc(HWND__ * hWnd, unsigned int nMsg, unsigned int wParam, long lParam)  Line 420 C++
  user32.dll!_InternalCallWinProc@20()  + 0x23 bytes 
  user32.dll!_UserCallWinProcCheckWow@32()  + 0xb3 bytes 
  user32.dll!_DispatchClientMessage@20()  + 0x4b bytes 
  user32.dll!___fnDWORD@4()  + 0x24 bytes 
  ntdll.dll!_KiUserCallbackDispatcher@12()  + 0x2e bytes 
  user32.dll!_DispatchClientMessage@20()  
  user32.dll!_NtUserTrackPopupMenuEx@24()  + 0xc bytes 
  user32.dll!_TrackPopupMenu@28()  + 0x1b bytes 
  Win32App.exe!CMenu::TrackPopupMenu(unsigned int nFlags, int x, int y, CWnd * pWnd, const tagRECT * lpRect)  Line 1310 + 0x26 bytes C++

Я даже не поленился и просмотрел исходный код в ворованных архивах Windows 2000. Это меню очень тесно интегрировано с оконными процедурами и всем, что обслуживает UI. Не мудрено, что во время останова программы, когда на экране меню, вся система начала вести себя немного странно.

В отличии от Combo Box, контекстное меню ОС не съедает щелчок вне окна меню, но при этом так же не пропускает WM_MOUSEMOVE и WM_NCMOUSEMOVE.

Codejock menu

А это реализация навороченного меню библиотекой Codejock Xtreme Toolkit Pro. WM_MOUSEMOVE и WM_NCMOUSEMOVE не блокируются, даже контекстная подсказка выскочила, что не очень то правильно. Точней они не блокируются для текущих контекстных окон (а их там два), блокируются для оставшихся тулбаров этой библиотеки (Ribbon в даном случае), а для остальных окон приложения (полоса прокрутки, область редактирования текста, бордера и пр.) - снова не блокируются.

Как и в случае с контекстным меню ОС, не съедает щелчек вне окон контекста.

Собственная реализация TrackPopupMenu устанавливает различные хуки для слежения за сообщениями, механизм SetCapture/ReleaseCapture здесь уже не при делах.

Кстати, прототип этой реализации - Microsoft Office 2007/2010, в котором WM_MOUSEMOVE блокируется для всех окон вне контекста, а WM_NCMOUSEMOVE работает слегка через жопу - при наведении на бордер окна курсор меняет свой внешний вид, кнопки минимизации, максимизации и закрытия окон прикидываются мертвыми, но если пошуршать курсором у правого-верхнего края окна, то крестик будет предательски нам подмигивать, выдавая ошибку в реализации этой задумки. Scenic Ribbon в, скажем, MS Paint имеет те же проблемы.

Visual Studio 2010 menu

На этой картинке находится кусочек навороченного меню Visual Studio 2010, очевидно реализованного управляемым кодом. WM_MOUSEMOVE блокируется, а вот в случае WM_NCMOUSEMOVE все несколько сложнее. По моим наблюдениям не блокируется движение мышью по заголовку окна, кнопки минимизации, максимизации и закрытия окон реагируют на наведение подсветкой, но при этом контекстная подсказка не появляется (как это сделано - пока что загадка). При наведении на полосу прокрутки никакой реакции нет.

Как и в случае с контекстным меню ОС, не съедает щелчек вне окон контекста.

Я хочу отметить, что все эти менюшки - обыкновенные окна, ничего сверхестественного. И они никогда не активируются и не крадут фокус ввода. Истинные джентльмены.

Предательские реализации всплывающих контекстных окон

Explorer menu

Контекстная подсказка этой кнопочки гордо сообщает нам о возможности Change your view. А при нажатии появляется якобы контекстное меню, которое почему-то приводит к деактивации главного окна Explorer. Взгляните на цвет заголовка, он ясно дает понять - окно более не активно. Предательское окно ворует фокус ввода, он находится на элементе управления типа Slider. Причем рамка фокуса изначально не отображается, лишь когда нажать клавиши стрелок (вверх или вниз).

При этом на блокировку WM_??MOUSEMOVE нет даже намека - все элементы управления реагируют на перемещение курсора, всплывают подсказки, причем под этим меню. А самое вкусное - WM_?BUTTONDOWN проглатывается, а последующий WM_?BUTTONUP проходит. Результат схлопывания меню зависит от реализации элемента управления, по которому был произведен щелчок, - ответная реакция на WM_?BUTTONDOWN или же на WM_?BUTTONUP.

Кстати, открыл для себя что градация слайдера имеет дискретную структуру с 4 шагами снизу (где разделители нарисованы) и непрерывную (с очень мелким шагом) в области, где разделителей нет. Иконки плавно меняют свой размер.

Весьма странная реализация, если сравнить с предыдущими.

Альтернативный подход

Открыв контекстное меню Firefox, вы убедитесь, что оно никак не влияет на остальные окна, что, впрочем, выглядит вполне логично. Щелчки обрабатываются как обычно, но перед этим меню пропадает с экрана.

С другой стороны, отсутствие реакции окон на движение мышью подсказывает пользователю, что где-то открыто контекстное меню или модальный диалог, но лишь потому, что ОС Windows работает подобным образом.

Реализация собственных контекстных меню

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

  1. Реакция окон (этого же приложения) вне контекста на перемещение указателя мыши
  2. Щелчки мышью по окнам (этого же приложения) вне контекста
  3. Деактивация активного окна приложения

Отслеживание мыши

Для слежения за мышью есть такие подходы:

  • SetCapture/ReleaseCapture

    С помощью этих функций можно произвести захват/освобождение мыши и направить все сообщения в оконную процедуру одного окна. Но именно с этим связана огромная проблема - мы же хотим отображать сложные окна с элементами управления, которые тоже должны получать мышиные сообщения. В результате обработчики WM_??MOUSE?? выпадающего окна должны будут сами обеспечивать попадание нужных сообщений в оконные процедуры дочерних окон. Нужных - хорошо написано, а на деле - всех возможных. Фактически мы будем эмулировать то, что делает сама ОС Windows. Может показаться, что достаточно лишь найти окно под курсором функцией WindowFromPoint и вызвать оконную процедуру найденного окна, но на самом деле нюансов огромное множество, как и эмулируемых сообщений.

    Сам по себе этот сценарий не позволяет контролировать механизм проглатывания щелчков вне контекста. Допустим, при щелчке мы хотим спрятать выпадающее окно, а после этого получить реакцию на щелчок окном, по которому он был произведен. Из-за SetCapture сообщение WM_LBUTTONDOWN извлекается из очереди и попадает в оконную процедуру выпадающего окна, где код определяет положение курсора и прячет окно, если оно находилось вне его области. Далее нужно обеспечить обработку этого сообщения, для этого вызываем AfxCallWndProc (или просто SendMessage). И делаем мы это из обработчика WM_LBUTTONDOWN нашего выпадающего окна! А что если реакцией на щелчок будет повторное выпадение окна (возможно вовсе не этого)? Мы пополним стек потока на добрую дюжину вызовов. И вот здесь все зависит от цикла сообщений. Есть два варианта:

    1. Показать окно и выйти из метода ShowPopup (неблокирующий вызов).

      Цикл сообщений при этом остается прежним (как он реализован в приложении), проблем со стеком здесь быть не должно, но это вносит свои трудности в удобство использования такого программного интерфейса - нельзя держать объекты на стеке (окна, контроллеры, политики и т.д.), при выходе из обработчика окно продолжает висеть на экране и следить за обстановкой, ожидая момента, когда можно спрятаться и уничтожить все следы своего сущевствования, уведомив владельца о произошедшем.

    2. Показать окно и запустить свой цикл сообщений в методе ShowPopup (как это делает TrackPopupMenu, блокирующий вызов).

      В этом случае мы можем вызвать переполнение стека банальными щелчками мыши, которые постоянно приводят к пропаданию текущего выпадающего окна и появлению нового - метод ShowPopup будет вызываться рекурсивно, каждый раз запуская новый цикл сообщений. Это весомый аргумент против. А бороться с ним крайне сложно. Фактически нужно куда-то сохранить полученное сообщение, которое привело к закрытию выпадающего окна, и уведомить внешний цикл сообщений, что его нужно обработать. К сожалению MFC такой тонкой настройки не предоставляет, придется заменить его цикл сообщений своим (банально скопировав код MFC и добавив туда свое). Это уже как-то слишком сложно для поддержки выпадающих окон.

    И последний, самый важный нюанс SetCapture: захват может производиться лишь одним окном и вложенность захвата не поддерживается. Это означает, что любой элемент управления, использующий функцию SetCapture, приведет к снятию установленного захвата и поломке выдающего окна. С подобным, вероятно, можно бороться, но это весьма затратно и совершенно не перспективно для исследования.

    Очевидно, что этот путь обречен на хардфакинг и успешность его сомнительна, отбрасываем.

  • Перехватчики (hooks) WH_GETMESSAGE, WH_MOUSE и WH_MOUSE_LL

    С их помощью можно следить за очередью сообщений, отлавливать интересные события. Перехватчики - единственный способ гарантированно получить доступ к содержимому очереди сообщений. Можно было написать свой цикл сообщений (вариант блокирующего вызова ShowPopup) и в промежутке между GetMessage и DispatchMessage выполнять анализ и предпринимать действия. Но есть одна проблема - циклы сообщений могут запускаться в результате реакций на действия пользователя. К примеру, если открыть Date and Time picker, - он запускал вложенный цикл сообщений в версиях ОС Windows до Vista, а также в версиях Common Controls до 6-й. Все это приводит к неконтролируемой потере управления над циклом сообщений и, как последствие, невозможности отлавливать интересующие события.

    Перехватчики WH_GETMESSAGE и WH_MOUSE получают управление перед возвратом функциями GetMessage и PeekMessage управления в случае наличия сообщения в очереди (удовлетворяющего критериям). Однако WH_MOUSE позволяет выполнять лишь мониторинг сообщений, а WH_GETMESSAGE - модифицировать ко всему прочему. WH_MOUSE_LL в этом плане похож на своего собрата WH_MOUSE, но система дает ему управление перед тем, как поместить сообщение в очередь. Это дает возможность спрятать всплывающее окно прежде, чем цикл сообщений получит щелчок мышью, который к этому привел.

Стратегии реализации

Реализация набора классов, которые позволяют показывать всплывающие окна, зависит от следующих факторов:

  • Механизм работы метода ShowPopup - блокирующий или неблокирующий
  • Необходимость проглатывания сообщений, которые приводят к закрытию всплывающего окна
  • Фильтрация перемещений указателя мыши
  • Отсутствие привязки к библиотеке, с момощью которой окно было создано (MFC, ATL, ...) - возможность работы с окнами, передаваемыми по дескриптору (HWND)
  • Возможность предоставить некую политику функционирования, с помощью которой можно сделать тонкую настройку всего механизма и реализовать сложные сценарии

Я остановился на блокирующем ShowPopup, с внутренним циклом обработки сообщений и возможностью настройки поведения через политику. Интерфейс моего класса CPopupController выглядит следующим образом:

public:
   CPopupController(IPopupEnvironment *pEnvironment);
 
public:
   void ShowPopup(HWND hWnd, HWND hOwner, const POINT &ptAnchor, IPopupPolicy *pPopupPolicy = NULL);
   void DismissPopup();

Для начала разберемся в каких случаях всплывающее окно должно быть закрыто без привязки к его внутренностям (поведение меню):

  1. Деактивация приложения (foreground thread больше не является потоком этого приложения)
  2. Щелчки вне контекста
  3. Уничтожение окна
  4. Изменение состояний окна на такие, в которых режим меню смысла больше не имеет (окно invisible, disabled etc.)

Реализовать это поведение достаточно просто - привязаться к всплывающему окну и реагировать на сообщения, присылаемые ОС Windows. Для этого можно использовать класс CWindowSubclass:

BEGIN_MSG_MAP(CPopupController)
   MESSAGE_HANDLER(WM_ACTIVATE, OnActivate)
   MESSAGE_HANDLER(WM_ACTIVATEAPP, OnActivateApp)
   MESSAGE_HANDLER(WM_SHOWWINDOW, OnShowWindow)
   MESSAGE_HANDLER(WM_ENABLE, OnEnable)
   MESSAGE_HANDLER(WM_SYSCOMMAND, OnSysCommand)
   MESSAGE_HANDLER(WM_MOUSEACTIVATE, OnMouseActivate)
   MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
END_MSG_MAP()
LRESULT CPopupController::OnActivate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(lParam);
 
   if (wParam == WA_INACTIVE) {
      if (IPopupPolicy * const pPopupPolicy = GetPopupPolicy()) {
         if (pPopupPolicy->DismissPopupOnLosingActivation()) {
            DismissPopup();
         }
      }
      else
         DismissPopup();
   }
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnActivateApp(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(lParam);
 
   if (wParam == FALSE) {
      DismissPopup();
   }
 
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnShowWindow(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(lParam);
 
   if (wParam == FALSE) {
      DismissPopup();
   }
 
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnEnable(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(lParam);
 
   if (wParam == FALSE) {
      DismissPopup();
   }
 
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnSysCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(lParam);
   
   if (wParam == SC_CLOSE) {
      DismissPopup();
   }
   
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnMouseActivate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(wParam);
   UNREFERENCED_PARAMETER(lParam);
 
   if (m_pPopupPolicy) {
      const IPopupPolicy::eMouseActivateResult maResult = m_pPopupPolicy->MouseActivateAction();
      if (maResult != IPopupPolicy::eDefaultMouseActivateAction) {
         return maResult;
      }
   }
 
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(wParam);
   UNREFERENCED_PARAMETER(lParam);
   
   ContinueMessageLoop(false);
 
   bHandled = FALSE;
   return 0;
}

Суть обработчиков - словить условия закрытия окна и, собственно, закрыть, выполнив все необходимые очистки. Исключение составляет обработчик WM_DESTROY - здесь мы просто устанавливаем флаг, который заставит завершить цикл сообщений.

Если вдруг код приложения по какой-то причине спрячет окно, уничтожит его, или же поменяет его состояние на disabled, - контроллер автоматически выполнит закрытие режима меню.

Следующий шаг - игра в кошки-мышки. Будем гоняться за мышью и лапкой (точней рукой сурового программиста) давать ей по носу. Для этого нам необходим перехватчик сообщений WH_GETMESSAGE. Здесь в игру вступает класс CPopupManager:

bool EnterPopupMode(CPopupController *pController);
bool IsPopupMode() const;
void ExitPopupMode(CPopupController *pController);

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

Полная версия менеджера выглядит следующим образом:

#pragma once
 
class CPopupController;
 
class CPopupManager : public CNoTrackObject
{
 
public:
   CPopupManager();
 
   static CPopupManager &Instance();
 
   bool EnterPopupMode(CPopupController *pController);
   bool IsPopupMode() const;
   void ExitPopupMode(CPopupController *pController);
 
private:
   static LRESULT CALLBACK GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam);
   static LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam);
   
   bool PreFilterMessage(LPMSG pMsg, bool preview);
   bool ActivatingWindow(HWND hWnd, LPCBTACTIVATESTRUCT pCBAS);
   bool SettingFocus(HWND hSetFocusWnd, HWND hKillFocusWnd);
 
private:
   CPopupController *m_pController;
   HHOOK m_hGetMessageHook;
   HHOOK m_hCBTHook;
 
};
#include "stdafx.h"
#include "PopupManager.h"
#include "PopupController.h"
#include "PopupPolicy.h"
 
CProcessLocal<CPopupManager> afxPopupManager;
 
CPopupManager::CPopupManager() : m_pController(NULL), m_hGetMessageHook(NULL), m_hCBTHook(NULL)
{
}
 
CPopupManager &CPopupManager::Instance()
{
   return(*afxPopupManager);
}
 
bool CPopupManager::EnterPopupMode(CPopupController *pController)
{
   ATLASSERT(pController && !IsPopupMode());
   if (!pController || IsPopupMode())
      return false;
 
   m_hGetMessageHook = ::SetWindowsHookEx(WH_GETMESSAGE, &CPopupManager::GetMsgProc, NULL, ::GetCurrentThreadId());
   ATLASSERT(m_hGetMessageHook);
 
   if (pController->GetPopupPolicy()) {
      m_hCBTHook = ::SetWindowsHookEx(WH_CBT, &CPopupManager::CBTProc, NULL, ::GetCurrentThreadId());
      ATLASSERT(m_hCBTHook);
   }
 
   m_pController = pController;
 
   return true;
}
 
bool CPopupManager::IsPopupMode() const
{
   return m_pController != NULL;
}
 
void CPopupManager::ExitPopupMode(CPopupController *pController)
{
   ATLASSERT(IsPopupMode());
   ATLASSERT(m_pController == pController);
 
   m_pController = NULL;
 
   if (m_hCBTHook) {
      ATLVERIFY(::UnhookWindowsHookEx(m_hCBTHook));
      m_hCBTHook = NULL;
   }
 
   if (m_hGetMessageHook) {
      ATLVERIFY(::UnhookWindowsHookEx(m_hGetMessageHook));
      m_hGetMessageHook = NULL;
   }     
}
 
LRESULT CPopupManager::GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam)
{
   if (nCode < 0) {
      return(::CallNextHookEx(NULL, nCode, wParam, lParam));
   }
 
   if (nCode == HC_ACTION) {
      LPMSG const pMsg = reinterpret_cast<LPMSG>(lParam);
      if (WM_MOUSEFIRST <= pMsg->message && pMsg->message <= WM_MOUSELAST
          || WM_NCMOUSEMOVE <= pMsg->message && pMsg->message <= WM_NCXBUTTONDBLCLK
          || WM_KEYFIRST <= pMsg->message && pMsg->message <= WM_KEYLAST) {
         if (Instance().PreFilterMessage(pMsg, wParam == PM_NOREMOVE)) {
            return 0;
         }
      }
   }
 
   return(::CallNextHookEx(NULL, nCode, wParam, lParam));
}
 
LRESULT CPopupManager::CBTProc(int nCode, WPARAM wParam, LPARAM lParam)
{
   if (nCode < 0) {
      return(::CallNextHookEx(NULL, nCode, wParam, lParam));
   }
 
   if (nCode == HCBT_ACTIVATE) {
      // The system is about to activate a window.
      if (Instance().ActivatingWindow(reinterpret_cast<HWND>(wParam), reinterpret_cast<LPCBTACTIVATESTRUCT>(lParam))) {
         return 0;
      }
   }
   else if (nCode == HCBT_SETFOCUS) {
      // A window is about to receive the keyboard focus.
      if (Instance().SettingFocus(reinterpret_cast<HWND>(wParam), reinterpret_cast<HWND>(lParam))) {
         return 0;
      }
   }
 
   return(::CallNextHookEx(NULL, nCode, wParam, lParam));
}
 
bool CPopupManager::PreFilterMessage(LPMSG pMsg, bool preview)
{
   ATLASSERT(m_pController);
   if (m_pController)
      return m_pController->PreFilterMessage(pMsg, preview);
   return false;
}
 
bool CPopupManager::ActivatingWindow(HWND hWnd, LPCBTACTIVATESTRUCT pCBAS)
{
   ATLASSERT(m_pController);
   if (m_pController)
      return m_pController->ActivatingWindow(hWnd, pCBAS);
   return false;
}
 
bool CPopupManager::SettingFocus(HWND hSetFocusWnd, HWND hKillFocusWnd)
{
   ATLASSERT(m_pController);
   if (m_pController)
      return m_pController->SettingFocus(hSetFocusWnd, hKillFocusWnd);
   return false;
}

В случае наличия политики также устанавливается перехватчик WH_CBT, который необходим для слежения за фокусом ввода и активациями окон.

Возвращаемся к кошкам-мышкам:

bool CPopupController::PreFilterMessage(LPMSG pMsg, bool preview) {
   if (IPopupPolicy * const pPopupPolicy = GetPopupPolicy()) {
      const IPopupPolicy::ePreFilterResult preFilterResult = pPopupPolicy->PreFilterMessage(pMsg, preview);
      if (preFilterResult == IPopupPolicy::ePreFilterBlockMessage) {
         pMsg->message = WM_NULL;
         return true;
      }
      else if (preFilterResult == IPopupPolicy::ePreFilterAcceptMessage) {
         return false;
      }
      // eContinueRouting
   }
 
   if (Dismissing())
      return false;
   
   bool dismiss = false;
   
   const HWND hCapture = ::GetCapture();   
   const bool isMouseButtonAction = IsMouseButtonAction(pMsg->message);
 
   if (hCapture) {
      if (isMouseButtonAction) {
         POINT ptAction = { GET_X_LPARAM(pMsg->lParam),  GET_Y_LPARAM(pMsg->lParam) };
         ATLVERIFY(::ClientToScreen(hCapture, &ptAction));
 
         const HWND hActionWnd = ::WindowFromPoint(ptAction);
         dismiss = !hActionWnd || hActionWnd != hCapture && !IsOwnerOrSelf(GetPopupHWnd(), hActionWnd);
      }
   }
   else {
      if (isMouseButtonAction) {
         dismiss = !IsOwnerOrSelf(GetPopupHWnd(), pMsg->hwnd);
      }
      else if (pMsg->message == WM_MOUSEMOVE) {
         pMsg->message = WM_NULL;
         return true;
      }
   }
 
   if (dismiss) {
      DismissPopup();
   }
 
   return dismiss;
}

Это код делает следующее:

  1. В первую очередь дает возможность политике фильтровать сообщения
  2. Ничего не делает если режим меню в состоянии закрытия
  3. Выясняет состояние захвата мыши и совершаемое мышью действие
  4. Если мышь захвачена неким окном - нужно выяснить какому окну предназначено сообщение в случае, если бы захват мыши не был установлен вовсе
  5. Если целевое окно - то, которое выполнило захват, одно из его дочерних окон, а также само выпадающее окно (вместе с его содержимым) - сообщение нужно пропустить
  6. В противном случае - выйти из режима меню
  7. Когда захвата мыши нет - выйти из режима меню в случае, когда произвели щелчок по окну вне контекста, а также проглотить сообщение WM_MOUSEMOVE вне контекста. WM_NCMOUSEMOVE я решил пропускать - ничего страшного в этом нет, пускай полосы прокрутки и остальные неклиентские элементы реагируют на происходящее

Зачем нужны все эти танцы с захватом мыши? Чтобы работали элементы управления типа Combo Box, Button и пр. Combo Box, как было описано выше, сам показывает выпадающее окно, используя SetCapture для фильтрации мыши.

Далее рассмотрим подготовку и вход в режим меню. Этим занимается метод ShowPopup:

void CPopupController::ShowPopup(HWND hPopup, HWND hOwner, const POINT &ptAnchor, IPopupPolicy *pPopupPolicy/* = NULL*/)
{
   CPopupManager &manager = CPopupManager::Instance();
 
   ATLASSERT(!manager.IsPopupMode());
   ATLASSERT(!m_hPopup);
   if (manager.IsPopupMode() || m_hPopup)
      return;
   
   const bool popupOk = hPopup && ::IsWindow(hPopup) && ::IsWindowEnabled(hPopup)
                               && (::GetWindowLongPtr(hPopup, GWL_STYLE) & WS_CHILD) == 0
                               && ::GetWindowThreadProcessId(hPopup, NULL) == ::GetCurrentThreadId();
   ATLASSERT(popupOk);
   if (!popupOk)
      return;
 
   if (hOwner && ::IsWindow(hOwner) && (::GetWindowLongPtr(hOwner, GWL_STYLE) & WS_CHILD) != 0) {
      ATLASSERT(FALSE);
      hOwner = ::GetAncestor(hOwner, GA_ROOT);
   }
   else
      hOwner = ::GetActiveWindow();
 
   const bool ownerOk = hOwner && ::GetWindowThreadProcessId(hOwner, NULL) == ::GetCurrentThreadId();
   ATLASSERT(ownerOk);
   if (!ownerOk)
      return;
   
   m_hPopup = hPopup;
   ATLASSERT(GetPopupHWnd() == hPopup);
 
   m_state.reset();
 
   if (const CWindowSubclass &window = CWindowSubclass(GetPopupHWnd(), this)) {
      if (const PopupScope &popupScope = PopupScope(manager, this)) {
         if (pPopupPolicy)
            m_pPopupPolicy = pPopupPolicy;
 
         SetLastError(0);         
         if (!::SetWindowLongPtr(GetPopupHWnd(), GWL_HWNDPARENT, reinterpret_cast<LONG_PTR>(hOwner))) {
            ATLVERIFY(GetLastError() == 0);
         }
         const DWORD swpFlags = SWP_NOSIZE | SWP_NOSENDCHANGING
                                | (m_pPopupPolicy && m_pPopupPolicy->ShowWithoutActivation() ? SWP_NOACTIVATE : 0);
 
         RECT rcWindow = { ptAnchor.x, ptAnchor.y, ptAnchor.x, ptAnchor.y };
         ATLVERIFY(::GetWindowRect(GetPopupHWnd(), &rcWindow));
         const SIZE szWindow = { rcWindow.right - rcWindow.left, rcWindow.bottom - rcWindow.top };
         ATLVERIFY(::CalculatePopupWindowPosition(&ptAnchor, &szWindow, TPM_LEFTALIGN, NULL, &rcWindow));
 
         ATLVERIFY(::SetWindowPos(GetPopupHWnd(), HWND_TOP, rcWindow.left, rcWindow.top, 0, 0, swpFlags));
 
         if (!::IsWindowVisible(GetPopupHWnd())) {
            if (!::AnimateWindow(GetPopupHWnd(), 200, AW_BLEND)) {
               ATLVERIFY(::SetWindowPos(GetPopupHWnd(), HWND_TOP, 0, 0, 0, 0, swpFlags | SWP_NOMOVE | SWP_SHOWWINDOW));
            }
         }
 
         if (!Dismissing() && ::IsWindowVisible(GetPopupHWnd())) {
            do {
               RunMessageLoop();
            } while (Dismissing(false) && m_pPopupPolicy && m_pPopupPolicy->CancelDismiss());
         }
 
         if (pPopupPolicy)
            m_pPopupPolicy = NULL;         
      }
   }
 
   if (::IsWindowVisible(GetPopupHWnd())) {
      ::ShowWindow(GetPopupHWnd(), SW_HIDE);
   }
 
   m_hPopup = NULL;
}

Что здесь происходит:

  1. Проверки состояний менеджера (нельзя входить в режим меню рекурсивно);
  2. Проверки входящих параметров, состояний окон и т.д.;
  3. Контекстный (scoped) сабклассинг окна;
  4. Контекстный (scoped) вход в режим меню;
  5. Привязка окна-владельца к выпадающему окну;
  6. Определение позиции выпадающего окна и его показ (с анимацией, если это возможно);
  7. Цикл циклов сообщений;
  8. Очистка состояния.

Цикл сообщений выглядит следующим образом:

void CPopupController::RunMessageLoop()
{
   ContinueMessageLoop(true);
   
   MSG dummy;
   while (ContinueMessageLoop()) {
      while (::PeekMessage(&dummy, NULL, 0, 0, PM_NOREMOVE)) {
         if (!ContinueMessageLoop()) {
            break;
         }
 
         if (!PumpMessage()) {
            ::PostQuitMessage(0);
            DismissPopup();
            break;
         }
 
         if (!ContinueMessageLoop()) {
            break;
         }
      }
      if (ContinueMessageLoop()) {
         ::WaitMessage();
      }
   }
}

Я вот только что прикинул - цикл сообщений можно было бы перенести в менеджер. Подумаю на досуге.

Важный момент - использование PeekMessage с параметром PM_NOREMOVE. Это сделано для того, чтобы иметь возможность выйти из режима меню до обработки сообщения. Как следствие - иметь возможность не проглатывать сообщение, которое привело к закрытию меню.

Обратите внимание на реализацию PumpMessage:

bool CPopupController::PumpMessage() {
   return m_pEnvironment->PumpMessage();
}

Теперь пример использования этого чуда:

void CChildView::OnRButtonUp(UINT nFlags, CPoint point) {
   POINT ptPopup = point;
   ClientToScreen(&ptPopup);
   
   struct XPopupPolicy : IPopupPolicy
   {
      //virtual bool ShowWithoutActivation() { return false; }
      //virtual eMouseActivateResult MouseActivateAction() { return eNoActivate; }
   };
   
   CPopupDlg popup;
   popup.Create(IDD_POPUP, ::AfxGetMainWnd());
   XPopupPolicy xPopupPolicy;
   CMFCPopupEnvironment mfcPopupEnv;
   CPopupController popupController(&mfcPopupEnv);
   popupController.ShowPopup(popup.GetSafeHwnd(), ::AfxGetMainWnd()->GetSafeHwnd(), ptPopup, &xPopupPolicy);
 
   __super::OnRButtonUp(nFlags, point);
}

Мой диалог содержит пару кнопок и выпадающий список. Все это отлично работает.

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

Код в свободный доступ не выкладываю, жадный. Спасибо за внимание.

Copyright 2007-2011 Chabster