Глава 20. Потоки


    Техника объектно-ориентированного программирования и ObjectWindows дают вам мощные средства инкапсуляции кода и данных и большие возможности построения взаимосвязанных структур объектов. Но что делать, если стоит простая задача, например, по хранению некоторых объектов на диске?

    Когда-то данные хранились исключительно в записях, и помещение данных на диск было тривиальной задачей. Но данные в программах ObjectWindows неразрывно связаны с объектами. Конечно, вы можете отделить данные от объекта и записать их в дисковый файл. Объединение дает вам значительный шаг в направлении прогресса, а разъединение отбрасывает вас назад.

    Есть ли в самом объектно-ориентированном программировании и ObjectWindows некоторые средства, которые могли бы разрешить эту проблему? Есть, и это потоки.

    Поток в ObjectWindows - это набор объектов на их пути куда-либо: обычно в файл, EMS, в последовательный порт или некоторое другое устройство. Потоки обслуживают операции ввода-вывода на уровне объектов, а не на уровне данных. При расширении объекта ObjectWindows вам нужно обеспечить обработку определенных вами дополнительных полей. Все сложные аспекты обработки на уровне объектов будут проделаны за вас.

Вопрос: объектный ввод-вывод


    Поскольку вы пишете программы на языке Паскаль, то знаете, что до выполнения операций ввода-вывода с файлом, вы сначала должны сообщить компилятору, какой тип данных вы будете писать и считывать из файла. Файл должен иметь тип, и этот тип должен быть установлен во время компиляции.

    Паскаль реализует в этой связи очень удобное правило: можно организовать доступ к файлу неопределенного типа с помощью процедур BlockWrite и BlockRead. Отсутствие проверки типа возлагает некоторую дополнительную ответственность на программиста, хотя позволяет очень быстро выполнять двоичные операции ввода-вывода.

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

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

Ответ: потоки


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

Полиморфизм потоков


    Потоки ObjectWindows позволяют вам работать с файлами определенного и неопределенного типа: проверка типа имеется, но тип посылаемого объекта не должен обязательно определяться во время компиляции. Смысл в том, что потоки знают, что они имеют дело с объектами, и поскольку все объекты являются производными от TObject, поток может их обработать. В действительности различные объекты ObjectWindows могут также легко записываться в один поток, как и группы идентичных объектов.

Потоки обрабатывают объекты


    Все что вам нужно сделать - это определить для потока, какие объекты ему нужно будет обрабатывать, чтобы он знал, как согласовывать данные с таблицами виртуальных методов. Затем без каких-либо усилий вы можете помещать объекты в поток и извлекать их из потока.

    Но каким образом один и тот же поток может считывать и записывать такие разные объекты как TCollection и TDialog, даже не зная в момент компиляции, какие типы объектов он будет обрабатывать? Это существенно отличается от традиционных операций ввода-вывода языка Паскаль. В действительности потоки могут обрабатывать даже новые типы объектов, которые вообще еще не были созданы к моменту компиляции потока.

    Ответом на это является так называемая регистрация. Каждому типу объекта ObjectWindows (или любому новому производному типу объекта) присваивается уникальный регистрационный номер. Этот номер записывается в поток перед данными объекта. Затем, при считывании объекта из потока, ObjectWindows сначала берет регистрационный номер и на его основании узнает, сколько данных нужно считывать и какие таблицы виртуальных методов подключать к данным.

Смысл использования потоков


    На фундаментальном уровне вы можете рассматривать потоки как файлы языка Паскаль. В своей основе файл языка Паскаль представляет собой последовательное устройство ввода-вывода: вы записываете в него и считываете из него. Поток - это полиморфическое устройство последовательного ввода-вывода, т.е. оно ведет себя, как последовательный файл, но вы можете считывать и записывать различные типы объектов в каждый момент времени.

    Потоки (как и файлы Паскаля) можно также просматривать, как устройства ввода-вывода произвольного доступа, искать определенное место в файле, считывать данные в этой точке или записывать данные в эту точку, возвращать позицию указателя файла и т.д. Все эти операции можно выполнять с потоками, и они описаны в разделе "Потоки с произвольным доступом".

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

