Сегодня будут рассмотрены остальные основные компоненты ODAC.
А для начала - интересная новость от IBM:
IBM выпустила бесплатную версию DB2
IBM начала предлагать бесплатную версию СУБД DB2 - DB2 Universal
Database Express-C, которую можно использовать не более чем на двух
двухпроцессорных серверах, имеющих до 4 Гбайт памяти. Продукт доступен
в вариантах для Linux и Windows. Отличием от аналогичных предложений
основных конкурентов IBM является отсутствие ограничений на количество
одновременно подключенных к СУБД пользователей и на размер баз. Однако
в DB2 Express-C отсутствуют некоторые из функций платной DB2 Express,
включая модуль DB2 Warehouse Manager, механизм тиражирования
источников данных формата Informix и адаптеры обмена данными DB2
Connect. Лицензия разрешает использовать Express-C для нужд
предприятия и использовать в составе коммерческих программных
продуктов. Продукт можно загрузить по этому адресу. До IBM бесплатные варианты своих СУБД также выпустили Oracle и Microsoft.
Если после выходных Вам лень работать, то ждем Вас на форуме, где
можно малость отвлечься от работы и поболтать на отвлеченные темы:
Культура программирования или как НАДО (или как не надо) писать программы.
К сожалению, в учебных заведениях, в лучшем случае преподают
только технологию программирования, худшие варианты рассматривать не будем...
А меня интересует другой вопрос - вопрос культуры программирования...
Обсудить
Как часто вы меняете работу ?
Голосование и обсуждение.
К сожалению, наблюдается тенденция, что для повышения зп и своего статуса приходится менять работу.
А для многих это стало способом быстрого увеличения зп и они буквально скачут с места на место.
Как часто меняете работу Вы ? С чем это связано и как по Вашему должно быть?
Обсудить
Собственно хочу узнать какую пользу природа хотела получить и что она получила в итоге, создав человечество на этом земном шаре?
Высказывайте свои предположения!
Обсудить
В начале сегодняшнего выпуска хотелось бы остановится на
объектах-полях. Объекты-поля не являются "собственностью" ODAC, а уже
заложены в механизм доступа к БД TDataSet.
При выполнении запроса датасет автоматически создает в памяти
поля-объекты и именно к ним происходит обращение при помоши
FieldByName/Fields. Однако такие поля-объекты можно создавать самим,
что дает определенные преимущества, например, можно настраивать формат
отображения, заголовок поля, а также обращаться к значению поля как к
обычному компоненту. Также можно создавать лукап-поля и калк-поля.
Рассмотрим все перечисленные возможности.
Создать объекты-поля очень легко - для этого нужно вызвать редактор
Fields Editor (одноименный пункт в контекстном меню датасета или
редактор свойства Fields). В появившемся пустом списке
из контекстного меню нужно выбрать один из
трех вариантов:
- Add fields... - добавить одно или несколько плей (из предложенных
в списке)
- New field... - создать новое поле (тут можно создать калк- или
лукап-поле)
- Add all fields... - добавить все поля.
Внимание! обратите внимание, что если Ваш запрос выбирает скажем 10
полей, а Вы создали 4 объекта-поля, то и обращаться в коде будет
возможно только к этим четерем полям. При попытке обратится к другим
полям будет выдана ошибка, мол поле не найдено.
Чтобы не лить много воды (хоть я и Водолей :) ) рассмотрим создание
полей на конкретном примере. Создаем новый проект, настраиваем
подключение к Ораклу (TOraSession, см 7 выпуск),
в TOraQuery пишем следующий запрос select owner, object_name,
object_type, status from all_objects. Теперь открываем редактор
полей и вызываем команду Add all fields. У вас должно получится
так:
Свойства подробно расписаны в справочной системе и я останавливаться
на них не буду, замечу, что назначение свойств понятно из названия.
Обратите просто внимание на такие свойства, как DisplayFormat,
DisplayLabel, ReadOnly, Required.
Остановлюсь только на событии OnGetText. Это событие позволяет
произвольно изменять визуальное содержимое поля. Например, если в поле
Вы храните пол человека в виде M/F, то в этом событии можно сделать
виртуальную подстановку на Муж/Жен:
procedure TForm1.OraQuery1STATUSGetText(Sender: TField;
var Text:
String;
DisplayText: Boolean);
begin
if Sender.AsString = 'F'
then Text := 'Жен'
else if Sender.AsString = 'M'
then Text := 'Муж'
else Text := 'Гемофродит'; // :)
end;
Если такие поля будут редактируемыми, то нужно также написать
обработчик OnSetText, где выполнить обратные преобразования.
Теперь для доступа к полям можно написать OraQuery1STATUS.Value,
хотя OraQuery1.FieldByName('status').asString никто не отменял :)
Очень часто Вы будите создавать калк- и лукап-поля. Для примера
создадим калк-поле. Для этого выберем команду New field. В появившемся
окне вводим имя поля, тип, и выбираем вид поля - Calculated:
Так как поле вычисляемое на стороне клиента, то Вы сами должны
позаботится о его значении. Для этого у TDataSet есть событие
onCalcFields:
procedure TForm1.OraQuery1CalcFields(DataSet: TDataSet);
begin
OraQuery1cnt.Value := OraQuery1.RecordCount;
end;
Здесь мы новому полю присваиваем кол-во считанных записей с
сервера. Запустите пример попередвигайтесь по гриду вперед/назад и посмотрите, какие значения будут у этого
поля. Если Вы внимательно читали предыдущие выпуски, то Вы без труда
поймете поведение этого поля.
Рассмотрим создание лукап-поля. Лукап-поля используются для
подстановки значения указанного поля из другой таблицы вместо поля
текущего запроса (организация связи основной запрос - справочники).
Давайте в нашем примере добавим новое поле, которое будет показывать
код владельца объекта базы. (Хотя это лучше сделать одним запросом).
Для этого добавим еще один компонент TOraQuery и припишем там запрос
select * from all_users. У OraQuery1 создаем новое поле:
Результат виден моментально.
Теперь вернемся к рассмотрению компонентов ODAC.
OraQuery может также выполнять хранимые процедуры/функции, а также
анонимные блоки PL/SQL. Вот рабочий пример вызова хранимой функции:
function IsUniqueRNN(dm: TDMCommon; clid: Double; RNN: Double; cltype: string): Boolean;
begin
if cltype = '1' then
dm.OraSession.ExecSQL(
'declare' + #13#10 +
' v_RESULT boolean;' + #13#10 +
'begin' + #13#10 +
' v_RESULT := pf.PKG_CLIENTS.ISUNIQUERNN(:CLID$, :RNN$, :CLIENTTYPE$);' + #13#10 +
' :RESULT := sys.DIUTIL.BOOL_TO_INT(v_RESULT);' + #13#10 +
'end;', [clid, rnn, '1'])
else
dm.OraSession.ExecSQL(
'declare' + #13#10 +
' v_RESULT boolean;' + #13#10 +
'begin' + #13#10 +
' v_RESULT := pf.PKG_CLIENTS.ISUNIQUERNN(:CLID$, :RNN$);' + #13#10 +
' :RESULT := sys.DIUTIL.BOOL_TO_INT(v_RESULT);' + #13#10 +
'end;', [clid, rnn]);
Result := dm.OraSession.ParamByName('Result').AsBoolean;
if cltype = '1' then
begin
dm.OraSession.ExecSQL(
'declare' + #13#10 +
' v_RESULT boolean;' + #13#10 +
'begin' + #13#10 +
' v_RESULT := pf.PKG_CLIENTS.ISUNIQUERNN(:CLID$, :RNN$, :CLIENTTYPE$);' + #13#10 +
' :RESULT := sys.DIUTIL.BOOL_TO_INT(v_RESULT);' + #13#10 +
'end;', [clid, rnn, '2']);
Result := dm.OraSession.ParamByName('Result').AsBoolean and Result;
end;
end;
Хотя здесь и используется компонент OraSession вместо OraQuery, но в
этом нет ничего страшного, можно смело использовать OraQuery.
Компонент TSmartQuery
В прошлом выпуске мы научились создавать "живые" запросы (это такие
запросы, которые поддерживают модификацию данных). Для этого нужно
было заполнить соответствующими командами свойства
SQLInsert/SQLUpdate/SQLDelete. Это все хорошо, если бы не было столь
утомительным :) (даже с учетом того, что редактор помогает
сгенерировать эти команды). SmartQuery расширяет возможности OraQuery
и самостоятельно во время генерирует нужные команды модификации.
Достаточно только заполнить свойства SQL, KeySequence, KeyFields - все
остальное сделает компонент. В конце выпуска будет приведен рабочий
пример.
SmartQuery поддерживает очень интересную возможность SmartRefresh (см.
одноименное свойство). Суть этой техники состоит в следующем. Как вы
наверное заметили при многопользовательской работе, если один клиент
внес какие-либо изменения в таблицу, то все другие клиенты для
отображения изменений должны переоткрыть нужные запросы. Это
справедливо для всех СУБД и всех библиотек доступа к СУБД. Но ODAC и
тут на высоте. Благодаря встроенному пакету Оракла dbms_pipe ODAC может
давать сигналы другим клиентам, что были внесены изменения в данные и
их нужно перечитать. Чтобы посмотреть SmartRefresh в действии,
создайте новый проект, "натравите" TSmartQuery на какую-либо табличку
(примерный запрос select t.*, t.rowid from mytable), включите
SmartRefresh и подключите SmartQuery к гриду. Запустите программу
несколько раз (или на разных компах) и внесите изменения на одном
клиенте. Посмотрите, как ведут себя другие клиенты.
Компонент TOraSQL
Этот компонент аналогичен TOraQuery за небольшим исключением. Он
предназначен для выполнения любой команды/процедуры/анонимного блока
PL/SQL, кроме команды select. В результате этого этот компонент
меньше весит и меньше расходует память.
Компонент TOraTable
Предназначен для работы с одной таблицей без написания какого-то бы ни
было sql-кода, просто указываете имя таблицы. Является наследником TSmartQuery.
Компонент TOraStoredProc
Предназначен для выполнения хранимых процедур/функций без написания какого-то бы ни
было sql-кода, просто укажите имя процедуры/функции. Приведенный выше
sql-код в функции IsUniqueRNN я получил именно с помощью этого
компонента.
Компонент TOraScript
Этот компонент предназначен для выполнения последовательности
команд. Вы можете сказать, что последовательность команд можно оформить
в виде PL/SQL кода и выполнить через другие компоненты, но в
PL/SQL коде нельзя указывать команды DDL. Вот тут и приходит на помощь
TOraScript. Каждая команда должна быть отделена от других символом
; или /, причем / должен начинаться с новой
строки и любой блок PL/SQL должен заканчиваться символом /.
Компонент TOraPackage
Предназначен для инкапсуляции работы с пакетами. Хочу заметить, что с
пакетами можно работать с помощью анонимного блока PL/SQL, а
следовательно с помощью компонент TOraQuery, TOraSmartQuery,
TOraSQL.
Компонент TOraLoader
Предназначен для быстрой загрузки данных в таблицу, используя
встроенные в Оракл возможности по заливке данных. На использование
этого компонента накладываются следующие ограничения:
- триггеры не поддерживаются
- проверочные ограничения не поддерживаются
- ограничения справочной целостности не поддерживаются
- кластерные таблицы не поддерживаются
- пользовательские типы данных не поддерживаются
Для использования загрузчика Оракла нужно выполнить следующие шаги:
- указать имя таблицы в свойстве TableName
- настроить поля, в которые будут загружаться данные (свойство
Columns)
- написать обработчик события OnGetColumnData или OnPutData
- вызвать метод Load для начала загрузки данных
Ниже приведены примеры обработчиков OnGetColumnData и OnPutData
procedure TfmMain.GetColumnData(Sender: TObject;
Column: TDAColumn; Row: Integer; var Value: Variant;
var EOF: Boolean);
begin
if Row
Где строка EOF := True; сигнализирует о том, что достигнут конец
данных.
procedure TfmMain.PutData(Sender: TDALoader);
var
Count: Integer;
i: Integer;
begin
Count := StrToInt(edRows.Text);
for i := 1 to Count do begin
Sender.PutColumnData(0, i, 1);
Sender.PutColumnData(1, i, Random(100));
Sender.PutColumnData(2, i, Random*100);
Sender.PutColumnData(3, i, 'abc01234567890123456789');
Sender.PutColumnData(4, i, Date);
end;
end;
Компонент TOraErrorHandler
Этот компонент позволяет транслировать сообщения Oracle в понятные
для пользователя сообщения. Трансляция текста ошибки проходит или в
событии OnError или с помощью специальной таблицы, которую можно
создать и редактировать в design-time с помощью самого компонента
(дважды кликните на компоненте). Например, таблица может содержать
такие данные:
Обработчик OnError может быть примерно таким:
procedure TdmCommon.OraErrorHandler1Error(Sender: TObject; E: Exception;
ErrorCode: Integer;
const ConstraintName:
String;
var Msg:
String);
var
s:
string;
begin
s := '';
case ErrorCode
of
1017: s := 'Неправильно введено имя пользователя/пароль';
12560: s := 'Неправильно введено имя пользователя/пароль или отсутствует связь с сервером';
12154: s := 'Введено неверное имя сервера';
end;
Msg := s;
if Msg = ''
then
begin
s := E.Message;
if (Pos('!* 0)
and (Pos('>*!', s) > 0)
then
Msg := Format('Поле ''%s'' должно содержать значение' , [Copy(s, Pos('!**!', s) - Pos('!*end;
end;
Тут все должно быть понятно, кроме разве конструкции
if (Pos('!* 0)
and (Pos('>*!', s) > 0)
then
Msg := Format('Поле ''%s'' должно содержать значение' , [Copy(s, Pos('!**!', s) - Pos('!*end;
Если у поля установлен флаг Required, то поле обязательно
должно иметь какое-либо значение. Если поле не будет содержать
никакого значения (скажем пользователь не ввел его) то датасет перед
отправкой данных на сервер выполнит проверку на наличие значения, и
если поле не содержит значение, то будет выведено стандартное
сообщение на английском языке (Field ... must have a value), что
обычному пользователю не совсем понятно. Хочу обратить Ваше внимание, что эту
ошибку генерирует не Оракл. А так как TOraErrorHandler обрабатывает
все ошибки, а не только ошибки Оракла, то введя для
отображаемого имени поля (TField.DisplayLabel) специальные маркеры,
можно написать единый централизованный обработчик для таких ошибок,
причем для каждого поля можно сделать свой текст ошибок.
Вот и закончился краткий обзор библиотеки ODAC. В заключении хочу
привести небольшой примерчик использования ODAC. В примере будет
реализован ввод плановых показателей по месяцам в разрезе городов.
Для начала была создана следующуя стуктура таблиц:
-- Create table
create table PERIODS_TAB
(
ID NUMBER not null,
YEAR NUMBER(4),
MONTH NUMBER(2)
);
-- Create/Recreate primary, unique and foreign key constraints
alter table PERIODS_TAB
add constraint PK_PERIODS primary key (ID);
-- Create/Recreate check constraints
alter table PERIODS_TAB
add constraint CHK_PERIODS_MONTH
check (month between 1 and 12);
-- Create/Recreate indexes
create unique index UN_PERIODS on PERIODS_TAB (YEAR, MONTH);
-- Grant/Revoke object privileges
grant select, insert, delete on PERIODS_TAB to PF_USER;
-- Create sequence
create sequence PERIODS_SEQ
minvalue 1
maxvalue 999999999999999999999999999
start with 1
increment by 1
nocache;
create or replace trigger trg_periods_bi
before insert on periods_tab
for each row
declare
-- local variables here
begin
if :new.id is null then
getid('periods', :new.id);
end if;
end trg_periods_bi;
-- Create table
create table PLAN_CITY_TAB
(
ID NUMBER not null,
IDPERIOD NUMBER not null,
IDCITY NUMBER not null,
PLAN_DOG NUMBER default 0,
PLAN_REM NUMBER default 0,
MIN_PLAN_DOG NUMBER default 5,
MIN_PLAN_REM NUMBER default 4
);
-- Create/Recreate primary, unique and foreign key constraints
alter table PLAN_CITY_TAB
add constraint PK_PLAN_CITY primary key (ID);
alter table PLAN_CITY_TAB
add constraint FK_PLAN_CITY_CITY foreign key (IDCITY)
references DIVISIONS_TAB (ID);
alter table PLAN_CITY_TAB
add constraint FK_PLAN_CITY_PERIOD foreign key (IDPERIOD)
references PERIODS_TAB (ID) on delete cascade;
-- Create/Recreate indexes
create index IDX_PLAN_CITY_CITY on PLAN_CITY_TAB (IDCITY)
;
create index IDX_PLAN_CITY_PETIOD on PLAN_CITY_TAB (IDPERIOD);
-- Grant/Revoke object privileges
grant select, update on PLAN_CITY_TAB to PF_USER;
-- Create sequence
create sequence PLAN_CITY_SEQ
minvalue 1
maxvalue 999999999999999999999999999
start with 1
increment by 1
nocache;
create or replace trigger trg_plan_city_bi
before insert on plan_city_tab
for each row
declare
-- local variables here
begin
if :new.id is null then
getid('plan_city', :new.id);
end if;
end trg_plan_city_bi;
create or replace trigger trg_periods_ai
after insert on periods_tab
for each row
declare
-- local variables here
begin
insert into plan_city_tab(idperiod, idcity)
select :new.id, id from divisions_tab where isagp='0';
end trg_periods_ai;
Несколько замечаний по структуре:
1. На таблицу periods_tab не выданы гранты на изменение, так как нет
абсолютно никакого смысла изменять период.
2. На таблицу PLAN_CITY_TAB не были выданы гранты на вставку и
удаление, так как вставка и удаление происходит автоматически -
добавление городов в триггере trg_periods_ai, а удаление -
ограничением справочной целостности FK_PLAN_CITY_PERIOD.
3. В триггерах trg_plan_city_bi и trg_periods_bi сначала проверяется,
установлено ли значение первичного ключа и если нет, то
генерируется.
4. Значение месяца должно быть в интервале 1..12 (проверка
CHK_PERIODS_MONTH)
Таким образом, вся бизнес-логика была реализована на стороне сервера. Клиент
будет реализовывать только интерфейс пользователя (ввод и
редактирование первичных данных).
В качестве интерфейса были взяты два DBGridEh (библиотека
EhLib). В первом гриде будут отображаться плановые периоды, а во
втором - планы по городам за выбранный период (классическая связь
master/detail)
Настройки первого грида:
object gPeriods: TDBGridEh
Left = 2
Top = 15
Width = 245
Height = 384
Align = alClient
DataSource = dsPeriods
Columns =
FieldName = 'YEAR'
Footers =
Title.Caption = 'Год'
end
item
EditButtons =
FieldName = 'MONTH'
Footers =
KeyList.Strings = (
'1'
'2'
'3'
'4'
'5'
'6'
'7'
'8'
'9'
'10'
'11'
'12')
PickList.Strings = (
'Январь'
'Февраль'
'Март'
'Апрель'
'Май'
'Июнь'
'Июль'
'Август'
'Сентябрь'
'Октябрь'
'Ноябрь'
'Декабрь')
Title.Caption = 'Месяц'
Width = 113
end>
end
end
Обратите внимание на второй столбец - в таблицу будет записываться
значение из KeyList, а в гриде отображаться соответствующее значение из
PickList.
Настройки второго грида:
object gCity: TDBGridEh
Left = 2
Top = 15
Width = 427
Height = 384
Align = alClient
AllowedOperations = [alopUpdateEh]
DataSource = dsPlanCity
ShowHint = True
UseMultiTitle = True
Columns =
FieldName = 'CITY'
Footers =
ToolTips = True
Width = 133
end
item
EditButtons =
FieldName = 'PLAN_DOG'
Footers =
end
item
EditButtons =
FieldName = 'PLAN_REM'
Footers =
end
item
EditButtons =
FieldName = 'MIN_PLAN_DOG'
Footers =
end
item
EditButtons =
FieldName = 'MIN_PLAN_REM'
Footers =
end>
end
Обратите внимание, что грид будет поддерживать только операции
редактирования (наши юзеры каким-то образом умудрились добавить
вручную города в таблицу plan_city_tab, хотя на уровне самой таблицы у
них не было прав, как им это удалось - уму непостижимо :)). Название
колонок грид берет у соответствующего датасета (а точнее у его
полей-объектов), в то время как у первого грида заголовки колонок были
заданы явно.
Настала очередь датасетов. С первым гридом ассоциирован датасет:
object qPeriods: TSmartQuery
KeyFields = 'ID'
KeySequence = 'PF.PERIODS_SEQ'
SequenceMode = smInsert
Session = dmCommon.OraSession
SQL.Strings = (
'select t.*, t.rowid from pf.periods_tab t')
FetchAll = True
ReadOnly = True
Options.ReturnParams = True
Left = 32
Top = 113
end
Тут должно быть все понятно (если внимательно читали прошлые
выпуски). Единственно хочу сказать, что сразу перевожу датасет в режим
ReadOnly, чтобы пользователи бесконтрольно не стали вводить периоды.
Второй датасет (ассоциирован со втором гридом):
object qPlanCity: TSmartQuery
KeySequence = 'PF.PLAN_CITY_SEQ'
SequenceMode = smInsert
Session = dmCommon.OraSession
SQL.Strings = (
'select t.*, t.rowid from pf.plan_city_tab t')
MasterFields = 'id'
DetailFields = 'idperiod'
MasterSource = dsPeriods
FetchAll = True
Left = 549
Top = 141
ParamData =
object qPlanCityID: TFloatField
FieldName = 'ID'
Required = True
end
object qPlanCityIDPERIOD: TFloatField
FieldName = 'IDPERIOD'
Required = True
end
object qPlanCityIDCITY: TFloatField
FieldName = 'IDCITY'
Required = True
end
object qPlanCityPLAN_DOG: TFloatField
DisplayLabel = 'План|Договора'
FieldName = 'PLAN_DOG'
end
object qPlanCityPLAN_REM: TFloatField
DisplayLabel = 'План|Заявления'
FieldName = 'PLAN_REM'
end
object qPlanCityMIN_PLAN_DOG: TFloatField
DisplayLabel = 'Минимальный план|Договора'
FieldName = 'MIN_PLAN_DOG'
end
object qPlanCityMIN_PLAN_REM: TFloatField
DisplayLabel = 'Минимальный план|Заявления'
FieldName = 'MIN_PLAN_REM'
end
object qPlanCityROWID: TStringField
FieldName = 'ROWID'
ReadOnly = True
Size = 18
end
object qPlanCityCITY: TStringField
DisplayLabel = 'Город'
FieldKind = fkLookup
FieldName = 'CITY'
LookupDataSet = qAreas
LookupKeyFields = 'ID'
LookupResultField = 'NAME'
KeyFields = 'IDCITY'
Size = 128
Lookup = True
end
end
В этом датасете было создано лукап-поле для отображения названия
города по его коду.
Осталась самая малость - при открытии формы открыть датасеты:
qPeriods.Open;
qPlanCity.Open;
и сделать три кнопочки:
* добавить период
* удалить период
* сохранить изменения
вот их обработчики:
procedure TFrmPensionHandBook.actAddPeriodExecute(Sender: TObject);
begin
qPeriods.ReadOnly := False; //Разрешаем редактировать датасет
qPeriods.Append; // добавляем пустую запись, конкретные значения
// юзер будет вводить непосредственно в гриде
end;
procedure TFrmPensionHandBook.actDelPeriodExecute(Sender: TObject);
begin
if Application.MessageBox('Вы дейсвительно хотите удалить период и все плановые показатели?', 'Подтверждение', MB_YESNO) ID_YES
then Exit;
qPeriods.ReadOnly := False; //Разрешаем редактировать датасет
qPeriods.Delete; // удаляем период, все города за этот период
//удалятся автоматически сервером
qPeriods.ReadOnly := True; //Запрещаем редактировать датасет
end;
procedure TFrmPensionHandBook.actPostPeriodExecute(Sender: TObject);
begin
qPeriods.Post; // Отсылаем изменения на сервер
qPeriods.ReadOnly := True; //Запрещаем редактировать датасет
qPlanCity.Refresh; // Обновляем планы по городам
end;
Ну и еще несколько обработчиков для управления кнопками
// Кнопка "сохранить изменения" доступна когда датасет не в режиме ReadOnly
procedure TFrmPensionHandBook.actPostPeriodUpdate(Sender: TObject);
begin
TAction(Sender).Enabled :=
not qPeriods.ReadOnly;
end;
// Кнопка "добавить период" доступна когда датасет в режиме ReadOnly
procedure TFrmPensionHandBook.actAddPeriodUpdate(Sender: TObject);
begin
TAction(Sender).Enabled := qPeriods.ReadOnly;
end;
// Кнопка "удалить период" доступна когда датасет в режиме ReadOnly и
// есть хотя бы одна запись
procedure TFrmPensionHandBook.actDelPeriodUpdate(Sender: TObject);
begin
TAction(Sender).Enabled := qPeriods.ReadOnly
and not qPeriods.IsEmpty;
end;
Форма в работе:
Вот и подошел к концу краткий обзор компонент ODAC. Надеюсь, что
данный цикл был для Вас полезен и Вы узнали что-то новое. С
библиотекой ODAC идет хорошая справка и множество примеров, которые
показывают все возможности и особенности ODAC, рекомендую их
просмотреть.
Вы не забыли, что мы ждем Вас на форуме Чертенок.ру?