In WinAPI, how to make a stretchable OpenGL window with correct mouse, trapped properly for games?

925 views Asked by At

I have read a lot of Stack Overflow over the years when struggling with making sense of Microsoft Windows' strange world of CreateWindowEx() .. etc. This question, when originally asked was "What is the best way to create a fluidly resizable OpenGL window in WinAPI?"

I've been struggling with getting WinAPI to make a window that:

  • Has an OpenGL context
  • Is properly centered on the main monitor (or any monitor determined by command line signal) in both multi-monitor and single-monitor displays when in "Windowed" mode or in "Fullscreen" mode
  • Has a fixed internal client screen size (viewport 2d)
  • Doesn't allow you to click outside causing it to lose focus at the wrong times or in special cases for multi-monitor
  • Can be resized fluidly, but doesn't change internal "client size" (meaning that it stretches the OpenGL content which is a fixed size to the new screen size) ... the idea here is to add a layer of virtualization, so that all pixels are expressed in the same 1920x1080 (1080p) coordinate system. This part is no problem for me.
  • Correctly handles mouse event translation from screen_size -> client_size equivalent via the screen->client ratio

In my homegrown App framework, I have to set the display size, and even then, Windows doesn't give me the right sized window. (Sometimes the title bar is subtracted, sometimes the scrollbars are subtracted, but the context draws under the title bar, for example.)

Also, recently when moving from 2010 EE (Win32 / Windows 7) to 2015 (win32 / Windows 10), I had to change the parameters to recenter the view because it was off-centered on the main display. Now, only sometimes are these values correct or incorrect. If I go "fullscreen" for example, the same values will draw above the top of the screen such that there is an area at the bottom of the screen that shows the "gl clear color" (in my case, orange)

I can play with these things by providing the following command line parameters:

  • -bordered (default, and has no effect really, is the default windowed mode with the title bar and such)
  • -borderless (seems to go into fullscreen mode, with the app off-center where win 0,0 is actually in screen center)
  • -windowed (or -window)

If I don't provide -window, it defaults to "full screen" resolution-adjusted (but only if supported I assume, otherwise it might throw an error).

Anyway, all of this is very bad because a) I have to write a bajillion cases for each resolution I'm working in, rather than write everything for 1080p and have it adjust to display size, which is what i want because it handles most new displays on laptops and desktops (this is Windows remember) (and only slightly squishes things in those corner cases) b) I cannot resize the window fluidly, also i have to trap the mouse at center and recalculate the mouse position, so I record only the deltas -- this is to avoid the mouse leaving the window and clicking the desktop, or floating off the monitor to some other monitor, even when it is hidden. I also have to make the mouse cursor invisible so the user doesn't see this, then show a simulated mouse cursor. c) Users who don't support specifically 1920x1080 won't be able to use full screen mode

Someone pointed this article out in another question (window border width and height in Win32 - how do I get it?): https://web.archive.org/web/20120716062211/http://suite101.com/article/client-area-size-with-movewindow-a17846

And I've read through this, learning that AdjustWindowRectEx() has some issues: AdjustWindowRectEx() and GetWindowRect() give wrong size with WS_OVERLAPPED

I don't use WS_OVERLAPPED, so this was only moderately helpful: AdjustWindowRectEx() and GetWindowRect() give wrong size with WS_OVERLAPPED

Here's how I do it now:

    display.Resized(display.w,display.h);

    // Fill in the window class structure for testing display type.

    winclass.cbSize = sizeof(WNDCLASSEX);
    winclass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
    winclass.lpfnWndProc = WinProc;
    winclass.cbClsExtra = 0;
    winclass.cbWndExtra = 0;
    winclass.hInstance = hinstance;
    winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    winclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
    winclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    winclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
    winclass.lpszMenuName = NULL;
    winclass.lpszClassName = WINDOW_CLASS_NAME;

    // Save the game instance handle
    display.hinstance = game_instance = hinstance;

    // Register the window class
    if (!RegisterClassEx(&winclass)) return(0);

    if (!gl.Init(hinstance, display.bits)) {
     return(0);
    }

    // Detect the display size and create the final display profile 

    DWORD winStyle=
     WS_EX_APPWINDOW |
     WS_EX_TOPMOST /*|
     WS_EX_ACCEPTFILES*/ ;

    // Adjust Window, Account For Window Borders
    int xPos = GetSystemMetrics(SM_CXSCREEN) - display.w;
    int yPos = GetSystemMetrics(SM_CYSCREEN) - display.h;
    RECT windowRect = {0, 0, display.w, display.h}; // Define Our Window Coordinates
    AdjustWindowRectEx (&windowRect, WS_POPUP, 0, winStyle );
    // Create the window
    if (!(hwnd = CreateWindowEx(
     winStyle,                    // extended style
     WINDOW_CLASS_NAME, // class
     gl.winTitle.c_str(),          // title
     ( gl.borderless || CmdLine.Option("-borderless") ) ? (WS_POPUPWINDOW | WS_VISIBLE)
     : (gl.noFullscreen ? ((CmdLine.Option("-bordered") ? WS_BORDER : 0) | WS_VISIBLE)
                        : (WS_POPUP | WS_VISIBLE)),  // use POPUP for full screen
       gl.noFullscreen && !CmdLine.Option("-recenter") ? xPos / 2 : 0,
       gl.noFullscreen && !CmdLine.Option("-recenter") ? yPos / 2 : 0,     // initial game window x,y
       display.w,         // initial game width
       display.h,         // initial game height
       HWND_DESKTOP,      // handle to parent
       NULL,              // handle to menu
       hinstance,         // instance of this application
       NULL)
      )         // extra creation parms
     ) {
     OUTPUT("WinAPI ERROR: Could not open window.\n");
     return(0);
    }

    if (gl.borderless || CmdLine.Option("-borderless") ) {
      LONG lStyle = GetWindowLong(hwnd, GWL_STYLE);
      lStyle &= ~(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZE | WS_MAXIMIZE | WS_SYSMENU);
      SetWindowLong(hwnd, GWL_STYLE, lStyle);
      LONG lExStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
      lExStyle &= ~(WS_EX_DLGMODALFRAME | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE);
      SetWindowLong(hwnd, GWL_EXSTYLE, lExStyle);
      SetWindowPos(hwnd, NULL, 0, 0, display.w, display.h, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
      }

      // Temporary change to full screen mode
      ZeroMemory(&game_screen, sizeof(game_screen)); // clear out size of DEVMODE    struct
    game_screen.dmSize = sizeof(game_screen);
    game_screen.dmPelsWidth = display.w;
    game_screen.dmPelsHeight = display.h;
    game_screen.dmBitsPerPel = display.bits;
    game_screen.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;
    ChangeDisplaySettings(&game_screen, CDS_FULLSCREEN);

    // save the game window handle
    display.hwnd = game_window = hwnd;
    display.hdc = game_dc = GetDC(display.hwnd = game_window); // get the GDI device context
                                                             // set up the pixel format desc struct

    pfd = {
     sizeof(PIXELFORMATDESCRIPTOR),   // size of this PFD
     1,                               // version number
     PFD_DRAW_TO_WINDOW |             // supports window
     PFD_SUPPORT_OPENGL |             // supports OpenGL
     PFD_DOUBLEBUFFER,                // support double buff
     PFD_TYPE_RGBA,                   // request RGBA format
     (BYTE)display.bits,              // select color depth
     0, 0, 0, 0, 0, 0,                // color bits ignored
     0,                               // no alpha buff
     0,                               // shift bit ignored
     0,                               // no accum buff
     0, 0, 0, 0,                      // accum bits ignored
     16,                              // 16-bit Z-buff (depth buff)
     0,                               // no stencil buff
     0,                               // no aux buff
     PFD_MAIN_PLANE,                  // main drawing layer
     0,                               // reserved
     0, 0, 0                          // layer masks ignored
    };
    int pf;  // pixel format

    if (!gl.arbMultisampleSupported) {
     if (!(pf = ChoosePixelFormat(game_dc, &pfd)))  // match the pixel format
     {
      MessageBox(game_window, "OpenGL could not be initialized -- ChoosePixelFormat Error ; report this to program authors for help!", "OpenGL Error", MB_OK);
      return FALSE; // error returned
     }
    } else {
     pf = gl.arbMultisampleFormat;
    }
    if (!SetPixelFormat(game_dc, pf, &pfd))        // set the pixel format
    {
     MessageBox(game_window, "OpenGL could not be initialized -- SetPixelFormat Error ; report this to program authors for help!", "OpenGL Error", MB_OK);
     return FALSE; // error returned
    }

    if (!(game_rc = wglCreateContext(game_dc)))    // create the rendering context
    {
     MessageBox(game_window, "OpenGL could not be initialized -- CreateContext Error ; report this to program authors for help!", "OpenGL Error", MB_OK);
     return FALSE; // error returned
    }

    if (!(upload_rc = wglCreateContext(game_dc)))    // create the rendering context
    {
     MessageBox(game_window, "Multiple OpenGL contexts could not be initialized -- CreateContext Error ; report this to program authors for help!", "OpenGL Error", MB_OK);
     return FALSE; // error returned
    } else { // Share as much as you can between two contexts
     if (!wglShareLists(game_rc, upload_rc)) {
      // could use GetLastError here
      MessageBox(game_window, "wglShareLists -- Error ; report this to program authors for help!", "OpenGL Error", MB_OK);
      return FALSE; // error returned
     }
    }

    if (!wglMakeCurrent(game_dc, display.hglrc = game_rc))         // make it  current
    {
     MessageBox(game_window, "OpenGL could not be initialized -- MakeCurrent Error ; report this to program authors for help!", "OpenGL Error", MB_OK);
     return FALSE; // error returned
    }

    ShowCursor(false);
    ShowWindow(game_window, SW_SHOWNORMAL);
    SetForegroundWindow(game_window);

In the above code, what I get is a window that has no resize functionality, hides the OS mouse cursor, and can only be exitted with ALT-TAB (or ALT-F4), and when it is exitted appears at the back of the windows Z-order. I always open my window using a parameter that sets display.w to 1920 and display.h to 1080, either in full screen or in Windowed mode. WM_SIZE is then called to adjust it to the client area.

Please note that the following WM_SIZE is called during the WinProc right after the initial time I set display.Resized(w,h):

    case WM_SIZE:
    {
     display.Resized(LOWORD(lparam), HIWORD(lparam));
     return (0);
    }
    break;

This is executed exactly once during app load, and in the first case it looks like the values are: 1918,1078


UPDATE: If I use the result of GetWindowRect() here, or GetClientRect() as shown below, the window mysteriously moves to Center-X,Center-Y of screen! What gives??

    //  RECT rect;
    //  if ( GetClientRect(hwnd,&rect) ) {
    //   display.Resized((int)rect.right,(int)rect.bottom);
    //  }
      //if ( GetWindowRect( hwnd, &rect ) ) {
      // display.Resized((int)ADIFF(rect.left,rect.right),(int)ADIFF(rect.top,rect.bottom));
      //}
      display.Resized(LOWORD(lparam), HIWORD(lparam));
      return (0);

What steps do I need to take to make the window stretchable such that the context is resized to the view, and the mouse is properly adjusted based on the screen ratio?

Basically, there are too many edge cases to make sense of all of this. As time has gone on since the 2 years ago that I asked this question, I've had other inconsistencies between full screen and window emerge.

From what I understand there are basically 3 types of windows:

  • Your normal on-screen moveable/resizable window for windowing GUIs, like this browser window (if you are not on mobile)
  • One matched to a display's resolution support (including resolutions smaller than its native) -- we call this "Full screen" (or Fullscreen, which isn't even a word)
  • One that is a normal on-screen window, but lacks a title bar, borders and scroll bars, and appears as large as the screen. Referred to "on the street" as a "Borderless Window"

I want to master all of these but in a way that makes them all accessible and doesn't require special cases. I've basically given up on doing so with WinAPI, but obviously multiple companies do this. Following Microsoft's documentation isn't very helpful, and I've experimented with a lot of different CreateWindow CreateWindowEx -- many of these features are deprecated by the way, or don't work at all.

(Maybe the best question is, WHEN WILL MICROSOFT CLEAN UP THIS CRAP? But I think we all know the answer.) .. any help to get it working would be appreciated.

I'm now working in: C++, Windows API, OpenGL 3.x / 4.x, Windows 10.

0

There are 0 answers