Creating a window that can be resized in only one direction


Today's Little Program shows a window that can be resized in only one direction, let's say vertically but not horizontally.

Start with the scratch program and make these changes:

UINT OnNcHitTest(HWND hwnd, int x, int y)
{
 UINT ht = FORWARD_WM_NCHITTEST(hwnd, x, y, DefWindowProc);
 switch (ht) {
 case HTBOTTOMLEFT:  ht = HTBOTTOM; break;
 case HTBOTTOMRIGHT: ht = HTBOTTOM; break;
 case HTTOPLEFT:     ht = HTTOP;    break;
 case HTTOPRIGHT:    ht = HTTOP;    break;
 case HTLEFT:        ht = HTBORDER; break;
 case HTRIGHT:       ht = HTBORDER; break;
 }
 return ht;
}

HANDLE_MSG(hwnd, WM_NCHITTEST, OnNcHitTest);

We accomplish this by removing horizontal resize behavior from the left and right edges and corners. For the corners, we remove the horizontal resizing, but leave the vertical resizing. For the edges, we remove resizing entirely by reporting that the left and right edges should act like an inert border.

Wait, we're not done yet. This handles resizing by grabbing the edges with the mouse, but it doesn't stop the user from hitting Alt+Space, followed by S (for Size), and then hitting the left or right arrow keys.

For that, we need to handle WM_GET­MIN­MAX­INFO.

void OnGetMinMaxInfo(HWND hwnd, LPMINMAXINFO lpmmi)
{
 RECT rc = { 0, 0, 500, 0 };
 AdjustWindowRectEx(&rc, GetWindowStyle(hwnd), FALSE,
                    GetWindowExStyle(hwnd));

 // Adjust the width
 lpmmi->ptMaxSize.x =
 lpmmi->ptMinTrackSize.x =
 lpmmi->ptMaxTrackSize.x = rc.right - rc.left;
}

HANDLE_MSG(hwnd, WM_GETMINMAXINFO, OnGetMinMaxInfo);

This works out great, except for the case of being maximized onto a secondary monitor, because we run into the mixed case of being small than the monitor in the horizontal direction, but larger than the monitor in the vertical direction.

void OnGetMinMaxInfo(HWND hwnd, LPMINMAXINFO lpmmi)
{
 RECT rc = { 0, 0, 500, 0 };
 AdjustWindowRectEx(&rc, GetWindowStyle(hwnd), FALSE,
                    GetWindowExStyle(hwnd));

 // Adjust the width
 lpmmi->ptMaxSize.x =
 lpmmi->ptMinTrackSize.x =
 lpmmi->ptMaxTrackSize.x = rc.right - rc.left;

 // Adjust the height
 MONITORINFO mi = { sizeof(mi) };
 GetMonitorInfo(MonitorFromWindow(hwnd,
                    MONITOR_DEFAULTTOPRIMARY), &mi);
 lpmmi->ptMaxSize.y = mi.rcWork.bottom - mi.rcWork.top
                    - lpmmi->ptMaxPosition.y + rc.bottom;
}

The math here is a little tricky. We want the window height to be the height of the work area of the window monitor, plus some extra goop in order to let the borders hang over the edge.

The first two terms are easy to explain: mi.rcWork.bottom - mi.rcWork.top is the height of the work area.

Next, we want to add the height consumed by the borders that hang off the top of the monitor. Fortunately, the window manager told us exactly how much the window is going to hang off the top of the monitor: It's in lpmmi->ptMaxPosition.y, but as a negative value since it is a coordinate that is off the top of the screen. We therefore have to negate it before adding it in.

Finally, we add the borders that hang off the bottom of the work area.

Yes, handling this mixed case (where the window is partly constrained and partly unconstrained) is annoying. Sorry.

Comments (13)
  1. Andrew says:

    The beginning of my programming career having roughly coincided with the release of the .NET Framework, I'm thankful for all the smart people who hid Win32 (which I've never used directly) behind a simple and intuitive API.  In Windows Forms, this is a matter of setting MaximumSize and MinimumSize such that their heights or widths are equal.

    [But I think you still get a resize cursor when you grab the left or right edge, which is misleading to the user. -Raymond]
  2. alegr1 says:

    You missed an opportunity to show how to handle the WM_SETCURSOR to make sure it won't show a resize cursor where it should not.

    [That's done by the WM_NCHITTEST handler. -Raymond]
  3. Rick C says:

    These small articles that successively build on each other are pretty nice.

  4. Eddie Lotter says:

    A great example of how to do something properly. Thanks Raymond.

  5. Joshua says:

    I'm curious, why don't you forward WM_GETMINMAXINFO and clamp the results?

  6. jps says:

    Does anyone know if any extra steps are needed for this code when running at higher DPI settings? I'm thinking of the 125%, 150% display properties (assuming the application is marked as DPI-aware).

  7. alegr1 says:

    [That's done by the WM_NCHITTEST handler. -Raymond]

    That's what I get for skimming over the text… I needed more coffee.

  8. alegr1 says:

    @Joshua:

    The window then would show the resize cursors on the sides where it doesn't allow to resize. And wrong resize cursors on the corners.

  9. Neil says:

    @alegr1 I hope Joshua's talking about the Alt+Space, S case.

  10. Joshua says:

    I was talking about the maximize case.

    @jps: No, unless you need to convert between units in the resize operation (I found a way to write almost all cases in a way that only requires adjustment at setup time).

  11. alegr1 says:

    @Neil:

    Just like me, Joshua fell victim of skimming over the code. The proposed OnGetMinMaxInfo function does, in fact, clip MINMAXINFO in horizontal direction.

  12. Joshua says:

    I don't see a FORWARD_GETMINMAXINFO there.

  13. 640k says:

    [But I think you still get a resize cursor when you grab the left or right edge, which is misleading to the user. -Raymond]

    Raymond, why don't you file a bug report to .net team.

    [Because I don't take orders from you. Also, I'm not convinced that it's a bug. They are doing what you said: The minimum and maximum widths are the same. You didn't say "Remove the resize cursor from my left and right edges." -Raymond]

Comments are closed.

Skip to main content