Для создания 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 *)
FOR i := 1 TO numSteps DO
INC(rect.top, 10);
INC(rect.left, 10);
res := WinApi.SetWindowPos(
mainWnd, HWND_TOPMOST, rect.left, rect.top, width, height, WinApi.SWP_SHOWWINDOW
);
IF res = 0 THEN (* leave the loop on error *)
i := numSteps + 1
END
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
Вариант первый
Создание простого исполняемого файла выполняется командой вида
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, только в обратном порядке.
Вариант второй
Возможно также создание «упакованных» файлов, но об этом пока что некому написать в wiki.
Полноценный самостоятельный 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 запись команды получилась существенно длиннее, но
1.
её можно поместить в меню и назначить клавиатурную комбинацию;
2.
её можно вызвать из собственного кода.
Проиллюстрирую последний пункт, создав процедуру для компоновки 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.Pos() < end), т.е. убрать знак равенства. Должно получиться так:
PROCEDURE RemoveWhiteSpaces (rd: TextModels.Reader; end: INTEGER);
BEGIN
WHILE (rd.Pos() < end) & (rd.char <= 20X) DO GetCh(rd) END
END RemoveWhiteSpaces;
Нетрудно догадаться, что так же можно автоматизировать перекомпиляцию и выгрузку модулей, тестирование, сборку дистрибутивов и т.п.