【问题标题】:dynamically create popup menu tree from sql server table in Delphi在 Delphi 中从 sql server 表动态创建弹出菜单树
【发布时间】:2013-01-16 10:38:51
【问题描述】:

我有一张这样的桌子:

id     parent_id     name
1          1         Root
2          1         Car
3          1         Plane
4          2         BMW
5          4         CLK

如何在 Delphi 中动态创建包含所有子项的弹出菜单?

它应该是这样的:

【问题讨论】:

  • 我想我应该你递归,但我不知道如何
  • 您对查询表或创建菜单项有什么问题?
  • ID 是有序的吗? parent_id 是否总是大于(或等于)ID?因为如果他们不是,它会变得有点复杂。
  • 1 1 Root - 非常错误的行。它被称为“无限递归”根元素应该没有父元素!并且意味着像 NULL 或 ZERO 之类的值
  • @Arioch'关于如何表示无根节点的约定只是一个约定。没什么大不了的。

标签: sql delphi menu tree delphi-xe2


【解决方案1】:

假设根元素的 Parent_ID 为 NULL,您可以发出请求

 Select ID, Parent_ID, Name from all_my_menus 
   order by Parent_ID nulls first, ID 
   where Menu_ID = :MenuIDParameter

1   <NULL>    Root
8   <NULL>    another root
2        1    Car
4        1    Plane
3        2    BMW
5        4    CLK

您还可以缓存在内存中创建的菜单项:var MI_by_id: TDictionary&lt;integer, TMenuItem&gt;;

遍历结果看起来像

var MI: TMenuItem;
    MI_by_id: TDictionary<integer, TMenuItem>;
begin 
  MI_by_id := TDictionary<integer, TMenuItem>.Create;
  try
    While not Query.EOF do begin
        MI := TMenuItem.Create(Self);
        MI.Caption := Query.Fields[2].AsString;
        MI.Tag := Query.Fields[0].AsInteger; // ID, would be helpful for OnClick event
        MI.OnClick := ...some click handler

        if Query.Fields[1].IsNull {no parent}
           then MainMenu.Items.Add(MI)
           else MI_by_id.Items[Query.Fields[1].AsInteger].Add(MI);

        MI_by_id.Add(MI.Tag, MI); //save shortcut to potential parent for future searching
        Query.Next;
    end;
  finally 
    MI_by_id.Free;
  end;
end;

实际上,由于我们在查询中对 Parent_ID 进行了排序,因此给定父级的所有子级都会生成单个连续列表,因此在填充最后一个子级之后(即在 parent_ID 获得新值之后)从字典中删除填充的父级可能会更好并将先前找到的父级缓存在另一个局部变量中(而不是通过字典进行另一次搜索)。 然而,以人为本的菜单的合理大小应该不值得这样做。但是您必须了解这种方法很可能会随着表的增长而扩展为 O(n*n),因此速度会非常快。

注意:这也要求对于每个非根元素 ID > ParentID(将 CHECK CONSTRAINT 放在表上)

1   <NULL>    Root
8   <NULL>    another root
7        1    Plane
3        4    BMW
4        7    CLK
5        8    Car

这会导致 BMW 在其父 CLK 创建之前绑定创建。 可以通过几种方式克服违反该条件的情况:

  • 递归加载:select &lt;items&gt; where Parent_id is null,然后为每个添加的菜单项执行select &lt;items&gt; where Parent_id = :current_memuitem_id,依此类推。这就像 VirtualTreeView 可以工作
  • 要求 SQL 服务器对树进行排序和展平 - 这通常称为自递归 SQL 选择,并且取决于服务器。
  • 再引入一个集合变量 - 无父菜单项。在将每个新项目添加到菜单后,应搜索此集合是否有待处理的子项从中提取并移至新创建的父项。

【讨论】:

  • +1;该算法(与我的示例中的算法相同)是 O(n),而不是 O(n*n):查询字典是 O(1),并且您只需对结果数据集中的每条记录进行一次准确的检查。
  • 我已经问过 OP 是否ID &lt;= PARENT_ID,他说是的。通过ID 订购,如果它有父母,你已经看过了,保证。如果该条件被取消,那么它会变得有点混乱,因为没有order by 可以保证您总是在孩子之前看到父母。如果没有这个条件,也有可能出现循环。
  • @Cosmin 依赖于 TDictionary 实现。如果它已排序 - 那么新项目插入将花费 O(n)。如果未排序,则搜索将花费 O(N)。据我所知,Delphi 没有 FiongerTrees :-)
  • @Cosmin 我更喜欢让每个给定父母的所有孩子都放在一个单独的部分中,这提供了优化“如果 Parent_ID 字段发生变化,只向父母询问字典”,但这留作作业对于主题启动器:-)
  • Delphi 的 TDictionary,就像我所知道的任何语言的任何字典一样,是一个 HASH TABLE。它不是按设计排序的!或者,如果您愿意,它会根据您添加到其中的项目的哈希码进行排序。这就是我喜欢它的原因,这就是为什么它是 O(1)
