Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Window not appearing centered on the monitor #1213

Closed
Timber1900 opened this issue Dec 9, 2020 · 6 comments
Closed

Window not appearing centered on the monitor #1213

Timber1900 opened this issue Dec 9, 2020 · 6 comments

Comments

@Timber1900
Copy link
Contributor

Description

When creating a openTK window with openTK 4.* the window does not appear in the middle of the screen

Repro steps

From what I can gather on my machine simply creating the window, even without rendering anything to it creates the issue

  1. Create the window without changing its location

Expected behavior

Window should appear centered on the screen
image

Actual behavior

Window appears slightly to the left and bottom
image

Related information

  • Windows 10
  • OpenTK 4.*
  • NetCore 3.1
  • DPI scaling is 100%
  • Workarround:

Center the window when creating the window using the location atribute:

public static class Screen
    {
        [DllImport("user32.dll")]
        private static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);

        /// <summary>
        ///     Gets the screen size
        /// </summary>
        /// <returns>Returns the screen size</returns>
        public static Vector2 GetScreenSize()
        {
            const int ENUM_CURRENT_SETTINGS = -1;

            DEVMODE devMode = default;
            devMode.dmSize = (short) Marshal.SizeOf(devMode);
            EnumDisplaySettings(null, ENUM_CURRENT_SETTINGS, ref devMode);
            return new Vector2(devMode.dmPelsWidth, devMode.dmPelsHeight);
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct DEVMODE
        {
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
            public readonly string dmDeviceName;

            public readonly short dmSpecVersion;
            public readonly short dmDriverVersion;
            public short dmSize;
            public readonly short dmDriverExtra;
            public readonly int dmFields;
            public readonly int dmPositionX;
            public readonly int dmPositionY;
            public readonly int dmDisplayOrientation;
            public readonly int dmDisplayFixedOutput;
            public readonly short dmColor;
            public readonly short dmDuplex;
            public readonly short dmYResolution;
            public readonly short dmTTOption;
            public readonly short dmCollate;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
            public readonly string dmFormName;

            public readonly short dmLogPixels;
            public readonly int dmBitsPerPel;
            public readonly int dmPelsWidth;
            public readonly int dmPelsHeight;
            public readonly int dmDisplayFlags;
            public readonly int dmDisplayFrequency;
            public readonly int dmICMMethod;
            public readonly int dmICMIntent;
            public readonly int dmMediaType;
            public readonly int dmDitherType;
            public readonly int dmReserved1;
            public readonly int dmReserved2;
            public readonly int dmPanningWidth;
            public readonly int dmPanningHeight;
        }
    }

public MainRenderWindow(int width, int height, string title, double fps) :
            base(CreateGameWindowSettings(fps), CreateNativeWindowSettings(width, height, title))
        {
        }

        private static GameWindowSettings CreateGameWindowSettings(double fps = 60.0)
        {
            var gws = new GameWindowSettings
            {
                UpdateFrequency = fps,
                RenderFrequency = fps
            };
            return gws;
        }

        private static NativeWindowSettings CreateNativeWindowSettings(int width = 1000, int height = 1000,
            string title = "OpenTK Window")
        {
            var MonitorSize = Screen.GetScreenSize();
            var nws = new NativeWindowSettings
            {
                Size = new Vector2i(width, height),
                Title = title,
                Location = new Vector2i((int) ((MonitorSize.X - width) / 2), (int) ((MonitorSize.Y - height - 10) / 2)) //This centers the screen on the monitor
            };
            return nws;
        }
}
@NogginBops
Copy link
Member

On windows windows don't get created in the center of the screen.
They are placed to the top left a bit offset from the last window that was opened in that spot.
In the change from opentk 3 to opentk 4 we probably when from our own default to the windows default.

To make a proper solution you can use the CurrentMonitor property of the NativeWindow (or GameWindow) to get monitor size and place the window in the center of that monitor.

@Timber1900
Copy link
Contributor Author

I'm sorry if I'm missing something obvious but from what I can gather the Current Monitor property only appears to have a pointer that I assume would be used in some function that gets the screen size,
image

However I can't seem to find this function.

Thanks for the help.

@seanofw
Copy link
Contributor

seanofw commented Dec 9, 2020

Centering a window isn't hard in the latest versions of OpenTK, but it's not especially obvious either. Most of the work involves interacting with data in the static Monitors collection, and not with the window or the MonitorHandle itself.

I recommend something like the sequence below, which accounts for several weird corner cases and has a nice fallback if for some reason GLFW produces really weird output. I've commented it heavily so you can see how it works:

public static void ResizeAndCenterWindow(NativeWindow window, int width, int height)
{
    int x, y;

    // Find out which monitor the window is already on.  If we can't find that out, then
    // just try to find the first monitor attached to the computer and use that instead.
    MonitorHandle currentMonitor = Monitors.GetMonitorFromWindow(window);
    if (Monitors.TryGetMonitorInfo(currentMonitor, out MonitorInfo monitorInfo)
        || Monitors.TryGetMonitorInfo(0, out monitorInfo))
    {
        // Calculate a suitable upper-left corner for the window, based on this monitor's
        // coordinates.  This should work correctly even in unusual multi-monitor layouts.
        Rectangle monitorRectangle = monitorInfo.ClientArea;
        x = (monitorRectangle.Right + monitorRectangle.Left - width) / 2;
        y = (monitorRectangle.Bottom + monitorRectangle.Top - height) / 2;

        // Avoid putting it offscreen.
        if (x < monitorRectangle.Left) x = monitorRectangle.Left;
        if (y < monitorRectangle.Top) y = monitorRectangle.Top;
    }
    else
    {
        // No idea what monitor this is, so just try to put the window somewhere reasonable,
        // like the upper-left corner of what's hopefully *a* monitor.  Alternatively, you
        // could throw an exception here.
        x = 32;
        y = 64;
    }

    // Actually move the window.
    window.ClientRectangle = new Box2i(x, y, width, height);
}

The keys to how this works are really in Monitors.GetMonitorFromWindow(window) and Monitors.TryGetMonitorInfo(...), which will tell you:

  • Which monitor your window is on right now (don't use NativeWindow.CurrentMonitor, which is not the same as what GetMonitorFromWindow() will answer, and can sometimes be just null), and
  • What that monitor's rectangle is relative to the overall "virtual screen" of the OS. Bear in mind that on multi-monitor setups, screen coordinate (0, 0) might not actually be on any monitor, so you have to ask the OS which monitor is the best one to put the window on, and position the window relative to whatever dimensions the OS gives you for that monitor.

Ideally, OpenTK should probably offer this as a simple method on NativeWindow like void CenterWindow() or void CenterWindow(Vector2i newSize) (or maybe CentreWindow() for our English friends? 😁), but as of OpenTK 4.3, you can at least implement the logic yourself.

Oh, also — the weird math in the middle is just an optimized version of the much simpler math below, with the / 2 factored out so that it doesn't have to be performed twice, and with everything mashed together:

// Find the center of the monitor.
int monitorCenterX = (monitorRectangle.Left + monitorRectangle.Right) / 2;
int monitorCenterY = (monitorRectangle.Top + monitorRectangle.Bottom) / 2;

// Find the top-left corner of the window when its center is at the monitor's center.
int newWindowX = monitorCenterX - (width / 2);
int newWindowY = monitorCenterY - (height / 2);

(And yes, I know they have slightly different rounding behavior, but the optimized is actually more correct for non-even sizes.)

@Timber1900
Copy link
Contributor Author

Timber1900 commented Dec 9, 2020

I'd like to just post some final remarks here before you guys close this issue,

The code you sent worked great, I just had to change one line:

cs window.ClientRectangle = new Rect(x, y, width, height);

  • Rect is not defined, the client rectangle is actually a Box2i so this becomes:

  • cs window.ClientRectangle = new Box2i(x, y, x + width, y + height); <= this.

Or in a non static context and without resing (my use case)

 public void ResizeAndCenterWindow()
        {
            int x, y;

            // Find out which monitor the window is already on.  If we can't find that out, then
            // just try to find the first monitor attached to the computer and use that instead.
            MonitorHandle currentMonitor = Monitors.GetMonitorFromWindow(this);
            if (Monitors.TryGetMonitorInfo(currentMonitor, out MonitorInfo monitorInfo)
                || Monitors.TryGetMonitorInfo(0, out monitorInfo))
            {
                // Calculate a suitable upper-left corner for the window, based on this monitor's
                // coordinates.  This should work correctly even in unusual multi-monitor layouts.
                Rectangle monitorRectangle = monitorInfo.ClientArea;
                x = (monitorRectangle.Right + monitorRectangle.Left - Size.X) / 2;
                y = (monitorRectangle.Bottom + monitorRectangle.Top - Size.Y) / 2;

                // Avoid putting it offscreen.
                if (x < monitorRectangle.Left) x = monitorRectangle.Left;
                if (y < monitorRectangle.Top) y = monitorRectangle.Top;
            }
            else
            {
                // No idea what monitor this is, so just try to put the window somewhere reasonable,
                // like the upper-left corner of what's hopefully *a* monitor.  Alternatively, you
                // could throw an exception here.
                x = 32;
                y = 64;
            }

            // Actually move the window.
            ClientRectangle = new Box2i(x, y, x + Size.X, y + Size.Y);
        }

Also I just want to point out here that this is a function you would call after constructing the window, it asks for a NativeWindow witch is essentially the actual class itself so you would pass in this when called from the class.

A native function that handles this would be great as it probably would save many hours of hassle on somebody's part (It definitely would have saved mine 😄).

Overall thanks for the help, this solution works great and is definitely better than my old one.

@seanofw
Copy link
Contributor

seanofw commented Dec 9, 2020

Yeah, I totally yanked that out and cleaned it up without building it. My original version doesn’t really look like that — it’s designed for the project it’s used in, and uses types and data OpenTK knows nothing about — but that’s enough that you were able to get the idea, at least.

I fixed up the one line, so anybody who Googles this in the future will find a working snippet of code.

@NogginBops
Copy link
Member

NogginBops commented Dec 10, 2020

Oh, sorry I gave you the wrong function in the beginning. I knew there was some API for it but I didn't look close enough to see that current monitor wasn't the API you wanted.

This could be added as a function to OpenTK. Just open a PR and we can look at getting it merged. Not sure if the function should be part of NativeWindow or Monitors. I'm thinking NativeWindow might be better for discoverability.

Closing this issue as the "intended" solution is the workaround. Feel free to open a PR though 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants