有人可能会问,客户区延伸至非客户区到底有什么意义。有些程序在布局上比较紧凑或者希望更美观等,无关紧要的菜单项希望能放到标题栏等非客户区,Form窗体控件本身并没有提供此功能。在这之前,有把窗体FormBorderStyle设为None重新绘制标题栏。还有文章通过调用“User32.dll”中的GetWindowDC函数和ReleaseDC函数来实现在标题栏上添加控件,这种方式虽然完全能在非客户区绘制,但是弊端便是无法在vista和windows7下透明主题时显示非客户绘制的内容,因为在透明主题下Aero会把非客户区从GDI+剥离出来让DirectX进行渲染。
传统方式(网络收集):
2 using System.Runtime.InteropServices;
3
4 [DllImport("user32.dll")]
5 privatestaticextern IntPtr GetWindowDC(IntPtr hWnd);
6 [DllImport("user32.dll")]
7 privatestaticexternint ReleaseDC(IntPtr hWnd, IntPtr hDC);
8
9 privateconstint WM_NCPAINT =0x0085;
10 privateconstint WM_NCACTIVATE =0x0086;
11 privateconstint WM_NCLBUTTONDOWN =0x00A1;
12 protectedoverridevoid WndProc(ref Message m)
13 {
14 base.WndProc(ref m);
15 Rectangle vRectangle =new Rectangle((Width -75) /2, 3, 75, 25);
16 switch (m.Msg)
17 {
18 case WM_NCPAINT:
19 case WM_NCACTIVATE:
20 IntPtr vHandle = GetWindowDC(m.HWnd);
21 Graphics vGraphics = Graphics.FromHdc(vHandle);
22 vGraphics.FillRectangle(new LinearGradientBrush(vRectangle,
23 Color.Pink, Color.Purple, LinearGradientMode.BackwardDiagonal),
24 vRectangle);
25
26 StringFormat vStringFormat =new StringFormat();
27 vStringFormat.Alignment = StringAlignment.Center;
28 vStringFormat.LineAlignment = StringAlignment.Center;
29 vGraphics.DrawString("TitlebarControl", Font, Brushes.BlanchedAlmond,
30 vRectangle, vStringFormat);
31
32 vGraphics.Dispose();
33 ReleaseDC(m.HWnd, vHandle);
34 break;
35 case WM_NCLBUTTONDOWN:
36 Point vPoint =new Point((int)m.LParam);
37 vPoint.Offset(-Left, -Top);
38 if (vRectangle.Contains(vPoint))
39 MessageBox.Show(vPoint.ToString());
40 break;
41 }
42 }
下面是windows7下实现,本文将一一讲解,本人能力有限,文中不周全的地方希望大家能够指出,这是对我最大的帮助,谢谢。
整理了一下,大致分为一下几个步骤:
- 扩展客户区
- 程序边框显示
- 模拟非客户区消息
- DwmDefWindowProc处理
1.扩展客户区
首先我们先把窗体背景颜色设为黑色。这样我们可以看到,黑色部分便是客户区。
如果要扩展则需要截获NCCALCSIZE消息,并且返回0,客户区占满整个窗体(包括非客户区),按照微软的说法就是删除了标准的框架,变成了自定义框架。
具体代码:
2 {
3 constint NCCALCSIZE =0x0083;
4 if (m.Msg == NCCALCSIZE)
5 {
6 if (m.WParam != (IntPtr)0)
7 {
8 m.Result = (IntPtr)0;
9 }
10 }
11 else
12 {
13 base.WndProc(ref m);
14 }
15 }
截获NCCALCSIZE之前和之后,效果如下:
2.程序边框显示
扩展客户区后,程序的边框也被覆盖,我们需要调用系统桌面管理系统(DWM)的提供的接口来将非客户端框架的边缘扩展到窗口内(这里扩展的是边缘,而不是扩展非客户区),具体为dwmapi.dll中的DwmExtendFrameIntoClientArea函数。(DWM更多信息具体见:http://msdn.microsoft.com/zh-cn/magazine/cc163435.aspx)
这里贴出具体代码供参考:
2 publicexternstaticint DwmIsCompositionEnabled(refint en);
3
4 [DllImport("dwmapi.dll")]
5 publicexternstaticint DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margin);
6
7 publicstruct MARGINS
8 {
9 publicint m_left;
10 publicint m_right;
11 publicint m_top;
12 publicint m_buttom;
13 };
14
15 ///<summary>
16 /// 边缘扩展
17 ///</summary>
18 ///<param name="form">指定窗体</param>
19 ///<param name="top">上方区域指定距离</param>
20 ///<param name="buttom">下方区域指定距离</param>
21 ///<param name="left">左方区域指定距离</param>
22 ///<param name="right">右方区域指定距离</param>
23 publicstaticvoid ApplyAero(Form form, int top, int buttom, int left, int right)
24 {
25 int en =0;
26
27 MARGINS mg =new MARGINS();
28 mg.m_buttom = buttom;
29 mg.m_left = left;
30 mg.m_right = right;
31 mg.m_top = top;
32
33 if (System.Environment.OSVersion.Version.Major >=6)//判断系统版本
34 {
35 DwmIsCompositionEnabled(ref en);
36 if (en >0)//判断是否已启用windows系统的透明效果
37 {
38 DwmExtendFrameIntoClientArea(form.Handle, ref mg);
39 }
40 else
41 {
42 MessageBox.Show("Desktop Composition is Disabled!");
43 }
44 }
45 else
46 {
47 MessageBox.Show("Please run this on Windows7.");
48 }
49 }
在程序启动或初始化时调用上面代码中的ApplyAero()方法,效果如下:
AeroExpand.ApplyAero(this, 30, 8, 8, 8);
你可以根据实际情况设置你想要多大的边框(注意:边框范围内的区域背景为黑色时,才会显示透明),这时你会发现,只是边框和标题栏出现了,但是他们并不能实现其功能,比如标题栏不能拖动,边缘不能调整大小。那是因为实际上标题栏和边缘实际上是客户区而已。
3.模拟非客户区消息
鼠标在客户区移动默认会返回HTCLIENT消息,然后windows做出相应的操作。这里我们可以人为的修改返回的消息,使在客户区的操作被windows认为是在标题栏或者是边缘。
这里先提供一个方法HitTestNCA(),此方法是根据鼠标在窗体上的位置来返回不同的消息的值:
2 {
3 constint HTCLIENT =1;//客户区消息
4 constint HTCAPTION =2;//标题栏消息
5
6 constint HTLEFT =10;//左边缘消息
7 constint HTRIGHT =11;//右边缘消息
8 constint HTTOP =12;//上边缘消息
9 constint HTTOPLEFT =13;//左上角边缘消息
10 constint HTTOPRIGHT =14;//右上角边缘消息
11 constint HTBOTTOM =15;//下边缘消息
12 constint HTBOTTOMLEFT =16;//左下角边缘消息
13 constint HTBOTTOMRIGHT =17;//右下角边缘消息
14
15 Point p =new Point(Control.MousePosition.X, Control.MousePosition.Y);//鼠标位置
16
17 //下面是判断鼠标处于某处,返回相应的值。
18 //不要轻易打乱下面顺序,优先级别(高-低):边角-边缘-标题栏
19
20 Rectangle topleft =this.RectangleToScreen(new Rectangle(0, 0, 8, 8));
21 if (topleft.Contains(p))
22 returnnew IntPtr(HTTOPLEFT);
23
24 Rectangle topright =this.RectangleToScreen(new Rectangle(Width -8, 0, 8, 8));
25 if (topright.Contains(p))
26 returnnew IntPtr(HTTOPRIGHT);
27
28 Rectangle botleft =this.RectangleToScreen(new Rectangle(0, Height -8, 8, 8));
29 if (botleft.Contains(p))
30 returnnew IntPtr(HTBOTTOMLEFT);
31
32 Rectangle botright =this.RectangleToScreen(new Rectangle(Width -8, Height -8, 8, 8));
33 if (botright.Contains(p))
34 returnnew IntPtr(HTBOTTOMRIGHT);
35
36 Rectangle top =this.RectangleToScreen(new Rectangle(0, 0, Width, 8));
37 if (top.Contains(p))
38 returnnew IntPtr(HTTOP);
39
40 Rectangle left =this.RectangleToScreen(new Rectangle(0, 0, 8, Height));
41 if (left.Contains(p))
42 returnnew IntPtr(HTLEFT);
43
44 Rectangle right =this.RectangleToScreen(new Rectangle(Width -8, 0, 8, Height));
45 if (right.Contains(p))
46 returnnew IntPtr(HTRIGHT);
47
48 Rectangle bottom =this.RectangleToScreen(new Rectangle(0, Height -8, Width, 8));
49 if (bottom.Contains(p))
50 returnnew IntPtr(HTBOTTOM);
51
52 Rectangle cap =this.RectangleToScreen(new Rectangle(0, 8, Width, 22));
53 if (cap.Contains(p))
54 returnnew IntPtr(HTCAPTION);
55
56 returnnew IntPtr(HTCLIENT);
57 }
以上代码具体值可根据需要修改,但要注意鼠标处于边角时,可能也处于边缘范围,所以最好不要打乱上面代码中判断的优先级。
接下便是需要截获NCHITTEST(鼠标移动或单机的消息)消息,把HitTestNCA()方法返回的值回发给windows,具体代码(WndProc()完整代码):
2 {
3 constint NCHITTEST =0x84;
4 constint NCCALCSIZE =0x0083;
5
6 if (m.Msg == NCCALCSIZE)
7 {
8 if (m.WParam != (IntPtr)0)
9 {
10 m.Result = (IntPtr)0;
11 }
12 }
13 elseif (m.Msg == NCHITTEST)
14 {
15 m.Result = HitTestNCA();
16 }
17 else
18 {
19 base.WndProc(ref m);
20 }
21 }
这样我们便在客户区模拟出了默认窗体非客户区的功能。
4.DwmDefWindowProc处理
虽然上面几个步骤大致实现了功能,但是我们可以发现,标题栏上面的最小化、最大化、关闭按钮还无法点击,这就需要我们使用DWM中的DwmDefWindowProc函数来进行处理。
如果要使自定义窗体框架中的按钮可以点击,首先需要发送消息给DwmDefWindowProc处理,然后把处理结果回发给windows,这样才能完成对标题栏默认按钮的处理(http://msdn.microsoft.com/en-us/library/bb688195(v=vs.85).aspx)。
具体代码(WndProc()完整代码):
2 publicstaticexternint DwmDefWindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, out IntPtr result);
3
4 protectedoverridevoid WndProc(ref Message m)
5 {
6 IntPtr result;
7 int dwmHandled = DwmDefWindowProc(m.HWnd, m.Msg, m.WParam, m.LParam, out result);
8 if (dwmHandled ==1)//判断是否被处理
9 {
10 m.Result = result;
11 return;
12 }
13
14 constint NCHITTEST =0x84;
15 constint NCCALCSIZE =0x0083;
16
17 if (m.Msg == NCCALCSIZE)//截获NCCALCSIZE消息
18 {
19 if (m.WParam != (IntPtr)0)
20 {
21 m.Result = (IntPtr)0;
22 }
23 }
24 elseif (m.Msg == NCHITTEST)//截获NCHITTEST消息
25 {
26 m.Result = HitTestNCA();
27 }
28 else
29 {
30 base.WndProc(ref m);
31 }
32 }
效果示例:
------------完整代码下载------------ (VS2010)
转自:
https://files.cnblogs.com/WangQ/TitlebarControl.rar