динамически создавать дерево всплывающих меню из таблицы sql server в Delphi
У меня есть такая таблица:
id parent_id name
1 1 Root
2 1 Car
3 1 Plane
4 2 BMW
5 4 CLK
Как я могу динамически создавать всплывающее меню со всеми подразделами в Delphi?
Вот как это должно выглядеть:
Изображение http://img217.imageshack.us/img217/5020/treees.jpg
5 ответов:
Предполагая, что корневой элемент имеет значение 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Вы также кэшируете в памяти созданные пункты меню:
var MI_by_id: TDictionary<integer, TMenuItem>;Прохождение через результаты будет выглядеть следующим образом
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), таким образом, начнет терять скорость очень быстро, как таблица растет.
- http://docwiki.embarcadero.com/Libraries/XE3/en/Vcl.Menus.TMenuItem.Add
- http://docwiki.embarcadero.com/CodeExamples/XE2/en/Generics.Collections.TDictionary_(Delphi)
Примечание: это также требует, чтобы для каждого некорневого элемента ID > ParentID (поставить ограничение проверки на таблицу)
1 <NULL> Root 8 <NULL> another root 7 1 Plane 3 4 BMW 4 7 CLK 5 8 CarЭто приведет к тому, что BMW будет привязан к create до того, как его Родительский CLK будет создан. Нарушение этих условий может быть преодолено немногими означает:
- рекурсивная загрузка:
select <items> where Parent_id is null, Затем для каждого из добавленных пунктов меню делаемselect <items> where Parent_id = :current_memuitem_idи так далее. Это похоже на работу VirtualTreeView- задать SQL-сервера для сортировки и придавить дерево - это, как правило, называют self-рекурсивной выборки SQL и сервера зависит.
- введем еще одну переменную коллекции-пункты меню без родителя. После каждого нового элемента, добавленного в меню, эту коллекцию следует искать, если есть ожидающие дети, чтобы извлечь из нее и переместить в вновь созданный родитель.
Слишком много решений для такой простой задачи. Жаль, что вы получили заказанные удостоверения личности, потому что без заказанных удостоверений было бы гораздо веселее. Вот мое собственное решение. На пустую форму бросьте кнопку, TClientDataSet и TPopupMenu. Сделайте форму PopupMenu = PopupMenu1, чтобы вы могли видеть результат. Добавьте это в Button1.OnClick:
Примечание: Янамеренно использую TClientDataSet, а не реальный запрос. Этот вопрос не о запросе, и это решение работает с любым Тдатасет потомка вы бросаете на него. Просто убедитесь, что результирующий набор упорядочен по
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;
Попробуйте это
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
Это решение требует, чтобы 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;
Интересную головоломку ...еще одна поздняя ночная мысль, практический ответ для повторного использования:)
Сделать производный компонент:
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 и поместить его на палитру:)
Comments