2

I'm creating a custom form (C#/Windows Forms/Vista/Windows7), and overriding WndProc to capture WM_NCPAINT, WM_NCCALCSIZE and WM_NCHITTEST to draw a custom frame. I'm almost done with it but there is a problem that I could not work around myself.

The problem is that NC_CALCSIZE makes my form shrink when I restore it after its maximized. I´ve googled it and found an answer from Bob Powell, and he stated that I neednt to handle NC_CALCSIZE when WPARAM is TRUE. After I've done that, WM_NCPAINT had no effect anymore (it does handle the WM_NCPAINT, but it does not paint the non-client area anymore, only after I invalidate it).

So, resuming, when I handle WM_NCCALCSIZE(WPARAM == TRUE) it shrinks my form, when I dont, it doesnt paint anymore.

Has anyone had this problem before? If more code is needed I can provide it. Tks.

Here's my WN_CALCSIZE code:

private void WndProcNonClientCalcSize(ref Message m)
    {
        if (m.WParam == WinAPI.FALSE)
        {
            this.Log(MethodInfo.GetCurrentMethod(), "FALSE");

            WinAPI.NCCALCSIZE_PARAMS csp;
            csp = (WinAPI.NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(WinAPI.NCCALCSIZE_PARAMS));
            csp.rectProposed.Top += this._nonClientHeight;
            csp.rectProposed.Bottom -= this._nonClientBorderThickness;
            csp.rectProposed.Left += this._nonClientBorderThickness;
            csp.rectProposed.Right -= this._nonClientBorderThickness;
            Marshal.StructureToPtr(csp, m.LParam, false);
        }
        else if (m.WParam == WinAPI.TRUE)
        {
            this.Log(MethodInfo.GetCurrentMethod(), "TRUE");

            WinAPI.NCCALCSIZE_PARAMS csp;
            csp = (WinAPI.NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(WinAPI.NCCALCSIZE_PARAMS));
            csp.rectProposed.Top += this._nonClientHeight;
            csp.rectProposed.Bottom -= this._nonClientBorderThickness;
            csp.rectProposed.Left += this._nonClientBorderThickness;
            csp.rectProposed.Right -= this._nonClientBorderThickness;
            Marshal.StructureToPtr(csp, m.LParam, false);
        }

        m.Result = WinAPI.TRUE;
    }

Here's my WM_NCPAINT code:

private bool WndProcNonClientPaint(ref Message m)
    {
        this.Log(MethodInfo.GetCurrentMethod(), string.Empty);

        this.PaintNonClient(m.HWnd, (IntPtr)m.WParam);
        m.Result = WinAPI.TRUE;
        return true;
    }

    private void PaintNonClient(IntPtr hWnd, IntPtr hRgn)
    {
        WinAPI.RECT windowRect = new WinAPI.RECT();
        WinAPI.GetWindowRect(hWnd, ref windowRect);

        Rectangle bounds = new Rectangle(0, 0,
            windowRect.Right - windowRect.Left,
            windowRect.Bottom - windowRect.Top);

        if (bounds.Width == 0 || bounds.Height == 0)
            return;

        Region clipRegion = new Region(bounds);

        if (hRgn != (IntPtr)1)
            clipRegion = Region.FromHrgn(hRgn);

        WinAPI.DCV dcv =
                WinAPI.DCV.WINDOW |
                WinAPI.DCV.INTERSECTRGN |
                WinAPI.DCV.CACHE |
                WinAPI.DCV.CLIPSIBLINGS;

        IntPtr hDC =
            WinAPI.GetDCEx(
                hWnd,
                hRgn,
                dcv);

        if (hDC == IntPtr.Zero)
            hDC = WinAPI.GetWindowDC(hWnd);

        IntPtr compatiblehDC = WinAPI.CreateCompatibleDC(hDC);
        IntPtr compatibleBitmap = WinAPI.CreateCompatibleBitmap(hDC, bounds.Width, bounds.Height);

        try
        {
            WinAPI.SelectObject(compatiblehDC, compatibleBitmap);
            WinAPI.BitBlt(compatiblehDC, 0, 0, bounds.Width, bounds.Height, hDC, 0, 0, WinAPI.SRCCOPY);

            using (Graphics g = Graphics.FromHdc(compatiblehDC))
            {
                Rectangle outterEdge = new Rectangle(0, 0, this.Width, this.Height);

                int x = this._nonClientBorderThickness;
                int y = this._nonClientHeight;
                int width = this.Width - (this._nonClientBorderThickness * 2);
                int height = this.Height - this._nonClientBorderThickness - this._nonClientHeight;

                Rectangle innerEdge = new Rectangle(x, y, width, height);

                GraphicsPath path = new GraphicsPath();

                path.AddRectangle(outterEdge);
                path.AddRectangle(innerEdge);

                using (SolidBrush brush = new SolidBrush(Color.FromArgb(45, 45, 48)))
                    g.FillPath(brush, path);

                path.Dispose();
            }

            WinAPI.BitBlt(hDC, 0, 0, bounds.Width, bounds.Height, compatiblehDC, 0, 0, WinAPI.SRCCOPY);
        }
        finally
        {
            WinAPI.DeleteObject(compatibleBitmap);
            WinAPI.DeleteDC(compatiblehDC);
        }
    }
