Как создать программу *.exe
Одним из современных методов разработки ПО на сегодня считается создание программных компонентов, из которых может быть сформирован1) программный комплекс любой сложности. Он постепенно заменяет старую методику создания монолитных программ2).
Отличным примером среды разработки, поддерживающей указанный современный метод, является Инструментальный комплекс «BlackBox», которая сама построена по данной методике. См. также:
- Школьная сборка — полностью русифицированая среда, дополненная набором примеров для начинающих и методическими рекомендациями.
- Пример разработки компонента — игры "Тетрис".
Общие принципы создания exe-файлов
Для создания exe-файлов используется команда DevLinker.LinkExe. Например, самое маленькое приложение будет выглядеть так:
MODULE PrivEmpty; END PrivEmpty. ^Q DevCompiler.Compile ^Q DevLinker.LinkExe Empty.exe := PrivEmpty ~
«^Q» обозначает коммандер. Щёлкнув последовательно по обоим коммандерам, вы получите файл Empty.exe размером 3'584 байт (3.5 Кб). При запуске он сразу же завершится с нулевым кодом выхода.
Этого уже достаточно для того, чтобы писать программы с использованием Windows API, например:
MODULE PrivMoveWindow; (* Simple moving window application by Alexander Iljin, June 08, 2006. *) IMPORT SYSTEM, WinApi; CONST defFontName = 'Verdana'; defMessage = 'Click me' + 0DX + 0AX + 'Esc - exit'; iconId = 1; HWND_TOPMOST = -1; (* this constant is not present in WinApi module *) VAR instance: WinApi.HINSTANCE; mainWnd: WinApi.HWND; defaultBrush: WinApi.HBRUSH; defaultFont: WinApi.HFONT; PROCEDURE MoveMainWindow; CONST numSteps = 50; VAR i, res: INTEGER; rect: WinApi.RECT; left, top, width, height: INTEGER; (* original window parameters *) BEGIN (* remember original window position *) res := WinApi.GetWindowRect(mainWnd, rect); IF res = 0 THEN RETURN END; left := rect.left; top := rect.top; width := rect.right - left; height := rect.bottom - top; (* move window *) i := 0; res := 1; WHILE (i < numSteps) & (res # 0) DO INC(rect.top, 10); INC(rect.left, 10); res := WinApi.SetWindowPos( mainWnd, HWND_TOPMOST, rect.left, rect.top, width, height, WinApi.SWP_SHOWWINDOW ); INC(i); END; (* restore original window position *) res := WinApi.SetWindowPos( mainWnd, HWND_TOPMOST, left, top, width, height, WinApi.SWP_SHOWWINDOW ); END MoveMainWindow; PROCEDURE WndHandler (wnd, msg, wParam, lParam: INTEGER): INTEGER; VAR res: INTEGER; ps: WinApi.PAINTSTRUCT; dc: WinApi.HDC; rect: WinApi.RECT; BEGIN CASE msg OF | WinApi.WM_DESTROY: res := WinApi.DeleteObject(defaultBrush); res := WinApi.DeleteObject(defaultFont); WinApi.PostQuitMessage(0) | WinApi.WM_PAINT: dc := WinApi.BeginPaint(wnd, ps); res := WinApi.SetBkMode(dc, WinApi.TRANSPARENT); res := WinApi.SelectObject(dc, defaultFont); res := WinApi.GetClientRect(wnd, rect); res := WinApi.DrawText( dc, defMessage, -1, rect, WinApi.DT_WORDBREAK + WinApi.DT_CENTER ); res := WinApi.EndPaint(wnd, ps) | WinApi.WM_CHAR: IF wParam = WinApi.VK_ESCAPE THEN WinApi.PostQuitMessage(0) ELSE MoveMainWindow END | WinApi.WM_LBUTTONDOWN: MoveMainWindow ELSE RETURN WinApi.DefWindowProc(wnd, msg, wParam, lParam) END; RETURN 0 END WndHandler; PROCEDURE OpenWindow; VAR class: WinApi.WNDCLASS; res: INTEGER; str: ARRAY LEN(defFontName)+1 OF SHORTCHAR; BEGIN defaultBrush := WinApi.CreateSolidBrush(WinApi.GetSysColor(WinApi.COLOR_BTNFACE)); str := defFontName; defaultFont := WinApi.CreateFont( -20, 0, 0, WinApi.FW_REGULAR, 0, 0, 0, 0, WinApi.DEFAULT_CHARSET, WinApi.OUT_DEFAULT_PRECIS, WinApi.CLIP_DEFAULT_PRECIS, WinApi.DEFAULT_QUALITY, WinApi.DEFAULT_PITCH, SYSTEM.VAL(WinApi.PtrSTR, SYSTEM.ADR(str)) ); class.hCursor := WinApi.LoadCursor(0, SYSTEM.VAL(WinApi.PtrSTR, WinApi.IDC_ARROW)); class.hIcon := WinApi.LoadIcon(instance, SYSTEM.VAL(WinApi.PtrSTR, iconId)); class.lpszMenuName := NIL; class.lpszClassName := "MoveWin"; class.hbrBackground := defaultBrush; class.style := WinApi.CS_VREDRAW + WinApi.CS_HREDRAW; class.hInstance := instance; class.lpfnWndProc := WndHandler; class.cbClsExtra := 0; class.cbWndExtra := 0; res := WinApi.RegisterClass(class); mainWnd := WinApi.CreateWindowEx( WinApi.WS_EX_TOPMOST, "MoveWin", "MoveWin", WinApi.WS_OVERLAPPEDWINDOW, 100, 100, 100, 100, 0, 0, instance, 0 ); res := WinApi.ShowWindow(mainWnd, WinApi.SW_SHOWDEFAULT); res := WinApi.UpdateWindow(mainWnd); END OpenWindow; PROCEDURE MainLoop; VAR msg: WinApi.MSG; res: INTEGER; BEGIN WHILE WinApi.GetMessage(msg, 0, 0, 0) # 0 DO res := WinApi.TranslateMessage(msg); res := WinApi.DispatchMessage(msg); END; WinApi.ExitProcess(msg.wParam) END MainLoop; BEGIN instance := WinApi.GetModuleHandle(NIL); OpenWindow; MainLoop END PrivMoveWindow. ^Q DevCompiler.Compile ^Q DevLinker.LinkExe MoveWin.exe := PrivMoveWindow ~
Полученная программа MoveWin.exe имеет размер 5'120 байт (5 Кб), а при запуске отображает окно с текстом «Click me» и «Esc - exit». При нажатии Esc или Alt+F4 программа завершается. При нажатии любой другой клавиши или щелчке левой кнопкой мыши по окну программы оно быстро перемещается на 50 шагов вправо-вниз, после чего возвращается в первоначальное положение. Обратите внимание, что MoveWin не использует динамическую память, поэтому не требует наличия модуля Kernel. Псевдомодули SYSTEM и WinApi не требуется указывать в команде LinkExe.
Если вы хотите задать иконку для exe-файла, измените команду линковки, например, на такую:
^Q DevLinker.LinkExe MoveWin.exe := PrivMoveWindow 1 applogo.ico ~
Иконка приложения будет взята из файла applogo.ico и включена в состав MoveWin.exe.
При любом варианте линковки требуется перечислить все модули, которые необходимо включить в файл. Это касается не только модулей, решающих конкретную задачу, но и всех импортируемых ими модулей. Модули в списке должны перечисляться в том порядке, в котором они будут загружаться в память. Например, имеется модуль:
MODULE TestExe; IMPORT Log; PROCEDURE Do*; BEGIN Log.String("Test."); Log.Ln END Do; END TestExe.
Список линкуемых модулей должен выглядеть так: Log TestExe. На самом деле, указанный список модулей неполон, потому как модуль Log сам по себе никаких действий не производит. Для того, чтобы эта программа заработала, нужно добавить модуль реализации журнала, например WinConsole: Kernel+ Log Files Dialog Math Strings WinConsole TestExe. Обратите внимание, как распух список при добавлении одного модуля - пришлось перечислить всё, что импортирует он, и всё, что импортируют остальные модули. Знак »+» после модуля Kernel обязателен, и указывает на модуль ядра. Модуль Kernel используется, если в программе создаются объекты с помощью NEW (то есть практически всегда).
Если используется русифицированная (например, школьная) версия Блэкбокса, то первым в списке нужно указывать модуль National
Для автоматического создания списка необходимых модулей можно воспользоваться подсистемами Alm01Gather, Bbt или ert0dev. Для этого их нужно установить.
Вариант первый
Создание простого исполняемого файла выполняется командой вида
DevLinker.LinkExe имяExe-файла := список модулей~
Для нашего примера команда будет выглядеть так:
DevLinker.LinkExe dos test.exe := Kernel+ Log Files Dialog Math Strings WinConsole TestExe~
Параметр dos перед именем файла указывает на необходимость создания консольного приложения. Для обычной программы никаких параметров указывать не надо. Файл будет создан в рабочем каталоге блэкбокса.
Полученная нами программа при запуске не выдаст ничего. Для того, чтобы она заработала, необходимо добавить пару строк:
MODULE TestExe; IMPORT Log; PROCEDURE Do*; BEGIN Log.String("Test."); Log.Ln END Do; BEGIN Do END TestExe.
Это связано с тем, что при исполнении exe, созданного командой DevLinker.LinkExe, выполняются все секции BEGIN всех перечисленных модулей. После того, как последний модуль отработает, вызываются все секции CLOSE, только в обратном порядке.
См. также: Console, модуль ConsoleTest.
Вариант второй
Создание «упакованных» файлов — набор необходимых для работы файлов (произвольного типа) приписывается в конец собранного exe-файла. Во время работы программы эти файлы доступны для чтения при использовании специальной реализации Files — HostPackedFiles.
Обычно придерживаются следующей схемы:
- статически линкуется минимальный exe-файл — стандартный blackbox.exe (содержит динамический загрузчик модулей StdLoader) с добавлением HostPackedFiles;
- кодовые файлы модулей с ресурсами и прочими файлами припаковываются к этому базовому exe;
- при запуске программы она работает как обычный ББ — модули, ресурсы и пр. загружаются динамически. Разница заключается только в расположении файлов.
Припакованные файлы видны «под» файлами из ФС операционной системы: если в ФС и в наборе припакованных файлов содержится файл с одним и тем же именем (и путём), то читается файл из ФС. Эта схема аналогична серверной конфигурации ББ, и при её использовании добавляет «третий уровень» для чтения:
- сначала файл ищется во вторичном (рабочем) каталоге;
- затем в первичном (установочном);
- и, наконец, в наборе припакованных файлов.
Данная схема может быть полезна при распространении и поддержке ПО, если одним из ключевых требований является минимум файлов в рабочем комплекте:
- Базовая версия распространяется в виде одного exe (обычный ББ, к которому припакованы компоненты данного ПО).
- Промежуточные обновления распространяются в виде архивов с набором изменившихся файлов, которые распаковываются рядом с exe и перекрывают старые версии в припакованном наборе. Это позволяет существенно снизить размеры файлов обновлений для «крупного» ПО.
- Только при накоплении большого числа обновлений выпускается новая базовая версия в виде одного exe.
- и т.д.
Команды для создания припакованных файлов описаны в документации модуля DevPacker. Пример использования имеется в следующем разделе.
Полноценный самостоятельный exe-файл на основе BlackBox
В русифицированной школьной версии BlackBox есть подсистема «Тренинг», содержащая диалоговое окно «тренажёра по склонению числительных». Однажды по просьбе А. И. Попкова мною (Александр Ильин) был изготовлен exe-файл, который позволял запустить данный тренажёр без установки BlackBox, т.е. достаточно было получить и запустить файл Training.exe. Тренажёр сотоял из единственного модуля Тренинг\Mod\Chals.odc (фигурирует как «ТренингChals» в параметрах команды LinkExe) и единственной диалоговой формы Тренинг\Rsrc\Chals.odc. Ниже приводится последовательность команд для создания Training.exe (всегда выполняйте все команды, иначе Блэкбокс после перезапуска будет работать неправильно из-за подмены модуля Config, об этом ниже):
^Q DevCompiler.CompileThis ТренингConfigToPack ~ ^Q DevLinker.Link Training.exe := National Kernel$+ Files HostFiles HostPackedFiles Math Strings Dates Meta Dialog Services Fonts Ports Stores Converters Sequencers Models Printers Log Views Controllers Properties Printing Mechanisms Containers Documents Windows StdCFrames Controls StdDialog StdApi StdCmds StdInterpreter HostRegistry HostFonts HostPorts OleData HostMechanisms HostWindows HostPrinters HostClipboard HostCFrames HostDialog HostCmds HostMenus TextModels TextRulers TextSetters TextViews TextControllers TextMappers FormModels FormViews FormControllers StdLinks StdMenuTool Init Config ТренингChals 1 applogo.ico ~ ^Q DevCompiler.CompileThis Config ~ ^Q DevPacker.PackThis Training.exe := "Тренинг/Rsrc/MenuToPack.odc" => "System/Rsrc/Menus.odc" "Тренинг/Rsrc/Chals.odc" ~
Для того, чтобы при запуске Training.exe сразу же отображалось окно тренажёра, перед линковкой выполняется подмена стандартного модуля Config путём компиляции модуля ТренингConfigToPack (см. ниже). После линковки стандартный модуль возвращается на место путём компиляции исходного текста оригинального Config. После этого в созданный Training.exe командой DevPacker.PackThis добавляются два файла ресурсов: диалоговая форма «Тренинг/Rsrc/Chals.odc» и файл меню «Тренинг/Rsrc/MenuToPack.odc» (см. ниже). Последний при упаковке переименовывается в «System/Rsrc/Menus.odc», чтобы заменить собой стандартное меню BlackBox.
Модуль Config, находящийся в файле «Тренинг\Mod\ConfigToPack.odc»:
MODULE Config; IMPORT Dialog; PROCEDURE Setup*; VAR res: INTEGER; BEGIN Dialog.Call("StdCmds.OpenToolDialog('Тренинг/Rsrc/Chals', 'Тренажер по склонению числительных')", "", res) END Setup; END Config.
Файл меню «Тренинг\Rsrc\MenuToPack.odc»:
MENU "Файл" "Открыть тренажёр" "" "StdCmds.OpenToolDialog('Тренинг/Rsrc/Chals', 'Тренажер по склонению числительных')" "" "Закончить работу" "" "HostMenus.Exit" "" END
Полученный файл Training.exe имеет размер 1'014'054 байт, т.е. чуть больше 990 Кб, его можно скачать отсюда: zip-архив, 413'944 байта.
Автоматизация компоновки
Как видно из предыдущего примера, компоновка простого приложения состоит из 4 щелчков мыши по определённой последовательности коммандеров, а ошибка в этой последовательности может обернуться временной потерей работоспособности BlackBox. К счастью, можно свести этот процесс к одному щелчку мыши, или даже нажатию горячей клавиши. Вашему вниманию предлагается следующий модуль:
MODULE PrivMake; (* Copyright (c) Alexander Iljin, June 08, 2006. *) IMPORT DevCompiler, DevLinker, DevCommanders, Dialog, TextModels, TextMappers; PROCEDURE ExecuteCommand*(cmd, param: ARRAY OF CHAR); VAR res: INTEGER; linkText: TextModels.Model; fmt: TextMappers.Formatter; oldPar: DevCommanders.Par; BEGIN linkText := TextModels.dir.New(); fmt.ConnectTo(linkText); fmt.WriteString(param); oldPar := DevCommanders.par; NEW(DevCommanders.par); DevCommanders.par.text := linkText; DevCommanders.par.beg := 0; DevCommanders.par.end := fmt.Pos(); Dialog.Call(cmd, '', res); DevCommanders.par := oldPar END ExecuteCommand; END PrivMake.
Команды, подобные DevCompiler.CompileThis, DevLinker.Link и DevPacker.PackThis, берут параметры для обработки из текста, находящегося сразу после команды, что делает невозможным их использование в меню. Процедура PrivMake.ExecuteCommand создаёт текстовый объект с содержанием параметра «param» и подставляет его в переменную DevCommanders.par, откуда затем и производит чтение команда, находящаяся в параметре «cmd». Например, следующий коммандер:
^Q DevCompiler.CompileThis Config ~
можно записать так:
^Q "PrivMake.ExecuteCommand ('DevCompiler.CompileThis', 'Config')"
Обратите внимание, что завершающий символ «~» здесь не требуется.
С использованием модуля PrivMake запись команды получилась существенно длиннее, но
- её можно поместить в меню и назначить клавиатурную комбинацию;
- её можно вызвать из собственного кода.
Проиллюстрирую последний пункт, создав процедуру для компоновки Training.exe:
MODULE PrivTrainingMake; (* Copyright (c) Alexander Iljin, August 07, 2009. *) IMPORT PrivMake; CONST cmdCompile = 'DevCompiler.CompileThis'; cmdCompileTempConfig = 'ТренингConfigToPack'; cmdCompileRestoreConfig = 'Config'; cmdLink = 'DevLinker.Link'; cmdLinkParam = 'Training.exe := National Kernel$+ Files HostFiles HostPackedFiles Math Strings Dates Meta Dialog' +'Services Fonts Ports Stores Converters Sequencers Models Printers Log Views Controllers Properties Printing' +'Mechanisms Containers Documents Windows StdCFrames Controls StdDialog StdApi StdCmds StdInterpreter' +'HostRegistry HostFonts HostPorts OleData HostMechanisms HostWindows HostPrinters HostClipboard HostCFrames' +'HostDialog HostCmds HostMenus TextModels TextRulers TextSetters TextViews TextControllers TextMappers' +'FormModels FormViews FormControllers StdLinks StdMenuTool Init Config ТренингChals 1 applogo.ico'; cmdPack = 'DevPacker.PackThis'; cmdPackParam = 'Training.exe := "Тренинг/Rsrc/MenuToPack.odc" => "System/Rsrc/Menus.odc" "Тренинг/Rsrc/Chals.odc"'; PROCEDURE Make*; BEGIN PrivMake.ExecuteCommand(cmdCompile, cmdCompileTempConfig); PrivMake.ExecuteCommand(cmdLink, cmdLinkParam); PrivMake.ExecuteCommand(cmdCompile, cmdCompileRestoreConfig); PrivMake.ExecuteCommand(cmdPack, cmdPackParam); END Make; END PrivTrainingMake.
Таким образом, четыре щелчка по коммандерам заменяются на один:
^Q PrivTrainingMake.Make
К сожалению, данный способ не будет работать в стандартном BlackBox 1.5 из-за ошибки в модуле DevPacker, которая приведёт к зацикливанию при разборе параметров команды PackThis, но эту ошибку легко исправить. Достаточно открыть исходный текст модуля DevPacker, найти в нём процедуру RemoveWhiteSpaces и заменить проверку (rd.Pos() <= end) на ~rd.eot & (rd.Pos() <= end). Должно получиться так:
PROCEDURE RemoveWhiteSpaces (rd: TextModels.Reader; end: INTEGER); BEGIN WHILE ~rd.eot & (rd.Pos() <= end) & (rd.char <= 20X) DO GetCh(rd) END END RemoveWhiteSpaces;
Нетрудно догадаться, что так же можно автоматизировать перекомпиляцию и выгрузку модулей, тестирование, сборку дистрибутивов и т.п.
Автор: Ильин А.С.
Авторы*: Горячев И.Н. Правки: Ильин А.С., PGR, Рюмшин Б.В.