SplitContainer控件扩展之收缩面板
补充说明,尤其一开始自己没注意到 “程序员之窗”网站提供了示例下载,所以可能我文中有些言语不太妥当,
在此说明,希望不要引起不必要的误会,此文仅当一个学习的例子。
--------------------------------------------------------------------
前几天路过一个网站时看到一个关于SplitContainer控件美化的文章,实现的效果如图:(直接引用自那个站)
说起这个站看到的这篇文章,我还真是生气。本来觉得这个效果蛮好的,也想学习下。结果这玩意TMD整个一篇废文,
为什么我要生气呢,这边文章只是展示了最终效果,并且贴了一点点无关紧要的代码,连如何实现,什么原理完全木有讲。给我等菜鸟看来真是一头雾水不说,还要误导大家。
不知道这种文章发出来是为了打广告呢,还是怎么的。
既然人家效果好,那咱就自己动手来实现吧,当然咱不能跟他一样,咱得把具体实现方法过程记录下来供不会的朋友学习。
好了,言归正传! 我们按照上面的图示来看,需要实现的就是SplitContainer 中间那有散点加小三角的那部分,
通过点击小三角就能直接隐藏一个Panel,。
我们一步一步来分析下,如何实现这种效果:
我们先创建一个类 SplitContainerEx 让它继承自SplitContainer,我们的目的是扩展,不是要完全自己做一个SplitContainer。
首先:要实现那种散点加小三角的按钮样式, 我们能想到的就是这里是一个按钮,按钮上面图片或者内容能表示为这种形式;
不过我觉得按钮的话,存在按钮样式,可能会使美观上看上去不太平滑, 所以我想应该是直接在SplitContainer 控件背景上绘制那些散点和小三角
第一步知道需要绘制,这也是做自定义控件首先可能会注意到的问题,实际上也就是重写OnPaint 函数了,需要一点GDI知识。
怎么来绘制这一部分呢,如果单纯绘制散点再绘制小三角我觉得是不是有点麻烦,那我们能不能先自己生成一张图片,图片就是散点+小三角,然后再将这张图片绘制到具体 位置呢。
OK,这么想到了,那就没问题了。因为对于我们菜鸟来说,想到是第一步,动手做才是第2步。 那我们就来实现绘制一张包括散点加小三角的Bitmap吧:
{
Bitmap bmp = new Bitmap(80, 9);
for (int i = 5; i <= 30; i += 5)
{
for (int j = 1; j <= 8; j += 3)
{
bmp.SetPixel(i, j, color);
}
}
for (int i = 50; i <= 75; i += 5)
{
for (int j = 1; j <= 8; j += 3)
{
bmp.SetPixel(i, j, color);
}
}
int p = 35, q = 45;
if (collapse)
{
int j = 1;
for (int i = p; i <= q; i++)
{
if (j > 8)
break;
bmp.SetPixel(i, j, color);
if (i == q)
{
p++;
q--;
j++;
i = p - 1;
}
}
}
else
{
int j = 8;
for (int i = p; i <= q; i++)
{
if (j < 1)
break;
bmp.SetPixel(i, j, color);
if (i == q)
{
p++;
q--;
j--;
i = p - 1;
}
}
}
return bmp;
}
我们首先创建一个 80*9大小的图片,通过SetPixel设置图片中点的颜色,那我们就分析下,高度为9的话,我们可以每隔3个像素竖直方向绘制一个点
即(x,2),(x,5),(x,8) 这样正好在竖直方向3个点并且看起来也位置适中比较美观
横向的是同一个道理,宽度80,那我就让2端的散点各位6个每隔间隔5像素 这种的话左边就是(5,y) (10,y) (15,y) (20,y)(25,y)(30,y)
再加上竖直方向的绘制,正好形成这样一种 6*3 矩阵散点外观效果,还是比较好看滴。
右端同样道理的绘制;
好了, 散点的绘制没问题了,中间小三角的绘制的话,一下就让我想起来大一学C时,老师总会要求你在CMD窗口中编写算法输出倒三角,菱形==。
不过,,,可怜的是我不是个好学生,算法还真不怎么样。
所以就有了这么一段绘制代码
if (collapse)
{
int j = 1;
for (int i = p; i <= q; i++)
{
if (j > 8)
break;
bmp.SetPixel(i, j, color);
if (i == q)
{
p++;
q--;
j++;
i = p - 1;
}
}
}
else
{
int j = 8;
for (int i = p; i <= q; i++)
{
if (j < 1)
break;
bmp.SetPixel(i, j, color);
if (i == q)
{
p++;
q--;
j--;
i = p - 1;
}
}
}
我们散点的绘制左端是到30像素 右端散点开始绘制从50像素,那我们小三角的底边宽度就从35到45。
然后我们这个时候要考虑到一个问题就是,我们绘制出来的这张图片,要根据Panel面板 收缩或者展开小三角的方向应该是不一样的,
所以绘制的时候就做一个判断能形成上下两种方向的小三角。
那完整的绘制呢,就是上面给出的 CreateCollapseImage 函数咯。
接着下一步,我们已经能制作出一个散点小三角样式的按钮了,那我们要绘制到哪里呢,。
太傻了我,当然要绘制到SplitContainer中间伸缩条的正中位置咯,其实SplitContainer控件的一些属性也已经给出可以定位的信息了,
我们来看下完整代码:
//需要绘制的图片
Bitmap bmp = CreateCollapseImage(!IsCollpase, CollpaseColor);
//绘制区域
if (this.Orientation == Orientation.Vertical)
{
rect.X = this.SplitterDistance;
rect.Y = this.Height <= 80 ? 0 : this.Height / 2 - 40;
rect.Width = 9;
rect.Height = 80;
bmp.RotateFlip(RotateFlipType.Rotate90FlipX);
}
else
{
rect.X = this.Width <= 80 ? 0 : this.Width / 2 - 40;
rect.Y = this.SplitterDistance;
rect.Width = 80;
rect.Height = 9;
}
//清除绘制区域
e.Graphics.SetClip(rect);
e.Graphics.Clear(this.BackColor);
//绘制
e.Graphics.DrawImage(bmp, rect);
base.OnPaint(e);
}
在这里呢,我们其实是需要判断SplitContainer 的分割方向是水平还是竖直,因为这关系到我们绘制这个收缩图片的位置。
还有需要注意容错的就是,如果SplitContainer的高度或者说宽度小于需要绘制的图片时,要注意定位好绘制的区域哦。
大家还要注意的是,可能水平和竖直的时候,散点小三角样式中小三角是水平还是竖直方向这个问题, 其实这个我们巧取下就可以了,直接生成为水平方向的,如果需要竖直
就用 Bitmap.RotateFlip(RotateFlipType.Rotate90FlipX); 方法直接旋转90度就OK咯。
好了,有了前面的两步之后,我们先来处理下细节方面的问题,在考虑如何实现收缩。
我们知道,我们要能够实现这种散点小三角样式,我们需要的SplitContainer的 SplitterWidth 必须得为9,为什么呢,因为我们需要这么宽或者高的距离来显示这个按钮样式
那就需要我们禁止我们开发时能修改SplitterWidth , 所以我们需要覆盖一下基类的SplitterWidth属性
/// <summary>
/// </summary>
public new int SplitterWidth
{
get
{
return base.SplitterWidth;
}
set
{
base.SplitterWidth = 9;
}
}
还有,我们要想想,如果我实现了这种点击收缩按钮面板就能收缩的扩展之后,我还需要让SplitContainer能够有拆分器吗?
我是觉得没必要了,不知道大家如何考虑。所以我需要把IsSplitterFixed 属性也给隐藏掉。
还有一点比较关键,直接影响我们这个扩展的美观程度, 那就是我鼠标移动到散点小三角样式的按钮上时,它是否应该有所表示或者说变化,比如颜色变化鼠标手势变化=
当然,这是必须的,所以由于为了使颜色变化我们还需要其他一些处理,会再下面讲到。
好了,简单说了几个细节,我们继续来讲主要的功能实现,不知道大家熟悉不熟悉SplitContainer这个控件的使用,其实它有很多不错的功能的,一些属性也可能是我们平时不注意的。比如 Panel2MinSize ,Panel2Collpase ,等。
大家可以先设置下这2个属性不同的值,看看效果如何,然后就接着进行我们的设计。
这里还要提一点就是,我觉得如果我们实现这样收缩面板的功能,是否要考虑收缩的面板是Panel1还是Panel2呢,
这里从我自己的需求,以及简单来说,我就单一实现我们只隐藏和展开Panel2,因为常理中我总觉得2是比较小的,在使用中做拆分的时候需要隐藏较小的那一面。
至于我们要怎么实现Panel2的隐藏,其实是非常简单的,先看代码:
if (rect.Contains(e.Location))
{
if(IsCollpase)
{
IsCollpase = false;
this.SplitterDistance = _HeightOrWidth;
}
else
{
IsCollpase = true;
_HeightOrWidth = this.SplitterDistance;
if (this.Orientation == Orientation.Vertical)
{
this.SplitterDistance = this.Width - 9;
}
else
{
this.SplitterDistance = this.Height - 9;
}
}
this.Invalidate(this.SplitterRectangle); //局部刷新绘制
}
base.OnMouseClick(e);
}
这是必须的,也就是我们在重写了OnPaint后,还需要重写的有OnMouseClick ,OnMouseMove ,这些东西在我们做自定义控件的时候是经常需要处理的!
我们这里是在 OnMouseClick 中判断点击,这里有一句if (rect.Contains(e.Location)) 大家可以对照OnPait函数里可以发现rect是一个全局的变量,
当然这个变量表示的区域就是我们的那个散点三角样式按钮的绘制区域,我们判断鼠标点击在这个区域时才进行处理:
处理的过程我必须要说下,其实也是比较巧取的,首先我们需要让需要隐藏的Panel2MinSize最小值为0 ,为什么要这样,大家可以看看我前面说的自己去设置看看效果。
Panel2MinSize 为0之后,我们在设置SplitterDistance 拆分器距离左边或者上边的像素距离,就能够很好的隐藏Panel2 并且让拆分器仍然显示,
最为巧妙的是 SplitterDistance 属性微软已经帮我们做的很好了,不管水平方向还是竖直方向都是用这同一个属性来表示的,所以我们的处理就非常简单了,
不要忘记保存下未收缩前的SplitterDistance 值哦(_HeightOrWidth),
然后就是我们操作的时候为了让样式随着操作同时变化就需要重绘某些部分了,比如this.Invalidate(this.SplitterRectangle); //局部刷新绘制
上面这段说明了,点击收缩之后的操作,还有一步就是鼠标移动到收缩按钮上之后样式的变化,代码如下:
if (this.SplitterRectangle.Contains(e.Location))
{
if (rect.Contains(e.Location))
{
this.Cursor = Cursors.Hand;
CollpaseColor = SystemColors.GradientActiveCaption;
}
else
{
this.Cursor = Cursors.Default;
CollpaseColor = SystemColors.ButtonShadow;
}
this.Invalidate(this.SplitterRectangle); //局部刷新绘制
}
base.OnMouseMove(e);
}
其实这些也都是做自定义控件中常用到的需要处理的地方,也就不再细说了!
当我们完成了上面这些代码之后,我们就可以实际测试一下了,下面这张图就是一张实际测试时候的图
实现到这里的时候,大部分也就算完成了,但是我们发现我们在SplitContainer控件的MouseMove事件里判断鼠标位置并设置 鼠标手势和收缩按钮的颜色
但是鼠标移出后必须要恢复的,
由于每一个控件都有自己的消息系统,当我们鼠标移出到Panel1或者Panel2的时候,SplitContainer控件的MouseMove 实际上并没有得到消息,
只有当我们鼠标在收缩按钮的横向那9个宽度或者高度的区域移动时才是真正的SplitContainer控件能拦截到的鼠标移动消息。
那我们就必须要加一点特殊处理了,当鼠标移动到Panel1或者Panel2中的时候恢复鼠标手势和散点小三角颜色, 处理Panel1 和Panel2的 MouseEnter 事件
{
if (this.Cursor == Cursors.Hand)
{
this.Cursor = Cursors.Default;
CollpaseColor = SystemColors.ButtonShadow;
this.Invalidate(this.SplitterRectangle); //局部刷新绘制
}
}
在此测试时,发现还有不完美,当Panel2或者Panel1中的控件又遮住它自身时,它自身的MouseEnter 又得不到消息了,所以我们还得继续添加处理:
为了不再啰嗦,就贴完整的代码咯,如下:
*
* Email:huliang@yahoo.cn
* Date:2011-01-12
*
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.ComponentModel;
namespace UserCtrl
{
[ToolboxBitmap(typeof(SplitContainer))]
public class SplitContainerEx : SplitContainer
{
enum MouseState
{
/// <summary>
/// 正常
/// </summary>
Normal,
/// <summary>
/// 鼠标移入
/// </summary>
Hover
}
public SplitContainerEx()
{
this.SetStyle(
ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.OptimizedDoubleBuffer, true);
this.SplitterWidth = 9;
this.Panel1MinSize = 0;
this.Panel2MinSize = 0;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new int SplitterWidth
{
get
{
return base.SplitterWidth;
}
set
{
base.SplitterWidth = 9;
}
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new int Panel1MinSize
{
get
{
return base.Panel1MinSize;
}
set
{
base.Panel1MinSize = 0;
}
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new int Panel2MinSize
{
get
{
return base.Panel2MinSize;
}
set
{
base.Panel2MinSize = 0;
}
}
public enum SplitterPanelEnum
{
Panel1,
Panel2
}
SplitterPanelEnum mCollpasePanel = SplitterPanelEnum.Panel2;
/// <summary>
/// 进行折叠或展开的SplitterPanel
/// </summary>
[DefaultValue(SplitterPanelEnum.Panel2)]
public SplitterPanelEnum CollpasePanel
{
get
{
return mCollpasePanel;
}
set
{
if (value != mCollpasePanel)
{
mCollpasePanel = value;
this.Invalidate(this.ControlRect);
}
}
}
bool mCollpased = false;
/// <summary>
/// 是否为折叠状态
/// </summary>
public bool IsCollpased
{
get { return mCollpased; }
}
Rectangle mRect = new Rectangle();
/// <summary>
/// 控制器绘制区域
/// </summary>
private Rectangle ControlRect
{
get
{
if (this.Orientation == Orientation.Horizontal)
{
mRect.X = this.Width <= 80 ? 0 : this.Width / 2 - 40;
mRect.Y = this.SplitterDistance;
mRect.Width = 80;
mRect.Height = 9;
}
else
{
mRect.X = this.SplitterDistance;
mRect.Y = this.Height <= 80 ? 0 : this.Height / 2 - 40;
mRect.Width = 9;
mRect.Height = 80;
}
return mRect;
}
}
/// <summary>
/// 鼠标状态
/// </summary>
MouseState mMouseState = MouseState.Normal;
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
//绘制参数
bool collpase = false;
if ((this.CollpasePanel == SplitterPanelEnum.Panel1 && mCollpased == false)
|| this.CollpasePanel == SplitterPanelEnum.Panel2 && mCollpased)
{
collpase = true;
}
Color color = mMouseState == MouseState.Normal ? SystemColors.ButtonShadow : SystemColors.ControlDarkDark;
//需要绘制的图片
Bitmap bmp = CreateControlImage(collpase, color);
//绘制区域
if (this.Orientation == Orientation.Vertical)
{
bmp.RotateFlip(RotateFlipType.Rotate90FlipX);
}
//清除绘制区域
e.Graphics.SetClip(this.SplitterRectangle); //这里需要注意一点就是需要清除拆分器整个区域,如果仅清除控制按钮区域,则会出现虚线状态
e.Graphics.Clear(this.BackColor);
//绘制
e.Graphics.DrawImage(bmp, this.ControlRect);
}
public new bool IsSplitterFixed
{
get
{
return base.IsSplitterFixed;
}
set
{
base.IsSplitterFixed = value;
//此处设计防止运行时更改base.IsSplitterFixed属性时导致mIsSplitterFixed变量判断失效
if (value && mIsSplitterFixed == false)
{
mIsSplitterFixed = true;
}
}
}
bool mIsSplitterFixed = true;
protected override void OnMouseMove(MouseEventArgs e)
{
//鼠标在控制按钮区域
if (this.SplitterRectangle.Contains(e.Location))
{
if (this.ControlRect.Contains(e.Location))
{
//如果拆分器可移动,则鼠标在控制按钮范围内时临时关闭拆分器
if (this.IsSplitterFixed == false)
{
this.IsSplitterFixed = true;
mIsSplitterFixed = false;
}
this.Cursor = Cursors.Hand;
mMouseState = MouseState.Hover;
this.Invalidate(this.ControlRect);
}
else
{
//如果拆分器为临时关闭,则开启拆分器
if (mIsSplitterFixed == false)
{
this.IsSplitterFixed = false;
if (this.Orientation == Orientation.Horizontal)
{
this.Cursor = Cursors.HSplit;
}
else
{
this.Cursor = Cursors.VSplit;
}
}
else
{
this.Cursor = Cursors.Default;
}
mMouseState = MouseState.Normal;
this.Invalidate(this.ControlRect);
}
}
base.OnMouseMove(e);
}
protected override void OnMouseLeave(EventArgs e)
{
this.Cursor = Cursors.Default;
mMouseState = MouseState.Normal;
this.Invalidate(this.ControlRect);
base.OnMouseLeave(e);
}
protected override void OnMouseClick(MouseEventArgs e)
{
if (this.ControlRect.Contains(e.Location))
{
CollpaseOrExpand();
}
base.OnMouseClick(e);
}
int _HeightOrWidth;
/// <summary>
/// 折叠或展开
/// </summary>
public void CollpaseOrExpand()
{
if (mCollpased)
{
mCollpased = false;
this.SplitterDistance = _HeightOrWidth;
}
else
{
mCollpased = true;
_HeightOrWidth = this.SplitterDistance;
if (CollpasePanel == SplitterPanelEnum.Panel1)
{
this.SplitterDistance = 0 ;
}
else
{
if(this.Orientation==Orientation.Horizontal)
{
this.SplitterDistance = this.Height - 9;
}
else
{
this.SplitterDistance = this.Width - 9;
}
}
}
this.Invalidate(this.ControlRect); //局部刷新绘制
}
/// <summary>
/// 需要绘制的用于折叠窗口的按钮样式
/// </summary>
/// <param name="collapse"></param>
/// <param name="color"></param>
/// <returns></returns>
private Bitmap CreateControlImage(bool collapse, Color color)
{
Bitmap bmp = new Bitmap(80, 9);
for (int x = 5; x <= 30; x += 5)
{
for (int y = 1; y <= 7; y += 3)
{
bmp.SetPixel(x, y, color);
}
}
for (int x = 50; x <= 75; x += 5)
{
for (int y = 1; y <= 7; y += 3)
{
bmp.SetPixel(x, y, color);
}
}
//控制小三角底边向上或者向下
if (collapse)
{
int k = 0;
for (int y = 7; y >= 1; y--)
{
for (int x = 35 + k; x <= 45 - k; x++)
{
bmp.SetPixel(x, y, color);
}
k++;
}
}
else
{
int k = 0;
for (int y = 1; y <= 7; y++)
{
for (int x = 35 + k; x <= 45 - k; x++)
{
bmp.SetPixel(x, y, color);
}
k++;
}
}
return bmp;
}
}
}
这下我们就可以将完整代码复制出来,自己来试试完整效果了。 本文也就结束咯,更完美更细致的处理还需要我们在实际需要的时候进行细化,这里只展示一种大概的实现方式,欢迎大家打击!