Ресурсы Win32, проекты для .NET Framework и как их подружить между собой

Автор: Алифанов Андрей
The RSDN Group

Источник: RSDN Magazine #2
Опубликовано: 25.02.2003
Версия текста: 1.0
Вместо введения
Описание утилиты WinRes
Командная строка
Использование в проектах
Внутреннее устройство

Исходные тексты проекта WinRes ~6KB
Скомпилированный модуль ~4.5KB

Вместо введения

Недавно мне пришла в голову простая мысль (удивительно, почему она не посетила меня раньше): зачем моей программе таскать за собой файл манифеста, для того, чтобы использовать новый стиль элементов управления, доступный в WindowsXP? Ведь гораздо проще записать этот самый манифест в виде ресурсов в исполняемый файл. Вроде все очень просто, если писать на C++. Но что делать, если используются языки программирования C# или VB.NET? Кроме того, передо мной встала задача: прочитать из ресурсов данные в формате HTML, а также картинку в GIF-формате. Все бы ничего, но кушать все это дело должен был Обозреватель Web-страниц, который понятия не имеет о ресурсах .NET-приложения, зато знает, как читать HTML из Win32-ресурсов.

Не знаю, зачем и почему, но плохие парни из M$ в очередной раз подложили нам свинью: в программы, написанные на C#, очень проблематично вставлять ресурсы в формате Win32. И вот почему:

  1. Это можно сделать только из командной строки, задав опцию компилятора /win32res:file, в IDE VS.NET данная опция недоступна. Конечно, можно написать небольшой make-файл и собирать проект с использованием компилятора командной строки. Но зачем тогда вообще использовать RAD-среду?
  2. Теряется возможность управлять версией сборки через AssemblyInfo.cs (по крайней мере, в документации M$ утверждается, что в файл будут вставлены только ресурсы из файла, заданного в вышеозначенной опции).
  3. Нужно иметь скомпилированный файл ресурсов.

Но это не повод, чтобы тут же позорно сдаться, выбросив белый флаг. И я стал искать обходные пути.

Задав вопрос в форумы на нашем сайте (см. здесь), я довольно быстро получил ответ и интересную ссылку (спасибо доброму человеку с ником MaxMP).

Вроде бы, все хорошо, но у утилиты ThemeMe оказалось два крупных недостатка:

В общем, недолго думая, я решил написать небольшую утилиту, позволяющую записывать в файл формата PE пользовательские ресурсы. Так примерно за три часа родилась утилита WinRes.exe, по иронии судьбы написанная на… C#.

Описание утилиты WinRes

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

Дополнительный проект

Все, что нужно сделать в этом случае:

  1. Добавить в текущий проект (solution) новый подпроект: Visual C++ Projects – Makefile Project, назвав его, например, PostBuild
  2. Сконфигурировать проект так, чтобы подпроект PostBuild собирался последним. Для этого нужно открыть диалоговое окно свойств проекта (solution), на закладке Project Dependencies выбрать подпроект PostBuild и указать, что он зависит от основного проекта на C# или VB.NET
  3. Открыть диалоговое окно свойств подпроекта PostBuild и на закладке Nmake ввести нужные команды в строках Build Command Line и Rebuild All Command Line. Это нужно сделать для каждой конфигурации проекта. В моем проекте правила выглядят следующим образом:
$(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

Add-in

Для использования данного способа необходимо скачать с сайта 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.


Эта статья опубликована в журнале RSDN Magazine #2. Информацию о журнале можно найти здесь