hoffstadt / DearPyGui

Dear PyGui: A fast and powerful Graphical User Interface Toolkit for Python with minimal dependencies
https://dearpygui.readthedocs.io/en/latest/
MIT License
12.94k stars 676 forks source link

Tree View with Table in DearPyGui #2379

Open Umbrella167 opened 3 weeks ago

Umbrella167 commented 3 weeks ago

I would like to request the addition of a feature in DearPyGui that allows the creation of a tree view inside a table. This feature is inspired by ImGui's capability to create such a layout. Below is a reference implementation using ImGui:

cpp

复制 if (ImGui::TreeNode("Tree view")) { static ImGuiTableFlags flags = ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody;

if (ImGui::BeginTable("3ways", 3, flags))
{
    ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_NoHide);
    ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f);
    ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 18.0f);
    ImGui::TableHeadersRow();

    struct MyTreeNode
    {
        const char*     Name;
        const char*     Type;
        int             Size;
        int             ChildIdx;
        int             ChildCount;
        static void DisplayNode(const MyTreeNode* node, const MyTreeNode* all_nodes)
        {
            ImGui::TableNextRow();
            ImGui::TableNextColumn();
            const bool is_folder = (node->ChildCount > 0);
            if (is_folder)
            {
                bool open = ImGui::TreeNodeEx(node->Name, ImGuiTreeNodeFlags_SpanFullWidth);
                ImGui::TableNextColumn();
                ImGui::TextDisabled("--");
                ImGui::TableNextColumn();
                ImGui::TextUnformatted(node->Type);
                if (open)
                {
                    for (int child_n = 0; child_n < node->ChildCount; child_n++)
                        DisplayNode(&all_nodes[node->ChildIdx + child_n], all_nodes);
                    ImGui::TreePop();
                }
            }
            else
            {
                ImGui::TreeNodeEx(node->Name, ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_SpanFullWidth);
                ImGui::TableNextColumn();
                ImGui::Text("%d", node->Size);
                ImGui::TableNextColumn();
                ImGui::TextUnformatted(node->Type);
            }
        }
    };
    static const MyTreeNode nodes[] =
    {
        { "Root",                         "Folder",       -1,       1, 3    }, // 0
        { "Music",                        "Folder",       -1,       4, 2    }, // 1
        { "Textures",                     "Folder",       -1,       6, 3    }, // 2
        { "desktop.ini",                  "System file",  1024,    -1,-1    }, // 3
        { "File1_a.wav",                  "Audio file",   123000,  -1,-1    }, // 4
        { "File1_b.wav",                  "Audio file",   456000,  -1,-1    }, // 5
        { "Image001.png",                 "Image file",   203128,  -1,-1    }, // 6
        { "Copy of Image001.png",         "Image file",   203256,  -1,-1    }, // 7
        { "Copy of Image001 (Final2).png","Image file",   203512,  -1,-1    }, // 8
    };

    MyTreeNode::DisplayNode(&nodes[0], nodes);

    ImGui::EndTable();
}
ImGui::TreePop();

} Benefits Enhanced UI Capability: This feature would allow users to create more complex and organized interfaces, especially useful for applications dealing with hierarchical data. Consistency with ImGui: Users familiar with ImGui's functionality would have an easier transition and more powerful tools at their disposal. Implementation Notes The feature should support various table flags such as resizable columns, row backgrounds, and customizable column widths. It should allow the nesting of tree nodes within table cells, maintaining the hierarchical structure. Thank you for considering this feature request. Implementing this would greatly enhance the versatility and usability of DearPyGui for many developers.

v-ein commented 3 weeks ago

Hello @Umbrella167 - have you tried to create such a layout using existing DPG widgets? If it doesn't work for you, could you please describe what goes wrong?

Umbrella167 commented 3 weeks ago

1 2

@v-ein Thank you for your response.

These two images illustrate the functionality I am trying to achieve. Specifically, it can be found in:

dpg.show_imgui_demo() --> Tables & Columns --> Tree view

I attempted to nest a tree node within a table; however, I was unable to achieve the desired effect.

Could you please advise on how to accomplish this using the existing DPG widgets, or if there's another approach I should consider?

Thank you for your assistance.

v-ein commented 3 weeks ago

Well, I must admit that it's not trivial but here is a workaround.

The problem with existing widgets is that DPG implements table rows as containers, whereas in ImGui it all works a bit differently... In DPG, you can't put a tree structure in different table rows because that would mean overlapping containers in the code (a tree node is a container, and a table row is a container too). So the workaround simulates the tree by explicitly showing and hiding the rows whenever a node is expanded or collapsed.

One more problem is that in the current version, the toggled_open handler does not fire when the tree node is collapsed, and there's no easy way to detect the collapse event. Therefore I used a selectable to handle this stuff. As a bonus, it highlights the entire row and captures mouse clicks on the entire row (whereas tree_node would only work within a single column). tree_node is still used to render the node the same way a regular tree node (not inserted into a table) would look.

#!/usr/local/bin/python3

from contextlib import contextmanager
from typing import Generator, Union
import dearpygui.dearpygui as dpg

dpg.create_context()
dpg.setup_dearpygui()
dpg.create_viewport(title="Test", width=500, height=300)

INDENT_STEP = 14    # actually depends on font size

def on_row_clicked(sender, value, user_data):
    # Make sure it happens quickly and without flickering
    with dpg.mutex():
        # We don't want to highlight the selectable as "selected"
        dpg.set_value(sender, False)

        table, row = user_data
        root_level, node = dpg.get_item_user_data(row)

        # First of all let's toggle the node's "expanded" status
        is_expanded = not dpg.get_value(node)
        dpg.set_value(node, is_expanded)
        # All children *beyond* this level (but not on this level) will be hidden
        hide_level = 10000 if is_expanded else root_level

        # Now manage the visibility of all the children as necessary
        rows = dpg.get_item_children(table, slot=1)
        root_idx = rows.index(row)
        # We don't want to look at rows preceding our current "root" node
        rows = rows[root_idx + 1:]
        for child_row in rows:
            child_level, child_node = dpg.get_item_user_data(child_row)
            if child_level <= root_level:
                break

            if child_level > hide_level:
                dpg.hide_item(child_row)
            else:
                dpg.show_item(child_row)
                hide_level = 10000 if dpg.get_value(child_node) else child_level

@contextmanager
def table_tree_node(*cells: str, leaf: bool = False) -> Generator[Union[int, str] , None, None]:
    table = dpg.top_container_stack()
    cur_level = dpg.get_item_user_data(table) or 0

    node = dpg.generate_uuid()

    with dpg.table_row(user_data=(cur_level, node)) as row:
        with dpg.group(horizontal=True, horizontal_spacing=0):
            dpg.add_selectable(span_columns=True, callback=on_row_clicked, user_data=(table, row))
            dpg.add_tree_node(
                    tag=node,
                    label=cells[0],
                    indent=cur_level*INDENT_STEP,
                    leaf=leaf,
                    bullet=leaf,
                    default_open=True)

        for label in cells[1:]:
            dpg.add_text(label)

    try:
        dpg.set_item_user_data(table, cur_level + 1)
        yield node
    finally:
        dpg.set_item_user_data(table, cur_level)

def add_table_tree_leaf(*cells: str) -> Union[int, str]:
    with table_tree_node(*cells, leaf=True) as node:
        pass
    return node

with dpg.window():
    dpg.set_primary_window(dpg.last_item(), True)

    # We need to adjust padding so that widgets of different types are positioned properly
    with dpg.theme() as table_theme:
        with dpg.theme_component(dpg.mvAll):
            # Frame padding affects vertical positioning of add_text items within the table
            dpg.add_theme_style(dpg.mvStyleVar_FramePadding, 4, 0)

    with dpg.table(
            borders_outerV=True,
            borders_outerH=True,
            row_background=True,
            policy=dpg.mvTable_SizingFixedFit):

        dpg.bind_item_theme(dpg.last_item(), table_theme)

        dpg.add_table_column(label="Name", width_stretch=True)
        dpg.add_table_column(label="Size")
        dpg.add_table_column(label="Type")

        with table_tree_node("Root", "--", "Folder"):
            with table_tree_node("Music", "--", "Folder"):
                add_table_tree_leaf("File1_a.wav", "123000", "Audio file")
                add_table_tree_leaf("File1_b.wav", "456000", "Audio file")
                with table_tree_node("My favourite", "--", "Folder"):
                    add_table_tree_leaf("Some pop.wav", "456000", "Audio file")
            with table_tree_node("Textures", "--", "Folder"):
                add_table_tree_leaf("Image001.png", "203128", "Image file")
                add_table_tree_leaf("Copy of Image001.png", "203256", "Image file")
                add_table_tree_leaf("Copy of Image001 (Final2).png", "203512", "Image file")
            add_table_tree_leaf("desktop.ini", "1024", "System file")

dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()
Umbrella167 commented 3 weeks ago

@v-ein Thank you so much for your detailed explanation and the workaround you provided! Your method is ingenious and exactly what I needed. This was a huge help to me. Thanks again for your patience and assistance!