Сообщений 1    Оценка 150        Оценить  
Система Orphus

Разработка DB-Aware компонентов

Авторы: Михаил Голованов
Евгений Веселов

Источник: RSDN Magazine #4-2003
Опубликовано: 24.01.2004
Исправлено: 11.02.2006
Версия текста: 1.0
Структура DB-Aware компонентов
Класс TDataLink
Выбор способа организации канала данных.
Разработка компонентов, отображающих набор данных
Разработка компонентов, модифицирующих набор данных

В этот раз мы бы хотели рассказать о процессе разработки компонентов для работы с наборами данным. Данная статья совсем не случайно является завершающей в нашем цикле, ведь для построения DB-Aware компонента, как вы сами убедитесь чуть позже, необходимы навыки создания как визуальных контролов, так и невизуальных компонентов, диалогов и т.д. Рэй Конопка назвал процесс создания таких компонент преобразованием. Почему? Об этом ниже.

Структура DB-Aware компонентов

Компоненты, работающие с данными, часто называют DB-Aware-компонентами (далее DB-A). Их отличительной особенностью является возможность подключения к какой-либо части базы данных, скажем, к таблице или отдельному полю. Создание компонентов для работы с данными не является сложной задачей. В отличие от других категорий компонентов, DB-A не имеют единой иерархии классов. Процесс создания таких компонентов может быть определен как преобразование какого-либо элемента управления в элемент управления, работающий с данными.

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

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

В зависимости от возможностей DB-A можно условно разделить на две категории:

Естественно, что первый тип компонентов устроен проще, чем второй.

В общем случае схема построения компонентов, работающих с данными, может быть представлена следующим образом (рисунок 1):


Рисунок 1

Перейдем к рассмотрению реализации DataLink. Для нее существует соответствующий класс TDataLink, ему посвящен следующий раздел.

Класс TDataLink

Класс TDataLink является базовым классом для иерархии классов связи с БД. Класс обеспечивает коммуникационный канал для чтения/записи в БД.

Данный класс описан в модуле DB, а его интерфейсная часть выглядит следующим образом (private-секция опущена):

TDataLink = class(TPersistent)
      
  protected
    procedure ActiveChanged; virtual;
    procedure CheckBrowseMode; virtual;
    procedure DataEvent(Event: TDataEvent; Info: Longint); virtual;
    procedure DataSetChanged; virtual;
    procedure DataSetScrolled(Distance: Integer); virtual;
    procedure EditingChanged; virtual;
    procedure FocusControl(Field: TFieldRef); virtual;
    function GetActiveRecord: Integer; virtual;
    function GetBOF: Boolean; virtual;
    function GetBufferCount: Integer; virtual;
    function GetEOF: Boolean; virtual;
    function GetRecordCount: Integer; virtual;
    procedure LayoutChanged; virtual;
    function MoveBy(Distance: Integer): Integer; virtual;
    procedure RecordChanged(Field: TField); virtual;
    procedure SetActiveRecord(Value: Integer); virtual;
    procedure SetBufferCount(Value: Integer); virtual;
    procedure UpdateData; virtual;
    property VisualControl: Boolean read FVisualControl write FVisualControl;
  public
    constructor Create;
    destructor Destroy; override;
    function Edit: Boolean;
    function ExecuteAction(Action: TBasicAction): Boolean; dynamic;
    function UpdateAction(Action: TBasicAction): Boolean; dynamic;
    procedure UpdateRecord;
    property Active: Boolean read FActive;
    property ActiveRecord: Integer read GetActiveRecord write SetActiveRecord;
    property BOF: Boolean read GetBOF;
    property BufferCount: Integer read FBufferCount write SetBufferCount;
    property DataSet: TDataSet read GetDataSet;
    property DataSource: TDataSource read FDataSource write SetDataSource;
    property DataSourceFixed: Boolean read FDataSourceFixed write FDataSourceFixed;
    property Editing: Boolean read FEditing;
    property Eof: Boolean read GetEOF;
    property ReadOnly: Boolean read FReadOnly write SetReadOnly;
    property RecordCount: Integer read GetRecordCount;
  end;

Теперь подробно остановимся на ключевых свойствах:

СвойстваОписание
property ActiveПоказывает, активен ли набор данных для данного TDataLink, если Active равно False, то набор не может ни читать, ни записывать данные в базу данных.
property ActiveRecordНомер текущей записи в наборе. Для ненаправленных наборов данных всегда равен 0
property BOFФлаг активности первой записи в наборе данных
property BufferCountКоличество кэшированных записей, для ненаправленных наборов - всегда равен 0.
property DataSetНабор данных, которым управляет данный TDataLink.
property DataSourceОпределяет объект TDataSource, который используется владельцем TDataLink для связи с базой данныхTDataLink также отвечает за обработку всех событий поступающих с набора данных.
property DataSourceFixedВ некоторых случаях необходимо запретить смену набора данных (например, во время проведения некоторых операций). По умолчанию DataSourceFixed равно False
property EditingНабор находится в состоянии редактирования
property EofАктивна последняя запись в наборе
property ReadOnlyУстановка режима «Только для чтения» для набора данных
property RecordCountКоличество записей в наборе
Таблица 1. Ключевые свойства класса TDataLink

Основные методы TDataLink:

МетодыОписание
procedure ActiveChanged; virtual;Вызывается тогда, когда набор данных изменил статус активности. Для TDataLink данный метод имеет пустую реализацию (как и большинство нижеследующих). Если компонент не должен обслуживать данное событие, просто не перекрывайте данный метод в потомке TDataLink. Это относится практически ко всем событиям, описанным далее
procedure CheckBrowseMode; virtual;Срабатывает ДО возникновения события, которое может изменить набор данных.
procedure DataEvent(Event: TDataEvent; Info: Longint); virtual;Вызывается при возникновении многих событий набора данных. type TDataEvent = (deFieldChange, deRecordChange, deDataSetChange, deDataSetScroll, deLayoutChange, deUpdateRecord, deUpdateState, deCheckBrowseMode, dePropertyChange, deFieldListChange, deFocusControl, deParentScroll, deConnectChange);procedure DataEvent(Event: TDataEvent; Info: Longint); virtual;DataSetChanged вызывается автоматически при возникновении различных событий. Event – содержит тип произошедшего события, а Info предоставляет дополнительную информацию о нем.DataSetChanged обрабатывает данные события путем вызова соответствующих методов, как показано ниже:
procedure DataSetChanged;DataSetChanged – отвечает за изменения содержимого набора данных. Все события, которые изменяют содержимое набора данных (активизация режима редактирования набора данных, вставка или удаление записей) форсируют вызов данного метода. События, специфичные представлению данных в наборе в контексте data-aware компонента (скроллинг, смена расположения информации) также ведут к вызову данного метода. Тем не менее функциональность этого метода сводится к тому, чтобы просто вызватьметод RecordChanged. Классы-наследники могут перекрыть данный метод с целью обеспечения дополнительной функциональности.
procedure DataSetScrolled(Distance: Integer);Реагирует на изменения положения активной записи в наборе (скроллинг )
procedure EditingChanged; virtual;Реакция на смену режима редактирования
procedure FocusControl(Field: TFieldRef); virtual;Форсирует установку фокуса на элемент управления, связанный с TDataLink. Естественно, в базовом классе метод не имеет реализации.
procedure LayoutChanged; virtual;Вызывает метод DataSetChanged. LayoutChanged призван обеспечить механизм для вызова метода, который обеспечит представление данных после изменения набора данных data-aware компонентом.Хорошим примером выступает изменения порядка следования столбцов в TCustomDBGrid.
function MoveBy(Distance: Integer): Integer; virtual;The MoveBy изменяет активную запись связанного набора данных в соответствии с параметром Distance. Если он больше 0, MoveBy активизирует соответствующую запись после текущей, иначе – до текущей. MoveBy возвращает количество записей, на которые произошло перемещение (данное значение может быть меньше Distance).MoveBy вызывает метод MoveBy связанного набора данных.
procedure RecordChanged(Field: TField); virtual;Вызывается при изменении содержимого текущей записи. Срабатывает после того, как выполнена операция Post набора данных.Параметр Field показывает какое именно поле изменило значение. Если Field=nil – любое количество полей или записей могли изменить значения.
Таблица 2. Основные методы класса TDataLink

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

При построении собственных компонентов используются потомки класса, например, TFieldLink. Ниже приведена классовая иерархия потомков TDataLink (рисунок 2):


Рисунок 2

Кратко опишем назначение классов-наследников TDataLink.

КлассОписание
TFieldDataLinkСвязь с полем таблицы БД.
TListSourceLinkTDataSourceLinkСвязь для организации Lookup полей
TNavDataLinkСвязь для навигатора под набору данных (TDBNavigator)
TDetailDataLinkTMasterDataLinkСвязь для управления наборами данных, связанными отношениями главный – подчиненный.
Таблица 3.

Выбор способа организации канала данных.

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

Давайте рассмотрим, как реализована эта концепция в одном из наиболее часто используемых классов – TDEdit, «усеченный» вариант описания которого приведен ниже:

TDBEdit = class(TCustomMaskEdit)
  private
    FDataLink: TFieldDataLink;
    FCanvas: TControlCanvas;
    // вырезано для экономии места

    procedure ActiveChange(Sender: TObject);
    procedure DataChange(Sender: TObject);
    procedure EditingChange(Sender: TObject);
    procedure SetDataField(const Value: string);
    procedure SetDataSource(Value: TDataSource);
    procedure SetFocused(Value: Boolean);
    procedure SetReadOnly(Value: Boolean);
    procedure UpdateData(Sender: TObject);
    procedure WMCut(var Message: TMessage); message WM_CUT;
    procedure WMPaste(var Message: TMessage); message WM_PASTE;
    procedure WMUndo(var Message: TMessage); message WM_UNDO;
    procedure CMEnter(var Message: TCMEnter); message CM_ENTER;
    procedure CMExit(var Message: TCMExit); message CM_EXIT;
    procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
    procedure CMGetDataLink(var Message: TMessage); message CM_GETDATALINK;

  protected
    procedure Change; override;
    function EditCanModify: Boolean; override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
    procedure KeyPress(var Key: Char); override;
    procedure Loaded; override;
    procedure Notification(AComponent: TComponent;
      Operation: TOperation); override;
    procedure Reset; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function UseRightToLeftAlignment: Boolean; override;
    property Field: TField read GetField;
    // вырезано для экономии места
  published
    // вырезано для экономии места
    property DataField: string read GetDataField write SetDataField;
    property DataSource: TDataSource read GetDataSource write SetDataSource;
    property ReadOnly: Boolean read GetReadOnly write SetReadOnly default False;
  end;

Как было сказано выше, канал связи создается в конструкторе. Методы обслуживания событий набора данных реализованы в виде обработчиков событий.

constructor TDBEdit.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  inherited ReadOnly := True;
  ControlStyle := ControlStyle + [csReplicatable];
  FDataLink := TFieldDataLink.Create;
  FDataLink.Control := Self;
  FDataLink.OnDataChange := DataChange;
  FDataLink.OnEditingChange := EditingChange;
  FDataLink.OnUpdateData := UpdateData;
  FDataLink.OnActiveChange := ActiveChange;
end;

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

Список событий TFieldDataLink приведен в таблице:

ОбработчикОписание
OnDataChangeИзменения данных вызванные одной из следующих причин:- Переход к новой записи или столбцу- Переход в режим редактирования набора данных- Изменения свойств DataSource или DataField -Восстановление старых значений вызовом Cancel
OnEditingChangeВход или выход в/из режима редактирования набора данных
OnUpdateDataЗапись изменений в БД
OnActiveChangeИзменение свойства Active подключенного набора данных
Таблица 4

В силу своей простоты такой способ организации DB-AWARE-компонентов является оптимальным при создании достаточно простых решений, однако он имеет несколько недостатков, среди которых:

Вторым распространенным подходом является написание своего канала связи для компонентов. Это намного более трудоемкая процедура, но она того стоит. Дальнейшие примеры этой главы демонстрируют разработку DB-Aware-компонентов именно таким способом.

Разработка компонентов, отображающих набор данных

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

В библиотеке стандартных компонентов Delphi есть интересный контрол – DBRadioGroup, в качестве значений для радиокнопок он использует список типа TStrings, который можно инициализировать как на этапе проектирования, так и на этапе разработки. Однако достаточно часто (например, при разработке систем тестирования) необходимо этот список инициализировать из БД, что данный компонент делать не в состоянии.

Мы напишем RadioGroup-компонент, который сможет заполнять список радиокнопок согласно данным из поля таблицы.


Рисунок 3

В данном случае удобнее всего начать разработку с наследника класса TDataLink, поэтому рассмотрим его объявление:

type
  TmmDBListRadioGroup = class;

  TmmRGListDataLink = class(TFieldDataLink)
  private
    FRadioGroup: TmmDBListRadioGroup;
  protected
    procedure ActiveChanged; override;
    procedure RecordChanged(Field: TField); override;
    function ValidListSource: Boolean;
    function ValidListField: Boolean;
  public
    constructor Create(AOwner: TmmDBListRadioGroup); reintroduce; virtual;
  end;

В первую очередь обратим внимание на упреждающее объявление класса TmmDBListRadioGroup. Согласно структуре DB-Aware компонента, оба класса, как класс связи с данными, так и сам компонент, должны «знать» друг о друге.

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

Теперь рассмотрим реализацию класса связи с данными:

procedure TmmRGListDataLink.ActiveChanged;
begin
  inherited;
  FRadioGroup.RefreshButtons;
end;

constructor TmmRGListDataLink.Create(AOwner: TmmDBListRadioGroup);
begin
  inherited Create;
  FRadioGroup := AOwner;
end;

procedure TmmRGListDataLink.RecordChanged(Field: TField);
begin
  inherited;
  if Assigned(Field) and (Field = DataSet.FindField(FieldName)) then
    FRadioGroup.RefreshButtons;
end;

function TmmRGListDataLink.ValidListField: Boolean;
begin
  Result := ValidListSource and Assigned(DataSet.FindField(FieldName))
end;

function TmmRGListDataLink.ValidListSource: Boolean;
begin
  Result := Assigned(DataSource) and Assigned(DataSet)
end;

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

Метод RecordChanged требует достаточно аккуратного обращения – он вызывается очень часто (при перемещении от одной записи к другой, смене состояния набора данных и т.д.). В некотором смысле данный метод напоминает контроллер обслуживания события движения мышкой, так как оба должны обслуживать огромное число событий и, естественно, должны работать очень быстро.

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

  mmDBListRadioGroup = class(TCustomRadioGroup)
  private
    FListLink: TmmRGListDataLink;
    FListField: string;
    procedure SetListField(const Value: string);
    procedure SetListSource(const Value: TDataSource);
    function GetListSource: TDataSource;
  protected
    procedure RefreshButtons; virtual;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property Items;
  published
    property ListSource: TDataSource read GetListSource write SetListSource;
    property ListField: string read FListField write SetListField;
    // published props
    property Align;
    property Anchors;
    property BiDiMode;
    property Caption;
    property Color;
    property Columns;
    property Ctl3D;
    property DragCursor;
    property DragKind;
    property DragMode;
    property Enabled;
    property Font;
    property ItemIndex;
    property Constraints;
    property ParentBiDiMode;
    property ParentColor;
    property ParentCtl3D;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ShowHint;
    property TabOrder;
    property TabStop;
    property Visible;
    property OnClick;
    property OnContextPopup;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDock;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnStartDock;
    property OnStartDrag;
  end;

constructor TmmDBListRadioGroup.Create(AOwner: TComponent);
begin
  inherited;
  FListLink := TmmRGListDataLink.Create(Self);
end;

destructor TmmDBListRadioGroup.Destroy;
begin
  FListLink.Free;
  inherited;
end;

function TmmDBListRadioGroup.GetListSource: TDataSource;
begin
  Result := FListLink.DataSource;
end;

procedure TmmDBListRadioGroup.RefreshButtons;
var
  VOldPlace: TBookmark;
begin
  if FListLink.ValidListSource and FListLink.DataSet.Active
    then
    with FListLink.DataSet do
    begin
      DisableControls;
      FListLink.FieldName := FListField;
      if FListLink.ValidListField then
      begin
        VOldPlace := FListLink.DataSet.GetBookmark;
        Items.Clear;
        First;
        while not Eof do
        begin
          Items.Add(FieldValues[ListField]);
          Next;
        end;
        FListLink.DataSet.GotoBookmark(VOldPlace);
      end;
      EnableControls;
    end
else
  begin
   Enabled := False;
   Items.Clear;
  end;
end;

procedure TmmDBListRadioGroup.SetListField(const Value: string);
begin
  if FListField <> Value then
  begin
    FListField := Value;
    RefreshButtons;
  end;
end;

procedure TmmDBListRadioGroup.SetListSource(const Value: TDataSource);
begin
  FListLink.DataSource := Value;
  RefreshButtons;
end;

Замечания к коду:

  1. Свойство DataSource компонента на самом деле - одноименное свойство TDataLink
  2. DB-Aware компонент выступает родителем для класса-связки, а, следовательно, создает и уничтожает его.
  3. Для того, чтобы отключить/включить TDataLink необходимо вызвать методы DisableControls/EnableControls набора данных.

Замечания по поводу правил хорошего тона:

Разработка компонентов, модифицирующих набор данных

Ранее мы рассмотрели компонент, который может отображать данные из набора, теперь попробуем создать еще один компонент, который сможет модифицировать набор данных.

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


Рисунок 4

Когда же набор данных переходит в режим редактирования или добавления новой записи компонент принимает следующий вид и позволяет автоматически занести выбранное значение в поле данных:


Рисунок 5

Для этого нам необходимо ввести еще один канал данных, ориентированный уже на модификацию данных.

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

Давайте рассмотрим реализацию канала связи для модификации данных :

  TmmDBLookUpRadioGroup = class;

  TmmRGWriteDataLink = class(TFieldDataLink)
  private
    FRadioGroup: TmmDBLookUpRadioGroup;
  protected
    procedure DataSetScrolled(Distance: Integer); override;
    procedure EditingChanged; override;
    function EditState: Boolean;
    function ValidDataSource: Boolean;
    function ValidDataField: Boolean;
  public
    constructor Create(AOwner: TmmDBLookUpRadioGroup); reintroduce; virtual;
  end;

{ TmmRGWriteDataLink }


constructor TmmRGWriteDataLink.Create(AOwner: TmmDBLookUpRadioGroup);
begin
  inherited Create;
  FRadioGroup := AOwner;
  EditingChanged;
end;


procedure TmmRGWriteDataLink.DataSetScrolled(Distance: Integer);
begin
  inherited;
  if ValidDataField then
    with FRadioGroup do
      ItemIndex := Items.IndexOf(DataSet.FieldByName(FieldName).AsString);
end;

procedure TmmRGWriteDataLink.EditingChanged;
begin
  inherited;
  FRadioGroup.Enabled:=EditState;
end;

function TmmRGWriteDataLink.EditState: Boolean;
begin
  Result := ValidDataField and (DataSet.State in [DsEdit, dsInsert])
end;

function TmmRGWriteDataLink.ValidDataField: Boolean;
begin
  Result := ValidDataSource and Assigned(DataSet.FindField(FieldName));
end;

function TmmRGWriteDataLink.ValidDataSource: Boolean;
begin
  Result := Assigned(DataSource) and Assigned(DataSet);
end;

Особого внимания заслуживают следующие методы:

procedure TmmRGWriteDataLink.DataSetScrolled (Distance: Integer) – в режиме просмотра набора данных списка значений обеспечивает автоматическую синхронизацию выбранной кнопки со значением ключевого поля.

procedure TmmRGWriteDataLink.EditingChanged – реагирует на активизацию / деактивацию режимов набора данных (вставка, редактирование)ипереводит компонент в режим модификации набора данных.

Теперь можно перейти к рассмотрению самого компонента:


  TmmDBLookUpRadioGroup = class(TmmDBListRadioGroup)
  private
    FDataLink: TmmRGWriteDataLink;
    procedure SetDataField(const Value: string);
    procedure SetDataSource(const Value: TDataSource);
    function GetDataField: string;
    function GetDataSource: TDataSource;
  protected
    procedure Click; override;
  public
    constructor Create(AOwner: TComponent); override;
  published
    property DataSource: TDataSource read GetDataSource write SetDataSource;
    property DataField: string read GetDataField write SetDataField;
  end;


  TmmDBLookUpRadioGroup = class(TmmDBListRadioGroup)
  private
    FDataLink: TmmRGWriteDataLink;
    procedure SetDataField(const Value: string);
    procedure SetDataSource(const Value: TDataSource);
    function GetDataField: string;
    function GetDataSource: TDataSource;
  protected
    procedure Click; override;
  public
    constructor Create(AOwner: TComponent); override;
  published
    property DataSource: TDataSource read GetDataSource write SetDataSource;
    property DataField: string read GetDataField write SetDataField;
  end;


{ TmmDBLookUpRadioGroup }

procedure TmmDBLookUpRadioGroup.Click;
begin
  inherited;
  if FDataLink.EditState then
    with FDataLink do
          Field.Value := Items[ItemIndex];
end;

constructor TmmDBLookUpRadioGroup.Create(Aowner: TComponent);
begin
  inherited;
  FDataLink := TmmRGWriteDataLink.Create(Self);
  Enabled:=False;
end;

function TmmDBLookUpRadioGroup.GetDataField: string;
begin
  Result := FDataLink.FieldName;
end;

function TmmDBLookUpRadioGroup.GetDataSource: TDataSource;
begin
  Result := FDataLink.DataSource;
end;

procedure TmmDBLookUpRadioGroup.SetDataField(const Value: string);
begin
  FDataLink.FieldName := Value;
end;

procedure TmmDBLookUpRadioGroup.SetDataSource(const Value: TDataSource);
begin
  FDataLink.DataSource := Value;
end;

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

Решить эту проблему помогло изучение исходных кодов TCustomRadioGroup. Обратите особое внимание на реализацию метода Click, который собственно и выполняет необходимые нам действия.

Данная статья завершает курс о разработке пользовательских компонентов Delphi. Мы надеемся, что предоставленный нами материал оказался для вас полезным. Материал для этого курса взят из нашей книги «Создание компонентов в среде Delphi. Руководство разработчика», которая выйдет примерно в начале осени.


Эта статья опубликована в журнале RSDN Magazine #4-2003. Информацию о журнале можно найти здесь
    Сообщений 1    Оценка 150        Оценить