WINFORM自定义皮肤制作(上)
最近要做个软件正在做技术准备,由于WINFORM生成的窗体很丑陋,一个好的软件除了功能性很重要外,UI的体验也是不容忽视的。习惯性的在网上搜素了下,换肤控件也有好几款,但是有些用起来不是很好用,好点的也要花很多银子哦,而且毕竟是别人写的,心里总不是个滋味,所以决定自己尝试着写写看,花了一个晚上终于做出来了个DEMO,貌似还不错,贴图如下(图片是直接是用的暴风影音的,寒自己一个。。)
下面和大家分享下。
首先分析下皮肤的制作原理,我的理解是把整个窗体(去边框后)划分为9个区域(如果有更复杂的界面,可以划分更多),有图有真相:
然后准备皮肤素材,切图,我的切图如下:
接着可以开工了:
1.初始化图片资源变量
protected int formMinX = 0;//最小化按钮的X坐标 protected int formMaxX = 0;//最大化按钮的X坐标 protected int formCloseX = 0;//关闭按钮的X坐标 protected int formTitleMarginLeft = 0;//标题栏的左边界 protected int formTitleMarginRight = 0;//标题栏的右边界 Image imgTopLeft = (Image)Resources.topleft;//窗体顶部左上角图片 Image imgTopRight = (Image)Resources.topright;//窗体顶部右上角图片 Image imgTopMiddle = (Image)Resources.topstretch;//窗体顶部中间图片 Image imgBottomLeft = (Image)Resources.bottomLeft;//窗体底部左下角图片 Image imgBottonRight = (Image)Resources.bottomRight;//窗体底部右下角图片 Image imgBottonmMiddle = (Image)Resources.bottomstretch;//窗体底部中间图片 Image imgMiddleLeft = (Image)Resources.LeftDrag_Mid;//窗体中部左边框图片 Image imgMiddleRight = (Image)Resources.RightDrag_Mid;//窗体中部右边框图片 Image imgFormMin = (Image)Resources.skin_btn_min;//最小化按钮 Image imgFormMax = (Image)Resources.skin_btn_max;//最大化按钮 Image imgFormClose = (Image)Resources.skin_btn_close;//关闭按钮 Image imgFormRestore = (Image)Resources.skin_btn_restore;//还原按钮
2.重写OnPaint事件。代码直接贴上来(比较简单,就是计算图片要绘制到窗体的坐标,然后把图片绘到窗体上)
protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); this.BackColor = Color.Black; //绘制皮肤 Graphics g = e.Graphics; //绘制窗体顶部左上角图片 g.DrawImage(imgTopLeft, 0, 0, imgTopLeft.Width, imgTopLeft.Height); int topRightX = e.ClipRectangle.Width - imgTopRight.Width; //绘制窗体顶部右上角图片 g.DrawImage(imgTopRight, topRightX, 0, imgTopRight.Width, imgTopRight.Height); int topMiddleWidth= e.ClipRectangle.Width - (imgTopLeft.Width + imgTopRight.Width) + 4; //绘制窗体顶部中间图片(标题栏) formTitleMarginLeft = imgTopLeft.Width; formTitleMarginRight = topRightX; g.DrawImage(imgTopMiddle, imgTopLeft.Width, 0, topMiddleWidth, imgTopMiddle.Height); //绘制窗体底部左下角图片 g.DrawImage(imgBottomLeft, 0, e.ClipRectangle.Height - imgBottomLeft.Height, imgBottomLeft.Width, imgBottomLeft.Height); //绘制窗体底部右下角图片 g.DrawImage(imgBottonRight, e.ClipRectangle.Width - imgBottomLeft.Width, e.ClipRectangle.Height - imgBottonRight.Height, imgBottonRight.Width, imgBottonRight.Height); //绘制窗体底部中间图片 g.DrawImage(imgBottonmMiddle, imgBottomLeft.Width, e.ClipRectangle.Height - imgBottonmMiddle.Height, e.ClipRectangle.Width - (imgBottomLeft.Width + imgBottomLeft.Width) + 4, imgBottonmMiddle.Height); //画左右边框 g.DrawImage(imgMiddleLeft, 0, imgTopLeft.Height, imgMiddleLeft.Width, e.ClipRectangle.Height - (imgTopLeft.Height + imgBottomLeft.Height)); g.DrawImage(imgMiddleRight, e.ClipRectangle.Width - imgMiddleRight.Width, imgTopRight.Height, imgMiddleRight.Width, e.ClipRectangle.Height - (imgTopLeft.Height + imgBottomLeft.Height)); //画右上角按钮(最小化,最大化,关闭) formMinX = topRightX; g.DrawImage(imgFormMin, topRightX, 0, imgFormMin.Width, imgFormMin.Height); if (this.WindowState == FormWindowState.Maximized) { imgFormMax = imgFormRestore; } else imgFormMax = (Image)Resources.skin_btn_max; formMaxX = topRightX + imgFormMin.Width; g.DrawImage(imgFormMax, topRightX + imgFormMin.Width, 0, imgFormMax.Width, imgFormMax.Height); formCloseX = topRightX + imgFormMax.Width + imgFormMin.Width; g.DrawImage(imgFormClose, topRightX + imgFormMax.Width + imgFormMin.Width, 0, imgFormClose.Width, imgFormClose.Height); }
3.当窗体大小发生变化的时候同样要重绘,所以重写OnSizeChanged的事件
protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged(e); Invalidate();//强制窗体重绘 }
OK,这样就完成了皮肤的绘制。接下来我们解决的问题有:
1.如何用鼠标拖拽改变无边框的大小
2.如何用鼠标移动无边框窗体
3.如何让绘制的最小化,最大化,关闭按钮执行操作(响应事件)
对于第1个问题拖拽改变无边框的大小,可以重写消息处理函数WndProc(除了这个我找不到其它好的方法了,如果哪个朋友知道请告诉我一声)
const int WM_NCHITTEST = 0x0084; const int HTLEFT = 10; const int HTRIGHT = 11; const int HTTOP = 12; const int HTTOPLEFT = 13; const int HTTOPRIGHT = 14; const int HTBOTTOM = 15; const int HTBOTTOMLEFT = 0x10; const int HTBOTTOMRIGHT = 17; protected override void WndProc(ref Message m) { base.WndProc(ref m); switch (m.Msg) { case WM_NCHITTEST: Point vPoint = new Point((int)m.LParam & 0xFFFF, (int)m.LParam >> 16 & 0xFFFF); vPoint = PointToClient(vPoint); if (vPoint.X <= 5) if (vPoint.Y <= 5) m.Result = (IntPtr)HTTOPLEFT; else if (vPoint.Y >= ClientSize.Height - 5) m.Result = (IntPtr)HTBOTTOMLEFT; else m.Result = (IntPtr)HTLEFT; else if (vPoint.X >= ClientSize.Width - 5) if (vPoint.Y <= 5) m.Result = (IntPtr)HTTOPRIGHT; else if (vPoint.Y >= ClientSize.Height - 5) m.Result = (IntPtr)HTBOTTOMRIGHT; else m.Result = (IntPtr)HTRIGHT; else if (vPoint.Y <= 5) m.Result = (IntPtr)HTTOP; else if (vPoint.Y >= ClientSize.Height - 5) m.Result = (IntPtr)HTBOTTOM; break; } }
第2个问题鼠标移动无边框窗体网上一般有三种方法(详见:[转]C#无边框窗体移动的三种方法)
其中有两种是用WINDOWS消息机制来完成,但是我发现如果用消息机制来处理会造成鼠标的双击或者单击事件不能使用,这一点让我很纠结,所以就采用了最原始的处理方式。
private Point mouseOffset; //记录鼠标指针的坐标 private bool isMouseDown = false; //记录鼠标按键是否按下 private void Main_MouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { mouseOffset = new Point(-e.X, -e.Y); isMouseDown = true; } }
private void Main_MouseUp(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { isMouseDown = false; } } private void Main_MouseMove(object sender, MouseEventArgs e) { if (isMouseDown) { Point mousePos = Control.MousePosition; mousePos.Offset(mouseOffset.X, mouseOffset.Y); Location = mousePos; } }
第3个问题我的思路是处理鼠标单击事件(这就是为什么我放弃了用消息处理机制来解决问题2),根据鼠标的坐标位置判断是在点击哪个图片来触发对应的事件
private void Main_MouseClick(object sender, MouseEventArgs e) { //判断鼠标是否点的是右上角按钮区域 if (e.X >= formMinX && e.X <= formMinX + Resources.skin_btn_min.Width && e.Y <= imgFormMin.Height) { this.WindowState = FormWindowState.Minimized; } if (e.X >= formMaxX && e.X <= formMaxX + Resources.skin_btn_max.Width && e.Y <= imgFormMax.Height) { if (this.WindowState != FormWindowState.Maximized) this.WindowState = FormWindowState.Maximized; else this.WindowState = FormWindowState.Normal; } if (e.X >= formCloseX && e.X <= formCloseX + Resources.skin_btn_close.Width && e.Y <= imgFormClose.Height) { Application.Exit(); } }
然后最后的问题就是解决双击“标题栏”来最大化或者还原窗体了,我的思路是在绘制窗体的时候,就记录标题栏的边界坐标值,然后在双击事件中,根据鼠标的坐标位置来判断触发最大化(还原)事件。
private void Main_MouseDoubleClick(object sender, MouseEventArgs e) { if (e.X >= formTitleMarginLeft && e.X <= formTitleMarginRight && e.Y <= imgTopMiddle.Height) { if (this.WindowState != FormWindowState.Maximized) this.WindowState = FormWindowState.Maximized; else this.WindowState = FormWindowState.Normal; } }
OK,整个皮肤的制作基本就完成了,乍一看貌似基本功能都实现了,但是如果想实现动态换肤,还是很麻烦的,下篇文章我会给出个动态换肤的解决方案和实现源码。
就这样了,欢迎拍砖。(源码下载)