Так как на дворе 2018 год, писать просто приложение как-то не очень. Давайте уж соответствовать веяниям времени – установщик будет с поддержкой Hi DPI режимов. Даже в ноутбуках уже 4К экраны не редкость, чего уж говорить про десктопы. Ну и так как установщик — это то, что должно быстро загрузиться будем экономить на том, что действительно не сложно сделать и самому. Ну и попробуем схитрить чтобы использовать векторную графику без дополнительных библиотек – нам же нужен красивый логотип!
Для начала последнее (на сегодня) руководство от Microsoft о том – как же надо писать High DPI Desktop приложения. Из него видно, какая нелёгкая судьба у режима поддержки разных DPI в приложениях под Windows. А уж сколько всяких функций уже добавлено… До Windows 10 система умела только рисовать шрифт в соответствии с DPI, но при этом отступы и прочие размеры жутко “плыли”, а всё остальное просто растягивалось. Этот режим называется XP style DPI scaling. Начиная c Vista – система рисовала приложение в буфер и растягивала уже итоговую картинку, но результат был тоже так себе – всё было размыто. Начиная с Windows 8.1 система стала присылать сообщение об изменении DPI налету, но только главному окну приложения. Только теперь не надо закрывать пользовательскую сессию, чтобы увидеть изменения DPI. И только в Windows 10 система научилась перерисовывать всё, что делается через GDI (текст и common controls) и то не сразу, а после Creators Update, но даже Windows 10 не может всего:
Так что если захочется написать DPI aware приложение, то согласно документу придётся делать UWP – только этот фреймворк поддерживает DPI и всё за вас сам сделает. Всё остальное потребует ручного управления в том или ином виде.
На самом деле нам это особо и не нужно – мы же и так сами всё рисуем. На старте получаем текущий DPI и будем слушать сообщение от системы – чтобы менять DPI налету (если система это поддерживает). Далее просто рисуем окна/компоненты как нам надо с учётом DPI ну и обновляем если пользователь решит изменить DPI во время работы программы или перетащить окно на другой монитор, где DPI отличается. Использовать Сommon Сontrols не получится, они до Windows 10 1703 не умеют реагировать на изменение DPI – так что всё сами.
Писать оконные процедуры в “С” стиле мне не нравится, потому были написаны обёртки на все компоненты — окна и элементы управления. Тут работы-то не много даже с нуля (это если всё заранее знаешь), а уж у каждого WINAPI программиста подобные давно уже есть в запасниках. Что же нам понадобится:
- Что-нибудь абстрактное для окон (windows)
- Что-нибудь абстрактное для элементов управления (controls)
- Текстовый элемент (textbox)
- Кнопка текстовая и с эффектами (button)
- Индикатор прогресса (progress)
- Ссылка (link)
- Флажки с текстом для выбора каких-нибудь настроек (checkbox)
- Хитрый элемент для векторного логотипа
- Ну и плюс разные их сочетания
Рассказывать всю теорию работы окон и элементов управления (это тоже, окна, кстати) в WINAPI тут не к месту. Вкратце примерно так: когда создаётся какой-то GUI элемент – система возвращает его HANDLE(HWND) и он указывает на данные созданного элемента. У элементов и окон есть главная процедура – которая разбирает обращения к элементу и делает (или не делает) то, что от неё хотят. Обращение к элементу идёт через сообщения ( само сообщение и два параметра к нему: wparam и lparam ). Чтобы что-то переделать, переопределяем главную процедуру на свою, обрабатываем нужные нам сообщения и отдаём управление основной процедуре сигнализируя – надо дальше что-то разбирать или мы и так всё сделали. В остальных системах GUI примерно так же устроен — ничего сложного.
Ну вот, теперь вы WINAPI программист. Почти.
Что мы в итоге делаем – главное окно, где можно нажать пару кнопок и выбрать несколько опций. По нажатию кнопки Install показываем модный индикатор прогресса с размытым фоном и работать это всё у нас будет начиная с Windows XP. За основу возьмём диалоговое окно – так проще всего.
Теперь давайте по порядку — что у нас с Hi DPI. Сама windows определяет поддержку DPI (dpi aware) со стороны приложения с помощью манифеста приложения – он с некоторого времени практически обязателен. Помимо DPI в Windows c 8.1, например, изменили и функцию GetVersion — если в манифесте не будет написано, какие системы Вы поддерживаете – GetVersion будет возвращать 6.2 ( будто ваше приложение запущено на Windows 8 на всех системах старше Windows 8 ) или максимально подходящую из списка систем который вы добавите. Так что манифест нужен по многим причинам. Пример манифеста можно найти тут, а все возможные опции манифеста перечислены вот тут.
Наше приложение может иметь 4 варианта работы с DPI:
- DPI Unaware — ничего не знает о DPI (всё будет делать система, но уж как получится)
- System DPI Awareness – берём системный DPI и на старте инициализируем наше приложение под него ( Widows Vista )
- Per-Monitor – берём не системный DPI, а DPI монитора, на котором отображается приложение. Обрабатываем сообщения когда пользователь перетаскивает приложение между разными мониторами с разным DPI, но они приходят только к top level window ( Widows 8.1 )
- Per-Monitor (V2) – тоже что и Per-Monitor, но сообщения система присылает всем нашим окнам. Плюс система сама подстраивает размеры не клиентской части окна, common controls и начальные размеры диалоговых окон созданных через CreateDialog ( Windows 10 начиная с Creators Update сборка 1703 ).
В последних трёх режимах система “убирает руки” от нашего приложения(почти), ничего не трогает и отдаёт нам все координаты и размеры в пикселях не виртуализируя их. Конечно, кроме вышеперечисленных моментов. Для нас лучше всего подойдёт Per-Monitor режим, а возможности Per-Monitor V2 будет только мешать.
Но тут не без проблем, конечно. Так как я люблю делать диалоговые окна в дизайнере Visual Studio – появляется серьёзная проблема. Сама Microsoft предлагает создавать диалоговые приложения вручную и все элементы в окно добавлять тоже вручную – на это я, простите, не подписывался. Проблема в том, что размеры диалогового окна в ресурсах (dialog template) хранятся в DLU (DIALOG UNITS). А текущее значение DLU меняются в зависимости от DPI системы. Получается у нас и DPI и DIALOG UNITS наслаиваются друг на друга. Причём налету они (DIALOG UNITS) не меняются в отличии от DPI и после изменения DPI срабатывают только в новой сессии. Мало того, если использовать функцию GetDialogBaseUnits() и попробовать посчитать изменения – размер поплывёт. Получается вычисление реального размера окна зависит ещё от каких-то параметров, что косвенно подтверждает вот это сообщение. То есть мы не сможем указать начальный и корректный размер на старте приложения. От дизайнера диалогов я отказываться не хочу и так появился код, который приводит начальный размер окна и элементов в окне будто он всегда в 96 DPI, а потом уже мы будем менять его в зависимости от DPI. Пришлось для этого написать обход ресурсов диалога и его элементов. Далее размеры окон/элементов перемножаются на DLU по умолчанию и наше окно на инициализации приводится к этому виду.
А вот теперь всё просто – после корректировки размеров окна и элементов на старте, берём DPI по умолчанию ( 96 ) и делаем начальную инициализацию размера окна под текущий DPI нашего окна, так же поступаем со всеми дочерними элементами в окне. Можно это сделать в один этап, но я оставил в два. А то совсем не понятно будет — что там происходит.
Но надо не забыть что, когда окну приходит сообщение WM_DPICHANGED, первым параметром будет новый DPI, а вторым рекомендуемый размер и положение окна, которое нам присылает система. Если этого не учитывать и считать самому – окно будет шарахаться на пол экрана (ну смотря как реализуете), когда будете его перетаскивать между мониторами с разным DPI.
То есть заниматься размером самого окна — не надо, система нам пришлёт нужный размер. А вот дочерние окна надо обработать (у нас же per-monitor режим версии 1).
Приложении реализовано как одно окно плюс InstallManager с разнообразными общими свойствами и опциями (реализован через singleton). Так в моём проекте гораздо удобнее, а тут тоже вроде не мешает.
Теперь детальное описание файлов проекта. Всё для окон в папке: webinstaller\windows\
Абстрактный, общий класс окон
class AbstractDialog
Базовый класс – удобно шарить основной функционал если окон несколько
class BaseDialog : public AbstractDialog
Обратите внимание на OnEraseBackground, там реализована возможность градиентного фона для окна. Для этого используется WINAPI функция GradientFill.
Если что, то фон можно поменять на обычный в webinstaller\install\installer_constants.h, достаточно убрать объявление GRADIENT_BG.
Конкретное окно – в нашем случае основное
class MainDialog : public BaseDialog
Всякие утилиты разной степени полезности
namespace WindowsUtils
Например RectHolder, который наследуется от структуры RECT и содержит несколько дополнительных методов типа – Width и Height. Иначе очень надоедает писать blablabla.bottom – blablabla.top и т.д.
И замена MulDiv (MultiplyThenDivide), которая не округляет результат, иначе могу вылезать неприятные скачки размеров при небольших значениях (важно для шрифтов).
Да, кстати, всё это частично выдрано из живого проекта, который заметно сложнее этого, так что не обессудьте – всякие странности в наличии. Местами приходилось на лету дописывать нужное взамен убранного и выглядит нелогично. В конце концов это же пример. Ну и загрузка, распаковка, установка сделаны заглушкой – это всё очень легко делается через WinInet функции и к основной теме статьи не очень относится.
А что у нас с элементами управления… По части поддержки DPI — всё просто, это делает окно. Да и остальная реализация тоже не особо сложная.
всё для лежит в папке: webinstaller\ui\
Базовый класс для элементов ui
class BaseControl
Наш static элемент, ну просто текст
class StaticControl : public BaseControl
Что интересного, так как может понадобиться перерисовать элемент без перерисовки окна, а фон у нас может быть с градиентом – приходится при первой отрисовке сохранить фон под элементом и рисовать каждый раз на нём. И там же ещё маленькая недоработка, иногда этот буфер читается, когда окно его не очистило и при перерисовке поверх – текст становится чуть жирнее. Вдруг найдёте как обойти :)
Текст, который ссылка
class LinkControl : public StaticControl
Хитрый элемент, который позволяет добавить и текст и несколько ссылок (нижняя строчка в окне)
class CompositeLinkControl : public LinkControl
Сделано чтобы удобно было писать про соглашение с пользователем и политику конфиденциальности. Если делать отдельными элементами, тогда будет морока с отступами и выравниванием.
Индикатор прогресса
class ProgressControl : public BaseControl
Обычный базовый элемент, который рисует полоску двумя цветами в зависимости от данных. Плюс режим marquee (трудно подобрать какое-то правильное описание по-русски в одно слово). Это когда мы точно не знаем сколько будет длиться операция и просто рисуем короткую полосу, которая по кругу ползает по индикатору. Для этого надо вызывать функцию элемента по таймеру из окна с параметрами 0, 0.
Квадратная кнопка
class RectButtonControl : public LinkControl
Кнопка условно квадратная, если установить цвет конкретно кнопки, то будет фон и края фона будут скруглены и размазаны – для красоты, конечно.
Текст в кнопке может рисоваться с тенью.
Наш хитрый элемент для векторного лого (и т.п.)
class LayeredStaticControl : public StaticControl
Начнём издалека. Так как размер у нас не один будет и не два ( 100%, 125%, 150%, 175%, 200% …), запастись картинками на все разрешения выглядит расточительно, в тоже время логотип обычно векторный. Но ничего популярного Windows не поддерживает кроме EMF, а это по сути — лог вызовов GDI/GDI+ для EMF/EMF+. Вариант неплохой, есть кое какие ограничения, но, если надо что-то достаточно сложное сделать и вы сможете завернуть в EMF+ нужное вам изображение — самое оно. Подкрутить отрисовку для приличного размытия краёв и может будет даже неплохо. Нужно только использовать GDI+ 1.1 ну и минимум это Windows Vista (суть в функции Metafile.ConvertToEmfPlus). Обычный EMF не сделает размытия краёв (antialiasing).
Если не использовать GDI+, тогда остаётся только один векторный компонент Windows – это отрисовка шрифтов. А поскольку она осуществляется одним цветом – придётся разбирать логотип на цвета.
Вот от того это и layered_static_control. Берётся несколько символов и рисуется последовательно один на другой с нужными цветами. На самом деле начертания символов и так составные в шрифте – так что тут ничего особо нового. Разве что мы этот механизм не сможем использовать, так как он внутри компонента и цвет поменять нельзя.
Для примера я взял SVG мыши из Twitter’a, разобрал на 4 части по количеству цветов, взял шрифт OpenSans и положил их туда вместо символов скобок подправив совсем чуть-чуть. Всё это делается за полчаса в удобном редакторе с открытым исходным кодом — FontForge. Заодно у нас будет симпатичный шрифт в окне.
Я загрузил SVG в Illustrator, выгрузил в SVG же уже по частям. Загрузил в FontForge и в самом FontForge подправил чтобы контуры замыкались. Для этого надо вызвать проверку ошибок элемента в FontForge: Элемент -> Проверка ошибок -> Контуры -> Открытые контуры. После проверки он подсветит нужную точку и надо просто схватить, подвинуть и положить на тоже место – он сам замкнёт этот контур.
Далее два важных шага: не забыть выставить символам одинаковые размеры/метрики (тем, которые мы используем для слоёв) и поменять имя шрифта (его свойства в FontForge) иначе, когда мы попробуем его загрузить уже в приложение, может оказаться, что он уже установлен в системе и будет использоваться системный (оригинальный).
Кстати, раз мы место экономим – можно удалить все неиспользуемые символы из шрифта, он станет тогда совсем маленьким ( русские + английские буквы + цифры и знаки — 29кб). Главное осторожно, многие символы в шрифте – составные. Верхняя чёрточка в букве й, например, находится в самом конце шрифта OpenSans и видна как отдельный элемент.
В сам элемент мы добавляем слои и их цвет – всё просто.
Части логотипа/символы можно рисовать с градиентами – для этого надо задействовать GDI+, там есть градиентные кисти. Надо сделать только новую реализацию StaticControl и LayeredStaticControl под GDI+.
Кстати, кнопка закрытия окна всё же не совсем похожа на букву Х, поэтому тоже сделана отдельно в шрифте ( вместо \ ).
Элемент для выбора опций и настроек (checkbox)
class CompositeCheckboxControl : public CompositeLinkControl
Тут тоже без вектора не обошлось, надо же как-то рисовать галку и всё остальное. Как я писал ранее — перерисовывать common controls может только Windows 10.1703. Всё аналогично примеру с мышью в логотипе, нужна будет рамка, чистый фон и сам флажок.
По части функционала, если Вы действительно захотите установить что-то со своей программой ( а вдруг ) или упомянуть что-то особенно важное – понадобится какая-то ссылка на соглашение отдельно. Это тут предусмотрено как отдельный элемент.
Элемент, для выбора который сам меняет статус выбранной опции в InstallManager – в остальном полностью наследует CompositeCheckboxControl
class OptionCheckboxControl : public CompositeCheckboxControl
Манифест приложения
webinstaller\installer\install.manifest
Обратите внимание на строку:
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True/PM</dpiAware>
Именно этим мы говорим системе, что поддерживаем режим DPI per Monitor. Все остальные DPI опции перечислены тут.
Синглтон менеджер
class InstallerManager
Различные системные функции (обёртки)
namespace SystemUtils
Из ресурсов у нас только шрифт и иконка
webinstaller\installer\resources\webinstaller.ttf
webinstaller\installer\resources\webinstaller.ico
Ну вот в общем-то и всё. 160кб на всё про всё (конечно без загрузки из сети, но это несколько кб от силы).
Что у нас в итоге по части DPI в WINAPI приложениях:
Если не рисовать все элементы окна самому — тогда можно переложить почти все проблемы с отрисовкой на Windows, но только начиная с Windows 10.1703. Решать проблемы в этом случае надо только с растровой графикой. Достаточно добавить в манифест приложения специальную опцию — gdiscaling и установить её в true. Тут подробно описано — как это работает. Конечно, если вы используете для отрисовки GDI или ваш фреймворк использует GDI.
Если не добавлять наборы растровых картинок в приложение — можно взять размером побольше и просто уменьшать через GDI+ под нужное разрешение — там есть достаточно приличный алгоритм интерполяции. Сама Microsoft его и советует.
В целом получается или делаем сами, или оставляем на откуп системе. Тем более Microsoft готовит новую опцию в Spring Creators Update — разрешить Windows исправлять размытость приложений. Как она работает пока не ясно, но скорее всего принудительно включает GDI scaling.
А, ну и конечно репозитарий с приложением на github.
Комментарии (12)
SOLON7
09.04.2018 17:401.Кажется все забыли про upx
ru.wikipedia.org/wiki/UPX
2.Мсье знает толк в извращениях.
3.Обычно Windows ос следующего поколения включают компоненты msvcrt.dll на которых написаны системные компоненты!antonn
10.04.2018 08:40Кажется все забыли какие это костыли — UPX, и почему о нем надо забыть. И речь не только про параноидальные антивирусы.
Достаточно того, что обычный exe отражен в память и ОС может по необходимости считывать его с диска (именно поэтому файл блокируется для удаления будучи запущенным). Это экономит использование ОЗУ, в том числе и при нескольких запущенных копиях программы (образ — один). При использовании UPX все это выкидывается, образ больше занимает памяти, несколько запущенных копий съедают еще больше.
PS Когда ресурсов в программе много, то может быть эффективнее подключить zlib и пожать их, распаковывая налету. Так для растровых картинок программы с ресурсами на 16Мб ужимал exe до 400кб (скринсейвер типа).
Einherjar
Кошмар какой. В соседней теме вон вообще 3д сцены в 64 кб умещают…
Я как то раз тоже веб-инсталлер писал на чистом апи, получилось около 70кб, 3/4 из которых которых были иконка (6 в одном ico от 16х16 до 256х256) (чтобы нормально отображать в проводнике в режиме крупных значков) и лицензионное соглашение. Для создания компактных приложений первое от чего следует отказаться это линковка на статические библиотеки crt и прочий подобный мусор.
crea7or Автор
Совсем от всего не отказаться, придётся брать какой-нибудь CRT tiny и долго его допиливать. И времени это займёт прилично. Как вариант можно попробовать собрать в VS 6.0 минимизировав использование std и убрать все c++ 11 фишки. Там можно будет выбрать динамическую линковку к msvcrt.dll — она с Windows XP в системе есть.
Einherjar
В crt нет абсолютно ничего жизненно необходимого для однокнопочного веб инсталлера, отказаться элементарно, сторонние альтернативы не нужны, ничего допиливать не надо, и никаких неудобств, было бы желание. C++ тут тоже не нужен, ни 11 ни какой либо еще, чистый C является идеальным выбором. Другой вопрос что кроме как ради спортивного интереса в этом практический пользы действительно нет.
danfe
Сравнение с интро и демками выглядит странно — задача ведь не
победить в специальной олимпиадепоразить всех минимальным размером, а реализовать необходимый функционал, оставаясь в рамках WinAPI. Размеры многих современных программ (в т.ч. инсталлеров) больше на порядки, на этом фоне разница между 160 и 70 килобайтами незаметна.(Disclaimer: я не люблю венду и WinAPI, считаю его уродливым и перегруженным, но он позволяет сделать очень многое сам по себе, и это очень круто. В юниксах, при всех их прочих достоинствах, практически сразу встает вопрос выбора хотя бы из-за отсутствия нативного гуи-тулкита.)
Насчет использования C++ (да хоть бы и последних версий) в стендалон-приложениях, распространяющихся в исполняемом виде (в отличие от популярных опенсорсных библиотек, которые часто нужно собирать устаревшими компиляторами или линковать с имеющимся legacy-кодом) можно не переживать; обернуть графические примитивы WinAPI в иерархию классов — вполне естественное желание.
Статья порадовала, автор молодец. Такими и должны быть программы — легкими и компактными, не стесняться пользоваться нативными возможностями операционных систем вместо того, чтобы тянуть за собой веб-браузер по любому поводу.
Antervis
В никсах есть пакетные менеджеры. В принципе, попытки натянуть устаревшие win концепции на linux это распространенная ошибка.
sumanai
В итоге в KDE куча пакетов от Gnome, в Gnome от KDE, выглядит это сборной солянкой и занимает лишнее место. Я тут конечно утрировал, но суть примерно такая.
danfe
Пакетные менеджеры, конечно, рулез, но я совсем про другое: в венде, если я хочу написать простое (или не очень) графическое приложение, долго раздумывать не приходится: WinAPI в зубы и вперед; там не нужно выбирать, какой пакет ставить: Gtk+, Qt или, например, FLTK.
WinAPI предоставляет удобный baseline: стандартную графику, работу со звуком и прочие компоненты. Юниксы, конечно, намного более удобная и приятная среда для разработки вообще, но так они не умеют: приходится определяться с графическим стеком, звуковой подсистемой, учитывать особенности разных версий месы и прочих ксоргов, конкретных реализаций libc (например, гнутой vs. BSD), помнить про кучу специфических вещей типа udev, которые есть в Linux, но нет в других юниксах, и которые влияют на диспетчеризацию событий, определение оборудования, и т.д.
Программа на WinAPI, написанная для условной WinNT, скорее всего заработает и в условной десятке. Про фрюниксы так сказать, к сожалению, нельзя.
Antervis
да и я совсем про другое: в линуксе конкретно инсталляторы писать вообще не надо. Но разрабы не заморачиваются и переносят софт один в один, а потом героически сражаются с проблемами типа конфликтов версий и путей.
Бтв, я б завязался на Qt. Всё равно популярные графические оболочки linux от него зависят