pthom / hello_imgui

Hello, Dear ImGui: unleash your creativity in app development and prototyping

Home Page:https://pthom.github.io/hello_imgui

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

creating multiple windows without docking

dcnieho opened this issue · comments

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 dcnieho/glassesValidator@6982233. 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:

  • default case as is, use just has a single window and API is as is. This is a light abstraction from the more involved way:
  • user has one or multiple windows. There is a settings struct per window (like you have for docking windows now) as well as runner parameters per window. So i can set e.g. resize_app_window_at_next_frame and all the window geometry stuff on a specific window, the main window is not special. params.docking_params.dockable_windows would become something like just params.windows
  • docking is some additional logic, where windows can be flagged as either host or dockable (if thats how it works, i'm hardly familiar with imgui docking). Thats only a few fields besides the many more per window fields that are for window setup whether docking or not.
  • Ideally it would be possible to create new windows at any time. You can envision users sometimes needed to use a second window for a while, e.g. to preview the effect of some image processing settings they did in some GUI (some random example).
  • There is the problem that it seems to be not possible to set your own user callbacks on newly created platform windows with the glfw backend, but thats an dear imgui problem. Or yeah, it would be possible, but the user would have to do their own chaincalling of imgui callbacks instead of the backend handling that nicely. Just an FYI, not your problem. I didn't end up needing it.

Hi Dee,

I need some more time to study this

Thanks and no worries. Happy to discuss

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()

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.

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()

Ah, of course. Thanks, super!