Filipe Scur
  • 133
  • 1
  • 9
  • You are not handling the FALSE case property. That gives you a RECT (not a NCCALCSIZE_PARAMS) that contains the size of the frame. You must modify it and make it *smaller* to indicate what part of the frame is the client area. – Hans Passant Sep 17 '12 at 16:38
  • It seems the FALSE case is not the problem, since its when I modify the TRUE case that i get the problem of not getting things properly drawn. Either way I did change the false case to this, but nothing change: WinAPI.RECT rect = (WinAPI.RECT)Marshal.PtrToStructure(m.LParam, typeof(WinAPI.RECT)); rect.Left += this._nonClientBorderThickness; rect.Top += this._nonClientHeight; rect.Right -= this._nonClientBorderThickness; rect.Bottom -= this._nonClientBorderThickness; Marshal.StructureToPtr(rect, m.LParam, false); – Filipe Scur Sep 17 '12 at 17:07
  • You are returning TRUE from WM_NCCALCSIZE, but you are supposed to return some flags. I think you want WVR_HREDRAW | WVR_VREDRAW so that the repaint occurs. – Tergiver Sep 17 '12 at 18:18
  • @Tergiver, I've tried this as well, but I couldnt get it working =/ It keeps shrinking my form or it doesnt paint correctly. Anyone have an axample?? – Filipe Scur Sep 17 '12 at 19:43

4 Answers4

2

Well I couldnt get it to work, it seems the return WM_NCCALCSIZE is more complex than I expected. I simply couldnt understand it, as it didnt do what i expected it to do. I tried to do what this article says but again it was no use:

http://blogs.msdn.com/b/oldnewthing/archive/2003/09/15/54925.aspx

So i googled again and I found this article on CodeProject, describing the same problem I had:

http://www.codeproject.com/Articles/55180/Extending-the-Non-Client-Area-in-Aero

The work around was to listen to WM_SYSCOMMAND and capture SC_RESTORE, setting my form width and height as I Maximized/Restored.