Установка потока

    Все что нужно сделать для использования потока - это инициализировать его. Точный синтаксис конструктора Init может быть разным, в зависимости от типа потока, с которым вы имеете дело. Например, если вы открываете поток DOS, вам нужно передать имя файла DOS и режим доступа (только чтение, только запись, чтение/запись) для содержащего поток файла.

    Например, для инициализации буферизированного потока DOS при загрузке набора объектов в программу, все что вам нужно это:

     var
       SaveFile: TBufStream;
     begin
       SaveFile.Init('COLLECT.DTA', stOpen, 1024);
        .
        .

    После инициализации потока все готово к работе.

    TStream это абстрактный механизм потока, поэтому вы будет работать не с ним, а с производными от TStream удобными объектами потока. Это будет, например, TDosStream, для выполнения дисковых операций ввода-вывода, TBufStream для буферизованных операций ввода-вывода (очень удобен для частых операций считывания или записи небольших объемов информации на диск) и TEmsStream для передачи объектов в память EMS. Кроме того, ObjectWindows реализует индексированные потоки с указателем, указывающим место в потоке. Перемещая этот указатель вы можете организовать произвольный доступ в потоке.

Чтение из потока и запись в поток


    Основной объект потока TStream реализует три главных метода, которые вам нужно четко понимать: Get, Put и Error. Get и Put грубо соответствуют процедурам Read и Write, которые вы используете в обычных операциях ввода-вывода. Error - это процедура, которая вызывается при появлении ошибок потока.

Метод Put

    Давайте сначала рассмотрим процедуру Put. Общий синтаксис метода Put следующий:

     SomeStream.Put(PSomeObject);
где SomeStream - это некоторый производный от TStream объект, который был инициализирован, а PSomeObject представляет собой указатель на некоторый производный от TObject объект, который зарегистрирован с потоком. Это все, что вам нужно сделать. Поток может из таблицы виртуальных методов PSomeObject узнать, какой это тип объекта (предполагается, что тип зарегистрирован), поэтому он знает какой номер идентификатора писать, и сколько после него будет данных.

    Специальный интерес для вас, как для программиста, работающего с ObjectWindows, состоит в том факте, что при помещении в поток группы с дочерними окнами, дочерние окна также автоматически помещаются в поток. Таким образом, запись сложных объектов не так уж и сложна, более того, это делается автоматически! Вы можете сохранить полное состояние диалога простым помещением объекта диалога в поток. При повторном запуске вашей программы и загрузке диалога будет выведено его состояние в момент записи.

Метод Get

    Считывание объектов из потока столь же просто. Все что вам нужно сделать, это вызвать функцию Get:

     PSomeObject := SomeStream.Get;

где SomeStream - это инициализированный поток ObjectWindows, а PSomeObject - указатель на некоторый тип объекта ObjectWindows. Get просто возвращает указатель на нечто, что он взял из потока. Сколько данных было взято и какой тип таблицы виртуальных методов (VMT) присвоен данным, определяется не типом PSomeObject, а типом объекта, обнаруженным в потоке. Следовательно, если объект в текущей позиции SomeStream имеет не совпадающий с PSomeObject тип, у вас будет некорректная информация.

    Как и Put, Get ищет сложные объекты. Следовательно, если вы ищите в потоке окно, которое владеет дочерними окнами, то они также будут загружены.

Метод Error

    И, наконец, процедура Error определяет что происходит при возникновении ошибки потока. По умолчанию TStream.Error просто устанавливает значение двух полей в потоке (Status и ErrorInfo). Если вы хотите сделать что-либо более содержательное, например, сгенерировать соответствующее сообщение о сбое в работе программы или вывести блок диалога c сообщением об ошибке, то вам нужно переопределить процедуру Error.

Закрытие потока


    Когда вы закончили использование потока, вы вызываете его метод Done, как вы обычно вызывали Close для дискового файла. Как и для других объектов ObjectWindows, это делается следующим образом:

     Dispose(SomeStream, Done);

как для уничтожения объекта потока, так и для его закрытия.

Как сделать объекты потоковыми


    Все стандартные объекты ObjectWindows готовы к использованию в потоках, и все потоки ObjectWindows узнают стандартные объекты. При изготовлении нового типа объекта, производного от стандартного объекта, его очень просто подготовить к использованию в потоке и известить потоки о его существовании.

Методы загрузки и хранения


    Действительное чтение и запись объектов в поток производится методами Load и Store. Каждый объект должен иметь эти методы для использования потока, поэтому вы никогда не будете вызывать их непосредственно (они вызываются из методов Get и Put.) Все что вам нужно сделать, это убедиться в том, что объект знает, как послать себя в поток, когда это потребуется.

    Благодаря объектно-ориентированному программированию это делается очень просто, т.к. большинство механизмов наследуются от объекта-предка. Все что должен делать ваш объект, это загружать и хранить те свои компоненты, которые вы в него добавляете, об остальном позаботится метод предка. Например, вы производите от TWindow новый вид окна с именем художника-сюрреалиста Рене Магритте, который нарисовал много известных картин с окнами:

     type
      TMagritte = object(TWindow)
        Surreal: Boolean;
        constructor Load(var S: TStream);
        procedure Store(var S: TStream);
         .
         .
         .
      end;

    Все что было добавлено к данным окна - это одно булевское поле. Для загрузки объекта вы просто считываете стандартный TWindow, а затем считываете дополнительный байт булевского поля. Типичные методы Load и Store для производных объектов будут выглядеть следующим образом:

     constructor TMagritte.Load(var S: Stream);
     begin
      TWindow.Load(S);                          { загрузка типа }
      S.Read(Surreal, SizeOf(Surreal));         { чтение
                                           дополнительных полей }
     end;

     procedure TMagritte.Store(var S: Stream);
     begin
      TWindow.Store(S);                       { сохранение типа }
      S.Write(Surreal, SizeOf(Surreal));      { запись
                                           дополнительных полей }
     end;

    Вы должны контролировать, что записывается и загружается один и тот же объем данных, и загрузка данных производится в той же последовательности, что и их запись. Компилятор не покажет ошибки. Если вы будете недостаточно аккуратны, то могут возникнуть серьезные проблемы. Если вы изменяете поля объекта, то нужно изменить и метод Load, и метод Store.

Регистрация потока


    Кроме определения методов Load и Store для новых объектов, вы также должны зарегистрировать этот новый тип объекта в потоках. Регистрация - это простой процесс, который состоит из двух этапов: сначала определяется запись регистрации потока, а затем она передается глобальной процедуре регистрации RegisterType.

    Примечание: ObjectWindows уже имеет зарегистрированными все стандартные объекты, поэтому вам нужно регистрировать только новые, определяемые вами объекты.

    Для определения записи регистрации потока нужно следовать приводимому ниже формату. Запись регистрации потока это запись языка Pascal типа TStreamRec, которая определяется следующим образом:

     PStreamRec = ^TStreamRec;
     TStreamRec = record
        ObjType: Word;
        VmtLink: Word;
        Load: Pointer;
        Store: Pointer;
        Next: Word;
     end;

    По соглашению всем регистрационным записям потока ObjectWindows присваивается то же имя, что и соответствующим типам объектов, но начальное "T" заменяется на "R". Следовательно, регистрационная запись для TCollection будет иметь имя RCollection. Такие абстрактные типы как TObject и TWindowsObject не имеют регистрационных записей, поскольку их экземпляры вы никогда не будете хранить в потоках.

Номера идентификаторов объектов


    Вам действительно нужно думать только о поле ObjType записи, все остальное делается механически. Каждому новому определяемому вами типу требуется его собственный уникальный идентификатор типа в виде числа. ObjectWindows резервирует регистрационные номера от 0 до 99 для стандартных объектов, поэтому ваши регистрационные номера будут лежать в диапазоне от 100 до 65535.

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

Автоматические поля


    Поле VmtLink это связь с таблицей виртуальных методов объектов (VMT). Вы просто задаете его как отклонение типа вашего объекта:

     RSomeObject.VmtLink := Ofs(TypeOf(TSomeObject)^);

    Поля Load и Store содержат соответственно адреса методов Load и Store.

     RSomeObject.Load := @TSomeObject.Load;
     RSomeObject.Store := @TSomeObject.Store;

    Значение последнего поля, Next, задается RegisterType и не требует никакого вмешательства с вашей стороны. Оно просто обеспечивает внутреннее использование скомпонованного списка регистрационных записей потока.

Регистрация на месте


    После конструирования регистрационной записи потока вы вызываете RegisterType с вашей записью в качестве параметра. Поэтому для регистрации вашего нового объекта TMagritte для его использования в потоке вы включаете следующий код:

     const
      RMagritte: TStreamRec = (
        ObjType: 100;
        VmtLink: Ofs(TypeOf(TMagritte)^);
        Load: @TMagritte.Load;
        Store: @TMagritte.Store
      );
     RegisterType(RMagritte);

    Вот и все. Теперь вы можете помещать (Put) экземпляры вашего нового типа объекта в любой поток ObjectWindows и считывать их из потоков.

Регистрация стандартных объектов


    ObjectWindows определяет регистрационные записи потоков для всех его стандартных объектов. Кроме того, модуль WObjects определяет процедуру RegisterWObjects, которая автоматически регистрирует все объекты этого модуля. Например, модуль OWindows содержит процедуру RegisterOWindows, а ODialogs - RegisterODialogs.

Механизм потока


    После того, как мы посмотрели на процесс использования потоков, следует заглянуть во внутреннюю работу, которую производит ObjectWindows c вашими объектами с помощью методов Put и Get. Это прекрасный пример взаимодействия объектов и использования встроенных в них методов.

Процесс Put


    Когда вы посылаете объект в поток с помощью метода Put, поток сначала берет указатель VMT со смещением 0 от объекта и просматривает список зарегистрированных типов потоков системы с целью найти совпадение. Когда это совпадение найдено, поток ищет регистрационный номер идентификатора объекта и записывает его в поток. Затем поток вызывает метод Store объекта для завершения записи объекта. Метод Store использует процедуру потока Write, которая действительно пишет корректное число байт в поток.

    Ваш объект не должен ничего знать о потоке - это может быть файл на диске, память EMS или любой другой вид потока - ваш объект просто говорит "запишите меня в поток", и поток делает все остальное.

Процесс Get


    Когда вы считываете объект из потока с помощью метода Get, сначала ищется номер его идентификатора, и просматривается на совпадение список зарегистрированных типов. После обнаружения совпадения регистрационная запись дает потоку местоположение метода Load объект и VMT. Затем для чтения нужного объема данных из потока вызывается метод Load.

    Вы опять просто говорите потоку, что нужно взять (Get) следующий объект и поместить его в место, определяемое заданным вами указателем. Ваш объект даже не беспокоится о том, с каким потоком он имеет дело. Поток сам беспокоится о считывании нужного объема данных из потока с помощью метода объекта Load, который в свою очередь опирается на метод потока Read.

    Для программиста все это достаточно прозрачно, но в то же время вы ясно должны понять, насколько важно зарегистрировать тип до проведения каких-либо попыток ввода-вывода с потоком.

Обработка указателей объектов со значением nil


    Вы можете записать в поток объект nil. Однако, если это сделать, то в поток запишется слово 0. При считывании идентификатора слова 0 поток возвратит указатель nil. Поэтому 0 считается зарезервированным и не может использоваться в качестве номера идентификатора объекта потока. ObjectWindows резервирует идентификатор потока от 0 до 99 для внутреннего использования.

Наборы в потоке: пример


    В Главе 19, "Наборы", вы уже видели как наборы могут содержать разные, но связанные объекты. Это свойство полиморфизма также применимо и к потокам, и их можно использовать для записи наборов на диск для последующего обращения, даже в другой программе. Вернемся к примеру COLLECT4.PAS. Что еще нужно добавить в эту программу для помещения набора в поток?

    Ответ будет очень простым. Сначала возьмем базовый объект TGraphObject и "научим" его хранить его данные (X и Y) в потоке. Для этого нужен метод Store. Затем определим новый метод Store для любого производного от TGraphObject объекта, в котором добавляются дополнительные поля (например, TGraphPie добавляет ArcStart и ArcEnd).

    Затем построим регистрационную запись для каждого типа объекта, который предполагается хранить, и зарегистрируем все эти типы при первом запуске вашей программы. Вот и все. Остальное будет подобно обычным операциям ввода-вывода в файл: определяется переменная потока; создается новый поток; одним простым оператором весь набор помещается в поток, и поток закрывается.

