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

Replacing standart scrollbars

Сейчас занимаемся созданием библиотеки элементов управления. UX команда, как обычно, выдумывает ненужный бред, а мы его рисуем. Порой, правда, приходится совсем туго (и даже не от того, что придурки-дизайнеры не думают головой). С одной из тугих проблем я боролся неделю. Проблема зовется заменить стандартные Windows Scrollbars на свои или просто owner draw scrollbars (custom scrollbar drawing). Гугление не дало абсолютно никакого полезного результата! Нашлась лишь одна статья на эту тему (Cool Scrollbars) и неработающий пример. Дальше вы прочитаете мой путь к решению и, конечно же, узнаете само решение.

Постановка задачи

Создать (запрограммировать) окно с полосой прокрутки вовсе несложно и есть два способа решения такой задачи: 1) рисовать на неклиентской (или даже клиентской) области, обрабатывать сообщения и т.д. 2) встраивать дополнительное окно, которое мимицирует поведение полосы прокрутки. Windows поддерживает оба механизма (смотреть There are two types of scrollbars и Scrollbars and Scrolling). Но для обоих случаев логика прокручивания ложится на разработчика и инкапсулируется в элементе управления. А что делать, если исходные коды недоступны, а полосы прокрутки нужно заменить на свои?

Итак, задача следующая - изменить внешний вид полос прокрутки у стандартных элементов управления (LISTBOX, COMBOBOX etc.), т.е. скроллбаров, которые появляются у окошек со стилем WS_VSCROLL или WS_HSCROLL.

Процесс подмены полос прокрутки (исследование проблемы)

Первое, что приходит в голову - убрать стиль WS_VSCROLL (займемся лишь вертикальной прокруткой, горизонтальная ничем не отличается), перехватить WM_NC-сообщения и рисовать свой красивый ползунок с прибамбасами. Все отлично, но есть проблемка: связь полоса прокрутки - элемент управления двунаправленная (скроллбар управляет окном и наоборот). В результате теряется реакция скроллбара на изменение состояния окна (например, активный элемент списка установлен нажатием клавиши END, соответственно, окно прокручено вниз).

Окно управляет состоянием ползунка с помощью метода int SetScrollInfo(HWND hwnd, int fnBar, LPCSCROLLINFO lpsi, BOOL fRedraw). В случае отсутствия стиля WS_VSCROLL SetScrollInfo не вызывается, соответственно, уведомления от окна о необходимости изменить положение ползунка не приходят. В результате стиль WS_VSCROLL обязателен и рисуется стандартный скроллбар. Что дальше? Дальше мы выясняем, что провоцирует отрисовку полосы прокрутки. И стараемся предотвратить возникновение этих ситуаций.

Реакция полос прокрутки связана с обработкой окном WM_NC-сообщений. Это просто и логично. Первое, что я сделал - перехватил эти сообщения, чтобы отключить их стандартную обработку. Я был очень удивлен, когда после этого опять увидел скроллбар!

Дальше я нашел функцию отрисовки скроллбара в библиотеке comctl32.dll и смотрел кто ее дергает. У меня Windows Vista и включен Aero. Вот, что выяснилось в результате:

>  comctl32.dll!DrawThumb2()
   comctl32.dll!xxxDrawThumb()  + 0x57 bytes
   comctl32.dll!_CCSetScrollInfo@16()  - 0x8ef9 bytes
   uxtheme.dll!_ThemeSetScrollInfoProc@16()  + 0x5c12 bytes
   user32.dll!_SetScrollInfo@16()  + 0x3c bytes
   comctl32.dll!ListBox_SetScrollParms()  + 0x336 bytes
   comctl32.dll!ListBox_NewITopEx()  + 0x51 bytes
   comctl32.dll!ListBox_NewITop()  + 0x12 bytes
   comctl32.dll!ListBox_InsureVisible()  - 0x36beb bytes
   comctl32.dll!ListBox_SetISelBase()  + 0x2b bytes
   comctl32.dll!ListBox_KeyInput()  + 0x47b bytes
   comctl32.dll!ListBox_WndProc()  + 0x49b bytes
   user32.dll!_InternalCallWinProc@20()  + 0x23 bytes
   user32.dll!_UserCallWinProcCheckWow@32()  + 0xb3 bytes
   user32.dll!_CallWindowProcAorW@24()  + 0x51 bytes
   user32.dll!_CallWindowProcW@20()  + 0x1b bytes
   ScrollbarsCust.exe!LBWndProc(HWND__ * hWnd=0x00460d0e, unsigned int message=0x00000100, unsigned int wParam=0x00000023, long lParam=0x014f0001)  Line 322   C++
   user32.dll!_InternalCallWinProc@20()  + 0x23 bytes
   user32.dll!_UserCallWinProcCheckWow@32()  + 0xb3 bytes
   user32.dll!_DispatchMessageWorker@8()  + 0xe6 bytes
   user32.dll!_DispatchMessageW@4()  + 0xf bytes
   ScrollbarsCust.exe!wWinMain(HINSTANCE__ * hInstance=0x008a0000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000949b6, int nCmdShow=0x00000001)  Line 95 + 0xb bytes  C++
   ScrollbarsCust.exe!__tmainCRTStartup()  Line 578 + 0x1c bytes  C
   kernel32.dll!@BaseThreadInitThunk@12()  + 0x12 bytes
   ntdll.dll!___RtlUserThreadStart@8()  + 0x27 bytes
   ntdll.dll!__RtlUserThreadStart@8()  + 0x1b bytes

Мы попали в функцию отрисовки после нажатия клавиши END, о чем говорят параметры вызова оконной процедуры ScrollbarsCust.exe!LBWndProc(HWND__ * hWnd=0x00460d0e, unsigned int message=0x00000100, unsigned int wParam=0x00000023, long lParam=0x014f0001). 0x00000100 - это WM_KEYDOWN. В стеке вызовов присутствует также функция SetScrollInfo - user32.dll!_SetScrollInfo@16(). И что самое интересное, именно она провоцирует отрисовку полосы прокрутки!

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

Что делать? Единственный выход, который я увидел, - перехватить вызов функции SetScrollInfo. Продвинутые читатели, возможно, зададут вопрос - почему не перехватить функции отрисовки? Во-первых, адреса этих функций известны только через отладочные символы. Во-вторых, логика отрисовки инкапсулирована в библиотеке comctl32.dll и в других ее версиях этих функций может вовсе не быть. А SetScrollInfo - часть Windows API, который врядли будет меняться в ближайшее время.

Других факторов, провоцирующих отрисовку полос прокрутки я не нашел (WM_NC не в счет - их мы все равно перехватываем и обрабатываем сами). Правда, есть уверенность, что они существуют, просто не проявились.

На этом исследование мое завершилось, пора переходить к кодированию.

Процесс подмены полос прокрутки (кодирование)

Итак, перехватываем SetScrollInfo. Матчасть состоит из одной статьи Process-wide API spying - an ultimate hack и одной функции:

PVOID HookAPI(PBYTE pbModule, PCSTR pszName, PVOID pvOrg, PVOID pvNew) {
    PIMAGE_THUNK_DATA r;
    PIMAGE_NT_HEADERS p;
    PIMAGE_IMPORT_DESCRIPTOR q;

    p = (PIMAGE_NT_HEADERS)(pbModule + ((PIMAGE_DOS_HEADER)pbModule)->e_lfanew);
    q = (PIMAGE_IMPORT_DESCRIPTOR)(pbModule + p->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

    for (; q->Name; q++) {
        if (lstrcmpiA(pszName, (PCSTR)(pbModule + q->Name)) == 0) {
            for (r = (PIMAGE_THUNK_DATA)(pbModule + q->FirstThunk); r->u1.Function; r++) {
                if ((PVOID)r->u1.Function == pvOrg) {
                    WriteProcessMemory(GetCurrentProcess(), &r->u1.Function, &pvNew, sizeof(PVOID), NULL);
                    return(pvOrg);
                }
            }
        }
    }
    return(NULL);
}

Что делаем? Сохраняем адрес оригинальной функции SetScrollInfo и подменяем ее адрес на свою реализацию:

typedef int (WINAPI *SetScrollInfoFun)(HWND hwnd, int fnBar, LPCSCROLLINFO lpsi, BOOL fRedraw);
SetScrollInfoFun originalSetScrollInfo;

int WINAPI MySetScrollInfo(HWND hwnd, int fnBar, LPCSCROLLINFO lpsi, BOOL fRedraw) {
    ...
}
HMODULE user32dllHandle = GetModuleHandle(_T("user32.dll"));
HMODULE comctl32dllHandle = GetModuleHandle(_T("comctl32.dll"));
originalSetScrollInfo = (SetScrollInfoFun)GetProcAddress(user32dllHandle, "SetScrollInfo");
HookAPI((PBYTE)comctl32dllHandle, "user32.dll", originalSetScrollInfo, &MySetScrollInfo);

Здесь, кстати, важно, что мы подменяем функцию только для кода в библиотеке conctl32.dll!

Вообще, для таких вещей существует интересная библиотека Microsoft Detours. Рекомендую взглянуть.

Кульминация спектакля - код функции MySetScrollInfo:

int WINAPI MySetScrollInfo(HWND hwnd, int fnBar, LPCSCROLLINFO lpsi, BOOL fRedraw) {
    int rv = originalSetScrollInfo(hwnd, fnBar, lpsi, /*fRedraw*/FALSE);
    ::SendMessage(hwnd, WM_USER + 1, 0, 0);
    return(rv);
}

Все просто - вызов оригинальной SetScrollInfo с измененным 4-м параметром (мы просим оригинал не рисовать полосу прокрутки, иначе изображение будет мигать). Далее нужно нарисовать свой ползунок с прибамбасами, для чего я посылаю окну специальное сообщение, на которое оно реагирует отрисовкой скроллбара.

Остается написать реакции на WM_NC-сообщения, код отрисовки полос прокрутки, код подмены оконной процедуры интересующего контрола и, наверное, фильтрацию окон в MySetScrollInfo - мы же не все окна хотим насиловать своими глупостями.

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

Visual Studio TR1 debugger features

Только что заметил полезную особенность Visual Studio 2008 с установленным Feature Pack:
Visual Studio 2008 TR1 debugger features
Посмотреть счетчик ссылок в boost - настоящая морока.

Back again to DCOM

Сколько лет уже технологиям COM/DCOM, а я все чаще обращаюсь к документу The Component Object Model Specification. Version 0.9. October 24, 1995. Можно найти ответы на все вопросы. А вопросы иногда возникают жестокие [:-)].

