Чудеса TStringList. Почему надо использовать объекты TStringList везде.

Чудеса TStringList

Object Pascal в сочетании с ассемблером в современной его форме Delphi 5/6/7 предоставляет неограниченные возможности для полета мысли программиста, и этой статьей мы откроем серию, в которой последовательно будем это демонстрировать.

Начнем мы с простого, крайне полезного, но мало известного, по крайней мере, для начинающих, стандартного класса Дельфи TStringList. Сейчас мы посмотрим, как красиво решаются типичные задачи при помощи этого класса.

Для начала в двух словах, какие такие замечательные свойства есть у объектов данного класса. TStringList - это класс, предназначенный для хранения списка строк и списка объектов с текстовым представлением (прямо как в 1С - это СписокЗначений). Кроме того, этот список может быть отсортирован по алфавиту или при помощи сравнительной функции, написанной программистом. Кроме того, этот список может быть интерпретирован как список значений (Name=string). Кроме того, этот список может быть сохранен в файл или поток, преобразован в непрерывную строку или строку, разделенную запятыми. Физически получаемая строка или поток представляет собой обычный текст, в котором строки разделены символами CR/LF (стандартный Windows Text File), или запятыми (CommaText, Excel). Ну и само собой, есть возможность загружать список строк из файла, потока, строки и строки, разделенной запятыми. Следует особо отметить замечательное свойство упаковки в строку, разделенную запятыми: при обратной распаковке строки всегда восстанавливаются в их исходном виде. Это означает, что допустима многократная вложенность строк, разделенных запятыми, дающая огромный выигрыш при упаковке/распаковке многомерных структурных данных в текстовый формат, что мы и продемонстрируем во второй задаче.

Итак, для начала рассмотрим список строк как список объектов с текстовым представлением, т. к. именно в данном ключе следует использовать список строк в реальных приложениях. Что из себя представляет объект с текстовым представлением? Это может быть, например, список товаров, имеющих помимо наименования еще и дополнительные параметры типа единицы измерения, количества в упаковке и цены. Итак, имеем предопределение типов:

type 
  TEdIzm=class
    public
      Name:string;
      Weight:double;
    end;
  TTovar=class
    public
      Name:string; //наименование
      EdIzm:TEdIzm; //ед. изм.
      CountUp:integer; //кол-во в упаковке
      PriceOut:currency; //цена продажная
    end;
var
  TovarList:TStringList;

Набор объектов TTovar - это классический справочник однородных товаров, например, хлебобулочных изделий. Поле Weight в классе TEdIzm требуется для перевода одних единиц в другие. Вернемся к нашим булкам. Допустим, поставщик "Карякинский Хлебозавод" предоставил нам текстовый файл, в котором находится информация о его новой продукции и отпускных ценах в формате текстового файла:

*Начало файла*
Товар1=[наименование товара]
ЕдИзм1=[наименование ед.изм.]
Вес1=[вес единицы измерения]
КолУп1=[количество в упаковке]
Цена1=[цена поставщика]
--
Товар2=[наименование товара]
ЕдИзм2=[наименование ед.изм.]
Вес2=[вес единицы измерения]
КолУп2=[количество в упаковке]
Цена2=[цена поставщика]
--
*Конец файла*

Всего в файле содержится, например, 2000 наименований. Наша задача - загрузить данные этого файла в список строк ListBox1:TListBox (лежащий на форме) в отсортированном виде так, чтобы была возможность по двойному щелчку просмотреть параметры каждого товара (для этого к каждой строке будет прикреплен объект типа TTovar).

Если решать эту задачу в лоб (как чтение построчно и разбор текстового файла), а так же напрямую добавлять в ListBox, то это обернется неэффективной работой компьютера, его подтормаживанием (на слабых машинах), и вообще, для дальнейших внесений изменений в программный код это решение не является лучшим. Гораздо эффективнее сделать "финт ушами", а именно, создать TStringList, загрузить в него исходный файл, создать второй TStringList, загрузить в него товары, отсортировать их, и в конце концов, присвоить свойству ListBox.Items:

function LoadTovary(filename:string):TStrings; 
var str:TStringList;
    tovar:TTovar;
    edizm:TEdIzm;
    I:integer;
begin
  //загружаем файл с данными
  str:=TStringList.Create;
  str.LoadFromFile(filename);
  //посчитаем, сколько будет товаров
  result:=TStringList.Create; // TStrings являедся предком TStringList, поэтому данное присвоение корректно
  result.Capacity:=str.count div 6;
  for I:=0 to result.capacity do
  begin
    tovar:=TTovar.Create;
    edizm:=TEdIzm.Create;
    tovar.Name:=str.Values['Товар'+inttostr(i)];
    edizm.Name:=str.Values['ЕдИзм'+inttostr(i)];
    edizm.Weight:=strtofloat(str.Values['Вес'+inttostr(i)]);
    tovar.CountUp:=strtoint(str.Values['КолУп'+inttostr(i)]);
    tovar.PriceOut:=strtoint(str.Values['Цена'+inttostr(i)]);
    tovar.EdIzm:=edizm;
    result.AddObject(tovar.Name,tovar);
  end;
  result.Sort;
end;

