Search code examples
delphidevexpressvcldelphi-10.2-tokyo

Delphi Loop a tcxtreeList for check and uncheck nodes depending on key values from string


I am pretty new to delphi development, I have a custom CheckTreeList component inherited from cxTreeList Devexpress component. When i check some nodes in a list, Those values are stored to a string in a format as shown below String Format for selected nodes as image The issue is I am not able to check the nodes of the checktreelist by looping through the nodes and values in the string. I have tried the below code for saving and loading the checked and unchecked nodes. Saving the checked node key values to string is working, But Loading the nodes and checking them is not working. The below is the component source code

unit DXCheckTreelist;

interface

uses
  System.Classes, cxTL, cxLookAndFeelPainters;

type

  TdxUnboundTreeListNode = class(TcxUnboundTreeListNode)

  protected
    procedure SetCheckState(AValue: TcxCheckBoxState); override;
  end;

  TdxCheckTreeList = class(TcxTreeList)
  Private
    FEnableStdTreebehaviour : Boolean;
  protected
    function CreateNode: TcxTreeListNode; override;
  Published
    Property EnableStdTreebehaviour: Boolean read FEnableStdTreebehaviour write FEnableStdTreebehaviour default False;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('DX Components', [TdxCheckTreeList]);
end;
{ TdxCheckTreeList }    
function TdxCheckTreeList.CreateNode: TcxTreeListNode;
begin
  Result := TdxUnboundTreeListNode.Create(Self);
  Changes := Changes + [tcStructure];
end;

{ TdxUnboundTreeListNode }

procedure TdxUnboundTreeListNode.SetCheckState(AValue: TcxCheckBoxState);
var
  ParentNode : TdxUnboundTreeListNode;
  PrevCheckState: TcxCheckBoxState;
const
  AState: array[TcxCheckBoxState] of TcxTreeListNodeCheckInfos = ([], [nciChecked], [nciGrayed]);
  AParentCheckState: array[Boolean] of TcxCheckBoxState = (cbsGrayed, cbsChecked);
begin

  if  TdxCheckTreeList(TreeList).FEnableStdTreebehaviour then
  begin
    inherited;
    Exit;
  end;

  if not CanChecked then
  begin
    State := State - [nsCheckStateInvalid];
    Exit;
  end;

  PrevCheckState := CheckState;
  CheckInfo := CheckInfo - [nciChecked, nciGrayed] + AState[AValue] + [nciChangeCheck];


  try
    if (CheckState in [cbsChecked, cbsUnchecked]) and HasChildren then
    begin
      LoadChildren;

      if AValue = cbsUnchecked then
        SetChildrenCheckState(CheckState, nil);
    end;

    ParentNode := TdxUnboundTreeListNode(Parent);

    if ParentNode <> nil then
    begin
      if ParentNode.IsRadioGroup and Checked then
        ParentNode.SetChildrenCheckState(cbsUnchecked, Self);

      if not (nciChangeCheck in ParentNode.CheckInfo) and (ParentNode <> Root) then
        ParentNode.CheckState := cbsChecked;
    end;
  finally
    CheckInfo := CheckInfo - [nciChangeCheck];
    State := State - [nsCheckStateInvalid];

    if CanChecked then
      Repaint(True);

    if (PrevCheckState <> CheckState) and Assigned(TcxTreeList(TreeList).OnNodeCheckChanged) then
      TcxTreeList(TreeList).OnNodeCheckChanged(TreeList, Self, CheckState);
  end;
end;
end.

In my case the property EnableStdTreebehaviour is set to true.

Code for saving selected node key values is

procedure TfrmTreeList.btnSaveDataClick(Sender: TObject);
var
  I, J: Integer;
  node, cnode: TcxTreeListNode;
  Result: String;
begin
  result:= '';
  for i := 0 to ctvMandatory.Count - 1 do
  begin
    node := TcxTreeListNode(ctvMandatory.Items[i]);
    if ctvMandatory.Items[i].CheckState in [cbsChecked, cbsGrayed] then
    begin
      if node.Level = 0 then Result:= Result + '[' + node.Values[1] + ']' + ',';

      for J := 0 to ctvMandatory.Items[i].Count - 1 do
      begin
        cnode := ctvMandatory.Items[i].Items[J];
        if (cnode.Checked) and (cnode.Level = 1) then
        begin
          Result:= Result + cnode.Values[2] + ',';
        end;
      end;
    end;
  end;

  if (Result <> '') and (Result[Length(result)] = ',') then
        result:= Copy(Result, 1, length(Result) -1 );
  Memo.Clear;

  if result <> '' then
  begin
    Memo.Lines.Add(Trim(Result));
    csv := result;
  end;
  for i := 0 to ctvMandatory.count - 1 do
  begin
    node := TcxTreeListNode(ctvMandatory.Items[i]);
    ctvMandatory.Items[i].Checked := False;
  end;
end;

Code i tried for loading and check node depend on keyvalue from string is

procedure TfrmTreeList.btnLoadDataClick(Sender: TObject);
var
  i, j, X: integer;
  node, cnode: TcxTreeListNode;
  sl,s2: TStringList;
  str: string;
  key, value, val: string;
begin
  chbAll.Checked:= csv = 'All';
  ctvMandatory.BeginUpdate;
  if chbAll.Checked then
  begin
  for i:= 0 to ctvMandatory.AbsoluteCount - 1 do
    ctvMandatory.Items[I].Checked := True;
    ctvMandatory.EndUpdate;
    SetMandatoryText;
    Exit;
  end;

  for i:= 0 to ctvMandatory.Count - 1 do
    ctvMandatory.Items[I].Checked := False;

  if csv = 'All' then
  begin
    for i:= 0 to ctvMandatory.AbsoluteCount - 1 do
      ctvMandatory.Items[I].Checked := True;
  end
  else
  if (length(csv) > 0) and (Pos(']', csv) = 0) then
  begin
    for i := 0 to ctvMandatory.Count - 1 do
    begin
      node:= TcxTreeListNode(ctvMandatory.Items[i]);
      if node.Level = 0 then
        ctvMandatory.Items[i].Checked:= True
      else
      if (node.Level = 1) and IsValueInCSV(csv, node.Values[1])  then
      begin
        ctvMandatory.Items[i].Checked := True;
      end;
    end;
  end
  else
  begin
   sl:= TStringList.Create;
   sl.Delimiter:= ',';
   sl.DelimitedText:= csv;
   node:= nil;
   s2:= TStringList.Create;
   s2.Delimiter:= ',';
   for str in sl do
   begin
     if (pos('[', str) > 0) then
     begin
       if (value <> '') and (value[Length(value)] = ',') then
          value := Copy(value, 1, length(value) -1);
       s2.DelimitedText:= value;
       if (node <> nil) and (value <> '') and (node.HasChildren) then
       begin
         for I := 0 to ctvMandatory.Count - 1 do
          begin
           while Node <> Nil do
           begin
             node:= TcxTreeListNode(ctvMandatory.Items[I]);
             node:= node.getFirstChild;
             if not node.Checked then
             begin
               val := '';
               for val in s2 do
               begin
                 node.Checked := true;
                 node.getNextSibling;
               end;
             end;
             s2.Clear;
           end;
          end;
       end;
       value:= '';
       val := '';
       key:= ReplaceStr(str, '[', '');
       key:= ReplaceStr(key, ']', '');
       for I := 0 to ctvMandatory.Count - 1 do
       begin
         if (TcxTreeListNode(ctvMandatory.Items[i]).Values[1] = key) and ((ctvMandatory.Items[i]).Level = 0) then
         begin
           node:= TcxTreeListNode(ctvMandatory.Items[i]);
           Break;
         end;
       end;
     end
     else
     begin
       value:= value + str + ',';
     end;
   end;
   if (value <> '') and (value[Length(value)] = ',') then
      value := Copy(value, 1, length(value) -1);
   s2.DelimitedText:= value;
   if (node <> nil) and (value <> '') and (node.HasChildren) then
   begin
    for I := 0 to ctvMandatory.Count - 1 do
    begin
     while Node <> Nil do
     begin
       node:= TcxTreeListNode(ctvMandatory.Items[I]);
       node:= node.getFirstChild;
       if not node.Checked then
       begin
         val := '';
         for val in s2 do
         begin
           node.Checked := true;
           node.getNextSibling;
         end;
       end;
       s2.Clear;
     end;
    end;
   end;
   sl.Free;
   s2.Free;
  end;
  ctvMandatory.EndUpdate;
  SetMandatoryText;
end;

function TfrmTreeList.IsValueInCSV(const CSV, Value: string): Boolean;
begin
  Result := IsValueInCSV(CSV, Value, False);
end;

function TfrmTreeList.IsValueInCSV(const CSV, Value: string; ResultIfBothEmpty: Boolean): Boolean;
begin
  if Trim(CSV) = Trim(Value) then
  begin
    if Trim(Value) = '' then
      Result := ResultIfBothEmpty
    else
      Result := True;
  end
  else
    Result := MatchStr(Value, SplitString(CSV, ','));
end;

Can some please check and help me on this issue?


Solution

  • Update I've updated this answer to provide a complete & self-contained example of saving the check marks of the TcxTreeList to a string (or TStringList) and then re-load them, both using the string format in the Q's screenshot. I've ignored the code in the Q and written it all from scratch, because that was easier than trying to guess what exactly you are intending to do in your code - if I was doing this myself, I wouldn't use the Q's method but instead save the tree nodes' states to a TClientDataSet or the Devex equivalent.

    The example requires only a few utility routines to do its job and these all take a TcxTreeList or TcxTreeListNode as an input parameter so these could be moved to another unit and re-used by other forms.

    These routines are as follows:

    function RootNodeToString(RootNode : TcxTreeListNode) : String;
    //  This saves a Root node and its subkeys in the format show in the Q's screenshot
    //  Note:  This does NOT save the RootNode's checked state because the q did not define
    //  whether it should
    
    function TreeListNodesToString(TreeList : TcxTreeList) : String;
    //  This saves all the TreeList's Root nodes and their subkeys
    //  in the format show in the Q's screenshot
    
    function RootNodeFromName(TreeList : TcxTreeList; AName : String) : TcxTreeListNode;
    //  Finds the RootNode having a given name or NIL if not found
    
    function ChildNodeFromName(RootNode : TcxTreeListNode; const AName : String) : TcxTreeListNode;
    //  Finds the ChildNode (of a RootNode) having a given name or NIL if not found
    
    function TreeListNodesToString(TreeList : TcxTreeList) : String;
    //  This saves all the TreeList's Root nodes and their subkeys
    //  in the format show in the Q's screenshot
    
    function RootNodeFromName(TreeList : TcxTreeList; AName : String) : TcxTreeListNode;
    //  Finds the RootNode having a given name or NIL if not found
    
    function ChildNodeFromName(RootNode : TcxTreeListNode; const AName : String) : TcxTreeListNode;
    //  Finds the ChildNode (of a RootNode) having a given name or NIL if not found
    
    procedure ClearChecks(TreeList : TcxTreeList; ClearChildren : Boolean);
    //  Clears all the checkmark in a cxTreeList
    

    Hopefully, these are all self-explanatory. The Implementation section of the example is

    const
      iCheckCol = 0;  //  the number of the checkbox column
      iNameCol  = 1;  //  the number of the name column
    
    function RootNodeToString(RootNode : TcxTreeListNode) : String;
    //  This saves a Root node and its subkeys in the format show in the Q's screenshot
    //  Note:  This does NOT save the RootNode's checked state because the q did not define
    //  whether it should
    var
      j : Integer;
      ANode : TcxTreeListNode;
    begin
      Result := '[' + RootNode.Values[iNameCol] + ']';
      for j := 0 to RootNode.Count - 1 do begin
         ANode := RootNode.Items[j];
         if ANode.Values[iCheckCol] then
           Result := Result + ',' + ANode.Values[iNameCol];
      end;
    end;
    
    function TreeListNodesToString(TreeList : TcxTreeList) : String;
    //  This saves all the TreeList's Root nodes and their subkeys
    //  in the format show in the Q's screenshot
    var
      i : Integer;
    begin
      Result := '';
      for i := 0 to TreeList.Count - 1 do begin
        if Result <> '' then
          Result := Result + ',';
        Result := Result + RootNodeToString(TreeList.Items[i]);
      end;
    end;
    
    function RootNodeFromName(TreeList : TcxTreeList; AName : String) : TcxTreeListNode;
    //  Finds the RootNode having a given name or NIL if not found
    var
      i : Integer;
    begin
      //  First remove the square brackets, if any
      if AName[1] = '[' then
        Delete(AName, 1, 1);
      if AName[Length(AName)] = ']' then
        Delete(AName, Length(AName), 1);
      //  Next, look for AName in TreeList
      for i := 0 to TreeList.Count - 1 do begin
        Result := TreeList.Items[i];
        if CompareText(Result.Values[iNameCol], AName) = 0 then exit; //CompareText is case-insensitive
      end;
      Result := Nil; // if we get to here,  we didn't find it
    end;
    
    function ChildNodeFromName(RootNode : TcxTreeListNode; const AName : String) : TcxTreeListNode;
    //  Finds the ChildNode (of a RootNode) having a given name or NIL if not found
    var
      i : Integer;
    begin
      for i := 0 to RootNode.Count - 1 do begin
        Result := RootNode.Items[i];
        if CompareText(Result.Values[iNameCol], AName) = 0 then exit; //CompareText is case-insensitive
      end;
      Result := Nil; // if we get to here,  we didn't find it
    end;
    
    procedure ClearChecks(TreeList : TcxTreeList; ClearChildren : Boolean);
    //  Clears all the checkmark in a cxTreeList
    var
      i,
      j : Integer;
      RootNode,
      ANode : TcxTreeListNode;
    begin
      //  This clears the checkmarks from all the Root nodes and, optionally,
      //  their children
      TreeList.BeginUpdate;
      try
        for i := 0 to TreeList.Count - 1 do begin
          RootNode := TreeList.Items[i];
          RootNode.Values[iCheckCol] := False;
          for j := 0 to RootNode.Count - 1 do begin
            ANode := RootNode.Items[j];
            ANode.Values[iCheckCol] := False;
          end;
        end;
      finally
        TreeList.EndUpdate;
      end;
    end;
    
    procedure LoadTreeListChecksFromString(TreeList : TcxTreeList; const Input : String);
    //  This clears the TreeList's checkmarks and then sets the checkmarks
    //  from the Input string.
    var
      RootKey,
      SubKey : String;
      RootNode,
      ChildNode : TcxTreeListNode;
      TL : TStringList;
      i : Integer;
    begin
      TreeList.BeginUpdate;
      try
        //  First, clear the treelist's checkmarks
        ClearChecks(TreeList, True);
    
        //  Next load the Input string into a TStringList to split it into a series
        //  of Root keys and Child keys
        TL := TStringList.Create;
        try
          TL.CommaText := Input;
    
          //  The i variable will be used to iterate through the contents of the  StringList
          i := 0;
          while i <= TL.Count - 1 do begin
            //  The first string in TL should be  Root key
            RootKey := TL[i];
            RootNode := RootNodeFromName(TreeList, RootKey);
            Assert(RootNode <> Nil);  // will raise exception if RootNode not found
            //  The question does not say what should happen about the checkmark on the root nodes
            Inc(i);
    
            //  Now, scan down the entries below the Root key and process retrive each if its sub-keys;
            //  stop when we get to the next Root key or reach the end of the Stringlist
            while (i <= TL.Count - 1) and (Pos('[', TL[i]) <> 1) do begin
              SubKey := TL[i];
              ChildNode := ChildNodeFromName(RootNode, SubKey);
              ChildNode.Values[iCheckCol] := True;
              Inc(i);
            end;
          end;
        finally
          TL.Free;
        end;
      finally
        TreeList.EndUpdate;
      end;
    end;
    
    procedure TForm1.SetUpTreeList;
    //  This sets up the form' cxTreeList with some Root nodes and Child nodes
    //  Some of the ChildNode's checkmarks are set to save having to click around
    //  to set things up manually
    var
      i,
      j : Integer;
      RootNode,
      ANode : TcxTreeListNode;
    begin
      for i := 0 to 3 do begin
        RootNode := cxTreeList1.Add;
        RootNode.AssignValues([Odd(i), 'RT' + IntToStr(i + 1)]);
        for j := 0 to 4 do begin
          ANode := RootNode.AddChild;
          ANode.AssignValues([Odd(i + j), Char(j + Ord('A'))]);
        end;
        RootNode.Expand(True);
      end;
      edSavedKeys.Text := TreeListNodesToString(cxTreeList1);
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      SetUpTreeList;
    end;
    
    procedure TForm1.btnClearClick(Sender: TObject);
    begin
      ClearChecks(cxTreeList1, True);
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    begin
      SetNodeChecked(cxTreeList1.FocusedNode, not cxTreeList1.FocusedNode.Values[iCheckCol]);
    end;
    
    procedure TForm1.SetNodeChecked(Node : TcxTreeListNode; Value : Boolean);
    begin
       if Node = Nil then exit;  // do nothing
       Node.Values[iCheckCol] := Value;
    end;
    
    procedure TForm1.btnLoadClick(Sender: TObject);
    begin
      ClearChecks(cxTreeList1, True);
      LoadTreeListChecksFromString(cxTreeList1, edSavedKeys.Text);
    end;
    
    end.
    

    Original answer

    The easiest way afaik to set the checkbox column of an unbound cxTreeList is simply to set the value in that column to True or False. So, assuming the CheckBox column of your cxTreeList is column 0, you can simply do this

    procedure TForm1.SetNodeChecked(Node : TcxTreeListNode; Value : Boolean);
    begin
       if Node = Nil then exit;  // do nothing
       Node.Values[0] := Value;
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    begin
      //  toggle the checkbox of the focused node using code
      SetNodeChecked(cxTreeList1.FocusedNode, not cxTreeList1.FocusedNode.Values[0]);
    end;
    

    I assume you can weave this into your existing code. I haven't really studied it, but suspect that you could simplify it quite a lot.