关于C#绘制qq好友列表控件

发现有bug 文章后面

我自己开了一个博客这两天在写自定义控件的开发 有兴趣的朋友欢迎访问:www.clzf.co

上一个效果图

左边那张图貌似忘了展示一个功能

源码下载

http://download.csdn.net/detail/crystal_lz/4755251   个人感觉注释还是比较详细

貌似这是第一次 直接继承Control写控件 以前都是UserControl    对于写控件真没啥经验 途中遇到各种问题 各种蛋疼   所以代码有啥不对的地方 多包涵

- -!、、其实  这里面有一个问题 就是TypeConverter哪里 不知道咋搞 不不用转换器的话 在窗体设计时候 向控件添加项的时候 调用默认构造器 然后其余属性挨个赋值 所以自动生成的代码 非常臃肿  最开始写了TypeConverter但是没有 果断窗体设计时自己都不生成代码了  经测试  转换器内的代码压根就没有执行  后来不知道咋搞的貌似有点反应了 不过编译就报错 说 属性 代码生成失败 无法将XXXX转换为 XXXX.InstanceDescriptor 我就郁闷了  然后百度说 把程序集版本改成 1 0 0 * 就可以  试了一下 确实没有报那个错了 不过又有其他问题 ......

XXXXXXXXXX........!@@¥%……&U总之 p话不多说 由于没有这方面经验 希望哪位会的人士 给予一下指点 到底要怎么才能让那个自己写的 TypeConverter 类正确执行 在窗体设计时候自动生成的代码 看上去和谐    虽然说 这东西一般都是 程序运行出来动态加载列表的   但是在窗体设计的时候 手动添加项的时候 自动生成的代码真的很不和谐 虽然一般情况下也不去看 但感觉已经不爽了...


好吧 上面的都是p话  只是希望有人能解决一下我的问题  现在来说说 我做这个控件的时候的感想吧  在做之前我在网上参考过一些 也下载过两个 不过感觉真不咋滴(我指的是 我下载的那两个 不代表全部)    运行出来 亲一色的 全是企鹅头 这也就算了 还亲一色的只有在线状态   XXXX...!@#¥ 我总结出来的就是 只以做的像为目的 压根就没有考虑过投入使用的问题  所以我在制作的时候考虑了要投入使用的问题(- -!、其实是压根本来就要用所以才考虑到的)  比如图片上 每一个子项拥有一个 布尔 值来决定是否闪烁  还有不同的在线状态 而且还是按照状态进行排序的......


现在来说思路 我在里面 分Item 和 SubItem    Item就是看到的分组而SubItem就是分组下的子项  所以我就搞了两个类 ChatListItem 和 ChatListSubItem 这两个类

ChatListItem被控件包含 而ChatListSubItem 被 ChatListItem 包含 所以大概的代码就是

public class ChatListBox : Control{
	XXXXXXX
	public ChatListItem[] Items {get; set;}
	XXXXXXX
	.......
}

public class ChatListItem{
	XXXXXXX
	public ChatListSubItem[] SubItems {get; set;}
	XXXXXXX
	.......
}

public class ChatListSubItem{
	public int ID {get; set;}
	public string NicName {get; set;}
	.........
	.........
}
当然 真实情况下不是用的数组 这里 只是为了方便   这样 就有了一个层次关系 然后就是绘制的问题

绘制的时候在 OnPaint 中循环Items

protected override void OnPaint(PaintEventArgs e){
	for(int i = 0;i < Items.Count;i++){				//循环绘制每一个Item
		DrawItem(items[i]);		//绘制Item
		if(items[i].IsOpen){	//如果Item的列表是展开的 那么还得绘制他的子项
			for(int j = 0;j < Items[i].SubItem.Count;j++){
				DrawSubItems(Items[i].SubItems[j]);	//绘制子项
			}
		}
	}
}
大概也就这样 不过里面还要计算区域 不能每个Item或者SubItem绘制的时候都重叠到一起啊 所以代码改一个就成这个样子

protected override void OnPaint(PaintEventArgs e){
	Rectangle rectItem = new Rectangle(0,0,this.Width,20);	//假设每个item的高度是20 这是第一个的
	Rectangle rectSubItem = new Rectangle(0,21,this.Width,50)	//假设每个SubItem高50 这个是第一个的 21 是在Item标题下移动一格像素的
	for(int i = 0;i < Items.Count;i++){				//循环绘制每一个Item
		DrawItem(items[i],rectItem);				//绘制Item	增加一个参数
		if(items[i].IsOpen){	//如果Item的列表是展开的 那么还得绘制他的子项
			rectSubItem.Y = rectItem.Bottom + 1;	//这个是展开的第一个的子项 那么他的位置 就该是Item标题的下面一个像素
			for(int j = 0;j < Items[i].SubItem.Count;j++){
				DrawSubItems(Items[i].SubItems[j],rectSubItem);	//绘制子项  增加一个参数
				rectSubItem.Y = rectSubItem.Bottom + 1;			//计算下一个子项的区域
			}
			rectItem.Y = rectSubItem.Bottom + 1;				//子项绘制完  那么该计算下一个Item的坐标
		}else{
			rectItem.Y = rectItem.Bottom;						//如果没有展开 那么下一个item的坐标就是上一个Item的下面一个像素
		}
	}
}
这样绘制上 基本没有啥问题了不过 这也只是绘制出来了而已 你还得在上面操作 比如鼠标点击一个子项  你用什么来判断鼠标点击了一个子项?

所以还得把每个Item和SubItem绘制在什么地方记录下来 到时候用鼠标坐标去比对

所以不管是ChatListItem 还是 ChatListSubItem 都要在他们里面加一个 Rectangle 类型的属性 每绘制一个Item或者SubItem 的时候就把那块区域赋值过去

到时候就用Rectangle.Contains(Point)来判断鼠标到底在那一块区域内

所以就有了我代码中的

for (int i = 0, lenItem = items.Count; i < lenItem; i++) {
	DrawItem(g, items[i], rectItem, sb);        //绘制列表项
	if (items[i].IsOpen) {                      //如果列表项展开绘制子项
		rectSubItem.Y = rectItem.Bottom + 1;
		for (int j = 0, lenSubItem = items[i].SubItems.Count; j < lenSubItem; j++) {
			DrawSubItem(g, items[i].SubItems[j], ref rectSubItem, sb);  //绘制子项
			rectSubItem.Y = rectSubItem.Bottom + 1;             //计算下一个子项的区域
			rectSubItem.Height = (int)iconSizeMode;				//大图标小图标模式的高度
		}
		rectItem.Height = rectSubItem.Bottom - rectItem.Top - (int)iconSizeMode - 1;
		//这里之所以给rectItem高度重新复制 是因为 SubItem 是包含在 Item 内部的
		//所以rectItem的高度就是 他标题的高度 加上他内部所有子项的高度 把他所有的子项都圈起来
	}
	items[i].Bounds = new Rectangle(rectItem.Location, rectItem.Size);
	rectItem.Y = rectItem.Bottom + 1;           //计算下一个列表项区域
	rectItem.Height = 25;
}
每个item和subitem有了一个自己的区域后 那么要判断鼠标是否落在他们某一个区域上就方便了

for(int i = 0;i < Items.Count;i++){	
	if(items[i].Bounds.Contains(MousePoint)){	//如果鼠标位置在 某一个item上
		if(items[i].IsOpen){					//判断该Item是否为展开的
			for(int j = 0;j < Items[i].SubItem.Count;j++){
				if(Items[i].SubItems[j].Bounds.Contains(MousePoint)){
					XXXXXXXX....;
					return;		//鼠标位置只可能在某一个Item 或者 SubItem 上所以找到一个后处理完事情直接return
				}	
			}//如果该项又是展开的 循环完子项却又没有找到符合条件的子项 那么就极有可能鼠标位置在标题上(因为每个Item或者SubItem有一像素间隔)
			if(new Rectangle(0,Items[i].Bounds.Top,this.Height,20).Contains(MousePoint)){
				........;
				return;
			}
		}
	}
}
主要思路 也就差不多是这些 怎么绘制 绘制后取得相应区域  剩下的就是一些细节上的功能 还有滚动条 

对对对 滚动条 所以在绘制的时候 要根据滚动条进行y坐标的一个偏移

g.TranslateTransform(0, -chatVScroll.Value);        //根据滚动条的值设置坐标偏移

这个是我控件中 完整的OnPaint

protected override void OnPaint(PaintEventArgs e) {
            Graphics g = e.Graphics;
            g.TranslateTransform(0, -chatVScroll.Value);        //根据滚动条的值设置坐标偏移
            Rectangle rectItem = new Rectangle(0, 1, this.Width, 25);                       //列表项区域
            Rectangle rectSubItem = new Rectangle(0, 26, this.Width, (int)iconSizeMode);    //子项区域
            SolidBrush sb = new SolidBrush(this.itemColor);
            try {
                for (int i = 0, lenItem = items.Count; i < lenItem; i++) {
                    DrawItem(g, items[i], rectItem, sb);        //绘制列表项
                    if (items[i].IsOpen) {                      //如果列表项展开绘制子项
                        rectSubItem.Y = rectItem.Bottom + 1;
                        for (int j = 0, lenSubItem = items[i].SubItems.Count; j < lenSubItem; j++) {
                            DrawSubItem(g, items[i].SubItems[j], ref rectSubItem, sb);  //绘制子项
                            rectSubItem.Y = rectSubItem.Bottom + 1;             //计算下一个子项的区域
                            rectSubItem.Height = (int)iconSizeMode;
                        }
                        rectItem.Height = rectSubItem.Bottom - rectItem.Top - (int)iconSizeMode - 1;
                    }
                    items[i].Bounds = new Rectangle(rectItem.Location, rectItem.Size);
                    rectItem.Y = rectItem.Bottom + 1;           //计算下一个列表项区域
                    rectItem.Height = 25;
                }
                g.ResetTransform();             //重置坐标系
                chatVScroll.VirtualHeight = rectItem.Bottom - 26;   //绘制完成计算虚拟高度决定是否绘制滚动条
                if (chatVScroll.ShouldBeDraw)   //是否绘制滚动条
                    chatVScroll.ReDrawScroll(g);
            } finally { sb.Dispose(); }
            base.OnPaint(e);
        }
也就是 在绘制控件的时候 坐标就不是 0 0 左上角位置开始绘制了 而是一个 0 Y 的一个虚拟的坐标了 Y 的值是由滚动条决定的  

所以在判断鼠标落的区域的时候 也有点变化了 Rectangle.Contains(MousePont.Y + 滚动条的值);

还有就是 离线 离开状态什么的 这些都是枚举值 然后ChatListSubItem实现一个排序接口 按照他们的顺序排序就可以了

其实 枚举值 也就相当于 int 值所以在 定义枚举类的时候 把顺序搞定  对SubItem排序的时候 按照一个升序排序就搞定了 这个和普通的排序没啥区别

还有就是 头像闪动的问题  在SubItem里面有一个 布尔值 来决定是否闪动 不仅是它 它所属的Item也的有关联  (在定义ChatListSubItem类的时候里面有个ChatListItem类型的一个Owner属性来表示该SubItem是属于哪个Item的  Item 同理有一个表示属于哪一个ChatListBox控件的 因为 在这些类中的一些操作要引发控件的重绘 所以要把他们一级一级关联)

因为 列表关闭的时候 头像没有办法闪动 所以只有列表标题闪动 所以在ChatListItem类里面 还定义了一个 计数器 保存在它列表下的子项闪动的个数 重绘的时候 如果列表关闭 计数器不为0 那么闪动列表标题 如果列表打开 那么闪动头像       当然在给ChatListSubItem的那个决定是否闪动的属性赋值的时候 响应的也要给所属的Item的计数器 进行操作

对于这个控件 我暂时能想到的就这些控件自身功能 然后就是给用户的接口了

我里面只定义了上个自定义事件 因为想了一下 在实际应用中  也就只有这三个有用

双击一个列表中的子项的时候   (QQ的话就弹出聊天对话框了)     【DoubleClickSubItem】

鼠标移动到一个子项的头像上面  (QQ这个时候 坐标出现一个小窗体 来加载该用户的一些资料)   【MouseEnterHead】

鼠标离开该子项的头像  (QQ的画 那个小窗体就消失了) 【MouseLeaveHead】


不过 我还是希望有谁能 解决一下那个TypeConverter的问题 虽然感觉没啥用处    不过 心理上却感觉不爽

修改bug

正如下面评论中 adrianEvin 提到的

“点击闪速,当闪速到没有时候再点闪速停止之后
图标都变成没有的了 鼠标移上去又好了 哈哈”

解决办法:


在ChatListSubItem中的 IsTwinkle 属性中加上最后一句

你可能感兴趣的:(无聊,控件,打酱油)