pthom / hello_imgui

Hello, Dear ImGui: unleash your creativity in app development and prototyping
https://pthom.github.io/hello_imgui
MIT License
678 stars 103 forks source link

creating multiple windows without docking #32

Closed dcnieho closed 1 year ago

dcnieho commented 1 year ago

I like the convenience hello_imgui provides as well as its extra features (autosize, etc).

I needed more than one window in my program, so i tried to find out how to do that with hello_imgui. Turns out i can (kinda ab-)use the docking support for that. See lines 140-164 in the diff of _image_gui.py here https://github.com/dcnieho/glassesValidator/commit/698223344658e6402772ef01aa6723e55f6b5e07. That gives me nice windows with native decorations for additional windows. At least on Windows, didn't test further yet.

I did however lose a lot of the hello_imgui features in the process for the additional windows: e.g. autosizing and such only works for the main window. And the fact that just about all in the docking support can be used without docking, makes it seem like docking is a bit of a misnomer. It suggests a (possibly significant) API change. I haven't fully thought this through:

pthom commented 1 year ago

Hi Dee,

I need some more time to study this

dcnieho commented 1 year ago

Thanks and no worries. Happy to discuss

pthom commented 1 year ago

Hi Dee,

Initially, DockingParams was created only for dockable windows, since the ImGui API for the initial positioning and docking of windows is a bit awkward. I had not envisioned a use outside of it, and I am still not sure it is that useful.

Anyway, it seems like you were confused by DockableWindow::windowSizeCondition which you set to ImGuiCond_Always. This has an effect only when you did set a manual size for the window.

See src/hello_imgui/internal/docking_details.cpp:

void ShowDockableWindows(std::vector<DockableWindow>& dockableWindows)
{
   // ...
    for (auto& dockableWindow: dockableWindows)
    { //...     
                if (dockableWindow.windowSize.x > 0.f)
                    ImGui::SetNextWindowSize(dockableWindow.windowSize, dockableWindow.windowSizeCondition);

You can use ImGuiWindowFlags_AlwaysAutoResize instead.

Since HelloImGui is a C++ project, I always prefer to use C++ example code. Here is an example in C++ that always auto resizes the window:

#include "hello_imgui/hello_imgui.h"

int main()
{
    auto gui1 = []()
    {
        static char msg[2048] = "Hello";
        static ImVec2 size(300.f, 300.f);

        ImGui::Text("Gui1");
        ImGui::SetNextItemWidth(100.f); ImGui::SliderFloat("size.x", &size.x, 10.f, 600.f);
        ImGui::SetNextItemWidth(100.f); ImGui::SliderFloat("size.y", &size.y, 10.f, 600.f);
        ImGui::InputTextMultiline("Text", msg, 2048, size);
    };

    HelloImGui::DockableWindow w1("gui1", "", gui1);
    w1.imGuiWindowFlags |= ImGuiWindowFlags_AlwaysAutoResize;

    HelloImGui::RunnerParams params;
    params.imGuiWindowParams.showMenuBar = true;
    params.imGuiWindowParams.enableViewports = true;
    params.dockingParams.dockableWindows = {w1};

    HelloImGui::Run(params);
}

Now, my next question is: is it worthwile renaming DockableWindow to Window and to make it more generic. I'm not sure. Let's see this on a python sample with two flavours.

Flavour 1: ab-using hello_imgui dockable windows

image
from typing import List
from imgui_bundle import imgui, hello_imgui, immapp, ImVec2

class MySatelliteWindow:
    text_size = ImVec2
    text = "Hello"

    def __init__(self):
        self.text_size = ImVec2(300, 300)

    def gui(self):
        imgui.push_id(str(id(self)))
        imgui.text(f"Satellite {id(self)}")
        imgui.set_next_item_width(100);
        _, self.text_size.x = imgui.slider_float("size.x", self.text_size.x, 10, 600)
        imgui.set_next_item_width(100);
        _, self.text_size.y = imgui.slider_float("size.y", self.text_size.y, 10, 600)

        _, self.text = imgui.input_text_multiline("Text", self.text, self.text_size)
        imgui.pop_id()

def main():
    params = hello_imgui.RunnerParams()
    params.imgui_window_params.enable_viewports = True
    # Add HelloImGui menu bar, with the View menu
    params.imgui_window_params.show_menu_bar = True

    satellite_windows: List[MySatelliteWindow] = []

    def gui():
        nonlocal params
        if imgui.button("add satellite window"):
            satellite_window = MySatelliteWindow()
            satellite_windows.append(satellite_window)

            dockable_window = hello_imgui.DockableWindow()
            dockable_window.imgui_window_flags = int(
                # imgui.WindowFlags_.no_move |
                # imgui.WindowFlags_.no_resize |
                imgui.WindowFlags_.no_collapse |
                # imgui.WindowFlags_.no_title_bar |
                # imgui.WindowFlags_.no_scrollbar |
                # imgui.WindowFlags_.no_scroll_with_mouse |

                imgui.WindowFlags_.always_auto_resize
            )
            dockable_window.label = f"satellite {len(params.docking_params.dockable_windows)}"
            dockable_window.gui_function = satellite_window.gui

            # (!) append will silently fail, because it is a bound std::vector...
            # params.docking_params.dockable_windows.append(dockable_window)
            # => Use `=` operator instead
            params.docking_params.dockable_windows = params.docking_params.dockable_windows + [dockable_window]

    params.callbacks.show_gui = gui
    hello_imgui.run(params)

if __name__ == "__main__":
    main()

Flavour 2: using standard imgui windows

from typing import List
from imgui_bundle import imgui, hello_imgui, immapp, ImVec2

class MySatelliteWindow:
    text_size = ImVec2
    text = "Hello"

    def __init__(self):
        self.text_size = ImVec2(300, 300)

    def gui(self):
        imgui.push_id(str(id(self)))
        imgui.text(f"Satellite {id(self)}")
        imgui.set_next_item_width(100);
        _, self.text_size.x = imgui.slider_float("size.x", self.text_size.x, 10, 600)
        imgui.set_next_item_width(100);
        _, self.text_size.y = imgui.slider_float("size.y", self.text_size.y, 10, 600)

        _, self.text = imgui.input_text_multiline("Text", self.text, self.text_size)
        imgui.pop_id()

def main():
    params = hello_imgui.RunnerParams()
    params.imgui_window_params.enable_viewports = True

    satellite_windows: List[MySatelliteWindow] = []

    def gui():
        nonlocal params
        if imgui.button("add satellite window"):
            satellite_window = MySatelliteWindow()
            satellite_windows.append(satellite_window)

        for i, satellite_window in enumerate(satellite_windows):
            window_flags = int(
                # imgui.WindowFlags_.no_move |
                # imgui.WindowFlags_.no_resize |
                imgui.WindowFlags_.no_collapse |
                # imgui.WindowFlags_.no_title_bar |
                # imgui.WindowFlags_.no_scrollbar |
                # imgui.WindowFlags_.no_scroll_with_mouse |
                imgui.WindowFlags_.always_auto_resize
            )
            imgui.begin(f"satellite {i}", None, window_flags)
            satellite_window.gui()
            imgui.end()

    params.callbacks.show_gui = gui
    hello_imgui.run(params)

if __name__ == "__main__":
    main()
dcnieho commented 1 year ago

Oh, thats nice! Indeed there is then no need to abuse dockable windows. I tried this before but it didn't work (I don't remember how exactly, just remember things going haywire...). There is only one small difference. I use OS decoration for my satellite windows, and these cannot be closed when using normal windows (mode 2 in the code below), but they can when abusing docking (mode 1):

from typing import List
from imgui_bundle import imgui, immapp, hello_imgui, immapp, ImVec2

class MySatelliteWindow:
    text_size = ImVec2
    text = "Hello"

    def __init__(self):
        self.text_size = ImVec2(300, 300)

    def gui(self):
        imgui.push_id(str(id(self)))
        imgui.text(f"Satellite {id(self)}")
        imgui.set_next_item_width(100);
        _, self.text_size.x = imgui.slider_float("size.x", self.text_size.x, 10, 600)
        imgui.set_next_item_width(100);
        _, self.text_size.y = imgui.slider_float("size.y", self.text_size.y, 10, 600)

        _, self.text = imgui.input_text_multiline("Text", self.text, self.text_size)
        imgui.pop_id()

def main():
    params = hello_imgui.RunnerParams()
    params.imgui_window_params.config_windows_move_from_title_bar_only = True
    params.imgui_window_params.enable_viewports = True

    satellite_windows: List[MySatelliteWindow] = []

    mode = 2
    def gui():
        nonlocal params
        if imgui.button("add satellite window"):
            satellite_window = MySatelliteWindow()
            satellite_windows.append(satellite_window)

            if mode==1:
                dockable_window = hello_imgui.DockableWindow()
                dockable_window.imgui_window_flags = int(
                    imgui.WindowFlags_.no_title_bar |
                    imgui.WindowFlags_.no_collapse |
                    imgui.WindowFlags_.no_scrollbar |
                    imgui.WindowFlags_.no_scroll_with_mouse |
                    imgui.WindowFlags_.always_auto_resize
                )
                dockable_window.label = f"satellite {len(params.docking_params.dockable_windows)}"
                dockable_window.gui_function = satellite_window.gui

                # (!) append will silently fail, because it is a bound std::vector...
                # params.docking_params.dockable_windows.append(dockable_window)
                # => Use `=` operator instead
                params.docking_params.dockable_windows = params.docking_params.dockable_windows + [dockable_window]

        if mode==2:
            for i, satellite_window in enumerate(satellite_windows):
                window_flags = int(
                    imgui.WindowFlags_.no_title_bar |
                    imgui.WindowFlags_.no_collapse |
                    imgui.WindowFlags_.no_scrollbar |
                    imgui.WindowFlags_.no_scroll_with_mouse |
                    imgui.WindowFlags_.always_auto_resize
                )
                imgui.begin(f"satellite {i}", None, window_flags)
                satellite_window.gui()
                imgui.end()

    def post_init():
        imgui.get_io().config_viewports_no_decoration = False
        imgui.get_io().config_viewports_no_auto_merge = True

    params.callbacks.show_gui = gui
    params.callbacks.post_init = post_init
    immapp.run(params)

if __name__ == "__main__":
    main()

Autosizing indeed works well, but your positioning logic cannot be invoked on these satellite windows. That would be nice, but maybe not worth the effort if not easily done.

pthom commented 1 year ago

On mode 2, you need to capture the return from imgui.begin, like so:

from typing import List
from imgui_bundle import imgui, immapp, hello_imgui, immapp, ImVec2

class MySatelliteWindow:
    text_size = ImVec2
    text = "Hello"

    def __init__(self):
        self.text_size = ImVec2(300, 300)

    def gui(self):
        imgui.push_id(str(id(self)))
        imgui.text(f"Satellite {id(self)}")
        imgui.set_next_item_width(100);
        _, self.text_size.x = imgui.slider_float("size.x", self.text_size.x, 10, 600)
        imgui.set_next_item_width(100);
        _, self.text_size.y = imgui.slider_float("size.y", self.text_size.y, 10, 600)

        _, self.text = imgui.input_text_multiline("Text", self.text, self.text_size)
        imgui.pop_id()

def main():
    params = hello_imgui.RunnerParams()
    params.imgui_window_params.config_windows_move_from_title_bar_only = True
    params.imgui_window_params.enable_viewports = True

    satellite_windows: List[MySatelliteWindow] = []
    satellite_windows_visible: List[bool] = []

    mode = 2
    def gui():
        nonlocal params
        if imgui.button("add satellite window"):
            satellite_window = MySatelliteWindow()
            satellite_windows.append(satellite_window)
            satellite_windows_visible.append(True)

            if mode==1:
                dockable_window = hello_imgui.DockableWindow()
                dockable_window.imgui_window_flags = int(
                    imgui.WindowFlags_.no_title_bar |
                    imgui.WindowFlags_.no_collapse |
                    imgui.WindowFlags_.no_scrollbar |
                    imgui.WindowFlags_.no_scroll_with_mouse |
                    imgui.WindowFlags_.always_auto_resize
                )
                dockable_window.label = f"satellite {len(params.docking_params.dockable_windows)}"
                dockable_window.gui_function = satellite_window.gui

                # (!) append will silently fail, because it is a bound std::vector...
                # params.docking_params.dockable_windows.append(dockable_window)
                # => Use `=` operator instead
                params.docking_params.dockable_windows = params.docking_params.dockable_windows + [dockable_window]

        if mode==2:
            for i, satellite_window in enumerate(satellite_windows):
                if satellite_windows_visible[i]:
                    window_flags = int(
                        imgui.WindowFlags_.no_title_bar |
                        imgui.WindowFlags_.no_collapse |
                        imgui.WindowFlags_.no_scrollbar |
                        imgui.WindowFlags_.no_scroll_with_mouse |
                        imgui.WindowFlags_.always_auto_resize
                    )
                    opened, satellite_windows_visible[i] = imgui.begin(f"satellite {i}", satellite_windows_visible[i], window_flags)
                    if opened:
                        satellite_window.gui()
                    imgui.end()

    def post_init():
        imgui.get_io().config_viewports_no_decoration = False
        imgui.get_io().config_viewports_no_auto_merge = True

    params.callbacks.show_gui = gui
    params.callbacks.post_init = post_init
    immapp.run(params)

if __name__ == "__main__":
    main()
dcnieho commented 1 year ago

Ah, of course. Thanks, super!