"Как написать Джойнер?”. Если поискать, то на любом
программистском форуме можно найти с десяток таких вопросов. Но, как
правило, все обитатели форумов приводят только схему работы таких
программ, мы же с вами разберём всё очень подробно и, я надеюсь, таких
вопросов станет меньше (в идеале – вообще не станет).
Программа-загрузчик
Загрузчик с информацией для "распаковки" файлов
Первая файл
Вторая файл
Вот схема того, что должно содержаться в файле, полученном в
результате работы джойнера.
Программа-загрузчик – это программа, которая должна считать из себя
заголовок с информацией для распаковки файлов (в нём будет
содержаться размер файлов, которые нужно распаковать и их имена),
распаковать их и запустить.
Немного теории
Для начала я расскажу, как работать с файлами на 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
|