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 - мы же не все окна хотим насиловать своими глупостями.

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

6 коммент.:

antiproton.treshold[a]gmail.com комментирует...

Спасибо за статью! У самого возникла та же проблема... Теперь знаю, что это из-за виндового бага такой геморрой!
Жаль, что ты не приложил какой-нить готовый пример, таким ламерам как я было бы легче разобраться ;) Хотя мне всё-равно под мфц писать...

"Нашлась лишь одна статья на эту тему (Cool Scrollbars) и неработающий пример."
Хм, мне потребовалось совсем немного усилий, чтобы его откомпилить... Работает!

Анонимный комментирует...

Your means of describing all in this paragraph is really good,
all can without difficulty understand it,
Thanks a lot.
My site > GFI Norte

Анонимный комментирует...

My family all the time say that I am wasting my time here at web, but I know I am getting
know-how everyday by reading such nice articles or reviews.
Here is my website GFI Norte

Анонимный комментирует...

А не могли бы вы подсказать, что нужно сделать, чтобы это заработало под x64?

Анонимный комментирует...

А скиньте фотку скроллов, посмотреть что получилось)

ivan комментирует...

Андрей, у Вас получилось создать свой Detour для стандартных скроллбаров?
Если да, то не поделитесь ли кодом, подобно Delphi Detours Library (http://code.google.com/p/delphi-detours-library/)?
Пользовался их исходниками для подмены системного цвета, а как подменить скроллбары с их помощью (и можно ли), не знаю.
Спасибо.

Отправить комментарий

Copyright 2007-2011 Chabster