简体   繁体   English

从 Delphi 中的 sql 服务器表动态创建弹出菜单树

[英]dynamically create popup menu tree from sql server table in Delphi

I have a table like this:我有一张这样的桌子:

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

How can I dynamically create popup menu with all subitems in Delphi?如何动态创建包含 Delphi 中所有子项的弹出菜单?

This is how it should look like:它应该是这样的:

图片

Too many solutions for such a simple problem. 针对这样一个简单问题的解决方案太多了。 Too bad you got ordered ID's because without ordered ID's things would have been more fun. 太糟糕了,你订购了ID,因为没有订购ID的东西会更有趣。 Here's my own solution. 这是我自己的解决方案。 On an empty form drop a button, a TClientDataSet and a TPopupMenu. 在空表单上放置一个按钮,一个TClientDataSet和一个TPopupMenu。 Make the form's PopupMenu = PopupMenu1 so you can see the result. 使表单的PopupMenu = PopupMenu1,以便您可以看到结果。 Add this to Button1.OnClick: 将其添加到Button1.OnClick:

Note: I'm intentionally using TClientDataSet and not a real Query. 注意:我故意使用TClientDataSet而不是真正的Query。 This question is not about the query and this solution works with whatever TDataSet descendant you throw at it. 这个问题与查询无关,此解决方案适用于您抛出的任何TDataSet后代。 Just make sure the result set is ordered on id , or else you could see the child nodes before the parents. 只需确保结果集按id排序,否则您可以在父项之前看到子节点。 Also note, half the code is used to fill up the ClientDataSet with the sample data in the question! 另请注意,一半的代码用于填充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;

Assuming root element has NULL as Parent_ID you can issue the request 假设根元素具有NULL作为Parent_ID,您可以发出请求

 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

You would also cache in-memory created menu items: var MI_by_id: TDictionary<integer, TMenuItem>; 您还可以缓存内存中创建的菜单项: var MI_by_id: TDictionary<integer, TMenuItem>;

The traversing through the results would look like 遍历结果看起来像

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;

Actually, since we made sort upon Parent_ID on the query, all the children for given parent make single continuous list, so could be better to remove populated parents from the dictionary after we populated last child (ie after parent_ID got new value) and caching previously found parent otherwise in another local variable (instead of making yet another search through the dictionary). 实际上,由于我们在查询上对Parent_ID进行了排序,所以给定父级的所有子节点都会生成单个连续列表,因此最好在我们填充最后一个子节点之后(即在parent_ID获取新值之后)从字典中删除已填充的父节点在另一个局部变量中找到父项(而不是通过字典进行另一次搜索)。 However reasonable size for human-targeted menu should be much less to worth this. 然而,以人为目标菜单的合理尺寸应该远不如此值得。 But you have to understand this approach most probably scales as O(n*n) thus would start loose speed very fast as the table grows. 但是你必须要理解这种方法最有可能随着O(n * n)而扩展,因此随着表的增长,它将以非常快的速度开始。

Note: this also requires that for every non-root element ID > ParentID (put CHECK CONSTRAINT on the table) 注意:这还要求每个非根元素ID> ParentID(在表上放置CHECK CONSTRAINT)

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

This would lead to BMW tied to create before its parent CLK created. 这将导致宝马在其母公司CLK创建之前与其联系。 Violation for that conditions can be overcome by few means: 可以通过以下手段克服对这些条件的违反:

  • recursive load: select <items> where Parent_id is null , then for each of the added menu items do select <items> where Parent_id = :current_memuitem_id and so on that. 递归加载: select <items> where Parent_id is null ,然后为每个添加的菜单项select <items> where Parent_id = :current_memuitem_id ,依此类推。 This is like VirtualTreeView would work 这就像VirtualTreeView一样
  • ask SQL server to sort and flatten the tree - this is usually called self-recursive SQL selection and is server-dependant. 要求SQL服务器对树进行排序和展平 - 这通常称为自递归SQL选择,并且与服务器相关。
  • introduce one more collection variable - menu items w/o parent. 再引入一个集合变量 - 没有父项的菜单项。 After each new item added to the menu this collection should be searched if there are pending children to extract from it and move into the newly created parent. 在每个新项目添加到菜单后,如果有待处理的子项从中提取并移动到新创建的父项中,则应搜索此集合。

Try this 尝试这个

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;

Taken from http://www.greatis.com/delphicb/tips/lib/components-addmenuitem.html 摘自http://www.greatis.com/delphicb/tips/lib/components-addmenuitem.html

This solution requires parent_id of root to be 0, tested with 此解决方案要求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'

should by optimized, have just a lack of time ... 应该优化,只是缺乏时间......

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;

Interesting conundrum ...another late night thought, a practical answer for re-use :) 有趣的难题......另一个深夜的想法,重用的实用答案:)

Make a derived component: 制作派生组件:

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

with code 用代码

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;

end; 结束;

Main form, Assumes your data: 主表格,假设您的数据:

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;

You could derive a TCascasePopupMenu and put it on the palette :) 您可以派生一个TCascasePopupMenu并将其放在调色板上:)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM