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

Завершив работу над первой версией программы “ClassExplorer”, я решил немного усовершенствовать её: разнести программный код по отдельным модулям, а также добавить механизм управления стилями, который был описан в статье “Хамелеон”. Но эти казалось бы небольшие изменения привели к появлению улучшенной технологии загрузки изображений для кнопок и создали кучу хлопот по настройке связей между модулями.

Модули

Как вам, наверное, уже известно, My Visual Database позволяет размещать исходные тексты скриптов в нескольких файлах, что очень удобно, если число строк кода превышает пару тысяч. Для того, чтобы подключить модуль (файл с расширением .pas) к основному коду, необходимо указать его в команде uses:

uses
  // уопрядочивать по степени зависимости: сначала те, которые ни на кого не ссылаются, затем зависимые
  'ConstVar.pas', // глобавльные константы и переменные приложения
  'System\Utils.pas', // системные процедуры
  'Tools\Tools.pas', // инструменты
  'VClass\VClass.pas', // виртуальные классы и расширения
  'Forms\Forms.pas', // формы
  'UserApp.pas'; // общие процедуры и функции приложения

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

К сожалению, встроенный редактор MVDB не поддерживает редактирование модулей, но выход легко нашелся в лице универсального редактора Notepad++, который используют многие программисты. Как видно по дереву файлов проекта, их получилось довольно много, а в команде uses присутствует только шесть. Остальные модули подключаются в описанных выше модулях, в них также прописана команда uses для файлов. Например, файл Forms.pas используется исключительно для регистрации модулей, в которых хранятся обработчики.
// перечень подключенных форм
 
uses 
  'Forms\ClassType.pas',
  'Forms\Event.pas',  
  'Forms\Example.pas',    
  'Forms\FuncProc.pas',
  'Forms\FuncProcParam.pas',    
  'Forms\FunctionParam.pas',  
  'Forms\Main.pas',
  'Forms\Method.pas',    
  'Forms\MethodParamList.pas',  
  'Forms\Property.pas',  
  'Forms\SearchResult.pas',
  'Forms\Task.pas',
  'Forms\TypeConst.pas';  
 
begin
end.

К сожалению, доподлинно неизвестно, как именно происходит связывание модулей, так как нет никаких ограничений на перекрёстные ссылки (как в Delphi). Иногда достаточно, чтобы используемый модуль присутствовал хотя бы в одной команде uses, а иногда приходится добавлять ссылки по зависимостям: если в модуле А вызывается процедура из модуля B, то в модуле А нужно прописать: uses “B.pas”;

Ещё один важный момент состоит в том, чтобы в каждом модуле имелся блок основной программы:

begin
end.

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

Картинки на кнопках

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

В статье “Семейный альбом” подробно рассматривается механизм загрузки изображений для кнопок, который активируется при запуске приложения и не требует дополнительного кода, если вы решили добавить ещё несколько кнопок. Достаточно поместить в папку нужные файлы в формате PNG, а механизм заменить изображения, используя соглашение по наименованию кнопок.

Но одна только загрузка не решает проблему, так как цвет картинок на кнопках должен сочетаться с фоном самих кнопок. И это проблема, решить которую элегантным способом не получится: в MVDB нет инструментов редактирования изображений в формате PNG, а изображения в формате BMP выглядят топорно из-за отсутствия полутонов по краю изображения, которое дает управление прозрачностью. Поэтому проблему пришлось решать не качеством, а количеством: если изображения кнопок не подходят, то для такой темы создается отдельная папка с изображениями. У меня получилась такая вот структура папок

Так как некоторые темы похожи между собой, то уникальных наборов изображений меньше, чем тем, что наводит на мысль о возможностях оптимизации данной системы. А пока я слегка модифицировал код системы изображений, связав его с системой стилей:
procedure Images_Init;
// инициализация данных
var
  i: integer;
