Fucking Date and Time Picker Controls or how to close DateTimePicker programmatically
Только что закончил фикс милейшего дефекта. DateTimePicker
вываливал
окно календаря и при щелчках мышью по заголовку родительского окна или в другие
области оно оставалось висеть пока не выбрана дата.
Происходило это потому, что элемент управления DateTimePicker
был
создан в потоке отличном от потока окна верхнего уровня, в котором он находится.
Да, знаю, многозадачный UI - это зло, но имеем то, что имеем.
В результате исследования выяснилось, что начиная с Windows Vista этой проблемы больше нет - окно закрывается в любом случае (маленькая поправочка - при использовании 6-й версии библиотеки comctl32). На Windows XP проблема имеет место быть в не зависимости от версии comctl32.
Помимо внедрения механизма, который позволяет перехватывать по запросу асинхронные сообщения со всех UI потоков, возникла еще одна задачка - програмно закрыть окно календаря.
Сначала показалось, что задачка из простых - было найдено сообщение
DTM_CLOSEMONTHCAL
,
которое выполняет нужную функцию. После попытки разобраться почему вариант не рабочий
выяснилось, что Minimum supported client - Windows Vista
. А нам для Windows
XP. Блядь-блядь-блядь!!!
Хрен с ним, решение все равно есть. Достаточно лишь понять как работает
DateTimePicker
. А он закрывает month calendar при нажатии клавиши Escape,
а также при некоторых манипуляциях мышью. Также следует учесть, что элемент управления
не теряет фокус во время отображения окна month calendar, а само выпадающее окно
никогда не активируется.
Первое, что пришло в голову, - эмулировать нажатие Escape когда month calendar
нужно закрыть програмно - выполнить SendKeys.SendWait("{ESC}");
. Вариант
оказался не рабочий. Причина весьма коварна - вызов этого метода запускает вложенный
цикл сообщений и крутит его до тех пор, пока внедренные сообщения о нажатии клавиш
не будут обработаны. А еще DateTimePicker
запускает свой цикл сообщений,
когда показывает окно календаря:
user32.dll!_NtUserGetMessage@16() + 0xc bytes comctl32.dll!_DPLBD_MonthCal@8() + 0x244 bytes comctl32.dll!_DPLButtonDown@12() + 0x79 bytes comctl32.dll!_DatePickWndProc@16() + 0x56b bytes user32.dll!_InternalCallWinProc@20() + 0x28 bytes user32.dll!_UserCallWinProcCheckWow@32() + 0xb7 bytes user32.dll!_CallWindowProcAorW@24() + 0x51 bytes user32.dll!_CallWindowProcW@20() + 0x1b bytes System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.DefWndProc(ref System.Windows.Forms.Message m) Line 810 + 0x31 bytes C# System.Windows.Forms.dll!System.Windows.Forms.Control.DefWndProc(ref System.Windows.Forms.Message m) Line 5729 + 0xa bytes C# System.Windows.Forms.dll!System.Windows.Forms.Control.WmMouseDown(ref System.Windows.Forms.Message m, System.Windows.Forms.MouseButtons button, int clicks) Line 12909 + 0xc bytes C# System.Windows.Forms.dll!System.Windows.Forms.Control.WndProc(ref System.Windows.Forms.Message m) Line 13761 C# System.Windows.Forms.dll!System.Windows.Forms.DateTimePicker.WndProc(ref System.Windows.Forms.Message m) Line 1686 C#
В этом цикле содержится логика закрытия окна календаря. Одно из ее составляющих
- ожидание сообщения WM_KEYDOWN
с VK_ESC
. Сообщение при
этом глотается, до окна оно не доходит. Именно поэтому первая попытка провалилась.
Выход - использовать SendKeys.Send("{ESC}");
, неблокирующий вызов,
который не запускает вложенный цикл обработки сообщений, просто вставляет в очередь
необходимые сообщения, которые в последствии обрабатываются циклом сообщений внутри
DPLBD_MonthCal
. Мне он тоже не подошел т.к. активировалось другое окно
и оно же получало нажатие Escape. В результате я просто делаю Win32.PostMessage(Handle,
Win32.WM_KEYDOWN, Win32.VK_ESC, IntPtr.Zero);
и все пучком. Код стероидов:
/// <summary> /// Month calendar visibility. /// </summary> public bool IsMonthCalendarShown { get { return (IsHandleCreated && Win32.SendMessage(Handle, Win32.DTM_GETMONTHCAL, IntPtr.Zero, IntPtr.Zero) != IntPtr.Zero); } } /// <summary> /// Closes month calendar if visible. /// </summary> /// <remarks> /// This is guaranteed to work on Windows Vista and above. Under Windows XP this method uses a hack. /// </remarks> public void CloseMonthCalendar() { if (!IsMonthCalendarShown) { return; } if (Win32.HasVistaAPI) { Win32.SendMessage(Handle, Win32.DTM_CLOSEMONTHCAL, IntPtr.Zero, IntPtr.Zero); } else { // NOTE: SendKeys.Send[Wait] can't be used here! // comctl32 runs its own message loop and awaits for WM_KEYDOWN with VK_ESC. It doesn't dispatch that however, // rather closes the month calendar instead. SendKeys.SendWait calls Application.DoEvents() which // runs message loop and does dispatch WM_KEY**** messages. So the month calendar is not closed. // SendKeys.Send might send input to wrong window (one being activated by mouse). // // HACK: So the only option here is to post WM_KEYDOWN with VK_ESC. // Win32.PostMessage(Handle, Win32.WM_KEYDOWN, Win32.VK_ESC, IntPtr.Zero); } }