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

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);
    }
}

SendKeys suck

Недавно пришлось поколупаться в исходном коде класса SendKeys. Его отвратительное качество меня просто поразило. Но самый больший ахуй случился когда я там наткнулся на следующий WTF:

[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] 
private static void LoadSendMethodFromConfig()
{ 
   if (!sendMethod.HasValue) 
   {
       sendMethod = SendMethodTypes.Default; 

       try
       {
           // read SendKeys value from config file, not case sensitive 
           string value = System.Configuration.ConfigurationManager.AppSettings.Get("SendKeys");
 
           if (value.Equals("JournalHook", StringComparison.OrdinalIgnoreCase)) 
               sendMethod = SendMethodTypes.JournalHook;
           else if (value.Equals("SendInput", StringComparison.OrdinalIgnoreCase)) 
               sendMethod = SendMethodTypes.SendInput;
       }
       catch {} // ignore any exceptions to keep existing SendKeys behavior
   } 
}

Если в файле конфигурации приложения отсутствует настройка SendKeys - value.Equals вываливает NullReferenceException, который успешно тушится блоком catch {}. Это пиздец, господа. Класс писал студент в качестве тестового задания для Microsoft? Не удивительно, что во фреймворке столько говнокода, ведь студента видимо приняли.

Copyright 2007-2011 Chabster