Monday, October 8, 2007

Forcing a window to maintain a particular aspect ratio

This is a piece of code I removed from my framework recently. I first wrote something like this many years ago, back when computer monitors were virtually all 4:3 aspect ratio.

Games typically run full-screen, but during development it's much more convenient to have them run in a window. For 3D games it's easy to resize the window. The problem is what to do when the window's client area doesn't match the aspect ratio of the full-screen monitor. You can letterbox the image, putting black borders on the sides, or you can crop, trimming part of the image off.

I opted to make the program restrict window resizing so that the window's aspect ratio would always match the target monitor's aspect ratio. This turns out not to be too hard to do in Windows.

First, you need to catch the WM_SIZING message in your message handler:

LRESULT WINAPI MsgProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_SIZING:
resize(int(wParam), *reinterpret_cast<LPRECT>(lParam));
return TRUE;

...
}

return DefWindowProc(hWnd, msg, wParam, lParam);
}


Second, you need to modify the rectangle that comes in with the WM_SIZING message so that it obeys your restrictions:

void resize(int edge, RECT & rect)
{
int size_x_desired = (rect.right - rect.left) - g_window_adjust_x;
int size_y_desired = (rect.bottom - rect.top) - g_window_adjust_y;

switch (edge)
{
case WMSZ_BOTTOM:
case WMSZ_TOP:
{
int size_x = g_window_adjust_x + (size_y_desired * window_ratio_x) / window_ratio_y;
rect.left = (rect.left + rect.right) / 2 - size_x / 2;
rect.right = rect.left + size_x;
}
break;
case WMSZ_BOTTOMLEFT:
{
int size_x, size_y;

if (size_x_desired * window_ratio_y > size_y_desired * window_ratio_x)
{
size_x = rect.right - rect.left;
size_y = g_window_adjust_y + ((size_x - g_window_adjust_x) * window_ratio_y) / window_ratio_x;
}
else
{
size_y = rect.bottom - rect.top;
size_x = g_window_adjust_x + ((size_y - g_window_adjust_y) * window_ratio_x) / window_ratio_y;
}

rect.left = rect.right - size_x;
rect.bottom = rect.top + size_y;
}
break;
case WMSZ_BOTTOMRIGHT:
{
int size_x, size_y;

if (size_x_desired * window_ratio_y > size_y_desired * window_ratio_x)
{
size_x = rect.right - rect.left;
size_y = g_window_adjust_y + ((size_x - g_window_adjust_x) * window_ratio_y) / window_ratio_x;
}
else
{
size_y = rect.bottom - rect.top;
size_x = g_window_adjust_x + ((size_y - g_window_adjust_y) * window_ratio_x) / window_ratio_y;
}

rect.right = rect.left + size_x;
rect.bottom = rect.top + size_y;
}
break;
case WMSZ_LEFT:
case WMSZ_RIGHT:
{
int size_y = g_window_adjust_y + (size_x_desired * window_ratio_y) / window_ratio_x;
rect.top = (rect.top + rect.bottom) / 2 - size_y / 2;
rect.bottom = rect.top + size_y;
}
break;
case WMSZ_TOPLEFT:
{
int size_x, size_y;

if (size_x_desired * window_ratio_y > size_y_desired * window_ratio_x)
{
size_x = rect.right - rect.left;
size_y = g_window_adjust_y + ((size_x - g_window_adjust_x) * window_ratio_y) / window_ratio_x;
}
else
{
size_y = rect.bottom - rect.top;
size_x = g_window_adjust_x + ((size_y - g_window_adjust_y) * window_ratio_x) / window_ratio_y;
}

rect.left = rect.right - size_x;
rect.top = rect.bottom - size_y;
}
break;
case WMSZ_TOPRIGHT:
{
int size_x, size_y;

if (size_x_desired * window_ratio_y > size_y_desired * window_ratio_x)
{
size_x = rect.right - rect.left;
size_y = g_window_adjust_y + ((size_x - g_window_adjust_x) * window_ratio_y) / window_ratio_x;
}
else
{
size_y = rect.bottom - rect.top;
size_x = g_window_adjust_x + ((size_y - g_window_adjust_y) * window_ratio_x) / window_ratio_y;
}

rect.right = rect.left + size_x;
rect.top = rect.bottom - size_y;
}
break;
}
}


window_ratio_x and window_ratio_y are integer constants that define the desired aspect ratio.

We want to enforce a specific aspect ratio for the client area, but the resize code is dealing with the dimensions of the overall window. To handle this, at startup I use AdjustWindowRect to find out how much padding there is in each dimension, and store it off in g_window_adjust_x and g_window_adjust_y:

int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE, LPSTR, int nShowCmd)
{
... // register window class

const int window_style = WS_OVERLAPPEDWINDOW;

RECT rect = { 0, 0, start_size_x, start_size_y };
AdjustWindowRect(&rect, window_style, FALSE);
g_window_adjust_x = (rect.right - rect.left) - start_size_x;
g_window_adjust_y = (rect.bottom - rect.top) - start_size_y;

... // create window, message handling loop, etc.
}


Finally, you can restrict the minimum window size so that it obeys the aspect ratio restriction. This involves answering the WM_GETMINMAXINFO message:

    case WM_GETMINMAXINFO:
{
MINMAXINFO * info = reinterpret_cast<MINMAXINFO *>(lParam);
info->ptMinTrackSize.y = ((info->ptMinTrackSize.x - g_window_adjust_x) * window_ratio_y) / window_ratio_x + g_window_adjust_y;
}
break;


I ended up removing this code this week because monitors have gotten much less homogeneous in their aspect ratios. My notebook computer, for instance, is 1440 by 900, which is an 8:5 ratio. HDTV aspect ratios are 16:9. A 1280x1024 notebook (with square pixels) has a 5:4 aspect ratio. I decided that it was better to just have my games adapt their UI and viewport to whatever resolution they're given.

No comments: