    ГЛАВА      13 

    Создание пользовательских наборов данных 

    Тодд Миллер 

    • Виртуальные наборы данных 

    В начале главы рассматриваются преимущества нового виртуального набора 
данных TDataSet. Сам по себе компонент TDataSet не является новинкой; новое в 
нем то, что он позволяет создавать пользовательские наборы данных. 

    • Основа наборов данных 

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

    1 Текстовый ASCII-драйвер без ВОЕ 
    Здесь приведен пример текстового драйвера, используемого при создании 
пользовательского набора данных. В данном случае —это ASCII-драйвер для текста 
с полями фиксированной длины, не включаемый в BDE. 

    До появления Delphi 3 возможности доступа к данным в некоторой степени 
ограничивались. В пакетах Delphi версий 1 и 2 предусматривалось три основных 
способа доступа к данным: воспользоваться таблицами Paradox или dBASE или в 
версии Delphi Client/Server использовать популярные SQL-серверы. Если же 
необходимая база данных не поддерживалась, то единственная возможность 
подключиться к ней— использовать ODBC-драйвер, к 1 тому же был необходим 
интерфейс BDE. Иногда это было проблемой, поскольку ODBC-драйверы часто очень 
медлительны в работе. А что если нет ODBC-драйвера для вашего набора данных? 
Без Delphi 3 пришлось бы разрабатывать механизм доступа к данным, а также 
создавать для отображения данных собственные элементы управле- 1 ния данными. 
А это тысячи..стррк кода! 
    С появлением Delphi 3 этому кошмару положен конец. Теперь каждый 
пользователь может получить доступ к любым данным и создать на их основе 
пользовательские наборы данных. С этими наборами данных можно использовать 
редактор полей Fields Editor, средства хранения данных и, что еще важнее, — 
знакомые элементы управления данными Delphi. 
    Чтобы добиться этого, в классе TDataSet Delphi 3 удалены и перенесены в 
новый класс TBDEDafaSet все привязки к интерфейсу BDE API. Теперь в классе 
TB'D'E'DStaSet содержатся основные функциональные возможности интерфейса BDE 
для классов ТТаЫе, TQuery, TStoredProc и т.д. А основные фунмщональные 
возможности компонента TD'ataSet сделаны виртуальнымй, поэтому с его помощью 
можно создать любой ТгользЗвЗтельский набор данных. Таким образом можно 
создавать производный от класса TDataSet набор данных, работающий со всеми 
элементами управления данными Delphi. Реализовав чуть больше 20 виртуальных 
методов компонента TDataSet, вы получите свой компонент набора данных. 

    309 

    Здесь нужно сделать предостережение: чтобы по-настоящему разобраться в 
этой главе, необходимо знать основы разработки компонентов, а также язык 
Object Pascal. В противном случае обратитесь к главам 16, "Создание 
компонентов'', и 17, "Сложные компоненты". Кроме того, предполагается, что 
читатель имеет ясное представление о концепции баз данных и работе с ними. Ну 
а теперь пора разобраться в том, что такое виртуальные наборы данных. 

    Компонент TDataSet, или Сердце пользовательского набора данных 

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

    Таблица 13.1. Свойства и методы компонента TDataSet после реализации 
виртуальных абстрактных методов 

    Категория                  Свойства и методы 

    Общие	Active,Open, Close 
    Перемещение	First,Last,Next,Prior,MoveBy,Refresh,EOF,BOF, IsEmpty 
    Редактирование	Edit,  Insert,  InsertRecord, Append, AppendRecord, Delete, 
Post,  Cancel,  Modified, State, CheckBrowseMode, SetFields 
    Работа с закладками	BookmarkValid, CompareBookmarks, GotoBookmark, 
FreeBookmark, Get-Bookmark, Bookmark 
    Работа с элементами управления данными	ControlsDisabled, DisableControls, 
EnableControls, IsLinkedTo 
    События	BeforeOpen,  AfterOpen,  BeforeClose, AfterClose, Beforelnsert, 
Afterlnsert, BeforeEdit, AfterEdit, BeforePost, AfterPost, BeforeCancel, 
AfterCancel, BeforeDelete, AfterDelete, BeforeScroll, After-Scroll, 
OnCalcFields, OnDeleteError, OnEditError, OnNewRecord, On-PostError 
    Работа с полями	FieldByName, FieldByNumber, FieldDefs, FieldCount, Fields, 
FieldValues, DefaultFields, FindField, GetFieldList, GetFieldNames, 
Up-dateRecord, ClearFields 
    Управление наборами данных низкого уровня	ActiveBuffer, CursorPosChanged, 
GetCsurrentRecord, Translate, RecordSize 

    Кроме методов и свойств, перечисленных в табл. 13.1, для работы с 
пользовательским компонентом набора данных в форме можно использовать редактор 
полей Fields Editor, с помощью которого можно создавать статические поля, 
вычисляемые поля, поля просмотра и т.д. Все это выполняется точно так же, как 
и для любого другого компонента, наследованного от TDataSet. Что выполняет 
пользователь? Об этом вы узнаете из следующего списка. 
    • Процедуры открытия, инициализации и закрытия таблиц. в Процедуры 
управления закладками. 
    • Процедуры управления буфером записи. 
    • Процедуры управления перемещением. 
    • Процедуры обработки исключительных ситуаций. 

    310                                     Часть III. Работа с базами данных 


    Пишем текстовый ASCII-драйвер базы данных 

    Прежде чем разбираться в деталях построения пользовательского набора 
данных, необходимо иметь представление, к каким данным и каким образом вы 
пытаетесь получить доступ, чтобы определить расположение полей набора данных. 
В качестве примера возьмем пользовательский набор данных, представляющий собой 
простой текстовый ASCII-драйвер, который может читать данные в текстовых 
таблицах со столбцами фиксированной длины. Компонент TFixedAsciiDataSet 
считывает заданный пользователем текстовый файл в список TStringList, в 
котором каждая строка представляет отдельную необработанную текстовую запись. 
Определение поля будет читаться из файла схемы, который является отдельным 
текстовым файлом, содержащим список определений полей, включающих имя, тип и 
размер поля. Имя файла схемы является тем же именем, что и имя текстового 
файла с расширением SCH. 
    Кроме примера в этой главе, в Delphi предоставляется еще два компонента: 
TBDEDataSet (в файле SOURCE \VCL\DBTABLES. РАЗ) и TClientDataSet (в файле 
\SOURCE\VCL\DATASET. РАЗ). Оба компонента являются'"хор5шими~примерами 
пользовательского набора данных. В нйх'можно~найти наиболее полные ответы на 
вопросы, связанные с переопределением методов компонента"ТЕа1а5е1. ————' 
    Компонент TFixedAsciiDataSet рассчитан на формат схемы, который является 
таким же, как формат, документированный для текстового ASCII-драйвера BDE. 
Однако поддерживается лишь фиксированное расположение полей, а текстовые 
записи с разделителями не поддерживаются. Это означает, что нельзя 
использовать с компонентом TFixedAsciiDataSet набор данных текстового файла с 
его файлом схемы, который был создан с помощью ASCII-драйвера BDE. 
    Чтобы ознакомиться с полным описанием формата файла схемы, обратитесь к 
файлу BDE32 . HLP. Он находится в папке, которая была определена при установке 
Delphi для системы BDE. Если вы работаете с Windows 95 "  или Windows NT 4.0, 
то для просмотра этого справочного файла используйте команду Start^Programs^ 
Borland Delphi 3 (riycK^riporpaMMbi^Borland Delphi 3). 
    На самом деле данные примера, которые мы будем использовать, являются 
текстовым файлом с файлом схемы, который содержит записи из файла примера базы 
данных Delphi EMPLOYEE. DB. Этот файл создан с помощью компонента Delphi 
TBatchMove. В листинге 13.1 показан файл схемы, который представляет наш 
текстовый файл EMPLOYEE. TXT. Этот файл есть на дополнительно распространяемой дискете. 

    Листинг 13.1. \UDELPHi3\CHPl3\EMPLOYEE. SCH. Определение схемы для 
некоторых полей данных 

    ПЕЯИЯЯ Filetype=Fixed 
    CharSet=ascii 
    Fieldl=EmpNo,Long Integer,11,00,00 
    Field2=FirstName,Char,20,00,11 
    Field3=LastName,Char,15,00,31 
    Field4=PhoneExt,Char,04,00,46 
    Field5=HireDate,TimeStamp,30,00,50 
    Field6=Salary,Float,20,02,80 
    
    Обратите внимание, что стандартный файл схемы начинается с заголовка, 
который заключается в квадратные скобки и содержит имя текстового файла, 
представленного определениями полей в файле схемы. 
    Далее определяется тип файла. В текстовом ASCII-драйвере BDE 
поддерживаются оба формата (DELIMITED и FIXED), но в нашем примере 
поддерживаются только фиксированные столбцы. Таким образом, значение типа 
файла должно быть FIXED. Следующий элемент CharSet поддерживается только 
системой BDE. И, наконец, мы добрались до оставшихся строк, которые содержат 
определения полей, соответствующих столбцам таблицы. Каждое определение поля 
должно начинаться с "Fieldx=", где х— номер поля (такой, как Fieldl, Field2 и 
т.д.). В остальной части каждой строки содержатся выделяемые запятыми 
элементы, определяющие следующие параметры. 
    1. Field Name. Придерживайтесь тех же ограничений, что и для имен полей Paradox. 
    2. Data Type. Тип данных поля (табл. 13.2). 
    3. Field Size. Количество символов или единиц, которое должно быть меньше 
