Модные кнопки для win32 приложений на CL
Aug. 19th, 2011 08:25 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Почему "модные"?
Ну потому, что look-and-feel в стиле Windows 2000 в 2011 году не моден.
Начну издалека.
Стандартные win32-контролы(так называемые Common Controls)(обертками над которыми являются классы MFC, Windows.Forms, или других подобных GUI-тулкитов), начиная с кнопок и заканчивая всякими там TreeView, в винде реализованы в библиотеке comctl32.dll. И с этой библиотекой связан один очень неприятный момент.
Дело в том, что давным-давно, а именно с появлением Windows XP, Microsoft выпустила 6ю версию этой библиотеки, в которую добавила кучу свистелок и перделок("темы" и т.п.), связанных главным образом с отрисовкой(благодаря этому, новые версии винды выглядят не так уныло, как раньше, т.е. во времена Win95-2000). Но, к несчастью, 6я comctl32 стала немножко обратно-несовместимой с предыдущими версиями, и по этой причине, если мы линкуемся с этой библиотекой, или же подружаем ее динамически(через LoadLibrary etc.), винда по дефолту подгружает 5ю версию, и все стандартные виджеты в нашем приложении выглядят так же, как они должны были выглядеть 11 лет назад.
Заставить систему подключать нам как минимум 6ю comctl32 можно через так называемые "контексты активации". К несчастью для динамичного лиспового видения программ, единственный способ задать стандартный контекст активации, или создать новый довольно статичен по своей сути - это так называемые "манифесты". Манифест представляет собой xml-документ, в котором описываются какие-либо свойства приложения/библиотеки(assembly), считываемые Windows при ее загрузке.
Возвращаясь к теме постинга - одними из самых важных вещей, описываемых в манифестах, являются требования к библиотекам и COM-компонентам, от которых данное конкретное assembly зависит, и именно таким образом мы можем сказать системе, что нам нужна как минимум 6я версия библиотеки Common Controls.
Задать манифест для приложения можно двумя способами - первый, и наиболее предпочтительный - вкомпилировать его в секцию "ресурсов" PE, а второй - записать его в файл с именем "имя-приложения.exe.manifest" и положить рядом с исполняемым файлом программы. Со вторым способом связана одна небольшая хитрость - дело в том, что Windows кэширует манифесты такого рода, и это значит, что если программа раньше была без манифеста, и уже когда-нибудь запускалась, а потом нам вдруг приспичило манифест создать, то система не будет о нем знать до того, как мы изменим(обновим дату изменения) исполняемый файл нашей программы.
Я предпочитаю использовать первый способ и собирать рантаймы лиспов с уже включенным в них манифестом.
Как это делается? Ну для примера возьмем SBCL.
Предположим, манифест имеет такой вид:
Запишем его в файл с именем, ну, пусть "program.manifest".
После этого создаем файл описания ресурсов(пусть будет "program.rc"),
и берем утилиту windres из тулчейна MinGW, с помощью которого мы компилируем этот файл:
Далее мы складываем полученный файл в директорию путь_к_сорцам_sbcl/src/runtime/ и немного изменяем файл GNUMakefile, находящийся там же; в частности, строчку
меняем на
Ну и после этого собираем SBCL как обычно(кстати, вот если кому надо, бинарник(x86-32) из HEAD форка
akovalenko, собранный таким образом, с иконкой от lisperati.com: http://rghost.ru/18554241).
Ну и напоследок, вот пример одной и той же программы(http://paste.lisp.org/display/124128) на Windows 7 со включенными Visual Styles(т.е. comctl32 >= 6.0.0.0)(справа) и без них(слева):

Ну потому, что look-and-feel в стиле Windows 2000 в 2011 году не моден.
Начну издалека.
Стандартные win32-контролы(так называемые Common Controls)(обертками над которыми являются классы MFC, Windows.Forms, или других подобных GUI-тулкитов), начиная с кнопок и заканчивая всякими там TreeView, в винде реализованы в библиотеке comctl32.dll. И с этой библиотекой связан один очень неприятный момент.
Дело в том, что давным-давно, а именно с появлением Windows XP, Microsoft выпустила 6ю версию этой библиотеки, в которую добавила кучу свистелок и перделок("темы" и т.п.), связанных главным образом с отрисовкой(благодаря этому, новые версии винды выглядят не так уныло, как раньше, т.е. во времена Win95-2000). Но, к несчастью, 6я comctl32 стала немножко обратно-несовместимой с предыдущими версиями, и по этой причине, если мы линкуемся с этой библиотекой, или же подружаем ее динамически(через LoadLibrary etc.), винда по дефолту подгружает 5ю версию, и все стандартные виджеты в нашем приложении выглядят так же, как они должны были выглядеть 11 лет назад.
Заставить систему подключать нам как минимум 6ю comctl32 можно через так называемые "контексты активации". К несчастью для динамичного лиспового видения программ, единственный способ задать стандартный контекст активации, или создать новый довольно статичен по своей сути - это так называемые "манифесты". Манифест представляет собой xml-документ, в котором описываются какие-либо свойства приложения/библиотеки(assembly), считываемые Windows при ее загрузке.
Возвращаясь к теме постинга - одними из самых важных вещей, описываемых в манифестах, являются требования к библиотекам и COM-компонентам, от которых данное конкретное assembly зависит, и именно таким образом мы можем сказать системе, что нам нужна как минимум 6я версия библиотеки Common Controls.
Задать манифест для приложения можно двумя способами - первый, и наиболее предпочтительный - вкомпилировать его в секцию "ресурсов" PE, а второй - записать его в файл с именем "имя-приложения.exe.manifest" и положить рядом с исполняемым файлом программы. Со вторым способом связана одна небольшая хитрость - дело в том, что Windows кэширует манифесты такого рода, и это значит, что если программа раньше была без манифеста, и уже когда-нибудь запускалась, а потом нам вдруг приспичило манифест создать, то система не будет о нем знать до того, как мы изменим(обновим дату изменения) исполняемый файл нашей программы.
Я предпочитаю использовать первый способ и собирать рантаймы лиспов с уже включенным в них манифестом.
Как это делается? Ну для примера возьмем SBCL.
Предположим, манифест имеет такой вид:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assemblyIdentity type="Win32" name="MyProgram" processorArchitecture="x86" version="1.0.0.0" /> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <security> <requestedPrivileges> <requestedExecutionLevel level="asInvoker" uiAccess="false"/> </requestedPrivileges> </security> </trustInfo> <dependency> <dependentAssembly> <assemblyIdentity type="Win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="x86" publicKeyToken="6595b64144ccf1df" language="*"/> </dependentAssembly> </dependency> </assembly>
Запишем его в файл с именем, ну, пусть "program.manifest".
После этого создаем файл описания ресурсов(пусть будет "program.rc"),
#include <winuser.h> 1 ICON program.ico // application icon CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST program.manifest
и берем утилиту windres из тулчейна MinGW, с помощью которого мы компилируем этот файл:
windres -i program.rc -o resources.res -O coff
Далее мы складываем полученный файл в директорию путь_к_сорцам_sbcl/src/runtime/ и немного изменяем файл GNUMakefile, находящийся там же; в частности, строчку
OBJS = $(C_SRC:.c=.o) $(ASSEM_SRC:.S=.o) ${OS_OBJS}
меняем на
OBJS = $(C_SRC:.c=.o) $(ASSEM_SRC:.S=.o) resources.res ${OS_OBJS}
Ну и после этого собираем SBCL как обычно(кстати, вот если кому надо, бинарник(x86-32) из HEAD форка
![[livejournal.com profile]](https://www.dreamwidth.org/img/external/lj-userinfo.gif)
Ну и напоследок, вот пример одной и той же программы(http://paste.lisp.org/display/124128) на Windows 7 со включенными Visual Styles(т.е. comctl32 >= 6.0.0.0)(справа) и без них(слева):

no subject
Date: 2011-08-19 05:49 pm (UTC)no subject
Date: 2011-08-19 05:55 pm (UTC)2) Там хитрый хак, похоже такой же, какой я видел в каком-то перловом модуле - в отдельную dll'ку суется манифест, этот манифест потом в рантайме вытаскивается, и из него создается контекст. В винформах этот манифест в какой-то из ассемблей BCL, наверное. Ну, а нам в лиспах лепить отдельную dll, и таким макаром извращаться - это еще сложнее, чем вот так вот, как я описал.
no subject
Date: 2011-08-19 07:16 pm (UTC)В том перловом модуле библиотека comctl сначала выгружалась, если она уже загружена, и потом загружалась снова, уже после создания контекста.
У меня вот такое ощущение, что именно поэтому в винформах его можно только "включить", потому что ну, если мы уже использовали какие-то контролы, то им, уже существующим, после такого финта ушами определенно настает жопа. В винформах хэндлы уже существующих контролов м.б. пересоздаются, не знаю, но если да, то это вообще совершенно дикий хак, и его реализация ну слишком накладно выйдет.
no subject
Date: 2011-08-19 07:34 pm (UTC)всмысле, его реализация вышла бы совсем дико накладно, если бы была двусторонняя(т.е. если бы можно было как включить контекст с 6й comctl, так и выключить его).
no subject
Date: 2011-08-19 07:55 pm (UTC)Пересоздаются :)
no subject
Date: 2011-08-19 07:56 pm (UTC)и танцы на хендлах, да.
no subject
Date: 2011-08-19 08:57 pm (UTC)но CL так CL. кроме того что подрубить манифест там еще нужно окнам стили выставлять строковые (сабклассы), что бы контролы получались такими как в Explorer.exe.
Эти обертки тоже надо тогда обязательно понаписывать.
no subject
Date: 2011-08-22 02:57 am (UTC)(Примечание для SBCL: если натравить UpdateResource на исполняемый дамп, он откусит built-in core. Так что работать надо с чистым sbcl.exe, а когда туда засунут манифест, иконки, versioninfo и что ещё захотелось -- тогда уже дампить (примечание 2: распилить исполняемый дамп на runtime+core довольно просто, в том числе из лиспа)).
no subject
Date: 2011-08-29 02:20 pm (UTC)Ну, да, это так. Но я как раз и пишу про "у себя дома", в принципе, ну то есть, как настроить это всё под себя, когда оно надо.
>Примечание для SBCL: если натравить UpdateResource на исполняемый дамп, он откусит built-in core.
Угу, я уже сталкивался с этим.
no subject
Date: 2011-08-29 02:21 pm (UTC)no subject
Date: 2011-08-30 05:21 pm (UTC)В смысле генерации имиджей у нас любопытная хрень только что появилась: возможность указывать комбинированный executable в качестве аргумента --core (оно есть в моих исходниках и прошло в апстрим, но бинарники с этой фичей я пока не собирал). Т.е., например, если у тебя вначале есть большой-развесистый dumped executable, он может скопировать свой исполняемый префикс (искать его длину проще всего через предпоследнее слово embedded core, а не через pe header), потом отредактировать ресурсы в копии, а потом запустить копию, указав в --core текущий *runtime-pathname*, и скомандовать копии, чтобы она сдампилась.
Есть ещё такая идея, кстати: завести ресурс, из которого будет читаться относительный путь к non-embedded core (более приоритетный, чем любая дедукция из SBCL_HOME и из "хвоста" GetModuleFileName). Таким образом можно будет деплоить пару exe+core, которая будет независима от окружения -- и я подозреваю, что в ситуации, когда одним exe дело уже не ограничивается, это не хуже embededded core по удобству (к примеру, если всё равно нужно воткнуть sqlite3.dll рядом -- нет особой разницы, в двух частях едет runtime+core или в одной).
no subject
Date: 2011-08-30 06:04 pm (UTC)Кстати, я тут на днях документировал полезные команды SB-ACLREPL, которые добавляю в "standalone exe" билды (не в msi). Типа, говоришь :qload hunchentoot, и (при необходимости) автоматом качается quicklisp, ставится, притаскивает hunchentoot (жалуясь в процессе на libssl, правда), компилирует его и грузит. Может, сгодится для чего (а если есть идеи по инновациям-модернизациям, они сгодятся мне :).