【解决方案2】:

对于这样一个简单的问题有太多的解决方案。太糟糕了,你得到了订购的 ID,因为没有订购的 ID,事情会更有趣。这是我自己的解决方案。在一个空窗体上放置一个按钮、一个 TClientDataSet 和一个 TPopupMenu。使表单的 PopupMenu = PopupMenu1 可以看到结果。将此添加到 Button1.OnClick:

注意:我有意使用 TClientDataSet 而不是真正的查询。这个问题与查询无关,这个解决方案适用于你扔给它的任何 TDataSet 后代。只需确保结果集按id 排序,否则您可以在父节点之前看到子节点。另请注意,一半的代码用于用问题中的示例数据填充 ClientDataSet!

procedure TForm16.Button1Click(Sender: TObject);
var Prev: TDictionary<Integer, TMenuItem>; // We will use this to keep track of previously generated nodes so we do not need to search for them
    CurrentItem, ParentItem: TMenuItem;
begin
  if not ClientDataSet1.Active then
  begin
    // Prepare the ClientDataSet1 structure
    ClientDataSet1.FieldDefs.Add('id', ftInteger);
    ClientDataSet1.FieldDefs.Add('parent_id', ftInteger);
    ClientDataSet1.FieldDefs.Add('name', ftString, 100);

    ClientDataSet1.CreateDataSet;

    // Fill the dataset
    ClientDataSet1.AppendRecord([1, 1, 'Root']);
    ClientDataSet1.AppendRecord([2, 1, 'Car']);
    ClientDataSet1.AppendRecord([3, 1, 'Plane']);
    ClientDataSet1.AppendRecord([4, 2, 'BMW']);
    ClientDataSet1.AppendRecord([5, 4, 'CLK']);
  end;

  // Clear the existing menu
  PopupMenu1.Items.Clear;

  // Prepare the loop
  Prev := TDictionary<Integer, TMenuItem>.Create;
  try
    ClientDataSet1.First; // Not required for a true SQL Query, only required here for re-entry
    while not ClientDataSet1.Eof do
    begin
      CurrentItem := TMenuItem.Create(Self);
      CurrentItem.Caption := ClientDataSet1['name'];

      if (not ClientDataSet1.FieldByName('parent_id').IsNull) and Prev.TryGetValue(ClientDataSet1['parent_id'], ParentItem) then
        ParentItem.Add(CurrentItem)
      else
        PopupMenu1.Items.Add(CurrentItem);

      // Put the current Item in the dictionary for future reference
      Prev.Add(ClientDataSet1['id'], CurrentItem);

      ClientDataSet1.Next;
    end;
  finally Prev.Free;
  end;
end;

【讨论】:

  • 据我所知,您可以制作克隆 CDS 而不是制作 TDictionary。并且在同一个数据集上有两个独立的游标非常有效。顺便说一句,内存 CDS 是否构建搜索索引?如果是这样,那么搜索父级可能需要 O(log n) 而不是 O(n)
  • TDictionary&lt;T&gt;,在Genercis.Collections 中实现是一个哈希表:查询它是一个 O(1) 操作,并且每次都优于 O(log n)。我们的工作集是整个数据集(表格)。我们知道它包含n 记录。由于我们需要至少查看每条记录一次,因此时间最少为O(n)。我们在循环中所做的事情与n 相乘:我的算法是O(n * 1) = O(n),而“快速”O(log n) 搜索将使我们获得O(n*log n)
  • 我们知道它包含 n 条记录 - 也许,在您将所有查询缓存到 CDS 之后,您就知道了。您的代码中的 Vut TDictionary 本身几乎不知道这一点。你不加热它。您也不保证唯一的哈希值。
  • @Arioch'The,我不会和你争论 TDictionary&lt;T&gt; 的优点,也不会讨论 big-O 符号如何使用 n
【解决方案3】:

试试这个

procedure TForm1.MyPopup(Sender: TObject);
begin
  with Sender as TMenuItem do ShowMessage(Caption);
end;

procedure TForm1.Button1Click(Sender: TObject);
var 
  MyItem,MySubItem1: TMenuItem;
begin
  Inc(Num);
  MyItem:=TMenuItem.Create(Self);
  MySubItem1:=TMenuItem.Create(Self);

  MyItem.Caption:='Hello'+IntToStr(Num);
  MySubItem1.Caption:='Good Bye'+IntToStr(Num);

  MainMenu1.Items.Add(MyItem);
  MainMenu1.Items[0].Insert(num-1,MySubItem1);

  MyItem.OnClick:=MyPopUp;
  MySubItem1.OnClick:=MyPopUp;
end;

取自http://www.greatis.com/delphicb/tips/lib/components-addmenuitem.html

