Обычно проекты, созданные в среде разработке My Visual Database, содержать только один модуль со скриптами – файл script.pas. Однако, если программа содержит много дополнительного функционала, то размер этого файла может быть огромным. Поэтому вполне логично разделить скрипты на логические части и хранить их в отдельных файлах.
Модульная архитектура улучшает восприятие исходного текста, позволяет повторно использовать отдельные модули, копируя нужные файлы в новый проект. Поэтому очередное улучшение “Справочника разработчика” связано с отображением модульной структуры проектов.
Хранимые данные
Поскольку заполнение сведений о модулях будет автоматическим, а модули могут храниться в иерархической системе папок, то структуру хранения данных нужно изменить, добавив несколько полей:

В поле path будет храниться полное имя модуля, поля parentID, nullID и isGroup служат для организации древовидного представления информации о модулях.
Формы
Форма табличного представления является динамической, поэтому для её создания понадобится всего лишь несколько сточек в файле dform.ini
[Unit_Tree]
table=unit
fields=unit.name,unit.status,unit.description,unit.editDate
captions=Название,Статус,Описание,Дата ред.
sort=ParentID,name
parentField=parentID
masterField=id_project
isDetail=True
parentControl=frmMain.tshUnit
filter=id=-1
Code language: SQL (Structured Query Language) (sql)
Форма редактирования традиционная:

