.NET Form with a customized border replacement

.NET Windows Forms offer the standard border choices of the operating system. You can have a borderless form but by default the user cant move or resize it.

This project was to create a form with no, or a very thin 'border' and for the size of that border to be a simple property that developer can change at design time. But also this form would still allow end-user to move and resize the window using standard mouse operations.

We also wanted to have a 'caption' area at the top of the form still, but have the main menu sited on that caption bar, reducing the wasted space of non-client area in standard Windows, and have a slightly more modern look.

Something that looks like this:

thinborderformsample.png

...showing the new user-drawn caption bar with an icon, some text and the main menu all on one line, and simple close control button. The coloring just to highlight the "new" client area for docked child controls but as this is all of the client area anyway there's nothing to stop positioning any child controls right on the title area if you wanted -- allowing some of the caption area free to support moving the form was our requirement but that's optional. And of course if the point is not to have such an obvious border you might not choose to paint in contrasting colors but just have an implied border with the resize functionality.

The deliverable will be a library with a Form that you could derive your own application form from and get the thin border and caption/menu behavior, whilst still allowing other Form design requirements like docking child controls.

This blog is just detailing the technique to do that, by capturing non-client Windows messages and some of our own painting, in case it proves useful to anyone requiring something similar.

Outline

Overall the plan is:

  • have a class derived from the standard System.Windows.Form
  • this will set a BorderStyle of none.
  • we'll have our own concept of a caption and border area within the client area of the window, default 4 pixel border all round and, 24 pixel caption at the top (i.e. much ilke a standard window but now these are easy to set from Form properties)
  • override WndProc and handle non-client message to easily get the resize functionality when user clicks and drags the border area
  • override OnPaint so we can draw our own "Caption" at the top of the Form, and have click and draw in that caption implement the move functionality
  • change the Padding property of the Forms that docked child controls do not over lap our fake borders and caption
  • if a MainMenu is set on the Form then move that main menu now into our "caption" area releasing more space for other controls.
  • pack this class in a library that can be shared or used elsewhere. Deriving from the new base class and adding some stuff in the designer, we'll get a Form that looks like the screenshot above as a base for developer to create their own application.

Detail

Now for some detail on the implementation.

  1. Create a new project for Windows Forms. Set the form to have BorderStyle of none - by default that is then a non-resizable or movable window with no caption bar. If you gave it a main menu it would look a bit odd...just a title-less menu stuck at the top.

  2. We're going to need some new properties to control the size of our new border and caption areas that corresponds to a 'border' on all sides of our form, which will be the area where the end-user can click and drag to resize and to the size of the 'caption' bar at the top which we will paint our selves and allow user the click and drag in order to move the form around.

     public int BorderWidth { get; set; }
     public int CaptionHeight { get; set; }
    
  3. Override WndProc and handle non-client hit test messages and work our which edge or corner the user has moved over inside our client area (i.e. we're basically creating our own non-client area

         protected override void WndProc(ref Message m)
         {
             switch (m.Msg)
             {
                 case WM_NCHITTEST:
                     {
                         Point pos = new Point(m.LParam.ToInt32());
                         pos = this.PointToClient(pos);
                         //
                         // over right edge
                         if (pos.X >= this.ClientSize.Width - BorderWidth)
                         {
                             // over right bottom corner
                             if (pos.Y >= this.ClientSize.Height - BorderWidth)
                             {
                                 m.Result = (IntPtr)HitTestValues.HTBOTTOMRIGHT;
                                 return;
                             }
                             else if (pos.Y < BorderWidth)
                             {
                                 m.Result = (IntPtr)HitTestValues.HTTOPRIGHT;
                                 return;
                             }
                             else
                             {
                                 m.Result = (IntPtr)HitTestValues.HTRIGHT;
                                 return;
                             }
                         }
                         // over left edge
                         if (pos.X < BorderWidth)
                         {
                             if (pos.Y >= this.ClientSize.Height - BorderWidth)
                             {
                                 m.Result = (IntPtr)HitTestValues.HTBOTTOMLEFT;
                                 return;
                             }
                             else if (pos.Y < BorderWidth)
                             {
                                 m.Result = (IntPtr)HitTestValues.HTTOPLEFT;
                                 return;
                             }
                             else
                             {
                                 m.Result = (IntPtr)HitTestValues.HTLEFT;
                                 return;
                             }
                         }
                         // between left/right edges and over bottom
                         if (pos.Y >= this.ClientSize.Height - BorderWidth)
                         {
                             m.Result = (IntPtr)HitTestValues.HTBOTTOM;
                             return;
                         }
                         // over top edge
                         else if (pos.Y < BorderWidth)
                         {
                             m.Result = (IntPtr)HitTestValues.HTTOP;
                             return;
                         }
                         // over top caption (but not over border edge)
                         else if (pos.Y < CaptionHeight)
                         {
                             // close box in top right...
                             if (this.ControlBox && pos.X > this.ClientSize.Width - CaptionHeight)
                                 m.Result = (IntPtr)HitTestValues.HTCLOSE;
                             // system menu/icon in top left...
                             else if (this.ShowIcon && pos.X < CaptionHeight)
                                 m.Result = (IntPtr)HitTestValues.HTSYSMENU;
                             // else just in the middle of caption for moving window around
                             else
                                 m.Result = (IntPtr)HitTestValues.HTCAPTION;
                             return;
                         }
                         break;
                     }
                 case WM_NCLBUTTONDOWN:
                     {
                         // handle mouse down on HTCLOSE in order to stop base starting a mouse capture
                         if (m.WParam == (IntPtr)HitTestValues.HTCLOSE
                          || m.WParam == (IntPtr)HitTestValues.HTSYSMENU
                             )
                         {
                             m.Result = (IntPtr)0;
                             return;
                         }
                         break;
                     }
                 case WM_NCLBUTTONUP:
                     {
                         // handle mouse down on HTCLOSE and close
                         if (m.WParam == (IntPtr)HitTestValues.HTCLOSE)
                         {
                             this.Close();
                             m.Result = (IntPtr)0;
                             return;
                         }
                         else if (m.WParam == (IntPtr)HitTestValues.HTSYSMENU)
                         {
                             // TODO: show system menu
                         }
                         break;
                     }
             }
             base.WndProc(ref m);
         }
    
  4. That WndProc also has a case for NCLBUTTONDOWN. In this case we need to tell Windows we're handling the HTCLOSE area otherwise it will start of mouse capture and stop us handling close.

  5. Also in WndProc we need to then actually handle the button up on the close button so we trap that and close our form. I've also coded for the sysmenu hittest i.e. for the icon in top left normally to then show the system menu -- but Windows is expecting borderless form to not have system menus so i've not found a way to get that to work (other than to re-implement the entire system menu for min/max/restore/close).

  6. With just the code as above we'll get a move-able and resize-able borderless form ... any client controls we add could go right up to the border and would stop us capturing mouse action over those controls. One solution to that is to force the "Padding" property on the form to always be at least the size of our border and caption and then Docked child controls wont conflict with our border.

  7. We can also paint the new caption area by overriding OnPaint allowing the end-user now to move the form around if the click and drag in this area...

         protected override void OnPaint(PaintEventArgs e)
         {
             base.OnPaint(e);
             // if includes redrawing top of window
             if (e.ClipRectangle.Y < CaptionHeight)
             {
                 var rc = new Rectangle(0, 0, this.ClientSize.Width, CaptionHeight);
                 var sb = new SolidBrush(CaptionColor);
                 e.Graphics.FillRectangle(sb, rc);
              }
    
  8. Another useful option for OnPaint would be to paint an icon top left if the is one...

             if (this.ShowIcon)
             {
                     var rcIcon = new Rectangle(0, 0, CaptionHeight, CaptionHeight);
                     e.Graphics.DrawIcon(this.Icon, rcIcon);
                 }
             }
    
  9. And to draw a close button top right using stardard Windows routine...

             if (this.ControlBox)
             {
                     var r = new Rectangle(this.ClientRect.Width-CaptionHeight, 0, CaptionHeight, CaptionHeight);
                     ControlPaint.DrawCaptionButton(e.Graphics, r, CaptionButton.Close, ButtonState.Normal | ButtonState.Flat);
             }
    

    BUT a much nicer simpler look just to draw a simple x ourselves...

             if (this.ControlBox)
             {
                     Pen p = Pens.Black;
                     int o = CaptionHeight / 3; // offset to cross ends inside the box
                     int w = CaptionHeight - o - o;// width/height of the cross
                     int l = this.ClientRect.Width-CaptionHeight;
                     e.Graphics.DrawLine(p, l + o, o, l + o + w, o + w);
                     e.Graphics.DrawLine(p, l + o, o + w, l + o + w, o);
             }
    
  10. Finally if there is a main menu defined for this form then lets move it to somewhere on the new caption area...

           if (this.MainMenuStrip != null)
           {
                    // start menu half way along the "caption"
                    var menuleft = this.ClientRect.Width/2;
                    this.MainMenuStrip.SetBounds(menuleft, 0, this.MainMenuStrip.Width, CaptionHeight);
           }
    

Summary

SO that's the key steps to implement the required overrides and custom painting to get the effect we want on the form border.

The full source code for the project is here with the implementation enhanced a bit from the above code snippets or the is a nuget package if you just wanted to use that implementation as a library ,but its more interesting to enhance and customize a bit yourself so I recommend taking the source.