Win API
Использование ресурсов при разработке приложений на Win API

:: Меню ::
:: На главную ::
:: FAQ ::
:: Заметки ::
:: Практика ::
:: Win API ::
:: Проекты ::
:: Скачать ::
:: Секреты ::
:: Ссылки ::

:: Сервис ::
:: Написать ::

:: MVP ::

:: RSS ::

Яндекс.Метрика


Программировать интерфейс – это конечно здорово, если разрабатываемое приложение будет иметь мало элементов управления (такое, как например keygen!). В противном случае может оказаться так, что львиную долю кода мы напишем для создания интерфейса (расположение контролов, их выравнивание и т.д.), и лишь несколько строк будут составлять функциональное наполнение программы. В таких случаях целесообразнее использовать ресурсы, о чем и пойдет речь в данной статье.

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

Начнем с простого, создадим форму с несколькими элементами управления, и далее, по ходу дела будем ее дорабатывать и наделять функционалом. Как работать с ресурсами в Delphi я уже писал и повторяться не буду, так что кому интересно, читаем статью "Хранение ресурсов внутри исполняемого файла и их использование".

С чего же начать? Конечно же, с создания ресурса, из которого мы и получим окно. Разберем структуру этого ресурса.

#define IDC_EDT1 1001
#define IDC_STC1 1002
#define IDC_BTN1 1003

DLG_WINDOW DIALOGEX 10,10,216,135
CAPTION "Пример использования ресурсов"
FONT 8,"MS Sans Serif",0,0
STYLE WS_VISIBLE|WS_CAPTION|WS_SYSMENU
EXSTYLE WS_EX_DLGMODALFRAME|WS_EX_APPWINDOW|WS_EX_CLIENTEDGE
{
  CONTROL "",IDC_EDT1,"Edit",WS_CHILDWINDOW|WS_VISIBLE|WS_TABSTOP,6,15,183,15,WS_EX_CLIENTEDGE
  CONTROL "Введите любой текст",IDC_STC1,"Static",WS_CHILDWINDOW|WS_VISIBLE,6,3,75,9
  CONTROL "...",IDC_BTN1,"Button",WS_CHILDWINDOW|WS_VISIBLE|WS_TABSTOP,195,15,15,15
}

Объявляется окно с идентификатором DLG_WINDOW, указываются размеры и положение окна, заголовок, параметры шрифта и стили. Внутри фигурных скобок перечисляются элементы управления, после ключевого слова CONTROL указываем необходимые параметры: текст, идентификатор, класс, стили, позицию и размеры, при необходимости указывается расширенный стиль. Ничего сложного нет, суть очень напоминает программный способ создания элементов управления, разница лишь в синтаксисе. Вместо фигурных скобок допускается использование ключевых слов BEGIN и END.

В качестве идентификаторов не обязательно использовать константы, но мне это представляется более удобным, так что приведенный выше код мог бы выглядеть и так:

DLG_WINDOW DIALOGEX 10,10,216,135
CAPTION "Пример использования ресурсов"
FONT 8,"MS Sans Serif",0,0
STYLE WS_VISIBLE|WS_CAPTION|WS_SYSMENU
EXSTYLE WS_EX_DLGMODALFRAME|WS_EX_APPWINDOW|WS_EX_CLIENTEDGE
BEGIN
  CONTROL "",1001,"Edit",WS_CHILDWINDOW|WS_VISIBLE|WS_TABSTOP,6,15,183,15,WS_EX_CLIENTEDGE
  CONTROL "Введите свое имя",1002,"Static",WS_CHILDWINDOW|WS_VISIBLE,6,3,75,9
  CONTROL "...",1003,"Button",WS_CHILDWINDOW|WS_VISIBLE|WS_TABSTOP,195,15,15,15
END

Половина дела сделана, осталось создать приложение на основе этого ресурса, для чего компилируем его, переходим в Delphi и пишем:

program Project1;

uses
  Windows, Messages;

{$R Dialog.res}

(* Оконная процедура *)
function DlgProc( Wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM ): bool; stdcall;
begin
   case Msg of
      WM_DESTROY, WM_CLOSE: begin
         Result := true;
         PostQuitMessage( 0 );
         Exit;
      end;
   end;
end;

begin
   DialogBox( hInstance, 'DLG_WINDOW', 0, @DlgProc );
end.

Сложно представить себе что то проще, диалоговое окно со всеми элементами управления создается одной стройкой! Все что нужно – указать идентификатор окна и назначить ему оконную процедуру, в которой пока что обрабатывается только сообщение о закрытии программы.

Сейчас программе явно не хватает индивидуальности, которую ей могла бы придать иконка. Для этого создадим еще один файл ресурсов (вообще то можно все писать и в одном файле, но в примере я буду делать отдельные файлы, полагая что это упростит освоение материала).

1 ICON "icon1.ico"
2 ICON "icon2.ico"
3 ICON "icon3.ico"
4 ICON "icon4.ico"
5 ICON "icon5.ico"

Сначала идет идентификатор ресурса, потом его тип, и в конце путь к файлу с иконкой. Кстати, иконка не обязательно должна быть одна (в данном примере их пять), можете предоставить пользователю возможность самому выбрать ту, которая ему больше понравится.

Назначить иконку окну можно в любой момент, но все же лучше сделать это в момент запуска программы, для чего нужно обработать сообщение WM_INITDIALOG:

{$R Icon.res}

{...}

(* Оконная процедура *)
function DlgProc( Wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM ): bool; stdcall;
begin
   case Msg of
      Result := true;
      WM_INITDIALOG: SendMessage( Wnd, WM_SETICON, 1, LoadIcon( HInstance, MAKEINTRESOURCE( 1 ) ) );
      {...}
   end;
end;

Смотрим дальше, и вот незадача, на компе уже давно тусуется Windows 7 (ну или в худшем случае XP), а контролы выглядят как в Windows 2000. Кто ж не такую программу позарится, надо срочно исправлять. Но как? Да проще некуда, добавим в ресурсы манифест, содержание которого будет примерно следующим:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
	version="1.0.0.0"
	processorArchitecture="X86"
	name="Company.Product.Name"
	type="win32"
/>
<description></description>
<dependency>
	<dependentAssembly>
		<assemblyIdentity
			type="win32"
			name="Microsoft.Windows.Common-Controls"
			version="6.0.0.0"
			processorArchitecture="X86"
			publicKeyToken="6595b64144ccf1df"
			language="*"
		/>
	</dependentAssembly>
</dependency>
</assembly>

Создаем файл ресурса:

1 24 "xpmanifest.xml"

Суть содержимого файла ресурса должна быть уже понятна – идентификатор ресурса, его тип и путь к файлу манифеста. Компилируем, и просто подключаем к программе.

{$R Manifest}

Разговор об интерфейсе закончим созданием главного меню. Не будем делать его навороченным, нам ведь главное понять принцип.

#define IDR_MENU1 10000
#define IDM_FILE 10001
#define IDM_EXIT 10002
#define IDM_HELP 10003
#define IDM_PROPERTY 10004

IDR_MENU1 MENU
BEGIN
  POPUP "Файл"
  BEGIN
    MENUITEM "Выход\tCtrl+Q",IDM_EXIT
  END
  POPUP "Помощь"
  BEGIN
    MENUITEM "Свойства файла",IDM_PROPERTY
  END
END

Описание меню смеет иерархическую структуру, в которой пункты главного меню обозначаются ключевым словом POPUP, а элементы выпадающего меню ключевым словом MENUITEM. Давайте подключим это меню к программе.

{$R MainMenu.res}

var
  Menu: HMenu;

(* Оконная процедура *)
function DlgProc( Wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM ): bool; stdcall;
begin
   Result := false;
   case Msg of
      WM_INITDIALOG: begin
         {...}
         Menu := LoadMenu( hInstance, MAKEINTRESOURCE( IDR_MENU1 ) );
         SetMenu( Wnd, Menu );
      end;
      {...}
   end;
end;

Теперь поговорим о функционале, и о том, как работать с элементами пользовательского интерфейса. Нажатие на кнопку или пункт системного меню генерирует сообщение WM_COMMAND, в младшем слове параметра wParam которого содержится идентификатор элемента управления, инициировавшего событие. Попробуем наделить функционалом пункт меню “Выход”.

(* Оконная процедура *)
function DlgProc( Wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM ): bool; stdcall;
begin
   case Msg of
      {...}
      WM_COMMAND: begin
         case LoWord( wParam ) of
            IDM_EXIT: EndDialog( Wnd, 0 );
         end;
      end;
   end;
end;