begin
  // инициализация массивов данных
  SetLength(ImagesDir,IMAGES_FORMAT_COUNT);
  SetLength(ImagesSize,IMAGES_FORMAT_COUNT);
  SetLength(ImagesList,IMAGES_FORMAT_COUNT);
  SetLength(ImagesNames,IMAGES_FORMAT_COUNT);
  ImagesDir[IMAGES_FORMAT_BUTTON] := IMAGES_DIR + 'Buttons\';
  // магия стиля
  Images_AddStyle(ImagesDir[IMAGES_FORMAT_BUTTON]);
  // размер
  ImagesSize[IMAGES_FORMAT_BUTTON] := 24;
  // загрузка данных
  for i := 0 to IMAGES_FORMAT_COUNT-1 do
  begin
    ImagesNames[i] := TStringList.Create;
    ImagesList[i] := TImageList.Create(frmMain);
    Images_Load(i); // загрузить изображения
  end;
  Images_ButtonsAssign( IMAGES_FORMAT_BUTTON );
end;
 
procedure Images_AddStyle(var ADir:string;);
// добавление "стильности" изображениям
var
  tmpDir:string;
begin
  tmpDir := ADir + Style_GetStyle() + '\';
  if DirectoryExists(tmpDir) then
    ADir := tmpDir;
end;

Работает это так: если для выбранного стиля существует вложенная папка с таким же названием, то изображения будут грузиться из неё, если нет – то из основной папки.

Стили

Система стилей была доработана таким образом, чтобы её легко можно было использовать в различных приложениях: добавлена процедура Style_SetStyle( AStyleName: string ); – переключение стиля.

procedure Style_SetStyle( AStyleName: string );
// переключение стиля
var
  tmpCurFile : string;
  tmpNewFile : string;
begin
  currentStyleName := AStyleName;
  IniFile_Write( STYLE_INI_SECTION, STYLE_INI_CUR_STYLE, currentStyleName );
  tmpCurFile := ExtractFilePath(Application.ExeName)+STYLE_FILE_NAME;
  tmpNewFile := ExtractFilePath(Application.ExeName)+STYLE_DIR+currentStyleName+STYLE_FILE_EXT;
  // удаляем текущий стиль
  DeleteFile( tmpCurFile );
  // копировать файл со стилем
  if FileExists(tmpNewFile) then
    CopyFile(tmpNewFile, tmpCurFile);
  // предупреждение
  if MessageBox('Для применения изменений требуется перезапуск программы. Выполнить сейчас?','Смена стиля ',MB_OKCANCEL ) = mrOK then
  begin
    frmMain.Close; // закрытие текущего приложения. На самом деле эта команда только создаёт сообщение, фактически закрытие произойдет после завершения текущей процедуры.
    OpenFile( Application.ExeName ); // запуск приложения
  end;
end;

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

procedure Style_Init;
// инициализация
var
  i: integer;
  s: string;
  tmpFileList: array of string;
begin
  s := GetFilesList ( ExtractFilePath(Application.ExeName)+STYLE_DIR  );
  if s <> '' then
  begin
    tmpFileList := SplitString(s, CR );
    // инициализщация списка стилей
    SetLength( arStyles, 1 );
    arStyles[0] := 'По умолчанию';
    for i:=0 to Length(tmpFileList)-1 do
    begin
      s := Trim(ExtractFileName( tmpFileList[i] ));
      if s<>'' then
      begin
        delete(s,length(s)-3,4);
        SetLength( arStyles, i+2 );
        arStyles[i+1] := s; // записываем название файла без расширения
      end;
    end;
    // текущий стиль - прочитать из настройки
    currentStyleName := IniFile_Read( STYLE_INI_SECTION, STYLE_INI_CUR_STYLE, arStyles[0] );
  end;
end;

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

О программе

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

Часть информации статична, а часть может меняться в процессе работы, поэтому я подключил свой обработчик UserApp_UpdateStatistic() на событие отображения формы, в котором обновляю статистику.
procedure UserApp_InitAboutForm;
// настройка формы "О программе"
var
  tmpLabel:TLabel;
  tmpLabel2:TLabel;
  i: integer;
