[英]dynamically create popup menu tree from sql server table in Delphi
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:
可以通过以下手段克服对这些条件的违反:
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 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.