или равно 20 для числовых типов данных. Этот параметр также представляет 
максимальное число символов, которое могут использовать типы данных, такие как 
дата или время (включая разделители "/" и ":''). 

    Глава 13. Создание пользовательских наборов данных                        311 

    4. Precision. Количество символов, следующих за десятичной точкой (только 
для типа FLOAT). Это значение компонентом TFixedAsciiDataSet игнорируется. 
    5. Field Offset. Количество символов от начала строки до начала данных 
поля. Используется только в формате BDE Fixed. Это значение игнорируется 
компонентом TFixedAsciiDataSet, поскольку оно вычисляется из значения размера 
поля, когда поля считываются из схемы. 
    В табл. 13,2 перечислены поддерживаемые типы данных. Позже вы увидите, что 
такой список типов данных можно получить для совместимости типов данных в 
Delphi, когда эти значения передаются в элементы управления данными и из них. 
    
    Таблица 13.2. Поддерживаемые типы данных схемы 

    Тип данных схемы	Описание 

    Char	Строка символов 
    Float	64-битовое значение с плавающей запятой 
    Number	16-битовое целое 
    Long Integer	32-битовое длинное целое 
    Bool	Логическое(булево) значение (Т или F) 
    Date	Поле даты (формат, указываемый в реестре) 
    Time	Поле времени (формат, указываемый в реестре) 
    Time stamp	Поле даты/времени (формат, указываемый в реестре) 


    Создание пользовательского набора данных 

    Сейчас, когда вы уже знаете, что вы создаете, засучим рукава и примемся за 
работу. Компонент TFixedAsciiDataSet используется, чтобы охватить 
функциональную группу методов целиком. Чтобы получить общее представление о 
том, чего мы хотим достичь и что такое функциональные группы, взгляните на 
листинг 13.2. В нем показано определение класса TFixedAsciiDataSet, который, 
как вы, может быть, и предполагали, является потомком класса TDataSet. Таким 
образом, для начала необходимо знать, что часть TDataSet делает для вас и что 
должны сделать вы для нее. 
    Основная функция класса TDataSet —управление группой буферов записей. 
Однако в классе TDataSet не детализируется содержимое буферовГИнтерпретация 
информации в буферах ^роис^ходйт в наследованном классе (в нашем случае— в 
TFixedAsciiDataSet). Кроме того, компонент TDataSet не устанавливает никаких 
требований к содержимому каждого из буферов записей. Другая важная 
функциональная возможность компонента TDataSet заключается в том, что он 
обеспечивает связь с любым компонентом данных, который соединяется с 
компонентами TDataSource. Так чего же ожидает компонент TDataSet от своих 
потомков? Он предполагает, что производный класс реализует все функции прямого 
доступа к буферу записи, которые включают в себя создание, наполнение, 
интерпретацию и освобождение буферов записей. 
    Кроме того, производный класс должен обеспечивать возможность 
использования закладок. Закладки _используются, чтобы назначить каждой записи 
уникальный, .идентификатор, и являются критическим'и"для корректного 
выполнения внутренних работ компонента TDataSet. Если вы создаете свой 
собственный пользоЦДЦ^^Щ вательский набор данных и он не имеет уникальных 
идентификаторов для каждой записи, вы должны построить такие идентификаторы, 
чтобы обеспечить правильную работу компонента TDataSet с вашим производным 
классом. Прежде чем двигаться дальше, обратите внимание на переменные, 
объявленные как private в классе TFixedAsciiDataSet и используемые для 
отслеживания различных фрагментов информации компонента, таких как последняя 
назначенная закладка, текущая запись и размер буфера записи. А сейчас можно 
просто взглянуть на комментарии в листинге 13.2, чтобы иметь представление о 
его назначении. Файл ASCIIDRV. PAS находится на дополнительно распространяемой дискете. 

    312                                      Часть III. Работа с базами данных 


    1 Листинг 13.2. \UDELPHI3\CHP13\ASCIIDRV.PAS. 

    Объявление класса для компонента TFixedAsciiDataSet, а также необходимых 
методов, которые должны обеспечиваться для всех пользовательских наборов данных 

    ""TfTxedAs'ciiDataset = class (TDataSet) 
    Р"^®                       Lir^t 
    FTextRecords    TStrings;//Исходные записи читаются из текстового файла. 
    FFieldInfo      TList;   // Смещение поля и размер в исходной записи. 
    FRawBufSize    ^ТЙТедёг; // Размер буфера исходной записи. 
    FRecBufSize     Integer; // FRawBufSize + размер записи TRecInfo. 
    FRecInfoOfa     Integer; // Смещение для данных TRecInfo в буфере данных. 
    FCurRec         Integer; // Фиксация текущей записи. 
    FFileName       string; // Имя файла ASCII-таблицы. 
    FLastBookmark   Integer; // Последняя закладка. 
    FSaveChanges    Boolean; // Определяет, были ли данные изменены. 
    procedure PopulateFieldDefsFromSchema; 
    protected 
    // Методы Dataset, которые должны быть переопределены. 
    // Открытие, инициализация и закрытие пользовательских наборов 
    данных. 
    procedure Internal-Open; override; 
    procedure InternalInitFieldDefs; override; 
    procedure InternalClose; override; 
    function IsCursorOpen: Boolean; override; 
    // Процедуры управления закладками набора данных. 
    procedure GetBookmarkData(Buffer: PChar; Data: Pointer); override; 
    procedure SetBookmarkData(Buffer: PChar; Data: Pointer); override; 
    function GetBookmarkFlag(Buffer: PChar): TBookmarkFlag; override; 
    procedure SetBookmarkFlag(Buffer: PChar; Value: TBookmarkFlag); override; 
    procedure InternalSetToRecord(Buffer: PChar); override; 
    procedure InternalGotoBookmark(Bookmark: Pointer); override; 
    // Процедуры управления буферами записей Dataset. 
    function GetRecordCount: Integer; override; 
    function AllocRecordBuffer: PChar; override; 
    procedure FreeRecordBuffer(var Buffer: PChar); override; 
    function GetRecord(Buffer: PChar; GetMode: TGetMode; 
    DoCheck: Boolean): TGetResult; override; 
    function GetRecordSize: Word; override; 
    procedure InternalInitRecord(Buffer: PChar); override; 
    procedure InternalAddRecord(Buffer: Pointer; Append: Boolean); override; 
    procedure InternalPost; override; 
    procedure InternalDelete; override; 
    // Процедуры управления полями Dataset. 
    procedure SetFieldData(Field: TField; Buffer: Pointer); override; 
    function GetFieldData(Field: TField; 
    Buffer: Pointer): Boolean; override; 
    // Процедуры управления перемещением Dataset. 
    procedure InternalFirst; override; 
    procedure InternalLast; override; 
    // Процедура обработки исключительных ситуаций Dataset. 
    procedure InternalHandleException; override; 
    protected 
    procedure SetFileName(Value : Strings
    published 
    property FileName: string read FFileName write SetFileName; 
    property Active; 
    property BeforeOpen; 
    property AfterOpen; 
    property BeforeClose; 


    Глава 13. Создание пользовательских наборов данных                       313 


    property AfterClose; 
    property Beforelnsert; 
    property Afterlnsert; 
    property BeforeEdit; 
    property AfterEdit; 
    property BeforePost; 
    property AfterPost; 
    property BeforeCancel; 
    property AfterCancel 
    property BeforeDelete; 
    property AfterDelete 
    property BeforeScroll; 
    property AfterScroll 
    property OnCalcFields; 
    property OnDeleteError; 
    property OnEditError 
    property OnNewRecord 
    property OnPostError 
    end; 


    ПРЕДОСТЕРЕЖЕНИЕ 

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

    Открытие, инициализация и закрытие пользовательских наборов данных 

    Для выполнения этих операций предусмотрены следующие методы. 
    И InternalOpen 
    • InternalInitFieldDefs 
    • InternalClose 
    IsCursorOpen 
    Реализация этих методов показана в листинге 13.3. 
    Рассмотрим сначала метод InternalOpen. Он предназначен для открытия набора 
данных, который влечет за собой установку определений полей (путем вызова 
InternalInitFieldDefs) и инициализацию основных внутренних значений, таких как 
размер буфера записи, смещение любой записи, обрабатываемой внутри структуры 
данных и сохраняемой в буфере, а также начальная позиция записи. 
    При реализации метода InternalOpen в классе TFixedAsciiDataSet мы сначала 
проверяем, создан ли текстовый файл, и, если создан, загружаем весь файл в 
TStringsList (FTextRecords). Затем каждой записи присваивается уникальный 
числовой идентификатор, который будет использоваться как закладка записи. 
Просмотрите метод InternalOpen в листинге 13.3 и увидите специальные 
присваивания для внутренних переменных. 

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

    Затем список FieldDefs класса TDataSet необходимо заполнить именами, 
типами и размерами полей набора данных. Такая обработка выполняется методом 
InternalInitFieldDefs, который, в свою очередь, вызывает privateметод 
PopulateFieldDefsFromSchema. Этот метод открывает связанный с ним файл SCH для 
набора данных и разбирает его для пополнения списка FieldDefs класса TDataSet. 
Код для разбора схемы приводится в листинге 13.4. 

    314                                     Часть III. Работа с базами данных 

    После определения полей вы уже знаете, сколько байтов занимает запись 
(FRawBufSize), и можете использовать это число как смещение для любых 
дополнительных данных, которые необходимо добавить в буфер записи для 
отслеживания определенной информации, например информации по закладкам. В этом 
примере добавляется специальная запись данных (TRecInfo) в конце каждого 
буфера. Следовательно, вам необходимо знать, где заканчиваются исходные 
данные, чтобы установить начало TRecInfo. Мы могли бы просто использовать 
переменную FRawBufSize, но, чтобы сохранить ясность документа, используется 
отдельная переменная (F^RecInfoOfs) для хранения значения смещения данных 
TRecInfo в конце буфера. И, наконецГразмер Boorinarksize устанавливается 
равным размеру целого числа, поскольку он указывает размер при хранении закладки. 
    Метод InternalClose определяет, изменились ли какие-либо записи, и, если 
изменились, снова переписывается весь файл, а затем освобождаются список 
TStringsList (FTextRecords), определения полей набора данных, а также 
информационный список внутренних полей {crii7coK~Fi7eldlnfo). Рассмотрим 
вкратце создание списка FFieldlnfo. 
    Метод IsCursorOpen передает набору данных TDataSet информацию о том, 
открыта ли таблица, что определяется (в нашем примере) по переменной Result, 
которая указывает, назначена ли переменная списка FTextRecords. 

    ; Листинг 13.3. \UDELPHI3\CHP13\ASCIIDRV. PAS. Процедуры открытия, 
инициализации и закрытия компонента 

    TFixedAsciiDataSet 
    procedure TFixedAsciiDataset.InternalOpen; 
    //-—————-———————_——————————-————_-——-———_--_ 
    // Назначение. Если имя файла существует, загрузите его и 
    //                соответствующий уникальный номер для каждой 
    //                записи, который будет использоваться как 
    //                внутреннее значение закладки. 
    //————————————————————————————————————_—__—— 
    var 
    I: Integer; 
    begin 
    FTextRecords := TStringList.Create; 
    try 
    // Открыть файл текстовых данных и прочесть его полностью. 
    if not FileExists(FileName) then Abort; 
    FTextRecords.LoadFromFile(FileNameTT""' 
    // Назначить уникальную закладку для каждой записи. 
    for I := 1 to FTextRecords-Count do 
    FTextRecords.Objects[I - 1] := Pointer(I); 
    // Инициализация внутренней позиции и последней закладки. 
    // Диапазон внутренних номеров записей — от 0 до 
    // FTextRecords.Count - 1, поэтому -1 и FTextRecords.Count 
    // используются как сигнальные значения BOF и EOF 
    // соответственно. 
    FCurRec := -1;    // Первоначально установим значение равным BOF. 
    FLastBookmark := FTextRecords.Count; 
    // Создание определения поля из схемы, а затем создание 
    // объектов TField из определения поля. И, наконец, 
    // связывание полей. 
    InternalInitFieldDefs; 
    if DefaultFields then CreateFields; 
    BindFields(True) ; 
    // Инициализация значения смещения для поиска TRecInfo 
    // в каждом буфере. 
    FRecInfoOfs := FRawBufSize; 
    // Вычисление размера буферов записей (заметьте, что это не 
    // то же самое, что и свойство RecordSize, которое получает 
    // только размер данных). 
    FRecBufSize := FRecInfoOfs + SizeOf(TRecInfo); 
    // Сообщите TDataSet, какой размер ТРЕБУЕТСЯ для закладок. 


    Глава 13. Создание пользовательских наборов данных                        315 


    BookmarkSize := SizeOf(Integer); 
    except 
    FTextRecords.Free; 
    FTextRecords := Nil; 
    Active := False; 
    Showmessage('Error opening text file: ' + FileName); 
    raise; 
    end; 
    end; 
    procedure TFixedAsciiDataset.InternallnitFieldDefs ; 
    // 
    // Назначение. Вызов процедуры анализа схемы для получения списка 
    //                   FieldDefs, заполненного текущим определением поля 
    //                   таблицы. 
    // 
    begin 
    PopuI ateFieldDefsFromSenema; 
    end; 
    procedure TFixedAsciiDataset.InternalClose; 
    // 
    // Назначение. Когда текстовая таблица закрывается, ее содержимое 
    //                перезаписывается, если были какие-то изменения. 
    //                Список текстовых записей-строк очищается. В конце, 
    //                после оператора finally, элементы списка внутренних 
    //                полей и список полей очищаются. 
    // 
    var 
    I : Integer; 
    begin 
    if FSaveChanges then FTextRecords.SaveToFile(FileName); 
    FTextRecords.Free; 
    FTextRecords := nil; 
    FLastBookmark := 0; 
    FCurRec := -1; 
    BindFieldsJFalse) ; 
    Ti~'De'tau 1 t'Fi e 1 d s'"'t hen DestroyFields; 
    // Очищаем записи внутренних полей 
    if Assigned(FFieldInfo) then 
    try 
    if FFieldInfo.Count > 0 then 
    for I := 0 to FFieldInfo.Count - 1 do 
    FreeMem(PFieldInfo(FFieldInfo[I]), SizeOf(TFieldInfo)) ; 
    finally 
    FFieldInfo.Free; // Удалить, если вставлена в набор данных. 
    FFieldInfo := Nil; 
    end; 
    end; 
    function TFixedAsciiDataset.IsCursorOpen: Boolean; 
    // 
    // Назначение. Эта функция нужна при открытии. Она сигнализирует 
    //                о доступности данных, даже если состояние — еще 
    //                dsInActive. 
    // 
    begin 
    Result := Assigned(FTextRecords) ; 
    end;          '~"— 

    Прежде чем перейти к следующей группе функций, следует рассмотреть еще 
пару моментов. Поскольку в списке ' FieldDefs не отслеживается относительное 
смещение или размер данных в байтах в буфере исходной записи, процедура 
анализа схемы ProcessField создает список записей TFieldInfo в списке 
FFieldInfo, в котором отслеживает

    316                                     Часть III. Работа с базами данных 

    ся смещение и размер каждого поля для использования в методах управления 
полями, рассматриваемых ниже, в разделе "Управление полями". Подпрограмма 
ProcessField также отображает различные типы данных схемы (см. табл. 13.2) в 
одном из следующих типов полей набора данных TDataSet: ftString, ftFloat, 
ftlnteger, ftBoolean, ftDate, ftTime или ftDateTime. 

    • Листинг 13.4. \UDELPHl3\CHPl3\ASCHDRV. PAS. Процедура анализа схемы 
компонента TFixedAsciiDataSet | 

    procedure TFixedAsciiDataset.PopulateFieldDefsFromSchema; 
    // 
    // Назначение. Анализ файла схемы, связанного с набором данных и 
    //                определенного в свойстве FileName. 
    // <Field<#» = <FieldName>,<Field Type>,<Field Size>, 
    //                 <Precision>,<0ffset> 
    // 
    // <Field Type> может быть одним из следующих типов: CHAR, FLOAT, 
    //                  NUMERIC, BOOL, LONG INTEGER, DATE, TIME или 
    //                  TIMESTAMP. 
    // <Field Size> — количество символов или единиц в поле. 
    //                 Общее максимальное количество символов 
    //                 для полей типа дата/время должно включать 
    //                 разделители '/' и ':'. 
    // <Precision> — точность для полей с плавающей запятой. 
    //                 Используется при форматировании значений, 
    //                 если они были изменены и записываются в 
    //                 текстовый файл. 
    //<0ffset>       — относительная позиция в пределах фиксированной 
    //                 текстовой записи, с которой начинается 
    //                 определенное поле. Значение будет игнорироваться, 
    //                 если его можно вычислить с помощью значения поля 
    //                 <Field Size>. Введено для совместимости с форматом 
    //                 схемы BDE. 
    // 
    var 
    SchemaFile : TStrings; 
    CalculatedOfset : Integer; 
    I : Integer; 
    const ValidFieldTypes : array[1..9] of string = ('CHAR', 'FLOAT', 
    'NUMERIC','BOOL', 'LONGINT', 'LONG INTEGER' , 'DATE', 
    'TIME','TIMESTAMP') ; 
    FieldTypes: array[1..9] of TFieldType = (ftString, ftFloat, 
    ftlnteger, ftBoolean, ftlnteger, ftlnteger, ftDate, 
    ftTime,ftDateTime) ; 
    procedure ProcessField(Fieldlnfo : String); 
    ////--——————---—---——---——-—-——-——-——--——----——-
    // Назначение. Эта процедура обрабатывает строку определения 
    //              поля, которое имеет следующий формат; 
    ////——————————————————————————————————— 
    var 
    FieldName, FieldTypeToken : String; 
    FIdSize, J : Integer; 
    FieldType : TFieldType; 
    FieldInfoRec : PFieldInfo; 
    function NextToken : String; 
    ////——————————————————————————————————————— 
    // Назначение. Получение текстового признака следующего 
    //              поля (имени поля и т.д.) 
    ////--————————-—————-————-—————————————————-—— 
    var 
    DelimPos : Integer; 
    begin 
    DelimPos := AnsiPos(',', Fieldlnfo); 


    Глава 13. Создание пользовательских наборов данных                       317 


    if DelimPos = 0 then 
    Result := Trim(FieldInfo) 
    else 
    begin 
    Result := TrimfCopy(Fieldlnfo, 1, DelimPos - 1)); 
    Fieldlnfo := Trim(Copy(FieldInfo, DelimPos + 1, 255)); 
    end; 
    end; 
    begin 
    with FieldDefs do 
    try 
    FieldName := NextToken;         // Получаем имя поля. 
    FieldTypeToken := NextToken;    // Получаем тип поля. 
    // Определяем индекс типа поля, сравнивая признак типа поля 
    // со строковой константой из массива ValidFieldTypes. 
    FieldType := ftUnknown; 
    for J := Low(ValidFieldTypes) to High(ValidFieldTypes) do 
    if FieldTypeToken = ValidFieldTypes[J] then 
    begin 
    FieldType := FieldTypes[J] ; 
    Break; 
    end; 
    FIdSize := StrToInt( NextToken ); // Получаем размер поля NextToken; 
    // Игнорируем точность 
    // значения с плавающей 
    // точкой. 
    NextToken;                        // Не замечаем смещения поля. 
    // Запоминаем внутри смещение, размер поля, а также позицию 
    // десятичной точки, если необходимо, для каждого поля 
    // файла схемы. 
    GetMem(FieldInfoRec, SizeOf(TFieldInfo)) ; 
    with FieldInfoRec^ do 
    begin 
    FieldOfs := CalculatedOfset; 
    FieldSize := FIdSize; 
    end; 
    FFieldInfo.Add(FieldInfoRec) ; 
    Inc(CalculatedOfset, FIdSize);    // Следующее смещение. 
    FRawBufSize := CalculatedOfset; // Переустановка размера 
    // буфера записей. 
    // Проверка и настройка данных для поля 
    // перед добавлением в список FieldDef набора данных. 
    if FieldType о ftString then FIdSize := 0; 
    // Добавление данных о поле схемы в список FieldDef. 
    // Примечание. Номера полей должны начинаться с 1. 
    TFieldDef.Create(FieldDefs, FieldName, FieldType, FIdSize, 
    False, FieldDefs.Count + 1) ; 
    except 
    raise EDatabaseError.Create ( 
    'Schema Read Error: Error processing field ' + FieldName + ' in schema file') 
    end; 
    end; 
    procedure ProcessTokens(const LeftToken, RightToken : String); 
    //——————————————————————————————————————— 
    // Назначение. Обработка каждой строки файла схемы BDE-типа. 
    //              Набор данных поддерживает только таблицы с 
    //              фиксированными столбцами, поэтому ключевые слова 
    318                                     Часть III. Работа с базами данных 

    //              схемы DELIMITER и SEPARATOR можно игнорировать. 
    //              Кроме того, этот набор данных не поддерживает 
    //              языковые драйверы, так что ключевое слово схемы 
    //              CHARSET можно игнорировать. Эта процедура также 
    //              игнорирует элемент [TableName], который допустим 
    //              в BDE-схемах. Все, что нас на самом деле волнует, 
    //              так это то, чтобы таблица имела тип FIXED и была 
    //              хотя бы одна строка определения поля. 
    // 
    begin 
    // Игнорируем эти ключевые слова. 
    if Length(LeftToken) > 0 then 
    if (LeftToken = 'CHARSET') or 
    (LeftToken = 'DELIMITER') or 
    (LeftToken = 'SEPARATOR') or 
    (LeftToken[l] = '[') then Exit; 
    if (LeftToken = 'FILETYPE') and (RightToken <> 'FIXED') then 
    Raise Exception.Create ( 
    'Schema Read Error: FILETYPE must be FIXED'); 
    // Самое важное — определения полей. 
    if AnsiPos('FIELD', LeftToken) > 0 then 
    ProcessField(RightToken) ; 
    end; 
    function GetSchemaFileName : String; 
    // 
    // Назначение. Определение имени файла схемы по имени 
    //              текстового файла. 
    // 
    begin 
    Result := ExtractFileName(FileName); 
    Result := CopyfResult, 1, AnsiPosC.', Result) ) + 'SCH'; 
    Result := ExtractFilePath(FileName) + Result; 
    end; 
    var 
    LeftStr, RightStr, SchemaFileName : String; 
    begin 
    CalculatedOfset := 1; 
    FieldDefs.Clear; 
    SchemaFile := TStringList.Create; 
    try 
    SchemaFileName := GetSchemaFileName; 
    if not FileExists(SchemaFileName) then Abort; 
    FFieldInfo := TList.Create; 
    try 
    SchemaFile.LoadFromFile( SchemaFileName ); 
    for I := 0 to SchemaFile.Count - 1 do 
    begin 
    LeftStr := AnsiUpperCase(Trim(Copy(SchemaFile[I], 1, AnsiPos ('=', 
SchemaFile[I]) - 1))); 
    RightStr := AnsiUpperCase(Trim(Copy(SchemaFile[I], 
    AnsiPos ('=', SchemaFile[I]) + 1, 255 ))); 
    ProcessTokens(LeftStr, RightStr) ; 
    end; 
    except 
    FFieldInfo.Free; 
    end; 
    finally 
    SchemaFile.Free; 
    end; 
    end; 


    Глава 13. Создание пользовательских наборов данных                        319 


    Работа с закладками 

    Закладки (bookmarks) — это уникальные значения для каждой записи 
пользовательского набора данных. Закладки могут быть простыми, например 
уникальными четырехбайтовыми целыми значениями, и сложными, например 
определенной пользователем структурой данных. Хотя компонент TDataSet зависит 
от существования закладок, их конкретная структура не имеет значения. 
Процедуры создания, интерпретации и очистки закладок должны быть реализованы в 
производном классе. Что касается компонента TDataSet, в нем реализована только 
передача указателя. Компонентом TDataSet и пользователями производных от него 
компонентов закладки используются для^перёмеТцеКИя по записям. 
    В Delphi 1 закладки были определены просто как указатели на уникальное 
значение для записи, когда закладка запрашивалась методом GetBookmark (см. 
фрагмент кода, приведенный ниже). Этот механизм прекрасно работал, однако 
разработчик должен был очищать закладку с помощью метода FreeBookmark, когда 
она больше не была нужна. 

    TBookmark = Pointer; 

    В Delphi 3 класс TBookmark все еще поддерживается, однако появилось более 
совершенное средство работы с закладками— свойство Bookmark. Оно возвращает 
переменную типа TBookmarkStr, а этот тип определен как строка 

    AnsiString. '"r<h              T"            " 
    TBookmarkStr = string; 

    Преимущество использования этого типа заключается в том, что закладка 
очищается .автоматически, как только перестает быть нужной, поскольку это 
происходит со всеми строками THna'AnsiString. За более детальной информацией о 
закладках обратитесь к оперативной справке Delphi.                 """""''""—— 
    Работа с закладками осуществляется с помощью следующих методов. 

    • GetBookmarkData 
    • SetBookmarkData 
    • GetBookmarkFlag 
    • SetBookmarkFlag 
    • InternalSetToRecord 
    • InternalGotoBookmark 

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

    for I := I to FText.Count do 
    FTextRecords.Objects [1-1] := Pointer(I); 

    Каждый раз, когда компонент TDataSet запрашивает определенную запись у 
компонента, производного от него, значение закладки записи сохраняется в 
структуре TRecInfo, которая добавляется к возвращаемому буфера записи. 
Информация структуры TRecInf о используется только компонентом, производным от TDataSet. 
    Вот описание записи TRecInfo. 

    PRecInfo = ^TRecInfo; 
    TRecInfo = packed record 
    Bookmark: Integer; 
    BookmarkFlag: TBookmarkFlag; 
    end; 

    Обратите внимание, что, кроме поля Bookmark, есть также поле 
BookmarkFj.pJ^'1-. Оно в основном не используется компонентом TDataSet 
непосредственно, однако для вашего пользовательского набора данных необходимо 
реализовать его для каждого буфера записи, с которым в данный момент работает 
компонент TDataSet. Вот схема буфера записи. 

    ActiveBuffer 1 
    I            Данные записи            | Закладка I Флаг закладки| 
    ActiveBuffer + FRecInfoOfs----^ 


    320                                     Часть III. Работа с базами данных 

    Когда программе требуется закладка для слежения за текущей записью, 
значение закладки не берется непосредственно из записи TRednfo в активном 
буфере записи, так как внутри вызывается'метод пользовательского набора данных 
GetBookmarkData и ему передается активный буфер записи.   ' 
    Как вы вскоре убедитесь, метод GetRecord, рассматриваемый в следующем 
разделе этой главы, при запросе компонентом TDataSet определенной записи, 
кроме передачи данных записи, заполняет структуру TRednfo. 

    Г Совет 1 
    Помните, что это только пример использования буфера записи для сохранения 
дополнительной информации. Такая реализация компонента TDataSet вовсе не обязательна. 

    Вы, наверное, догадались, как методы GetBookmarkData, SetBookmarkData, 
GetBookmarkFlag и SetBookmarkFlag используются компонентом TDataSet. Эти 
процедуры очень просты. Их работа описана в комментариях листинга 13.5. Все 
четыре процедуры состоят из одной строки. 
    А какую роль играет переменная BookmarkFlags? Компонентом TDataSet эта 
переменная используется для определения тяпа записи: запись может быть 
текущей, вставленной или расположенной в начале СврПили .вконце „файла (EOF). 
Все флаги BookmarkFlags, за исключением флага текущей записи, назначаются 
внутри компонентом TDataSet. Запись помечается текущим флагом при каждом 
вызове для нее переопределенного в производном классе метода GetRecord 
компонентом TDataSet. Метод GetRecord рассматривается ниже в этой главе. 
    Чтобы завершить рассмотрение закладок, выясним, как они используются для 
изменения позиции текущей записи. Эту задачу выполняют два метода— 
InternalSetToRecord и InternalGotoBookmark. Они делают одно и то же— получают 
значение закладки из компонента TDataSet и пытаются переместиться к записи, 
относящейся к этой закладке. 
    Данные методы могут обрабатывать одни и те же исходные данные закладок. 
Просто InternalSetToRecord можно назвать InternalGotoBookmark, поскольку 
InternalGotoBookmark выполняет всю необходимую работу. Единственное реальное 
различие между^этими^етрдачи_заключается в способе передачи параметров 
закладок. Метод trrt'en'CTtSe'ETo'Record получает~указатель на закладку как 
заканчивающуюся двоичным нулем строку, а метод InternalGotoBookmark — как 
указатель на буфер. 
    В примере TFixedAsciiDataset процедура InternalGotoBookmark использует 
метод IndexOfObject объекта FTextRecords списка TStringList для поиска 
значения закладки. Если сравнение прошло успешно, текущая запись (FCurRec) 
устанавливается как новая. В противном случае выдается сообщение об ошибке. 
Вот все, что касается методов управления закладками. Далее перейдем к методам 
управления записями. 

    Листинг 13.5. ASCIIDRV. РАЗ. Процедура управления закладками компонента 
TFixedAsciiDataset        яв.а 

    procedure TFixedAsciiDataset.GetBookmarkData(Buffer: PChar; 
    Data: Pointer); 
    // 
    // Назначение. Возвращает уникальную закладку в буфере записи, 
    //                  передаваемую в TDataSet. Закладка записи 
    //                   сохраняется в специальной записи TRednfo, которая 
    //                  размещается в конце буфера записей, сразу за 
    //                       ''ре"альНамй"''данньаа?';' ---"•"""""'""""" 
    // 
    begin 
    PInteger(Data) /\ := PRednfo (Buffer + FRednfoOfs) .Bookmark; 
    end; 
    procedure TFixedAsciiDataset.SetBookmarkData(Buffer: PChar; 
    Data: Pointer) ; 
    // 
    // Назначение. Устанавливает уникальное значение закладки записи равным 
    //                  значению, передаваемому в TDataSet. Закладка 
    //                  записи сохраняется в специальной записи TRednfo, 
    //                  которая размещается в конце буфера записей, сразу 
    //                  за реальными данными. 
    // 
    begin 
    PRednfo (Buffer + FRednfoOfs) .Bookmark := PInteger (Data) "; 
    end; 


    Глава 13. Создание пользовательских наборов данных                        321 
    11 Использование Delnhi 


    function TFixedAsciiDataset.GetBookmarkFlag(Buffer: PChar) : 
    TBookmarkFlag; 
    // 
    // Назначение. Возвращает BookmarkFlag в буфере записи, который 
    //                  передается в TDataSet. BookmarkFlag записи 
    //                   сохраняется в специальной записи TRecInfo, которая 
    //                   размещается в конце буфера записей, сразу за 
    //                  реальными данными. 
    // 
    begin 
    Result := PRednfo (Buffer + FRednfoOfs) .BookmarkFlag; 
    end; 
    procedure TFixedAsciiDataset.SetBookmarkFlag(Buffer: PChar; 
    Value: TBookmarkFlag); 
    // 
    // Назначение. Устанавливает значение BookmarkFlag записи равным 
    //                   значению, передаваемому в TDataSet. BookmarkFlag 
    //                   записи сохраняется в специальной записи TRecInfo, 
    //                   которая размещается в конце буфера записей, сразу 
    //                   за реальными данными. 
    // 
    begin 
    PRednfo (Buffer + FRednfoOfs) . BookmarkFlag := Value; 
    end; 
    procedure TFixedAsciiDataset.InternalSetToRecord(Buffer: PChar) ; 
    // 
    // Назначение. Эта функция делает то же самое, что и функция 
    //                   InternalGotoBookmark, но ей в качестве параметра 
    //                  передается буфер записи. 
    // 
    begin 
    InternalGotoBookmark (@PRednfo (Buffer + FRednfoOfs) .Bookmark) ; 
    end; 
    procedure TFixedAsciiDataset.InternalGotoBookmark(Bookmark: 
    Pointer) ; 
    // 
    // Назначение. Перемещение к записи с целым значением закладки, 
    //                   которое равно переданному. Если такой записи нет, 
    //                   выдается ошибка. 
    // 
    var 
    Index: Integer; 
    begin 
    Index := FTextRecords.IndexOfObject(TObject(PInteger(Bookmark) /\)); 
    if Index <> -1 then 
    FCurRec := Index 
    else 
    DatabaseError('Bookmark not found'); 
    end; 
    Управление записями 
    Работа с записями осуществляется с помощью следующих девяти методов. 
    I GetRecordCount 
    • AllocRecordBuffer 
    II FreeRecordBuffer 
    • GetRecordSize 
    Г InternalInitRecord 


    322                                     Часть III. Работа с базами данных 


    • InternalAddRecord 
    • Internal-Post 
    • Internal-Delete 
    • GetRecord 

    Что делают первые четыре процедуры, содержащие по одной строке кода, ясно 
из их названий (их реализация, а также комментарии приведены в листинге 13.6). 
    Метод InternalInitRecord вызывается, когда для компонента TDataSet 
требуется инициализировать буфер записи. После выполнения этого метода в 
компоненте TFixedAsciiDataSet буфер просто заполняется пробелами (в кодах 
ASCII 32). 
    Метод InternalAddRecord вызывается, когда записи добавляются неявно с 
помощью методов InsertRecord и AppendRecord. Метод InternalAddRecord 
последовательно получает буфер записей, который уже заполнен данными поля с 
помощью компонента TDataSet. 
    Иначе говоря, компонент TDataSet выбирает данные, которые передаются 
методу InsertRecord или AppendRecord, и для каждого поля таблицы вызывает 
метод SetFieldData, заполняющий буфер записей. Метод SetFieldData 
рассматривается ниже в этой главе, в разделе "Управление полями". В листинге 
13.6 показано, как вставляются и добавляются новые записи в таблицу. 
    Метод InternalPost вызывается, если запись была сохранена (или 
зарегистрирована) в режиме Insert, Append или Edit. Эта процедура, по 
существу, такая же, как и метод InternalAddRecord (для этого достаточно 
взглянуть на несколько строк ее кода). 
    Метод InternalDelete удаляет текущую запись и, если это была последняя 
запись объекта FTextRecords списка TStringList, уменьшается на единицу номер 
текущей записи (FCurRec). 
    Последний, но не наименьший, метод GetRecord, в основном, выполняет три 
задачи. Наиболее важная из них — обработка запросов компонента TDataSet на 
текущую, предыдущую и следующую записи. Другая задача— возврат значения 
состояния (TGetResult), которое показывает компоненту TDataSet результат 
выполнения его последнего запроса. Перечислимый тип, TGetResult, определяется так: 

    TGetResult = (grOK, grBOF, grEOF, grError) ; 

    И, наконец, метод GetRecord генерирует исключительную ситуацию, если флаг 
DoCheck установлен (True) и возникла ошибка. Перейдем к обработке полей, 
которая имеет, наверное, наиболее сложный код во всем пользовательском наборе данных. 

    j Листинг 13.6. ASCIIDRV. РАЗ. Процедуры управления записями компонента 
TFixedAsciiDataSet              j 

    function TFixedAsciiDataSet.GetRecordCount: Longint; 
    // 
    // Назначение. Возврат текущего количества записей в текстовом 
    //                   файле набора данных. 
    // 
    begin 
    Result := FTextRecords.Count; 
    end; 
    function TFixedAsciiDataSet.AllocRecordBuffer: PChar; 
    // 
    // Назначение. Выделение буфера записи. Здесь используется 
    //                   значение FRecBufSize, которое равно сумме размеров 
    //                  данных и структуры TRecInfo. 
    // 
    begin 
    Result := StrAlloc(FRecBufSize); 
    end; 
    procedure TFixedAsciiDataSet.FreeRecordBuffer(var Buffer: PChar); 
    // 
    // Назначение. Очистка буфера записи. Убедитесь перед очисткой 
    //                   всех выделенных буферов, что значение FRecBufSize 
    //                   не изменилось. 
    // 
    begin 
    StrDispose(Buffer) ; 
    end; 


    Глава 13. Создание пользовательских наборов данных                        323 


    function TFixedAsciiDataset.GetRecordSize: Word; 
    // 
    // Назначение. Вычисление суммы размера "сырых" данных и размера 
    //                   структуры TRecInfo. 
    // 
    begin 
    Result := FRawBufSize; 
    end; 
    procedure TFixedAsciiDataset.InternalInitRecord(Buffer: PChar); 
    // 
    // Назначение. Вызывается методами TDataSet.InitRecord и 
    //                  TDataSet.ClearFields при необходимости очистить 
    //                  буфер. Поскольку пробелы означают пропущенные 
    //                   нулевые поля (значение Null), необходимо, чтобы 
    //                  методу GetFieldData было известно, как обрабатывать 
    //                  такие поля. 
    // 
    begin 
    FillCharfBuffer^, RecordSize, 32); 
    end; 
    procedure TFixedAsciiDataset.InternalAddRecord(Buffer: Pointer; 
    Append: Boolean); 
    // 
    // Назначение. Вызывается косвенно методами TDataSet.InsertRecord 
    //                  и TDataSet.AppendRecord. После увеличения номера 
    //                   последней закладки, FLastBookmark, добавляется 
    //                  строковый буфер в массив текстовых записей вместе с 
    //                  указателем на объект, содержащий уникальное 
    //                   значение закладки для этой новой записи. 
    // 
    begin 
    Inc(FLastBookmark) ; 
    if Append then InternalLast; 
    FTextRecords.InsertObject(FCurRec, PChar(Buffer), Pointer(FLastBookmark)) ; 
    FSaveChanges := True; 
    end; 
    procedure TFixedAsciiDataset. Internal-Post; 
    // 
    // Назначение. Отмечает, что данные изменились и 
    //               должны быть сохранены, когда таблица будет закрыта. 
    //                  Затем мы сохраняем измененное содержимое 
    //                  существующей записи или добавляем новую запись. 
    // 
    begin 
    if State = dsEdit then 
    FTextRecords[FCurRec] := ActiveBuffer 
    else 
    begin 
    // Мы вставляем (или добавляем) закладку, увеличивая значение счетчика закладок 
    //и сохраняя данные. 
    Inc(FLastBookmark) ; 
    FTextRecords.InsertObject(FCurRec, ActiveBuffer, 
    Pointer(FLastBookmark)); 
    end; 
    // Устанавливаем флаг, который нужен для сохранения 
    // изменений перед закрытием файла. 
    FSaveChanges := True; 
    end; 


    324                                     Часть III. Работа с базами данных 


    procedure TFixedAsciiDataset.InternalDelete; 
    // 
    // Назначение. Делает отметку, что данные изменились, поэтому 
    //                  они должны быть сохранены при закрытии таблицы. 
    //                   Затем удаляем запись. Кроме того, если запись, 
    //                   которая удалена, была последней, значение текущей 
    //                   закладки записи уменьшается на 1. 
    // 
    begin 
    FTextRecords.Delete(FCurRec) ; 
    if FCurRec >= FTextRecords.Count then Dec(FCurRec); 
    FSaveChanges := True; 
    end; 
    function TFixedAsciiDataset.GetRecord(Buffer: PChar; 
    GetMode: TGetMode; DoCheck: Boolean): TGetResult; 
    // 
    // Назначение. Это многоцелевая функция, которая выполняет три 
    //                   задачи: получает данные о текущей, предыдущей 
    //                  и следующей записях. Она возвращает состояние 
    //                   (TGetResult) и, если флаг DoCheck равен True и 
    //                  возникла ошибка, вызывает исключительную 
    //                  ситуацию. 
    // 
    begin 
    if FTextRecords.Count < 1 then 
    Result := grEOF 
    else begin 
    Result := grOK; 
    case GetMode of 
    gmNext: 
    if FCurRec >= RecordCount - 1 then 
    Result := grEOF 
    else 
    Inc(FCurRec) ; 
    gmPrior: 
    if FCurRec <= 0 then 
    Result := grBOF 
    else 
    Dec(FCurRec) ; 
    gmCurrent: 
    if (FCurRec < 0) or (FCurRec >= RecordCount) then 
    Result := grError; 
    end; 
    if Result = grOK then 
    begin 
    StrLCopy(Buffer, PChar(FTextRecords[FCurRec]), FRecBufSize) ; 
    with PRednfo (Buffer + FRednfoOfs) л do 
    begin 
    BookmarkFlag := bfCurrent; 
    Bookmark := Integer(FTextRecords.Objects[FCurRec]); 
    end; 
    end else 
    if (Result = grError) and DoCheck then 
    DatabaseError('No Records'); 
    end; 
    end; 


    Управление полями 

    Управление полями осуществляется с помощью методов GetFieldData и SetFieldData. 
    Методу GetFieldData передается объект типа TField и указатель на буфер. 
