Продолжение статьи “Сложная простота”

Копипаст – это самое распространенное заболевание исходного кода программы. Генезис его таков: кажется, что достаточно скопировать, немного подправив, некий участок кода несколько раз и результат готов! Зачем делать сложно, если можно сделать просто?

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

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

Сложность восприятия

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

Сложность изменения

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

Практика

В предыдущей статье для инициализации приложения был предложен код процедуры InitForm, содержащий копипаст (не буду его копировать – ведь мы решили от него избавиться 🙂 )

Самый простой (и единственно доступный в My Visual Database) способ борьбы с копипастом – создание процедуры или функции, в которую необходимо переместить повторяющиеся команды.

В нашем случае мы напишем функцию создания разделителя – Splitter_Create().

Разделитель (3) обычно используют, чтобы менять размеры двух смежных визуальных компонентов (1,2), которые расположены на родительском компоненте (4). Разделитель бывает горизонтальным (как на рисунке) или вертикальным. При этом один из разделенных компонентов выровнен по краю родительского компонента, а второй – выравнивание (растягивание) на всю доступную область. За выравнивание отвечает свойство Align, которое объявлено у класса TControl и доступно у всех его наследников, в частности – у всех визуальных компонентов пользовательского интерфейса, имеющихся в палитре компонентов MVDB.

AlignОписание
alLeftПрижать к левому краю
alRigthПрижать к правому краю
alTopПрижать к верхнему краю
alBottomПрижать к нижнему краю
alClientРастянуть на всю область
alNoneНичего не делать (значение по умолчанию)
Примечание. К сожалению, в MVDB свойство Align не доступно из скриптов для классов TControl и TWinControl, поэтому придется подставлять в этом месте "костыль".

Функция Splitter_Create() возвращает ссылку на созданный разделитель.

function Splitter_Create( ASplitControl:TControl; AControl: TControl; AAlign:TAlign; AWidth:integer = 7 ): TSplitter;
// создание сплиттера
// ASplitControl - контрол, размер которого будет меняться сплитером
// AControl - соседний контрол, который обычно занимает всё остальное пространство родительского контрола
// AAlign - ширина (высота) сплиттера
begin
  // выравниваем сдвигаемый контрол
  // ASplitControl.Align := alLeft;
  // К сожалению, в MVDN у класса TContol забыли сделать поддержку свойства Align, которое есть в оригинальном классе Delphi,
  // поэтому приходится писать больше кода:
  if ASplitControl is TdbPageControl then TdbPageControl(ASplitControl).Align := AAlign else
  if ASplitControl is TdbPanel then TdbPanel(ASplitControl).Align := AAlign;
  // создаем сплиттер
  Result := TSplitter.Create( AControl.Owner );
  // к имени основного контрола добавляем префикс сплиттера
  Result.Name := T_SPLITTER + ASplitControl.Name;
  Result.Parent := AControl.Parent;
  Result.Left := ASplitControl.Width + 1;
  Result.Width := AWidth;
  Result.Height := AWidth;
  Result.Align := AAlign;
  // выравниваем соседний контрол
  // AControl.Align := alClient;
  // опять неудобство...
  if AControl is TdbPageControl then TdbPageControl(AControl).Align := alClient else
  if AControl is TdbPanel then TdbPanel(AControl).Align := alClient;
  // восстановить позицию сплиттера
  Splitter_LoadPosition(Result);
end;

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

const
  T_SPLITTER = 'spl'; // префикс для сплиттера
 
var
  // храним ссылки на сплиттеры
  Spl_1:TSplitter;
  Spl_2:TSplitter;
  Spl_3:TSplitter;
  Spl_4:TSplitter;
 
procedure InitForm;
var
  tmpSplitter: TSplitter;
