Search
Close this search box.

Drawing Custom Borders in Windows Forms

Note: This is first draft of unpublished article. Check here often for updates. I await your comments and suggestions.

Download source code from ProjectDistributor.net

Figure 1. Sample form with custom drawn border.

I have split the code into two primary classes. First one, FormWithNonClientArea, extends the standard Form class by adding support for non-client area messages (more on this shortly) and can be uses for various scenarios. Second one, CustomBorderForm, utilizes these messages and represents a base class for drawing graphical borders composed of several bitmap parts. It also draws a form header including icon, text and form buttons (more on this later). This way I can separate dirty plumbing required for enabling non-client drawing and actual drawing of the graphical elements. So lets see how it works.

Extending Form with Non-Client Area Painting

Each window we see on screen (be it a Form, UserControl or any other Control) is described by two rectangles: the bounds of the window and it’s client area. Bounds specify the location and size of the window as a whole while the client area specifies the region inside the window that is available for client controls. By default Windows Forms allows us to access only the client part of the window. To gain access to the non-client part we need to intercept some additional windows messages. We can do this by overriding the WndProc message loop. For each message I defined dedicated method, so my WndProc method only redirects calls to this methods.

Positioning the Client Ractangle

If we are going to draw our custom borders good chances are that their size and proportions will differ from the standard ones. To correct this we need to specify a new client rectangle for the window. This is done in the WM_NCCALCSIZE message. This message can be raised in two ways. When WParam is equal to zero the LParam points to RECT structure with window bound that we should adjust to proposed client ractangle. Alternatively, when WParam value is one the LParam points to NCCALCSIZE_PARAMS strucure allowing to move the existing client area inside the window. For our purpose we will simply adjust the proposed rectangle to required coordinates.

private void WmNCCalcSize(ref Message m) {
  if (m.WParam == NativeMethods.FALSE) {
    NativeMethods.RECT ncRect =
        (NativeMethods.RECT)m.GetLParam(typeof(NativeMethods.RECT));
    Rectangle proposed = ncRect.Rect;
    OnNonClientAreaCalcSize(ref proposed);
    ncRect = NativeMethods.RECT.FromRectangle(proposed);
    Marshal.StructureToPtr(ncRect, m.LParam, false);
    m.Result = IntPtr.Zero;
  } else if (m.WParam == NativeMethods.TRUE) {
    NativeMethods.NCCALCSIZE_PARAMS ncParams =
        (NativeMethods.NCCALCSIZE_PARAMS)m.GetLParam(
            typeof(NativeMethods.NCCALCSIZE_PARAMS));
    Rectangle proposed = ncParams.rectProposed.Rect;
    OnNonClientAreaCalcSize(ref proposed);
    ncParams.rectProposed = NativeMethods.RECT.FromRectangle(proposed);
    Marshal.StructureToPtr(ncParams, m.LParam, false);
  }
}

Note that this method calls a virtual OnNonClientAreaCalcSize method taking a Rectangle that you can overwrite in your code.

Painting the non-client area

The main message responsible for painting the non-client area is the WM_NCPAINT message. The WParam for this message contains the handle to a clip region or 1 if entire window should be repainted. So to paint anything we only need to create a Graphics object from the window handle and use it as we would in the typical OnPaint method.

private void WmNCPaint(ref Message msg) {
  PaintNonClientArea(msg.HWnd, (IntPtr)msg.WParam);
  msg.Result = NativeMethods.TRUE;
}

Now is the tricky part; If you leave it that way you quickly notice that on some occasions you still get some parts of the standard border painted over your brand new framing. That indicates that there are some other messages that cause painting in the non-client area.

The first one is the WM_SETTEXT message that transports new title for the window (stored as Text property on the Form). Apparently it also repaints the border in order to update the title bar. Of course, we still want to send out the new title so we need to pass the message to the DefWndProc method. But we will handle painting on our own.

private void WmSetText(ref Message msg) {
  DefWndProc(ref msg);
  PaintNonClientArea(msg.HWnd, (IntPtr)1);
}

The second culprit happens to be the WM_ACTIVATE message that is responsible for switching the window active state. Window is active when it is the top level window that you interact with and it has different border to show that. When you switch to another window the first one updates its border to indicate that it has lost the focus. The WParam of this messages holds the window active state and is 1 when border should be drawn as active and zero otherwise. We will handle the painting and skip to the DefWndProc only when window is minimized.

private void WmNCActivate(ref Message msg) {
  bool active = (msg.WParam == NativeMethods.TRUE);
  if (this.WindowState == FormWindowState.Minimized)
    DefWndProc(ref msg);
  else {
    PaintNonClientArea(msg.HWnd, (IntPtr)1);
    msg.Result = NativeMethods.TRUE;
  }
}

I agree that this is big design inconsequence and all painting should be done in one place but it is around for a long time and we must live with it. Now that we cleared this out we can get down to actual painting.

The most important thing here is to get the correct hDC handle and we wil use native GetDCEx function for that. It takes three parameters: the window handle, the clip region and option. First two we got already from the messages. As for the options the MSDN states that only WINDOW and INTERSECTRGN are needed, but other sources confirm that CACHE is required on Win9x and you need CLIPSIBLINGS to prevent painting on overlapping windows.

If we get a valid hDC we can quickly create the Graphics object using Graphics.FromHdc() method, paint our stuff and dispose it. Here it is worth noting that when we dispose a Graphics instance it will also automatically free the hDC so there is no need for calling the ReleaseDC manually.

private void PaintNonClientArea(IntPtr hWnd, IntPtr hRgn) {
  NativeMethods.RECT windowRect = new NativeMethods.RECT();
  if (NativeMethods.GetWindowRect(hWnd, ref windowRect) == 0)
    return;

  Rectangle bounds = new Rectangle(0, 0, windowRect.right - windowRect.left,
                                   windowRect.bottom - windowRect.top);

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

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

  IntPtr hDC = NativeMethods.GetDCEx(
      hWnd, hRgn,
      (int)(NativeMethods.DCX.DCX_WINDOW | NativeMethods.DCX.DCX_INTERSECTRGN |
            NativeMethods.DCX.DCX_CACHE | NativeMethods.DCX.DCX_CLIPSIBLINGS));

  if (hDC == IntPtr.Zero)
    return;

  using (Graphics g = Graphics.FromHdc(hDC)) {
    OnNonClientAreaPaint(new NonClientPaintEventArgs(g, bounds, clipRegion));
  }
}

At the begining ot this method I use native GetWindowRect function to get the correct coordinates of the window. At this point the Bounds property is not accurate and especially during resizing seems to always stay behind. Next I validate window size as obviously no painting is needed when it is empty. The actual painting should be done in the virtual OnNonClientAreaPaint method.

Removing flicker with double-buffering

Unfortunatelly painting this way is fine only as long as you don’t try to resize the window. When you do you will see very unpleasant flickering. Totally not cool. We need to apply double-buffering in order to fix it and I just found a cool mechanism in .NET Framework that should help with that.

There is a class called a BufferedGraphics buried in the System.Drawing namespace. It’s the same class that is used when you set DoubleBuffered flag on any control. (To be honest I haven’t checked if this class existed prior to .NET 2.0). There is also a factory class called BufferedGraphicsManager that we use to create such object. The Allocate method takes either an existing Graphics object or the targetDC handle. Having an instance of BufferedGraphics we obtain a real Graphics object, do the painting as usual, and then call the Render method to draw the buffered image to the screen (presumably using some form of bit blitting).

using (BufferedGraphics bg = BufferedGraphicsManager.Current.Allocate(hDC,
                                                                      bounds)) {
  Graphics g = bg.Graphics;
  OnNonClientAreaPaint(new NonClientPaintEventArgs(g, bounds, clipRegion));
  bg.Render();
}

Whew, the above code looks to simple to possibly work. And indeed it doesn’t. It all looks good when the window stays active, but when it gets covered by another window suddenly all of the client area gets painted in black. So there is something missing, like establishing a clip region to exclude this area from bliting. I hope that someone smarter then me could help and figure out a better way to fix this.

A not so scary ghost story

There are two more things that need to be done in order to get perfectly drawn custom border. First thing is to completely get rid of XP themes on our window. We have already taken over all painting but when themes are turned on they also might affect other aspects of window. For example thay would likely change the window shape to something non-rectangular (like adding round corners) and obviously we want to prevent this. We will use the SetWindowTheme native function with empty parameters to completely disable theming on the current window. Note however that this will only affect the window itself so you don’t need to worry that you loose theming on the control placed in it’s content area.

As for the second thing, I wonder how many of you heard about windows ghosting feature? I didn’t know about it until recently. Quoting MSDN “Window ghosting is a Windows Manager feature that lets the user minimize, move, or close the main window of an application that is not responding.” Basically when the process doesn’t respond to window messages within designeted time (hangs) the Windows Manager will finally loose patience and draw the window frame by itself allowing the user to do something with the application. This can hapen when the process executes some long running task (like query or complex processing) in the same thread as the windows message loop.

In theory for a well written application that delegates all heavy processing to background workers this should never happen. But I haven’t written such application yet. This feature can be disabled using DisableWindowGhosting native function but only for the entire application. Now it’s your decision whether you want to present the users with consistent user experience even on these odd occasions or you can cope with some occasional quirks but let the user control the situation all the time.

protected override void OnHandleCreated(EventArgs e) {
  NativeMethods.SetWindowTheme(this.Handle, "", "");
  NativeMethods.DisableProcessWindowsGhosting();
  base.OnHandleCreated(e);
}

Handling mouse actions

After we have positioned and painted the border it’s time to make it behave like the normal one does. One such behavior is indicating whether the form is sizable when mouse moves on the border. Another is that when we double-click on the title bar the windows maximizes or restores respectively. And of course the window recognizes when mouse is over or user clicks on the form buttons (minimize, maximize, close, and help). To make this work properly we need to tell the border how all these elements are positioned on our form. This is done in the WM_NCHITTEST message. The LParam holds the screen coordinates of current mouse position. As a result we should return a hit-test code telling the system what part of window the mouse is over.

private void WmNCHitTest(ref Message m) {
  Point clientPoint = this.PointToWindow(new Point(msg.LParam.ToInt32()));
  m.Result = new System.IntPtr(OnNonClientAreaHitTest(clientPoint));
}

When mouse is on the border edge and the window is resizable we should return values like HTLEFT, HTTOPRIGHT or HTBOTTOM . When mosue is over one of the window buttons we return code for that buton (HTMINBUTTON, HTMAXBUTTON, HTCLOSE). To indicate that mouse hovers over the title bar we can pass the HTCAPTION value. Finally when mouse is inside the client rectangle we should return the HTCLIENT value.

Capturing mouse move is quite simple. The WM_NCMOUSEMOVE message delivers new mouse position each time it is moved over the non client area. Here, I am using the standard MouseMoveEventArgs to pass this to the virtual method.

private void WmNCMouseMove(ref Message msg) {
  Point clientPoint = this.PointToWindow(new Point(msg.LParam.ToInt32()));
  OnNonClientMouseMove(new MouseEventArgs(MouseButtons.None, 0, clientPoint.X,
                                          clientPoint.Y, 0));
  msg.Result = IntPtr.Zero;
}

To capture mouse click we should intercept the WM_NCLBUTTONDOWN and WM_NCLBUTTONUP messages, for the left mouse button (and similar for the other two). For all these messages the WParam contains the hit-test value that we returned when processing the WM_NCHITTEST message, and the LParam contains the screen coordinates of the mouse cursor. I’m using extended NonClientMouseEventArgs to pass all this information to the virtual method. In return the method should set the Handled flag to indicate that our application processed this message.

private void WmNCLButtonDown(ref Message msg) {
  Point pt = this.PointToWindow(new Point(msg.LParam.ToInt32()));
  NonClientMouseEventArgs args = new NonClientMouseEventArgs(
      MouseButtons.Left, 1, pt.X, pt.Y, 0, msg.WParam.ToInt32());
  OnNonClientMouseDown(args);
  if (!args.Handled) {
    DefWndProc(ref msg);
  }
  msg.Result = NativeMethods.TRUE;
}

Continued in part two, where you learn how to actually construct a border from bitmap parts and how to handle title bar buttons.

posted on Monday, July 04, 2005 9:00 PM

This article is part of the GWB Archives. Original Author:  Szymon Kobalczyk’s Blog

Related Posts