欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

浅谈Winform控件开发(一):使用GDI+美化基础窗口

程序员文章站 2022-05-18 14:21:44
写在前面: 本系列随笔将作为我对于winform控件开发的心得总结,方便对一些读者在GDI+、winform等技术方面进行一个入门级的讲解,抛砖引玉。 别问为什么不用WPF,为什么不用QT。问就是懒,不想学。 本项目所有代码均开源在https://github.com/muxiang/PowerCo ......
  •  写在前面:
      • 本系列随笔将作为我对于winform控件开发的心得总结,方便对一些读者在gdi+、winform等技术方面进行一个入门级的讲解,抛砖引玉。
      • 别问为什么不用wpf,为什么不用qt。问就是懒,不想学。
      • 本项目所有代码均开源在https://github.com/muxiang/powercontrol
      • 效果预览:(gif,3.4mb)

    浅谈Winform控件开发(一):使用GDI+美化基础窗口

  • 本系列第一篇内容将仅包含对于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 ……
    相关常量以及p/invoke相关方法已在我的库中定义,详见msdn,也可从查询。
    同样在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上点个星星,谢谢大家。

转载请注明原作者,谢谢。