Работа с данными в UniLines

В этой статье мы рассмотрим, как взаимодействовать с UniLines в рантайме, а именно:

  • Как обращаться к хранящимся данным;
  • Как формировать таблицу без участия метаобъекта;
  • Как происходит добавление/изменение/удаление строк.

Обращение к данным UniLines

В прошлой статье уже было разобрано, что данные хранятся внутри каждой ноды, в поле 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;

Алгоритм действий таков:

  1. Вызвав x := ULF.Value['x'], мы вызываем геттер GetValue;
  2. Внутри GetValue вызывается GetColIndex('x');
  3. GetColIndex прогоняет цикл по элементам списка Cls, и находит, есть ли колонка с отключенной опцией Is_LineNumber, и при этом имеется соответствующий элемент в списке названий колонок Fields и возвращает номер колонки;
  4. Затем берётся нужное значение из 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;

Пройти тест

Назад