C#重绘windows窗体标题栏和边框
摘要
windows桌面应用程序都有标准的标题栏和边框,大部分程序也默认使用这些样式,一些对视觉效果要求较高的程序,如QQ, MSN,迅雷等聊天工具的样式则与传统的windows程序大不相同,其中迅雷还将他们的BOLT界面引擎开放,使得大家也可以创建类似迅雷一样的界面。 那么这些软件的界面是怎样实现的呢,使用C#是否也可以实现类似界面?
重绘方式
常见的自定义标题栏和边框的方式有两种,一种是隐藏标题栏和边框(称为非客户区),然后在客户区(可以放置控件的空间)使用一些常用的控件和图片来表示边 框,这种方式较简单而麻烦,但如标题栏的拖动,边框的拖拽来改变窗体大小等效果,则有需要重新实现,另外有些客户区的鼠标事件,控件布局等也需要注意调 整;另一种则是大部分软件实现方式,也较难一些;它利用windows的消息机制,截获windows消息,从而改变消息的行为。即windows的一些 消息,会引起窗体绘制或重绘标题栏和边框的行为,因此只要结果这部分消息,然后开发人员自己处理绘制过程,并忽略默认行为,从而达到自定义的目的。 C#绘制接口
windows消息对于C#开发新手来说较生疏,原因是.net已经将windows消息机制进行了封装,使得我们很难发现windows消息的踪迹,其 实它是以另一个身份存在着--事件。如控件的OnClick,Mouse等事件,都是对windows消息的封装,这样的目的更容易理解,和运 用。.net提供了处理消息的接口,常用的方法为Control控件的void WndProc(ref Message m)方法,该方法用于接收任何发送到该控件的windows消息。那么我们就可以通过重写该方法来截获绘制窗体标题栏和边框的消息了。
找到了截获windows消息的接口,那么就需要知道哪些windows消息会引起窗体标题栏和边框的重绘。使用工具SPY++查看消息,发现 windows消息WM_NCPAINT(0x85)和 WM_NCACTIVATE(0x86),WM_NCRBUTTONDOWN(0x00A4),WM_SETCURSOR(0x0020),WM_NCLBUTTONUP(0x00A2),WM_NCLBUTTONDOWN(0xA1) 等会重绘标题栏和边框。其中WM_NCPAINT和WM_NCACTIVATE会引起重绘标题栏和边框,消息WM_NCRBUTTONDOWN会触发标题 栏的右键菜单,截获该消息可以自定义标题栏的右键菜单;其他消息会引起ConrtolBox(最小化,最大化,关闭按钮区域)的重绘。因此我们可以从截获 这些消息入手。如下为WndProc方法的结构: using System;
using System.Collections.Generic; using System.Windows.Forms;
using System.ComponentModel; using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices; using System.Diagnostics;
namespace CaptionBox {
public class ThemeForm : Form { #region private structs
struct _NonClientSizeInfo {
public Size CaptionButtonSize; public Size BorderSize; public int CaptionHeight;
public Rectangle CaptionRect; public Rectangle Rect;
public Rectangle ClientRect; public int Width; public int Height; };
#endregion #region constants
const int WM_NCACTIVATE = 0x86; const int WM_NCPAINT = 0x85;
const int WM_NCLBUTTONDOWN = 0xA1; const int WM_NCRBUTTONDOWN = 0x00A4; const int WM_NCRBUTTONUP = 0x00A5; const int WM_NCMOUSEMOVE = 0x00A0; const int WM_NCLBUTTONUP = 0x00A2; const int WM_NCCALCSIZE = 0x0083; const int WM_NCMOUSEHOVER = 0x02A0; const int WM_NCMOUSELEAVE = 0x02A2; const int WM_NCHITTEST = 0x0084; const int WM_NCCREATE = 0x0081; //const int WM_RBUTTONUP = 0x0205; const int WM_LBUTTONDOWN = 0x0201; const int WM_CAPTURECHANGED = 0x0215; const int WM_LBUTTONUP = 0x0202; const int WM_SETCURSOR = 0x0020; const int WM_CLOSE = 0x0010;
const int WM_SYSCOMMAND = 0x0112;
const int WM_MOUSEMOVE = 0x0200; const int WM_SIZE = 0x0005; const int WM_SIZING = 0x0214;
const int WM_GETMINMAXINFO = 0x0024; const int WM_ENTERSIZEMOVE = 0x0231; const int WM_WINDOWPOSCHANGING = 0x0046;
// FOR WM_SIZING MSG WPARAM const int WMSZ_BOTTOM = 6;
const int WMSZ_BOTTOMLEFT = 7; const int WMSZ_BOTTOMRIGHT = 8; const int WMSZ_LEFT = 1; const int WMSZ_RIGHT = 2; const int WMSZ_TOP = 3; const int WMSZ_TOPLEFT = 4; const int WMSZ_TOPRIGHT = 5; // left mouse button is down. const int MK_LBUTTON = 0x0001; const int SC_CLOSE = 0xF060; const int SC_MAXIMIZE = 0xF030; const int SC_MINIMIZE = 0xF020; const int SC_RESTORE = 0xF120; const int SC_CONTEXTHELP = 0xF180; const int HTCAPTION = 2; const int HTCLOSE = 20; const int HTHELP = 21; const int HTMAXBUTTON = 9; const int HTMINBUTTON = 8; const int HTTOP = 12; const int SM_CYBORDER = 6; const int SM_CXBORDER = 5; const int SM_CYCAPTION = 4; const int CS_DropSHADOW = 0x20000; const int GCL_STYLE = (-26); #endregion
#region windows api
[DllImport(\
private static extern IntPtr GetWindowDC(IntPtr hwnd); [DllImport(\
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetWindowRect(IntPtr hwnd, ref _RECT rect);
[DllImport(\
private static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc); [DllImport(\
public static extern int SetClassLong(IntPtr hwnd, int nIndex, int dwNewLong);
[DllImport(\
public static extern int GetClassLong(IntPtr hwnd, int nIndex); #endregion
#region default constructor
public ThemeForm() { Text = \
CloseButtonImage = Properties.Resources.close.ToBitmap(); CloseButtonHoverImage = Properties.Resources.close2.ToBitmap(); CloseButtonPressDownImage = Properties.Resources.close2.ToBitmap();
MaximumButtonImage = Properties.Resources.max.ToBitmap(); MaximumButtonHoverImage = Properties.Resources.max2.ToBitmap(); MaximumButtonPressDownImage = Properties.Resources.max2.ToBitmap(); MaximumNormalButtonImage
Properties.Resources.maxnorm.ToBitmap(); MaximumNormalButtonHoverImage Properties.Resources.maxnorm2.ToBitmap(); MaximumNormalButtonPressDownImage Properties.Resources.maxnorm2.ToBitmap();
= = =
MinimumButtonImage = Properties.Resources.min.ToBitmap(); MinimumButtonHoverImage = Properties.Resources.min2.ToBitmap(); MinimumButtonPressDownImage = Properties.Resources.min2.ToBitmap();
HelpButtonImage = Properties.Resources.help.ToBitmap(); HelpButtonHoverImage = Properties.Resources.help2.ToBitmap(); HelpButtonPressDownImage = Properties.Resources.help2.ToBitmap(); CaptionColor = Brushes.White;
CaptionBackgroundColor = Color.DimGray;
SetClassLong(this.Handle, GCL_STYLE, GetClassLong(this.Handle, GCL_STYLE) | CS_DropSHADOW); //API函数加载,实现窗体边框阴影效果 }
#endregion
[DefaultValue(\ [Browsable(true)]
[Category(\ public virtual ContextMenuStrip CaptionContextMenu { get; set; } protected virtual void OnCaptionContextMenu(int x, int y) { if (this.CaptionContextMenu != null) this.CaptionContextMenu.Show(x, y); }
#region properties
[Category(\
[Description(\ [DisplayName(\ [DesignOnly(true)]
public Image CloseButtonImage { get; set; }
[Category(\ [Description(\button image pressed down in control box.\ [DisplayName(\ [DesignOnly(true)]
public Image CloseButtonPressDownImage { get; set; } [Category(\
[Description(\ [DisplayName(\ [DesignOnly(true)]
public Image CloseButtonHoverImage { get; set; }