Автор Николай Озниев,
дата публикации: 21 марта 2004г.
дата обновления: 12 июня 2004г.
Приложение со свойствами платформы. Редактор баз данных


Универсальность редактора

Редактор баз данных, как уже было замечено, должен обладать рядом специфических свойств, вытекающих из специфики самой платформы – в непредсказуемости действий пользователя. Поэтому в нем появляется ряд ограничений, не свойственных «жестким» программам. Например, операции ввода, редактирования и удаления данных становятся унифицированными, ибо заранее неизвестно, к какому источнику данных пожелает подключиться пользователь, т.е. редактор должен быть в значительной степени универсальным. Он не должен зависеть от того, как пользователь выстроит систему данных, и должен быть пригоден для любой структуры таблицы и запроса. В данном случае он построен на базе TStringGrid (рис.1.). Так как TSringGrid не пригоден для непосредственного отображения текстов большого объема, графики и форматированных дат, использованы дополнительные компоненты, приведенные в таблице.

Компонент Назначение
TMemo Редактирование текстовых данных
TСomboBox Выбор данных из списков (для полей списочных типов)
TPanel c TImage Работа с полями графического формата
TDateTimePicker Работа с полями типов TDateTime, TDate и TTime

Для остальных типов данных используются непосредственно ячейки TSringGrid. Принцип редактирования состоит в следующем. Выбранная запись из таблицы или запроса переносится в объект TStrinGrid, в котором в зависимости от типов данных при необходимости вставляются приведенные выше компоненты. Затем пользователь редактирует информацию в интересующих его полях, т.е. в указанных компонентах или в ячейках TStringGrid, и в заключение производится обновление записи в таблице непосредственно в БД. Структуры для таблиц и полей при этом находят непосредственное применение. В частности, заголовки редактируемых колонок берутся из этих структур.


Рис. 1. Редактор БД

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

Для того, чтобы можно было рассказать о работе редактора баз данных, нужно располагать возможностью наблюдать данные, т.е. надо сначала создать форму для отображения данных из таблицы или запроса. Поэтому мы в данной статье сначала опишем пример приложения, в котором можно выбрать источник данных, т.е. любую таблицу или XSQL-запрос, подключить этот источник данных к объекту TDBGrid, а затем производить операции вставки данных, редактирования данных и их удаления. И уже после этого опишем работу собственно редактора баз данных.

Описание приложения

Итак, расширим нашу простую платформу, для чего введем главную форму (рис. 2), на которой расположим кнопки для создания объекта TDbInterface, а также запуска уже знакомых читателям менеджера таблиц и менеджера XSQL-запросов. Кроме того, на этой же форме расположим кнопку входа в форму для отображения данных. Последняя должна содержать некий стандартный набор компонент, позволяющий подключать источник данных (таблицу или XSQL-запрос) и работать с данными.


Рис. 2. Главная форма учебного приложения

Обратимся к рис. 2. Кнопки Менеджер таблиц и Менеджер XSQL приведут нас к известным формам из прошлых статей и мы их пока трогать не будем. А вот кнопка Работа с БД откроет форму, показанную на рис. 3. Эта форма является упрощенной версией стандартной универсальной формы платформы и обладает некоторыми возможностями, заимстовованными из нее.


Рис.3. Упрощенная универсальная форма

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

Подключение (переключение) источников данных TDBGrid

Для этого нужно кликнуть левой кнопкой мышки по заголовку TDBEdit, удерживая клавишу Ctrl. При этом в модуле формы сработает строка кода

FpTFbDataStr := Select_pTFbDataStr(FDbInterface, FpTFbDataStr,
  'Выбрать структуру для подключения-переключения', False);

обработчика MouseDown объекта TDBGrid. Функция Select_pTFbDataStr вызовет диалог GetTbFr, позволяющий выбрать структуру таблицы или XSQL-запроса для подключения к TDBGrid.

Выбор колонок для отображения

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

  // Оформление TColumns для ADbGrid при заданной структуре pTFbDataStr
  Function Set_DbGridColumnTitlesEx(ADbInterface : TDbInterface;
    ADbGrid : TDbGrid; ApTFbDataStr : pTFbDataStr; AShowSelFLFr : Boolean;
      Var AFldList : TStrings) : Bool;

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

Ввод новой записи, редактирование и удаление записей

Эти действия выполняются соответственно кнопками,снабженными подсказками Ввод новой записи, Редактирование текущей записи и Удалить выделенные записи, идущими вслед за кнопкой F. При этом кнопка Удалить выделенные записи удаляет из базы данных набор записей, выделенных в TDBGrid. В зависимости от типа подключенного источника данных поведение системы будет отличаться, например, система не позволит вводить новую запись в XSQL-запрос, если он сформирован на базе нескольких таблиц.

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

Открытие БД

Самая левая кнопка выполняет эту задачу. При этом происходит переоткрытие запроса, подключенного к TDBGrid, если при выборе источника данных в диалоге GetTbFr выбрана компонента sQuery для структур pTTableInfo или pTXSQLInfo. Если же в диалоге GetTbFr выбрана компонента sTable, то происходит открытие таблицы БД. Подробнее о типах данных pTTableInfo и pTXSQLInfo можно прочитать в предыдущих статьях данного цикла.

Приведенная форма (рис. 3) упрощена достаточно серьезно. Так, например, в ней нет механизмов сохранения настроек, т.е. при каждом запуске приложения заново нужно проводить подключение нужного источника данных. Нет возможности сортировать записи, вести поиск, распечатывать отчеты и использовать ряд других функций, которые изъяты для предельного упрощения изложения. Рассказ об универсальной форме платформы, являющейся базой для создания большого набора пользовательских экранных форм, - отдельная тема. Чтобы подробно ознакомиться с основными принципами работы редактора баз данных пока нам достаточно иметь приведенные выше возможности. Итак, давайте поближе познакомимся с этим редактором.

Подготовительные действия перед вызовом редактора

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

Редактор работает только с одной выделенной записью при редактировании и позволяет вводить в базу данных также только одну запись. При вставке в базу данных новой записи вся работа выполняется редактором. При редактировании данных изменения в таблицах баз данных производятся не самим редактором баз данных, а методом объекта TDbInterface

  // Реализация всех найденных редактирований в структуре ApTTableInfo
Function TDbInterface.Update_pTTableInfoTable(
  ApTTableInfo: pTTableInfo): Bool;

Вызов редактора проводится различными обработчиками: кликом по кнопке редактирования или ввода новой записи или двойным кликом по редактируемой текущей записи в TDBGrid, - все зависит от устройства конкретного интерфейса пользователя. Наряду с этим в платформе реализован определенный стандарт по устройству экранной формы для пользовательского интерфейса, который и используется при ее развитии. Этот стандарт предусматривает:

Из приведенного описания следует, что кнопки ввода, редактирования и удаления записей расположены на TPanel, находящемся на одном и том же родителе совместно с TDBGrid. Это обстоятельство является принципиальным и используется при установке обработчиков на органы управления. Сами обработчики вынесены в отдельный модуль Ev.pas. Обработчик для ввода новой записи реализован как процедура

// Обработчик стандартного ToolPanel - ввод в БД новой записи
Procedure TFbEvent.ToolPanelNewSpeedBtnClick(Sender: TObject);
Var
  wSpeedBtn    : TSpeedButton;
  wToolPnl     : TPanel;
  wDbGrid      : TDbGrid;
  wpTFbDataStr : pTFbDataStr;
begin  
  // Запись будет представлена в TDbGrid, распол. на родителе TPanel
  if not(Sender is TSpeedButton) then
    Exit;

  wSpeedBtn := Sender as TSpeedButton;
  if not(wSpeedBtn.Parent is TPanel) then
    Exit;

  // Если задана ссылка apActionDbGrid - то работа с ней, иначе - поиск
  if apGCActionDbGrid <> nil then
    wDbGrid := apGCActionDbGrid
  else
    begin
      wToolPnl := TPanel(wSpeedBtn.Parent);
      wDbGrid  := GetTControlNearDbGrid(wToolPnl);
    end;
    
  if wDbGrid = nil then
    Exit;
  // В зависимости от типа структуры в памяти
  wpTFbDataStr := FDbInterface.Get_pTFbDataStr(wDbGrid, apMsg);
  if wpTFbDataStr = nil then
    Exit;

  try
    case wpTFbDataStr.FbDataStrType of
      TableType : DBGridDataSetInputClick(wDbGrid);

      XSQLType :
        begin
          // Определить число таблиц в XSQL-запросе
          if wpTFbDataStr.FbXSQLStr.sFROM_tables.Count = 1 then
            begin
              DBGridDataSetInputClick(wDbGrid);
            end
          else
            FbKernelWarning(
              'Для хранимых SQL-запросов, объединяющих более одной таблицы' +
              #13' данных, ввод новой записи в БД не предусмотрен.' +
              #13'Если необходимо ввести информацию в таблицы, ' +
              'включенные в SQL-запрос,' +
              #13'проведите эту операцию для каждой' +
              ' из этих таблиц в отдельности.');
          Exit;
        end;

      ISFSQLType : Exit;
      VSQLType   : Exit;
      NoStrType  : Exit;
    end;
  finally
  end;
  wDbGrid.Refresh;
end;

Как следует из этого кода, в конце концов происходит вызов обработчика DBGridDataSetInputClick().

Обработчик для редактирования записи реализован как процедура

// Обработчик стандартного ToolPanel - редактирование текущей записи БД
Procedure TFbEvent.ToolPanelEditSpeedBtnClick(Sender: TObject);
Var
  wSpeedBtn : TSpeedButton;
  wToolPnl  : TPanel;
  wDbGrid   : TDbGrid;