begin
  // убрать главное меню
  frmMain.Menu := nil;
  // снижаем мерцаиние формы при перерисовке
  frmMain.DoubleBuffered := True;
  // добавляем сплиттеры и выравнивание
  Spl_1 := Splitter_Create( frmMain.pgcClassView, frmMain.pgcClass, alLeft);   // классы
  Spl_2 := Splitter_Create( frmMain.pgcTypeView, frmMain.pgcType, alLeft); // типы
  Spl_3 := Splitter_Create( frmMain.pgcFunctionView, frmMain.pgcFunction, alLeft); // функции
  frmMain.pgcVariableView.align := alClient;   // переменные
  frmMain.pgcTaskView.align := alClient;   // задачи
  Spl_4 := Splitter_Create( frmMain.panMethod, frmMain.pgcMethodParamView, alTop); // методы
  //
  Form_ShowOnWinControl( frmClassTree, frmMain.tshClassTree );
  Form_ShowOnWinControl( frmClassList, frmMain.tshClassList );
  Form_ShowOnWinControl( frmTypeList, frmMain.tshTypeList);
  Form_ShowOnWinControl( frmFunctionTree, frmMain.tshFunctionTree );
  Form_ShowOnWinControl( frmFunctionList, frmMain.tshFunctionList );
  Form_ShowOnWinControl( frmVariableList, frmMain.tshVariableList );
  Form_ShowOnWinControl( frmTaskTree, frmMain.tshTaskTree );
  Form_ShowOnWinControl( frmTaskList, frmMain.tshTaskList );
  Form_ShowOnWinControl( frmProperty, frmMain.tshProperty );
  Form_ShowOnWinControl( frmMethod, frmMain.panMethod );
  Form_ShowOnWinControl( frmEvent, frmMain.tshEvent );
  Form_ShowOnWinControl( frmMethodParamList, frmMain.tshMethodParamList );
  Form_ShowOnWinControl( frmTypeConst, frmMain.tshTypeConst );
  Form_ShowOnWinControl( frmFunctionParam, frmMain.tshFunctionParam );
  Form_ShowOnWinControl( frmSearchResult, frmMain.tshSearchResult );
  Form_ShowOnWinControl( frmExampleView, frmMain.tshExample );
  //
  Tree_LoadCollapseList( frmFunctionTree.trvMain );
  Tree_LoadCollapseList( frmClassTree.trvMain );
  Tree_LoadCollapseList( frmTaskTree.trvMain );
end;

Переменные нам понадобились для того, чтобы организовать хранение заданного пользователем положения разделителя. Записывать эти данные будем в файл settings.ini, в котором уже хранятся настройки ширины колонок в табличном представлении данных. Для этого создадим ещё пару процедур: Splitter_SavePosition() и Splitter_LoadPosition(). Обратите внимание, что сохраняется не положение разделителя, а размер одного из компонентов, к которому прижат разделитель. Это обусловлено работой механизма выравнивания компонентов: изменяя размеры прижатого к краю компонента, мы автоматически меняем положение разделителя и размеры второго компонента, который растянут на всю область (Align = alClient).

procedure Splitter_SavePosition( ASplitter:TSplitter );
// сохранение положения
var
  tmpPosition: integer;
  tmpControl: TControl;
  tmpForm: TAForm;
begin
  // записывать надо не положение сплиттера, а размер контрола, у которого меняется размер
  CForm(ASplitter,tmpForm);
  // находим главный контрол
  FindC(tmpForm,DeleteClassName(ASplitter.Name),tmpControl);
  // в зависимости от типа выравнивания сплиттера запоминаем его положение по вертикали или горизонтали
  if (ASplitter.Align = alLeft) or (ASplitter.Align = alRight) then
    tmpPosition := tmpControl.Width
  else
    tmpPosition := tmpControl.Height;
  IniFile_Write('Splitters',TAForm(TComponent(ASplitter).Owner).Name+'.' +ASplitter.Name+'.Position', IntToStr(tmpPosition) );
end;
 
procedure Splitter_LoadPosition( ASplitter:TSplitter );
// восстановлдение положения
var
  tmpPosition: string;
  tmpControl: TControl;
  tmpForm: TAForm;
begin
  CForm(ASplitter,tmpForm);
  // находим главный контрол
  FindC(tmpForm,DeleteClassName(ASplitter.Name),tmpControl);
  //
  tmpPosition := IniFile_Read('Splitters',TAForm(TComponent(ASplitter).Owner).Name+'.' +ASplitter.Name+'.Position', '');
  if ValidInt(tmpPosition) then
  begin
    // в зависимости от типа выравнивания сплиттера восстанавливаем его положение по вертикали или горизонтали
    if (ASplitter.Align = alLeft) or (ASplitter.Align = alRight) then
      tmpControl.Width := StrToInt(tmpPosition)
    else
      tmpControl.Height := StrToInt(tmpPosition);
  end;
end;

Тут внимательный читатель заметит, что вместо непосредственной записи или чтения из файла настроек я вызываю некие функции IniFile_Write() и IniFile_Read(). Это связано с тем, что мне пришла в голову идея подобным образом сохранять различные параметры других компонентов, а чтобы избежать копипаста… Правильно! Нужно создать подпрограмму. Или две:

