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. Можно найти ответы на все вопросы. А вопросы иногда возникают жестокие [:-)].

Copyright 2007-2011 Chabster