版权所有,转载请注明出处:http://guangboo.org/2013/02/05/change-size-of-client-area
windows窗口样式
windows下都有相对固定的窗体样式,当然这些样式可能会根据windows主题会有很大的变换,就像皮肤一样。但windows也提供了一些API,来允许我们修改默认的窗体样式,以实现各自的样式需求,如《C#重绘Windows窗体标题栏和边框》示例。windows在各不同版本的操作系统,各种不同主题下,窗体的样式会有很大不同,除了标题字体,颜色的变化,还有窗体的形状。如有些是圆角矩形、有些是实角矩形;有些窗体边框较宽,有些较窄;有些标题栏较高,有些则较矮,等等。而窗体的是否为圆角矩形,我在《Setwindowrgn函数应用--截图,绘制多边形窗体》中已经有写过,修改窗体形状的方法。而本文主要针对窗体的边框厚度,和标题栏的高度如何修改,提出一些解决方案。因为windows应用程序在不同版本的windows操作系统和主题下通常都表现不同的外观,因此要想保持外观的一致,就必须了解窗体的形状及边框和标题栏高度的修改的方法,这样才能保证统一。
WM_NCCALCSIZE消息
当windows客户区的大小和位置需要重新计算时会发出该消息,因此,拦截该消息可以控制windows窗体客户区的尺寸和位置。WM_NCCALCSIZE的定义为:
#define WM_NCCALCSIZE 0x0083
C#定义为:
const int WM_NCCALCSIZE = 0x0083;
并且当消息参数wParam为TRUE时,lParam参数为NCCALCSIZE_PARAMS类型的指针,该类型包含了可用于计算客户区大小和位置的信息。NCCALCSIZE_PARAMS的签名:
typedef struct tagNCCALCSIZE_PARAMS { RECT rgrc[3]; PWINDOWPOS lppos; } NCCALCSIZE_PARAMS, *LPNCCALCSIZE_PARAMS;
C#声明:
struct NCCALCSIZE_PARAMS { public _RECT rcNewWindow; public _RECT rcOldWindow; public _RECT rcClient; IntPtr lppos; } struct _RECT { public int left; public int top; public int right; public int bottom; }
NCCALCSIZE_PARAMS结构体
C#对NCCALCSIZE_PARAMS的声明与C/C++的声明看似有一点点不同,C/C++定义的是RECT数组,而C#则变成了三个单独的RECT字段,当前效果是一样的,只不过一个使用[]来所有对象,而一个是自己访问而已。也就是前者的rgrc[0]与后者的rcNewWindow对应,前者的rgrc[1]与rcOldWindow对应,和前者的rgrc[2]与rcClient对应。只所有C#分别定义,主要是为了更好的说明这三个RECT分别表示的意思。
第一个RECT或rcNewWindow存储了窗体被移动或尺寸被修改后的窗体的坐标信息,这是窗口马上要应用的信息,即窗口马上就移动到坐标(rcNewWindow.left, rcNewWindow.top),并且尺寸将调整为(rcNewWindow.right-rcNewWindow.left, rcNewWindow.bottom-rcNewWindow.top);而第二个RECT或rcOldWindow存储移动前或尺寸被改变前窗体的坐标信息;第三个RECT或rcClient存储了移动前或尺寸修改前客户区的坐标信息。如果窗体是子窗体,那么坐标信息都使相对父窗体的,否则坐标是相对于屏幕的。然而,当消息处理完毕后,第一个RECT或rcNewWindow则被用来存储移动后或大小改变后的客户区的坐标信息,即计算够的结果。也可以推测出计算过程使用rcNewWindow坐标与当前操作系统和主题下的窗体标题栏的高度和窗体边框的厚度等信息计算得来。假设窗体高度为CaptionHeight,窗体边框厚度为BorderWidth,那么客户区就可以通过公式计算得出客户区域坐标:
client.left = rcNewWindow.left + BorderWidth client.top = rcNewWindow.top + BorderWidth + CaptionHeight client.right = rcNewWindow.right - BorderWidth client.bottom = rcNewWindow.bottom - BorderWidth
其实窗体边框厚度和窗体高度都可以从类System.Windows.Forms.SystemInformation中获取,不过要根据窗体类型和主题等选择不同的厚度和高度,这里不再详细说明。你可以在窗口创建时拦截WM_NCCALCSIZE消息,获取NCCALCSIZE_PARAMS参数,并根据rcNewWindow和rcClient来计算窗体边框厚度和标题栏高度。
修改客户区域
前面的文字目的,主要是为了说明客户区域的坐标信息,是根据窗体的新坐标信息、窗体边框厚度和标题栏的高度进行计算得来的。因此我们可以拦截WM_NCCALCSIZE消息,人为的修改窗体的坐标信息,从而影响客户区坐标信息的计算结果。
由前面说的公式可以看出,对新窗口坐标的增减就是对客户区坐标的增减,因此,如下拦截消息代码,实现了加宽,加高客户区的效果:
private void AdjustClientRect(ref _RECT rcClient) { rcClient.left -= 3; rcClient.right += 3; rcClient.bottom += 3; } const int WM_NCCALCSIZE = 0x0083; protected override void WndProc(ref Message m) { switch (m.Msg) { case WM_NCCALCSIZE: { if (m.WParam != IntPtr.Zero) { NCCALCSIZE_PARAMS rcsize = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure (m.LParam, typeof(NCCALCSIZE_PARAMS)); AdjustClientRect(ref rcsize.rcNewWindow); Marshal.StructureToPtr(rcsize, m.LParam, false); } m.Result = new IntPtr(1); } break; } base.WndProc(ref m); }
由于客户区的宽和高都增加了,但窗体大大小和位置并没有变化,因此运行的效果就如图所示,左右和下边框的厚度明显变小,而标题栏没有变化,因为我们没有调整rcClient.top字段。
因此,如果你再重绘窗体,希望窗体在各版本的windows操作系统和主题中表现一致,可以希望保持窗体的版块厚度为BorderWidth,你就可以这样来调整代码:
int BorerWidth = 1; private void AdjustClientRect(ref _RECT rcClient) { rcClient.left -= SystemInformation.FrameBorderSize.Width - BorerWidth; rcClient.right += SystemInformation.FrameBorderSize.Width - BorerWidth; rcClient.bottom += SystemInformation.FrameBorderSize.Width - BorerWidth; }
这里也没有设置rcClient.top,以为窗体顶部的边框在标题栏上访,这里简单的修改rcClient.top是达不到预期效果的,需要重绘标题栏才行,可以参见《C#重绘Windows窗体标题栏和边框》。运行效果如下:
当然,你也可以利用这个方法,在不设置FormBorderStyle属性为None的情况下,实现无边框窗口的效果。简单修改代码即可实现:
private void AdjustClientRect(ref _RECT rcClient) { rcClient.left -= SystemInformation.FrameBorderSize.Width; rcClient.right += SystemInformation.FrameBorderSize.Width; rcClient.bottom += SystemInformation.FrameBorderSize.Width; rcClient.top -= SystemInformation.FrameBorderSize.Width + SystemInformation.CaptionHeight; }
总结
虽然通过拦截WM_NCCALCSIZE消息的方法可以实现修改客户区域坐标、边框厚度、无边框等效果,但还是文章开头所说的,实现在各种windows操作系统版本和主题下统一样式才是主要应用,当然这只是统一样式的一部分,可以统一窗口边框的厚度,至于窗体的边框的样式,甚至整个窗体的样式,还是需要结合重绘标题栏和边框的,如需要,可以参考《C#重绘Windows窗体标题栏和边框》的实现。