begin
  // Запись представлена текущей строкой TDbGrid, распол. на родителе TPanel
  if not(Sender is TSpeedButton) then
    Exit;

  wSpeedBtn := Sender as TSpeedButton;
  if not(wSpeedBtn.Parent is TPanel) then
    Exit;

  wDbGrid := apGCActionDbGrid;
  if wDbGrid = nil then
    begin
      if not(wSpeedBtn.Parent is TPanel) then
        Exit;
      wToolPnl := TPanel(wSpeedBtn.Parent);
      wDbGrid  := GetTControlNearDbGrid(wToolPnl);
    end;

  if wDbGrid = nil then
    Exit;
  // В зависимости от типа структуры в памяти
  FDbInterface.ppTFbDataStr := FDbInterface.Get_pTFbDataStr(wDbGrid, apMsg);
  if FDbInterface.ppTFbDataStr = nil then
    Exit;

  case FDbInterface.ppTFbDataStr.FbDataStrType of
    TableType :
      begin
        DBGridDataSetEditClick(wDbGrid);
        apGCEditFieldsList := nil;
      end;

    XSQLType :
      begin
        DBGridDataSetEditClick(wDbGrid);
        apGCEditFieldsList := nil;
      end;

    ISFSQLType : Exit;
    VSQLType   : Exit;
    NoStrType  : Exit;
  end;
  wDbGrid.Refresh;
end;

Здесь в конце концов происходит вызов обработчика DBGridDataSetEditClick().

Таким образом, основная подготовительная работа перед обращением к редактору баз данных выполняется в приведенных обработчиках ToolPanelNewSpeedBtnClick и ToolPanelEditSpeedBtnClick.

Отметим также, что если интерфейс пользователя отличается от описанного выше, то имеется возможность непосредственного вызова обработчиков DBGridDataSetInputClick() и DBGridDataSetEditClick(), что и используется в ряде случаев в описываемой платформе.

Работа процедуры DBGridDataSetInputClick

Упрощенно процедура имеет вид:

// Обработчик для ввода новой записи в БД
Procedure TFbEvent.DBGridDataSetInputClick(Sender: TObject);
Var
  wQuery       : TQuery;
  wFldOrderL   : TStrings;
  wDbGrid      : TDBGrid;
  wDataSource  : TDataSource;
  wDataSet     : TDataSet;
  wpTTableInfo : pTTableInfo;
begin
  wDbGrid := Sender as TDbGrid;
  if not DBGrid_IsAccessable(wDbGrid, wDataSource, wDataSet, apMsg) then
    Exit;
  if not wDataSet.Active then
    Exit;
  wQuery := nil;

  // Настройка appTFbDataStr под заданный TDataSource
  if not FDbInterface.Init_pTFbDataStr(wDataSource, apMsg) then
    Exit;

  case FDbInterface.ppTFbDataStr.FbDataStrType of
    TableType  :
      begin
        wpTTableInfo := FDbInterface.ppTFbDataStr.FbTableStr;
        if wpTTableInfo.spTInfoCategory.sTFbDbType = icVirtual then
          apIdentityID := 0
        else
          begin
            if not HasFbId(FDbInterface, wpTTableInfo) then
              Exit;

            // Установка сброшенного ключа
            apIdentityID := 0;
            if wDataSet is TQuery then
              wQuery := wDataSet as TQuery;
          end;
      end;

    XSQLType   :
      begin
        // Установка сброшенного ключа
        apIdentityID := 0;
        wQuery := wDataSet as TQuery;
      end;

    ISFSQLType : ;
    VSQLType   : ;
    NoStrType  : ;
  end;

  // Получим порядок следования полей в TDBGrid
  wFldOrderL := GetFieldOrderList(FDbInterface, wDbGrid);

  if DbEditorExFr = nil then
    DbEditorExFr := TDbEditorExFr.Create(nil);
  try
    // Запуск редактора deEx для ввода новой записи
    if not ShowDbEditorExFr(wDbGrid, wFldOrderL, False) then
      Exit;
  finally
    wFldOrderL.Clear;
    wFldOrderL.Free;
    DbEditorExFr.Free;
    DbEditorExFr := nil;
  end;

  if wQuery = nil then
    Exit;

  // Если это был TQuery - встать на запись
  case FDbInterface.ppTFbDataStr.FbDataStrType of
    TableType  :
      begin
        wQuery.Close;
        wQuery.Open;
        wDataSource.Enabled := False;
        try
          wQuery.First;
          repeat
            if wQuery.FieldByName('id').AsInteger = apIdentityID then
              Break;
            wQuery.Next;
          until wQuery.EOF;
        finally
          wDataSource.Enabled := True;
          apGCDbGrid.Parent.Refresh;
        end;
      end;

    XSQLType   : ;
    ISFSQLType : ;
    VSQLType   : ;
    NoStrType  : ;
  end;
end; 

В этой процедуре производятся следующие действия:

  1. Проверка условий запуска редактора баз данных,
  2. Инициализация буферной структуры FpTFbDataStr в интерфейсе FdbInterface,
  3. Сброс специальной глобальной переменной apIdentityID, служащей для хранения идентификатора активной записи (ключа активной записи), - после ввода новой записи ее идентификатор будет передан в apIdentityID,
  4. Запоминание списка наблюдаемых в TDBGrid полей в специальном списке, который передается в функцию вызова редактора,
  5. Создание экземпляра редактора и его запуск с помощью функции, предназначенной для этого,
  6. Пост-обработка для позиционирования на введенной записи, если запись успешно введена в базу данных.

Работа процедуры DBGridDataSetEditClick

Упрощенно процедура имеет вид:

// Обработчик для редактирования выбранной записи в БД для TDBGrid
Procedure TFbEvent.DBGridDataSetEditClick(Sender: TObject);
Var
  kTb, k  : Integer;
  wDbGrid : TDBGrid;
  wID_Set, s,
  wIDVal_Set   : String;
  wFldOrderL   : TStrings;
  wpTFieldInfo : pTFieldInfo;
  wpTTableInfo : pTTableInfo;
  wpTXSQLInfo  : pTXSQLInfo;

  // Проверка существования заданного поля в TQuery
  Function FieldExistsInQuery(AQuery : TQuery; AFldName : String) : Bool;
  Var
    k : Integer;
  begin
    Result := False;
    for k:=0 to AQuery.Fields.Count-1 do
      begin
        Result := AQuery.Fields[k].FieldName = AFldName;
        if Result then
          Exit;
      end;
  end;

begin
  wDbGrid := Sender as TDbGrid;
  // Инициализация структуры FDbInterface.ppTFbDataStr
  if not FDbInterface.Init_pTFbDataStr(wDbGrid.DataSource, apMsg) then
    Exit;

  // Сброс ключа для возврата к текущей записи 
  wID_Set    := '';
  wIDVal_Set := '';

  case FDbInterface.ppTFbDataStr.FbDataStrType of
    TableType :
      begin
        apIdentityID := 0;
        wID_Set      := '';
        wIDVal_Set   := '';
        wpTTableInfo := FDbInterface.ppTFbDataStr.FbTableStr;
        if wpTTableInfo.spTInfoCategory.sTFbDbType <> icVirtual then
          apIdentityID := wDbGrid.DataSource.DataSet.
            FieldByName('id').AsInteger;
        if apIdentityID = 0 then
          for k:=1 to wpTTableInfo.sFieldsL.Count-1 do
            begin
              wpTFieldInfo := pTFieldInfo(wpTTableInfo.sFieldsL[k]);
              if wpTFieldInfo = nil then
                Continue;

              s := ZrName(wpTFieldInfo);
              if wID_Set = '' then
                wID_Set := s
              else
                wID_Set := wID_Set + ';' + s;

              if wIDVal_Set = '' then
                wIDVal_Set := wpTTableInfo.sQuery.FieldByName(s).AsString
              else
                wIDVal_Set := wIDVal_Set + ',' +
                  wpTTableInfo.sQuery.FieldByName(s).AsString;
            end;
      end;

    XSQLType :
      begin
        wpTXSQLInfo := FDbInterface.ppTFbDataStr.FbXSQLStr;
        // Определение списка ключевых полей и их значений
        wID_Set := wpTXSQLInfo.sFROM_tables[0] + '_id';

        if not FieldExistsInQuery(wpTXSQLInfo.sQuery, wID_Set) then
          Exit;

        wIDVal_Set := wpTXSQLInfo.sQuery.FieldByName(wID_Set).AsString;
        for kTb:=1 to wpTXSQLInfo.sFROM_tables.Count-1 do
          begin
            s := wpTXSQLInfo.sFROM_tables[kTb] + '_id';
            wID_Set    := wID_Set + ';' + s;
            wIDVal_Set := wIDVal_Set + ',' +
              wpTXSQLInfo.sQuery.FieldByName(s).AsString;
          end;
      end;

    ISFSQLType : ;
    VSQLType   : ;
    NoStrType  : ;
  end;

  wFldOrderL := GetFieldOrderList(FDbInterface, wDbGrid);
  if DbEditorExFr = nil then
    DbEditorExFr := TDbEditorExFr.Create(nil);
  try
    // Запуск редактора deEx для редактирования
    if not ShowDbEditorExFr(wDbGrid, wFldOrderL, True) then
      Exit;
  finally
    wFldOrderL.Clear;
    wFldOrderL.Free;
    DbEditorExFr.Free;
    DbEditorExFr := nil;
  end;

  case FDbInterface.ppTFbDataStr.FbDataStrType of
    TableType  :
      begin
        wpTTableInfo := FDbInterface.ppTFbDataStr.FbTableStr;
        if apIdentityID <= 0 then
          Exit;

        // Возврат на редактированную запись в таблице
        if apGCDbGrid.DataSource.DataSet is TTable then
          begin
            apGCCurrentTable.Close;
            apGCCurrentTable.Open;
            if not apGCCurrentTable.FindKey([apIdentityID]) then
              Exit;
          end

        // Если это был TQuery - встать на запись
        else if wpTTableInfo.sQuery <> nil then
          begin
            wpTTableInfo.sQuery.Close;
            wpTTableInfo.sQuery.Open;
            apGCDbGrid.DataSource.Enabled := False;
            try
              wpTTableInfo.sQuery.First;
              repeat
                if wpTTableInfo.sQuery.FieldByName('id').AsInteger =
                apIdentityID then
                  Break;
                wpTTableInfo.sQuery.Next;
              until wpTTableInfo.sQuery.EOF;
            finally
              apGCDbGrid.DataSource.Enabled := True;
              apGCDbGrid.Parent.Refresh;
            end;
          end;
      end;

    XSQLType :
      begin
        RefreshDataSet(FDbInterface.ppTFbDataStr.FbXSQLStr.sQuery, wID_Set);
      end;

    ISFSQLType : ;
    VSQLType   : ;
    NoStrType  : ;
  end;
end;

В этой процедуре производятся следующие действия:

  1. Проверка условий запуска редактора баз данных,
  2. Инициализация буферной структуры FpTFbDataStr в интерфейсе FdbInterface,
  3. Сброс специальной глобальной переменной apIdentityID, служащей для хранения идентификатора активной записи (ключа активной записи),
  4. Вслед за этим установка этого ключа равным идентификатору активной (редактируемой) записи, и, если такого идентификатора не оказалось, то определение списка полей для создания ключа возврата на редактируемую запись,
  5. Запоминание списка наблюдаемых в TDBGrid полей в специальном списке, который передается в функцию вызова редактора,
  6. Создание экземпляра редактора и его запуск с помощью функции, предназначенной для этого,
  7. Пост-обработка для позиционирования на редактируемой записи, если запись успешно отредактирована.

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

Наконец, все прелюдии завершены и надо обратиться к работе редактора непосредственно.

Создание формы редактора

Обработчик OnCreate формы редактора содержит некоторые важные детали, которые приведены в нижеследующем коде (приведенный текст обработчика сокращен), на которые нужно обратить внимание.

Procedure TDbEditorExFr.FormCreate(Sender: TObject);
begin
  //Список значений, выбранных для редактирования
  FSelectedFValues := TStringList.Create;

  //Вывод в редактор только наблюдаемых полей - признак
  FEditOnlyVisibleFields := True;
  //При вставке из справочников связанных записей редактировать
  //только наблюдаемые поля - признак
  FEditOnlyVisibleFieldsList := True;

  //Список ссылок на обрабатываемые структуры полей
  FAccessingFldList := nil;

  //Загрузка меню для выбора режима работы редактора
  Load_EditorModePUMenu;
  //Вариант выбора из справочника по умолчанию
  FTEdModeType := apDbSel;
  StatusLbl.Caption := 'Режим: ' + cTEdModeArray[FTEdModeType][2];
end;

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

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

Следующим параметром является признак FEditOnlyVisibleFieldsList, устанавливающий режим вставки в редактируемую или вновь вводимую запись значений полей из связанных таблиц. Например, если при редактировании записи в амбулаторной карте в ней содержится ссылка на отделение поликлиники, то редактор позволяет автоматизировать вставку ссылки на отделение. Если таких ссылок по различным справочникам будет несколько, то часть из них может попасть в число полей, которые в данный момент пользователь не наблюдает, и может статься, что в эти поля связанные ссылки как раз не следует устанавливать. Если так, то признак FeditOnlyVisibleFieldsList = False. По умолчанию все связи автоматически обновляются согласно связям (о самих связях разговор еще впереди). Смысл остальных строк обработчика FormCreate пока не столь принципиален и пока опустим их описание, хотя разобраться в них читатель сможет без особого труда, пользуясь комментариями и полным кодом проекта.

Активизация формы редактора

Обработчик FormActivate содержит важнейшие детали, показанные в следующем сокращенном коде (полный код приведем в прилагаемом проекте):

Procedure TDbEditorExFr.FormActivate(Sender: TObject);
begin
  // Установка объектов pTFieldDataObj 
  Set_pTFieldDataObj;
  // СОБСТВЕННО ЗАГРУЗКА РЕДАКТОРА
  FbFormActivate(Sender);
  Set_StringGridCellEditOptions(StringGrid, StringGrid.Col, StringGrid.Row);
end;

Процедура Set_pTFieldDataObj формирует объекты связи данных с полем. Подробное описание этих объектов в данной статье не будет изложено. Укажу лишь, что используются они для того, чтобы реализовывать простейшие бизнес-правила, а также устанавливать связи между таблицами. Например, если конкретное поле базы данных предназначено для регистрации даты изменения, то в это поле нужно вводить текущую дату. Для этого создается объект связи данных с полем и в нем устанавливается SQL-выражение, вычисляющее текущую дату. Если такой объект создан и связан с данным полем (через структуру поля), то в момент создания новой записи или при редактировании данных это поле автоматически заполняется в редакторе баз данных текущей датой, освобождая пользователя от необходимости вводить дату вручную. Примерно по такой же схеме реализуется и связь между таблицами, - в этом случае создается объект связи данных с полем, но в нем уже указывается ссылка на поле ведущей таблицы, или любой другой таблицы, из которой нужно выбрать значение для вставки в данное поле. Если связанная таблица открыта, то из ее текущей записи извлекается значение и вставляется в текущее поле новой или редактируемой записи. Все подобные действия редактор баз данных выполняет посредством объектов связи данных с полем.

Следующей важнейшей операцией в обработчике FormCreate является загрузка редактора, осуществляемая процедурой FbFormActivate. Основная задача, решаемая при этом - отображение информации при входе в редактор:

// Отображение информации при входе в редактор 
Procedure TDbEditorExFr.FbFormActivate(Sender: TObject);
begin
  if FDbInterface = nil then
    Exit;

  // Инициализация структур данных для StringGrid
  Do_InitStringGridInfos;

  // Если режим редактирования..
  if FEditMode then
    // ..вставить данные непосредственно в ячейки
    Put_InitialData;

  // ..если необходимо - вставить данные, поступающие через связи
  Put_pTFieldDataObjData;

  // ..вставить необходимые компоненты редактирования данных
  Put_EditControls;
end;