Используя значения FieldNo и DataType объекта TField, метод GetFieldData 
извлекает исходные данные поля из текущего буфера записи и пересылает их в 
указанный буфер. Когда необходимые данные поля помещаются в выходной буфер, он 
(при необходимости) 

    Глава 13. Создание пользовательских наборов данных                       325 

    сначала должен быть конвертирован в универсальный тип формата данных. В 
табл. 13.3 показаны оба универсальных формата, необходимые для компонента 
TDataSet, а также текстовые форматы, которые используются компонентом TFixedAsciiDataSet. 
    Метод SetFieldData выполняет задачу, противоположную той, которую 
выполняет метод GetFieldData. Он тоже получает объект Т Fie Id и указатель 
буфера. Но в этом случае буфер содержит данные (в соответствующем 
универсальном формате) для сохранения в текущей записи, основанной на 
информации заданного объекта TField. Метод SetFieldData преобразует данные 
буфера в соответствующее строковое представление для вставки в текущий буфер 
записи. Для того чтобы разобраться в этом преобразовании, обратитесь к табл. 
13.3, в которой перечислены универсальные форматы. Код, приведенный в 
листинге, также очень поучителен для понимания процесса преобразования. 
    Из всех методов компонента TDataSet, которые необходимо переопределить, 
методы GetFieldData и SetFieldData сложнее всего заставить правильно работать 
из-за конвертирования данных. 

    Таблица 13.3. Универсальные форматы данных, используемые методами 