Entity Framework rationale

Традиционные клиент-сервер приложения делегируют операции запроса и сохранения информации системам баз данных. Последние оперируют данными в форме строк и таблиц, а приложения - в рамках высокоуровневых конструкций языка (классы, структуры). Такая нестыковка имеет вполне определенное имя - Object-relational impedance mismatch. С наступлением эпохи SOA, серверов приложений а также многоуровневых приложений, необходимость в службах манипуляций данными, которые тесно интегрированы со средой разработки, крайне возросла.

ADO.NET Entity Framework - это платформа разработки data-driven приложений, которая повышает уровень абстракции с реляционного уровня до концептуального, тем самым существенно уменьшая проблему impedance mismatch.

Мотивация

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

  1. Физическая модель.

    Описывает способ хранения данных в контексте физических ресурсов - память, диски, форматы, индексы и т.д.

  2. Логическая (реляционная) модель.

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

  3. Концептуальная модель.

    Охватывает основные информационные сущности предметной области, а также их связи. Хорошо известная всем (и самая популярная) Entity-Relationship Model была представлена Peter Chen в 1976 году. UML является более свежим примером концептуальной модели. Большинство приложений входят в этап концептуального прототипирования на ранних этапах жизненного цикла приложений. К сожалению, концептуальные модели остаются картинками, которые невозможно совместить с реалиями реализации. Важным достижением Entity Framework является возможность представления концептуальнй модели, как реальной абстракции платформы разработки.

  4. Модель программирования\представления.

    Описывает представление артефактов концептуальной модели. Что-то необходимо преобразовать в объекты языка программирования для реализации бизнес логики, другое нужно передать в виде потока XML и т.д. Короче, необходим гибкий механизм трансформации.

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

Entity Framework - это платформа разработки, которая повышает уровень абстракции с реляционного до концептуального. Она имеет собственный язык запросов, гибкую архитектуру. До некоторой степени концептуальная модель не зависит от реляционной - отображение является независимой внешней единицей метаданных. Получается, можно менять схему базы данных в некоторых границах без аффекта кода приложения. Механизм провайдеров теоретически позволяет использовать любую базу данных, которая удовлетворяет требованиям Entity Framework.

В моей вступительной статье The splendour and misery of Entity Framework designer, architecture and runtime я рассказывал о Entity Data Model, или сокращенно - EDM. Зачем понадобилось изобретать колесо в виде новой концепции моделирования? Было ли действительно изобретено колесо? Почему бы не использовать или расширить одну из существующих концепций моделирования? Кандидаты на эту позицию следующие:

  1. SQL data model.

    Стандарт SQL99 расширяет модель, добавляя объектно-реляционные возможности.

  2. Managed data model (CLR).
  3. XSD data model.
  4. UML data model.

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

Важным аспектом Entity Data Model является ориентированность на данные (как в SQL), а не на объектно-ссылочные концепции (как в C#). Именно поэтому Entity Framework больше тяготеет к ORM нежели к доменной модели приложения. Именно поэтому Entity Framework имеет мощный язык запросов - eSQL. При этом ничто не мешает построить уровень бизнес логики поверх концептуальной модели Entity Framework.

C++ recursive template

Попробуйте угадать результат компиляции и запуска такой программы:

#include "stdafx.h"

template<class T>
struct Z
{
    Z<T*> z;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Z<int> z;
    return 0;
}
Copyright 2007-2011 Chabster