浅谈Winform控件开发(一):使用GDI+美化基础窗口
- 写在前面:
- 本系列随笔将作为我对于winform控件开发的心得总结,方便对一些读者在gdi+、winform等技术方面进行一个入门级的讲解,抛砖引玉。
- 别问为什么不用wpf,为什么不用qt。问就是懒,不想学。
- 本项目所有代码均开源在https://github.com/muxiang/powercontrol
- 效果预览:(gif,3.4mb)
- 本系列第一篇内容将仅包含对于winform基础窗口也就是system.windows.forms.form的美化,后续将对一些常用控件如button、combobox、checkbox、textbox等进行修改,并提供一些其他如loading遮罩层等常见控件。
- 对于基础窗口的美化,首要的任务就是先把基础标题栏干掉。这个过程中会涉及一些windows消息机制。
- 首先,我们新建一个类xform,派生自system.windows.forms.form。
1 /// <summary> 2 /// 表示组成应用程序的用户界面的窗口或对话框。 3 /// </summary> 4 [toolboxitem(false)] 5 public class xform : form 6 ...
随后,我们定义一些常量
1 /// <summary> 2 /// 标题栏高度 3 /// </summary> 4 public const int titlebarheight = 30; 5 6 // 边框宽度 7 private const int borderwidth = 4; 8 // 标题栏图标大小 9 private const int iconsize = 16; 10 // 标题栏按钮大小 11 private const int buttonwidth = 30; 12 private const int buttonheight = 30;
覆盖基类属性formborderstyle使base.formborderstyle保持none,覆盖基类属性padding返回或设置正确的内边距
1 /// <summary> 2 /// 获取或设置窗体的边框样式。 3 /// </summary> 4 [browsable(true)] 5 [category("appearance")] 6 [description("获取或设置窗体的边框样式。")] 7 [defaultvalue(formborderstyle.sizable)] 8 public new formborderstyle formborderstyle 9 { 10 get => _formborderstyle; 11 set 12 { 13 _formborderstyle = value; 14 updatestyles(); 15 drawtitlebar(); 16 } 17 } 18 19 /// <summary> 20 /// 获取或设置窗体的内边距。 21 /// </summary> 22 [browsable(true)] 23 [category("appearance")] 24 [description("获取或设置窗体的内边距。")] 25 public new padding padding 26 { 27 get => new padding(base.padding.left, base.padding.top, base.padding.right, base.padding.bottom - titlebarheight); 28 set => base.padding = new padding(value.left, value.top, value.right, value.bottom + titlebarheight); 29 }
※最后一步也是最关键的一步:重新定义窗口客户区边界。重写wndproc并处理wm_nccalcsize消息。
1 protected override void wndproc(ref message m) 2 { 3 switch (m.msg) 4 { 5 case wm_nccalcsize: 6 { 7 // 自定义客户区 8 if (m.wparam != intptr.zero && _formborderstyle != formborderstyle.none) 9 { 10 nccalcsize_params @params = (nccalcsize_params) 11 marshal.ptrtostructure(m.lparam, typeof(nccalcsize_params)); 12 @params.rgrc[0].top += titlebarheight; 13 @params.rgrc[0].bottom += titlebarheight; 14 marshal.structuretoptr(@params, m.lparam, false); 15 m.result = (intptr)(wvr_aligntop | wvr_alignbottom | wvr_redraw); 16 } 17 18 base.wndproc(ref m); 19 break; 20 } 21 ……
同样在wndproc中处理wm_ncpaint消息1 case wm_ncpaint: 2 { 3 drawtitlebar(); 4 m.result = (intptr)1; 5 break; 6 }
drawtitlebar()方法定义如下:
1 /// <summary> 2 /// 绘制标题栏 3 /// </summary> 4 private void drawtitlebar() 5 { 6 if (_formborderstyle == formborderstyle.none) 7 return; 8 9 drawtitlebackgroundtexticon(); 10 createbuttonimages(); 11 drawtitlebuttons(); 12 }
首先使用线性渐变画刷绘制标题栏背景、图标、标题文字:
1 /// <summary> 2 /// 绘制标题栏背景、文字、图标 3 /// </summary> 4 private void drawtitlebackgroundtexticon() 5 { 6 intptr hdc = getwindowdc(handle); 7 graphics g = graphics.fromhdc(hdc); 8 9 // 标题栏背景 10 using (brush brstitlebar = new lineargradientbrush(titlebarrectangle, 11 _titlebarstartcolor, _titlebarendcolor, lineargradientmode.horizontal)) 12 g.fillrectangle(brstitlebar, titlebarrectangle); 13 14 // 标题栏图标 15 if (showicon) 16 g.drawicon(icon, new rectangle( 17 borderwidth, titlebarrectangle.top + (titlebarrectangle.height - iconsize) / 2, 18 iconsize, iconsize)); 19 20 // 标题文本 21 const int txtx = borderwidth + iconsize; 22 sizef sztext = g.measurestring(text, systemfonts.captionfont, width, stringformat.genericdefault); 23 using brush brstext = new solidbrush(_titlebarforecolor); 24 g.drawstring(text, 25 systemfonts.captionfont, 26 brstext, 27 new rectanglef(txtx, 28 titlebarrectangle.top + (titlebarrectangle.bottom - sztext.height) / 2, 29 width - borderwidth * 2, 30 titlebarheight), 31 stringformat.genericdefault); 32 33 g.dispose(); 34 releasedc(handle, hdc); 35 }
随后绘制标题栏按钮,犹豫篇幅限制,在此不多赘述,详见源码中createbuttonimages()与drawtitlebuttons()。
至此,表面工作基本做完了,但这个窗口还不像个窗口,因为最小化、最大化、关闭以及调整窗口大小都不好用。
为什么?因为还有很多工作要做,首先,同样在wndproc中处理wm_nchittest消息,通过m.result指定当前鼠标位置位于标题栏、最小化按钮、最大化按钮、关闭按钮或上下左右边框
1 case wm_nchittest: 2 { 3 base.wndproc(ref m); 4 5 point pt = pointtoclient(new point((int)m.lparam & 0xffff, (int)m.lparam >> 16 & 0xffff)); 6 7 _usersizedormoved = true; 8 9 switch (_formborderstyle) 10 { 11 case formborderstyle.none: 12 break; 13 case formborderstyle.fixedsingle: 14 case formborderstyle.fixed3d: 15 case formborderstyle.fixeddialog: 16 case formborderstyle.fixedtoolwindow: 17 if (pt.y < 0) 18 { 19 _usersizedormoved = false; 20 m.result = (intptr)htcaption; 21 } 22 23 if (correcttological(closebuttonrectangle).contains(pt)) 24 m.result = (intptr)htclose; 25 if (correcttological(maximizebuttonrectangle).contains(pt)) 26 m.result = (intptr)htmaxbutton; 27 if (correcttological(minimizebuttonrectangle).contains(pt)) 28 m.result = (intptr)htminbutton; 29 30 break; 31 case formborderstyle.sizable: 32 case formborderstyle.sizabletoolwindow: 33 if (pt.y < 0) 34 { 35 _usersizedormoved = false; 36 m.result = (intptr)htcaption; 37 } 38 39 if (correcttological(closebuttonrectangle).contains(pt)) 40 m.result = (intptr)htclose; 41 if (correcttological(maximizebuttonrectangle).contains(pt)) 42 m.result = (intptr)htmaxbutton; 43 if (correcttological(minimizebuttonrectangle).contains(pt)) 44 m.result = (intptr)htminbutton; 45 46 if (windowstate == formwindowstate.maximized) 47 break; 48 49 bool btop = pt.y <= -titlebarheight + borderwidth; 50 bool bbottom = pt.y >= height - titlebarheight - borderwidth; 51 bool bleft = pt.x <= borderwidth; 52 bool bright = pt.x >= width - borderwidth; 53 54 if (bleft) 55 { 56 _usersizedormoved = true; 57 if (btop) 58 m.result = (intptr)httopleft; 59 else if (bbottom) 60 m.result = (intptr)htbottomleft; 61 else 62 m.result = (intptr)htleft; 63 } 64 else if (bright) 65 { 66 _usersizedormoved = true; 67 if (btop) 68 m.result = (intptr)httopright; 69 else if (bbottom) 70 m.result = (intptr)htbottomright; 71 else 72 m.result = (intptr)htright; 73 } 74 else if (btop) 75 { 76 _usersizedormoved = true; 77 m.result = (intptr)httop; 78 } 79 else if (bbottom) 80 { 81 _usersizedormoved = true; 82 m.result = (intptr)htbottom; 83 } 84 break; 85 default: 86 throw new argumentoutofrangeexception(); 87 } 88 break; 89 }
随后以同样的方式处理wm_nclbuttondblclk、wm_nclbuttondown、wm_nclbuttonup、wm_ncmousemove等消息,进行标题栏按钮等元素重绘,不多赘述。
现在窗口进行正常的单击、双击、调整尺寸,我们在最后为窗口添加阴影
首先定义一个可以承载32位位图的分层窗口(layered window)来负责主窗口阴影的呈现,详见源码中xformshadow类,此处仅列出用于创建分层窗口的核心代码:
1 private void updatebmp(bitmap bmp) 2 { 3 if (!ishandlecreated) return; 4 5 if (!image.iscanonicalpixelformat(bmp.pixelformat) || !image.isalphapixelformat(bmp.pixelformat)) 6 throw new argumentexception(@"位图格式不正确", nameof(bmp)); 7 8 intptr oldbits = intptr.zero; 9 intptr screendc = getdc(intptr.zero); 10 intptr hbmp = intptr.zero; 11 intptr memdc = createcompatibledc(screendc); 12 13 try 14 { 15 point formlocation = new point(left, top); 16 size bitmapsize = new size(bmp.width, bmp.height); 17 blendfunction blendfunc = new blendfunction( 18 ac_src_over, 19 0, 20 255, 21 ac_src_alpha); 22 23 point srcloc = new point(0, 0); 24 25 hbmp = bmp.gethbitmap(color.fromargb(0)); 26 oldbits = selectobject(memdc, hbmp); 27 28 updatelayeredwindow( 29 handle, 30 screendc, 31 ref formlocation, 32 ref bitmapsize, 33 memdc, 34 ref srcloc, 35 0, 36 ref blendfunc, 37 ulw_alpha); 38 } 39 finally 40 { 41 if (hbmp != intptr.zero) 42 { 43 selectobject(memdc, oldbits); 44 deleteobject(hbmp); 45 } 46 47 releasedc(intptr.zero, screendc); 48 deletedc(memdc); 49 } 50 }
最后通过路径渐变画刷创建阴影位图,通过位图构建分层窗口,并与主窗口建立父子关系:
1 /// <summary> 2 /// 构建阴影 3 /// </summary> 4 private void buildshadow() 5 { 6 lock (this) 7 { 8 _buildingshadow = true; 9 10 if (_shadow != null && !_shadow.isdisposed && !_shadow.disposing) 11 { 12 // 解除父子窗口关系 13 setwindowlong( 14 handle, 15 gwl_hwndparent, 16 0); 17 18 _shadow.dispose(); 19 } 20 21 bitmap bmpbackground = new bitmap(width + borderwidth * 4, height + borderwidth * 4); 22 23 graphicspath gp = new graphicspath(); 24 gp.addrectangle(new rectangle(0, 0, bmpbackground.width, bmpbackground.height)); 25 26 using (graphics g = graphics.fromimage(bmpbackground)) 27 using (pathgradientbrush brs = new pathgradientbrush(gp)) 28 { 29 g.compositingmode = compositingmode.sourcecopy; 30 g.interpolationmode = interpolationmode.highqualitybicubic; 31 g.pixeloffsetmode = pixeloffsetmode.highquality; 32 g.smoothingmode = smoothingmode.antialias; 33 34 // 中心颜色 35 brs.centercolor = color.fromargb(100, color.black); 36 // 指定从实际阴影边界到窗口边框边界的渐变 37 brs.focusscales = new pointf(1 - borderwidth * 4f / width, 1 - borderwidth * 4f / height); 38 // 边框环绕颜色 39 brs.surroundcolors = new[] { color.fromargb(0, 0, 0, 0) }; 40 // 掏空窗口实际区域 41 gp.addrectangle(new rectangle(borderwidth * 2, borderwidth * 2, width, height)); 42 g.fillpath(brs, gp); 43 } 44 45 gp.dispose(); 46 47 _shadow = new xformshadow(bmpbackground); 48 49 _buildingshadow = false; 50 51 alignshadow(); 52 _shadow.show(); 53 54 // 设置父子窗口关系 55 setwindowlong( 56 handle, 57 gwl_hwndparent, 58 _shadow.handle.toint32()); 59 60 activate(); 61 }//end of lock(this) 62 }
感谢大家能读到这里,代码中如有错误,或存在其它建议,欢迎在评论区或github指正。
如果觉得本文对你有帮助,还请点个推荐或github上点个星星,谢谢大家。
转载请注明原作者,谢谢。
上一篇: experience-of-optimizing-spark-s
下一篇: 苹果难抢么