динамически создавать дерево всплывающих меню из таблицы 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

872   5  

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), таким образом, начнет терять скорость очень быстро, как таблица растет.

Примечание: это также требует, чтобы для каждого некорневого элемента 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

    Ничего не найдено.