Суббота, 26.07.2025, 15:06
Приветствую Вас Гость | RSS

Мир программирования

Каталог статей

Главная » Статьи » Статьи различные » Разные трюки в Delphi

Пишем Joiner

"Как написать Джойнер?”. Если поискать, то на любом программистском форуме можно найти с десяток таких вопросов. Но, как правило, все обитатели форумов приводят только схему работы таких программ, мы же с вами разберём всё очень подробно и, я надеюсь, таких вопросов станет меньше (в идеале – вообще не станет).

Программа-загрузчик
Загрузчик с информацией для "распаковки" файлов
Первая файл
Вторая файл

Вот схема того, что должно содержаться в файле, полученном в результате работы джойнера.

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

Немного теории

Для начала я расскажу, как работать с файлами на WinApi. Если ты знаешь об этом всё, то можешь пропустить эту часть статьи. Для того чтобы читать\писать в файл его необходимо открыть. Для этого используется функция CreateFile.

function CreateFile(lpFileName: PChar; //указатель на строку с именем файла 
dwDesiredAccess, //определяет тип доступа к фалу
dwShareMode: DWORD; //права доступа к файлу другими программами.
lpSecurityAttributes: PSecurityAttributes; //указатель на структуру TSecurityAttributes
dwCreationDisposition, //что нужно сделать с файлом (открыть, создать новый и тд).
dwFlagsAndAttributes: DWORD; //свойство, которое определяет атрибуты файла
hTemplateFile: THandle): THandle;

dwDesiredAccess (тип доступа к файлу) может быть:
GENERIC_WRITE – только для записи
GENERIC_READ – только для чтения
GENERIC_ALL – и чтение, и запись

dwShareMode может быть:

FILE_SHARE_READ - файл доступен другим только для чтения
FILE_SHARE_WRITE – файл доступен только программам для записи

Функция возвращает указатель на файа (THandle) если всё прошло успешно или же возвращает константу INVALID_HANDLE_VALUE. Собственно для самого чтения или записи в файл используется функция WriteFile:

function WriteFile(hFile: THandle; //указатель на открытый файл 
const Buffer;//переменная, которая будет записана в файл.

Как видишь она без типа, что даёт нам возможность писать в файл ЛЮБУЮ информацию.

NumberOfBytesToWrite: Cardinal; //сколько байт из Buffer’а нужно записать в файл

var NumberOfBytesWritten: Cardinal; //сколько было записано на самом деле
lpOverlapped: POverlapped): Cardinal; //указатель на структуру TOverlapped

Если всё прошло успешно, функция возвращает значение, отличное от нуля, если же нет – то она возвращает нуль.

Аналогично выглядит функция ReadFile, поэтому я не буду её здесь расписывать. Но чтобы нам считать из файла, иногда требуется установить позицию в файле. Для этого существует функция SetFilePointer:

function SetFilePointer(hFile: Cardinal; //указатель на открытый файл,

lDistanceToMove: integer; //на сколько двигать позицию в файле
lDistanceToMoveHigh: integer; //тоже, что и предыдущий параметр, но позволяет задавать позицию в фале, размер которого может быть до 2^64 – 2 (2 в 64 степени минус 2)

dwMoveMethod: Cardinal): Cardinal; //относительно какой позиции в файле
//двигать Может быть:

FILE_BEGIN – относительно начала файла,
FILE_END – относительно конца файла
FILE_CURRENT – относительно текущей позиции в файле.

Долгожданная практика

Наша программ будет состоять из двух модулей:

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

Сам заголовок мы будем объявлять в обеих программах следующим образом:

 type 
FRec = record
fname1, fname2: string[30]; //Первый и второй файл
fsize1, fsize2: cardinal; //...и размеры
end;

Почему fsizeX типа Cardinal? Почему не integer? Дело в том, что размер файла не может быть отрицателен, а тип integer поддерживает работу с отрицательными числами.

Вот что у меня получилось, когда я "нарисовал” окно программы-конфигуратора:

Конечно, весь код программ здесь рассматривать бессмысленно, мы рассмотрим лишь основные процедуры обоих программ. Итак, вот код программы-конфигуратора (джойнера), а точнее того, что должно быть написано в обработчике кнопки "Join It!”:

 
var fhDest, fhF: tHandle; //в принципе, можно указывать тип не tHandle, а Cardinal, т.к tHandle объявлен как LongWord, а это тоже самое (занимает столько же места в памяти и имеет такие же границы), что и Cardinal…
b,bw: cardinal;
buf: array [0..1024] of char; //массив для копирования файлов в прогу-загрузчик
fDesc: FRec; //Переменная-заголовок
begin
if not SaveDialog1.Execute then exit;
 
//Копируем файл-загрузчик (из директории Loader)
CopyFile(PChar(ExtractFilePath(Application.ExeName) + 'Loader\project2.exe'), PChar(SaveDialog1.FileName), false);
//здесь "Loader\Project2.exe” – это относительный путь к программе-загрузчику
 
//Т.к CopyFile сразу возвращает управление нашеё программе, то нем необходимо подождать, пока файл скопируется.
 
//В принципе, наш Loader весит всего 15Kb, но если он не успеет скопироваться, то будет не очень хорошо...
 
//Ах да, я совсем забыл, что реализовал функцию GetFS, она возвращает размер файла в байтах, имя которого указано в параметре.
 
while GetFS(ExtractFilePath(Application.ExeName)+'Loader\project2.exe')<>GetFS(SaveDialog1.FileName) do
Application.ProcessMessages;
 
fhDest := CreateFile(PChar(SaveDialog1.FileName), GENERIC_WRITE, 0, nil,
OPEN_EXISTING, 0, 0);
 