My WM_NCCALCSIZE became this:

    private void WndProcNonClientCalcSize(ref Message m)
    {
        if (m.WParam == WinAPI.FALSE)
        {
            this.Log(MethodInfo.GetCurrentMethod(), "FALSE");

            WinAPI.RECT rect = (WinAPI.RECT)Marshal.PtrToStructure(m.LParam, typeof(WinAPI.RECT));
            rect.Left += this._nonClientBorderThickness;
            rect.Top += this._nonClientHeight;
            rect.Right -= this._nonClientBorderThickness;
            rect.Bottom -= this._nonClientBorderThickness;
            Marshal.StructureToPtr(rect, m.LParam, false);

            m.Result = WinAPI.FALSE;
        }
        else if (m.WParam == WinAPI.TRUE)
        {
            this.Log(MethodInfo.GetCurrentMethod(), "TRUE");

            WinAPI.NCCALCSIZE_PARAMS csp;
            csp = (WinAPI.NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(WinAPI.NCCALCSIZE_PARAMS));

            WinAPI.RECT rectNewClient = csp.rectProposed;

            rectNewClient.Left += this._nonClientBorderThickness;
            rectNewClient.Top += this._nonClientHeight;
            rectNewClient.Right -= this._nonClientBorderThickness;
            rectNewClient.Bottom -= this._nonClientBorderThickness;

            csp.rectProposed = rectNewClient;
            csp.rectBeforeMove = csp.rectProposed;

            Marshal.StructureToPtr(csp, m.LParam, false);

            m.Result = (IntPtr)(WinAPI.NCCALCSIZE_RESULTS.ValidRects);
        }
    }

and my WM_SYSCOMMAND:

    private void WndProcSysCommand(ref Message m)
    {
        UInt32 param;
        if (IntPtr.Size == 4)
            param = (UInt32)(m.WParam.ToInt32());
        else
            param = (UInt32)(m.WParam.ToInt64());

        if ((param & 0xFFF0) == (int)WinAPI.SystemCommands.SC_RESTORE)
        {
            this.Height = this._storedHeight;
            this.Width = this._storedWidth;
        }
        else if (this.WindowState == FormWindowState.Normal)
        {
            this._storedHeight = this.Height;
            this._storedWidth = this.Width;
        }
        base.WndProc(ref m);
    }

It might not be the best solution, but got the work done. If anyone could provide a better solution, I would really apreciate.

Tks, Hans Passant and Tergiver for your attention.

Filipe Scur
  • 133
  • 1
  • 9
  • 1
    Just a (small) performance note: You might want to enclose the Height and Width change with `SuspendLayout(); Height=; Width=; ResumeLayout(false)` otherwise if both Width and Height changed, you might get two re-calc-layouts instead of one. – Tergiver Sep 20 '12 at 19:15
  • Also `UInt32 param = m.WParam.ToInt32()` accomplishes the same thing as the IntPtr.Size check, cast to long, cast back to int, without the extra casting. – Tergiver Sep 20 '12 at 19:17
2

It's not problem with WM_NCCALCSIZE. When processing WM_NCCALCSISE, .NET call RestoreWindowBoundsIfNecessary() method, so the window's size changed (See Changing a window's restore position using SetWindowPlacement doesn't work on every window). You can either manually cache form's size when form is minimized and reset when it's restored, or do not call base WndProc() method, which call RestoreWindowBoundsIfNecessary()

Community
  • 1
  • 1
binhnx218
  • 125
  • 3
  • 10
0

I have had different results and I'd actually like to get public input on this solution:

protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
{
    width += BORDER_LEFT_WIDTH + BORDER_RIGHT_WIDTH;
    height += BORDER_TOP_HEIGHT + TITLEBAR_HEIGHT + BORDER_BOTTOM_HEIGHT;
    base.SetBoundsCore(x, y, width, height, specified);
}

This, in effect, counteracts the shrinkage I was running into while dealing with a similar problem.

KatDevsGames
  • 1,109
  • 10
  • 21
0

It looks like the problem has more elegant solution, though still hackish. See Custom window frame with DWM: how to handle WM_NCCALCSIZE correctly.

The difference is that solutions here either change window size causing redraw/flickering or artificially shrink bounds, while solution from the post above just say that your window does not have title/border hence size of the window and client area are equal.

By the way, if you want client area to occupy all window size WndProcNonClientCalcSize can be simplified to

private void WndProcNonClientCalcSize(ref Message m)
{
  m.Result = IntPtr.Zero;
}
Community
  • 1
  • 1
Crabba
  • 21
  • 2