Добавление методов Store


    Приведем методы Store. Обратите внимание, что для PGraphEllipse и PGraphRect не требуются свои собственные методы, т.к. они не добавляют новых полей к унаследованным от PGraphObject:

     type
      PGraphObject = ^TGraphObject;
      TGraphObject = object(TObject)
       Rect: TRect;
       constructor Init(Bounds: TRect);
       procedure Draw(DC: HDC); virtual;
       procedure Store(var S: TStream); virtual;
     end;

     PGraphEllipse = ^TGraphEllipse;
     TGraphEllipse = object(TGraphObject)
      procedure Draw(DC: HDC); virtual;
     end;

     PGraphRect = ^TGraphRect;
     TGraphRect = object(TGraphObject)
      procedure Draw(DC: HDC); virtual;
     end;

     PGraphPie = ^TGraphPie;
     TGraphPie = object(TGraphObject)
      ArcStart, ArcEnd: TPoint;
      constructor Init(Bounds: TRect);
      procedure Draw(DC: HDC); virtual;
      procedure Store(var S: TStream); virtual;
     end;

    Реализация метода Store вполне очевидна. Каждый объект вызывает свой унаследованный метод Store, который хранит все унаследованные данные. Затем вызывается метод Write для записи дополнительных данных:

     procedure TGraphObject.Store(var S: TStream);
     begin
       S.Write(Rect, SizeOf(Rect));
     end;
     procedure TGraphPie.Store(var S: TStream);
     begin
       TGraphObject.Store(S);
       S.Write(ArcStart, SizeOf(ArcStart));
       S.Write(ArcEnd, SizeOf(ArcEnd));
     end;

    Обратите внимание, что метод TStream Write делает двоичную запись. Его первый параметр может быть переменной любого типа, но TStream.Write не может узнать размеры этой переменной. Второй параметр содержит эту информацию, и вы должны придерживаться соглашения относительно использования стандартной функции SizeOf. Таким образом, компилятор всегда может гарантировать, что вы всегда считываете и записываете нужное количество данных.

Регистрация записей


    Наш последний шаг состоит в определении константы регистрационной записи для каждого производного типа. Хороший прием программирования состоит в следовании соглашению ObjectWindows относительно использования для имени типа идентификатора, где вместо первой буквы T ставится R.

    Помните о том, что каждой регистрационной записи присваивается уникальный номер идентификатора объекта (ObjType). Номера от 0 до 99 резервируются ObjectWindows для стандартных объектов. Хорошо бы отслеживать все номера идентификаторов ваших объектов потока в некотором центральном месте, чтобы избежать дублирования.

     const
      RGraphEllipse: TStreamRec = (
       ObjType: 150;
       VmtLink: Ofs(TypeOf(TGraphEllipse)^);
       Load: nil;               { метод загрузки отсутствует }
       Store: @TGraphEllipse.Store);
      RGraphRect: TStreamRec = (
       ObjType: 151;
       VmtLink: Ofs(TypeOf(TGraphRect)^);
       Load: nil;               { метод загрузки отсутствует }
       Store: @TGraphRect.Store);
      RGraphPie: TStreamRec = (
       ObjType: 152;
       VmtLink: Ofs(TypeOf(TGraphPie)^);
       Load: nil;               { метод загрузки отсутствует }
       Store: @TGraphPie.Store);

    Вам не нужно регистрационная запись для TGraphObject, так как это абстрактный тип, и он никогда не будет помещаться в набор или в поток. Указатель Load каждой регистрационной записи устанавливается в nil, поскольку в данном примере рассматривается только помещение данных в поток. В следующем примере методы Load будут определены, и изменены регистрационные записи (см. STREAM2.PAS).

Регистрация


    Всегда нужно зарегистрировать каждую из этих записей до проведения каких-либо операций ввода-вывода с потоком. Самый простой способ сделать это состоит в том, чтобы объединить их все в одну процедуру и вызвать ее в самом начале вашей программы (или в методе Init вашего приложения):

     procedure StreamRegistration;
     begin
       RegisterType(RCollection);
       RegisterType(RGraphEllipse);
       RegisterType(RGraphRect);
       RegisterType(RGraphPie);
     end;

    Обратите внимание, что вам нужно зарегистрировать TCollection (используя его запись RCollection - теперь вы видите, что соглашения о присваивании имен упрощают программирование), хотя вы и не определили TCollection. Правило очень простое и незабываемое именно вы отвечаете за регистрацию каждого типа объекта, который ваша программа будет помещать в поток.

Запись в поток


    Нужно следовать обычной последовательности операций ввода-вывода в файл: создать поток; поместить в него данные (набор); закрыть поток. Вам не нужно писать итератор ForEach для помещения в поток каждого элемента набора. Вы просто говорите потоку, что нужно поместить (Put) набор в поток:

     var
       .
       .
       .
      GraphicsStream: TBufStream;
     begin
       .
       .
       .
      StreamRegistration;    { регистрация всех объектов потока }
      GraphicsStream.Init('GRAPH.SMT', stCreate, 1024);
      GraphicsStream.Put(GraphicsList);        { выходной набор }
      if GraphicsStream.Status <> 0 then
       Status:=em_Stream;
      GraphicsStream.Done;                       { сброс потока }
     end;

    В результате создастся файл на диске, который содержит всю информацию, необходимую для "считывания" набора назад в память. Когда поток открыт, и ищется набор, то (см. STREAM2.PAS) "магически" восстанавливаются все скрытые связи между набором и его элементами, объекты и их таблицы виртуальных методов. Следующий раздел поясняет, как помещать в поток объекты, которые содержат связи с другими объектами.

Как все хранится?


    Относительно потоков нужно сделать важное предостережение: только владелец объекта должен записывать его в поток. Это предостережение аналогично традиционному предостережению языка Паскаль, которое вам должно быть известно: только владелец указателя может уничтожить его.

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

Поля в потоке


    Много раз вы видели, что удобно хранить указатель на дочерние окна группы в локальной переменной (поле данных объекта). Например, блок диалога может хранить указатель на его объекты управления в полях с мнемоническими именами для более удобного доступа (OKButton или FileINputLine). При создании такого дочернего окна порождающее окно будет иметь на него два указателя, один в поле, и еще один - в списке дочерних окон. Если на это не обратить внимания, то считывание такого объекта из потока приведет к дублированию.

    Решение состоит в использовании методов TWindowsObject GetChildPtr и PutChildPtr. При хранении поля, которое является дочерним окном, вместо записи указателя, как если бы это была простая переменная, вы вызываете метод PutChildPtr, который записывает ссылку на позицию дочернего окна в списке дочерних окон группы. Аналогично, при загрузке (Load) группы из потока, вы вызываете GetChildPtr, который гарантирует, что поле и список дочерних окон указывают на один и тот же объект. Приведем короткий пример использования GetChildPtr и PutChildPtr в простом окне:

     type
       TDemoWinodw = object(TWindow)
       Msg: PStatic;
       constructor Load(var S: TStream);
       procedure Store(var S: TStream);
     end;

     constructor TDemoWindow.Load(var S: TStream);
     begin
       TWindow.Load(S);
       GetChildPtr(S, Msg);
     end;

     procedure TDemoWindow.Store(var S: TStream);
     begin
       TWindow.Store(S);
       PutChildPtr(S, Msg);
     end;

    Давайте рассмотрим, чем этот метод Store отличается от обычного Store. После обычного сохранения окна все что нам нужно сделать, это записать ссылку на поле Msg, вместо записи самого поля, как мы это обычно делали. Действительный объект кнопки хранится в виде дочернего окна для окна, которое вызывается TWindow.Store.

    Кроме этого нужно поместить эту информацию в поток с указанием того, что Msg указывает на это дочернее окно. Метод Load производит обратные действия, сначала загружая окно и его дочернее окно командной кнопки, а уже затем восстанавливая указатель на это дочернее окно через Msg.

Родство экземпляров окон


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

    Как и для дочерних окон, при чтении и записи родственных ссылок в поток могут возникнуть проблемы. Решение также будет аналогичным. Методы TWindowsObject PutSiblingPtr и GetSiblingPtr предоставляют средства доступа к родственникам:

     type
      TActivateRadioButton=object(TRadioButton)
      EditControl: PEdit;
        .
        .
        .
      constructor Load(var S: TStream);
      procedure Store(var S: TStream); virtual;
        .
        .
        .
     end;

     constructor TActivateRadioButton.Load(var S: TStream);
     begin
       TRadioButton.Load(S);
       GetPeerPtr(S, EditControl);
     end;

     procedure TActivateRadioButton.Store(var S: TStream);
     begin
       TRadioButton.Load(S);
       PutPeerPtr(S, EditControl);
     end;

    Единственно о чем нужно побеспокоиться, так это о загрузке ссылок на родственные окна, которые еще не загружены (т.е. в списке дочерних окон они идут ниже и, следовательно, позднее в потоке). ObjectWindows автоматически обрабатывает эту ситуацию, отслеживая все подобные будущие ссылки и разрешая их после загрузки всех дочерних окон. Вам нужно иметь в виду, что родственные ссылки не будут действовать, пока Load не выполнится целиком. Принимая это во внимание, вы не должны помещать в Load никакой код, который использует дочерние окна, которые зависят от их родственных окон, поскольку в этом случае результат может быть непредсказуемым.

Копирование потока


    TStream имеет метод CopyFrom(S,Count), который копирует заданное число байт (Count) из заданного потока S. Метод CopyFrom может быть использован для копирования содержимого одного потока в другой. Если, например, вы циклически обращаетесь к дисковому потоку, то можете скопировать его в поток EMS для организации более быстрого доступа:

     NewStream := New(TEmsStream, Init(OldStream^.GetSize));
     OldStream^.Seek(0);
     NewStream^.CopyFrom(OldStream, OldStream^.GetSize);

Потоки произвольного доступа


    До этого момента мы работали с потоками как с устройствами последовательного доступа: вы помещали (Put) объекты в конец вашего потока и считывали их назад (Get) в той же последовательности. Но ObjectWindows имеет и более мощные средства. Имеется возможность рассматривать поток как виртуальное устройство произвольного доступа. Кроме методов Get и Put, которые соответствуют Read и Write при работе с файлом, потоки обладают средствами проведения операций Seek, FilePos, FileSize и Truncate.

    - Процедура потока Seek перемещает текущий указатель потока к заданной позиции (число байт от начала потока), как стандартная процедура Seek языка Паскаль.

    - Процедура GetPos по своему действию обратна процедуре Seek. Она возвращает значение Longint с текущей позицией потока.

    - Функция GetSize возвращает размер потока в байтах.

    - Процедура Truncate удаляет все данные, которые расположены после текущей позиции потока, при этом текущая позиция потока становится концом потока.

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

Необъектные элементы потоков


    В поток можно записывать и элементы, которые не являются объектами, но для этого следует использовать несколько иной подход. Стандартные методы потока Get и Put требуют загрузки или записи объекта, производного от TObject. Если вам нужно создать поток, который состоит не из объектов, переходите на нижний уровень процедур Read и Write, где в поток записывается или из него считывается заданное число байт. Этот же механизм используют методы Get и Put для чтения и записи данных об объектах. Вы просто обходите механизм VMT, который заложен в Put и Get.

Разработка пользователем собственных потоков


    Данный раздел суммирует возможности методов и обработки ошибок потоков ObjectWindows, чтобы вы знали, что можно использовать для создания новых типов потоков.

    Сам TStream является абстрактным объектом и его можно расширить для создания удобного типа потока. Большинство методов TStream являются абстрактными и должны быть реализованы как их производные методы, основывающиеся на абстрактных методах TStream. Полностью реализованы только методы Error, Get и Put. GetPos, GetSize, Read, Seek, SetPos, Truncate и Write должны быть переписаны. Если производный тип объекта имеет буфер, то должен быть переписан и метод Flush.

Обработка ошибок потока


    TStream имеет метод Error(Code, Info), который вызывается при обнаружении ошибки потока. Error просто присваивает полю Status потока значение одной из констант, приведенных в Главе 21 "Справочник по ObjectWindows" в разделе "Константы stXXXX".

    Поле ErrorInfo не определено, если значение Status не есть stGetError или stPutError. Если значение поля Status равно stGetError, то поле ErrorInfo содержит номер идентификатора потока незарегистрированного типа. Если значение поля Status равно stPutError, то поле ErrorInfo содержит смещение VMT типа, который вы пытались поместить в поток. Вы можете переписать TStream.Error для генерации любого уровня обработки ошибок, включая ошибки этапа выполнения.