重绘TabControl
本文转载自:http://blog.csdn.net/conmajia/article/details/7596718
作者:野比 (conmajia@gmail.com)
时间:May, 2012
封面图片为野比原创,请勿未经允许私自引用
#1-1
嗯,各位,又是我,生物钟颠倒的家伙。
今天我要山寨的是大名鼎鼎的Apple,传说中的「被山寨之王」。
没错,都被我山寨好几次了。
说起Apple,相信大家对他家的各种产品,不管他软还是硬,都有相当的好感。
最近Apple把自家的Web浏览器Safari升级到了第5版,并同步推出了Windows版,支持WinXP开始的全部Windows版本。
不得不说,这是一个很给力的浏览器,它看起来就像这样。
Icon for Safari
其实我并不是苹果控,我控红富士要多点。客观的评价Safari,这个软件界面华丽,速度快,但在Windows平台上,TopSites首页资源消耗巨大,操作习惯和常规Win浏览器有一定区别,部分网页不支持或不兼容(WebKit引擎)。
不多说了,这不是重点。重点在于它的「偏好设置(Preference)」界面,就是这个:
看到这个,你肯定会觉得怎么苹果的东西会变得这么一般呢?不过就是TabControl上面增加了几个图标嘛。
嗯,朋友,你说的似乎没错。但是,我曾经也算中肯的评价过苹果的东西,抛开外观,苹果的特点之一就是「闷骚」,还有「OCD」,也就是强迫症。
听我这么说显得很干瘪,那么就让我顺着导航标签,一路点击过去,看看会发生什么事。
没错,这窗口会自动伸缩,而且是动画的!这就是apple闷骚的地方!
为了不让他一家独骚,为了不辜负他被山寨之王的名头,我只好勉为其难的山寨一番了。
山寨前的准备
山寨其实没啥好准备的,但还是需要几样重要的东西:
- 原装货:Apple Safari 5
- 照相机:Snagit 10
- 生产线:Visual Studio 2005
- 手册:MSDN
- 苦力:野比
分析,分析
山寨的灵魂在于分析,首先把刚才拍的高清果照扯过来分解了。
所以,我把他分解成这几个部分:
- 根据标签不同修改窗体标题
- 导航标签
- 标签面板
- 自动缩放
组件设计
分析了其中的功能,那么就要想想怎么来实现。
从功能来看,这个窗口实际上是由多个子面板切换来实现的,最多他加了点自动缩放。所以从本质来说,还是一个标签切换的窗口。
我最早想到的就是大名鼎鼎却又丑得无以复加的TabControl。
按照标签切换这个思想,TabControl完全可以胜任这次的山寨需求。但是TabControl这么丑,必须要给它整整容才行。想不到我竟然有整容的才华。
下手吧,年轻人!
因为要改动的地方会很多,所以还是完全自己来绘制标签好了。为了完全自定义TabControl,同时方便循环利用,从TabControl派生一个我们自己的标签控件TabControlEx。
- public class TabControlEx : System.Windows.Forms.TabControl
这就是我们的TabControlEx,看起来和TabControl没什么两样(那是当然的)。
为了让他看起来不太一样,在构造函数里加上下面的代码。
- base.SetStyle(
- ControlStyles.UserPaint | // 控件将自行绘制,而不是通过操作系统来绘制
- ControlStyles.OptimizedDoubleBuffer | // 该控件首先在缓冲区中绘制,而不是直接绘制到屏幕上,这样可以减少闪烁
- ControlStyles.AllPaintingInWmPaint | // 控件将忽略 WM_ERASEBKGND 窗口消息以减少闪烁
- ControlStyles.ResizeRedraw | // 在调整控件大小时重绘控件
- ControlStyles.SupportsTransparentBackColor, // 控件接受 alpha 组件小于 255 的 BackColor 以模拟透明
- true); // 设置以上值为 true
- base.UpdateStyles();
这段代码的意思就像注释里说的,注意ControlStyles这个枚举是可以按位组合的,所以上面要用「或(|)」来进行连接,这样系统就会完全忽视TabControl这个基类的界面显示,而使用我们自己的方式来呈现UI。
现在TabControlEx看起来是这样的。
啥米?!!OMG!东西哪去了??
嗯,当我第一次玩UserPaint的时候,也被吓了一跳。其实这就是上面我们设置的那句ControlStyles.UserPaint,于是系统就不帮我们画任何东西了。
所以从现在开始,一切都要靠自己了。下面所有的绘制都在OnPaint()方法中绘制。
为了先让我们找到方向,在OnPaint()方法中,我们先把Tab的位置找到,为此我们给每个Tab的边框都画出来。
- protected override void OnPaint(PaintEventArgs e)
- {
- for (int i = ; i < this.TabCount; i++)
- {
- e.Graphics.DrawRectangle(Pens.Red, this.GetTabRect(i));
- }
- }
TabControl.GetTabRect(int)的功能是获得指定index的标签的矩形位置。画完后,我们的TabControlEx看起来不那么迷糊了。
可是,标签的大小还是不对,我们要的不是普通的那种长条,而是闷骚的苹果的瘦高型,要像这样。
嗯,好吧,我们回到构造函数,用下面的语句来设置大小。
- this.SizeMode = TabSizeMode.Fixed; // 大小模式为固定
- this.ItemSize = new Size(, ); // 设定每个标签的尺寸
上面设置44x55其实只是因为苹果原版刚好是这么大,先这么着,后面如果不合适了,回头再来改。现在标签是这样的了。
Apple标签的选中状态是带阴影的,看起来很酷,可是如果我用GDI+来画的话,什么渐变什么变换,烦都烦死了。怎么办呢?
请记住,我们正在山寨。所谓山寨的精神,就是不问方法、不择手段,只要最后「看起来一样」就行了。所以,我决定用上抠图大法,把apple的背景图抠出来。
把这个背景保存为TabBackground.bmp文件,然后添加到项目中,把它做成「嵌入的资源」,就像这样。
然后我们用一个变量来保存背景图。因为这张图随时会用到,所以还是做成全局变量(类级别),在构造函数里读取图片。
- Image backImage;
- public TabControlEx()
- {
- // (略)
- backImage = new Bitmap(this.GetType(), "TabButtonBackground.bmp"); // 从资源文件(嵌入到程序集)里读取图片
- }
现在有了图标,加上去看看吧。在OnPaint()里这样写。
- if (this.SelectedIndex == i)
- {
- e.Graphics.DrawImage(backImage, this.GetTabRect(i));
- }
只有被选中的标签才会出现这种背景。于是,标签变成这样了。
绘制文字
这会看着还挺单调的,所以我们来加点料。下面来画文字。说起文字,我想你应该注意到了,Safari的标签文字,都是带有阴影的(准确的说是高光)。
所以,在绘制文字时,先用高光色绘制第一遍,再用普通文字色(黑)绘制第二遍。
- protected override void OnPaint(PaintEventArgs e)
- {
- for (int i = ; i < this.TabCount; i++)
- {
- // (略)
- // Calculate text position
- Rectangle bounds = this.GetTabRect(i);
- PointF textPoint = new PointF();
- SizeF textSize = TextRenderer.MeasureText(this.TabPages[i].Text, this.Font);
- // 注意要加上每个标签的左偏移量X
- textPoint.X
- = bounds.X + (bounds.Width - textSize.Width) / ;
- textPoint.Y
- = bounds.Bottom - textSize.Height - this.Padding.Y;
- // Draw highlights
- e.Graphics.DrawString(
- this.TabPages[i].Text,
- this.Font,
- SystemBrushes.ControlLightLight, // 高光颜色
- textPoint.X,
- textPoint.Y);
- // 绘制正常文字
- textPoint.Y--;
- e.Graphics.DrawString(
- this.TabPages[i].Text,
- this.Font,
- SystemBrushes.ControlText, // 正常颜色
- textPoint.X,
- textPoint.Y);
- }
- }
缤纷色彩的源泉:图标
文字也有了,那么接下来就轮到图标了。TabControl是用ImageList控件来存储自己使用的图标的,那么添加一个ImageList,然后加入图标。注意这里都要32x32的图标,所以应该设置ImageList.ImageSize为32x32。
- // 绘制图标
- if (this.ImageList != null)
- {
- int index = this.TabPages[i].ImageIndex;
- string key = this.TabPages[i].ImageKey;
- Image icon = new Bitmap(, );
- if (index > -)
- {
- icon = this.ImageList.Images[index];
- }
- if (!string.IsNullOrEmpty(key))
- {
- icon = this.ImageList.Images[key];
- }
- e.Graphics.DrawImage(
- icon,
- bounds.X + (bounds.Width - icon.Width) / ,
- bounds.Top + this.Padding.Y);
- }
嗯,现在我们的标签看起来像那么回事了,接下来就该难看的红线条退休了。再完善一下,我们的标签就OK了。
同步滚动演示。上面是山寨,下面是正品,正品打开了文字抗锯齿,我们也可以,在OnPaint()事件开始加入这样的代码。
- e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
到此,标签导航部分已经完成,剩下的,就是窗体的自动缩放功能了。
作者:野比 (conmajia@gmail.com)
时间:May, 2012
#1-2
嗯,还是我。
现在继续昨天的山寨。昨天我们分析得到了4条需要山寨的部分,如下。
- 根据标签不同修改窗体标题
- 导航标签
- 标签面板
- 自动缩放
通过昨天的努力,我们已经搞定了第2、3条,所以,今天的任务,就只剩下两条
- 根据标签不同修改窗体标题
- 导航标签
- 标签面板
- 自动缩放
修改窗体标题
我们参考下图,
我们制作的TabControlEx是作为它所在窗体的子控件存在的,为了获得包含TabControlEx的窗体(的引用),可以调用TabControlEx的FindForm()方法(从Control继承)。FindForm()可以获取容纳该控件的顶层窗体,在我们的例子里,就是我们的山寨Safari窗体。
为了在TabControlEx刚刚加入父控件的时候(也就是窗体初始化的时候)就能够顺利「劫持」到窗体的引用,并修改它的标题(否则显示Tab0的时候会发现窗体的标题还未改变),我们重写一下TabControlEx的ParentChanged事件。
- // 对父窗体的引用
- Form oldman;
- protected override void OnParentChanged(EventArgs e)
- {
- // 如果没有劫持到,则搜索
- if (oldman == null)
- oldman = this.FindForm();
- oldman.Text = this.TabPages[].Text;
- }
这样,我们就可以在启动时就修改父窗体标题了。我们最终的目的是每次切换标签时都改变父窗体标题,现在我们拿到了窗体的引用,只需要重写TabControlEx的Selected事件。
- protected override void OnSelected(TabControlEventArgs e)
- {
- parent.Text = e.TabPage.Text;
- }
下面是完成之后的效果
自动调整窗体大小
完成了杂项工作,现在要进入今天的重点:自动调整大小。在开始之前,先来回顾一下这个闷骚的功能。
下面来好好分析一下到底发生了什么事。
注意,大家发现右下角那个问号没有?根据观察,那个问号始终是保持在窗体右下角的,这就好办了,直接Anchor到Right和Bottom就行。因此下面的分析中直接无视它了。
从本质上来看,因为切换的标签内容高度不同,所以窗体高度也发生了改变。但不管怎么变,窗体的底部到最下面一个控件的距离Δ没有变化,参考分析图。
所以,动画就是在H1-H2这段距离内发生的。另外,值得注意的是,Safari是在窗体动画完成,调整大小到位以后,才显示新标签的控件,这样做可以显得很有动感,而且留下了足够的时间加载控件。所以,动画应该在标签的Selecting事件里解决,而显示控件留到Selected事件。
下面来分析大小调整的算法。
山寨算法:从不追求精确还原
通过慢镜头分析,可以看到在相同时间差内窗体大小的运动距离是不同的,这说明窗体大小不是匀速改变的。
为了不让算法影响我们的设计进度,将算法写在单独的方法里(最正规的应该是写成委托,直接传递方法,但你认为一个山寨货有必要吗)。
- private double getHeight(double time)
- {
- // (略)
- }
既然这样,那么算法的问题我们稍后再来讨论,现在研究怎样让窗体动起来。
由于动画过程较长,将近1秒,那么我们实现的时候应当尽量以不影响主线程为前提。除了动不动就多线程这种有点大炮打蚊子太2的方法外,我们还可以用系统自带的Timer。在每个Timer.Tick事件里挪一步,合起来就成了动画。
- // Δ常量
- int FORM_DELTA = ;
- // 动画用Timer
- Timer timer;
- // 经历时间计数器
- int elapsed = ;
- // 构造函数
- public TabControlEx()
- {
- // (略)
- // 初始化Timer
- timer = new Timer();
- timer.Interval = ;
- timer.Enabled = false;
- timer.Tick += new EventHandler(timer_Tick);
- }
- // Timer tickle
- void timer_Tick(object sender, EventArgs e)
- {
- if (parent == null)
- return;
- elapsed++;
- parent.Height = getHeight(elapsed, FORM_DELTA);
- }
现在我们可以填写刚才分析的Selecting和Selected事件了。
算法尝试
目前流行的加减速函数有很多,最简单的从1次函数(匀速)、2次函数(匀加速)到3、4甚至5次函数都有人在用。这类指数型的加速函数使用简单方便,用得很多。下面是在Mahematica里绘制的几种函数曲线,从上倒下分别为:g=10的自由落体函数,y=x^2,y=x^3,y=x^4和y=x直线(注意:为了让大家看清函数细节,x和y轴不是1:1的)。
看起来要实现又加速又减速还真是麻烦,看来只有去掉减速了。反正山寨嘛,只要「看起来像」就行了。没办法,我们是搞山寨的,手艺当然不行了,所以到底用那种,还真的不知道。山寨大法告诉我们,不知道的东西,「试,就对了」。那么就选3个版本的getHeight()来试试。
作者:野比 (conmajia@gmail.com)
时间:May, 2012
(未完待续)
重绘TabControl的更多相关文章
- C# 重绘tabControl,添加关闭按钮(页签)
C# 重绘tabControl,添加关闭按钮(页签) 调用方法 参数: /// <summary> /// 初始化 /// </summary> /// <param n ...
- C#重绘TabControl
C#重绘TabControl的Tabpage标签,添加图片及关闭按钮 Code highlighting produced by Actipro CodeHighlighter (freeware)h ...
- WinForm中重绘TabControl选项卡标题
最近开发WinForm频繁使用了TabControl控件,这个控件的选项卡没有BackgroundImage这个属性,那么如何为其各个选项卡添加背景图片呢?(这里说的是每个TabPage的头部,也就是 ...
- C# 重绘tabControl,添加关闭按钮(续)
在上一篇随笔中,添加关闭按钮是可以实现 ,但细心一点就会发现,每次关闭一个选项卡,tableControl都会自动跳到第一个页面,显然 这不是我们想要的,为此,我修改了部分的代码.除此之外,我还添加了 ...
- C# 自定义重绘TabControl
using System.Drawing; using System.Windows.Forms; using System.Drawing.Drawing2D; using System.Runti ...
- TabControl控件重绘
原文地址:http://www.codeproject.com/Articles/91387/Painting-Your-Own-Tabs-Second-Edition 在网上看到重绘TabContr ...
- 关于DOM的操作以及性能优化问题-重绘重排
写在前面: 大家都知道DOM的操作很昂贵. 然后贵在什么地方呢? 一.访问DOM元素 二.修改DOM引起的重绘重排 一.访问DOM 像书上的比喻:把DOM和JavaScript(这里指ECMScri ...
- 关于repaint(重绘)和reflow( 回流)
repaint就是重绘,reflow就是回流.repaint主要是针对某一个DOM元素进行的重绘,reflow则是回流,针对整个页面的重排 严重性: 在性能优先的前提下,性能消耗 reflow大于re ...
- MFC 滑动条的重绘
MFC自带的滑动条的样子是这样的. 比较难看,所以需要重绘下,重绘后的样子是这样的. 代码如下: CustomSliderCtr.h #pragma once // CCustomSliderCtr ...
随机推荐
- Debian 入门安装与配置2
Debian 入门安装与配置2 1. C/C++开发必装软件 atp-get install gcc 这个不用说,用来编译C程序 apt-get install g++ 用来编译C++程序 ap ...
- [翻译]深度学习的机器(The learning machines)
学习的机器 用大量的数据识别图像和语音,深度学习的计算机(deep-learning computers) 向真正意义上的人工智能迈出了一大步. Nicola Jones Computer Scien ...
- Ansible安装配置Nginx
一.思路 现在一台机器上编译安装好nginx.打包,然后在用ansible去下发 cd /etc/ansible 进入ansible配置文件目录 mkdir roles/{common,install ...
- [HAOI2012]音量调节
题目描述 一个吉他手准备参加一场演出.他不喜欢在演出时始终使用同一个音量,所以他决定每一首歌之前他都需要改变一次音量.在演出开始之前,他已经做好一个列表,里面写着每首歌开始之前他想要改变的音量是多少. ...
- hihoCoder挑战赛23
hihoCoder挑战赛23 A.Emulator 题意 给一张图,有\(N(N \le 300)\)个点, 给出任意两点之间的最短路. 求最多可以去掉多少条边,使得任意两点的最短路长度不变. 思路 ...
- CDH hive的安装
tar zxvf 解压包 配置环境变量 export HIVE_HOME=/usr/local/soft/hiveexport PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HO ...
- [poj 3261]Milk Patterns
后缀数组搞一下就可以了喵~ 其实这道题的第一个想法是 SAM ,建完后缀自动机后拓扑排序跑一遍统计下每个子串的出现次数就 O(N) 就妥妥过掉了 后缀树也是 O(N) 的,统计一下每个节点对应的子树中 ...
- 只显示 前100个字 java 实现截取字符串!使用! <c:if test="${fn:length(onebeans.info)>100 }">${ fn:substri
博客 文章 只显示 前100个字 java 实现截取字符串!使用! <c:if test="${fn:length(onebeans.info)>100 }">$ ...
- ABBYY应用到的行业有哪些
不同的行业组织和企业有不同的业务流程和规定,在OCR文字识别领域,ABBYY FineReader 12给各个行业都提供了有效解决方案,满足其特定需求的同时还帮助他们提高业务流程处理效率,降低成本,全 ...
- 【转】HTML - embed 与 object 之争
在 HTML 里嵌入文本和图片之外的事物,就会用到嵌入标签,而嵌入标签在各浏览器之间的不统一,一直是让开发人员很头痛的问题.一切都要从嵌入 SUN 公司的 Applet Java 小程序开始. 当时, ...