Здравствуйте, уважаемы читатели. Давно я написал здесь. Не как не мог определиться, с темой статьи – идей вроде много, а о чем писать не знаю. Но тут подвернулась идея, я как раз начал искать новую работу. Причин тому несколько:

  • Я оставался там потому что мой проект, мое детище которое поднимал с нуля было не завершено и не отработано. Сейчас оно максимально динамично и настраиваемый.
  • Подходит к концу мой контракт, который я заключал на год, чтоб дописать проект.
  • И в принципе мне больше там не чего делать, а хочется развивать и расти.

Ну, вот начал я искать работу соответственно на hh.ru. Заполнил резюме, отметил несколько вакансий, которые мне понравились, и стал ждать.) Ждать долго не пришлось, потому, как в тот же день по одной вакансии мне прислали тестовое задание.

После прочтения тестового задания, я был в небольшом шоке. Просто на работе у нас задачу ставили по принципу «Я хочу..Делайте..». А тут четко и грамотно прописанное задание. Мне стало интересно, тем более я собирался в отпуск и решил в отпуске по работать над ним.

В этой статье пойдет речь о том как я реализовал данное задание. Я затрону следующие моменты:

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

 

Мозговой штурм…

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

— список доступных документов;
— панель для ввода;
— кнопка «Сохранить».

Список доступных документов. На эту роль я взял ListBox, на мой взгляд, он наиболее подходит для подобных задач. В него при запуске должен загружаться список документов (таблица sp_TypeDocument). После чего при клике, не любом документе из базы берется список необходимы компонентов и формируется форма ввода. Вот тут я на некоторое время тормознул, ведь для создаваемых динамически компонентов, нужно создавать заранее переменные. А представим что водном документе, 10 компонентов TEdit, а в другом 5. Суть в том что я не знаю заранее сколько нужно прописать переменных. И тут меня осенило, ведь можно просто создать несколько динамических массивов каждый для своего компонента. Вот так я создавал отдельные типы для новых переменных:

Uses ……
type
TEditArray=array of TEdit;
type
TGroupBoxArray=array of TGroupBox;
type
TLabelArray=array of TLabel;
type
TDateTimePickerArray=array of TDateTimePicker;
type

TFormDocument = class(TForm)

……

private
EditNabor:TEditArray; //Это собственно массивы с которым мы будем работать.
GroupBoxNabor:TGroupBoxArray;
LabelNabor:TLabelArray;
DateTimePickerNabor:TDateTimePickerArray;
public
{ Public declarations }

end;

Панель ввода данных. На эту роль отлично подошел ScrollBox. Для тех, кто не знает, эта такая панель, которая может расширять внутрь себя. Т.е. если высота (Height) ScrollBox превысит, высоту формы или компонента, на котором она лежит, то сбоку появляется полоса прокрутки. Важное для нас свойство так как, мы же заранее не можем знать, сколько полей ввода будет входить в документ.

Кнопка «Сохранить». Сохранение документа тоже должно быть динамичным. Поэтому на не подходит статичный запрос. Я подумал использовать сохраненную процедуру, а набор необходимых параметров и соответствие полей ввода, с которых брать данные хранить в таблице.

Вот в принципе и все наброски,  а точнее первое, что пришло мне в голову при прочтении задания.

 

Создаем основной интерфейс.

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

//////////Блок создания списка документов/////////////
ListDocument:= TListBox.Create(FormDocument);
ListDocument.Parent:=FormDocument;
ListDocument.Align:=alLeft;
ListDocument.Height:=FormDocument.ClientHeight;
ListDocument.Width:=186;
ListDocument.OnClick:=ListDocumentClick;

sql:='SELECT * FROM sp_TypeDocument WHERE Active='+C+'1'+C;

ADOQuery.SQL.Clear;
ADOQuery.SQL.Add(sql);
ADOQuery.Open;

ADOQuery.First;

for i := 0 to ADOQuery.RecordCount-1 do
begin
ListDocument.Items.Add(ADOQuery.FieldByName('NameDocument').AsString);
ADOQuery.Next;
end;

//////////Блок создания списка документов/////////////

//////////Блок создания кнопки сохранить/////////////
ButtonSave:=TButton.Create(FormDocument);
ButtonSave.Parent:=FormDocument;
ButtonSave.Top:=FormDocument.ClientHeight-40;
ButtonSave.Left:=ListDocument.Width;
ButtonSave.Height:=40;
ButtonSave.Width:=FormDocument.ClientWidth-ListDocument.Width;
ButtonSave.Caption:='Сохранить';
ButtonSave.Font.Style:=[fsBold];
ButtonSave.OnClick:=ButtonSaveClick;
ButtonSave.Enabled:=False;
//////////Блок создания кнопки сохранить/////////////

//////////Блок создания ScrollBox/////////////
PanelData:=TScrollBox.Create(FormDocument);
PanelData.Parent:=FormDocument;
PanelData.Top:=0;
PanelData.Left:=ListDocument.Width;
PanelData.Height:=FormDocument.ClientHeight-40;
PanelData.Width:=FormDocument.ClientWidth-ListDocument.Width;
//////////Блок создания ScrollBox/////////////

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

Теперь генерируем форму ввода данных

Вот здесь начнется магия.)

И так как я уже писал выше основное событие это клик на записи в Списке доступных документов (ListDocument), поэтому описываем действия, которые будут выполняться при событии ListDocumentClick.
Я весь код процедуры приводить не буду, можете скачать исходники и посмотреть. Я лишь хочу акцентировать ваше внимание на нескольких моментах.

В первую очередь, очищаем массивы от предыдущих элементов.

if Length(LabelNabor)>0 then //Проверка есть что-нибудь в массиве
begin
for I := 0 to Length(LabelNabor)-1 do
begin
LabelNabor[i].Destroy; ///Уничтожаем каждый элемент
end;
LabelNabor:=nil; //Загуляем массив
end;

 

Получаем наименование документа, по которому был совершен клик. Здесь применяется обертка, на случай если клик совершен по пустому месту, чтобы в пустую не выполнять процедуру.

try      //На случай если кликнули по пустому месту
TipDocument:=ListDocument.Items.Strings[ListDocument.ItemIndex];
except
exit;
end;

 

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

id Наименование объекта Родитель
1 GroupBox NULL
2 Edit1 GroupBox
3 Label1 GroupBox

 

В таблице представлена упрощенная схема, которая используется в программе. Если поле «Родитель» имеет значение NULL  или просто пустое – это элемент находится на вершине нашей структуры. В противном случае в поле хранится, указатель (имя родительского объекта). Т.е. сначала надо создать GroupBox, а потом уже создавать Edit и Label и в свойстве Parent указывается GroupBox. Тогда компоненты визуально, ложатся именно в GroupBox.

Итак, мы определись, что сначала будем создавать «главные объекты», которые находятся в вершине нашего древа.

sql:='SELECT TypeObject FROM SpicokObjectDocument WHERE NameDocument='+C+TipDocument+C+' AND ParentObject IS NULL group by SpicokObjectDocument.TypeObject'; //Запрос на получение всех объектов с пустым «Родителем»

ADOQuery.SQL.Clear;
ADOQuery.SQL.Add(sql);
ADOQuery.Open;    //Сначала выбираем получаем список "главных" объектов

for i := 0 to ADOQuery.RecordCount-1 do
begin
//Бегаем в цикле по списку и проверим тип объекта
if ADOQuery.FieldByName('TypeObject').AsString='GroupBox' then
begin
sql:='SELECT * FROM SpicokObjectDocument WHERE NameDocument='+C+TipDocument+C+' AND ParentObject IS NULL AND TypeObject='+C+'GroupBox'+C;

ADOQuery2.SQL.Clear;
ADOQuery2.SQL.Add(sql);
ADOQuery2.Open;

SetLength(GroupBoxNabor, ADOQuery2.RecordCount);

for g := 0 to ADOQuery2.RecordCount-1 do
begin
GroupBoxNabor[g]:=TGroupBox.Create(nil); //Создаем объект
GroupBoxNabor[g].Parent:=PanelData; //Все главные объекты складываю по умолчанию на главную панель.
///Далее определяем основные свойства
//Первый четыре это положение и размер

GroupBoxNabor[g].Left:=ADOQuery2.FieldByName('LeftObject').AsInteger;
GroupBoxNabor[g].Top:=ADOQuery2.FieldByName('TopObject').AsInteger;
GroupBoxNabor[g].Height:=ADOQuery2.FieldByName('HeightObject').AsInteger;
GroupBoxNabor[g].Width:=ADOQuery2.FieldByName('WidthObject').AsInteger;

//Определяем название объекта и надпись
GroupBoxNabor[g].Name:=ADOQuery2.FieldByName('NameObject').AsString;
GroupBoxNabor[g].Caption:=ADOQuery2.FieldByName('CaptionObject').AsString;

ADOQuery2.Next;
end;
end;
ADOQuery.Next;
end;

Я хотел бы обратить ваше внимание, зачем я проверяю тип объекта:

1) Определить в какой массив его записать…Конечно можно сделать динамический массив, типа TComponent, и писать туда все без разбору. Я так не стал делать чтоб потом не запутаться, к чему я обращаюсь.
2) Так код будешь масштабируемым, например вам нужно еще сделать главными компонентами Panel. Все просто дописываете их в таблицу в базе а в коде, в цикле построения главных объектов, просто добавить еще одной условие, по аналогии с кодом для ComboBox.

Далее тот же принцип применяется для построения дочерних объектов.

-Запрос на получения списка активных* дочерних объектов.
-Открываем цикл списку объектов.
—Запрос для получения настроек каждого объекта
—Проверяем типа объекта, и заносим в соответствующий массив
—На забываем определить родителя с помощью процедуры ParentComponent.
-Закрываем цикл.

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

sql:='SELECT TypeObject FROM SpicokObjectDocument WHERE NameDocument='+C+TipDocument+C+' AND ParentObject IS Not NULL AND Active='+C+'1'+C+' group by SpicokObjectDocument.TypeObject';

ADOQuery.SQL.Clear;
ADOQuery.SQL.Add(sql);
ADOQuery.Open;    //Выбираем получаем список "дочерних" объектов

for i := 0 to ADOQuery.RecordCount-1 do
begin
if ADOQuery.FieldByName('TypeObject').AsString='Label' then
begin
sql:='SELECT * FROM SpicokObjectDocument WHERE NameDocument='+C+TipDocument+C+' AND ParentObject IS NOT NULL AND TypeObject='+C+'Label'+C+' AND Active='+C+'1'+C;

ADOQuery2.SQL.Clear;
ADOQuery2.SQL.Add(sql);
ADOQuery2.Open;

SetLength(LabelNabor, ADOQuery2.RecordCount);

for g := 0 to ADOQuery2.RecordCount-1 do
begin
LabelNabor[g]:=TLabel.Create(self); //Создаем объект
LabelNabor[g].Left:=ADOQuery2.FieldByName('LeftObject').AsInteger;
LabelNabor[g].Top:=ADOQuery2.FieldByName('TopObject').AsInteger;
LabelNabor[g].Height:=ADOQuery2.FieldByName('HeightObject').AsInteger;
LabelNabor[g].Width:=ADOQuery2.FieldByName('WidthObject').AsInteger;
LabelNabor[g].Name:=ADOQuery2.FieldByName('NameObject').AsString;
LabelNabor[g].Caption:=ADOQuery2.FieldByName('CaptionObject').AsString;
ParentComponent(ADOQuery2.FieldByName('ParentObject').AsString,ADOQuery2.FieldByName('NameObject').AsString,LabelNabor[g]);
ADOQuery2.Next;
end;
end;
***********************************************************

//Пропущено куча кода

**************************************************************
end;
ADOQuery.Next;
end;
//Проверка есть смысл активировать кнопку сохранить или нет
if (Length(EditNabor)>0) or (Length(DateTimePickerNabor)>0) then
begin
ButtonSave.Enabled:=True;
end;
end;

 

Активных — да я не описался, видите в запросе параметр Active=’+C+’1’+C. Я с недавнего времени, при проектировании справочников всегда добавляю поле активности записи, потому как не раз встречался с ситуацией, когда сегодня запись не нужна. А через неделю вот понадобилась, причем нужна, вот сейчас в данную секунду. Поэтому лучше просто изменить в таблице один параметр, чем удалять и снова заносить запись.

Так теперь немного подробнее про процедуру ParentComponent. Суть работы процедуры в том, что я ей передаю Name объекта-родителя, и указатель на объекта массива которому нужно прописать родителя. Все дело в том, что в свойстве объекта  Parent, должен быть указан указатель на существующий объект TWinControl.

procedure TFormDocument.ParentComponent(NamePatern,NameChild:String;child:TComponent);
var //Присвоение родительского компонента
i:Integer; //Определение идет по имени
begin

 for i := 0 to Length(GroupBoxNabor)-1 do
 begin
 if GroupBoxNabor[i].Name=NamePatern then
 begin
 if (child is TLabel) then
 begin
 TLabel(child).Parent:=GroupBoxNabor[i];
 exit;
 end;
 if (child is TEdit) then
 begin
 TEdit(child).Parent:=GroupBoxNabor[i];
 exit;
 end;

 if (child is TDateTimePicker) then
 begin
  TDateTimePicker(child).Parent:=GroupBoxNabor[i];
   exit;
   end;
  end;
 end;
end;

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

динамически созданный интерфейс
динамически созданный интерфейс

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

Проверяем «что за бред» несет юзверь.

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

Я знаю две схемы валидации данных:

— «Жесткий», это когда пользователь, не может перейти к следующему полю ввода, или шагу по обработке данных пока, правильно не выполнил первый. Данный, подход не плох, но требует много писанины, да и я не считаю его очень гибким. Я думаю проверка, должна не мешать пользователю работать с программой а помогать ему.

— «Свободный», то есть можешь творить, что хочешь, но я в конце все равно все перепроверю, и скажу где ты накосячил. Вот такой вариант мне нравится больше и пускай тут нужно больше работать, на опережение косяков юзверя, но он на мой взгляд более гибкий. И более удобный для пользования на взгляд конечного пользователя.

В данном случае мы будем рассматривать «свободный» подход.

Начну с начала, у каждого поля ввода есть такое событие OnExit – это событие которое срабатывает, когда курсор покидает поле ввода. Поимому лучше момента не придумать, чтоб проверить данные. Причем например, я буду бегать по полям клавишей Tab, если что не правильно после того как покинул поле я получу сообщение.

Сообщение… Это очень важный момент, многие новички просто обожают использовать  ShowMessage, но прости меня господи даже не думайте его применять в данной схеме. Представим ситуацию, когда после каждого поля я буду получать ShowMessage, да тут не то, что старая бабулька взбесится и пошлет вас на*** с вашей программой тут нормальный человек не выдержит.

Поэтому мы будем использовать BallonHint, такая  подсказка в виде балона как в комиксах) Такой вариант выглядит довольно приемлемо.

Вот пример:

Проверка вводимых данных в поле ввода
Проверка вводимых данных в поле ввода

Теперь следующий вопрос, как проверять и чем… Тут все просто, я тоже задался вопросом,  а как провести валидность данных. Нужны же какие-то шаблоны на соответствие которым я буду проверять. И тут меня осенило – РЕГУЛЯРНЫЕ ВЫРАЖЕНИЯ. Для  тех, кто не в теме регулярные выражения   — формальный язык поиска и осуществления манипуляций сподстроками в тексте, основанный на использовании метасимволов (символов-джокеровангл. wildcard characters). По сути это строка-образец (англ. pattern, по-русски её часто называют «шаблоном», «маской»), состоящая из символов и метасимволов и задающая правило поиска. Источник: Википедия.

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

Создаем две переменных, для работы с регулярными выражениями.

  re:TRegex;   //Регулярное выражение
  mc:TMatchCollection;   //То что извлекло регулярное выражение

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

  RegExpModel,Declaration:String;//Регулярное выражение из справочника и описание

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

type

 TModelEdit=array of array of String;
…

var

 ModelEdit : TModelEdit;
…

procedure TFormDocument.FormCreate(Sender: TObject);
begin
 sql:='SELECT * FROM sp_Model ';
 ADOQuery.SQL.Text:= sql;
 ADOQuery.Open;

 SetLength(ModelEdit, ADOQuery.RecordCount);  //Задаем рамер массива

  //Теперь наполним его правилами из справочника
  for i := 0 to (ADOQuery.RecordCount-1) do
  begin
    SetLength(ModelEdit[i], 3);
    ModelEdit[i,0]:=ADOQuery.FieldByName('NameEdit').AsString;
    ModelEdit[i,1]:=ADOQuery.FieldByName('RegExpModel').AsString;
    ModelEdit[i,2]:=ADOQuery.FieldByName('Declaration').AsString;
    ADOQuery.Next;
  end;
 ADOQuery.Close;
end;

При вызове события OnExit, выполняется следующий код:

procedure TFormDocument.EditExit(Sender: TObject);
begin
  SeachModel(TEdit(Sender).Name);    //Определение модели проверки для текущего поля
  if trim(RegExpModel)<>'' then
  begin
    if TRegEx.IsMatch(TEdit(Sender).Text,RegExpModel) then
    begin
      re.Create(RegExpModel);
      mc := re.Matches(TEdit(Sender).Text);  //получаем результат регулярного выражения из текста
      if Length(TEdit(Sender).Text)<>Length(mc[0].Value) then
      begin
        ShowHint(Declaration,TEdit(Sender));   //Показываем подсказку
        TEdit(Sender).Color:=clRed;  //Выделяем красным
      end
      else
      begin
       balloonhint.HideHint; //Прячим подсказку
       TEdit(Sender).Color:=clWhite;  ///Снимаем красное выделение
      end;
    end
    else
    begin
      ShowHint(Declaration,TEdit(Sender));   //Показываем подсказку
      TEdit(Sender).Color:=clRed;       //Выделяем красным
    end;
  end;
end;

Здесь,  SeachModel – эта процедура которая ведет поиск по массиву ModelEdit, с условие указанного  имени поля (TEdit(Sender).Name). Если поиск успешный то заполняются переменные RegExpModel и Declaration. Вызывая функцию TRegEx.IsMatch, мы проверяем соответствует ли содержимое поля (TEdit(Sender).Text), указанному регулярному выражению.
Далее мы выдергиваем ту часть строки, которая соответствует шаблону (регулярному выражению) и сравниваем ее длину с исходной. Это проверка, введена для того чтоб исключить случаи когда шаблону соответствует часть строки, а не полностью.

    re.Create(RegExpModel);
    mc := re.Matches(TEdit(Sender).Text);  //получаем результат регулярного выражения из текста

    if Length(TEdit(Sender).Text)<>Length(mc[0].Value) then

Вот, наверное, и все что я могу пояснить по данным примерам. Если вам понравилась статья, или сайт поделитесь ссылкой в соц сетях. Как обычно прикладываю все исходники. Бэкап тестовой базы данных для ms sql 2005 скачать.