Пишем 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.zip4.63 КБ

Комментарии

2 комментария(ев)
аватар: VAN32
VAN32
Дата: СБ, 27/03/2010 - 05:03
Звание: Наблюдатель
Сообщений: 1

Напишить статю для Borland C++Builder 6, пожалуста Smile:)Smile:)

аватар: Spider_NET
Spider_NET
Дата: Втр, 30/03/2010 - 07:56
Звание: Мастер
Сообщений: 2455

Будет время - напишем Smile