...
ListBox1.Items:=LoadTovary('fromhlzd.txt');
...

Заметим, что если написать функцию типа TStringListSortCompare, то можно будет сортировать не только по текстовому представлению, но и по любым другим признакам, например, цене:

function StringListComparePrice(List: TStringList; Index1, Index2: Integer): Integer;
begin
  Result := TTovar(List.Objects[Index1]).PriceOut-TTovar(List.Objects[Index2]).PriceOut;
end;

...
result.CustomSort(StringListComparePrice);
...

Ну и в конце концов, продемонстрируем реакцию на двойное нажатие на получившемся списке товаров. По двойному щелчку мы покажем отпускную цену товара:

procedure TForm1.ListBox1DblClick(Sender: TObject);
begin
  showmessage('Цена поставщика '+
floattostr(TTovar(TListBox(sender).Items.Objects[TListBox(sender).ItemIndex]).PriceOut)
  );
end;

Кстати, не забываем освобождать системные ресурсы объектов перед удалением строк методами ListBox1.Items.Delete()/Clear() или str.Delete()/Clear() (str:TStringList), если это требуется, а так же при закрытии приложения:

procedure TForm1.FormDestroy(Sender: TObject);
var i:integer;
begin
  for i:=0 to ListBox1.Items.Count-1 do
  with TTovar(ListBox1.Items.Objects[i]) do
  begin
    EdIzm.Free;
    Free;
  end;
end;

Теперь немного усложним задачу. Пусть у нас есть не один файл, а десять - от десяти разных поставщиков. Нам надо в общей сложности загрузить 20000 наименований и затем отсортировать их. Если мы будем именно так и делать, то сортировка такого большого списка объектов займет значительное время, это и составляет задачу. Однако такая задача решается очень просто - достаточно после создания объекта TStringList сразу присвоить его свойству Sorted значение true. После этого вставка новых строк будет осуществляться с помощью быстрого алгоритма, однако потеряется возможность сортировать по параметрам прикрепленных объектов, т.к. в случае Sorted=true сортировка производится автоматически только по текстовому представлению (см. исходники TStringList). При Sorted=true, обработка дубликатов определяется свойством Duplicates. Можно пропускать, разрешать и запрещать дубликаты объектов с одинаковым текстовым представлением. При этом, если дубликаты пропускаются или запрещены, необходимо следить за освобождением ресурсов объектов, не добавленных в список. По умолчанию, свойство Duplicates равно dupIgnore, что означает, что дубликаты пропускаются, поэтому по умолчанию надо следить за освобождением ресурсов.

Стоит отметить, что при сохранении или загрузке списка строк, прикрепленные к строкам объекты не сохраняются и не восстанавливаются в стандартном TStringList, однако можно очень красиво решить эту проблему - написать потомка TStringList с переопределенными процедурами GetTextStr и SetTextStr, в которых придумать и реализовать собственный формат хранения объектов и их текстового представления в виде непрерывного текста.

Итак, мы увидели, что TStringList позволяет решать самые разнообразные задачи от группировки объектов с текстовым представлением до простой обработки списка значений переменных. Теперь посмотрим, как этот класс позволяет решать задачи упаковки/распаковки сложно-структурированных данных в текстовый вид и обратно. Такая задача очень часто возникает в коммуникационных задачах, например, при передаче данных по сети или между программными модулями.

Предположим, что нам надо запустить некую программу obrab.exe с параметром командной строки, в которой необходимо передать выбранный товар из сформированного выше списка. Напишем упаковщик и распаковщик данных. С TStringList эта задача программируется за две секунды:

Упаковщик
procedure runobrab;
var strtov,stred:TStringList;
begin
  strtov:=TStringList.Create;
  with TTovar(ListBox1.Items.Objects[ListBox1.ItemIndex]) do
  begin
    stred:=TStringList.Create;
    stred.Values['ЕдИзм']:=EdIzm.Name;
    stred.Values['Вес']:=floattostr(EdIzm.Weight);
    strtov.Values['Товар']:=Name;
    strtov.Values['ЕдИзм']:=stred.CommaText;
    strtov.values['КолУп']:=inttostr(CountUp);
    strtov.values['Цена']:=floattostr(PriceOut);
  end;
  winexec('obrab.exe '+strtov.CommaText,SW_SHOWNORMAL);
  strtov.Free;
  stred.Free;
end;

Распаковщик
function getparam:TTovar;
var strtov,stred:TStringList;
begin
  strtov:=TStringList.Create;
  stred:=TStringList.Create;
  strtov.CommaText:=paramstr(1);
  stred.CommaText:=strtov.Values['ЕдИзм'];
  result:=TTovar.Create;
  with result do
  begin
    EdIzm:=TEdIzm.Create;
    EdIzm.Name:=stred.Values['ЕдИзм'];
    EdIzm.Weight:=strtofloat(stred.Values['Вес']);
    Name:=strtov.Values['Товар'];
    CountUp:=strtoint(strtov.values['КолУп']);
    PriceOut:=strtofloat(strtov.values['Цена']);
  end;
  stred.Free;
  strtov.Free;
end;

Продолжение следует…

(С) Цованян Роман, 2004 г.



Опубликовал admin
22 Июн, Вторник 2004г.



Программирование для чайников.