На этом шаге мы рассмотрим создание компонента, реализующего указанную задачу.
Приведенный на предыдущем шаге пример имеет ряд недостатков. Так, если внести изменения в объект, содержащийся в OLE-коптейнере, а затем закрыть приложение, то содержимое контейнера не будет сохранено в таблице. Дело в том, что при редактировании объектов в OLE-контейнере компонент TTable не информируется о происходящих изменениях, поэтому не выставляется флаг, свидетельствующий о том, что данная запись пользователем изменена. Соответственно, на компоненте TDBNavigator недоступны кнопки Post и Cancel.
Выход в данной ситуации заключается в создании на базе TOleContainег компонента, чувствительного к данным (data-aware component). В компонентах такого типа должен быть создан объект TDataFieldLink. Этот объект связывается с источником данных и каким-либо определенным полем из таблицы. Он имеет событие OnDataChange, происходящее всякий раз, когда новые данные считываются из таблицы. В обработчике этого события данные помещаются в OLE-контейнер. Другое событие - OnUpdateData - вызывается для считывания совершенных изменений. Это событие вызывается только в том случае, если либо ранее был вызван метод Edit объекта TDataFieldLink, переводящий текущую запись в состояние редактирования; либо свойство Modified равно True. Последнее говорит о том, что в записи были сделаны изменения.
Поскольку компонент TOleContainег не имеет события OnChange (как, например, компонент TEdit), то метод Modified следует вызывать при активации OLE-контейиера, а также при выполнении метода InsertObjectDialog и при очистке содержимого OLE-контейнера. Соответственно, эти два метода в компоненте TDBOleContainег перекрыты. Кроме того, чувствительные к данным компоненты обязаны откликаться на сообщение CM_GETDATALINK и возвращать источник данных.
Исходный текст компонента TDBO1eContainег приведен ниже:
unit DBOleContainer; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, OleCtnrs, DB, DBCtrls; type TDBOleContainer = class(TOleContainer) private { Private declarations } FDataLink: TFieldDataLink; FAutoDisplay: Boolean; FFocused: Boolean; FObjectLoaded: Boolean; FDummy:integer; FFromActivate:boolean; procedure DataChange(Sender: TObject); function GetDataField: string; function GetDataSource: TDataSource; function GetField: TField; function GetReadOnly: Boolean; procedure SetDataField(const Value: string); procedure SetDataSource(Value: TDataSource); procedure SetReadOnly(Value: Boolean); procedure SetAutoDisplay(Value: Boolean); procedure SetFocused(Value: Boolean); procedure UpdateData(Sender: TObject); procedure CMEnter(var Message: TCMEnter); message CM_ENTER; procedure CMExit(var Message: TCMExit); message CM_EXIT; procedure WMLButtonDblClk(var Message: TWMLButtonDblClk); message WM_LBUTTONDBLCLK; procedure CMGetDataLink(var Message: TMessage); message CM_GETDATALINK; procedure DoDeactivate(Sender:TObject); protected { Protected declarations } procedure Loaded; override; procedure Notification(AComponent: TComponent; Operation: TOperation); override; procedure LoadObject; virtual; public { Public declarations } constructor Create(AOwner: TComponent); override; destructor Destroy; override; property Field: TField read GetField; function InsertObjectDialog:boolean; procedure DestroyObject; published { Published declarations } property DataSource:TDataSource read GetDataSource write SetDataSource; property DataField:string read GetDataField write SetDataField; property ReadOnly: Boolean read GetReadOnly write SetReadOnly default False; property AutoDisplay: Boolean read FAutoDisplay write SetAutoDisplay default True; property AutoActivate:integer read FDummy; end; procedure Register; implementation const Signature:integer=-525465623; constructor TDBOLEContainer.Create(AOwner:TComponent); begin inherited Create(AOwner); inherited AutoActivate:=aaDoubleClick; ControlStyle:=ControlStyle+[csReplicatable]; FAutoDisplay:=True; FDataLink:=TFieldDataLink.Create; FDataLink.Control:=Self; FDataLink.OnDataChange:=DataChange; FDataLink.OnUpdateData:=UpdateData; OnDeactivate:=DoDeactivate; end; destructor TDBOLEContainer.Destroy; begin FDataLink.Free; FDataLink:=nil; inherited Destroy; end; procedure TDBOLEContainer.Loaded; begin inherited Loaded; if (csDesigning in ComponentState) then DataChange(Self); end; procedure TDBOLEContainer.Notification(AComponent: TComponent; Operation: TOperation); begin inherited Notification(AComponent, Operation); if (Operation=opRemove) and (FDataLink<>nil) and (AComponent=DataSource) then DataSource:=nil; end; procedure TDBOLEContainer.DoDeactivate(Sender:TObject); begin if Modified then begin if not FDataLink.Editing then FDataLink.Edit; FDataLink.Modified; end; end; function TDBOLEContainer.GetDataSource:TDataSource; begin Result:=FDataLink.DataSource; end; procedure TDBOLEContainer.SetDataSource(Value: TDataSource); begin FDataLink.DataSource:=Value; if Value<>nil then Value.FreeNotification(Self); end; function TDBOLEContainer.GetDataField:string; begin Result:=FDataLink.FieldName; end; procedure TDBOLEContainer.SetDataField(const Value: string); begin FDataLink.FieldName:=Value; end; function TDBOLEContainer.GetReadOnly:Boolean; begin Result:=FDataLink.ReadOnly; end; procedure TDBOLEContainer.SetReadOnly(Value: Boolean); begin FDataLink.ReadOnly:=Value; if Value then inherited AutoActivate:=aaDoubleClick else inherited AutoActivate:=aaManual; end; function TDBOLEContainer.GetField:TField; begin Result:=FDataLink.Field; end; procedure TDBOLEContainer.LoadObject; var Stream:TMemoryStream; N:integer; begin if not FObjectLoaded and Assigned(FDataLink.Field) and FDataLink.Field.IsBlob then begin inherited DestroyObject; Stream:=nil; try {Creation memory stream and saving content from database} Stream:=TMemoryStream.Create; TBlobField(FDataLink.Field).SaveToStream(Stream); Stream.Seek(0,soFromBeginning); if Stream.Size>4 then begin {if size<4 then bad field - even though signature was not entered} Stream.Read(N,sizeof(N)); if N=Signature then LoadFromStream(Stream); end; if Assigned(Stream) then begin Stream.Free; Stream:=nil; end; FObjectLoaded:=True; except on E:exception do begin if Assigned(Stream) then Stream.Free; MessageDlg(E.Message,mtError,[mbOK],0); end; end; Modified:=False; end; end; procedure TDBOLEContainer.DataChange(Sender: TObject); begin if (FDataLink.Field <>nil) then if FDataLink.Field.IsBlob then begin if FAutoDisplay or (FDataLink.Editing and FObjectLoaded) then begin FObjectLoaded:=False; LoadObject; end else begin FObjectLoaded:=False; end; end; if HandleAllocated then RedrawWindow(Handle, nil, 0, RDW_INVALIDATE or RDW_ERASE or RDW_FRAME); end; procedure TDBOLEContainer.UpdateData(Sender: TObject); var Stream:TMemoryStream; begin {Read OLE data from container} if FDataLink.Field.IsBlob then begin Stream:=nil; try Stream:=TMemoryStream.Create; Stream.Write(Signature,sizeof(Signature)); if Assigned(OleObjectInterface) then SaveToStream(Stream); Stream.Seek(0,soFromBeginning); TBlobField(FDataLink.Field).LoadFromStream(Stream); if Assigned(Stream) then begin Stream.Free; Stream:=nil; end; Modified:=False; except on E:exception do begin if Assigned(Stream) then Stream.Free; MessageDlg(E.Message,mtError,[mbOK],0); end; end; end; end; procedure TDBOLEContainer.SetFocused(Value: Boolean); begin if FFocused <> Value then begin FFocused:=Value; if not Assigned(FDataLink.Field) or not FDataLink.Field.IsBlob then FDataLink.Reset; end; end; procedure TDBOLEContainer.CMEnter(var Message: TCMEnter); begin if FFromActivate then begin inherited; Exit; end; SetFocused(True); inherited; end; procedure TDBOLEContainer.CMExit(var Message: TCMExit); begin if FFromActivate then begin inherited; Exit; end; try FDataLink.UpdateRecord; except SetFocus; raise; end; SetFocused(False); inherited; end; procedure TDBOLEContainer.SetAutoDisplay(Value: Boolean); begin if FAutoDisplay <> Value then begin FAutoDisplay:=Value; if Value then LoadObject; end; end; procedure TDBOLEContainer.WMLButtonDblClk(var Message: TWMLButtonDblClk); begin if Assigned(OleObjectInterface) then try FFromActivate:=True; FDataLink.Edit; inherited; FDataLink.Modified; finally FFromActivate:=False; end else try FFromActivate:=True; FObjectLoaded:=True; FDataLink.Edit; if inherited InsertObjectDialog then FDataLink.Modified; finally FFromActivate:=False; end; end; procedure TDBOLEContainer.CMGetDataLink(var Message: TMessage); begin Message.Result := Integer(FDataLink); end; function TDBOLEContainer.InsertObjectDialog:boolean; begin Result:=False; try FFromActivate:=True; FObjectLoaded:=True; FDataLink.Edit; Result:=inherited InsertObjectDialog; if Result then FDataLink.Modified; finally FFromActivate:=False; end; end; procedure TDBOleContainer.DestroyObject; begin FDataLink.Edit; inherited DestroyObject; FDataLink.Modified; Invalidate; end; procedure Register; begin RegisterComponents('Samples', [TDBOleContainer]); end; end.
Теперь приведем алгоритм его инсталляции. Отметим, что он будет расположен на вкладке Samples: об этом говорит первый параметр процедуры RegisterComponents().
Выполнете команду File | Close All в среде Delphi, после чего приступайте с созданию заготовки компонента. Для этого выполните пункт меню Component | New Component и заполните поля в соответствии с рисунком 1:
Рис.1. Создание компонента на основе существующего VCL-компонента
Здесь заполняются следующие поля:
Нажмите кнопку OK. Появится окно с редактором кода, в котором разместите приведенный выше код. После этого сохраните отредактированный PAS-файл под тем же именем, что и имя компонента. В результате вы получите файл DBOleContainer.pas.
Теперь установим компонент в палитру компонентов. Для этого выполним пункт меню Component | Install Component. В результате окно, которое заполним соответственно с рисунком 2:
Рис.2. Окно добавления компонента в палитру
Перечислим поля, которые нужно заполнить:
По нажатию кнопки OK появляется окно с предупреждением, что пакет будет перестроен:
Рис.3. Окно с предупреждением
Подтвердив изменение пакета, мы через некоторое время получим сообщение о том, что компонент установлен:
Рис.4. Сообщение об установке компонента
Напомним, что он находится на вкладке Samples.
Для тестирования компонента можно создать проект, аналогичный предыдущему, но с компонентом TDBOleContainer. Выполните пункт меню File | New | Application и подтвердите сохранение изменений в пакете.
Рис.5. Приложение на этапе разработки
Обратите внимание, что использование компонента, чувствительного к данным, приводит к тому, что данные видны па этапе разработки, как и в случае стандартных компонентов DataControls (рисунок 5).
Создадим для кнопок соответствующие обработчики событий. Кнопка Insert используется для размещения в поле нового OLE-компонента:
procedure TForm1.Button1Click(Sender: TObject); //Insert begin if DBOleContainer1.InsertObjectDialog then if Assigned(DBOLEContainer1.DataSource) then DBOLEContainer1.DataSource.Edit; end;
Кнопка Destroy очищает поле:
procedure TForm1.Button2Click(Sender: TObject); //Destroy begin DBOLEContainer1.DestroyObject; if Assigned(DBOLEContainer1.DataSource) then DBOLEContainer1.DataSource.Edit; end;
Таким образом, мы получили приложение, позволяющее сохранять OLE-объекты в базах данных и свободное от указанных выше недостатков.
В заключение поясним, как можно удалить установленный на этом шаге компонент.
Выполним пункт меню Component | Install Packages. В появившемся окне выберем пакет, указанный на рисунке 6, и нажмем кнопку Edit:
Рис.6. Выбор пакета для редактирования
Подтвердив открытие пакета, вы увидите окно, приведенное на рисунке 7:
Рис.7. Содержимое пакета
Выбираем компонент, согласно рисунку 7, и нажимаем клавишу Remove для удаления компонента, а затем Compile для перекомпиляции пакета. Через некоторое время появится сообщение о деинсталляции компонента, аналогичное приведенному на рисунке 4. При закрытии окна не забудьте подтвердить сохранение изменений в пакете.
Со следующего шага мы начнем рассматривать создание и использование серверов и контроллеров автоматизации.