Модераторы: Poseidon, Snowy, bems, MetalFan
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> TReader/TWriter. Статья. 
:(
    Опции темы
THandle
Дата 8.3.2009, 14:48 (ссылка) |   (голосов:4) Загрузка ... Загрузка ... Быстрая цитата Цитата


Хранитель Клуба
Group Icon
Награды: 1



Профиль
Группа: Админ
Сообщений: 3639
Регистрация: 31.7.2007
Где: Moscow, Dubai

Репутация: 65
Всего: 372



Введение.

Всем привет.
Хочу в этой небольшой статейке-обзоре описать два очень удобных класса - TWriter и TReader.
Даже с первого взгляда видно, что они что-то куда-то пишут и читают.
Но что? Куда? Откуда? Помню, любил я в детстве эти вопросы задавать... Эх, были времена... Молодость... Ну, это мы что-то отвлеклись от темы. А работают эти классы с потоками (наследниками класса TStream).
TWriter пишет в ассоциированный с ним поток данные, а TReader соответственно читает.
Но зачем это нужно? Какие-то отдельные классы... Ведь в TStream'ах есть же методы Write/Read.
В TStream'ах все пишется одним методом. И часто возникают вопросы, например, по записи строки. Человек подсовывает в Write сам указатель на строку, а не данные, которые начинаются с String[1], и получается, что строка в файл у него не пишется. Таких тем на форумах очень много и возникают они довольно часто.
К тому же некоторые методы TStream'ов используют TWriter/TReader в своих целях - например, для записи компонентов.
В рассматриваемых же классах есть готовые функции для записи/чтения любых данных. Можно записывать данные секциями. В общем, намного удобнее, чем вручную все вгонять в поток.
Но, к сожалению многие просто не знают о существовании этих классов, и мучаются с вопросами, пример которых я описал выше.
Я тоже когда-то таким занимался smile
Сейчас же хочу поделиться со всеми, тем, что существует более удобный способ записи и чтения данных.

Статья будет состоять из трех частей.
В первой я расскажу о классе TWriter, о записи с его помощью данных в поток. Во второй - о классе TReader, и, соответственно, о чтении данных из потока. В третьей части мы рассмотрим дополнительные возможности этих классов, а так же рассмотрим, как Delphi сохраняет свои формы в файлы dfm.
 
PM   Вверх
THandle
Дата 8.3.2009, 14:48 (ссылка) |    (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


Хранитель Клуба
Group Icon
Награды: 1



Профиль
Группа: Админ
Сообщений: 3639
Регистрация: 31.7.2007
Где: Moscow, Dubai

Репутация: 65
Всего: 372



Часть 1. TWriter.

Итак. Хватит просто так болтать, примемся за дело. Сначала я опишу TWriter. Ведь откуда читать, если файла/куска памяти нет? Неоткуда :( Поэтому прежде чем заняться чтением - займемся записью.

Оба класса - и TWriter, и TReader имеют одного предка - TFiler. В него заложены основные методы взаимодействия с потоком.
Конструктор TWriter получил от TFiler'а:

Код

    constructor Create(Stream: TStream; BufSize: Integer);


Первый параметр - ссылка на существующий экземпляр любого наследника класса TStream.
Второй - любое положительное ( BufSize > 0 ) число. Отвечает за размер буфера для записи данных. Что это за буфер такой?
Дело в том, что TWriter пишет не сразу в ассоциированный с ним поток данных, а в буфер, находящийся внутри класса. Объявлен он как указатель (Pointer), в конструкторе под него выделяется соответствующий кусок памяти размером BufSize. Первый вопрос, который возник лично у меня, когда я это узнал: что же то получается, с помощью одного экземпляра рассматриваемого нами класса, можно записать в поток только кусок данных размером с BufSize?
На самом деле, этот буфер перезаписываемый. Допустим, указали Вы размер буфера 16 байт, записали сначала строку в 10 символов. В самом потоке этих данных еще нет, их туда никто не записывал. Теперь записываете еще 10 байт информации. А TWriter смотрит, в буфере мало места осталось, он пишет кусок данных, размером  с оставшееся свободное в буфере место, после чего записывает все данные из буфера в поток и устанавливает значение полю FBufPos в 0. Это поле отвечает за позицию в буфере, с которой начинается свободная его часть, то есть FBufSize-FBufPos и есть то количество байт, которое свободно в буфере для записи данных.
Еще есть такое свойство:

Код

    property Position: Longint read GetPosition write SetPosition;


Оно содержит в себе размер уже всех записанных данных. И тех, которые были записаны в сам поток, и тех которые пока еще находятся в буфере.
В деструкторе, конечно же, все данные из буфера записываются в поток.

Чтобы принудительно записать все данные из буфера в поток и очистить буфер следует вызвать метод:

Код

   procedure FlushBuffer;



Теперь несколько слов о том, в каком виде пишутся данные.
У TWriter'а есть свой формат записи данных. Все типы данных перечислены в типе:

Код

  TValueType = (vaNull, vaList, vaInt8, vaInt16, vaInt32, vaExtended,
    vaString, vaIdent, vaFalse, vaTrue, vaBinary, vaSet, vaLString,
    vaNil, vaCollection, vaSingle, vaCurrency, vaDate, vaWString,
    vaInt64, vaUTF8String, vaDouble);


На данном этапе разбирать каждое значение, думаю, не стоит. Займемся этим при обсуждении конкретных методов записи данных.

А теперь, как раз, рассмотрим эти самые методы:
 
Код

    procedure Write(const Buf; Count: Longint);


К этому методу сводится, в принципе, вся запись (Все прочие методы в конечном счете вызывают Write). Записывает кусок данных в буфер.

Код

    procedure WriteBoolean(Value: Boolean);


Понятно, что данный метод записывает в буфер переменную типа Boolean.
Вроде бы все ясно и просто, НО, как мы помним, TWriter пишет данные в своем формате(TValueType). Посмотрим, что он делает с Boolean:

Код

procedure TWriter.WriteBoolean(Value: Boolean);
begin
  if Value then
    WriteValue(vaTrue)
  else
    WriteValue(vaFalse);
end;


То есть записывает явно не значения 1 или 0(true or false), а пишет значения соответствующих элементов перечисляемого типа TValueType.
В итоге получается что данные, записанные с помощью TWriter можно, не создавая себе лишних проблем, прочитать только с помощью TReader'а.
Итак, WriteBoolean записывает вместо true - vaTrue, вместо false - vaFalse.
Едем дальше.

Код

    procedure WriteInteger(Value: Longint); overload;
    procedure WriteInteger(Value: Int64); overload;


Первая процедура предназначена для записи типов Byte, ShortInt, SmallInt, Integer - то есть всех чисел с диапазоном значений <= Integer.
Проверяет значение и пишет соответственно vaInt8, vaInt16, vaInt32.
Посмотреть подробнее как проверяет можете в Classes.pas - тут код приводить не буду, нет нужды smile

Вторая процедура записывает целое значение, превосходящее Integer, то есть Int64.
Если переданное значение меньше Integer, то вызывается первый вариант WriteInteger.
Иначе записывает значение типа vaInt64.
Во как по-умному сделано smile

Теперь рассмотрим запись строк. Для этого предназначены следующие методы:

Код

    procedure WriteStr(const Value: AnsiString);
    procedure WriteUTF8Str(const Value: string); inline;
    procedure WriteString(const Value: UnicodeString);
    procedure WriteWideString(const Value: UnicodeString);


Первый(WriteStr) во всех версиях Delphi(включая 2009) записывает Ansi ShortString строку. То есть строку длиной не более 255 символов.
В параметре он принимает «нормальную» (не ограниченную 255 символами) строку, и если её длинна больше только что приведенного магического числа, то строка записывается не полностью, а пишутся только первые 255 символов.

Второй метод, точно так же как и предыдущий, записывает первые 255 символов UTF8 строки.

Третий метод записывает строку, как есть, без всяких обрезаний. Только в версиях Delphi ниже 2009 записывает не Unicode, а ANSI строку. Соответственно, объявлена она там немного по-другому (с другим типом параметра).

Четвертая процедурка из этого списка в 2009 играет ту же роль что и WriteString. Точнее все её действия сводятся к вызову этого метода.
В Delphi более ранних версий записывает Unicode строку.

Различным строкам из набора TValueType соответственно соответствуют:

vaString - строка длиной менее 255 символов.
vaLString - ANSI строка неограниченной длины.
vaWString - Unicode строка.
vaUTF8String - строка в кодировке UTF8.


Теперь поговорим о записи символов:

Код

    procedure WriteChar(Value: Char);
    procedure WriteWideChar(Value: WideChar);


Оба метода записывают символ в соответствующей кодировке.
В Delphi 2009 они идентичны.
WriteWideChar появился только в 2007 версии (возможно и раньше (например в 2006), но проверить, к сожалению, не могу, это уж сами. Но в Delphi 7 о таком методе еще не слыхали) .


Теперь о записи действительных чисел. За это отвечают четыре следующих метода:

Код

    procedure WriteFloat(const Value: Extended);
    procedure WriteSingle(const Value: Single);
    procedure WriteDouble(const Value: Double);
    procedure WriteCurrency(const Value: Currency);


Ничего сверхъестественного они не делают - только пишут то, что они должны писать: значения соответствующего типа из набора TValueType. Типы для этих функций таковы: vaExtended, vaSingle, vaDouble и vaCurrency соответственно. То есть, сначала пишется тип данных, берущийся из TValueType, а потом пишутся сами данные. И так во всех случаях записи.

Дата и время записываются с помощью метода:

Код

    procedure WriteDate(const Value: TDateTime);


Тут тоже ничего интересного. Из набора TValueType данным этого типа соответствует значение vaDate.

Данные вариантного (Variant) типа записываются с помощью:

Код

    procedure WriteVariant(const Value: Variant);


Переданный параметр анализируется на значения типа varXXX и записывается с помощью одного из рассмотренных уже нами методов.
Значению типа Variant varEmpty соответствует из набора TValueType значение vaNil. А значению varNull - vaNull соответственно.
Единственное что мне в этом методе не понятно, так это то, зачем там нужен вот такой код:

Код

    varBoolean:
      if Value then
        WriteValue(vaTrue)
      else
        WriteValue(vaFalse);


Почему бы не вызвать просто метод WriteBoolean, и не копировать код, как для других типов. Например:

Код

    varString:
      WriteString(Value);


Для меня осталось это загадкой - почему так сделали. Но, не важно, мелочь. smile
В данном методе возможно возбуждение исключения, поэтому, пожалуйста, не забываете заключать свой код в блок try..finally..end.

Теперь рассмотрим такой метод:

Код

    procedure WriteIdent(const Ident: string);


Метод этот записывает четыре идентификатора либо же просто UTF8 строчку, обозначая её как vaIdent. Это можно использовать для записи каких-то что-то идентифицирующих строк. Лично мне этот метода никогда не пригождался.
Рассмотрим четыре значения параметра Ident, которые данный метод пишет не просто как строку, а как нечто неизведанное... Но нам уже знакомое smile

Итак, это строки:
'False', 'True', 'Null', 'Nil' - все равно, в каком регистре написанные. Записываются они не как строка, а как значение типов: vaFalse, vaTrue, vaNull, vaNil соответственно.
Может, кому и пригодится smile

И перед тем как начать разбор записи самых "вкусных" элементов - компонентов, взглянем одним глазом на метод:

Код

    procedure WriteSignature;


Этот метод пишет сигнатуру TPF0. Вот так. Просто 4 байта. Можете использовать это как Вам захочется, а то как эту сигнатуру использует Delphi, куда пишет её - узнаем чуть позже, при рассмотрении метода WriteDescendent.

Ну а теперь...
TWriter позволяет записывать в поток компоненты, всех их свойства, даже целые формы! На самом деле Delphi использует этот класс для формирования dfm файла формы, но об этом поговорим в самом конце, в третьей части статьи, уже, после того как разберемся со всеми нюансами записи компонентов.

Для начала разберем метод записи компонента:

Код

    procedure WriteComponent(Component: TComponent);


Этот метод записывает указанный в параметре компонент в поток. То есть записывает все данные содержащиеся в компоненте. Если свойство Root:

Код

    property Root: TComponent read FRoot write SetRoot;


установлено, то компонент на которое оно указывает будет считаться корневым. Что это значит? Рассмотрим несколько примеров.

Для всех примеров на форме должно быть две панели(TPanel), ну и какие либо компоненты на эти панельки накиданные. Например, сделаем так. На первой будет TEdit, TCheckBox. А на второй TListBox.

1 пример.

Код

var
  W: TWriter;
  FS: TFileStream;
begin
  FS := TFileStream.Create('E:\TestFile', fmCreate);
  try
    W := TWriter.Create(FS, 16);
    try
      W.WriteComponent(Panel1);
    finally
      W.Free;
    end;
  finally
    FS.Free;
  end;
end;


Что этот код делает? Он записывает в файловый поток компонент Panel1, без его дочерних компонентов. Без. Root не установлен, корня нет, идти неоткуда - вот и пишется только сам Panel1 и его свойства.
Разберем второй пример.

2.

Код


var
  W: TWriter;
  FS: TFileStream;
begin
  FS := TFileStream.Create('E:\TestFile', fmCreate);
  try
    W := TWriter.Create(FS, 16);
    try
      W.Root := Panel1;
      W.WriteComponent(Panel1);
    finally
      W.Free;
    end;
  finally
    FS.Free;
  end;
end;


Вроде бы Root установлен. Почему не пишутся дочерние компоненты, расположенные на панели? Вроде же все правильно...
А дело тут вот в чем. Да, Panel1 является Parent'ом для лежащих на ней компонентов, но, в Root надо указывать не Parent, а Owner, владельца. Для всех компонентов созданных в Design-time(во время батонокидательства) владельцем является сама форма, на которой эти компоненты расположились. Соответственно в Root надо указывать именно Owner'а, то есть форму. НО!!! Не стоит забывать, что если Вы компоненты создаете динамически, то Owner'ом может быть тот, кого установите Вы. В таких случаях Вам придется ставить в Root уже соответствующий указатель на владельца. Не забывайте об этом. Теперь два примера иллюстрирующие только что написанный текст:

1.

Код

var
  W: TWriter;
  FS: TFileStream;
begin
  FS := TFileStream.Create('E:\TestFile', fmCreate);
  try
    W := TWriter.Create(FS, 16);
    try
      W.Root := Form1;
      W.WriteComponent(Panel1);
    finally
      W.Free;
    end;
  finally
    FS.Free;
  end;
end;


Таким образом отлично сохраняется сама Panel1 и все лежащие на ней компоненты с Owner = Root. То есть, если вы создадите динамически какой-то компонент, с Owner'ом отличающимся от Root'а, но с Parent'ом компонента, который входит в состав записываемых, то такой динамически созданный компонент в поток записан не будет. Но зачем говорить, рассмотрим еще один пример:

2.

Код

procedure TForm1.Button1Click(Sender: TObject);
var
  W: TWriter;
  FS: TFileStream;
begin
  FS := TFileStream.Create('E:\TestFile', fmCreate);
  try
    W := TWriter.Create(FS, 16);
    try
      with TPanel.Create(Panel1) do
      begin
        Parent := Panel1;
        Name := 'PanelXXX';
      end;
      W.Root := Form1;
      W.WriteComponent(Panel1);
    finally
      W.Free;
    end;
  finally
    FS.Free;
  end;
end;


Как видите динамически созданный компонент типа TPanel с именем PanelXXX, Parent'ом равным записываемому в поток компоненту(Panel1) и с отличным от Root владельцем никуда не записывается, чего и следовало ожидать.
 
Теперь кратенько опишу еще одно свойство:

Код

    property LookupRoot: TComponent read FLookupRoot;


Это read-only свойство используется при записи TWriter'ом вложенных фреймов. При записи компонентов, находящихся на фрейме указывает на этот фрейм, как на владельца. То же самое касается и TReader'а, то есть чтица данных. Используется точно так же. Рассматривать никакого примера мы не будем, так как в данной статье мы все-таки обсуждаем не фреймы.

Теперь еще одно важное свойство:

Код

    property Ancestor: TPersistent read FAncestor write FAncestor;


Давайте представим следующую ситуацию. У нас есть компонент, с огромным количеством свойств. А установлено(отличается от дефолтного) только одно(образно - значения таких свойств как длина, ширина и им подобные, конечно же, всегда при визуальном проектировании отличаются от заданных изначально(хотя при динамическом создании можно ничего этого и не трогать)) - вот теперь вопрос: зачем спрашивается нам писать, тратить драгоценные байты на жестком диске, все эти не установленные свойства? Мне тоже не очень понятно, зачем это делать.
Так вот. Свойство Ancestor используется для сравнения записываемого компонента с неким прототипом. То есть записываются только те свойства, которые в записываемом компоненте не равны свойствам в прототипе.
Использовать свойство Ancestor напрямую - не рекомендуется. Ведь если Вы, допустим, укажете в нем некую панель(TPanel), а записывать будете различные компоненты, то получится не известно что при сравнении свойств, и записано будет уже явно не то, что Вам нужно. Если уж очень надо записать компонент, сравнивая его с каким то прототипом, то используйте WriteDescendent, о котором речь пойдет чуть позже.
Вернемся к нашему свойству. Ancestor устанавливается в значение отличное от nil, только если это форма разработанная в визуальном дизайнере.

Теперь хочу сказать пару слов о записи свойств компонентов.
Стандартно записываются только свойства определенные в секции published - то есть те, которые доступны в Object Inspector.
При объявлении свойств в секции Published мы заставляем компилятор добавлять дополнительную RTTI информацию о компоненте, с которой, в том числе, позже работает всеми нами любимый Object Inspector.
По этой причине в данной секции класса не могут быть объявлены некоторые типы данных. Разрешены стандартные типы, строки, массивы, классы и указатели на методы. Попробуйте объявить свойство в виде указателя(Pointer) - ничего у Вас не выйдет.

Теперь еще одно свойство:

Код

    property RootAncestor: TComponent read FRootAncestor write FRootAncestor;


Оно используется внутри TWriter'а для сравнения корневого компонента(Root'а) с неким прототипом. В принципе, действует аналогично Root.

Так же у рассматриваемого нами класса есть еще одно интересное свойство:

Код

    property IgnoreChildren: Boolean read FIgnoreChildren write FIgnoreChildren;


Если это свойство установлено, то при записи компонента не будут записаны принадлежащие ему компоненты.
Рассмотрим для большей наглядности два примерчика:

1.
Код


procedure TForm1.Button1Click(Sender: TObject);
var
  W: TWriter;
  FS: TFileStream;
begin
  FS := TFileStream.Create('E:\TestFile1', fmCreate);
  try
    W := TWriter.Create(FS, 16);
    try
      W.Root := Form1;
      W.IgnoreChildren := true;
      W.WriteComponent(Form1);
    finally
      W.Free;
    end;
  finally
    FS.Free;
  end;
end;


Как видите в получившемся файле записана только форма. Никакие компоненты на ней расположенные записаны не были.

2.

Этот пример стандартный, уже приводился:

Код

procedure TForm1.Button1Click(Sender: TObject);
var
  W: TWriter;
  FS: TFileStream;
begin
  FS := TFileStream.Create('E:\TestFile', fmCreate);
  try
    W := TWriter.Create(FS, 16);
    try
      W.Root := Form1;
      W.WriteComponent(Form1);
    finally
      W.Free;
    end;
  finally
    FS.Free;
  end;
end;


Записываются все компоненты с Owner'ом равным Form1.

Как Вы уже поняли, при IgnoreChildren = true, пишется только сам компонент, а при false еще и все ему принадлежащие. По умолчанию это свойство установлено в false.

Теперь еще один метод записи компонента в поток.

Код

    procedure WriteDescendent(Root: TComponent; AAncestor: TComponent);


Это в принципе тот же WriteComponent, только с небольшими отличиями.
1. Сначала записывает сигнатуру, а потом уже компонент и все ему принадлежащее.
2. Устанавливает значение свойств Ancestor и RootAncestor в значение переданное в параметре AAncestor, а свойства Root и LookupRoot в значение первого параметра(Root).

Ну а после всего этого, собственно говоря, вызывается сам WriteComponent.

Теперь еще один метод записи:

Код

    procedure WriteRootComponent(Root: TComponent);


Приведу лишь его код. Разбирайтесь сами ;)

Код

procedure TWriter.WriteRootComponent(Root: TComponent);
begin
  WriteDescendent(Root, nil);
end;


Ну, и последий метод записи:

Код

    procedure WriteCollection(Value: TCollection);


Как ясно из названия метода - он записывает в поток коллекцию.
Что это за шутка такая - TCollection - можно почтить по следующей ссылке:

http://www.delphikingdom.com/asp/viewitem.asp?catalogid=215

Теперь же о том как проходит запись.
Сначала, независимо от того передана ли в параметре живая коллекция или же nil, записывается значение из набора TValueType(да, давненько мы о нем не говорили). А именно vaCollection.
А дальше записываются все свойства каждого элемента коллекции.

На этом записывающие методы TWriter'а мы рассматривать заканчиваем, так как они у него уже закончились smile

Ах, да. Есть еще две такие процедурки, методы TWriter'а:

Код

    procedure WriteListBegin;
    procedure WriteListEnd;


Что они делают? Это что-то наподобие begin..end'а в самом Delphi.
Они ничего не делают, кроме того, что записывают в буфер значения из TValueType - vaList при вызове WriteListBegin и vaNull при WriteListEnd.
Это позволяет разбивать записываемые данные на разделы, что бывает довольно удобно.
Только никогда не забывайте закрывать открытый WriteListBegin'ом раздел с помощью вызова WriteListEnd, ну или если вам так хочется, просто записью vaNull. smile
Больше сказать об этих методах нечего.


На этом рассмотрение класса TWriter мы заканчиваем, и 1 часть статьи соответственно тоже заканчивается. Дальше мы рассмотрим класс TReader.


Это сообщение отредактировал(а) THandle - 8.3.2009, 15:20
PM   Вверх
THandle
Дата 8.3.2009, 14:48 (ссылка) |    (голосов:2) Загрузка ... Загрузка ... Быстрая цитата Цитата


Хранитель Клуба
Group Icon
Награды: 1



Профиль
Группа: Админ
Сообщений: 3639
Регистрация: 31.7.2007
Где: Moscow, Dubai

Репутация: 65
Всего: 372



Часть 2. TReader.

Итак, переходим ко второй части статьи. Тут мы рассмотрим класс TReader, который используется для чтения данных из потока.

Эта часть статьи будет немного короче, так как все мы уже рассмотрели. В TReader'е просто соответствующие методы для чтения определенного типа данных, и я думаю Вам понятно, что если Вы, допустим, записали данные с помощью TWriter.WriteInteger, то читать их нужно с помощью TReader.ReadInteger. Эти методы мы рассматривать не будем, лишь кратенько пробежимся по их списку, а разберем только наиболее интересное и что-то новенькое.

Итак. Рассмотрим методы, которые особого интереса для нас не представляют. То есть те, работа которых понятна по аналогии с методами записи TWriter'а.

Код

    procedure Read(var Buf; Count: Longint);


Этот метод читает в Buf данные размером в Count байт.
К этому методу, аналогично TWriter.Write сводится все чтение данных.

Код

    function ReadBoolean: Boolean;


Читает из потока данные в переменную типа Boolean.
Следует обратить внимание на то, что сам метод выглядит таким образом:

Код

function TReader.ReadBoolean: Boolean;
begin
  Result := ReadValue = vaTrue;
end;



То есть, вызывается метод ReadValue, которое возвращает значение типа TValueType, а результат возвращается простым сравнения этого значения с vaTrue.
Следовательно, все равно, что прочитает ReadValue. Пусть там будет хоть vaString или что-то еще - в любом случае будет возвращен результат в виде false, и ошибку того, что Вы прочитали не те данные, получить будет невозможно.
Поэтому, будьте аккуратнее с чтением. Читайте только данные только в том порядке и тех типов, как они были записаны.
 
Код

    function ReadChar: Char;
    function ReadWideChar: WideChar;


Читают значения типа Char. Осторожно, возможно возникновения исключение, если прочитается строка, а не символ.
Еще раз говорю о том, что бы Вы следили за порядком чтения данных и не забывать ставить try..finally..end.

Код

    function ReadStr: string;
    function ReadString: string;


Читают строки типа vaString, vaLString.

Код

    function ReadWideString: WideString;


Читает vaUTF8String, vaWString.

Код

    function ReadInteger: Longint;
    function ReadInt64: Int64;


Читают значения целочисленных типов. С типами все как в TWriter.

Код

    function ReadFloat: Extended;
    function ReadSingle: Single;
    function ReadDouble: Double;
    function ReadCurrency: Currency;


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

Код

    function ReadVariant: Variant;


Читает значение типа Variant.
Проверяет, какого типа является следующее значение и вызывает метод чтения для соответствующего типа. Если тип данных определить не удалось(записаны какие-то левые данные), то будет возбужденно исключение. Аккуратнее с этим.

Код

    function ReadDate: TDateTime;


Читает дату/время. 
Тут все ясно.

Код

    procedure ReadSignature;


Читает сигнатуру TPF0. Если читается что-то другое, то возбуждается исключение.

Код

   function ReadValue: TValueType;


Как уже говорилось - данный метод считывает из потока SizeOf(TValueType) байт и возвращает тип данных, идущих за этой записью.
Таким методом можно считывать вручную данные, своими методами. Хотя конечно не рекомендую этого делать, ведь есть уже готовые методы ;)

Код

   procedure ReadCollection(Collection: TCollection);


Считывает из потока коллекцию. Ссылка на статью про коллекции была в первой части данного обзора. Считывание происходит аналогично записи. Считываются все элементы коллекции и их свойства.


Теперь поговорим о считывании компонентов. Тут тоже ничего сложного нет, если не углубляться в основы того, как что читается.
Статья у нас обзорная, углубляться не будем. Если интересно - посмотрите сами реализацию этих методов.
Начнем.

Код

    function ReadComponent(Component: TComponent): TComponent;


Эта функция считывает компонент. Но тут не все так просто как с записью. Прежде чем читать, следует установить несколько свойств. Посмотрим, что же это за свойства.

Код

    property Root: TComponent read FRoot write SetRoot;


Свойство Root перешло к TReader еще от своего предка - TFiler'а.
Но это не столь важно.
Используется оно так же, как и при записи. Указывает на владельца читаемого компонента. Обязательно надо устанавливать(Если конечно не используется ReadRootComponent, о которой чуть позже).
Следующее свойство:

Код

    property Parent: TComponent read FParent write FParent;


Отвечает за родителя читаемого компонента. Актуально только для визуальных компонентов(наследников TControl). Для них обязательно следует устанавливать это свойство в валидное значения. А, например, для TOpneDialog - не нужно.

И третье свойство, которое, впрочем, устанавливать не обязательно:

Код

    property Owner: TComponent read FOwner write FOwner;


Если оно не установлено, то владельцем прочитанного из потока компонента станет Root. Если же установлено, то, соответственно, владельцем компонента будет уже, то значение, в которое Вы установите свойство Owner.  Смотрите, не намудрите с этим ;)

Итак, все эти свойства мы рассмотрели. Теперь идем дальше.

Код

    procedure BeginReferences;
    procedure EndReferences;
    procedure FixupReferences;



Вот. Есть такие три метода. Что они делают?
Эти методы гарантируют, что до того момента, пока все компоненты не будут загружены, ссылки на них нигде использоваться не будет.
В BeginReferences создается TList, в который добавляются все считываемые компоненты. При считывании, в свойству ComponentState, являющиеся множеством, добавляется значение csLoading.
BeginReferences должен вызываться до считывания компонентов. 
В FixupReferences у всех, уже прочитанных компонентов, из множества ComponentState убирается значение csLoading и компоненты становится можно использовать.
В EndReferences просто освобождается список, в котором были ссылки на прочитанные компоненты.
Если Вы вызываете BeginReferences - Вы обязаны:
1. Вызывать FixupReferences и EndReferences.
2. Обязательно заключать чтение компонентов в блок try..finally, таким образом:

Код

  Reader.BeginReferences;
  try
    Reader.ReadComponent(...);
  finally
    Reader.FixupReferences;
    Reader.EndReferences;
  end; 


Обязательно!!!

Далее.

Код

    function ReadRootComponent(Root: TComponent): TComponent;


Свойство Root устанавливается в значение единственного параметра этого метода.
Предыдущие замечания про XXXReferences касаются и этого метода.

Теперь, я хотел бы немного отойти от скучного рассмотрения методов, и разобрать пару небольших примеров.
В Интернете для записи некоего компонента нам предлагают написать процедуру следующего типа(не "дословно", но смысл тот же):

Код

procedure WriteComponentToFile(const FileName: String; Component: TComponent);
var
  FS: TFileStream;
  Writer: TWriter;
begin
  if not Assigned(Component) then
    Exit;
  FS := TFileStream.Create(FileName, fmCreate);
  try
    Writer := TWriter.Create(FS, 256);
    try
      Writer.Root := Component.Owner;
      Writer.WriteComponent(Component);
    finally
      Writer.Free;
    end;
  finally
    FS.Free;
  end;
end;



Вот такой код. Сохраняет компонент(и его дочерние компоненты), переданный во втором параметре процедуры, в файл, имя которого передается, соответственно, в первом smile
Все отлично сохраняется. Никаких проблем нет.
Каким кодом нам предлагают все это безобразие читать?
А вот таким:
Код


procedure ReadComponentFromFile(const FileName: String; Component: TComponent);
var
  FS: TFileStream;
  Reader: TReader;
begin
  if not Assigned(Component) then
    Exit;
  FS := TFileStream.Create(FileName, fmOpenRead);
  try
    Reader := TReader.Create(FS, 256);
    try
      Reader.Root := Component.Owner;
      Reader.Parent := TControl(Component).Parent;
      FreeAndNil(Component);
      Reader.BeginReferences;
      try
        Component := Reader.ReadComponent(Nil);
      finally
        Reader.FixupReferences;
        Reader.EndReferences;
      end;
    finally
      Reader.Free;
    end;
  finally
    FS.Free;
  end;
end;




Все работает просто замечательно. Радостный программист пишет код дальше не задумываясь, что тут такого происходит. А что тут, собственно, не так?
Давайте разберемся.
Эта процедура считывает вместо компонента, передаваемого во втором параметре, какой-то компонент из потока данных, в данном случае представленного файлом.
Но что же происходит? Старый компонент уничтожается полностью, а на его место читается новый. Это может быть вообще что угодно. Но не будем представлять ситуацию, когда вместо суслика мы получаем крокодила, а взглянем на то, что получится, если считывать тот же компонент.
Представим. На форме панель(TPanel). На панели TEdit.
Сохраняем сие чудо в файл, с помощью вызова:

Код

  WriteComponentToFile('E:\Test1', Panel1);


Сохранили. Все прекрасно.
Дальше. Считываем аналогичным образом.

Код

  ReadComponentFromFile('E:\Test1', Panel1);


Все данные считались. Все, на первый взгляд, тоже замечательно. Но. Вот прочитали мы. Наша программа работает дальше. И где-то, мимолетно, мы обращаемся к Form1.
Программа начинает ругаться матом на пользователя. Он пугается, падает со стула, ломает, допустим, руку, и Вас отдают под суд.
Почему возникает ошибка?
Передали мы в процедуру ReadComponentFromFile нашу панельку, взяли у нее, то, что нам надо(владельца и родителя), да и уничтожили её совсем, вызвав FreeAndNil. 
Panel1 = Nil. Все. При любом обращении, при любой попытке вызова метода Panel1 у нас получится AV.
Но ведь панель же у нас на форме есть??? Это же не призрак?
Есть. Найти её можно с помощью того же FindComponent. Имя, и другие свойства, у неё не изменились. Но ссылка стала не действительна.
И именно вот такой код встречается в большинстве статей/ответов в Интернете. Как исправить, особо ничего не переделывая?
Не сложно. К примеру, так:

Код


function ReadComponentFromFile(const FileName: String; Component: TComponent): TComponent;
var
  FS: TFileStream;
  Reader: TReader;
begin
  if not Assigned(Component) then
    Exit;
  Result := Nil;
  FS := TFileStream.Create(FileName, fmOpenRead);
  try
    Reader := TReader.Create(FS, 256);
    try
      Reader.Root := Component.Owner;
      Reader.Parent := TControl(Component).Parent;
      FreeAndNil(Component);
      Reader.BeginReferences;
      try
        Component := Reader.ReadComponent(Nil);
        Result := Component;
      finally
        Reader.FixupReferences;
        Reader.EndReferences;
      end;
    finally
      Reader.Free;
    end;
  finally
    FS.Free;
  end;
end;


Ну, и вызывать эту функцию соответствующим образом:

Код

   Panel1 := ReadComponentFromFile('E:\Test1', Panel1) as TPanel;


Далее. Есть такой метод:

Код

    procedure ReadComponents(AOwner, AParent: TComponent;
      Proc: TReadComponentsProc);


Этот метод считывает компоненты из потока данных. Каким образом он их считывает?
Компоненты из потока читаются, пока не будет обнаружено значение равное vaNull.
Это проверяется функцией:

Код

    function EndOfList: Boolean;


Она считывает из буфера(да, да - вспоминаем, что у нас TReader/TWriter работают с потоком через буфер) один байт и возвращает true, если прочитанное значение равно vaNull и false, если не равно.
Так вот, перед тем как начать считывать компоненты свойства Root и Owner устанавливаются в значение, переданное в параметре AOwner, а свойства Parent, соответственнно, устанавливается в значение параметра AParent.
Дальше вызывается метод BeginReferences(то есть, нам его теперь вызывать не требуется).
И запускается цикл по считыванию компонентов, в котором, после прочтение компонента, проверяется, существует ли некая процедура Proc(третий параметр этого метода), и если существует, то она вызывается. Это удобно для выполнения каких-то своих действий при считывании компонентов.
Тип процедуры имеет следующий вид:

Код

  TReadComponentsProc = procedure (Component: TComponent) of object;


Единственный параметр, который в нее передается из ReadComponents -только что считанный компонент.
После прочтения всех компонентов и обнаружения значения vaNull, вызываются методы FixupReferences и EndReferences.
Все то, что я сейчас сказал, Вы могли свободно узнать, посмотрев на код этого метода:

Код



procedure TReader.ReadComponents(AOwner, AParent: TComponent;
  Proc: TReadComponentsProc);
var
  Component: TComponent;
begin
  Root := AOwner;
  Owner := AOwner;
  Parent := AParent;
  BeginReferences;
  try
    FFinder := TClassFinder.Create(TPersistentClass(AOwner.ClassType), True);
    try
      while not EndOfList do
      begin
        ReadSignature;
        Component := ReadComponent(nil);
        if Assigned(Proc) then Proc(Component);
      end;
      ReadListEnd;
      FixupReferences;
    finally
      FFinder.Free;
    end;
  finally
    EndReferences;
  end;
end;



Ну, думаю с этим методом все ясно. Попробуйте реализовать какую-нибудь свою процедурку обработки считываемых компонентов. smile

Осталось нам в TReader'е рассмотреть совсем чуть-чуть. Всего несколько методов. Итак, отвлеклись на секунду, и продолжим.

Код

    procedure FlushBuffer; override;


Этот метод очень просто - он лишь "очищает"(ставит в ноль свойство Position) буфер.

Код

    procedure SkipValue;


Этот метод пропускает следующее значение, которое обнаруживает в потоке данных. Он просто считывает соответствующее количество байт идущих после идентификатора типа этих данных(TValueType, о котором мы говорили в первой части).

Следующий метод:

Код

    procedure CopyValue(Writer: TWriter);


Он копирует считываемое значение в некий TWriter, переданный в единственном параметре этого метода. Все просто. Проверяется тип считываемого значения, и вызывается соответствующий метод записи TWriter'а, который это значение пишет.

Код

    procedure CheckValue(Value: TValueType);


Этот метод проверяет, является ли считываемое значение из буфера значением типа Value(единственный параметр этого метода).
Если тип тот, который должен быть - то спокойно программа выполняется дальше.
Если же нет, то, "о, ужас!!!", возбуждается исключение, сообщающее нам об этой прискорбной ошибке.

Последние два метода, которые мы расмотрим, соответствуют методам WriteListBegin и WriteListEnd класса TWriter.

Код

    procedure ReadListBegin;
    procedure ReadListEnd;


Что в них происходит - говорить не буду. Все ясно из кода этих методов:

Код

procedure TReader.ReadListBegin;
begin
  CheckValue(vaList);
end;

procedure TReader.ReadListEnd;
begin
  CheckValue(vaNull);
end;



На этом мы завершаем рассмотрение класса TReader.
Поздравляю, теперь Вы знаете достаточно, чтобы использовать эти классы по назначению.
Если интересно узнать еще кое-что, то читайте третью часть статьи, под кодовым названием "Немного в дополнение".

PM   Вверх
THandle
Дата 8.3.2009, 14:48 (ссылка) |    (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


Хранитель Клуба
Group Icon
Награды: 1



Профиль
Группа: Админ
Сообщений: 3639
Регистрация: 31.7.2007
Где: Moscow, Dubai

Репутация: 65
Всего: 372



Часть 3. Немного в дополнение.

Итак, приветствую всех в третьей части этой статьи.
В этой части мы рассмотрим некоторые вещи, которые одновременно и относятся к TReader/TWriter, но и не являются рассмотрением возможностей чтения/записи этих классов. Это часть - дополнительный материал. Если Вы уже узнали все, что хотели - можете эту часть не читать. Хотя тут даже и читать то нечего. Пара примеров smile
Итак. Начнем.



3.1. Запись и считывание форм в Delphi.

Всем известно, что Delphi сохраняет автоматически свои формы в файлы dfm.
Давайте рассмотрим, как это делается.
Имеется у нас форма. IDE Delphi хочет её сохранить. Вызывается процедура:

Код

procedure WriteComponentResFile(const FileName: string; Instance: TComponent);


Первый параметр - имя файла, в который нужно сохранять, второй - сохраняемый компонент. Все просто.

Рассмотрим, что же происходит дальше. Вот код WriteComponentResFile:

Код

procedure WriteComponentResFile(const FileName: string; Instance: TComponent);
var
  Stream: TStream;
begin
  Stream := TFileStream.Create(FileName, fmCreate);
  try
    Stream.WriteComponentRes(Instance.ClassName, Instance);
  finally
    Stream.Free;
  end;
end;


Как Вы видите, тут создается файловый поток данных, и вызывается его метод WriteComponentRes. В этом методе после еще нескольких "перенаправлений" все сводится к вызову метода TStream - WriteDescendent:

Код

procedure TStream.WriteDescendent(Instance, Ancestor: TComponent);
var
  Writer: TWriter;
begin
  Writer := TWriter.Create(Self, 4096);
  try
    Writer.WriteDescendent(Instance, Ancestor);
  finally
    Writer.Free;
  end;
end;


В нем Вам, если Вы читали предыдущие части, все должно быть ясно. 

В итоге имеем то, что все сводится к работе TWriter'а. Компонент записывается в файл и все.
Но это не файл dfm. Нет. Это бинарный файл ресурсов. Но как же создается dfm?
Бинарные данные преобразовать в текстовые очень просто - процедурой ObjectBinaryToText:
Код


procedure ObjectBinaryToText(Input, Output: TStream);


Её параметры - два потока данных. Первый - поток с бинарными данными. Во второй мы получаем преобразованные в текст данные из первого. (Первый - Input, второй - Output, если вдруг кто запутался).
Аналогичным образом можно преобразовать текстовое представление компонента в бинарный вид. Это делает процедура ObjectTextToBinary.

Все это касается не только dfm(VCL), но и xfm(CLX) файлов.

А теперь напишем небольшую программку-пример.
Что этот пример будет уметь делать?
1. Открывать dfm файл формы нашего примера, считывать из него форму и показывать её.
2. При закрытии считываемой формы программа будет спрашивать: Не хотите ли Вы сохранить эту форму со всеми внесенными изменениями?

Пример идет в аттаче к этому посту.

Думаю, в его коде все Вам понятно.




3.2. Сохранение/чтение не Published свойств.

Как Вы уже знаете, стандартные методы записи свойств компонентов из класса TReader, и соответствующие им методы чтения из TReader, работают только с теми свойствами компонента, которые объявлены в секции Published.
Но что делать нам, если вдруг потребуется сохранить, допустим, Public свойство компонента?
В этой заключительной части статьи мы как раз рассмотрим такой метод.

Посмотрим на 2 метода, общие и для TReader и для TWriter. Эти свойства идут еще от TFiler'а.

Код

procedure DefineProperty(const Name: string;
  ReadData: TReaderProc; WriteData: TWriterProc; HasData: Boolean);

procedure DefineBinaryProperty(const Name: string;
  ReadData, WriteData: TStreamProc; HasData: Boolean);


Разберемся с первым.
Этот метод определяет свойство, которое должно быть сохранено наряду с published свойствами.
Параметр WriteData - указатель на процедуру, которая будет производить запись данных. В TReader этот параметр игнорируется.
Параметр ReadData - указатель на процедуру по считыванию данных. В TWriter этот параметр игнорируется.
Естественно, все эти процедуры мы должны написать сами smile
Последний параметр - HasData, используется только при записи, для определения того, действительно ли нужно записывать это свойство.

DefineBinaryProperty отличается от DefineProperty тем, что данные свойства сразу идут в поток/из него, пропуская буферы наследников TFiler'а.

Для того чтобы опередилить не Published свойство, нужно перекрыть метод:
Код


    procedure DefineProperties(Filer: TFiler); override;


И определить для его параметра Filer нужные свойства через DefineProperty.
После этого написать процедуры записи/чтения данных и, в принципе, все.

Сначала я хотел привести пример в этой статье, того как писать такие компоненты. Но, все что приходило в голову слишком надумано и никогда не сможет применяться в реальной жизни. Лучше посмотрите на реальные компоненты, которые используют этот метод.
Например, одна из первых ссылок в Google:

http://www.delphisources.ru/pages/faq/base...mbers_list.html

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




Заключение.

На этом я заканчиваю. Возможно, позже, я дополню третью часть еще чем-то. Но сейчас я не вижу ничего такого, что можно было бы еще написать.
Надеюсь, этот небольшой обзор был Вам полезным.
С уважением ко всем читателям, THandle.



Благодарности.


Rrader - за замечания по статье. Ну как всегда в общем. Огромное спасибо.
Kbl4AH - за помощь в оценке статьи со стороны человека изучающего Delphi.
THandle - за идею.



Присоединённый файл ( Кол-во скачиваний: 74 )
Присоединённый файл  Example1.rar 17,87 Kb
PM   Вверх
mr.Anderson
Дата 8.3.2009, 15:29 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


iOS Lead Developer
****


Профиль
Группа: Участник Клуба
Сообщений: 3374
Регистрация: 20.12.2004
Где: далеко

Репутация: 3
Всего: 128



Хорошая статья ) Лови +


--------------------
user posted image

user posted image
PM MAIL ICQ Skype   Вверх
SneG0K
Дата 8.3.2009, 15:42 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Max Mara
***


Профиль
Группа: Завсегдатай
Сообщений: 1887
Регистрация: 1.12.2007
Где: Wis Dells

Репутация: 1
Всего: 54



Молодец, отлично все расписал.
PM WWW Skype   Вверх
Kbl4AH
Дата 8.3.2009, 20:57 (ссылка)  | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 741
Регистрация: 1.4.2008
Где: Вятка

Репутация: 1
Всего: 15



Афтар, зачот smile Пишы ищо smile 

ЗЫ. Примером смог открыть только форму примера smile  Придется, видимо, RAD2009 установить.
PM MAIL ICQ   Вверх
THandle
Дата 9.3.2009, 10:01 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Хранитель Клуба
Group Icon
Награды: 1



Профиль
Группа: Админ
Сообщений: 3639
Регистрация: 31.7.2007
Где: Moscow, Dubai

Репутация: 65
Всего: 372



Kbl4AH, Пример нормальный. Только что открыл его 7, 2006, 2007, 2009 версией Delphi.

Если у кого выскакивает ошибка на 
Код


Aplllication.MainFormOnTaskBar := True;


То эту строку просто удалите.


Kbl4AH, или какая ошибка и в каком месте у тебя возникает?
PM   Вверх
Kbl4AH
Дата 9.3.2009, 11:21 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 741
Регистрация: 1.4.2008
Где: Вятка

Репутация: 1
Всего: 15



Цитата(THandle @  9.3.2009,  10:01 Найти цитируемый пост)
Kbl4AH, или какая ошибка и в каком месте у тебя возникает?

Сильно затупил и был невнимателен... Никаких ошибок НЕТ!

Это сообщение отредактировал(а) Kbl4AH - 9.3.2009, 11:36
PM MAIL ICQ   Вверх
GN1
Дата 13.3.2009, 23:42 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 10
Регистрация: 23.4.2008
Где: Казахстан

Репутация: нет
Всего: нет



Спасибо за статью.
Жаль что на http://delphiplus.org не размещают ссылки на статьи с винграда :( За всем не уследишь.

Хотел поставить "+", постов не хватает  smile 
PM MAIL WWW   Вверх
bems
Дата 14.3.2009, 05:16 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
****


Профиль
Группа: Комодератор
Сообщений: 3400
Регистрация: 5.1.2006

Репутация: 31
Всего: 88



GN1, не вопрос, поможем


--------------------
Обижено школьников: 8
PM MAIL   Вверх
UniBomb
Дата 15.3.2009, 19:43 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок
***
Награды: 1



Профиль
Группа: Участник Клуба
Сообщений: 1754
Регистрация: 24.10.2006
Где: Санкт-Петербург

Репутация: нет
Всего: 97



GN1, и я поставлю от нас двоих  smile 


--------------------
PM MAIL ICQ Skype   Вверх
execoma
  Дата 14.3.2011, 11:11 (ссылка) |    (голосов:1) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 16
Регистрация: 8.3.2010

Репутация: нет
Всего: -2



Автору респект!  
PM MAIL   Вверх
MegaVolt
Дата 14.8.2012, 15:06 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 27
Регистрация: 20.10.2006

Репутация: нет
Всего: нет



Я бы добавил сюда дополнение из статьи про сериализацию объектов 
Цитата
по умолчанию компонент не сериализует вместе с собой ничего. Впрочем, в VCL есть два класса-наследника TComponent, которые сериализуют именно то, чем владеют: это TForm и TDataModule, и если посмотреть их исходный код, можно увидеть, что в них переопределён protected-метод TComponent.GetChildren.

PM MAIL   Вверх
Иванqwe
Дата 19.1.2013, 21:30 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Новичок



Профиль
Группа: Участник
Сообщений: 1
Регистрация: 18.1.2013

Репутация: нет
Всего: нет



подскажите,  что написано на гроуп боксе и на кнопке?
потому что у меня знаки вопроса выскакивают
PM MAIL   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "Delphi: Общие вопросы"
SnowyMetalFan
bemsPoseidon
Rrader

Запрещается!

1. Публиковать ссылки на вскрытые компоненты

2. Обсуждать взлом компонентов и делиться вскрытыми компонентами

  • Литературу по Дельфи обсуждаем здесь
  • Действия модераторов можно обсудить здесь
  • С просьбами о написании курсовой, реферата и т.п. обращаться сюда
  • Вопросы по реализации алгоритмов рассматриваются здесь
  • 90% ответов на свои вопросы можно найти в DRKB (Delphi Russian Knowledge Base) - крупнейшем в рунете сборнике материалов по Дельфи


Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Snowy, MetalFan, bems, Poseidon, Rrader.

 
0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Delphi: Общие вопросы | Следующая тема »


 




[ Время генерации скрипта: 0.2799 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.