procedure IniFile_Write(ASection:string;AParamName:string;AValue:string);
// запись строкового значения
var
  tmpIniFile: TIniFile;
begin
  tmpIniFile := TIniFile.Create(Application.SettingsFile);
  tmpIniFile.WriteString(ASection, AParamName, AValue);
  tmpIniFile.Free;
end;
 
function IniFile_Read(ASection:string;AParamName:string;AValueDef:string=''):string;
// чтение строкового значения
var
  tmpIniFile: TIniFile;
begin
  tmpIniFile := TIniFile.Create(Application.SettingsFile);
  Result := tmpIniFile.ReadString(ASection, AParamName, AValueDef);
  tmpIniFile.Free;
end;

Но тут возникает правильный вопрос: как предвидеть возникновение копипаста и сразу создавать структурированный программный код? Этот навык дает только регулярная практика. Чем больше кода вы пишите, тем ясней будут видны ситуации, в которых возникает копипаст. Но не переживайте: от копипаста можно избавиться в любой момент, было бы желание. А помогает в этом знание объектной модели.

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

Собственно, ради изучения и систематизации данных по объектной модели My Visual Database создается приложение, описываемое в данной статье. А пока добавлю ещё несколько полезных функций, которые решают Проблему дерева.

Упрямое дерево

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

procedure Tree_SeveCollapseList( ATree:TdbTreeView );
// сохранить в настройки
begin
  IniFile_Write('Trees',TAForm(TComponent(ATree).Owner).Name+'.' +ATree.Name+'.CollapseList',ATree.TagString);
end;
 
procedure Tree_LoadCollapseList( ATree:TdbTreeView );
// загрузить в настройки
begin
  ATree.TagString := IniFile_Read('Trees',TAForm(TComponent(ATree).Owner).Name+'.' +ATree.Name+'.CollapseList', '');
end;
 
procedure Tree_RestoreCollapseList( ATree:TdbTreeView );
// восстановить из памяти
var
  i:integer;
  j: integer;
  Nodes: array of string;
begin
  ATree.BeginUpdate;
  Nodes := SplitString(ATree.TagString,',');
  // используем список ID записей для сворачивания узлов
  for j := 0 to ATree.RowCount-1 do
  begin
    for i := 0 to Length(Nodes) - 1 do
    begin
      if ATree.dbIndexToID(j) = StrToInt( Nodes[i] ) then
      begin
        ATree.Expanded[j] := False;
        break;
      end;
    end;
  end;
  ATree.EndUpdate;
end;
 
procedure Tree_SetCollapseList( ATree:TdbTreeView );
// сохранить в память
var
  i:integer;
begin
  // строим список из ID записей узлов, которые свёрнуты
  ATree.TagString := '';
  for i:= 0 to ATree.RowCount-1 do
  begin
    if not ATree.Expanded[i] then
    begin
      if ATree.TagString <> '' then
        ATree.TagString :=  ATree.TagString + ',';
      ATree.TagString :=  ATree.TagString + IntToStr( ATree.dbIndexToID(i) );
    end;
  end;
end;

Идея контроля за деревом следующая: записывать в свойстве TagString список ID записей для тех узлов, которые были свернуты. Для этого используется процедура Tree_SetCollapseList(), которую можно вызывать, когда дерево теряет фокус (событие OnExit). А в момент, когда дерево обновляет свою структуру (событие OnChange) вызывать процедуру, которая свернет нужные узлы. Запоминание ID записей позволяет алгоритму корректно работать в случае изменения структуры дерева (удаление и добавление узлов). Загрузка структуры (вызов процедуры Tree_LoadCollapseTree() ) осуществляется при запуске программы, в процедуре InitForm(), а сохранение (Tree_SaveCollapseTree()) – при закрытии программы (закрытии главной формы приложения). Там же производится запись данных о положении разделителей.

procedure frmMain_OnClose (Sender: TObject; Action: string);
begin
  Tree_SetCollapseList( frmFunctionTree.trvMain );
  Tree_SeveCollapseList( frmFunctionTree.trvMain );
  //
  Tree_SetCollapseList( frmClassTree.trvMain );
  Tree_SeveCollapseList( frmClassTree.trvMain );
  //
  Tree_SetCollapseList( frmTaskTree.trvMain );
  Tree_SeveCollapseList( frmTaskTree.trvMain );
  //
  Splitter_SavePosition( Spl_1 );
  Splitter_SavePosition( Spl_2 );
  Splitter_SavePosition( Spl_3 );
  Splitter_SavePosition( Spl_4 );
end;

Продолжение: Давайте дружить таблицами

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *