Хранение данных происходит с помощью типа TLinesData, являющегося record.
В TLinesData самое важное - это NodeColList, список класса TVariantList, хранит в себе значения всех колонок для конкретной строки данных.
Здесь нам важно ознакомиться и понять принцип заполнения следующих полей класса UniLines:
Для этого используется процедура Open. Она принимает в себя следующие параметры:
Учитывая все параметры по умолчанию, то для самой простой загрузки данных, без необходимости их редактирования, будет достаточно и такого:
ulf.Open(2820, null, '', RdTr);
Краткая последовательность загрузки данных такова:
MetaObject:=DMForm.MetaObjectList.MainRoot.FindObjectByTag(AMetaObject_Tag); //находится нужный метаобъект по тагу; qQuery.SelectSQL.Text:=MetaQuery.GetFullSQL(sAddWhere, CheckManyToOneLinks, True, False, CheckColumns, False, iCastlingTables); //формируется запрос; qQuery.Open; //открывается датасет FillColumns; //заполнение колонок LoadData(null); //загрузка данных
Самое интересное происходит как раз в двух последних процедурах.
FillColumns выглядит следующим образом:
//цикл по всем метаколонкам метазапроса for i:=0 to MetaQuery.MetaColumnsCount-1 do begin cl:=MetaQuery.MetaColumns[i]; //получение конкретной метаколонки if cl.Is_LineNumber or cl.OnlyForFilters then //условия, что колонке не нужно имя fn := '' else begin //определение названия колонки if MetaQuery.SQL='' then fn := qQuery.Fields[j].FieldName else fn := cl.SQL_Select; inc(j); end; //добавление колонки if FillColumn(cl,fn) then inc(cnt); end;
В FillColumn из интересного происходит следующее:
col:=AddColumn; //вызывает TVirtualTreeColumnFooter(VST.Header.Columns.Add) - добавление колонки в заголовок VST {дальше много кода по выставлению различных свойств у этой колонки} Fields.Add(AFieldName); //добавляет название колонки в Fields fr:=ClsAdd(Cl, col); //добавляет в список Cls "фрейм-колонку" - которая объединяет в себе ссылки на объекты колонок VST и мета-колонки {выставление оставшихся свойств}
Таким образом после FillColumns у нас заполняется список названий колонок Fields, список колонок Cls (элементы которого являются ссылками на колонки VST и на метаколонки).
Затем вызывается LoadData, у которого из основного:
qQuery.First; while (not qQuery.Eof) do begin //addNewLine по умолчанию True if (AddNewLine) then Dat := AddData(nil) //здесь формируется NodeColList об этом будет ниже else //... несущественные сейчас операции if (Dat <> nil) then begin //проходимся по списку наших колонок for i:=0 to Cls.Count-1 do begin //вытаскиваем ссылку на метаколонку fr := TFrameColumn(Cls.Items[i]); cl := fr.cl; //... //заполняем значение в NodeColList для текущей колонки в массиве tempVariant := qQuery.FieldByName(Fields[i]).Value; if VarType(tempVariant)=8209 then begin if VarIsNull(tempVariant) then Dat^.NodeColList.Items[i]:= qQuery.FieldByName(Fields[i]).Value else Dat^.NodeColList.Items[i]:= qQuery.FieldByName(Fields[i]).asString; end else Dat^.NodeColList.Items[i]:= qQuery.FieldByName(Fields[i]).Value; end end end
И теперь ещё посмотрим, что происходит внутри AddData:
function TUniLinesFrame.AddData(ANode: PVirtualNode = nil): PLinesData; var Node: PVirtualNode; i: integer; begin //если мы передали ANode = nil, то будем создавать новую ноду, иначе работаем с переданной if ANode=nil then begin Node:=VST.AddChild(nil); if FIsMultiline then Node.States:=Node.States+[vsMultiline]; end else Node:=ANode; //... Result:=VST.GetNodeData(Node); //получаем данные текущей ноды (запись типа PLinesData) Result.NodeColList:=TVariantList.Create; //создаём объект NodeColList for i:=0 to Cls.Count-1 do Result.NodeColList.Add(Null); //заполняем null-ами элементы по числу наших колонок в Cls //инициализация остальных параметров, указание опций по умолчанию end;
Подытожим последовательность происходящих действий:
Рассмотрим модуль DocInWay.pas (карточка накладной).
Например, в модуле , есть UniLines, который отображает данные по услугам из этой накладной. Сами накладные у нас хранятся в таблице DOCS_IN_WAY, а услуги в ней - в таблице DOCS_IN_WAY_SERVS.
Когда мы создаём новую накладную, в ней никаких услуг быть не может, но для инициализации колонок нам всё равно нужно выполнить процедуру Open:
ULF.Open(1581, -1, 'SP_GEN_DOCS_IN_WAY_SERVS_ID', UpTr);
Но когда открывается существующая накладная, мы делаем так:
ULF.Open(1581, ID, 'SP_GEN_DOCS_IN_WAY_SERVS_ID', UpTr);
В качестве ID используется ID текущей накладной, которую мы открываем.
При формировании запроса будет сформировано условие dis.doc_in_way_id = :MAIN_ID, которое оставит только те строки, которые относятся к накладной. В этот :MAIN_ID будет подставляться переданный ID. Это делается за счёт наличия колонки со следующими опциями:
Другой пример, журнал заказов - модуль ZakazPrepHim.pas.
ULF.Open(66, Null, ' ', RdTr, False, WhereSQL, True, ' ', True, False, cbDateKind.ItemIndex);
Обновление данных происходит с помощью процедуры procedure TUniLinesFrame.Refresh. Она принимает следующие параметры:
Основная логика Refresh такова:
//...предварительные проверки и инициализации //если у нас не поменялось условие для фильтрации, нам не нужен :MAIN_ID и у нас неполная перезагрузка данных, то: if (sAddWhere=sLastAddWhere) and (Object_ID=Null) and (not FullReload) and (not IsVisiblePage) then begin //получаем список конкретных записей, которые были изменены (добавлены, изменены или удалены) ids:=GiveMeResultSET('SELECT DISTINCT ITEM_ID FROM META_OBJECTS_CHANGES MOC '+ 'WHERE ID>:p1', [FLastLoadId], RdTr); try //по каждой записи получаем все произошедшие изменения while not ids.Eof do begin Ch:=GiveMeResultSET('SELECT * FROM META_OBJECTS_CHANGES MOC '+ 'WHERE ID>:p1 AND MOC.ITEM_ID=:p2 '+ 'ORDER by MOC.change_date,MOC.item_id', [FLastLoadId, FldAsInt64(ids.FieldByName('ITEM_ID'))], RdTr); try WasUpdate:=False; WasInsert:=False; WasDels:=False; while not Ch.Eof do begin case Ch.FieldByName('CHANGE_TYPE').AsInteger of 0: WasInsert:=True; 1: WasUpdate:=True; 2: WasDels:=True; end; Ch.Next; end; //через флаги определяем конечное состояние записи, были ли она в конечном счёте добавлена, обновлена или удалена if WasInsert and WasUpdate then WasUpdate:=false; if WasDels and WasInsert then begin WasDels:=False; WasInsert:=False; end; //если запись добавлена или обновлена, то выполнить LoadLine if WasInsert then begin if LoadNewFromMetachanges then begin Located:=LocateByPrimaryValue(FldAsInt64(ids.FieldByName('ITEM_ID')), False); //определяем, есть ли у нас эта запись LoadLine(FldAsInt64(ids.FieldByName('ITEM_ID')), not Located); //<---------- WasChanges; end; end else begin if LocateByPrimaryValue(FldAsInt64(ids.FieldByName('ITEM_ID')), True) then begin if WasUpdate then begin // update line if Self.Value[PrimaryFieldParsed]=FldAsInt64(ids.FieldByName('ITEM_ID')) then begin LoadLine(FldAsInt64(ids.FieldByName('ITEM_ID')), False); //<---------------------- WasChanges; end; end else //если запись удалена, то удаляем соответствующую ноду из VST if WasDels then begin if Self.Value[PrimaryFieldParsed]=FldAsInt64(ids.FieldByName('ITEM_ID')) then begin Self.VST.DeleteNode(Self.VST.FocusedNode); WasChanges; end; end; end; end; finally ch.Free; end; ids.Next; end; finally ids.Free; end; FLastLoadId:=FastSQLInt('SELECT * FROM SP_GEN_META_OBJECTS_CHANGES_ID', [])-1; end
Основная загрузка данных происходит в процедуре LoadLine. В качестве входящих параметров принимает PrimaryKey нужной записи и AddNewLine - флаг, что нам надо добавить новую строку. PrimaryKey нужен, чтобы в запрос метаобъекта поставить условие по конкретному ID. Поле для условия берётся по флагу Primary Field у колонки метаобъекта.
Происходит это так:
procedure TUniLinesFrame.LoadLine(PrimaryKey: Variant; AddNewLine: Boolean = False); var vOldCur: TCursor; vWhere: String; begin //...предварительные обработки //если запрос ещё не сформирован, то формируем его, добавляя в условие параметр PRIM if (MetaQuery.SQL = '') then qQuery.SelectSQL.Text := MetaQuery.GetFullSQL(MetaQuery.PrimaryField +'=:PRIM'+ vWhere, CheckManyToOneLinks) else qQuery.SelectSQL.Text := MetaQuery.GetFullSQL(MetaQuery.PrimaryFieldOrigin +'=:PRIM'+ vWhere, False); //передаём параметр qQuery.ParamByName('PRIM').Value := PrimaryKey; qQuery.Open; //формируем датасет LoadData(PrimaryKey, AddNewLine); //используем уже рассмотренную ранее процедуру по загрузке данных end;
Т.е. LoadLine, по своей сути, формирует запрос данных, но с таким условием, чтобы получить только одну запись. Разумеется, для нормальной работы соответствующий метаобъект должен быть корректно настроен.
Если же нам требуется полная перезагрузка данных, то тут процесс схож с процедурой Open и только дополнен очищением текущих данных и ссылок, после чего также формируется запрос, формируется датасет и вызывается LoadData.
Рассмотрим модуль DocsInWay.pas (журнал накладных).
На журнале расположены кнопки, выступающие в роли быстрой фильтрации журнала:
На нажатие каждой кнопки повешен обработчик, в процессе работы которого формируется некоторая строка sWhere, содержащая в себе необходимое условие для запроса, после чего вызывается
ULF.Refresh(null, sWhere, True);
UniLines позволяет легко сохранять имеющиеся данные в базу. Для этого используется процедура Save. Она не принимает в себя никаких параметров. Разберём, как именно она работает:
procedure TUniLinesFrame.Save; var Node: PVirtualNode; Dat: PLinesData; oldCur: TCursor; prevCurLine: PLinesData; begin {... предварительные обработки} try //начинаем работать непосредственно с нодами VST, начиная с первой и до последней по очереди Node:=VST.GetFirst; while Node<>nil do begin //получаем данные, хранящиеся в этой ноде Dat:=VST.GetNodeData(Node); curLine:=Dat; //если состояние LineState имеет значения lnInsert, lsUpdate или lsDelete, то вызываем процедуру OnSaveLine, если она имеется; if (Dat^.LineState<>lsNone) and (Dat^.LineState<>lsEmpty) then begin if Assigned(FOnSaveLine) then FOnSaveLine(Dat^.LineState); end; //Отработка OnSaveLine может поменять состояние текущей строки (например, там может быть запрет на дальнейшие операции путём выставления lsNone). if (Dat^.LineState<>lsNone) and (Dat^.LineState<>lsEmpty) then begin //если состояние не поменялось, то запускается SaveLine SaveLine; //после него запускается обработчик OnAfterSaveLine, если он имеется. if Assigned(FOnAfterSaveLine) then FOnAfterSaveLine(Dat^.LineState); end; Node:=VST.GetNext(Node); end; // Помечаем все изменные строки как неизмененные Node:=VST.GetFirst; while Node<>nil do begin Dat:=VST.GetNodeData(Node); curLine:=Dat; if (Dat^.LineState<>lsNone) and not ((Dat^.LineState = lsDelete) or (Dat^.LineState = lsEmpty)) then begin Dat^.LineState:=lsNone; end; Node:=VST.GetNext(Node); end; finally Screen.Cursor:=oldCur; curLine:=prevCurLine; end; end;
Здесь появляется ещё один важный параметр - LineState, который указывается для данных ноды. В этой статье подробно мы не будем останавливаться, отметим про него, что это - условно «состояние» строки, была ли она добавлена, обновлена или удалена (значение LineState будет принимать соответственно lsInsert, lsUpdate, lnDelete). В остальных случаях она будет приниимать значение lsNone (стандартное значение при загрузке данных) или lsEmpty.
OnSaveLine и OnAfterSaveLine - базово этих обработчиков нет, но они могут быть заданы в рантайме для тех или иных целей. Примеры рассмотрим позже, а пока обратим внимание на самую интересную часть, процедуру SaveLine:
procedure TUniLinesFrame.SaveLine; var i: integer; PrimTbl, PrimFld: string; PrimVal, CurVal: variant; MetaCol: TMetaColumn; begin //если lineState стал lsNone, то ничего не делается; if LineState=lsNone then Exit; {...прочие очистки} //перебираем список колонок for i:=0 to Cls.Count-1 do begin MetaCol:=GetMetaColumn(i); //выбираем только те колонки, у которых указана опция Is_Field и не указана Is_LineNumber if (MetaCol.Is_Field) and (not MetaCol.Is_LineNumber) then begin //получаем значение, содержащееся в определенной колонке текущей записи curVal:=curLine.NodeColList.Items[i]; //если колонка с опцией Is_Primary, текущее значение Null, то генерируем значение ID на основе той процедуры, которую мы передали при Open у текущего UniLines; if MetaCol.Is_Primary then begin if (CurVal=null) then begin if (FProcGenID<>'') then begin CurVal:=FastSQLVal('select * from '+FProcGenID, []); curLine.NodeColList.Items[i]:=CurVal; if curLine.LineState=lsUpdate then curLine.LineState:=lsInsert; end; end; end; {...доп. обработки в зависимости от типа значения колонки} //заполняем массив полей и их значений sqlFlds.Add(Fields[i]); sqlVals.Add(CurVal); end; end; {...} //получаем название таблицы, в которую будем сохранять данные, и название поля с первичным ключом MetaCol:=MetaQuery.PrimaryColumn; if MetaCol<>nil then begin PrimTbl:=MetaCol.TableName; PrimFld:=MetaCol.FieldName; i:=GetMetaColumnIndex(MetaCol); PrimVal:=curLine.NodeColList.Items[i]; end; //в зависимости от текущего состояния строки, делаем нужную операцию с базой данных if (PrimTbl<>'') and (PrimFld<>'') then case curLine.LineState of lsInsert: begin InsertFromControls(PrimTbl, sqlFlds, sqlVals, RdTr); end; lsUpdate: begin UpdateFromControls(PrimTbl, PrimFld, PrimVal, sqlFlds, sqlVals, RdTr); end; lsDelete: begin DeleteFromControls(PrimTbl, PrimFld, PrimVal, RdTr); end; end else raise Exception.Create('Primary key for '+MetaObject.ObjName+' not found'); end;
Обратим внимание, что коммита здесь не происходит, его надо выполнять отдельно. Также, если не нашли Primary-колонку, будет выдан эксэпшн.
Рассмотрим простой пример сохранения, в котором опишем полную последовательность шагов, что за чем происходит. Обратим внимание на модуль PlanMobileItem.pas - карточка выезда в журнале выездов. В качестве сохраняемого UniLines здесь используется таблица с перечнем заказов, по которому должен быть выезд.
Открытие Unilines:
ULF.Open(5758, -1, 'SP_GEN_MOBILE_PLAN_ORDERS_ID', UpTr, False);
Здесь есть колонка «ВнНомер», с опциями Primary, Is_Field. В качестве поля используется ID из таблицы MOBILE_PLAN_ORDERS.
Заполнение этой таблицы данными делается в процессе работы пользователя, механизмы заполнения рассмотрим в другой статье.
При закрытии карточки выезда вызывается ULF.Save, и далее по шагам: