Практика
Нарезка и склейка файлов

:: Меню ::
:: На главную ::
:: FAQ ::
:: Заметки ::
:: Практика ::
:: Win API ::
:: Проекты ::
:: Скачать ::
:: Секреты ::
:: Ссылки ::

:: Сервис ::
:: Написать ::

:: MVP ::

:: RSS ::

Яндекс.Метрика


Для начала определимся с методом решения. Можно воспользоваться процедурами BlockRead и BlockWrite, а можно использовать поток TFileStream. С моей (субъективной) точки зрения, процедуры вроде BlockRead и BlockWrite морально устарели. По этому я, не колеблясь, выбрал второй вариант.

Рассмотрим основные свойства и методы класса TFileStream, которые пригодятся нам для решения этой задачи.

    TFileStream
  • Create - создает экземпляр потока. Первый параметр - имя файла, с которым нам предстоит работать. В качестве второго параметра передаются флаги, определяющие метод работы с файлом. Я рассмотрю только те флаги, которые нам пригодятся, информацию о других флагах вы можете посмотреть в справочной системе Delphi.
    • fmCreate - создает файл с указанным именем.
    • fmOpenRead - открывает файл на чтение.
    • fmOpenWrite - открывает файл на запись.
  • Position - возвращает текущую позицию в потоке (количество байт от начала данных).
  • Size - возвращает размер потока в байтах.
  • CopyFrom - копирует указанное число байтов из одного потока в другой.
  • Free - Уничтожает объект и освобождает связанную с ним память.
Теперь посмотрим на функцию нарезки файла. Пусть вас не пугает ее размер, все достаточно просто, да и комментариев тоже хватает.

(* Функция для нарезки файла *)
function CutFileStream( inFile, OutPath: string; outSize: Cardinal;
                        PB: TProgressBar = nil ): boolean;
var
  inStream: TFileStream;  // Входной файловый поток
  outStream: TFileStream; // Выходной файловый поток
  posStream: Cardinal;    // Текущая позиция во входном потоке
  fCount: Cardinal;       // Количество выходных файлов
  currentFile: Cardinal;  // Номер текущего выходного файла
  outFileName: string;    // Шаблон выходного имени файла
  longExt: Cardinal;      // Количество цифр в расширении выходного файла
begin
   // Если файл не найден, выходим
   if not FileExists( inFile ) then
   begin
      Result := false;
      Exit;
   end;

   // Выходной файл не может быть больше входного или равен ему
   inStream := TFileStream.Create( inFile, fmOpenRead );

   // Инициализируем переменные
   Result := true;
   posStream := 0;
   currentFile := 1;
   if PB <> nil then
   begin
      PB.Position := 0;
      PB.Max := InStream.Size;
   end;

   // Узнаем количество выходных файлов
   fCount := inStream.Size div outSize;
   if ( inStream.Size mod outSize ) <> 0 then
      Inc( fCount );

   // В этом блоке, зная количество выходных файлов,
   // мы определяем, сколько цифр должно быть
   // в расширении выходного файла. Например:
   //    fCount < 10   => *.#f
   //    fCount < 100  => *.##f
   //    fCount < 1000 => *.###f и т.д.
   longExt := Length( Format( '%d', [fCount] ) );
   if OutPath[Length( OutPath )] <> '\' then
      OutPath := OutPath + '\';
   outFileName := OutPath + ExtractFileName( inFile ) + '.f%.' + IntToStr( longExt ) + 'd';

   // Здесь происходит нарезка файла
   try
      repeat
         OutStream := TFileStream.Create( Format( outFileName, [currentFile] ), fmCreate );
         if ( InStream.Size - posStream ) < outSize then
            posStream := posStream + OutStream.CopyFrom( InStream, InStream.Size - posStream )
         else
            posStream := posStream + OutStream.CopyFrom( InStream, outSize );
         OutStream.Free;
         if PB <> nil then PB.Position := posStream;
         Inc( currentFile );
         Application.ProcessMessages;
      until posStream >= InStream.Size;
   except
      Result := false;
   end;

   if PB <> nil then PB.Position := 0;
   inStream.Free;
end;

Начинаем разбираться. Первым делом проверяем наличие входного файла, и если он не найден, благополучно выходим из процедуры. Создаем экземпляр потока, открываем на чтение файл, который собираемся разрезать. Далее идет инициализация переменных, пропустим это место. Затем идет определение количества выходных файлов, остановлюсь на этом немного подробнее. Номер текущего выходного файла записывается в его расширение, в дальнейшем, по этим номерам файлы будут собраны. Сколько же цифр должно быть в расширении? Допустим 2. Тогда максимальное количество выходных файлов не должно превышать 99. По идее этого вполне достаточно, сложно представить ситуацию, когда придется "крошить" файл более мелко. Но если все же этот предел будет превышен, мы неминуемо попадем в неприятную ситуацию, связанную с повтором номеров. Тогда почему не увеличить количество цифр, скажем до 3. Теперь максимальное количество выходных файлов не должно превышать 999. Запас огромный (хотя его совсем не сложно преодолеть). А если выходных файлов будет мало, то в начале расширения будут присутствовать бесполезные нули, что с моей точки зрения выглядит не очень хорошо. К чему я все это рассказал. С моей точки зрения подобный подход имеет два больших минуса. Во-первых: слишком маленький предел мы задать не можем, велика вероятность выхода за эти пределы, что приведет к печальным последствиям. С другой стороны, большой предел приведет к тому, что в расширении будет много цифр, а при небольшом количестве выходных файлов это будет смотреться нелепо (например *.00001, *.00002 и т.д.). Во-вторых: программа просто лишается универсальности. Именно поэтому я предпочитаю производить подобный расчет для каждого нарезаемого файла. Завершает процедуру непосредственно нарезка файла.

Половина дела сделана. Файл нарезан, теперь осталось собрать все части файла в одно целое. Для начала подготовим список файлов, которые будем объединять. Файлы будем выбирать при помощи OpenDialog1 с включенной опцией ofAllowMultiSelect. Имена всех файлов помещаем в строковый массив tmp. Рассмотрим, как подготавливается этот массив.

var
  Form1: TForm1;
  tmp: array of string;

{...}

(* Открываем файлы, которые нужно склеить *)
procedure TForm1.SelectStickFileClick(Sender: TObject);
var
  i: integer;
begin
   OpenDialog.Options := OpenDialog.Options + [ofAllowMultiSelect];
   if OpenDialog.Execute then
   begin
      // Устанавливаем длинну массива
      SetLength( tmp, OpenDialog.Files.Count );
      // Заполняем массив
      for i := Low( tmp ) to High( tmp ) do
         tmp[i] := OpenDialog.Files.Strings[i];
      // Обязательно сортируем
      Sort( tmp );
      // Выводим результат
      for i := Low( tmp ) to High( tmp ) do
         ListBoxForStick.Items.Add( tmp[i] );
      EditStickDir.Text := ExtractFilePath( OpenDialog.FileName );
   end;
end;

Здесь все просто и понятно, скажу пару слов о сортировке. Несмотря на то, что в открытом диалоге все файлы упорядочены, не факт, что они упорядочены и в OpenDialog1.Files.Strings. Если массив не отсортировать, результат может оказаться непредсказуемым. Не стану приводить здесь описание функции сортировки Sort, вы можете написать ее сами. Разберем функцию склейки файла.

(* Функция для склейки файла *)
function StickFileStream( inFiles: array of string; outDir: string;
                          PB: TProgressBar = nil ): boolean;
var
  inStream: TFileStream;  // Входной файловый поток
  outStream: TFileStream; // Выходной файловый поток
  outFileName: string;    // Шаблон выходного имени файла
  recFirstFile: boolean;  // Признак записи первого файла
  i: integer;             // Цикловая переменная
begin
   // Инициализируем переменные
   if PB <> nil then
   begin
      PB.Position := 0;
      PB.Max := GetSizeAllFiles( inFiles );
   end;
   Result := true;
   recFirstFile := false;
   outFileName := outDir + ExtractFileNameEx( inFiles[0] );

   // Здесь происходит склейка файлов
   try
      for i := Low( inFiles ) to High( inFiles ) do
      begin
         inStream := TFileStream.Create( inFiles[i], fmOpenRead );

         // При записи первой части файла выходной файл создаеитс,
         // при записи всех последующих выходной файл открывается на запись
         if recFirstFile then
            outStream := TFileStream.Create( outFileName, fmOpenWrite )
         else
         begin
            outStream := TFileStream.Create( outFileName, fmCreate );
            recFirstFile := true;
         end;

         outStream.Position := outStream.Size;
         outStream.CopyFrom( inStream, inStream.Size );

         if PB <> nil then
            PB.Position := outStream.Size + inStream.Size;

         outStream.Free;
         inStream.Free;
         Application.ProcessMessages;
      end;
   except
      Result := false;
   end;

   if PB <> nil then PB.Position := 0;
end;

В отличие от стандартной функции ExtractFileName, ExtractFileNameEx (ее код приведен ниже) возвращает имя файла без расширения. Если необходимо показать ход выполнения операции склеивания файлов в ProgressBar1, то функция GetSizeAllFiles (она так же приведена ниже) позволит определить размер конечного файла, а, следовательно, и ProgressBar1.Max.

(* Функция возвращает имя файла без расширения *)
function ExtractFileNameEx( fName: string ): string;
begin
   if FileExists( fName ) then
   begin
      Result := ExtractFileName( fName );
      Delete( Result, LastDelimiter( '.', Result ), Length( Result ) );
   end
   else
      Result := '';
end;

(* Функция находит общий размер склеиваемых файлов *)
function GetSizeAllFiles( var x: array of string ): longint;
var
  i: integer;
  sr: TSearchRec;
begin
   Result := 0;
   for i := Low( x ) to High( x ) do
      if FileExists( x[i] ) then
      begin
         FindFirst( x[i], faAnyFile, sr );
         Result := Result + sr.Size;
         FindClose( sr );
      end;
end;

На сегодня это все. Удачи в программировании.

.: Пример к данной статье :.


При использовании материала - ссылка на сайт обязательна