====== Работа с данными в UniLines ====== В этой статье мы рассмотрим, как взаимодействовать с UniLines в рантайме, а именно: * Как обращаться к хранящимся данным; * Как формировать таблицу без участия метаобъекта; * Как происходит добавление/изменение/удаление строк. ===== Обращение к данным UniLines ===== [[https://doc.agb.is/unilinesdb|В прошлой статье]] уже было разобрано, что данные хранятся внутри каждой ноды, в поле NodeColList. Однако, обращаться к ним "голым способом", прописывая каждый раз цикл поиска нужного значения, было бы очень... громоздко. В связи с этим широко используется следующее свойство: property Value[Name: string]:variant read GetValue write SetValue; Оно же используется не только для считывания, но и записи. Пример использования: var x, y: variant; begin x := 3; ULF.Value['x'] := x; y := ULF.Value['x']; //<----y будет хранить значение 3 end Если крайне сжато, то это работает так: * В ULF должна существовать наша колонка 'x'; * Для текущей выбранной строки в эту колонку 'x' будет записано значение 3; * В текущей выбранной строке из колонки 'x' взять значение. Теперь рассмотрим, как это происходит внутри. У Value есть геттер GetValue и сеттер SetValue. Внутри они используют функцию GetColIndex function TUniLinesFrame.GetValue(Name: string): variant; var i: integer; begin Result:=null; if (curLine<>nil) and (curLine.NodeColList<>nil) then begin i:=GetColIndex(Name); try Result:=curLine.NodeColList.Items[i]; except end; end; end; procedure TUniLinesFrame.SetValue(Name: string; const Value: variant); var i: integer; begin if curLine<>nil then begin i:=GetColIndex(Name); curLine.NodeColList.Items[i]:=Value; if (not (nsLoading in LinesState)) and (i<>-1) and GetMetaColumn(i).Do_Summ then DoFullSumma(i); end; end; function TUniLinesFrame.GetColIndex(fldName: string): int64; var i: integer; s: string; cl: TMetaColumn; begin Result:=-1; s:=UpperCase(fldName); for i:=0 to Cls.Count-1 do begin cl := GetMetaColumn(i); if not cl.Is_LineNumber then begin if UpperCase(Fields[i])=s then begin Result:=i; Break; end; end end; end; Алгоритм действий таков: - Вызвав x := ULF.Value['x'], мы вызываем геттер GetValue; - Внутри GetValue вызывается GetColIndex('x'); - GetColIndex прогоняет цикл по элементам списка Cls, и находит, есть ли колонка с отключенной опцией Is_LineNumber, и при этом имеется соответствующий элемент в списке названий колонок Fields и возвращает номер колонки; - Затем берётся нужное значение из i-го элемента списка NodeColList. Похожим образом значение будет записано в NodeColList в сеттере SetValue, и вызваны дополнительные обработчики по суммированию данных. Ещё здесь стоит обратить внимание, что значения берутся из конкретного CurLine. CurLine - это указатель на данные конкретной ноды. Как он меняется, рассмотрим ниже. ===== Как формировать таблицу без участия метаобъекта ===== UniLines можно формировать и без метаобъектов. На практике такое используется, к примеру, если нужно сделать многоуровневую структуру данных. В таком режиме работа с ULF становится ближе к работе напрямую с VST, но существуют некоторые упрощения, учитывающие уже известные нам возможности по хранению и доступа к данным. ==== Добавление колонок ==== Во-первых, для такого ULF требуется вручную задать колонки, с которыми потом планируется работа. Для этих целей существует функция AddFieldColumn. Входящие параметры: * FieldName: string - внутреннее название колонки (только на английском языке, чтобы из кода можно было к ней обращаться); * ColName: string - название колонки для отображения в дереве (на любом языке); * Pas_type: string - название типа данных, который будет содержаться в колонке (одно из - STRING, INTEGER, DOUBLE, BOOLEAN, TDATETIME); * DisplayFormat: string - формат отображения значений вида DOUBLE; * MaxWidth: variant - максимальная ширина колонки в дереве. Передавать null, чтобы было без ограничений по размерам; * IsVisible : boolean = True - будет ли видима колонка в дереве или нет (например, можно скрыть колонки со значениями разных ID); * ADo_Summ: boolean = False - требуется ли суммирование по этой колонке внизу дерева (актуально для числовых значений); * AIs_Money: boolean = False - задаёт колонке зелёный фон; * DefaultWidth: integer = 0 - ширина колонки по умолчанию. В случае 0 расчёт ширины будет производить само VST. Кратко рассмотрим, что делает эта функция: function TUniLinesFrame.AddFieldColumn(FieldName, ColName, Pas_Type, DisplayFormat: string; MaxWidth: variant; IsVisible: boolean = True; ADo_Summ: boolean = False; AIs_Money: boolean = False; DefaultWidth: integer = 0): TMetaColumn; var col: TVirtualTreeColumnFooter; cl: TMetaColumn; s, rd: string; Node: PVirtualNode; Dat: PLinesData; fr: TFrameColumn; begin //создаём колонку средством VST; col:=AddColumn; {...} //создаём соответствующую метаколонку; cl:=TMetaColumn.Create(RdTr, TmpQuery.UpTr); Result:=cl; {...указание различных параметров} //создание объединённой колонки TFrame fr:=ClsAdd(cl, col); Fields.AddObject(FieldName, fr); //добавление названия в список названий колонок ValueFlds.AddObject(FieldName, fr); //добавление названия колонок в список, который обрабатывается для удаления колонок {...выставление оставшихся настроек} // Добавляем в каждую ноду по пустому значению VST.BeginUpdate; try Node:=VST.GetFirst; while Node<>nil do begin Dat:=VST.GetNodeData(Node); Dat^.NodeColList.Add(null); Node:=VST.GetNext(Node); end; finally VST.EndUpdate; end; end; Т.е. в конце функции, добавляющей колонку, для каждой существующей сейчас ноды в неё будет записан null. ==== Добавление строк ==== После того, как были добавлены все необходимые колонки, можно добавлять строки. Это делается с помощью процедуры AddLine, основная логика которой такова: procedure TUniLinesFrame.AddLine(AddFirstLine: boolean = False); var Node: PVirtualNode; begin if AddFirstLine then Node:=VST.InsertNode(nil, amAddChildFirst) else Node:=VST.AddChild(nil); {...выставление разных опций} curLine:=AddData(Node); LastNode:=Node; end; Напрямую в VST добавляется новая нода, либо в самое начало (если передали AddFirstLine = True), либо в конец. AddData заполняет соответствующий ноде NoceColList null-ами. Также обратим внимание на то, что AddLine изменяет значение curLine (с которым мы встретились при получении и присвоении значений колонки), и запоминает в целом новую ноду в LastNode. Смена указателя curLine нам позволяет писать код следующего вида: ULFResults.AddLine; ULFResults.Value['RESULT_ID'] := objID; ULFResults.Value['action_id'] := q.FieldByName('call_action_id').AsInt64; ULFResults.Value['NAME'] := q.FieldByName('action_name').AsString; ULFResults.Value['Type'] := q.FieldByName('type_name').AsString; Т.е. добавив новую строку AddLine, мы сразу для новой строки можем указывать нужные значения. ==== Перебор строк UniLines ==== Ещё одна типовая задача - пройтись по всем строкам UniLines. Рассмотрим на конкретном примере. Модуль BnakStatemens.pas (журнал банковских выписок). В журнале есть кнопка, нажатие которой должно создать банковские документы по всем подходящим по условиям выпискам, которые отображаются в ULF. Реализация следующая (приведён немного упрощённый вид): ULF.DisableControls; try ULF.FirstLine; for i:=0 to ULF.LinesCount-1 do begin if (ULF.Value['CONTR_ID'] <> NULL) and (ULF.Value['contr_accoun_id'] <> NULL) and (ULF.Value['DOC_ID'] = NULL) then begin try WriteLog('Line'); DocID := CreateBankDoc(ULF.Value['CONTR_ID'], ULF.Value['contr_accoun_id'], ULF.Value['SUMMA'], 0, ULF.Value['BASES'], ULF.Value['DOC_NUM'], ULF.Value['DOC_DATE'], ULF.Value['is_incoming'], ULF.Value['ACCOUNT_IN']); except on E:Exception do SimpleMessage(E.Message); end; end; ULF.NextLine; end; finally ULF.EnableControls; end; Здесь мы видим целый набор новых функций - парные DisableControls, EnableControls; FirstLine; LinesCount; NextLine. Большинство из них интуитивно понятны, но разберём всё по порядку, что кого и зачем. Начнём с FirstLine: function TUniLinesFrame.FirstLine(const iFirstVisible: Boolean = True): Boolean; var vNode: PVirtualNode; vDat: PLinesData; begin {...предварительные проверки} if iIsVisible then vNode := VST.GetFirstVisible else vNode := VST.GetFirst; while vNode <> nil do begin vDat := VST.GetNodeData(vNode); if (vDat <> nil) and (vDat^.LineState <> lsEmpty) then begin VST.FocusedNode := vNode; VST.Selected[vNode] := True; Break; end; vNode := pNextNode(vNode, iFirstVisible); end; end; Ноды в VST могут быть видимые и невидимые, и в зависимости от переданного параметра iFirstVisible в качестве старта мы возьмём соответствующую ноду. От неё в VST ищется нода, имеющая данные, при этом у которой LineState <> lnEmpty. Затем, это важно, происходит фокусировка на этой ноде (VST.FocusedNode) и она считается выбранной (VST.Selected[vNode]). Мы уже знаем, что данные берутся из CurLine, но здесь он явно не меняется. Так каким образом мы получаем возможность получать данные из первой ноды? Это делается за счёт обработчика на VST - VSTFocusChanged: procedure TUniLinesFrame.VSTFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex); begin curNode:=Node; if Node<>nil then begin curLine:=VST.GetNodeData(Node); if Assigned(FOnLineClick) then if MetaObject<>nil then FOnLineClick(MetaQuery.PrimaryColumn, MetaQuery.PrimaryField, curLine.NodeColList.Items[PrimaryIndex]) else FOnLineClick(nil, '', curLine.NodeColList.Items[PrimaryIndex]) end else curLine:=nil; end; Он срабатывает в тот момент, когда мы делаем VST.FocusedNode := vNode; и именно за счёт этого обработчика мы меняем curLine и получаем возможность взять данные из первой ноды. Помимо этого срабатывает обработчик FOnLineClick, если он был задан (по умолчанию отсутствует). Теперь LinesCount - подсчёт числа строк. В зависимости от переданных параметров он может учитывать ноды с определённым LineState, учитывать или нет невидимые ноды, но всё это необязательные параметры, и в самой простой реализации он выглядит так: function TUniLinesFrame.LinesCount(LineState: TLineState = lsNone; OnlyVisible: boolean = True): int64; var i: integer; Node: PVirtualNode; Dat: PLinesData; begin i:=0; Node:=VST.GetFirstVisible; while Node<>nil do begin Dat:=VST.GetNodeData(Node); if Dat^.LineState<>lsEmpty then i:=i+1; Node:=VST.GetNextVisible(Node); end; Result := i; end Т.е. это простой счётчик числа видимых нод в VST, с полями самого ULF тут никакой работы не происходит. На очереди NextLine: function TUniLinesFrame.NextLine(const iNextVisible: Boolean = True): Boolean; var vResNode, vCurNode: PVirtualNode; vDat: PLinesData; begin {...предварительные инициализации} //поиск подходящей ноды vCurNode := pNextNode(VST.FocusedNode, iNextVisible); while (vCurNode <> nil) do begin vDat := VST.GetNodeData(vCurNode); if (vDat <> nil) and (vDat^.LineState <> lsEmpty) then begin vResNode := vCurNode; Break; end; vCurNode := pNextNode(vCurNode, iNextVisible); end; //смена фокуса if (vResNode <> nil) and (VST.FocusedNode <> vResNode) then begin Result := True; VST.FocusedNode := vResNode; VST.Selected[vResNode] := True; end; end; В целом, NextLine похож на FirstLine. И он тоже меняет фокус, изменение которого меняет curLine. Смена фокуса чревата тем, что прогон цикла по ULF заставит каждую строку поочередно подсветиться синим цветом. Во-первых, это очень раздражает глаза, во-вторых, на это также тратится существенное время. Чтобы такого избежать были сделаны Disable/EnableControls. Если сжато, то их принцип таков: procedure TUniLinesFrame.DisableControls(DoSetLastAsDis: boolean = False); var Node: PVirtualNode; Dat: PLinesData; begin VST.BeginUpdate; if curNode<>nil then DisNode:=curNode else DisNode:=VST.FocusedNode; {...дополнительный код, суть которого в том, что в DisNode, если он оказался пустым, записывается ссылка просто на первую видимую ноду} end; procedure TUniLinesFrame.EnableControls; begin VST.FocusedNode:=nil; VST.ClearSelection; VST.EndUpdate; if DisNode<>nil then begin VST.FocusedNode:=DisNode; if VST.IsVisible[DisNode] then VST.Selected[DisNode]:=True; end; end; Здесь очень важно понять, что DisableControls вызывает процедуру VST.BeginUpdate, что отключает любую перерисовку дерева. При этом мы также запоминаем ноду, на которой последний раз был фокус. EnableControls вызывает VST.EndUpdate, снова включая отрисовку, и возвращает фокус. Таким образом мы уходим от мельтешения строчек на экране и значительно ускоряется процесс прохода по строкам ULF. И т.к. мы выключили перерисовку VST, то весь цикл прогона обязательно надо выполнять в try-finally, и в finally выполнять EnableControls, иначе в случае какой-либо ошибки внутри цикла отрисовка будет потеряна. Таким образом типовой цикл по строкам ULF выглядит следующим образом: ULF.DisableControls; try ULF.FirstLine; for i:=0 to ULF.LinesCount-1 do begin //бизнес-логика, что нам нужно сделать с нашей строкой. ULF.NextLine; end; finally ULF.EnableControls; end; [[https://forms.gle/2w8aEWH3TFb6EcTr6|Пройти тест]] [[https://doc.agb.is/develop/unilines|Назад]]