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
- мы же не все окна хотим насиловать своими глупостями.
Описанное не претендует на конечное решение, код приведен исключительно для понимания механизма, которым была побеждена проблема подмены стандартных полос прокрутки. Возможно, я доведу это все до консистентности и выдам библиотеку, которая позволит элементарно настраивать характеристики и внешний вид скроллбаров любых окон. Но, это будет нескоро.ы