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 justparams.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
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!