Так как в “Справочнике разработчика” реализована система локализации, необходимо добавить в файлы “English.txt” и “Русский.txt” перевод для элементов данной формы редактирования.
Обновление списка модулей
Процедура обновления UpdateProjectList_UpdateUnitList() может вызываться как отдельно (по кнопке на главной форме), так и в цикле выполнения UpdateProjectList_UpdateList(), поэтому отображение прогресса выполнения сделано в ней опционально. Второе важное отличие – автоматическое создание родительских узлов дерева, которые соответствуют физическим папкам для хранения фалов.
procedure UpdateProjectList_UpdateUnitList( AIDProject:integer; AShowProgress:boolean);
// обновление информации по модулям проекта
// AIDProject - ID проекта
// AShowProgress - флаг отображения прогресса
var
tmpScriptDir: string;
tmpFileList: string;
tmpFiles: array of string;
i: integer;
tmpCount: integer;
tmpMaxCount: integer;
tmpUnitName: string;
tmpSQL: string;
tmpDataSet:TDataSet;
tmpFileName: string;
tmpIDParent: string;
tmpName: string; // короткое имя
tmpDirs: array of string;
function FindParent:string;
var
j: integer;
s: string;
tmpPredResult: string;
// поиск родительской папки по максимальному совпадению путей
begin
tmpSQL := 'SELECT id FROM unit WHERE (id_project='+IntToStr(AIDProject)+') and (isGroup = 1) and (path = '+StrToSQL(ExtractFilePath(tmpUnitName))+')';
Result := SQLExecute(tmpSQL);
// если имя составное, а родитель не найден, то создать родителя!
if (Result = '') and (tmpUnitName <> tmpName) then
begin
tmpPredResult := 'NULL';
tmpDirs := SplitString(tmpUnitName,'\');
s:='';
for j:= 0 to Length(tmpDirs)-2 do
begin
if s<>'' then
s := s+'\';
s := s + tmpDirs[j];
// проверяем папки
tmpSQL := 'SELECT id FROM unit WHERE (isGroup = 1) and (id_project = '+IntToStr(AIDProject)+') and (path = '+StrToSQL(s)+') ';
Result := SQLExecute(tmpSQL);
if Result = '' then
begin
tmpSQL := 'INSERT INTO unit (id_project,path,name,status,ParentID,isGroup) VALUES ('+IntToStr(AIDProject)+','+StrToSQL(s)+','+StrToSQL(ExtractFileName(s))+','+'"Новая папка",'+tmpPredResult+',1)';
SQLExecute(tmpSQL);
Result := IntToStr( Last_Insert_ID() );
end;
tmpPredResult := Result;
end;
end
else
if Result = '' then
Result := 'NULL';
end;
begin
if AShowProgress then
Progress(0,0,'Обновление данных о модулях');
try
tmpScriptDir := SQLExecute('SELECT path FROM project WHERE id='+IntToStr(AIDProject))+'\script';
if not DirectoryExists(tmpScriptDir) then
exit;
// проверка имеющихся записей
tmpSQL := 'SELECT count(*) FROM unit WHERE id_project='+IntToStr(AIDProject);
tmpMaxCount := SQLExecute(tmpSQL);
tmpCount := 0;
tmpSQL := 'SELECT * FROM unit WHERE id_project='+IntToStr(AIDProject);
SQLQuery(tmpSQL,tmpDataSet);
try
while not tmpDataSet.EOF do
begin
if AShowProgress then
Progress(tmpCount,tmpMaxCount,'Проверка папок и файлов')
else
Application.ProcessMessages;
tmpFileName := Trim( tmpScriptDir +'\'+ tmpDataSet.FieldByName('path').asString );
if (tmpDataSet.FieldByName('isGroup').asInteger = 0) AND (not FileExists( tmpFileName )) then
begin
tmpSQL := 'UPDATE unit SET status = "Модуль не найден" WHERE id = '+tmpDataSet.FieldByName('id').asString;
SQLExecute(tmpSQL);
end
else
if (tmpDataSet.FieldByName('isGroup').asInteger = 1) AND (not DirectoryExists( tmpFileName )) then
begin
tmpSQL := 'UPDATE unit SET status = "Папка не найдена" WHERE id = '+tmpDataSet.FieldByName('id').asString;
SQLExecute(tmpSQL);
end;
tmpDataSet.Next;
inc(tmpCount);
end;
finally
tmpDataSet.Free;
end;
// добавление новых
tmpFileList := Trim(GetFilesList(tmpScriptDir,'*.pas'));
tmpFiles := SplitString(tmpFileList,chr(10));
tmpMaxCount := Length(tmpFiles);
for i:=0 to Length(tmpFiles)-1 do
begin
if AShowProgress then
Progress(i,tmpMaxCount,'Анализ модулей ')
else
Application.ProcessMessages;
tmpUnitName := Trim( tmpFiles[i] );
tmpName := ExtractFileName(tmpUnitName);
delete(tmpUnitName,1,Length(tmpScriptDir)+1);
tmpSQL := 'SELECT count(*) FROM unit WHERE (id_project = '+IntToStr(AIDProject)+') AND (path = '+StrToSQL(tmpUnitName)+')';
if SQLExecute(tmpSQL) = 0 then // модуль не найден
begin
//
tmpIDParent := FindParent;
tmpSQL := 'INSERT INTO unit (id_project,path,name,status,ParentID,isGroup) VALUES ('+IntToStr(AIDProject)+','+StrToSQL(tmpUnitName)+','+StrToSQL(tmpName)+','+'"Новый модуль",'+tmpIDParent+',0)';
SQLExecute(tmpSQL);
end;
end;
finally
if AShowProgress then
Progress();
end;
end;
Code language: Delphi (delphi)
Процедура работает в две фазы:
- Проверка имеющихся записей
- Добавление новых
Отсутствие папки со скриптами не является ошибкой, так как могут быть проекты, отлично справляющиеся со своими задачами без единой строчки кода.

Форматирование исходных кодов
В модуль formatter.pas добавлена процедура Formatter_FormatDir(), которая умеет форматировать папки с исходными файлами.
const
FORMATTER_PARAM_DIR = '-silent -config:%0.s -d %1.s -r '; //
procedure Formatter_FormatDir( APath:string );
// форматирование исходного текста Delphi внешней утилитой - все файлы в папке, рекурсивно
var
tmpConfigFile: string;
tmpFormatter: string;
tmpParam: string;
begin
// утилита и файл конфигурации лежат в специальной папке внутри нашего проекта
tmpFormatter := ExtractFilePath( Application.ExeName ) + FORMATTER_EXE;
tmpConfigFile := ExtractFilePath( Application.ExeName ) + FORMATTER_CONFIG;
// если чего-то не нашли, то возбуждаем исключение
if not FileExists(tmpFormatter) then
RaiseException('Утилита форматирования не найдена: '+tmpFormatter)
else
if (FORMATTER_CONFIG <> '') and not FileExists(tmpConfigFile) then
RaiseException('Файл конфигурации не найден: '+tmpConfigFile)
else
begin
// делаем конвертацию
tmpParam := Format(FORMATTER_PARAM_DIR,[tmpConfigFile,APath]);
OpenFile(tmpParam,tmpFormatter);
end;
end;
Code language: Delphi (delphi)
Внимание! В файле настройки правил для утилиты formatter.exe необходимо изменить следующий параметр форматирования:
<Option Name="DelphiFormatter.CompilerDirectivesCapitalization" Type="Borland.Together.OpenAPI3.Config.CAPITALIZATION" Category="DelphiCapitalization" Value="AsIs" Levels="Formatter" />
Code language: HTML, XML (xml)
Просмотр файла
Если выделить модуль, а затем в нижней части экрана выбрать вкладку “Код”, то в ней отобразится исходный код модуля с подсветкой синтаксиса.

Другие доработки
Форма dtfUnit_Tree создала прецедент, в котором форма одновременно является зависимой и отображает древовидные данные, поэтому семантику описания динамических форм пришлось доработать, разделив понятия
- parentField – поле для хранения ссылки на родительский узел дерева
- masterField – поле для хранения связи с главной таблицей
Доработки также затронули следующие процедуры и функции: DTF_Create(), frmUpdateProjectList_btnUpdate_OnClick(), UserApp_GetMasterID(), UserApp_UpdateDD().