【讨论】:

  • 抱歉,这行不通。我需要从我的 SQL 表中加载它
  • @MaxatUtepbergenov:因此,使用示例代码创建一个函数,并在循环中调用该函数,循环遍历表中的记录。
【解决方案4】:

此方案要求root的parent_id为0,测试用

Select 1 as ID,          0 as Parent_ID,         'Root' as Name
union
Select 2,          1,        ' Car'
union
Select 3 ,         1,         'Plane'
union
Select 4,          2,        'BMW'
union
Select 5,          4,         'CLK'

应该通过优化,只是时间不够……

Function GetMenu(pop:TPopupmenu;ID:Integer):TMenuItem;
var
 i:Integer;
 Function CheckItem(mi:TMenuItem):TMenuItem;
    var
     i:Integer;
    begin
      Result := nil;
      if mi.Name = 'DYN_' + INtToStr(ID) then Result := mi
      else  for i := 0 to mi.Count-1 do
        if not Assigned(Result) then Result := CheckItem(mi[i]);
    end;
begin
  Result := nil;
  for i := 0 to pop.Items.Count-1 do
    begin
      if not Assigned(Result) then Result := CheckItem(pop.Items[i]);
      if Assigned(Result) then Break;
    end;
end;


Function InsertMenuItem(pop:TPopupMenu;mi:TMenuItem;ID:Integer;Const caption:String):TMenuItem;
begin
    Result := TMenuItem.Create(pop);
    Result.Caption := caption;
    Result.Name := 'DYN_' + INtToStr(ID) ;
    if not Assigned(mi) then pop.Items.Add(Result) else mi.Add(Result);

end;

Function AddMenuItem(pop:TPopupmenu;ID:Integer;Ads:TDataset):TMenuItem;
begin
  Ads.Locate('ID',ID,[]);
  Result := GetMenu(pop,id);
  if (not Assigned(Result))   then
    begin
     if  (Ads.FieldByName('parent_ID').AsInteger<>0) then
       begin
        result := AddMenuItem(pop,Ads.FieldByName('parent_ID').AsInteger,Ads);
        Ads.Locate('ID',ID,[]);
       end;
     Result := InsertMenuItem(pop,Result,ID,Ads.FieldByName('Name').AsString);
    end;
  Ads.Locate('ID',ID,[]);
end;

procedure TForm1.Button1Click(Sender: TObject);

begin
   while not ADS.Eof do
      begin
        AddMenuItem(Popupmenu1,ads.FieldByName('ID').AsInteger,Ads);
        Ads.Next
      end;
end;

【讨论】:

    【解决方案5】:

    有趣的难题......另一个深夜的想法,重复使用的实用答案:)

    制作派生组件:

    type
      TCascadeMenuItem = class(TMenuItem)
      private
        Id: Integer;
      public
        function AddItem(const ToId, WithId: Integer; AName: string): Boolean;
      end;
    

    有代码

    function TCascadeMenuItem.AddItem(const ToId, WithId: Integer; AName: string): Boolean;
    var
      i: Integer;
      cmi: TCascadeMenuItem;
    begin
      if ToId = Id then
      begin
        cmi := TCascadeMenuItem.Create(Owner);
        cmi.Caption := AName;
        cmi.Id := WithId;
        Add(cmi);
        Result := True;
      end
      else begin
        i := 0;
        Result := False;
        while (i < Count) and (not Result) do
        begin
          Result := TCascadeMenuItem(Items[i]).AddItem(ToId,WithId, ANAme);
          inc(i);
        end;
      end;
    

    结束;

    主表格,假设您的数据:

    procedure TForm4.Button2Click(Sender: TObject);
    var
      mi: TCascadeMenuItem;
      i: Integer;
      Added: Boolean;
    begin
        cds1.First;
        while not cds1.Eof do
        begin
          i := 0;
          Added := False;
          while (i < pup.Items.Count) and (not Added) do
          begin
            Added := TCascadeMenuItem(pup.Items[i]).AddItem(cds1Parent_Id.AsInteger, cds1id.AsInteger, cds1name.AsString);
            inc(i);
          end;
          if not Added then
          begin  // new root
            mi := TCasCadeMenuItem.Create(Self);
            mi.Caption := cds1name.AsString;
            mi.id := cds1Parent_Id.AsInteger;
            pup.Items.Add(mi);
          end;
          cds1.Next;
        end;
    end;
    

    您可以派生一个 TCascasePopupMenu 并将其放在调色板上:)

    【讨论】:

    • 不错的方法,但您应该考虑 root parent_id=0 或 null 的情况
    • @kobik 是的,但是 OP 说“这是我的数据”并且这个答案使用它 - 而不是强加一个新条件。如果 Parent 为零,则将其更改为使用新根几乎是微不足道的。
    猜你喜欢
    • 2016-03-16
    • 2012-09-01
    • 2018-11-18
    • 1970-01-01
    • 1970-01-01
    • 2015-02-26
    • 2023-03-23
    • 2018-10-07
    相关资源
    最近更新 更多