Вы заметили, что рядом с пунктом меню "Выход" указан акселератор (или, говоря иначе, комбинация горячих клавиш)? Однако пока это всего лишь часть заголовка меню, надо это исправить. К сожалению, хотя горячие клавиши и можно поместить в ресурсы, подключить их у меня не получилось, а дело здесь в следующем. Если бы мы создавали форму не из ресурса, мы сами бы написали для нее цикл обработки сообщений. А так как нажатие "горячих" клавиш не нужно транслировать в символьные сообщения, цикл выглядел бы следующим образом:

var
  hAccelTable: HACCEL;

{...}

hAccelTable := LoadAccelerators( hInstance, MAKEINTRESOURCE( IDR_ACCEL1 ) ) ;

while GetMessage( Msg, 0, 0, 0 ) do
begin
   if TranslateAccelerator( Msg.hwnd, hAccelTable, Msg ) = 0 then
   begin
      TranslateMessage( Msg );
      DispatchMessage( Msg );
   end;
end;

Создавая форму из ресурса мы назначаем ей оконную процедуру, сообщения в которую приходят уже транслированные, и в этом вся загвоздка. Однако ситуация не безвыходная, решение существует, и заключается оно в обработке сообщения WM_MENUCHAR. Получив это сообщение мы проверяем, какие клавиши нажаты в данный момент на клавиатуре, и если нажата нужная нам комбинация, выполняем соответствующее действие. На практике это выглядит так:

(* Оконная процедура *)
function DlgProc( Wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM ): bool; stdcall;
var
  {...}
  State: TKeyboardState;
begin
   Result := false;
   case Msg of
      {...}
      WM_MENUCHAR: begin
         GetKeyboardState( State );
         if ( ( State[Ord( 'Q' )] and 128 ) <> 0 ) and ( ( State[Ord( VK_CONTROL )] and 128 ) <> 0 ) then
         //if ( GetAsyncKeyState( Ord( 'Q' ) ) <> 0 ) and ( ( State[Ord( VK_CONTROL )] and 128 ) <> 0 ) then
         begin
            DlgProc( Wnd, WM_COMMAND, IDM_EXIT, 0 );
            Result := true;
         end;
      end;
      {...}
   end;
end;

Для того чтобы диалоговая форма начала получать сообщение WM_MENUCHAR, меню должно быть раскрыто, или хотя бы активировано, что достигается нажатием клавиши Alt.

Теперь переходим к работе с контролами. Для получения и изменения текста нужно воспользоваться функциями GetDlgItemText и SetDlgItemText. Попробуем получать текст из элемента “Edit” по мере его заполнения и выводить в элемент “Static”.

(* Оконная процедура *)
function DlgProc( Wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM ): bool; stdcall;
var
  p: PChar;
  n: integer;
begin
   Result := false;
   case Msg of
      WM_INITDIALOG: begin
         {...}
         SetDlgItemText( Wnd, IDC_STC2, PWideChar( 'Здравствуйте, ' ) );
      end;
      {...}
      WM_COMMAND: begin
         case LoWord( wParam ) of
            {...}
            IDC_EDT1: begin
               case HiWord( wParam ) of
                  EN_CHANGE: begin
                     Result := true;
                     n := GetWindowTextLength( lParam ) + 1;
                     GetMem( p, n );
                     GetDlgItemText( Wnd, IDC_EDT1, p, n );
                     SetDlgItemText( Wnd, IDC_STC2, PWideChar( 'Здравствуйте, ' + p ) );
                     FreeMem( p, n );
                  end;
               end;
            end;
         end;
      end;
   end;
end;

Так как я делаю пример на Delphi XE, то привожу строку к типу PWideChar, если же у вас версия Delphi ниже 2009, то приводить (здесь и в дальнейшем) следует к типу PChar.

В приведенном примере мы объединяем введенный пользователем текст со строкой, а почему бы эту строку не поместить в ресурс?

STRINGTABLE DISCARDABLE
LANGUAGE LANG_RUSSIAN, 0x1
{
  2000 "Здравствуйте, %s."
}

Приветственное сообщение будем показывать после нажатия на кнопку, код в этом случае будет несколько отличаться от приведенного выше.

(* Оконная процедура *)
function DlgProc( Wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM ): bool; stdcall;
var
  p: PChar;
  n: integer;
begin
   Result := false;
   case Msg of
      {...}
      WM_COMMAND: begin
         case LoWord( wParam ) of
            {...}
            IDC_BTN1: begin
               Result := true;
               n := GetWindowTextLength( GetDlgItem( Wnd, IDC_EDT1 ) ) + 1;
               GetMem( p, n );
               GetDlgItemText( Wnd, IDC_EDT1, p, n );
               MessageBox( Wnd, PWideChar( Format( LoadStr( 2000 ), [p] ) ), 'Приветствие!', 0 );
               FreeMem( p, n );
            end;
         end;
      end;
   end;
end;

Если при обработке сообщения от “Edit” мы получали Handle элемента управления в параметре lParam, то теперь нам нжно получить его самостоятельно, для чего нужно воспользоваться функцией GetDlgItem. Строку загружаем из ресурса, используя функцию LoadStr, передав в нее идентификатор строки.

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

1 VERSIONINFO
FILEVERSION 1,0,0,0    // Версия файла
PRODUCTVERSION 1,0,0,0 // Версия продукта
FILEOS VOS__WINDOWS32  // Операционная система
FILETYPE VFT_APP       // Тип файла
BEGIN
  BLOCK "StringFileInfo"
  BEGIN
    BLOCK "041904E3"  // Блок с информацией на русском языке
    BEGIN
      VALUE "FileVersion", "1.0.0.0\0"
      VALUE "ProductVersion", "1.0.0.0\0"
      VALUE "InternalName", "Project1.exe\0"                     // Внутреннее имя
      VALUE "OriginalFilename", "PROJECT1.EXE\0"                 // Оригинальное имя файла
      VALUE "FileDescription", "Пример работы с ресурсами\0"     // Продукт
      VALUE "ProductName", "Project1\0"
      VALUE "CompanyName", "Decoding™\0"
      VALUE "LegalCopyright", "©Decoding. Все права защищены.\0" // Авторское право
      VALUE "LegalTrademarks", "Торговая марка\0"
      VALUE "Comments", "Комментарий\0"
      VALUE "CustomValue1", "Дополнительное значение 1\0"
      VALUE "CustomValue2", "Дополнительное значение 2\0"
    END
    BLOCK "041904E3"  // Блок с информацией на английском языке
    BEGIN
      VALUE "FileVersion", "1.0.0.0\0"
      VALUE "ProductVersion", "1.0.0.0\0"
      VALUE "InternalName", "Project1.exe\0"                            // Внутреннее имя
      VALUE "OriginalFilename", "PROJECT1.EXE\0"                        // Оригинальное имя файла
      VALUE "FileDescription", "An example of working with resources\0" // Продукт
      VALUE "ProductName", "Project1\0"
      VALUE "CompanyName", "Decoding™\0"
      VALUE "LegalCopyright", "©Decoding. All Rights Reserved.\0"       // Авторское право
      VALUE "LegalTrademarks", "Trademark\0"
      VALUE "Comments", "Comments\0"
      VALUE "CustomValue1", "Additional value 1\0"
      VALUE "CustomValue2", "Additional value 2\0"
    END
  END
  BLOCK "VarFileInfo"
  BEGIN
    VALUE "Translation", 0x0419, 0x04E3, 0x0409, 0x04B0
  END
END

Первым идет идентификатор информационного ресурса, это значение должно равняться 1. Далее следует информация о версии файла, его типе и операционной системе, для которой он предназначен (за подробностями отправляемся в MSDN (ищите по словосочетанию “VERSIONINFO Resource”) – найдете что то вроде этого, этого или этого).

Далее следует блок “StringFileInfo”, который, в свою очередь, включает в себя блоки с информацией о файле. Количество блоков может быть произвольным, в зависимости от того, на какой рынок ориентирована ваша программа. В данном примере присутствует два блока – первый для русифицированных операционных систем, второй – для англоязычных. Имя блока формируется следующим образом: язык + кодовая страница:

041904E3 = 0x0419 (1049, Russian) + 0x04E3 (1251, Cyrillic)
040904B0 = 0x0409 (1033, U.S. English) + 0x04B0 (1200, Unicode)


Далее, в блоке VarFileInfo, мы указываем, поддержку каких языков мы включили в ресурс.

Теперь, если мы откроем свойства файла, мы увидим нашу информацию. Правда не всю, Windows покажет нам только те предопределенные значения, о которых она знает, а именно: Comments, CompanyName, FileDescription, FileVersion, InternalName, LegalCopyright, LegalTrademarks, OriginalFilename, PrivateBuild, ProductName, ProductVersion, SpecialBuild.

Научим нашу программу показывать эту информацию.

function FileVersion( Info: string ): string;
var
  szName: array[0..255] of Char;
  P: Pointer;
  Value: Pointer;
  Len: UINT;
  GetTranslationString: string;
  FFileName: PChar;
  FValid: boolean;
  FSize: DWORD;
  FHandle: DWORD;
  FBuffer: PChar;
