Примеры применения потоковой системы VCL

Абдулин Марат

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

Потоковая система предназначена для решения самых разных прикладных задач. К их числу можно отнести:

  • сохранение результатов редактирования и защита от сбоев;
  • взаимодействие с буфером обмена;
  • клонирование объектов;
  • сохранение данных объекта в базе данных;
  • передачу данных объекта между границами процессов;
  • чтение данных формы из файла;
  • поддержку сервисов перманентности [4] семейства интерфейсов IPersist*.

Основная идея

В основу рассматриваемых здесь решений положена идея представления данных объекта в виде потока байтов. С таким потоком можно сделать практически все что угодно: записать на диск, сохранить в memo-поле, упаковать в строку или OleVariant, поместить в буфер обмена и проделать многие другие манипуляции. Чтобы представить данные объекта в виде потока байтов, совсем не обязательно писать собственные процедуры - они уже реализованы в потоковой системе.

Основное требование - объект должен быть компонентом, пусть невизуальным, пусть не зарегистрированным в палитре компонентов, достаточно того, чтобы он был наследником класса TComponent.

Если у вас уже есть такой объект, то сохранение его данных в потоке и восстановление из потока не представляет особой сложности. Сохранить данные объекта в потоке можно при помощи методов TStream.WriteComponent или TStream.WriteComponentRes, а восстановить - при помощи методов TStream.ReadComponent и TStream.ReadComponentRes соответственно. Указанные методы работают с разными форматами потока данных компонента. Если используются методы WriteComponent и ReadComponent, то в потоке хранятся только данные компонента. Если WriteComponentRes и ReadComponentRes, то в потоке присутствует заголовок Windows-ресурса, за которым следуют данные самого компонента, такой поток является Windows-ресурсом.

Сохранение результатов редактирования и защита от сбоев

Сохранение данных объекта в потоке используется прежде всего для сохранения объекта в файле. В VCL в модуле Classes реализованы процедуры WriteComponentResFile и ReadComponentResFile. Первая сохраняет данные объекта в бинарном файле, а вторая восстанавливает данные объекта из бинарного файла. Если же вы хотите сохранять данные в текстовом формате, то потоковая система поможет выполнить преобразование бинарного формата в текстовый (ObjectBinaryToText) и обратно (ObjectTextToBinary).
Продемонстрирую сохранение в поток и преобразование в текстовый формат на примере:

// Процедура записи компонента Instance в текстовый поток Stream
procedure SaveComponentAsTextStream(Stream: TStream; Instance: TComponent);
var
inS: TMemoryStream;
begin
inS := TMemoryStream.Create;
try
// Записать компонент в поток в памяти
inS.WriteComponent(Instance);
// Вернуть смещение
inS.Position := 0;
// Преобразовать бинарные данные в текстовые
ObjectBinaryToText(inS, Stream);
finally
inS.Free;
end;
end;

// Процедура чтения компонента Instance из текстового потока Stream
procedure LoadComponentAsTextStream(Stream: TStream; Instance: TComponent);
var
outS: TMemoryStream;
begin
outS := TMemoryStream.Create;
try
// Преобразовать текст в бинарные данные
ObjectTextToBinary(Stream, outS);
// Вернуть смещение
outS.Position := 0;
// Прочитать компонент из потока
outS.ReadComponent(Instance);
finally
outS.Free;
end;
end;

Главный недостаток сохранения данных компонента в текстовом формате - длительное время выполнения процедур преобразования одного формата в другой, в особенности при вызове процедуры ObjectTextToBinary. Если смириться с этим недостатком, то вы получаете чрезвычайно простую реализацию, универсальность и, конечно же, текстовый формат данных. Файлы в таком формате можно читать и править в любом текстовом редакторе. Очень удобно в таком файле хранить параметры конфигурации программы (без особых усилий вы получаете своего рода ini-файл); данные программы-редактора (вам не нужно будет придумывать собственные форматы хранения данных и писать нетривиальные процедуры преобразования программного представления данных в текстовое и наоборот); и, например, критически важные программные данные, которые программа систематически сохраняет на диске (такие данные легче читать и анализировать).

Взаимодействие с буфером обмена

Пользователи (в том числе и программисты) привыкли к тому, что в любом редакторе есть функции взаимодействия с буфером обмена. Реализация этих функций - не такое уж сложное дело. Данные компонента можно поместить в буфер обмена с помощью метода TClipboard.SetComponent, и извлечь из буфера при помощи метода TClipboard.GetComponent. Эти методы помещают и извлекают данные компонента в формате CF_COMPONENT, который регистрируется библиотекой VCL (модуль Clipbrd) в момент загрузки программы.

Формат CF_COMPONENT будет понятен только вашей программе, и это вполне устроит конечных пользователей, но, вполне возможно, не устроит вас - программиста, потому что функции взаимодействия с буфером обмена по непонятным причинам не работает, и вы уже не первый день пытаетесь понять почему. Разобраться в этом помогут процедуры CopyComponentAsText и PasteComponentAsText, которые для взаимодействия с буфером обмена используют текстовый формат CF_TEXT:

// Процедура копирования данных компонента Instance в буфер обмена в текстовом формате
procedure CopyComponentAsText(Instance: TComponent);
const
END_OF_STR: Char = #0;
var
inS: TMemoryStream;
outS: TMemoryStream;
begin
outS := nil;
inS := TMemoryStream.Create;
try
// Записать компонент в поток
inS.WriteComponent(Instance);
inS.Position := 0;
outS := TMemoryStream.Create;
// Преобразовать записанные данные в текстовый формат
ObjectBinaryToText(inS, outS);
// Записать терминальный символ
outS.WriteBuffer(END_OF_STR, 1);
// Записать данные в буфер обмена
Clipboard.SetTextBuf(outS.Memory);
finally
inS.Free;
outS.Free;
end
end;

// Процедура восстановления данных компонента Instance из буфера обмена
function PasteComponentAsText(Instance: TComponent): Boolean;

procedure GetClipboardTextToStream(Stream: TStream);
var
S: String;
begin
S := Clipboard.AsText;
Stream.WriteBuffer(PChar(S)^, Length(S));
end;

var
inS: TMemoryStream;
outS: TMemoryStream;
begin
Result := False;
if not Clipboard.HasFormat(CF_TEXT) then Exit;

outS := nil;
inS := TMemoryStream.Create;
try
try
GetClipboardTextToStream(inS);
inS.Position := 0;
outS := TMemoryStream.Create;
ObjectTextToBinary(inS, outS);
outS.Position := 0;
outS.ReadComponent(Instance);
Result := True;
except
{};
end;
finally
inS.Free;
outS.Free;
end;
end;

Имейте в виду, что преобразование бинарного формата в текстовый и обратно займет некоторое время. Впрочем, конечные пользователи заметят это, только если к процедуре обратиться несколько раз подряд, например в цикле.

Клонирование объектов

Другой пример применения потоковой системы - создание точной копии объекта (клонирование объекта). Функция клонирования объекта представляет собой реализацию классического шаблона проектирования (design pattern) Prototype (см. [1]), суть которого заключается в конструировании объекта по его прототипу.

// Создает клон компонента Instance, устанавливает у клона свойства Owner, Parent
function CloneComponent(AOwner, AParent, Instance: TComponent): TComponent;
var
SaveName: TComponentName;
begin
if Instance = nil then
begin
Result := nil;
Exit;
end;
SaveName := Instance.Name;
try
Instance.Name := ''; // чтобы имя компонента не попало в поток
Result := TComponentClass(Instance.ClassType).Create(AOwner);
try
CopyComponent(Instance, Result, AParent);
except
Result.Free;
Result := nil;
raise;
end;
finally
Instance.Name := SaveName;
end;
end;

// Копирует данные из компонента SrcComp в DstComp, если необходимо, устанавливает родителя у компонента DstComp
procedure CopyComponent(SrcComp, DstComp, AParent: TComponent);
var
S: TMemoryStream;
begin
if not (DstComp is SrcComp.ClassType) then
raise Exception.CreateFmt('Данные экземпляра класса %s не могут быть'+
' присвоены экземпляру класса %s',
[SrcComp.ClassName, DstComp.ClassName]);
S := TMemoryStream.Create;
try
S.WriteComponent(SrcComp);
S.Position := 0;
ReadComponentParented(nil, AParent, DstComp, S);
finally
S.Free;
end;
end;

// Читает компонент Instance из потока S
procedure ReadComponentParented(AOwner, AParent: TComponent;
var Instance: TComponent; S: TStream);
var
R: TReader;
begin
R := TReader.Create(S, 256);
try
R.Parent := AParent;
if Instance = nil then
Instance := R.ReadRootComponent(AOwner)
else
R.ReadRootComponent(Instance);
finally
R.Free;
end;
end;

Функция CloneComponent вызывает процедуру CopyComponent, в которой данные SrcComp записываются в поток, а затем читаются из потока в DstComp. Хотел бы обратить ваше внимание на то, что при записи компонента SrcComp в поток попадают только значения, отличные от значений по умолчанию. Поэтому не удивляйтесь, если при прямом использовании процедуры CopyComponent (т.е. при использовании этой процедуры в обход CloneComponent) вы обнаружите различия в значениях свойств в SrcComp и DstComp. Это может произойти, потому что компонент SrcComp «умолчит» о тех свойствах, значения которых совпадают со значениями, устанавливаемыми в его конструкторе. Для решения этой проблемы необходимо перед вызовом процедуры CopyComponent установить у свойств компонента DstComp значения по умолчанию. Код установки значений по умолчанию можно вынести в специальный метод компонента, скажем, SetDefaults, который можно будет вызывать как из конструктора, так и напрямую. Замечу, что при вызове процедуры CopyComponent из CloneComponent такой проблемы не будет, поскольку в процедуре CloneComponent новый компонент сначала конструируется (и его свойства приобретают значения по умолчанию), а затем в него копируются данные из исходного SrcComp-компонента.

Сохранение данных объекта в базе данных

Еще один пример применения потоковой системы - сохранение данных объекта в memo-поле записи таблицы. Большинство реляционных базы данных не поддерживает прямое сохранение данных объектов в таблице, но эту функциональность можно реализовать самому. Чтобы сохранить данные объекта в memo-поле необходимо преобразовать их в поток байтов, а затем поместить в Memo-поле. Вот процедуры сохранения и восстановления данных объекта в Memo-поле:

// Записывает компонент в Blob-поле
procedure ComponentToBlobField(Instance: TComponent; AField: TBlobField);
var
S: TMemoryStream;
begin
S := TMemoryStream.Create;
try
// Сохранить данные компонента в потоке
S.WriteComponent(Instance);
S.Position := 0;
// Сохранить данные потока в Blob-поле
AField.LoadFromStream(S);
finally
S.Free;
end;
end;

// Читает данные компонента из Blob-поля
procedure BlobFieldToComponent(AField: TBlobField; Instance: TComponent);
var
S: TMemoryStream;
begin
S := TMemoryStream.Create;
try
// Сохранить данные Blob-поля в потоке
AField.SaveToStream(S);
S.Position := 0;
// Прочитать данные компонента из потока
S.ReadComponent(Instance);
finally
S.Free;
end;
end;

Передача данных объекта между процессами

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

Передачу данных объекта проще всего осуществить, используя механизм Automation. Большие массивы бинарной (несимвольной) информации принято передавать серверу Automation при помощи параметров типа OleVariant. Например, передачу данных объекта серверу Automation можно выполнить так: вызывающая сторона упаковывает данные объекта в OleVariant и передает их серверу Automation, а принимающая сторона, получает данные и извлекает их из OleVariant.

Упаковкой данных объекта в OleVariant занимается процедура ComponentToVariant, а извлечением данных - процедура VariantToComponent:

// Упаковка компонента в Variant
function ComponentToVariant(Instance: TComponent): OleVariant;
var
S: TMemoryStream;
varData: Pointer;
begin
S := TMemoryStream.Create;
try
// Сохранить компонент в потоке
S.WriteComponent(Instance);
// Создать Variant нужной вместимости
Result := VarArrayCreate([0, S.Size - 1], varByte);
varData := VarArrayLock(Result);
try
S.Position := 0;
// Скопировать данные из потока в Variant
S.Read(VarData^, S.Size);
finally
VarArrayUnlock(Result);
end;
finally
S.Free;
end;
end;

// Извлечение компонента из Variant
procedure VariantToComponent(const V: OleVariant; Instance: TComponent);
var
S: TMemoryStream;
P: Pointer;
begin
P := VarArrayLock(V);
try
S := TMemoryStream.Create;
try
// Скопировать данные из Variant в поток
S.Write(P^, VarArrayHighBound(V, 1) - VarArrayLowBound(V, 1) + 1);
S.Position := 0;
// Прочитать компонент из потока
S.ReadComponent(Instance);
finally
S.Free;
end;
finally
VarArrayUnlock(V);
end;
end;

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

Чтение данных формы из файла

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

Простейший способ решения проблемы - поставлять пользователю стандартную форму и несколько дополнительных форм. Универсальное решение - поставлять конструктор форм, с помощью которого пользователь самостоятельно добавит нужные поля и удалит ненужные.
Стандартную форму можно хранить как ресурс приложения, а дополнительные формы или формы, сконструированные пользователем, можно хранить во внешнем хранилище (в файле, в Memo-поле базы данных, в потоке структурированного хранилища и т.д.)

Данные формы можно напрямую записать в поток методом TStream.WriteComponentRes. Небольшие сложности возникают при чтении данных формы из потока; чтобы корректно прочитать форму из потока придется позаимствовать код TApplication.CreateForm

// Форма должна быть записана в поток при помощи процедуры TStream.WriteComponentRes
// При помощи этой процедуры нельзя конструировать
// Application.MainForm
procedure CreateFormFromStream(AOwner: TComponent; InstanceClass: TComponentClass;
var Reference; Stream: TStream);
var
Instance: TComponent;
dm: TDataModule;
fm: TCustomForm;
begin
Instance := TComponent(InstanceClass.NewInstance);
TComponent(Reference) := Instance;
try
if Instance is TDataModule then
begin
dm := TDataModule(Instance);
dm.CreateNew(AOwner);
Stream.ReadComponentRes(dm);
end
else
if Instance is TCustomForm then
begin
fm := TCustomForm(Instance);
fm.CreateNew(AOwner);
Stream.ReadComponentRes(fm);
end;
except
Instance.Free;
TComponent(Reference) := nil;
raise;
end;
end;

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

Поддержка сервисов перманентности в COM-объектах

В отличие от объектов Delphi, COM-объекты должны самостоятельно реализовывать сервисы записи и чтения собственных данных. Клиенты COM-объектов получают доступ к этим сервисам (еще их называют сервисами перманентности [4]) через один из интерфейсов семейства IPersist* (IPersistFile, IPersistMemory, IPersistStream, IPersistStorage, IPersistMoniker). Если ваш COM-объект должен поддерживать один из этих интерфейсов, то для их реализации также можно воспользоваться потоковой системой VCL.

Рассмотрим реализацию сервисов перманентности на примере интерфейса IPersistStream. Данные COM-объекта мы будем хранить в специальном компоненте, а реализацию методов интерфейса IPersistStream делегируем вспомогательному классу TOlePersist, у которого будет ссылка на компонент, хранящий данные COM-объекта. При таком подходе класс TOlePersist можно будет использовать в разных реализациях COM-объектов.

Наибольший интерес в интерфейсе IPersistStream представляют методы IPersistStream.Load и IPersistStream.Save. Их реализация приводится ниже:

function TolePersist.PersistStreamLoad(const stm: IStream): HResult;
var
Temp: TComponent;
S: TOleStream;
SavePosition: Integer;
begin
if stm = nil then
begin
Result := E_POINTER;
Exit;
end;
try
S := TOleStream.Create(stm);
try
// Убедиться в том, что компонент можно прочитать
SavePosition := S.Position;
Temp := TComponentClass(fComponent.ClassType).Create(nil);
try
S.ReadComponent(Temp);
finally
Temp.Free;
S.Position := SavePosition;
end;
// теперь можно прочитать сам компонент
S.ReadComponent(fComponent);
finally
S.Free;
end;
Result := S_OK;
except
Result := E_UNEXPECTED;
end;
end;

function TolePersist.PersistStreamSave(const stm: IStream; fClearDirty: BOOL): HResult;
var
S: TOleStream;
begin
if stm = nil then
begin
Result := E_POINTER;
Exit;
end;
try
S := TOleStream.Create(stm);
try
S.WriteComponent(fComponent);
finally
S.Free;
end;
if fClearDirty then SetModified(False);
Result := S_OK;
except
Result := E_UNEXPECTED;
end;
end;

Реализацию других методов интерфейса IPersistStream вы можете найти в демонстрационной программе, которую можно скачать с сайта www.programme.ru. Там же вы сможете найти и код других примеров, описанных в статье.

Заключение

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

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

Если вы еще не знакомы с потоковой системой, советую обратить внимание на литературу, приведенную в библиографии, в частности на две книги Рея Лишнера [2] и [3], в них содержится информация в достаточном объеме. Здесь я только вскользь упомянул особенности самой потоковой системы, этой теме нужно посвящать отдельную статью, а, лучше целую серию статей. Надеюсь, моя статья подвигнет других программистов поделиться знаниями.

Применяя потоковую систему начиная с первой версии Delphi, я продолжаю открывать для себя ее новые возможности, и недавно обнаружил несколько статей Андрея Чудина, связанных с этой темой, собственно он и стал автором идеи публикации данной статьи в журнале «Программист», за что я выражаю ему свою признательность. Я рекомендую изучить и его статьи и взять на вооружение описываемые там процедуры, без них библиотеку нельзя будет назвать полной.

Библиография

  1. Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования. - СПб: Питер, 2001. (серия «Библиотека программиста»).
  2. Лишнер Рей. Секреты Delphi 2: Пер. с англ./Рей Лишнер. - К.: НИПФ «ДиаСофт» Лтд., 1996.
  3. Лишнер Р. Delphi. Справочник. - Пер. с англ. - СПб: Символ-Плюс, 2001.
  4. Дэвид Чеппел. Технология ActiveX и OLE/Пер. с англ. - М.: Издательский отдел «Русская Редакция» ТОО «Channel Trading Ltd», 1997.

Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=3344