Применение фреймов для написания правильных приложений

Не знаю как вы, а я в свое время часто встречался со следующей ситуацией. При разработке довольно больших проектов количество форм с временем разрасталось. И чем далее, тем хуже. Но даже это не было большой проблемой до тех пор, пока я работал сам – все таки себя можно самодисциплинировать – заставить использовать единообразные наименования форм, методов, переменных. Но после того, как я стал работать в команде, проблема стала во весь рост – рефакторинг зачастую стал сводиться к “переписать все”, так как у каждого программиста свое понимание “правильно написаного кода”.После некоторых раздумий я решил создать некий “движок”, который облегчит написания немаленьких проектов. В основу этого движка я поставил такие принципы:
1. Все без исключения объекты для работы с базами данных должны находится в модулях данных, причем количество объектов в базе данных не должно превышать некий критический предел (для меня – до 50 объектов) – дальше стает сложно ориентироваться;
2. Все операции по работе с данными з БД должны также описываться в модулях данных, в соответствующих событиях или ActionList;
3. В главной форме не должно содержаться кода по работе с режимами, только вызов режима и вызов абстрактных, общих для всех методов, которые будут переопределяться в каждом соответствующем режиме.
4. Пользовательский интерфейс всех режимов должен быть полностью единообразен.
5. Режим должен иметь “право” изменения главного окна.
6. Режим не должен знать о существовании других режимов и и других форм вообще, для режима должно быть доступны только главная форма и модули данных.
7. Модули данных не должны знать о существовании режимов.
8. Режимы должны создаваться динамически, дабы не занимать лишнюю память.

Для реализации поставленной задачи наиболее подходили фреймы. Используя фреймы, удалось добиться создания единообразного интерфейса, т.к. у нас была одна главная форма, на которой просто менялись кадры - “фреймы”. Однако первая реализация была неудачная, так как для работы со специфическими функциями вынуждала делать все фреймы на главной форме и переключаться между ними с помощью свойства Visible.

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

Потому было решено отойти от “тяжелого наследия” :) процедурного программирования и использовать основные принципы ООП.

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

unit UnitFrameBase;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  shellapi, DB, DBGrids, UnitConstTypes_etc;

type
  TFrameBase = class(TFrame)
  private
    {описание фрейма - для вывода в заголовке программы}
    FrameDescription: string;
    {основной источник данных}
    FrameCurrentDataSet: TDataSet;
    {флаг загрузки данных}
    LoadingData: Boolean;
    {флаг выполнения длительной работы - например генерации очень большого отчёта}
    MakeLongTimeWork: Boolean;
  public
    { функция, выполняющаяся сразу после создания фрейма}
    procedure InitialisationFrame(Param1, Param2: Integer); virtual;
    { виртуальная функция фильтра данных }
    procedure Filter(Param1, Param2: Integer); virtual;
    { виртуальная функция показа дополнительной информации}
    procedure ShowAdditionalInformation(); virtual;
    { виртуальная функция скрытия/показа наследуемых контролов}
    procedure ShowContols(); virtual;
    { виртуальная функция открытия наборов данных (подключения к уже открытым}
    procedure OpenNeededTables(Param1, Param2: Integer); virtual;
    {процедуры работы с наборами данных}
    {добавление}
    function AddRecord(): TFunctionResult; virtual;
    {редактирование}
    function EditRecord(): TFunctionResult; virtual;
    {удаление}
    function DeleteRecord(): TFunctionResult; virtual;
    {Сохранить}
    function PostRecord(): TFunctionResult; virtual;
    {Отменить}
    function CancelRecord(): TFunctionResult; virtual;
    {Открыть свойства}
    function GetProperty(): boolean; virtual;
    {функции экспорта в различные форматы}
    function ExportData(Parameter: string): TFunctionResult; virtual;
    {функции импорта из различных форматов}
    function ImportData(Parameter: string): TFunctionResult; virtual;
    {Комментарий: функции записи/чтения заголовка фрейма}
    function GetDesc: string;
    procedure SetDesc(Description: string);
    {Процедуры перехода к следующей, предыдущей, первой, последней записи}
    procedure GotoNextElement; virtual;
    procedure GotoPrevElement; virtual;
    procedure GotoFirstElement; virtual;
    procedure GotoLastElement; virtual;
    {функция, возвращающая главный Grid фрейма}
    function GetMainGrid: TDBGrid;virtual;
   {функции установки состояния загрузки данных}
    procedure SetLoadingData(LD: boolean);
    function GetLoadingData: boolean;
   {функции установки состояния длительного процесса}
    procedure SetMakeLongTimeWork (LTW: boolean);
    function GetMakeLongTimeWork : boolean;
    {функции возвращающие текущий источник данных}
    procedure SetFrameCurrentDataSet(DS: TDataSet);
    function GetFrameCurrentDataSet: TDataSet;
    {виртуальный деструктор}
    destructor Destroy;override;
    {процедуры записи/чтения настроек фрейма в файлы}
    {эти процедуры не вызываются автоматически в конструкторах/деструкторах}
    {вызов этих функций лучше покласть на процедуру вывода фрейма}
    function SaveFrameToFile(FileName: TFileName): TFunctionResult; virtual;
    function LoadFromFileToFrame(FileName: TFileName): TFunctionResult; virtual;
  published
    {описание фрейма – для отображения режима в заголовке}
    property FrameDesc: string read GetDesc write SetDesc;
  end;

implementation

uses UnitFormMain, MyDBGrid;

{$R *.dfm}

procedure TFrameBase.ShowAdditionalInformation();
begin
//---в принципе часто нужно что-то помещать в статусбар
end;


procedure TFrameBase.Filter(Param1, Param2: Integer);
begin
//---фильтрация текущего набора данных
//---два параметра передаются через SendMessage
end;

procedure TFrameBase.ShowContols();
begin
//---чтобы скрывать/отображать нужные в текущем режиме контролы
end;

procedure TFrameBase.OpenNeededTables(Param1, Param2: Integer);
begin
// по умолчанию откроем текущий датасет фрейма
 if Assigned(FrameCurrentDataSet) then
  FrameCurrentDataSet.Active := true;
end;

procedure TFrameBase.InitialisationFrame(Param1, Param2: Integer);
begin
//-------------
  SetLoadingData(true);
//откроем нужные таблицы
  OpenNeededTables(Param1, Param2);
//отфильтруем их
  Filter(Param1, Param2);
//покажем нужные контролы
  ShowContols;
  SetLoadingData(false);
end;

{процедуры работы с наборами данных}

{добавление}
function TFrameBase.AddRecord(): TFunctionResult;
var
 E: Exception;
begin
//попытаемся добавить запись в текущий датасет фрейма
 Result.Successful := false;
 if Assigned(FrameCurrentDataSet) then
  begin
  try
  FrameCurrentDataSet.Append;
  Result.Successful := true;
  except on E:Exception do
    Result.MessageOnError := E.Message;
  end;
  end;
end;

{редактирование}
function TFrameBase.EditRecord(): TFunctionResult;
var
 E: Exception;
begin
//попытаемся изменить запись в текущем датасете фрейма
 Result.Successful := false;
 if Assigned(FrameCurrentDataSet)then
  begin
  try
  FrameCurrentDataSet.Edit;
  Result.Successful := true;
  except on E:Exception do
    Result.MessageOnError := E.Message;
  end;
  end;
end;

{удаление}
function TFrameBase.DeleteRecord(): TFunctionResult;
var
 E: Exception;
begin
//попытаемся удалить запись из текущего датасета фрейма
 Result.Successful := false;
 if Assigned(FrameCurrentDataSet) then
  begin
  try
  FrameCurrentDataSet.Delete;
  Result.Successful := true;
  except on E:Exception do
    Result.MessageOnError := E.Message;
  end;
  end;
end;


{Сохранить}
function TFrameBase.PostRecord(): TFunctionResult;
var
 E: Exception;
begin
//попытаемся послать Post
 Result.Successful := false;
 if Assigned(FrameCurrentDataSet) then
  begin
  try
  FrameCurrentDataSet.Post;
  Result.Successful := true;
  except on E:Exception do
    Result.MessageOnError := E.Message;
  end;
  end;
end;


{Отменить}
function TFrameBase.CancelRecord(): TFunctionResult;
var
 E: Exception;
begin
//попытаемся послать Cancel
 Result.Successful := false;
 if Assigned(FrameCurrentDataSet) then
  begin
  try
  FrameCurrentDataSet.Cancel;
  Result.Successful := true;
  except on E:Exception do
    Result.MessageOnError := E.Message;
  end;
  end;
end;


{Открыть свойства}
function TFrameBase.GetProperty(): boolean;
begin
  Result := false;
   if Assigned(FrameCurrentDataSet) then
     if not FrameCurrentDataSet.IsEmpty then
      Result := true;
end;

{функции экспорта в различные форматы}
function TFrameBase.ExportData(Parameter: string): TFunctionResult;
var
 FResult: TFunctionResult;
begin
  Result.Successful := False;
  Result.MessageOnError := 'Not Save';
//если передан параметр AsIs
//то сохранить текущий грид в xls
  if (Parameter = 'AsIs') then
  begin
    with TSaveDialog.Create(Self) do
    try
      begin
        Filter := 'Файли гіпертексту|*.htm';
        Title := 'Вкажіть назву файлу';
        DefaultExt := 'htm';
        Options := Options + [ofPathMustExist];
        if Execute then
        begin
          FResult := TMyDBGrid(GetMainGrid).SaveToHTML(FileName, false);
          Result := FResult;
          ShellExecute(Self.Handle, 'open', PChar(FileName), nil, nil, SW_SHOW);
        end
      end;
    finally
      Free;
    end;
  end;
end;

function TFrameBase.ImportData(Parameter: string): TFunctionResult;
begin
  Result.Successful := False;
  Result.MessageOnError := 'Not implemented method';
end;

{Комментарий: функции записи/чтения заголовка фрейма}
function TFrameBase.GetDesc: string;
begin
  Result := FrameDescription
end;

procedure TFrameBase.SetDesc(Description: string);
begin
  FrameDescription := Description
end;

procedure TFrameBase.GotoNextElement;
begin
   if Assigned(FrameCurrentDataSet) then
    if not FrameCurrentDataSet.Eof then
     FrameCurrentDataSet.Next;
end;

procedure TFrameBase.GotoPrevElement;
begin
   if Assigned(FrameCurrentDataSet) then
    if not FrameCurrentDataSet.Bof then
     FrameCurrentDataSet.Prior;
end;

procedure TFrameBase.GotoFirstElement;
begin
   if Assigned(FrameCurrentDataSet) then
    if not FrameCurrentDataSet.Bof then
     FrameCurrentDataSet.First;
end;

procedure TFrameBase.GotoLastElement;
begin
   if Assigned(FrameCurrentDataSet) then
    if not FrameCurrentDataSet.Eof then
     FrameCurrentDataSet.Last;
end;


procedure TFrameBase.SetLoadingData(LD: boolean);
begin
  LoadingData := LD;
end;

function TFrameBase.GetLoadingData: boolean;
begin
  Result := LoadingData;
end;

function TFrameBase.GetMainGrid: TDBGrid;
begin
 Result := nil;
end;

procedure TFrameBase.SetMakeLongTimeWork (LTW: boolean);
begin
  MakeLongTimeWork := LTW;
end;

function TFrameBase.GetMakeLongTimeWork : boolean;
begin
  Result := MakeLongTimeWork;
end;

{функции возвращающие текущий источник данных}
procedure TFrameBase.SetFrameCurrentDataSet(DS: TDataSet);
begin
  FrameCurrentDataSet := DS;
end;

function TFrameBase.GetFrameCurrentDataSet: TDataSet;
begin
  Result := FrameCurrentDataSet;
end;


destructor TFrameBase.Destroy;
begin
  if Assigned(FrameCurrentDataSet) then
   FrameCurrentDataSet.Active := false;
  inherited Destroy;
end;

{процедуры записи/чтения настроек фрейма в файлы}
function TFrameBase.SaveFrameToFile(FileName: TFileName): TFunctionResult;
var
  E: Exception;
  ms: TMemoryStream;
  fs: TFileStream;
begin
  try
  fs := TFileStream.Create(FileName, fmCreate or fmOpenWrite);
  ms := TMemoryStream.Create;
  try
    ms.WriteComponent(self);
    ms.Seek(0, soFromBeginning);
    ObjectBinaryToText(ms, fs);
  finally
    ms.Free;
    fs.free;
  end;
  Result.Successful := true;
  Except on E:Exception do
   begin
    Result.Successful := false;
    Result.MessageOnError := E.Message;
   end;
  end;
end;

function TFrameBase.LoadFromFileToFrame(FileName: TFileName): TFunctionResult;
var
 ComponentIdx: integer;
  ms: TMemoryStream;
  fs: TFileStream;
begin
//уничтожим все существующие на фрейме компоненты
//чтобы не было конфликтов
 for ComponentIdx := self.ComponentCount-1 downto 0 do
  self.Components[ComponentIdx].Free;
 try
//загрузим фрейм из файла
  ms := TMemoryStream.Create;
  fs := TFileStream.Create(FileName, fmOpenRead);
  try
    ObjectTextToBinary(fs, ms);
    ms.Seek(0, soFromBeginning);
    ms.ReadComponent(self);
  finally
    ms.Free;
    fs.free;
  end;
  Result.Successful := true;
 except on E:Exception do
  begin
   Result.Successful := false;
   Result.MessageOnError := E.Message;
  end;
 end;
end;
end.
Как можно заметить, многие функции базового фрейма возвращают значение типа TFunctionResult. Эта структура определена в модуле UnitConstTypes_etc, в который в будущем будут добавляться другие типы, константы. Функции возвращают флаг успешного завершения операции, а случае возникновения ошибки – текст сообщения об ошибке.

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

Кроме того, в процедуре экспорта данных вызывается метод SaveToHTML текущего грида фрейма. Этот метод определён в модуле MyDBGrid.

Перейдем к отображению фреймов на главной форме приложения.

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

Сначала создадим свой тип
 
type
  TFrameClass = class of TFrameBase;
  Теперь нужно создать правильно работающую главную форму. В uses добавим использование модуля UnitFrameBase, в раздел public внесем объект MainFrame класса TFrameBase. Теперь нужно написать функцию, которая будет корректно отображать нужный фрейм при открытии нужного режима.

function TFormMain.ProcShowFrame(FrameClassName: AnsiString; ParentPanel: TWinControl): TFunctionResult;
var
  FrameClass: TClass;
  FunctionResult: TFunctionResult;
  E: Exception;
begin
  Result.Successful := False;
  FrameClass := GetClass(FrameClassName);
  if FrameClass = nil then //если такой тип фрейма незарегистрирован
   begin
    Result.MessageOnError := Format('Class %s not registered',[FrameClassName]);
    Exit;
   end;
  //запретить прорисовку контейнера фреймов
  try
    begin
      LockWindowUpdate(ParentPanel.Handle); // не будем перерисовывать подложку, чтобы не было мерцаний
  //если фрейм не пуст, очистим его
      if Assigned(MainFrame) then
        if MainFrame.ClassType = FrameClass then
        begin
          Result.Successful := true;
          Exit; //если мы пытамся пересоздать текущий фрейм ним же, то выход
        end
        else
          begin
           FunctionResult := MainFrame.SaveFrameToFile(Format('%s.dat',[MainFrame.ClassName]));
           if not FunctionResult.Successful then
              ListBoxLog.Items.Add('Error on Save Frame: '+FunctionResult.MessageOnError);
           MainFrame.Destroy;
          end;
  //создать фрейм по указанному типу
      try
        MainFrame := TFrameClass(FrameClass).Create(FormMain);
        if FileExists(Format('%s.dat',[MainFrame.ClassName])) then
         begin
        FunctionResult := MainFrame.LoadFromFileToFrame(Format('%s.dat',[MainFrame.ClassName]));
        if not FunctionResult.Successful then
         ListBoxLog.Items.Add('Error on Load Frame: '+FunctionResult.MessageOnError);
       end;
      except on E:Exception do
        begin
          Result.MessageOnError := E.Message;
          MainFrame := nil;
          Exit;
        end;
      end;
      MainFrame.Parent := ParentPanel;
      MainFrame.Align := alClient;
    end;
  finally
    LockWindowUpdate(0); //разрешить прорисовку контейнера фреймов
  end;
  Result.Successful := true;
end;
Как же вызвать данную функцию? Создадим дополнительно сообщение

const FILTER_EVENT = WM_USER + 101, – для вызова процедур фильтрации. Param1 и Param2 используются для формирования нужных запросов в однотипных фреймах. Теперь нужно написать обработчик сообщения FILTER_EVENT.
procedure TFormMain.CX_FILTER(var Msg: TMessage);
begin
  if Assigned(MainFrame) and (not FormMain.isShutdown) then
  begin
    MainFrame.Filter(Msg.wParam, Msg.LParam);
    MainFrame.ShowAdditionalInformation;
  end;
end;
Перейдем к отображению меню фреймов. В качестве источника хранения структуры древовидного меню можно использовать xml файл (это удобно, если приложение не использует БД), таблицу в используемой в приложении БД или же хранить структуру прямо в приложении (очень удобно это делать в dxTreeList от DevExpress, однако эти компоненты платные). Так как во всех моих приложениях используются базы данных (в основном СУБД Oracle или Firebird), то я выбрал второй вариант. Создадим таблицу следующей структуры
CREATE TABLE TECH_APP_MENU (
    MENU_ID       IDENTIFIER NOT NULL,
    ITEM_TYPE     VARCHAR(10) DEFAULT 'item' NOT NULL,
    ITEM_CAPTION  VARCHAR(32) NOT NULL,
    FRAME_NAME    VARCHAR(32) DEFAULT 'TFrameUnknown' NOT NULL,
    PARAM1        NONIDENTIFIER,
    PARAM2        NONIDENTIFIER,
    PARENT_ID     NONIDENTIFIER NOT NULL,
    ITEM_ICON     NONIDENTIFIER
);
Кроме того, для обеспечения целостности дерева, с таблицей связано несколько триггеров и ограничений. Полностью структуру таблицы и тексты ограничений вы можете помотреть в исходном тексте БД.

Из этой реляционной таблицы довольно легко создать дерево с помощью рекурсивной процедуры, текст которой вы также можете посмотреть в исходном тексте БД. Заметим, что принципы создания такой процедуры взяты из книги “Мир InterBase”.

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

Процедура отображения дерева приведена ниже. Сначала определим структуру элементов меню
type TMenuNodes = record
    MENU_ID: integer;
    ITEM_TYPE: WideString;
    ITEM_CAPTION:  WideString;
    FRAME_NAME:    WideString;
    PARAM1: integer;
    PARAM2: integer;
    PARENT_ID: integer;
    LEVEL: integer;
    ISLEAF: boolean;
    ParentNode: TTreeNode;
   end;
PTMenuNodes = ^TMenuNodes;
Естественно, что структура элемента меню совпадает со структурой таблицы в БД. Создадим меню следующей функцией
procedure TFormMain.GenerateMenu(Tree: TTreeView);
var
  vData : PTMenuNodes;
  Node, LastNode : TTreeNode;
begin
 try
 LockWindowUpdate(Tree.Handle);
 SendMessage(Tree.Handle, TVM_DELETEITEM, 0, Longint(TVI_ROOT)); //очистим дерево
 if not DataModuleMain.DBMain.Connected then
  Exit; 
 with DataModuleMain.GET_MENU do
  begin
   LastNode := nil;
   Active := True;
   First;
   while not Eof do
    begin
     New(vData);
      with vData^ do
       begin
     MENU_ID := FieldByName('MEM_ID').AsInteger;
     ITEM_TYPE := FieldByName('ITEM_TYPE').AsString;
     ITEM_CAPTION := FieldByName('ITEM_CAPTION').AsString;
     FRAME_NAME := FieldByName('FRAME_NAME').AsString;
     PARAM1 := FieldByName('PARAM1').AsInteger;
     PARAM2 := FieldByName('PARAM2').AsInteger;
     PARENT_ID := FieldByName('MEM_PID').AsInteger;
     LEVEL := FieldByName('OUTLEVEL').AsInteger;
     ISLEAF := boolean(FieldByName('IS_LEAF').AsInteger);
      end;
     if vData.LEVEL = 1 then
      begin
     Node := TreeViewMenu.Items.Add(nil,vData^.ITEM_CAPTION);
     vData.ParentNode := nil;
      end
       else if PTMenuNodes(LastNode.Data)^.LEVEL<vData.LEVEL then
      begin
     Node := TreeViewMenu.Items.AddChild(LastNode,vData^.ITEM_CAPTION);
     vData.ParentNode := LastNode;
      end
       else if PTMenuNodes(LastNode.Data)^.LEVEL=vData.LEVEL then
      begin
     Node := TreeViewMenu.Items.AddChild(LastNode.Parent,vData^.ITEM_CAPTION);
     vData.ParentNode := LastNode.Parent;
      end
       else if PTMenuNodes(LastNode.Data)^.LEVEL>vData.LEVEL then
      begin
     while PTMenuNodes(LastNode.Data)^.LEVEL>=vData.LEVEL do
      LastNode := LastNode.Parent;
     Node := TreeViewMenu.Items.AddChild(LastNode,vData^.ITEM_CAPTION);
     vData.ParentNode := LastNode.Parent;
      end;
{здесь компилятор выдает сообщение, что Node может быть неинициализированной.
 Однако при используемой в программе схеме хранения данных
 данная переменная обязательно будет инициализирована}
     Node.Data := vData;
     Node.ImageIndex := FieldByName('ITEM_ICON').AsInteger;
     Node.SelectedIndex := FieldByName('ITEM_ICON').AsInteger;
     LastNode := Node;
     Next;
    end;
  end;
  finally
   LockWindowUpdate(0);
  end;
end;
Обработчик изменения элемента в меню будет иметь следующий вид
procedure TFormMain.TreeViewMenuChange(Sender: TObject; Node: TTreeNode);
var
  vData : PTMenuNodes;
  CurrentNodeIcon: TIcon;
  FunctionResult: TFunctionResult;
begin
 vData := Node.Data;
 FunctionResult := ProcShowFrame(vData^.FRAME_NAME,PanelFrame);
 if FunctionResult.Successful then
   begin
  MainFrame.InitialisationFrame(vData^.PARAM1,vData^.PARAM2);
  Caption := Format('%s - %s',[Application.Title,MainFrame.FrameDesc]);
  CurrentNodeIcon := TIcon.Create;
  ImageListApp.GetIcon(Node.SelectedIndex,CurrentNodeIcon);
  FormMain.Icon := CurrentNodeIcon;
  CurrentNodeIcon.Free;
   end
   else
    ListBoxLog.Items.Add(Format('Error on show frame %s: %s',[vData^.FRAME_NAME,FunctionResult.MessageOnError]));
end;
Создадим панель инструментов с кнопками, которые будут выполнять нужные функции. Как мы определили, обязательно нужны функции добавления, изменения, удаления, просмотра, экспорта в Excel и других отчетов, импорта из внешних источников. Код обработчиков нажатия на эти кнопки будет максимально прост. Например код для добавления
procedure TFormMain.ToolButtonAddClick(Sender: TObject);
var
 FResult: TFunctionResult;
begin
 if Assigned(MainFrame) then
  begin
  FResult := MainFrame.AddRecord;
  if not FResult.Successful then
   ListBoxLog.Items.Add('Error on Add: '+FResult.MessageOnError);
  end;
end;
Аналогично и для других. Кроме того, в главной форме нужно еще создать общие элементы для фильтрации. Это могут быть элементы для фильтрации по датам, по рассчетным счетам и т.д. Однако в обработчике изменения по этим элементам нужно всего-лишь послать сообщение CX_FILTER. Как обработать полученное сообщение, будет решать конкретный фрейм.

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

Теперь нужно создать фреймы. Фрейм должен наследоваться от созданного выше абстрактного фрейма TFrameBase. После этого нужно переопределить необходимые режимы.

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

Таким образом для добавления нового режима в простейшем случае нужно прописать всего 9 строчек кода!

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

Что дает такой подход? Написав каркас приложения в самом начале разработки проекта, далее все изменения (при появлении новых требований) сводится к включению нового фрейма (унаследованного от базового фрейма), добавлении записи в таблице меню (можно, кстати, создать визуальный редактор этой таблицы) и регистрации типа в в процедуре RegisterClasses. И все! Таким образом режимы могут разрабатываться разными разработчиками, которым не нужно согласовывать свои стили программирования (хотя, все же, это желательно) – все нужные функции определены, нужно их только переопределить и наполнить необходимым содержанием.

Вот и всё, Удачи!

    No results found.
Отменить.