//Устанавливаем позицию в файле - 0 байт с конца.
SetFilePointer(fhDest, 0, nil, FILE_END);
 
//Заполняем заголовок...
fDesc.fname1 := ExtractFileName(Edit1.Text);
fDesc.fname2 := ExtractFileName(Edit2.Text);
 
fDesc.fsize1 := GetFS(Edit1.Text);
fDesc.fsize2 := GetFS(Edit2.Text);
 
//И записываем его в файл
WriteFile(fhDest, fDesc, sizeof(fDesc), b, nil);
 
//Открываем первый файл...
fhF := CreateFile(PChar(Edit1.Text), GENERIC_READ, 0, nil, OPEN_EXISTING, 0, 0);
 
//.. и записываем его наш файл.
repeat
ReadFile(fhF, buf, sizeof(buf), b, nil);
WriteFile(fhDest, buf, b, bw, nil);
ZeroMemory(@buf, sizeof(buf)); //Обнуляем наш буфер, это нужно для того, чтобы если файл кончится, в файл в которым мы пишем не попал всякий "мусор”.
until b<>1025;
 
CloseHandle(fhF);
 
//Всё то же самое делаем и со вторым файлом
fhF := CreateFile(PChar(Edit2.Text), GENERIC_READ, 0, nil, OPEN_EXISTING, 0, 0);
 
repeat
ReadFile(fhF, buf, sizeof(buf), b, nil);
WriteFile(fhDest, buf, b, bw, nil);
ZeroMemory(@buf, sizeof(buf));
until b<>1025;
 
CloseHandle(fhF);
CloseHandle(fhDest);
 
MessageBox(handle, 'Joining done! :D', 'yea!', MB_OK+MB_ICONEXCLAMATION);
end;

Надеюсь всё понятно ;) А вот и код программы-загрузчика:

//Здесь fSize – это константа, которая определяет размер скомпилированного файла.

var fhSou, fhDest: tHandle; 
fInfo: FRec; //переменная-заголовок
b, bw: cardinal;
buf: char;
i: integer;
begin
//Открываем сами себя ;) ...
fhSou := CreateFile(PChar(ParamStr(0)), GENERIC_READ, 0, nil, OPEN_EXISTING, 0,
0);
//.. и устанавливаем позицию в файле = нашему размеру
SetFilePointer(fhSou, fsize, nil, FILE_BEGIN);
 
//Считываем заголовок
ReadFile(fhSou, fInfo, sizeof(fInfo), b, nil);
 
//Создаём файл #1...
fhDest := CreateFile(PChar(String(fInfo.fname1)), GENERIC_WRITE, 0, nil, CREATE_NEW, 0, 0);
 
//.. и переписываем из себя в него прграмму #1
for i:=1 to fInfo.fsize1 do
begin
ReadFile(fhSou, buf, sizeof(buf), bw, nil);
WriteFile(fhDest, buf, sizeof(buf), bw, nil);
end;
 
//Закрываем файл #1
CloseHandle(fhDest);
 
//Далее всё тоже самое, только с файлом #2...
fhDest := CreateFile(PChar(String(fInfo.fname2)), GENERIC_WRITE, 0,
CREATE_NEW, 0, 0);
 
for i:=1 to fInfo.fsize2 do
begin
ReadFile(fhSou, buf, sizeof(buf), bw, nil);
WriteFile(fhDest, buf, sizeof(buf), bw, nil);
end;
 
CloseHandle(fhDest);
 
//Закрываем себя
CloseHandle(fhSou);
 
//Запускаем первый и второй файлы...
ShellExecute(0,'open',PChar(String(fInfo.fname1)),'','', SW_SHOWNORMAL);
ShellExecute(0,'open',PChar(String(fInfo.fname2)),'','', SW_SHOWNORMAL);
 
//Можно было бы использовать и WinExec, но у ShellExecute возможностей побольше, да и если
//в uses добавить ShellAPI, то размер EXE-шника не изменится.
end.

Надеюсь и этот участок кода не вызвал затруднений в понимании. Если что-то непонятно, то дам тебе совет: Если то, что тебе непонятно, это какая-то функция или процедура, а может быть константа (типа, GENERIC_WRITE), то выдели непонятное слово и нажми Ctrl+F1. Дело в том, что в справке всё ОЧЕНЬ толково разъяснено, правда, на английском.

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

Ну, это я что-то отвлёкся…

Что можно улучшить в программе? Да ещё много всего! Можно, например,
увеличить число файлов, которые можно объединить (или сделать их неограниченным (ну, конечно не неограниченным, памяти то на всех не хватит :)).

А можно, сделать так, чтобы какие-то программы запускались невидимыми, какие-то видимыми, какие-то вообще не запускались (например, если ты хочешь приджойнить dll-ку, то зачем её запускать?). Можно добавить шифрование данных, чтобы никто не смог разъединить файлы. А ещё можно добавить какой-нибудь алгоритм сжатия и на основе этого джойнера написать инсталлятор :). Как видишь, поле для экспериментов и дальнейшего развития просто таки неограниченное! Благо структура-заголовок позволяет без лишней траты нервов добавлять или убирать свойства. Так что дерзай! Удачи!

Written by: Curve ака Корсунов Артём

Вложение
Joiner_src.zip
Категория: Разные трюки в Delphi | Добавил: VintProg (05.06.2010)
Просмотров: 1876 | Рейтинг: 0.0/0
Всего комментариев: 0
Имя *:
Email *:
Код *:
Категории раздела
Наш опрос
Оцените мой сайт
Всего ответов: 88
Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0
Форма входа
Поиск