Win32ComponentPeer and ComponentBoundsConstrainer issue


#1

The Win32ComponentPeer neglects to take into account the windowBorder in the function handleSizeConstraining(). So if you were to use setSizeLimits(500,500,0x7fffffff,0x7fffffff) on its current constrainer, rather than the content component being limited to 500 by 500, the entire window including the native title bar and standard windows border gets limited to 500 by 500, with the content component being smaller.

The obvious solution is to take into account the windowBorder as is done elsewhere. Here’s a corrected version:

// juce_win32_Windowing.cpp
LRESULT handleSizeConstraining (RECT* const r, const WPARAM wParam)
{
  if (constrainer != 0 && (styleFlags & (windowHasTitleBar | windowIsResizable)) == (windowHasTitleBar | windowIsResizable))
  {
    Rectangle<int> pos (r->left+windowBorder.getLeft(),
      r->top+windowBorder.getTop(),
      (r->right-windowBorder.getRight()) - (r->left+windowBorder.getLeft()),
      (r->bottom-windowBorder.getBottom()) - (r->top+windowBorder.getTop()));

    constrainer->checkBounds (pos, windowBorder.addedTo (component->getBounds()),
      Desktop::getInstance().getAllMonitorDisplayAreas().getBounds(),
      wParam == WMSZ_TOP || wParam == WMSZ_TOPLEFT || wParam == WMSZ_TOPRIGHT,
      wParam == WMSZ_LEFT || wParam == WMSZ_TOPLEFT || wParam == WMSZ_BOTTOMLEFT,
      wParam == WMSZ_BOTTOM || wParam == WMSZ_BOTTOMLEFT || wParam == WMSZ_BOTTOMRIGHT,
      wParam == WMSZ_RIGHT || wParam == WMSZ_TOPRIGHT || wParam == WMSZ_BOTTOMRIGHT);
    r->left = pos.getX()-windowBorder.getLeft();
    r->top = pos.getY()-windowBorder.getTop();
    r->right = pos.getRight()+windowBorder.getRight();
    r->bottom = pos.getBottom()+windowBorder.getBottom();
  }

  return TRUE;
}

Unfortunately this suffers from a couple of issues:

  1. This only works when setUsingNativeTitleBar (true). For a non native title bar window, it doesn’t even go through the function handleSizeConstraining, inexplicably.

  2. If the content component has an attached menu bar, the menu bar is not subtracted from the calculation for constraining the window size. In other words, if you want your content component in a native title bar enabled window that has my code from above, to have a minimum size of 500, 500, and your content component has a menu bar, then the resulting vertical size will be less than 500. But this will be the topic of another post.


#2

Another problem is that when using setUsingNativeTitleBar(true), ComponentBoundsConstrainer::resizeStart() and ComponentBoundsConstrainer::resizeEnd() do not get called. This should happen somewhere in juce_win32_Windowing but its not there.

So I went ahead and fixed this in my version of juce_win32_Windowing.cpp, feel free to use it, or to modify it as you see fit.

First we need to handle the left mouse being pressed in the resizable window border by looking for NCLBUTTONDOWN and then checking which non client item was hit tested:

// juce_win32_Windowing.cpp
/* We will never get WM_NCLBUTTONUP because DefWindowProc
   captures the mouse during a window border resizing operation, so
   we will use a state variable and do it on WM_CAPTURECHANGED */
case WM_NCLBUTTONDOWN:
    if (!sendInputAttemptWhenModalMessage())
    {
      switch (wParam)
      {
      case HTBOTTOM:
      case HTBOTTOMLEFT:
      case HTBOTTOMRIGHT:
      case HTGROWBOX: // same as HTSIZE
      case HTLEFT:
      case HTRIGHT:
      case HTTOP:
      case HTTOPLEFT:
      case HTTOPRIGHT:
        {
          if (constrainer != 0 &&
             (styleFlags & (windowHasTitleBar | windowIsResizable)) == (windowHasTitleBar | windowIsResizable))
          {
            // if this goes off it means we missed a call to resizeEnd()
            jassert (!needToCallResizeEnd);
            constrainer->resizeStart();
            needToCallResizeEnd = true;
          }
        }
        break;
      default:
        break;
      };
    }
    break;

case WM_NCRBUTTONDOWN:
case WM_NCMBUTTONDOWN:
  sendInputAttemptWhenModalMessage();
  break;

Unfortunately we can’t respond to WM_NCLBUTTONUP because the DefWindowProc captures the mouse using SetCapture() in order to track the dragging of the resize border (If a window has captured the mouse, this message is not posted).

So the workaround is to set a flag needToCallResizeEnd to true and wait for WM_CAPTURECHANGED:

case WM_CAPTURECHANGED:
    if (needToCallResizeEnd)
    {
      if (constrainer!=0 )
        constrainer->resizeEnd ();
      needToCallResizeEnd = false;
    }

    doCaptureChanged();
    return 0;

bool needToCallResizeEnd will have to have to be added as a class member and initialized to false

The result of this change is that any ComponentBoundsConstrainer that is set on a TopLevelWindow that uses setUsingNativeTitleBar(true) will have resizeStart() and resizeEnd() called appropriately, which is exactly the behavior when using setUsingNativeTitleBar(false).

Thanks for listening!


#3

It was never intended to work like you’re suggesting - the constrainer has always acted on the whole window, not the content component. Even if it was “wrong” to do it that way, which I don’t really think it is, I couldn’t change it without messing up people’s existing code. If you want to set specific sizes for your content component, you can always just add the frame size to the bounds limits that you give to the constrainer.

Interesting point about those start/end callbacks though - thanks, I’ll take a look at that…


#4

Yeah that would mess people up, and I don’t think there is a “right” way - there are use-cases for both scenarios. I wrote my own small shim that goes between the constrainer and the window (see my other post) but it needs a few things to be tweaked in Juce. As long as there is enough functionality to cleanly get the information needed to make the adjustment to the window constraints, I’m cool.

Constrainer acting on the whole window is inconsistent with other ComponentPeer functions that take the non client area into account, for example consider:

    void ComponentPeer::setSize (int w, int h)
    {
        SetWindowPos (hwnd, 0, 0, 0,
                      w + windowBorder.getLeftAndRight(),
                      h + windowBorder.getTopAndBottom(),
                      SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOOWNERZORDER);
    }

setSize() leaves the content component at an exact size since it adds in the window border.