Использование DB Controls без базы данных
С другой стороны, в случае, если операций вставки производится существенно больше, чем операций поиска по закладке (путем присваивания свойству Bookmark, т.е. не считая вызовов InternalSetToRecord), может оказаться выгоднее (и уж точно – проще) искать записи последовательным перебором, а InternalSetToRecord реализовать так, как описано ранее (DoBeforeInsert и InternalInsert). Разумеется, дилемма отпадает, если вставку всех записей производить как добавление в конец набора – тогда можно просто принять в качестве закладки значение физического курсора. Чуть не забыл! Insert и Append – не единственные методы для вставки записей. Если имеется буфер записи, в который уже записаны значения полей, то можно добавить в набор запись с такими значениями полей вызовом InsertRecord. Этот метод обращается к AddRecord, а тот, в свою очередь, к InternalInsertRecord. Такой способ добавления записи тоже нужно обрабатывать. Native-формат данных Приведу обещанное описание Native-формата данных:·Данные для поля (каждому полю соответствует экземпляр потомка TField), имеющего DataType=ftInteger или ftLargeint, в Native-формате представлены просто в виде числа типа longint (или, что то же самое, Integer); данные для ftSmallint – числом типа Smallint, ftWord – типа Word. ·Для полей с DataType=ftString данные хранятся в виде последовательности символов, завершающейся #0 (символом с кодом 0); длина последовательности вместе с #0 не должна превышать значение_поля_Size + 1. Если поле Transliterate данного экземпляра TStringField равно True, то потомок TDataSet должен предоставить метод Translate для преобразования данных из кодировки символов, используемой в конкретном наборе данных, в кодировку ANSI (и обратно). В том виде, как он реализован в TDataSet, метод Translate просто возвращает исходную строку. ·Для полей с DataType=ftWideString данные хранятся в формате WideString. ·Для полей ftFloat и ftCurrency – в виде числа типа double. ·Для полей ftBoolean – в виде WordBool. ·Для полей ftDateTime, ftDate и ftTime – в виде TDateTime. ·Для полей с DataType=ftTimeStamp – в виде TSQLTimeStamp (модуль SqlTimSt). ·Для полей ftBytes и ftVarBytes – в виде последовательности байтов. ·Для полей ftBCD и ftFMTBCD – в виде TBcd (модуль FMTBcd). ·Для полей ftBLOB, ftMemo, ftGraphic данные так же, как и для ftBytes, хранятся в виде последовательности байтов; потомок TDataSet обязан предоставить метод CreateBlobStream для создания объекта-потомка TStream, обеспечивающего доступ к данным. ·Для полей ftReference – в виде массива байтов. ·Для полей ftVariant – в любом виде, совместимом с одним из представлений данных в Variant (см. описание Variant в справочной системе Delphi; подробное описание доступно в MSDN (ключевые слова, например, – “VARIANT and VARIANTARG”). ·Для полей ftInterface, ftIDispatch – в виде указателя на интерфейс. ·Для полей ftGuid – в виде строки, которая может быть расшифрована вызовом StringToGuid. ·Для полей ftADT и ftArray – определяются типами содержащихся в них полей (каждое поле читается отдельно). ·Для полей ftDataSet – насколько я понял, формат хранимых данных не имеет значения; потомок TDataSet обязан предоставить метод CreateNestedDataset для создания экземпляра нужного потомка TDataSet на основе содержащейся в данном поле информации. Упрощение интерфейса Когда я только начинал писать эту статью, основной моей целью было показать, что интерфейс между DBGrid и, например, массивом можно наладить достаточно легко. Но, возможно, для таких простых ситуаций имеет смысл пойти дальше и разработать свой собственный интерфейс как надстройку над классом TDataSet, для обеспечения функционирования которого достаточно будет перекрыть всего десяток методов. Последовав советам, я решил вместо перекрывания методов ввести систему событий. С учетом сказанного, второй пример будет состоять из двух частей: TMyDataSet – переходник, обеспечивающий упрощенное взаимодействие с методами TDataSet, используя обычный для Delphi механизм событий, и TMyData – класс, содержащий собственно адресную книгу и методы работы с ней, которые и назначаются в качестве реакций на события TMyDataSet. Эту схему событий я решил основать на принципе обращения к записям по их последовательным номерам, отсчитываемым от единицы. На мой взгляд, это вполне оправданно, т.к. я адаптирую интерфейс TDataSet именно к простым наборам данных (таким, как списки или массивы), а для существенно более сложных наборов целесообразнее, возможно, работать с обычным интерфейсом TDataSet. Вот введенные мной события: FPerformOpenConnection: procedure(DS: TDataSet) of object; -Установление соединения с носителем набора данных FPerformCloseConnection: procedure(DS: TDataSet) of object; -Разрыв соединения FPerformFillFieldDefs: procedure(FieldDefsRef: TFieldDefs) of object; -Формирование списка определений полей(аргумент - объект - список полей) FPerformGetFieldData: procedure(Index: integer; Field: TField; out Data) of object; -Чтение значения поля Field записи с номером Index FPerformSetFieldData: procedure(Index: integer; Field: TField; var Data) of object; -Изменение значения поля PerformGetCanModify: function: boolean of object; -Определение того, можно ли вносить изменения в набор FPerformGetRecordCount: function: integer of object; -Определение количества записей в наборе FPerformDeleteRecord: procedure(Index: integer) of object; -Удаление записи FPerformClearFields: procedure(Index: integer) of object; -Очистка полей записи FPerformCreateFloatingRecord: function(SourceIndex: integer): integer of object; -Создание “плавающей” записи.Передается номер записи, значения полей которой нужно скопировать в “плавающую”(0, если копировать ничего не нужно) FPerformFreeFloatingRecord: procedure(Index: integer) of object; -Удаление “плавающей” записи с номером Index FPerformDefloatRecord: procedure(FlIndex, DestIndex: integer; DefloatMode: TDefloatMode) of object; -Помещение “плавающей” записи с номером Index в набор в позицию, занимаемую записью DestIndex.DefloatMode указывает, вставляется ли “плавающая” запись вместо DestIndex или перед ней FPerformGetBookmarkSize: function: integer of object; -Определение размера закладки FPerformGetBookmark: function(Index: integer): TBookmarkStr of object; -Опрос закладки FPerformSetBookmark: procedure(Index: integer; NewBookmark: TBookmarkStr) of object; -Установка закладки для записи FPerformFindBookmark: function(Bookmark: TBookmarkStr): integer of object; -Переход к записи по закладке Работа осуществляется таким образом: класс, поставляющий обработчики событий (будем называть его поставщиком), берет на себя хранение данных и закладок, а также операции с записями по их номерам. Предлагаемый же потомок TDataSet отвечает за поддержание взаимно однозначного соответствия записей и их номеров и за перевод обычных последовательностей операций TDataSet в вызовы вышеперечисленных событий. Особо следует пояснить понятие “плавающей” записи. Предполагается, что поставщик не считывает записи набора в какие-то свои кэш-буферы, а читает содержимое полей прямо из набора. В случае чтения это работает, но в случае редактирования записей производимые изменения следует где-то накапливать, то есть в этой ситуации временное хранилище создавать придется. Однако, есть способ упростить эту операцию. Хранилище можно (так как одновременно при таком подходе будет использоваться не более одного хранилища) реализовать в виде, например, какого-то зарезервированного элемента набора (массива, списка). Соответственно, можно сделать единообразным доступ как к записям набора, так и к этому хранилищу. Когда поступает запрос на начало редактирования существующей записи или на добавление новой, у поставщика запрашивается создание такого хранилища. В ответ поставщик возвра&;#1097;ает число – это может быть, например, номер зарезервированной записи, или вообще любой номер такой, что в дальнейшем поставщик будет в состоянии понять, что этот номер относится именно к хранилищу. Таким образом, роль хранилища с точки зрения нашего потомка TDataSet будет играть запись с этим номером – ее мы и назовем “плавающей записью”. Затем происходит либо очистка полей плавающей записи, либо (в случае редактирования существующей записи) копирование в эти поля значений полей исходной записи. В дальнейшем, пока изменения не будут подтверждены или отменены, вместо чтения и записи полей исходной будет производиться чтение и запись полей “плавающей”. При отмене изменений у поставщика запрашивается освобождение “плавающей” записи, при подтверждении – вызывается FPerformDefloatRecord (см. описание этого события). Отмечу небольшое затруднение с подменой чтения/записи исходной записи на чтение/запись плавающей. Как уже говорилось, некоторые элементы управления в цикле, в котором они опрашивают поля записей (что приводит к вызову GetFieldData), изменяют единственную реализацию логического курсора в TDataSet, указатель ActiveBuffer (присваивая ему затем прежнее значение). Поэтому для того, чтобы определить, потребовали ли от TDataSet доступ именно к редактируемой записи, адрес буфера редактируемой записи надо в момент начала редактирования сохранить в какой-либо переменной, а в GetFieldData сравнивать значение ActiveBuffer с этим сохраненным значением. Все вышесказанное естественным образом воплощается в примерно такой код: Листинг 3 TMyDataSet с возможностью редактирования
interface
uses DB, Classes {for TComponent};
type
TDBCursor = integer; //Тип данных для физического курсора - номер записи
TDefloatMode = (dmInsert, dmOverwrite);
TRecordBuffer = packed record
RecordIndex: TDBCursor; //Номер записи (считая от 1)
BookmarkFlag: TBookmarkFlag;
end;
PRecordBuffer = ^TRecordBuffer;
TMyDataSet = class(TDataset)
private
FIsOpen: boolean;
FCursor: integer;
FInsertingBefore: integer;
FFloatingRecordIndex: integer;
FEditingBuffer: pchar; //Хранит адрес буфера для редактируемой записи
//При редактировании большую часть времени совпадает с ActiveBuffer,
//но в процессе отрисовки связанных элементов управления последний
//временно изменяется
protected
procedure InternalHandleException; override;
procedure InternalInitFieldDefs; override;
procedure InternalOpen; override;
function IsCursorOpen: Boolean; override;
procedure InternalClose; override;
procedure SetActive(Value: Boolean); override; //Вызывает Finalize при
// закрытии TDataSet
procedure Finalize; virtual; //логическое завершение текущих операций
//(вызов Close/Post и т.п.)
function GetRecord(Buffer: PChar; GetMode: TGetMode;
DoCheck: Boolean): TGetResult; override;
function AllocRecordBuffer: PChar; override;
procedure FreeRecordBuffer(var Buffer: PChar); override;
procedure InternalFirst; override;
procedure InternalLast; override;
procedure InternalSetToRecord(Buffer: PChar); override;
function GetCanModify: Boolean; override;
procedure InternalEdit; override;
procedure InternalCancel; override;
procedure DoBeforeInsert; override;
procedure InternalInsert; override;
procedure InternalInitRecord(Buffer: PChar); override;
procedure InternalPost; override;
procedure InternalDelete; override;
function GetBookmarkFlag(Buffer: PChar): TBookmarkFlag; override;
procedure SetBookmarkFlag(Buffer: PChar; Value: TBookmarkFlag);
override;
procedure InternalAddRecord(Buffer: Pointer; DoAppend: Boolean);
override;
procedure GetBookmarkData(Buffer: PChar; Data: Pointer); override;
procedure SetBookmarkData(Buffer: PChar; Data: Pointer); override;
procedure InternalGotoBookmark(Bookmark: Pointer); override;
function GetRecordCount: Integer; override;
procedure SetRecNo(Value: Integer); override;
function GetRecNo: Integer; override;
public
constructor Create(AOwner: TComponent); override;
function GetFieldData(Field: TField; Buffer: Pointer): Boolean;
override;
procedure SetFieldData(Field: TField; Buffer: Pointer); override;
function BookmarkValid(Bookmark: TBookmark): Boolean; override;
public //Конечно, это лучше реализовывать в виде свойств, но в целях
// экономии места я сделал эти процедурные переменные полями
FPerformOpenConnection: procedure(DS: TDataSet) of object;
FPerformCloseConnection: procedure(DS: TDataSet) of object;
FPerformFillFieldDefs: procedure(FieldDefsRef: TFieldDefs) of object;
FPerformGetFieldData: procedure(Index: integer; Field: TField; out Data)
of object;
FPerformSetFieldData: procedure(Index: integer; Field: TField; var Data)
of object;
FPerformGetCanModify: function: boolean of object;
FPerformGetRecordCount: function: integer of object;
FPerformDeleteRecord: procedure(Index: integer) of object;
FPerformClearFields: procedure(Index: integer) of object;
FPerformCreateFloatingRecord: function(SourceIndex: integer): integer
of object;
FPerformFreeFloatingRecord: procedure(Index: integer) of object;
FPerformDefloatRecord: procedure(FlIndex, DestIndex: integer;
DefloatMode: TDefloatMode) of object;
FPerformGetBookmarkSize: function: integer of object;
FPerformGetBookmark: function(Index: integer): TBookmarkStr of object;
FPerformSetBookmark: procedure(Index: integer; NewBookmark: TBookmarkStr)
of object;
FPerformFindBookmark: function(Bookmark: TBookmarkStr): integer
of object;
end;
implementation
uses SysUtils, forms;
procedure TMyDataSet.InternalHandleException;
begin
Application.HandleException(Self)
end;
procedure TMyDataSet.InternalInitFieldDefs;
begin
if Assigned(FPerformFillFieldDefs) then
FPerformFillFieldDefs(FieldDefs)
else
FieldDefs.Clear;
end;
procedure TMyDataSet.InternalOpen;
begin
InternalInitFieldDefs;
if DefaultFields then
CreateFields;
BindFields(true);
FIsOpen := true;
FCursor := 0;
if Assigned(FPerformGetBookmarkSize) then
BookmarkSize := FPerformGetBookmarkSize()
else
BookmarkSize := 0;
if Assigned(FPerformOpenConnection) then
FPerformOpenConnection(self);
end;
function TMyDataSet.IsCursorOpen: Boolean;
begin
result := FIsOpen
end;
procedure TMyDataSet.InternalClose;
begin
if Assigned(FPerformCloseConnection) then
FPerformCloseConnection(self);
BindFields(False);
if DefaultFields then
DestroyFields;
FIsOpen := false;
end;
procedure TMyDataSet.InternalFirst;
begin
FCursor := 0
end;
procedure TMyDataSet.InternalLast;
begin
FCursor := RecordCount + 1
end;
function TMyDataSet.GetRecord(Buffer: PChar; GetMode: TGetMode;
DoCheck: Boolean): TGetResult;
begin
result := grOK;
case GetMode of
gmPrior: if FCursor <= 1 then
result := grBOF
else
Dec(FCursor);
gm: if FCursor >= RecordCount then
result := grEOF
else
Inc(FCursor);
gmCurrent: if (FCursor < 1) or (FCursor > RecordCount) then
Result := grError;
end;
if result = grOK then
with PRecordBuffer(Buffer)^ do
begin
RecordIndex := FCursor;
BookmarkFlag := bfCurrent;
end;
if (result = grError) and DoCheck then
DatabaseError('Ошибка в GetRecord()');
end;
function TMyDataSet.GetRecordCount: Integer;
begin
if Assigned(FPerformGetRecordCount) then
Result := FPerformGetRecordCount()
else
raise EDatabaseError.Create('Для работы с ' + ClassName
+ ' требуется задать обработчик события OnGetRecordCount!');
end;
procedure TMyDataSet.SetRecNo(Value: Integer);
begin
CheckBrowseMode;
if (Value < 1) or (Value >= RecordCount + 1) then
exit;
FCursor := Value;
Resync([])
end;
function TMyDataSet.GetRecNo: Integer;
begin
Result := PRecordBuffer(ActiveBuffer)^.RecordIndex + 1
end;
procedure TMyDataSet.InternalSetToRecord(Buffer: PChar);
begin
FCursor := PRecordBuffer(Buffer)^.RecordIndex;
end;
function TMyDataSet.AllocRecordBuffer: PChar;
begin
GetMem(result, sizeof(TRecordBuffer))
end;
procedure TMyDataSet.FreeRecordBuffer(var Buffer: PChar);
begin
FreeMem(Buffer)
end;
procedure TMyDataSet.InternalInitRecord(Buffer: PChar);
//Вообще-то, это просто операция очистки записи.
//Так как наш буфер не содержит указателей на динамически создаваемые
//структуры данных, то нам не нужно удалять структуры, связанные с
//предыдущим содержимым буфера. Но вот для поставщика это может быть
//необходимо, и его надо подробно уведомить о ситуации
begin
//Если это повторный вызов (когда TDataSet уже находится в одном из режимов
//редактирования) - такой выполняется при ClearFields - то надо просто
//очистить запись, в противном случае TDataSet мы хотим вставить новую
//запись, и поставщик должен сначала создать “плавающую” запись.
if not (State in dsEditModes) then
if Assigned(FPerformCreateFloatingRecord) then
FFloatingRecordIndex := FPerformCreateFloatingRecord(0);
if Assigned(FPerformClearFields) then
FPerformClearFields(PRecordBuffer(ActiveBuffer)^.RecordIndex);
end;
function TMyDataSet.GetCanModify: Boolean;
begin
if assigned(FPerformGetCanModify) then
result := FPerformGetCanModify
else
result := false
end;
function TMyDataSet.GetFieldData(Field: TField; Buffer: Pointer): Boolean;
var
Index: integer;
begin
result := assigned(FPerformGetFieldData);
if (State in dsEditModes) and (ActiveBuffer = FEditingBuffer) then
Index := FFloatingRecordIndex //перенаправляем к “плавающей” записи
else
Index := PRecordBuffer(ActiveBuffer)^.RecordIndex;
if result then
FPerformGetFieldData(Index, Field, Buffer^);
end;
constructor TMyDataSet.Create(AOwner: TComponent);
begin
inherited;
FIsOpen := false;
if assigned(FPerformGetBookmarkSize) then
BookmarkSize := FPerformGetBookmarkSize()
else
BookmarkSize := 0;
end;
function TMyDataSet.GetBookmarkFlag(Buffer: PChar): TBookmarkFlag;
begin
result := PRecordBuffer(Buffer)^.BookmarkFlag;
end;
procedure TMyDataSet.SetBookmarkFlag(Buffer: PChar; Value: TBookmarkFlag);
begin
PRecordBuffer(Buffer)^.BookmarkFlag := Value;
end;
procedure TMyDataSet.InternalPost;
begin
inherited; //=CheckRequiredFields
case State of
dsEdit:
if Assigned(FPerformDefloatRecord) then
FPerformDefloatRecord(FFloatingRecordIndex, FCursor, dmOverwrite);
dsInsert:
begin
if Assigned(FPerformDefloatRecord) then
FPerformDefloatRecord(FFloatingRecordIndex, FCursor, dmInsert);
PRecordBuffer(ActiveBuffer)^.RecordIndex := FCursor;
PRecordBuffer(ActiveBuffer)^.BookMarkFlag := bfCurrent;
end;
end;
end;
procedure TMyDataSet.SetFieldData(Field: TField; Buffer: Pointer);
begin
//Для этой операции Index всегда является индексом “плавающей” записи
//Перенаправляем изменения в Floating record
if Assigned(FPerformSetFieldData) then
FPerformSetFieldData(FFloatingRecordIndex, Field, Buffer^);
DataEvent(deFieldChange, cardinal(Field));
end;
procedure TMyDataSet.InternalDelete;
begin
if not Assigned(FPerformGetRecordCount) then
raise EDatabaseError.Create('Для работы с ' + ClassName
+ ' требуется задать обработчик события PerformGetRecordCount!');
if Assigned(FPerformDeleteRecord) then
FPerformDeleteRecord(PRecordBuffer(ActiveBuffer)^.RecordIndex);
if FCursor = FPerformGetRecordCount() then
Dec(FCursor);
end;
procedure TMyDataSet.InternalAddRecord(Buffer: Pointer; DoAppend: Boolean);
//Согласно справке, при вызове InsertRecord активной становится вставленная
//запись. Причем можно отследить, что заботиться об этом должна
//InternalAddRecord. Поэтому в случае DoAppend просто прыгаем в конец
//(вернее, за конец)
begin
//В момент вызова уже создана Floating-запись, и в нее скопированы значения
if DoAppend then
InternalLast;
FPerformDefloatRecord(FFloatingRecordIndex, FCursor, dmInsert);
end;
procedure TMyDataSet.GetBookmarkData(Buffer: PChar; Data: Pointer);
var
res: string;
begin
if BookmarkSize = 0 then
exit;
if Assigned(FPerformGetBookmark) then
res := FPerformGetBookmark(PRecordBuffer(Buffer)^.RecordIndex)
else
res := '';
strpcopy(Data, res);
end;
procedure TMyDataSet.SetBookmarkData(Buffer: PChar; Data: Pointer);
begin
if Assigned(FPerformSetBookmark) then
FPerformSetBookmark(PRecordBuffer(Buffer)^.RecordIndex, string(pchar(Data)))
end;
procedure TMyDataSet.InternalGotoBookmark(Bookmark: Pointer);
var
Pos: integer;
begin
if not assigned(FPerformFindBookmark) then
exit;
Pos := FPerformFindBookmark(string(Bookmark));
if Pos > -1 then
FCursor := Pos;
end;
function TMyDataSet.BookmarkValid(Bookmark: TBookmark): Boolean;
begin
result := assigned(FPerformFindBookmark);
if result then
result := (-1 <> FPerformFindBookmark(string(Bookmark)))
end;
procedure TMyDataSet.DoBeforeInsert;
begin
inherited;
FInsertingBefore := PRecordBuffer(ActiveBuffer)^.RecordIndex;
end;
procedure TMyDataSet.InternalInsert;
begin
FEditingBuffer := ActiveBuffer;
//Чтобы InternalSetToRecord переходил на правильную позицию
with PRecordBuffer(ActiveBuffer)^ do
if BookmarkFlag = bfInserted then
RecordIndex := FInsertingBefore;
//А если bfEOF или bfBOF, то InternalSetToRecord и не будет вызываться
end;
procedure TMyDataSet.InternalCancel;
begin
if Assigned(FPerformFreeFloatingRecord) then
FPerformFreeFloatingRecord(FFloatingRecordIndex);
end;
procedure TMyDataSet.InternalEdit;
begin
FEditingBuffer := ActiveBuffer;
if Assigned(FPerformCreateFloatingRecord) then
FFloatingRecordIndex :=
FPerformCreateFloatingRecord(PRecordBuffer(ActiveBuffer)^.RecordIndex);
end;
procedure TMyDataSet.SetActive(Value: Boolean);
begin
if (Value <> Active) and (Value = false) then
Finalize;
inherited;
end;
procedure TMyDataSet.Finalize;
begin
Cancel; //вдруг что-то редактировалось
end;
end.
А поставщик в нашем примере с адресной книгой может быть для такого упрощенного интерфейса реализован весьма несложно, например, так:
Листинг 4
interface
uses Classes, DB, DataSet3, SysUtils;
type
TMyRecord = class
Name: string;
EMail: string;
Bookmark: pointer; //В нормальном состоянии - указатель на саму запись
constructor Create(sName, sEMail: string);
procedure Change(sName, sEMail: string);
end;
TMyData = class
List: TList;
constructor Create; reintroduce;
destructor Destroy; override;
procedure FillFieldDefs(FieldDefsRef: TFieldDefs);
procedure GetFieldData(Index: integer; Field: TField; out Data);
procedure SetFieldData(Index: integer; Field: TField; var Data);
function GetCanModify: boolean;
function GetRecordCount: integer;
procedure DeleteRecord(Index: integer);
procedure ClearFields(Index: integer);
function CreateFloatingRecord(SourceIndex: integer): integer;
procedure DefloatRecord(FlIndex, DestIndex: integer;
DefloatMode: TDefloatMode);
function GetBookmarkSize: integer;
function GetBookmark(Index: integer): TBookmarkStr;
procedure SetBookmark(Index: integer; NewBookmark: TBookmarkStr);
function FindBookmark(Bookmark: TBookmarkStr): integer;
end;
implementation
procedure TMyRecord.Change(sName, sEMail: string);
begin
Name := sName;
EMail := sEMail
end;
constructor TMyRecord.Create(sName, sEMail: string);
begin
inherited Create;
Change(sName, sEMail);
Bookmark := self;
end;
{ TMyData }
constructor TMyData.Create;
begin
List := TList.Create;
List.Add(TMyRecord.Create('Temporary', 'Temporary'));
//Первый элемент - место под “плавающую” запись
List.Add(TMyRecord.Create('Name1', 'email1'));
List.Add(TMyRecord.Create('Name2', 'email2'));
List.Add(TMyRecord.Create('Name3', 'email3'));
end;
destructor TMyData.Destroy;
var
i: integer;
begin
for i := 0 to List.Count - 1 do
TMyRecord(List[i]).free;
List.Free;
inherited;
end;
procedure TMyData.ClearFields(Index: integer);
begin
TMyRecord(List[0]).Name := '(name)';
TMyRecord(List[0]).EMail := '(email)';
end;
function TMyData.CreateFloatingRecord(SourceIndex: integer): integer;
begin
result := 0;
if SourceIndex <> 0 then
with TMyRecord(List[SourceIndex]) do
begin
TMyRecord(List[0]).Name := Name;
TMyRecord(List[0]).EMail := EMail
end;
end;
procedure TMyData.DefloatRecord(FlIndex, DestIndex: integer;
DefloatMode: TDefloatMode);
begin
if DefloatMode = dmInsert then
List.Insert(DestIndex, TMyRecord.Create(
TMyRecord(List[0]).Name, TMyRecord(List[0]).EMail))
//При этом автоматически ставится новый Bookmark
else
with TMyRecord(List[DestIndex]) do
begin
Name := TMyRecord(List[0]).Name;
Email := TMyRecord(List[0]).EMail
end;
end;
procedure TMyData.FillFieldDefs(FieldDefsRef: TFieldDefs);
begin
FieldDefsRef.Clear;
with FieldDefsRef.AddFieldDef do
begin
DataType := ftString;
FieldNo := 1;
Name := 'Имя';
Size := 40;
end;
with FieldDefsRef.AddFieldDef do
begin
DataType := ftString;
FieldNo := 2;
Name := 'E-Mail';
Size := 50;
end;
end;
procedure TMyData.DeleteRecord(Index: integer);
begin
List.Delete(Index)
end;
function TMyData.GetCanModify: boolean;
begin
result := true
end;
procedure TMyData.SetFieldData(Index: integer; Field: TField; var Data);
begin
case Field.FieldNo of
1: TMyRecord(List[Index]).Name := strpas(@Data);
2: TMyRecord(List[Index]).Email := strpas(@Data);
end;
end;
procedure TMyData.GetFieldData(Index: integer; Field: TField; out Data);
begin
case Field.FieldNo of
1: strpcopy(@Data, TMyRecord(List[Index]).Name);
2: strpcopy(@Data, TMyRecord(List[Index]).EMail);
end;
end;
function TMyData.GetRecordCount: integer;
begin
result := List.Count - 1; {0-й элемент не считаем}
end;
function TMyData.GetBookmarkSize: integer;
begin
result := 10; {'$xxxxxxxx'+NULL}
end;
procedure TMyData.SetBookmark(Index: integer; NewBookmark: TBookmarkStr);
begin
TMyRecord(List[Index]).BookMark := pointer(strtoint(NewBookmark))
end;
function TMyData.FindBookmark(Bookmark: TBookmarkStr): integer;
var
i: integer;
p: pointer;
begin
p := pointer(strtoint(pchar(Bookmark)));
result := -1;
for i := 1 to List.Count - 1 do
if TMyRecord(List[i]).Bookmark = p then
begin
result := i;
break
end;
end;
function TMyData.GetBookmark(Index: integer): TBookmarkStr;
begin
result := '$' + IntToHex(cardinal(TMyRecord(List[Index]).BookMark), 8)
end;
end.
Для проверки работы можно использова&;#1090;ь все тот же небольшой проект, включив в Uses модуль DataSet3 вместо DataSet0, а также модуль MyData, и добавив подключение к нашему экземпляру TMyDataSet определенных в модуле MyData обработчиков событий. Для этого, конечно, потребуется создать экземпляр TMyData.
Заключение
Необходимо заметить, что приведенные примеры имеют своей целью лишь показать в минимальном объеме возможную реализацию потомка TDataSet и один из способов его адаптации к простым задачам, и ни в коей мере не претендуют на качественное решение возникающих задач (например, приведенный способ поиска записи по закладке на практике при больших объемах данных может оказаться непомерно медленным). Тем не менее, примеры рабочие.
Приведенной информации, на мой взгляд, достаточно, чтобы адаптировать свои структуры данных для работы с TDataSource и DB-Aware элементами управления. Конечно, на практике могут встретиться ситуации, требующие дополнительных усилий и реализации еще каких-либо действий (например, поиска записи по значению части ее полей, фильтрации записей и т.п.); смею надеяться, однако, что выполненная мною работа поможет и в этом, несколько развеяв туман вокруг внутренних взаимосвязей методов TDataSet и восполнив катастрофических масштабов недостаток документации по этой теме.
Отправить комментарий