begin
   try
      FFileName := StrPCopy( StrAlloc( Length( ParamStr( 0 ) ) + 1 ), ParamStr( 0 ) );
      FValid := False;
      FSize := GetFileVersionInfoSize( FFileName, FHandle );
      if FSize > 0 then
      try
         GetMem( FBuffer, FSize );
         FValid := GetFileVersionInfo( FFileName, FHandle, FSize, FBuffer );
      except
         FValid := False;
         raise;
      end;
      Result := '';
      if FValid then
         VerQueryValue( FBuffer, '\VarFileInfo\Translation', P, Len )
      else
         P := nil;
      if P <> nil then
         GetTranslationString := IntToHex( MakeLong( HiWord( Longint( P^ ) ),
                                           LoWord( Longint( P^ ) ) ), 8 );
      if FValid then
      begin
         StrPCopy( szName, '\StringFileInfo\' + GetTranslationString + '\' + Info );
         if VerQueryValue( FBuffer, szName, Value, Len ) then
            Result := StrPas( PChar( Value ) );
      end;
   finally
      if FBuffer <> nil then
         FreeMem( FBuffer, FSize );
      StrDispose( FFileName );
   end;
end;

(* Оконная процедура *)
function DlgProc( Wnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM ): bool; stdcall;
var
  {...}
  StrMsg: string;
begin
   Result := false;
   case Msg of
      {...}
      WM_COMMAND: begin
         case LoWord( wParam ) of
            {...}
            IDM_PROPERTY: begin
               StrMsg := 'Компания:' + #9#9#9 + FileVersion( 'CompanyName' ) + #13 +
                         'Описание файла:' + #9#9#9 + FileVersion( 'FileDescription' ) + #13 +
                         'Версия файла:' + #9#9#9 + FileVersion( 'FileVersion' ) + #13 +
                         'Внутреннее имя:' + #9#9#9 + FileVersion( 'InternalName ') + #13 +
                         'Авторское право:' + #9#9#9 + FileVersion( 'LegalCopyright' ) + #13 +
                         'Торговая марка:' + #9#9#9 + FileVersion( 'LegalTrademarks' ) + #13 +
                         'Оригинальное имя файла:' + #9#9 + FileVersion( 'OriginalFilename' ) + #13 +
                         'Название продукта:' + #9#9#9 + FileVersion( 'ProductName' ) + #13 +
                         'Версия продукта:' + #9#9#9 + FileVersion( 'ProductVersion' ) + #13 +
                         'Комментарий:' + #9#9#9+ FileVersion( 'Comments' ) + #13 +
                         'Дополнительная информация:' + #9 + FileVersion( 'CustomValue1' ) + #13 +
                         'Дополнительная информация:' + #9 + FileVersion( 'CustomValue2' );
               MessageBox( Wnd, PWideChar( StrMsg ), 'Информация о файле', 0 );
            end;
            {...}
         end;
      end;
   end;
end;

Пара слов о неприятных моментах. Компилируя ресурс утилитой brcc32.exe, входящей в состов BDS 2006 и выше (а возможно и BDS 2005, не проверял), русские символы будут отображаться абракадаброй. В интернете можно найти информацию о том, что в библиотеки rw32core.dll, идущей в комплекте с этими студиями, напрочь отсутствует VERSIONINFO. Звучит это несколько странно, если учесть тот факт, что сама студия без каких либо проблем вставляет в файл этот ресурс, но все же поверим на слово. Решения тут 2 – либо все писать на английском, либо заменить эту библиотеку аналогичной из D6 или D7 (другие не проверял).

Ну, вот и все, рассказал практически все, что хотел, осталось сказать несколько слов о создании файлов с ресурсами. Писать все это в ручную – дело неблагодарное, отнимающее значительную часть времени, мало чем отличающееся от ручного программирования интерфейса. Процесс создания файлов с ресурсами можно и нужно автоматизировать, и я хочу порекомендовать вам программу “Resource editor”. Благодаря визуальному редактору проектировать интерфейс так же просто, как и в Delphi, да и создание ресурсов других типов не потребует от вас много сил и времени, так что пользуйтесь, не стесняйтесь! И не забудьте в настройках на вкладке "Behaviour" установить флажок "Borland compatible".

А я на этом прощаюсь, удачи в программировании.

.: Пример к данной статье :.


При использовании материала - ссылка на сайт обязательна