Здесь приведен полный код этой процедуры, из комментариев которого ясно, что он делает.

Инициализация структур данных для StringGrid

Выполняет процедура

Procedure TDbEditorExFr.Do_InitStringGridInfos

В ней производится:

  1. Проверка наличия глобальной структуры FDbInterface.ppTFbDataStr, которая ранее была инициализирована в обработчике DBGridDataSetEditClick или DBGridDataSetInputClick,
  2. Очистка сетки StringGrid и установка необходимых надписей колонок,
  3. Оформление StringGrid, т.е. расчет необходимого числа строк, подгонка размеров колонок под наименования колонок отображаемой записи и т.д.,
  4. Привязка колонок из списка FAccessingFldList к сетке StringGrid, при которой первая колонка заполняется смысловыми наименованиями полей, а ссылки TObject ячеек этой колонки запоминают указатели на соответствующие структуры полей из списка FAccessingFldList. С этого момента редактор получает под свое управление запись из обрабатываемого источника (таблицы или XSQL-запроса). Но редактор еще полностью не оформлен.

Вставка данных в StringGrid

Если установлен режим редактирования (поле FEditMode = True), то в процедуре FbFormActivate производится вставка текущих данных непосредственно в ячейки StringGrid. Работа эта выполняется в процедуре

// ..вставить данные непосредственно в ячейки
Procedure TDbEditorExFr.Put_InitialData

Т.к. на предыдущем шаге уже была проведена привязка структур полей к ячейкам первого столбца StringGrid, то работа данной процедуры очень простая: делается цикл по строкам, на каждом шаге цикла извлекается ссылка на структуру поля из ячейки StringGrid, а затем формируется строковое представление информации, считывая ее из соответствующей колонки текущей (редактируемой) записи базы данных. Думаю, читателям не составит труда понять эту простую схему по исходым кодам прилагаемого проекта. Особенность имеется для полей с типами данных ftBlob, ftGraphic. Данные этих типов в общем случае не могут быть помещены непосредственно в ячейки StringGrid, поэтому в StringGrid записывается специальный маркер – цифра 1. О том, как в этом случае производится отображение данных, речь пойдет чуть позже.

Отметим, что независимо от режима работы редактора (вставка новых записей или редактирование существующей записи) всегда производится выполнение процедуры вставки данных, поступающих через объекты связи данных с полем

// Вставить данные, поступающие через связи
Procedure TDbEditorExFr.Put_pTFieldDataObjData.

Завершение активизации формы

Последней выполняется процедура Put_EditControls, фактической задачей которой является подготовка информации и запуск функции

// Вставка (при необходимости) объекта для редактирования
Function TDbEditorExFr.InsertComponentEx(ARow : Integer) : Bool;
Var
  k : Integer;
  wFieldName   : String;
  wpTFieldInfo : pTFieldInfo;
  wpMTTableInfo: pTTableInfo;
  wpMTFieldInfo: pTFieldInfo;
  wDataSource  : TDataSource;
  wpTFbCommonType : pTFbCommonType;
begin
  Result := False;

  wpTFieldInfo    := pTFieldInfo(StringGrid.Objects[0, ARow]);
  wpTFbCommonType := FDbInterface.Get_pTFbCommonType(wpTFieldInfo);
  if (wpTFieldInfo = nil) or (wpTFbCommonType = nil) then
    Exit;

  // Получить DataSource, FieldName из ApTFieldInfo и ADataSet
  if not Get_DataSource(wpTFieldInfo,
  FDbInterface.ppTFbDataStr.FbTableStr.sQuery, wDataSource, wFieldName) then
    Exit;

  case wpTFbCommonType.FbTypeGroup of
    FldGroup :
      case wpTFbCommonType.FbFld.sType of
        // Тонкая обработка внутри этого блока
        ftTime, ftDate, ftDateTime :
          begin
            // Создать объект для отображения даты и времени
            Create_DTPicker(ARow, wpTFbCommonType);
          end;

        ftMemo :
          begin
            Create_TMemo(ARow, wFieldName, wpTFieldInfo);
          end;

        ftGraphic :
          begin
            FbKernel__('TEditLqFr.InsertComponentEx: ftGraphic');
          end;

        ftBlob :
          begin
            // Создаем TPanel и размещаем на нем TImage
            Create_TPanel_TImage(ARow,
              FDbInterface.ppTFbDataStr.FbTableStr.sQuery, wFieldName);
          end;
        else
          Exit;
      end;

    RefGroup : Exit;

    PicGroup :
      begin
        // Индекс заданного комбинированного 
        // типа в FDbInterface.FbPicTypesList
        k := FDbInterface.FbPicTypesList.IndexOfObject(
          TObject(wpTFbCommonType));
        // Если тип не найден - выход
        if k < 0 then
          Exit;
        // Вставить TComboBox
        Create_TComboBox(ARow, wpTFbCommonType, wFieldName, wpTFieldInfo);
      end;

    LUpGroup :
      begin
        wpMTTableInfo := wpTFieldInfo.sMTTableInfo;
        wpMTFieldInfo := wpTFieldInfo.sMTFieldInfo;
        if (wpMTTableInfo <> nil) and (wpMTFieldInfo <> nil) then
          if wpMTTableInfo.sQuery.Active then
            begin
              StringGrid.Cells[1, ARow] := wpMTTableInfo.sQuery.FieldByName(
                ZrName(wpMTFieldInfo)).AsString;
            end;
      end;

    NoGroup  : Exit;
  end;
  Result := True; 
end;

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

Ввод и редактирование во встроенных компонентах

Данная задача актуальна для полей, в которых ввод (редактирование) информации производится посредством объектов, перечисленных в таблице 1.

Основные требования, которым должен удовлетворять редактор при работе с этими объектами, можно сформулировать так:

  1. При получении фокуса ячейкой, в которой находятся данные, автоматически должен активизироваться соответствующий вставленный компонент, и фокус ввода должен немедленно переключаться на него,
  2. После окончания ввода пользователь может нажать клавишу ENTER, а может и забыть это сделать. В первом случае введенные (измененные) данные должны быть скопированы в соответствующую ячейку TStringGrid. Во втором случае такое же действие должно быть реализовано при уходе фокуса от вставленной компоненты. Исключением из этого правила является работа с графическими объектами, когда данные хранятся непосредственно в TImage,
  3. При всяких изменениях данных, кроме графических, обновленные данные должны поступать в соответствующую ячейку TStringGrid,
  4. Должна быть возможность в удобной форме вводить даты, время и составные данные, состоящие из даты и времени,
  5. Должна быть возможность редактировать большой текст с предоставлением достаточного поля для ввода и наблюдения за текстом. Это требование реализуется путем ввода субредактора для текстовых полей,
  6. Для полей списочного типа в объект TComboBox автоматически должен передаваться весь список значений, который соответсвует данному типу,
  7. Сетка должна обеспечивать растяжение размера клеток в вертикальном направлении, и при этом для TMemo и TPanel(TImage) должно производиться необходимое масштабирование, причем изображение в TImage не должно нарушать свои пропорции. При достижении предельных размеров по обоим координатам изображение далее не должно увеличиваться, даже если пользователь будет растягивать ячейку дальше.

Совокупность указанных требований обеспечивается совместной работой обработчиков встроенных компонент и сетки TStringGrid.

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

    // TDATETIMEPICKER 
    // Передача данных в StringGrid - вариант Change
    Procedure DTPChange(Sender: TObject);
    // Передача данных в StringGrid - вариант KeyPress
    Procedure DTPKeyPress(Sender: TObject; Var Key: Char);
    // Переключатель режимов работы объекта TDateTimePicker
    Procedure DTPKeyUp(Sender: TObject; Var Key: Word; Shift: TShiftState);

    // TCOMBOBOX 
    // Выбор значений из списка
    Procedure ComboBoxChange(Sender: TObject);
    // Передача данных в StringGrid
    Procedure ComboBoxKeyPress(Sender: TObject; Var Key: Char);
    // Выбор значений из списка
    Procedure ComboBoxClick(Sender: TObject);
    // Раскрытие списка
    Procedure ComboBoxEnter(Sender: TObject);
    // Фиксируем факт редактирования
    Procedure ComboBoxExit(Sender: TObject);
    // Управление переключениями между TComboBox и TStringGrid
    Procedure CMDialogKey(Var msg: TCMDialogKey); Message CM_DIALOGKEY;

    // TМЕМО 
    // Передача текста в ячейку StringGrid - вариант KeyPress
    Procedure MemoKeyPress(Sender: TObject; Var Key: Char);
    // Вызов субредактора
    Procedure MemoKeyDown(Sender: TObject; Var Key: Word; Shift: TShiftState);
    // Передача текста в ячейку StringGrid - вариант Change
    Procedure MemoChange(Sender: TObject);
    // Передача текста в Memo
    Procedure MemoEnter(Sender: TObject);
    // Передача текста в StringGrid
    Procedure MemoExit(Sender: TObject);

    // TPANEL, TIMAGE 
    // Установка номера текущей строки StringGrid
    Procedure ImagePanelMouseDown(Sender: TObject;
      Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    // Загрузка графического файла с диска
    Procedure ImagePanelDblClick(Sender: TObject);
    // Найти TImage по значению его свойства Tag
    Function GetImageByTag(ATag: Integer): TImage;

Совместная работа этого набора с совокупностью обработчиков событий для TStringGrid

    // TSTRINGGRID
    // Перерисовка StringGrid с учетом встроенных компонент
    Procedure StringGridDrawCell(Sender: TObject; ACol, ARow: Integer;
      Rect: TRect; State: TGridDrawState);
    // Передача данных во встроенные компоненты
    Procedure StringGridKeyPress(Sender: TObject; Var Key: Char);
    // Передача управления во встроенный компонент
    Procedure StringGridKeyUp(Sender: TObject; Var Key: Word;
      Shift: TShiftState);
    // Настройка StringGrid при выборе конкретной строки
    Procedure StringGridSelectCell(Sender: TObject; ACol, ARow: Integer;
      Var CanSelect: Boolean);
    // Дополнительные операции - очистка строки
    Procedure StringGridSetEditText(Sender: TObject; ACol, ARow: Integer;

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

Дополнительный сервис редактора

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

В данном редакторе реализованы некоторые возможности подобного свойства. Их можно разделить на два класса: стандартный сервис и специальный сервис для данной платформы.

Стандартный сервис

Этот сервис реализован через всплывающее меню, продублированное кнопками на ToolBar. Он обеспечивает операции с данными в текущей ячейке StringGrid: копирование данных из ячейки в буфер обмена, вставку в ячейку данных из буфера обмена, вставку в ячейку данных из файла и очистку ячейки. Все операции выполняются с учетом наличия встроенных компонент. Например, для копирования в буфер обмена используется процедура

// Копировать в буфер
Function Save_TControlDataToClipBoard(AStrGrid : TStringGrid) : Bool;
Var
  wS : String;
  wTControl  : TControl;
  wSelStart,
  wSelLength,
  wCol, wRow : Integer;
  wClipboard : TClipboard;
  wFormat    : Word;
  wData      : THandle;
  wPalette   : HPALETTE;
  wInpEditor : TInplaceEdit;
begin
  Result := False;
  wCol := AStrGrid.Col;
  wRow := AStrGrid.Row;
  wSelStart  := 0;
  wSelLength := 0;

  if (wCol <> 1) or (wRow = 0) then
    Exit;

  // Обработка текста из ячейки [1, wRow]
  wS := '';
  if AStrGrid.Objects[1, wRow] = nil then
    begin
      wS := AStrGrid.Cells[1, wRow];
      wInpEditor := TFb_StringGrid(AStrGrid).InplaceEditor;
      if wInpEditor = nil then
        begin
          wSelStart  := 0;
          wSelLength := Length(wS);
        end
      else
        begin
          wSelStart  := TFb_StringGrid(AStrGrid).InplaceEditor.SelStart;
          wSelLength := TFb_StringGrid(AStrGrid).InplaceEditor.SelLength;
        end;
    end

  // Обработка данных, связанных с прикрепленным TControl
  else if TObject(AStrGrid.Objects[1, wRow]) is TControl then
    begin
      wTControl := TControl(AStrGrid.Objects[1, wRow]);
      if wTControl is TMemo then
        begin
          wSelStart  := TMemo(wTControl).SelStart;
          wSelLength := TMemo(wTControl).SelLength;
          wS := TMemo(wTControl).Text;
        end

      else if wTControl is TComboBox then
        begin
          wSelStart  := TComboBox(wTControl).SelStart;
          wSelLength := TComboBox(wTControl).SelLength;
          wS := TComboBox(wTControl).Text;
        end

      else if wTControl is TDateTimePicker then
        begin
          DateTimeToString(wS, TDateTimePicker(wTControl).Format,
            TDateTimePicker(wTControl).DateTime);
          wSelStart  := 0;
          wSelLength := Length(wS);
        end

      else if wTControl is TImage then
        begin
          try
            TImage(wTControl).
              Picture.SaveToClipboardFormat(wFormat, wData, wPalette);
          except
            FbKernelWarning('Формат графики не поддерживается для буфера обмена');
          end;
          ClipBoard.SetAsHandle(wFormat, wData);
          Exit;
        end
    end;

  wS := Copy(wS, wSelStart + 1, wSelLength);
  if wS = '' then
    Exit;

  wClipboard := TClipboard.Create;
  try
    wClipboard.AsText := wS;
  finally
    wClipboard.Free;
  end;
end;

Из этой процедуры видно, что при отсутствии встроенной компоненты в буфер копируется либо все содержимое ячейки (если StringGrid не переведен в режим редактирования), либо выделенная часть текста, когда StringGrid находится в режиме редактирования. Если же в данной ячейки содержится встроенная компонента, то копируется информация из этих компонент, причем графическое изображение копируется полностью, т.к. редактирование собственно рисунка в редакторе БД не производится. Остальные операции стандартного сервиса реализуются примерно по той же схеме, что можно видеть из прилагаемого проекта.

Для реализации стандартных возможностей нужно кликнуть правой кнопкой мыши в ячейку StringGrid с данными.

Специальный сервис

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

// Выбор значений для вставки из БД
Procedure TDbEditorExFr.StringGridMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);

// Выбор свободного кода для поля строкового типа
Procedure SelectCode(ADbInterface : TDbInterface;
  AStrGr : TStringGrid; ARow : Integer);

// Выбор свободного номера амбулаторной карты по маске
Procedure SelectCardNum(ADbInterface : TDbInterface;
  AStrGr : TStringGrid; ARow : Integer);

// Выбор целого числа для поля числового типа
Procedure SelectInt(ADbInterface : TDbInterface;
  AStrGr : TStringGrid; ARow : Integer; AEdModeType : TEdModeType);

Эти процедуры входят в модуль редактора.

Для реализации специальных возможностей нужно кликнуть правой кнопкой мыши в ячейку StringGrid с наименованием поля (не в ячейку с данными!).

Краткие пояснения:

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

Кроме того редактор позволяет перемещать строки вверх-вниз и очищать все ячейки данных одновременно.

Отмечу, что при изложении статьи все-таки пришлось несколько упростить редактор. Штатный редактор имеет немного больше функций. Читатели, поработавшие с редактором из приложенного проекта, могут это увидеть, сравнив его со скриншотом рис. 4.


Рис. 4. Скриншот штатного редактора платформы

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

(Продолжение следует, уважаемые мастера Delphi)