Water
Water

Reputation: 3705

How to get multiple windows in OpenTK

Is there a way to get multiple functioning windows in OpenTK?

When I do window.Run() it takes away control from me until it the window is closed.

I do not want to drop down to using GLFW for management because I want to continue using the OpenTK framework. In particular, the GL function bindings that it takes care of for me.

Upvotes: 2

Views: 253

Answers (1)

Kasper Olesen
Kasper Olesen

Reputation: 136

I have been trying to figure out a way of doing this myself for a while. This is the closest I have come so far. Problems is OpenTK is not designed to handle this, so instead of letting it render and load the windows using Run on the OpenTK windows, you have to create your own loading and rendering methods that replaces those.

My solution is to create a BaseWindow that inherits from the GameWindow and then only render the window that is focused. This solves input issues as well since only one of the windows are updated at a time.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using ImGuiNET;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;

namespace UI
{

    public abstract class BaseWindow : GameWindow
    {
        public ImGuiController _controller {get; private set;};

        protected BaseWindow(GameWindowSettings gameWindowSettings, NativeWindowSettings nativeWindowSettings)
            : base(gameWindowSettings, nativeWindowSettings)
        {
            MakeCurrent();
            OnLoad();
        }

        protected override void OnLoad()
        {
            MakeCurrent();
            base.OnLoad();
            _controller = new ImGuiController(ClientSize.X, ClientSize.Y);
        }

        protected override void OnRenderFrame(FrameEventArgs args)
        {
            _controller.Render();
            base.OnRenderFrame(args);
            SwapBuffers();
        }

        public void UpdateAndRender()
        {
            MakeCurrent();
            ProcessEvents(1f/60);
            _controller.Update(this,1f/60f, true);
            OnRenderFrame(new FrameEventArgs());
        }

        protected override void OnResize(ResizeEventArgs e)
        {
            MakeCurrent();
            base.OnResize(e);
            GL.Viewport(0, 0, ClientSize.X, ClientSize.Y);
            _controller.WindowResized(ClientSize.X, ClientSize.Y);
        }

        protected override void OnTextInput(TextInputEventArgs e)
        {
            MakeCurrent();
            base.OnTextInput(e);
            _controller.PressChar((char)e.Unicode);
        }

        protected override void OnMouseDown(MouseButtonEventArgs e)
        {
            MakeCurrent();
            base.OnMouseDown(e);
            _controller.MouseDown(e.Button);
        }

        protected override void OnMouseUp(MouseButtonEventArgs e)
        {
            MakeCurrent();
            base.OnMouseUp(e);
            _controller.MouseUp(e.Button);
        }

        protected override void OnMouseMove(MouseMoveEventArgs e)
        {
            MakeCurrent();
            base.OnMouseMove(e);
            _controller.MouseMove(e.Position);
        }

        protected override void OnMouseWheel(MouseWheelEventArgs e)
        {
            MakeCurrent();
            base.OnMouseWheel(e);
            _controller.MouseScroll(e.Offset);
        }
        
        public new void Close()
        {
            MakeCurrent();
            base.Close();
            Dispose();
        }
        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);
        }

        protected override void OnUnload()
        {
            base.OnUnload();
            MakeCurrent();
            _controller.Dispose();
        }
    }

    public class WindowManager
    {
        private List<BaseWindow> windows = new List<BaseWindow>();
        private bool isRunning = true;
        public static BaseWindow focusedWindow;

        public void AddWindow(BaseWindow window)
        {
            Console.WriteLine($"Adding window: {window.GetType().Name}");
            windows.Add(window);
            window.Closing += (e) => 
            {
                Console.WriteLine($"Window '{window.Title}' is closing.");
                windows.Remove(window);
                if (windows.Count == 0)
                {
                    Console.WriteLine("No windows left, setting isRunning to false.");
                    isRunning = false;
                }
            };
        }

        public void Run()
        {
            try
            {
                Console.WriteLine("Starting WindowManager...");
                int loopCount = 0;
                while (isRunning && windows.Any())
                {
                    loopCount++;
                    focusedWindow = null;
                    for (int i = windows.Count - 1; i >= 0; i--)
                    {
                        var window = windows[i];
                        if (window.Exists)
                        {
                            if (window.IsFocused)
                            {
                                focusedWindow = window;
                                window.UpdateAndRender();
                            }
                        }
                        else
                        {
                            Console.WriteLine($"Window '{window.Title}' no longer exists. Removing from list.");
                            windows.RemoveAt(i);
                        }
                    }
                    
                    for (int i = windows.Count - 1; i >= 0; i--)
                    {
                        var window = windows[i];
                        if (window.Exists)
                        {
                            if (focusedWindow == null)
                            {
                                window.UpdateAndRender();
                            }
                        }
                        else
                        {
                            Console.WriteLine($"Window '{window.Title}' no longer exists. Removing from list.");
                            windows.RemoveAt(i);
                        }
                    }

                    if (windows.Count == 0)
                    {
                        Console.WriteLine("No windows left. Exiting loop.");
                        isRunning = false;
                    }
                    Thread.Sleep(16); // ~60 FPS (1/60*1000)
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Unhandled exception in WindowManager: {ex.Message}");
                Console.WriteLine(ex.StackTrace);
            }
            finally
            {
                Console.WriteLine("Cleaning up windows...");
                foreach (var window in windows)
                {
                    try
                    {
                        Console.WriteLine($"Closing window: {window.GetType().Name}");
                        window.Close();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"Error closing window '{window.Title}': {ex.Message}");
                    }
                }
                Console.WriteLine("WindowManager finished.");
            }
        }
    }
}

For my usecase I also wanted the other window to update, but it did not need to update f.ex. 60 frames per second, so one UI I made update every 180 frames or 3 seconds, and in the focused window I am f.ex. checking if a textinput element was focused, store that it was focused and then refocus it if it was focused and the window it is in is focused. You can do that with code like this:

if (ImGui.InputText("Search", ref _searchQuery, 100))
{
    UpdateFilter();
}
if(IsFocused && !ImGui.IsItemFocused() && _isSearchInputFocused)
{
    ImGui.SetKeyboardFocusHere(-1);
}
_isSearchInputFocused = ImGui.IsItemFocused();

And then in the WindowManager you can change this line:

if (window.IsFocused)

To

if (window.IsFocused || loopCount % 180 == 1)

Using modulus on the loopcount it will render all windows every 180 loops and otherwise only render the focused window and the InputText code should then help refocus the textinput element when that happens. If you do not add this, it will cause the UI to blink a lot, f.ex. the mouse hover when going over text, the input text marker and such. If you need to update both windows all the time, the problem is that the other window then might grab the keypresses instead of the focused window. That could maybe be worked around with a keypress cache that windows will add to if a key is pressed while they are not focused, but I am still trying to make that solution reliable enough to use.

Do do hope there is a simpler solution than this, but so far this is what I am going with.

Upvotes: 0

Related Questions