GetFieldData и SetFieldData 

    Тип поля	Представление универсального формата данных Строка TFixedAsciiDataSet 

    ftString	Pchar строкового буфера	Извлеченные символы до ограничителя nil 
    ftFloat	Ptr на 8-байтовый тип Double	Строковое представление плавающего с точкой 
    ftlnteger	Ptr на 4-байтовый тип Integer	Строковое представление целого 
    ftBoolean	Ptr на 2-байтовый тип WordBool. False —0 True о 0	Строка типа 
Тхххх или Fxxxx 
    ftDate	Ptr на 4-байтовый тип Integer Дни с 1/1/0001+1	Строка даты 
    ftTime	Ptr на 4-байтовый тип Integer Миллисекунды от полуночи	Строка времени 
    ftDateTime	Ptr на 8-байтовый тип Double	Строка даты/времени Миллисекунды с 
момента 1/1/0001+1 

    Листинг 13.7. ASCIIDRV. PAS. Процедуры управления записями компонента TFixedAsciiDataSet 

    function TFixedAsciiDataSet.GetFieldData(Field: TFieid; 
    Buffer: Pointer): Boolean; 
    //———————————————————————————————————————_ 
    // Назначение. Копирование данных из буфера записи в буфер полей. 
    // 
    var 
    FieldAddrInBuffer : Pointer; 
    FieldText : String; 
    function AsciiDateTimeToDateTime(DateTimeStr : String): Double; 
    const 
    TimeDelta : Double = 0; 
    var 
    AM_PM_Pos : Byte; 
    begin 
    AM_PM_Pos := AnsiPosCM', AnsiUpperCase (DateTimeStr) ) - 1; 
    if AM_PM_Pos > 0 then 
    begin 
    if UpCase(DateTimeStr[AM_PM_Pos]) = 'P' then 
    TimeDelta := 0.5;   // Преобразовать к военному времени; 
    // добавить 12 часов. 
    DateTimeStr := Trim(Copy(DateTimeStr, I, AM_PM_Pos - 1)); 
    end; 


    326                                     Часть III. Работа с базами данных 


    Result := TimeStampToMSecs(DateTimeToTimeStamp( 
    StrToDateTime(DateTimeStr) + TimeDelta)) ; 
    end; 
    begin 
    Result := False; 
    if IsEmpty or (Field.FieldNo < 1) then Exit; 
    //if Field.FieldNo = 0 then Showmessage('Field.Name'); 
    with Field, PFieldInfo(FFieldInfo[FieldNo - 1])" do 
    try 
    FieldAddrInBuffer := Pointer(Integer(ActiveBuffer) + FieldOfs - 1); 
    // Извлечение текстового значения поля из буфера. 
    FieldText := '' ; 
    SetLength(FieldText, FieldSize + 1); 
    StrLCopytPChar(FieldText), FieldAddrInBuffer, FieldSize); 
    FieldText := Trim(FieldText); 
    if Length(TrimRight(FieldText)) = 0 then Exit; 
    // Выполнить преобразование/передачу поля в выходной буфер. 
    case Field.DataType of 
    ftString:   StrLCopy(Buffer, FieldAddrInBuffer, FieldSize); 
    ftFloat:    Double(Buffer") := StrToFloat(FieldText); 
    ftlnteger: Longint(Buffer") := SysUtils.StrToInt(FieldText); 
    ftBoolean: WordBool(Buffer") := FieldText[1] = 'Т'; 
    ftDate:     Longint(Buffer") := 
    DateTimeToTimeStamp(StrToDate(FieldText)).Date; 
    ftTime:     Longint(Buffer") := 
    DateTimeToTimeStamp(StrToTime(FieldText)).Time; 
    ftDateTime: Double(Buffer") := 
    AsciiDateTimeToDateTime(FieldText) ; 
    end; 
    except 
    // Если появилась ошибка преобразования, то причиной 
    // может быть пустое (пробел) или искаженное значение поля. 
    // В обоих случаях возвращается значение False, чтобы 
    // указать, что содержимое поля имеет значение Null. 
    // Переменная Result установлена равной False выше. 
    end; 
    // Все работает нормально, поэтому возвращаем значение True. 
    Result := True; 
    end; 
    procedure TFixedAsciiDataset.SetFieldData(Field: TField; 
    Buffer: Pointer) ; 
    //-_—————————————————————_————————————————_
    // Назначение. Копирование данных из буфера полей в 
    //             соответствующее место буфера записи. 
    // 
    function Right Justify(const Str : String; Size : Integer):String; 
    // 
    // Назначение. Заполняем строку достаточным количеством 
    //             пробелов, чтобы выровнять ее по результирующей 
    //             строке данного размера. 
    // 
    begin 
    SetLength(Result, Size); 
    FillMemory(Pointer(Result), Size, ORD(' ')); 
    Result := Result + Str; 
    Result := Copy(Result, Length(Result) - Size + 1, Size); 
    end; 


    Глава 13. Создание пользовательских наборов данных                        327 


    const 
    TrueText : PChar = "I" ; 
    FalseText : PChar = 'F'; 
    var 
    FieldAddrInBuffer : Pointer; 
    TempStr : String; 
    StrSize : Integer; 
    TimeStamp :TTimeStamp; 
    begin 
    if Field.FieldNo < 0 then Exit; 
    With field, PFleldInfofFrieldInfo[FieldNo - 1])" do 
    try 
    FieldAddrInBuffer := Pointer(Integer(ActiveBuffer) + FieldOfs - 1); 
    FillMemory(FieldAddrInBuffer, FieldSize, ORD(' ')); // Очищаем поле. 
    case  DataType  of 
    ftString: 
    begin 
    StrSize := StrLen(Buffer) ; 
    if StrSize > FieldSize then StrSize := FieldSize; 
    MoveMemory(FieldAddrInBuffer, Buffer, StrSize); 
    end; 
    ftBoolean: 
    if WordBool(Buffer") then 
    MoveMemory(FieldAddrInBuffer, TrueText, FieldSize) 
    else 
    MoveMemory(FieldAddrInBuffer, FalseText, FieldSize); 
    ftlnteger: 
    begin 
    TempStr := Right Justify(IntToStr (Longint(Buffer")), FieldSize); 
    MoveMemory(FieldAddrInBuffer, Pointer(TempStr), FieldSize); 
    end; 
    ftFloat: 
    begin 
    TempStr := RightJustifytFloatToStr(Double(Buffer")), FieldSize); 
    MoveMemory(FieldAddrInBuffer, Pointer(TempStr), FieldSize); 
    end; 
    ftDate: 
    begin 
    TimeStamp.Time :° 0; 
    TimeStamp.Date := Longint (Buffer") ; 
    TempStr := DateTimeToStr(TimeStampToDateTime(TimeStamp)); 
    MoveMemory(FieldAddrInBuffer, Pointer(TempStr), FieldSize); 
    end; 
    ftTime: 
    begin 
    TimeStamp.Time := Longint(Buffer") ; 
    TimeStamp.Date := 0; 
    TempStr := DateTimeToStr(TimeStampToDateTime(TimeStamp)); 
    MoveMemory(FieldAddrInBuffer, Pointer(TempStr), FieldSize); 
    end; 
    ftDateTime: 
    begin 
    TempStr := FormatDateTime(ShortDateFormat + ' ' + 
    ShortTimeFormat, TimeStampToDateTime( MSecsToTimeStamp(Double(Buffer")))) ; 
    MoveMemory(FieldAddrInBuffer, Pointer(TempStr), Length(TempStr)); 
    end; 
    end; 
    except 
    Abort; 
    end; 
    DataEvent(deFieldChange, Longint(Field)) ; 
    end; 


    328                                     Часть III. Работа с базами данных 


    Управление перемещением 

    Иногда необходимо перейти к первой или последней записи набора данных. 
Предназначенные для этого специальные процедуры перемещения называются 
InternalFirst и InternalLast и показаны в листинге 13.8. Метод InternalFirst 
просто устанавливает значение счетчика текущей записи (FCurRec) на 1 меньше 
номера первой записи, т.е. равным -1. Метод InternalLast, с другой стороны, 
устанавливает значение счетчика текущей записи равным FTextRecords . Count, 
которое на 1 больше номера последней записи. Смысл состоит в том, чтобы 
имитировать обращение к записям, выходящим за границы файла. Эти критические 
значения используются для определения, когда набор данных находится в начале 
или конце файла. Обработка этих состояний выполняется в методе Resync 
компонента TDataSet. 

    Листинг 13.8. ASCIIDRV. РАЗ. Процедуры компонента TFixedAsciiDataSet для 
управления перемещением 

    procedure TFixedAsciiDataset.InternalFirst; 
    //———————————————————————————————————————_
    // Назначение. Имитируется состояние начала обработки файла. Это 
    //             означает, что мы должны установить позицию на 
    //             специальное место перед первой записью. 
    //             В противном случае мы окажемся на второй записи 
    //             после вызова метода Resync. 
    // 
    begin 
    FCurRec := -1; 
    end; 
    procedure TFixedAsciiDataset.InternalLast; 
    // 
    // Назначение. Имитируется состояние достижения конца файла. 
    //                   Переменная FTextRecords.Count на единицу больше 
    //                   наибольшего значения счетчика записей FCurRec. 
    // 
    begin 
    FCurRec := FTextRecords.Count; 
    end; 


    Процедура обработки исключительных ситуаций 

    Заключительный метод компонента TDataSet, который мы рассмотрим, 
InternalHandleException, вызывается, если возникает ошибка при прохождении 
данных через компонент во время разработки. В примере TFixedAsciiDataset 
просто передает исключительные ситуации стандартному обработчику 
исключительных ситуаций Delphi, как видно из листинга 13.9. 

    1 Листинг 13.9. ASCIIDRV. PAS. Процедура обработки исключительных ситуаций 
компонента TFixedAsciiDataset 

    procedure TFixedAsciiDataset.InternalHandleException; 
    begin 
    Application.HandleException(Self) ; 
    end; 


    Резюме 

    Теперь вы знаете об всем, что нужно для создания пользовательского набора 
данных в Delphi 3. Чтобы увидеть весь исходный файл компонента 
TFixedAsciiDataSet, загрузите файл ASCIIDRV. PAS. Осталось только рассказать о 
том, как добавить компонент TFixedAsciiDataSet в палитру компонентов Delphi. 
    Сначала нужно добавить специальную процедуру регистрации, которая 
вызывается Delphi при загрузке пакета компонента. В листинге 13.10 приведена 
процедура Register, используемая в этом примере. 

    Глава 13. Создание пользовательских наборов данных                        329 


    ЛИСТИНГ 13.10. \UDELPHI3\CHP13\ASCIIDRV.PAS. 

    Процедура Register ДЛЯ добавления TFixedAsciiData- ,f| 
    Set в палитру компонентов Delphi                                           
                       ' 
    procedure Register; 
    implementation 
    uses SysUtils, Forms, Dialogs; 
    procedure Registers
    begin 
    RegisterComponents('UDelphi3',[Asciidrv.TFixedAsciiDataset]) ; 
    end; 

    Теперь, когда процедура регистрации реализована, можно скомпоновать пакет 
для нашего компонента. Для создания и инсталляции пакета компонента выполните 
следующие действия. 
    1. Выберите команду File^Close All и сохраните все открытые проекты. 
    2. Выберите команду File^New и щелкните на пиктограмме Package. 
    3. Когда появится диалоговое окно New Package, в поле File Name наберите 
путь к каталогу Delphi 3\Bin (например, \ PROGRAM FILES\BORLAND\DELPHI 3\BIN) 
с последующим именем файла пакета (AS С DRV 30). 
    4. В поле Description наберите UD3 Chapter 13 - Custom DataSet Example. 
    5. Щелкните на кнопке ОК. 
    6. В следующем диалоговом окне щелкните на кнопке Add и добавьте файл 
компонента ASCIIDRV. PAS. 
    (По умолчанию он находится в каталоге \UDELPHI3\CH13.) 
    
    7. Выберите вкладку Requires и снова щелкните на кнопке Add, чтобы 
добавить необходимый пакет. В диалоговом окне Add Package наберите следующее 
имя пакета: VCLDB30. Обратите внимание на то, что пакет VCL30 установлен по умолчанию. 
    8. Щелкните на кнопке Install. 
    9. Закройте диалоговое окно Package. 
    Можете создать новый проект и поместить из вкладки UDelphi3 компонент 
TFixedAsciiDataset в форму. На дискете также можно найти проект, который 
использовался для тестирования ASCII-драйвера. Чтобы увидеть компонент 
TFixedAsciiDataset в действии, загрузите следующий проект: 
\UDELPHI3\CH13\CUSTOMDB. DPR. 
    Может показаться, что для создания пользовательского набора данных 
необходимо реализовать огромное количество методов. Однако если разбить эти 
методы на группы, то с ними можно легко справиться. Для пользовательских 
наборов данных существует гораздо больше функциональных возможностей, чем 
можно реализовать. В этой главе был пролит свет на минимальный набор процедур, 
которые необходимо реализовать, чтобы получить работающий пользовательский 
набор данных. Теперь у вас есть работающая модель, которую можно расширить, 
как вам угодно. 

    330                                     Часть III. Работа с базами данных 
