Вместо введения
Описание утилиты WinRes |
Исходные тексты проекта WinRes ~6KB
Скомпилированный модуль ~4.5KB
Недавно мне пришла в голову простая мысль (удивительно, почему она не посетила меня раньше): зачем моей программе таскать за собой файл манифеста, для того, чтобы использовать новый стиль элементов управления, доступный в WindowsXP? Ведь гораздо проще записать этот самый манифест в виде ресурсов в исполняемый файл. Вроде все очень просто, если писать на C++. Но что делать, если используются языки программирования C# или VB.NET? Кроме того, передо мной встала задача: прочитать из ресурсов данные в формате HTML, а также картинку в GIF-формате. Все бы ничего, но кушать все это дело должен был Обозреватель Web-страниц, который понятия не имеет о ресурсах .NET-приложения, зато знает, как читать HTML из Win32-ресурсов.
Не знаю, зачем и почему, но плохие парни из M$ в очередной раз подложили нам свинью: в программы, написанные на C#, очень проблематично вставлять ресурсы в формате Win32. И вот почему:
Но это не повод, чтобы тут же позорно сдаться, выбросив белый флаг. И я стал искать обходные пути.
Задав вопрос в форумы на нашем сайте (см. здесь), я довольно быстро получил ответ и интересную ссылку (спасибо доброму человеку с ником MaxMP).
Вроде бы, все хорошо, но у утилиты ThemeMe оказалось два крупных недостатка:
В общем, недолго думая, я решил написать небольшую утилиту, позволяющую записывать в файл формата PE пользовательские ресурсы. Так примерно за три часа родилась утилита WinRes.exe, по иронии судьбы написанная на… C#.
Эта утилита не претендует на универсальность и имеет достаточно много ограничений. Но свое дело она делает, и имхо, делает неплохо. Работает утилита только в консольном режиме, принимая параметры через командную строку – это позволяет использовать ее при построении проектов из IDE.
Утилита может использоваться как для добавления, так и для замены уже существующих в файле ресурсов – это обеспечивается используемыми функциями Win32 API. Особенности использования этих функций в программе на C# будут описаны позднее, в разделе Внутреннее устройство.
В этом режиме командная строка задает единственный ресурс, который нужно добавить или изменить. Этот режим наиболее удобно применять в проектах, так как можно задать полный путь как к исполняемому файлу, так и к файлу ресурсов, к тому же можно использовать макросы IDE VS.NET.
Формат строки:
WinRes.exe appfile resfile restype resid |
Параметры командной строки:
Идентификатор | Описание |
---|---|
RT_BITMAP | Растровое изображение |
RT_CURSOR | Курсор |
RT_ICON | Значок |
RT_HTML | Данные в формате HTML |
RT_MANIFEST | Манифест для comctl32.dll версии 6.0 |
RT_RCDATA | Определяемые приложением данные |
Десятичное число | Пользовательский ресурс |
Любой другой идентификатор | Пользовательский ресурс, заданный строкой |
Примеры использования:
$(SolutionDir)WinRes.exe $(SolutionDir)bin\$(OutDir)\$(SolutionName).exe $(SolutionDir)app.manifest RT_MANIFEST 1 $(SolutionDir)WinRes.exe $(SolutionDir)bin\$(OutDir)\$(SolutionName).exe $(SolutionDir)about.html RT_HTML 1 $(SolutionDir)WinRes.exe $(SolutionDir)bin\$(OutDir)\$(SolutionName).exe $(SolutionDir)smile.gif GIF 1 $(SolutionDir)WinRes.exe $(SolutionDir)bin\$(OutDir)\$(SolutionName).exe $(SolutionDir)smile.gif 25 1 |
В этом режиме командная строка содержит имя пакетного файла с описанием ресурсов, которые нужно добавить или изменить.
Формат строки:
WinRes.exe appfile batchfile |
Параметры командной строки:
Пакетный файл представляет собой обычный текстовый файл. В каждой строке этого файла задается один ресурс в следующем виде:
resfile restype resid |
где
Пример использования:
WinRes.exe Desktop.app.exe resources.txt Где файл resources.txt имеет следующий вид: app.manifest RT_MANIFEST 1 "c:\my projects\desktop.app\about.html" RT_HTML 1 smile.gif GIF 1 |
ПРИМЕЧАНИЕ Утилита понимает длинные имена файлов, в том числе содержащие пробелы. Для корректной работы утилиты такие имена надо заключать в двойные кавычки. |
ПРЕДУПРЕЖДЕНИЕ Строковые типы ресурсов с пробелами НЕ допускаются. |
Для того чтобы вставлять ресурсы в исполняемый файл, необходимо иметь возможность при каждой сборке проекта выполнять необходимые действия после создания собственно исполняемого файла, то есть на этапе так называемого PostBuild.
К сожалению, в проектах на C# и VB.NET невозможно задать правила, аналогичные PreBuild и PostBuild правилам "сишных" проектов. Но это ограничение, как и многие другие ограничения VS.NET, можно обойти.
На данный момент мне известно два способа это сделать, оба способа я узнал из документации к утилите ThemeMe.
Все, что нужно сделать в этом случае:
$(SolutionDir)WinRes.exe $(SolutionDir)bin\$(OutDir)\$(SolutionName).exe $(SolutionDir)app.manifest RT_MANIFEST 1 $(SolutionDir)WinRes.exe $(SolutionDir)bin\$(OutDir)\$(SolutionName).exe $(SolutionDir)about.html RT_HTML 1 $(SolutionDir)WinRes.exe $(SolutionDir)bin\$(OutDir)\$(SolutionName).exe $(SolutionDir)smile.gif GIF 1 |
Для использования данного способа необходимо скачать с сайта Microsoft и установить соответствующий Add-in к VisualStudio.NET, после этого появится возможность задавать для проектов C# и VB.NET PreBuild и PostBuild правила. Детально я этот способ рассматривать не буду, так как на данный момент Add-in недоделан и неудобен в использовании, в частности, нет возможности редактировать введенные правила. К счастью, этот Add-in распространяется в исходном виде и при желании его можно привести в человеческий вид.
ПРИМЕЧАНИЕ Скачать Add-in можно здесь: Microsoft Visual Studio .NET Automation Sample: PrePostBuildRules Add-in Кроме того, на странице Automation Samples for Visual Studio.NET доступно большое число других расширений |
Структурно в программе можно выделить три основных блока:
Первые два блока я разбирать не буду: в них нет ничего особенно интересного. Кому интересно: смотрите исходный код, он содержит довольно подробные комментарии. А вот на третьем я остановлюсь подробней.
По большому счету программа представляет собой небольшую обертку для трех вызовов функций WIN32 API, вот прототипы этих функций на языке C:
HANDLE BeginUpdateResource(LPCTSTR pFileName, BOOL bDeleteExisitingResources); BOOL UpdateResource(HANDLE hUpdate, LPCTSTR lpType, LPCTSTR lpName, WORD wLanguage, LPVOID lpData, DWORD cbData); BOOL EndUpdateResource(HANDLE hUpdate, BOOL fDiscard); |
А вот соответствующие им функции на языке C#:
[DllImport("KERNEL32.DLL", EntryPoint="BeginUpdateResourceW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern IntPtr BeginUpdateResource(string pFileName, bool bDeleteExistingResources); [DllImport("KERNEL32.DLL", EntryPoint="UpdateResourceW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool UpdateResource(IntPtr hUpdate, UInt32 pType, UInt32 pName, UInt16 wLanguage, byte[] pData, UInt32 cbData); [DllImport("KERNEL32.DLL", EntryPoint="UpdateResourceW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool UpdateResource(IntPtr hUpdate, string pType, UInt32 pName, UInt16 wLanguage, byte[] pData, UInt32 cbData); [DllImport("KERNEL32.DLL", EntryPoint="EndUpdateResourceW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool EndUpdateResource(IntPtr hUpdate, bool bDiscard); |
Вы можете спросить: почему используются Unicode-версии функций? Ведь это делает невозможным работу утилиты в ОС типа Windows9x? Отвечу: потому что функции xxxUpdateResource существуют только в API WindowsNT/2000/XP и недоступны в API Windows9x. А так как кодировка Unicode является родной для WindowsNT – ее использование предпочтительней. Но для любителей ОС Windows9x есть и хорошая новость: в PlatformSDK включены библиотеки, позволяющие разрабатывать программы в кодировке Unicode для этих систем. Я сам не проверял, но знающие люди утверждают, что вышеописанные функции прекрасно работают и в ОС Windows9x с установленным The Microsoft Layer for Unicode. Узнать о том, что это такое, можно по адресу http://www.microsoft.com/globaldev/articles/mslu_announce.asp, а скачать и узнать, как с этим работать – по адресу http://msdn.microsoft.com/library/default.asp?url=/library/en-us/win9x/unilayer_4e05.asp. Кстати, на нашем сайте опубликована статья Павла Блудова, посвященная этой теме. Статью можно найти здесь: http://www.rsdn.ru/article/?baseserv/uni98.xml. |
Процесс записи ресурсов в файл начинается с вызова BeginUpdateResource. При этом флаг bDeleteExistingResources задает режим записи: с удалением существующих ресурсов или без.
Заканчивается процесс записи вызовом EndUpdateResource. Если флаг bDiscard установлен в TRUE, то запись ресурсов отменяется, в противном случае ресурсы записываются в файл.
Между вызовами этих двух функций можно обновлять ресурсы с помощью функции UpdateResource, причем вызывать ее можно неоднократно.
Внимательный читатель, конечно же, заметил, что в описании прототипов для языка C# функция UpdateResource "размножилась" и существует в двух экземплярах. Почему так? Дело в том, что в языке C# невозможно (или, по крайней мере, я не знаю как) использовать трюки в стиле C: в данном случае - пользоваться макросом MAKEINTRESOURCE для представления числовых значений как строк. Поэтому пришлось писать два прототипа функции: один для ресурсов, тип которых задан числовым идентификатором (например, RT_MANIFEST), другой – для ресурсов, тип которых задан строкой (например, "GIF"). Хотя и это еще не законченное решение: для реализации полного аналога "сишной" функции нужно написать четыре прототипа. Но мне для работы хватает этих двух, думаю, большинству из вас тоже.
Ниже дан листинг законченного приложения на C#, записывающего ресурсы в исполняемый файл формата PE. Для упрощения кода проверка ошибок убрана. Полный рабочий код смотрите в исходных текстах проекта WinRes.
using System; using System.Runtime.InteropServices; using System.IO; namespace WinRes { class MainClass { // Прототипы функций WIN32 API для записи ресурсов в файл формата PE [DllImport("KERNEL32.DLL", EntryPoint="BeginUpdateResourceW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern IntPtr BeginUpdateResource(string pFileName, bool bDeleteExistingResources); [DllImport("KERNEL32.DLL", EntryPoint="UpdateResourceW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool UpdateResource(IntPtr hUpdate, UInt32 pType, UInt32 pName, UInt16 wLanguage, byte[] pData, UInt32 cbData); [DllImport("KERNEL32.DLL", EntryPoint="UpdateResourceW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool UpdateResource(IntPtr hUpdate, string pType, UInt32 pName, UInt16 wLanguage, byte[] pData, UInt32 cbData); [DllImport("KERNEL32.DLL", EntryPoint="EndUpdateResourceW", SetLastError=true, CharSet=CharSet.Unicode, ExactSpelling=true, CallingConvention=CallingConvention.StdCall)] public static extern bool EndUpdateResource(IntPtr hUpdate, bool bDiscard); // Точка входа в программу // В данном примере предполагается, что программа запущена следующим образом: // WinRes.exe PEFile.exe ResFile ResType ResId [STAThread] static void Main(string[] args) { // Начинаем процесс записи ресурсов // IntPtr hUpdate = BeginUpdateResource(args[0], false); if (hUpdate != IntPtr.Zero) { // Здесь можно многократно вызывать UpdateResource для записи нужных ресурсов // Мы запишем один ресурс, переданный в командной строке // Читаем ресурс из файла в буфер using (BinaryReader reader = new BinaryReader(File.OpenRead(args[1]))) { long nCount = new FileInfo(args[1]).Length; byte[] bytes = reader.ReadBytes((int)nCount); reader.Close(); // Записываем ресурс bool bSuccess = UpdateResource(hUpdate, args[2], Convert.ToUInt32(args[3]), 1049, bytes, (UInt32)nCount); if (bSuccess) Console.Write("Ресурс \"{0}\" успешно записан\n", args[1]); else Console.Write("Ошибка при записи ресурса \"{0}\"\n", args[1]); // Заканчиваем запись EndUpdateResource(hUpdate, !bSuccess); } } } }; } |
Вот, собственно и все, что я хотел сказать в данной статье. Как видим, в программах, предназначенных для платформы .NET вполне можно хранить и использовать старые добрые ресурсы Win32.