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? Не удивительно, что во фреймворке столько говнокода, ведь студента видимо приняли.

How to make CDialog inherit parent background color

Частенько бывает удобно встраивать один диалог в другой в качестве дочернего элемента управления. Все просто - ставим стили:

STYLE DS_CONTROL | WS_CHILD
EXSTYLE WS_EX_CONTROLPARENT

и создаем окно вызовом CDialog::Create.

Одна из проблем - диалог не запрашивает у родителя цвет для своего фона. В результате вложенный диалог другого цвета. Одно из решений - пробрасывать обработку сообщения WM_CTLCOLOR выше по иерархии окон:

struct AFX_CTLCOLOR
{
 HWND hWnd;
 HDC hDC;
 UINT nCtlType;
};
 
afx_msg HBRUSH CFontChooser::OnCtlColor(CDC *pDC, CWnd *pWnd, UINT nCtlColor) {
 if (GetStyle() & WS_CHILD) {
 AFX_CTLCOLOR ctl;
 ctl.hDC = pDC->GetSafeHdc();
 ctl.hWnd = pWnd->GetSafeHwnd();
 ctl.nCtlType = nCtlColor;
 
 return(reinterpret_cast<HBRUSH>(GetParent()->SendMessage(WM_CTLCOLOR, 0, reinterpret_cast<LPARAM>(&ctl))));
 }
 
 return(__super::OnCtlColor(pDC, pWnd, nCtlColor));
}

Проблемка здесь в том, что WM_CTLCOLOR эмулируется средствами MFC. Для этого используется локальная для библиотечного cpp-файла структурка AFX_CTLCOLOR. Которую пришлось сдублировать у себя в коде...

Russian mentality

Вот что такое Россия с точки зрения охраны секретных сведений?

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

Взять хоть эту гостиницу. К каждому приезжающему отношение сверхбдительное: извольте документы на прописку, да по какой надобности изволили прибыть, да нельзя ли получить телеграфное подтверждение от командирующей инстанции. Военный порт, неприступная крепость! Горничная баба Катя в первую же отлучку обшарила весь багаж, аж носовые платки перебрала (это Зепп установил по приставшему к батисту длинному полуседому волосу). То ли тетка в морской контрразведке подрабатывает, то ли просто энтузиастка — под предлогом патриотической бдительности удовлетворяет женское любопытство. И что же? Довольно было угостить пожилую фрау чаем, выслушать рассказ о нелегкой женской доле, самому посетовать на горькую судьбину вдовца с шестью крошками на руках (имелись и фотокарточки крошек, а как же) — и сделалась Катюша верной союзницей. Стала называть «сынком», все секреты выложила — и про полового Мишку, который филер, и про обязательную «лепортацию» о каждом постояльце. А всё потому что Зепп стал для нее свой.

Своим в России можно всё, правила написаны для чужих. Поэтому и законы здесь не более чем условность, удобная для сильных и досадная для слабых. Недаром сакральный лозунг русских — «жить по правде». Но правда-то у каждого своя. Это право, то бишь закон, для всех общий, а общее — оно заведомо не свое.

Именно здесь, на стыке общего и своего, казенного и личного, угадывалась замочная скважина. Только Зепп никак не мог нащупать ее отмычкой.

How to force raw pointer marshaling when doing automatic COM marshaling

Иногда требуется запретить выполнять стандартный маршалинг указателя на интерфейс средствами OLE32. Например, я хочу использовать некий интерфейс IFoo из .NET-а, реализация которого попала в CLR из неуправляемого кода, при этом информации в реестре по этому интерфейсу нет (proxy/stub и tlb не зарегистрированы). Как было неоднократно показано в предыдущих заметках о взаимодействии управляемого и неуправляемого кода, при попытке использовать IFoo из другого потока получим исключение.

Избежать автоматического маршалинга указателя на интерфейс можно, самостоятельно реализовав IMarshal. Но тут кроется еще одна засада - метод GetUnmarshalClass обязан возвратить CLSID класса, который будет выполнять роль proxy. И этот класс должен быть зарегистрирован, что нам явно не подходит.

На помощь приходит функция CoCreateFreeThreadedMarshaler, которая выполняет всю грязную работу по получению необходимой функциональности. Результирующий объект нужно подключить, как tear-off реализацию IMarshal к требуемому классу.

Copyright 2007-2011 Chabster