Custom window frame with DWM: how to handle WM_NCC

2019-04-30 11:28发布

问题:

I'm trying to make a custom window frame for my form using DWM. The platform is C# WinForms, Pinvoking DWM.

Following the MSDN article on making custom window frame with DWM, the main steps are next:

  1. Remove standard frame (non-client area), returning 0 in answer to WM_NCCALCSIZE message
  2. Extend the frame into client area using DwmExtendFrameIntoClientArea function

I handle WM_NCCALCSIZE message in the next way:

protected override void WndProc(ref Message m)
{
   switch (m.Msg)
   {
       case WM_NCCALCSIZE:
            if (isDwmWindowFramePaintEnabled() && m.WParam != IntPtr.Zero)
            {
                m.Result = IntPtr.Zero;
            }
            else
            {
                base.WndProc(ref m);
            }
            return;
   }
}

According to MSDN documentation on WM_NCCALCSIZE,

When wParam is TRUE, simply returning 0 without processing the NCCALCSIZE_PARAMS rectangles will cause the client area to resize to the size of the window, including the window frame. This will remove the window frame and caption items from your window, leaving only the client area displayed.

Everything is fine and works for me except one issue. When I maximize/restore window, it's always growing a little bit when it gets restored. I think, the issue is something like this:

  1. When window gets restored, it contains client area only
  2. Windows tries to give some non-client area to the window
  3. In WM_NCCALCSIZE client area grows to contain non-client area

So, like this window grows a little bit each time I maximize/restore it. I need to remove non-client area to paint custom form frame with DWM. I can't simply set window border style to none as then DWM will not paint the window caption and borders.

Please, help to solve the issue and happily have a custom window frame.

回答1:

This is actually a bug in Windows Forms and there's a workaround. In function Form.SizeFromClientSize(int, int) the AdjustWindowRectEx function is used to translate the size and it always uses the default measurements and can't be overridden. This function is called from two places:

  1. RestoreWindowBoundsIfNecessary in WM_WINDOWPOSCHANGED window message handler
  2. SetClientSizeCore

The workaround is the following:

  • Override CreateParams in the Form:

    private bool createParamsHack;
    
    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            // Remove styles that affect the border size
            if (createParamsHack)
                cp.Style &= ~(int)(WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_THICKFRAME);
            return cp;
        }
    }
    
  • Override WndProc and insert the following code to handle WM_WINDOWPOSCHANGED:

        if (m.Msg == WM_WINDOWPOSCHANGED)
        {
            createParamsHack = true;
            base.WndProc(ref m);
            createParamsHack = false;
        }
    
  • Override SetClientSizeCore:

    protected override void SetClientSizeCore(int x, int y)
    {
        createParamsHack = true;
        base.SetClientSizeCore(x, y);
        createParamsHack = false;
    }
    

It may also be good idea to override SizeFromClientSize(Size) to return the correct measurements, but it is not strictly necessary.