begin
 
  frmdbCoreAbout.Caption := 'О программе';
 
  tmpLabel:=TLabel( frmdbCoreAbout.FindComponent('LinkLabel1') );
  tmpLabel.Caption := frmMain.Caption; //  'My Visual Database Class Explorer';
  tmpLabel.Font.Size := 14;
  tmpLabel.Font.Style := fsBold;
 
  tmpLabel2 := TLabel.Create( frmdbCoreAbout );
  tmpLabel2.Name := 'labVersion';
  tmpLabel2.Parent := tmpLabel.Parent;
  tmpLabel2.Left := tmpLabel.Left;
  tmpLabel2.Top := tmpLabel.Top + 30;
  tmpLabel2.Caption := 'Версия 1.1';
  tmpLabel2.Font.Size := 11;
 
  tmpLabel := TLabel( frmdbCoreAbout.FindComponent('Label2') );
  tmpLabel.Caption := 'Copyright '+chr($A9)+' 2022 Константин Паньков';
  tmpLabel.Top := tmpLabel.Top + 50;
  tmpLabel.Font.Size := 11;
 
  tmpLabel2 := TLabel.Create( frmdbCoreAbout );
  tmpLabel2.Name := 'labLink';
  tmpLabel2.Parent := tmpLabel.Parent;
  tmpLabel2.Left := tmpLabel.Left;
  tmpLabel2.Top := tmpLabel.Top + 24;
  tmpLabel2.Font := tmpLabel.Font;
 
  Label_LinkCreate( tmpLabel2, 'https://k245.ru');
 
  tmpLabel2 := TLabel.Create( frmdbCoreAbout );
  tmpLabel2.Name := 'labDatabaseDate';
  tmpLabel2.Parent := tmpLabel.Parent;
  tmpLabel2.Left := tmpLabel.Left;
  tmpLabel2.Top :=  tmpLabel.Top + 224;
  tmpLabel2.Font := tmpLabel.Font;
  tmpLabel2.Hint := App_GetDBFileName(True);
  tmpLabel2.ShowHint := True;
 
  for i:=1 to 4 do
  begin
    tmpLabel2 := TLabel.Create( frmdbCoreAbout );
    tmpLabel2.Name := 'labStat_'+IntToStr(i);
    tmpLabel2.Parent := tmpLabel.Parent;
    tmpLabel2.Left := tmpLabel.Left;
    tmpLabel2.Top :=  tmpLabel.Top + 60+ i*20;
    tmpLabel2.Font := tmpLabel.Font;
  end;
 
  TAForm(frmdbCoreAbout).OnShow := 'UserApp_UpdateStatistic';
end;
 
procedure UserApp_UpdateStatistic(Sender: TObject; Action: string);
var
  tmpLabel:TLabel;
begin
  // сведения о количестве записей
  FindC(TAForm(frmdbCoreAbout),'labStat_1',tmpLabel );
  tmpLabel.Caption := 'Задачи: '+IntToStr(SQLExecute('SELECT count(*) FROM task'));
  FindC(TAForm(frmdbCoreAbout),'labStat_2',tmpLabel );
  tmpLabel.Caption := 'Классы/типы: '+IntToStr(SQLExecute('SELECT count(*) FROM classType'));
  FindC(TAForm(frmdbCoreAbout),'labStat_3',tmpLabel );
  tmpLabel.Caption := 'Функции: '+IntToStr(SQLExecute('SELECT count(*) FROM funcProc'));
  FindC(TAForm(frmdbCoreAbout),'labStat_4',tmpLabel );
  tmpLabel.Caption := 'Примеры: '+IntToStr(SQLExecute('SELECT count(*) FROM example'));
  // сведения о файле БД
  FindC(TAForm(frmdbCoreAbout),'labDatabaseDate',tmpLabel );
  tmpLabel.Caption := 'Дата обновления базы: '+DateToStr(GetFileLastWriteTime(tmpLabel.Hint));
end;

Ссылки

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

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