作者:野比 (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
  • 苦力:野比

分析,分析

山寨的灵魂在于分析,首先把刚才拍的高清果照扯过来分解了。

所以,我把他分解成这几个部分:

  1. 根据标签不同修改窗体标题
  2. 导航标签
  3. 标签面板
  4. 自动缩放

组件设计

分析了其中的功能,那么就要想想怎么来实现。

从功能来看,这个窗口实际上是由多个子面板切换来实现的,最多他加了点自动缩放。所以从本质来说,还是一个标签切换的窗口。

我最早想到的就是大名鼎鼎却又丑得无以复加的TabControl。

按照标签切换这个思想,TabControl完全可以胜任这次的山寨需求。但是TabControl这么丑,必须要给它整整容才行。想不到我竟然有整容的才华。

下手吧,年轻人!

因为要改动的地方会很多,所以还是完全自己来绘制标签好了。为了完全自定义TabControl,同时方便循环利用,从TabControl派生一个我们自己的标签控件TabControlEx。

  1. public class TabControlEx : System.Windows.Forms.TabControl

这就是我们的TabControlEx,看起来和TabControl没什么两样(那是当然的)。

为了让他看起来不太一样,在构造函数里加上下面的代码。

  1. base.SetStyle(
  2. ControlStyles.UserPaint |                      // 控件将自行绘制,而不是通过操作系统来绘制
  3. ControlStyles.OptimizedDoubleBuffer |          // 该控件首先在缓冲区中绘制,而不是直接绘制到屏幕上,这样可以减少闪烁
  4. ControlStyles.AllPaintingInWmPaint |           // 控件将忽略 WM_ERASEBKGND 窗口消息以减少闪烁
  5. ControlStyles.ResizeRedraw |                   // 在调整控件大小时重绘控件
  6. ControlStyles.SupportsTransparentBackColor,    // 控件接受 alpha 组件小于 255 的 BackColor 以模拟透明
  7. true);                                         // 设置以上值为 true
  8. base.UpdateStyles();

这段代码的意思就像注释里说的,注意ControlStyles这个枚举是可以按位组合的,所以上面要用「或(|)」来进行连接,这样系统就会完全忽视TabControl这个基类的界面显示,而使用我们自己的方式来呈现UI。

现在TabControlEx看起来是这样的。

啥米?!!OMG!东西哪去了??

嗯,当我第一次玩UserPaint的时候,也被吓了一跳。其实这就是上面我们设置的那句ControlStyles.UserPaint,于是系统就不帮我们画任何东西了。

所以从现在开始,一切都要靠自己了。下面所有的绘制都在OnPaint()方法中绘制。

为了先让我们找到方向,在OnPaint()方法中,我们先把Tab的位置找到,为此我们给每个Tab的边框都画出来。

  1. protected override void OnPaint(PaintEventArgs e)
  2. {
  3. for (int i = 0; i < this.TabCount; i++)
  4. {
  5. e.Graphics.DrawRectangle(Pens.Red, this.GetTabRect(i));
  6. }
  7. }

TabControl.GetTabRect(int)的功能是获得指定index的标签的矩形位置。画完后,我们的TabControlEx看起来不那么迷糊了。

可是,标签的大小还是不对,我们要的不是普通的那种长条,而是闷骚的苹果的瘦高型,要像这样。

嗯,好吧,我们回到构造函数,用下面的语句来设置大小。

  1. this.SizeMode = TabSizeMode.Fixed;  // 大小模式为固定
  2. this.ItemSize = new Size(44, 55);   // 设定每个标签的尺寸

上面设置44x55其实只是因为苹果原版刚好是这么大,先这么着,后面如果不合适了,回头再来改。现在标签是这样的了。

Apple标签的选中状态是带阴影的,看起来很酷,可是如果我用GDI+来画的话,什么渐变什么变换,烦都烦死了。怎么办呢?

请记住,我们正在山寨。所谓山寨的精神,就是不问方法、不择手段,只要最后「看起来一样」就行了。所以,我决定用上抠图大法,把apple的背景图抠出来。

把这个背景保存为TabBackground.bmp文件,然后添加到项目中,把它做成「嵌入的资源」,就像这样。

然后我们用一个变量来保存背景图。因为这张图随时会用到,所以还是做成全局变量(类级别),在构造函数里读取图片。

  1. Image backImage;
  2. public TabControlEx()
  3. {
  4. // (略)
  5. backImage = new Bitmap(this.GetType(), "TabButtonBackground.bmp");   // 从资源文件(嵌入到程序集)里读取图片
  6. }

现在有了图标,加上去看看吧。在OnPaint()里这样写。

  1. if (this.SelectedIndex == i)
  2. {
  3. e.Graphics.DrawImage(backImage, this.GetTabRect(i));
  4. }

只有被选中的标签才会出现这种背景。于是,标签变成这样了。

绘制文字

这会看着还挺单调的,所以我们来加点料。下面来画文字。说起文字,我想你应该注意到了,Safari的标签文字,都是带有阴影的(准确的说是高光)。

所以,在绘制文字时,先用高光色绘制第一遍,再用普通文字色(黑)绘制第二遍。

  1. protected override void OnPaint(PaintEventArgs e)
  2. {
  3. for (int i = 0; i < this.TabCount; i++)
  4. {
  5. // (略)
  6. // Calculate text position
  7. Rectangle bounds = this.GetTabRect(i);
  8. PointF textPoint = new PointF();
  9. SizeF textSize = TextRenderer.MeasureText(this.TabPages[i].Text, this.Font);
  10. // 注意要加上每个标签的左偏移量X
  11. textPoint.X
  12. = bounds.X + (bounds.Width - textSize.Width) / 2;
  13. textPoint.Y
  14. = bounds.Bottom - textSize.Height - this.Padding.Y;
  15. // Draw highlights
  16. e.Graphics.DrawString(
  17. this.TabPages[i].Text,
  18. this.Font,
  19. SystemBrushes.ControlLightLight,    // 高光颜色
  20. textPoint.X,
  21. textPoint.Y);
  22. // 绘制正常文字
  23. textPoint.Y--;
  24. e.Graphics.DrawString(
  25. this.TabPages[i].Text,
  26. this.Font,
  27. SystemBrushes.ControlText,    // 正常颜色
  28. textPoint.X,
  29. textPoint.Y);
  30. }
  31. }

缤纷色彩的源泉:图标

文字也有了,那么接下来就轮到图标了。TabControl是用ImageList控件来存储自己使用的图标的,那么添加一个ImageList,然后加入图标。注意这里都要32x32的图标,所以应该设置ImageList.ImageSize为32x32。

  1. // 绘制图标
  2. if (this.ImageList != null)
  3. {
  4. int index = this.TabPages[i].ImageIndex;
  5. string key = this.TabPages[i].ImageKey;
  6. Image icon = new Bitmap(1, 1);
  7. if (index > -1)
  8. {
  9. icon = this.ImageList.Images[index];
  10. }
  11. if (!string.IsNullOrEmpty(key))
  12. {
  13. icon = this.ImageList.Images[key];
  14. }
  15. e.Graphics.DrawImage(
  16. icon,
  17. bounds.X + (bounds.Width - icon.Width) / 2,
  18. bounds.Top + this.Padding.Y);
  19. }

嗯,现在我们的标签看起来像那么回事了,接下来就该难看的红线条退休了。再完善一下,我们的标签就OK了。

  

同步滚动演示。上面是山寨,下面是正品,正品打开了文字抗锯齿,我们也可以,在OnPaint()事件开始加入这样的代码。

  1. e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;

到此,标签导航部分已经完成,剩下的,就是窗体的自动缩放功能了。

作者:野比 (conmajia@gmail.com

时间:May, 2012

#1-2

嗯,还是我。

现在继续昨天的山寨。昨天我们分析得到了4条需要山寨的部分,如下。

  1. 根据标签不同修改窗体标题
  2. 导航标签
  3. 标签面板
  4. 自动缩放

通过昨天的努力,我们已经搞定了第2、3条,所以,今天的任务,就只剩下两条

  1. 根据标签不同修改窗体标题
  2. 导航标签
  3. 标签面板
  4. 自动缩放

 

修改窗体标题

我们参考下图,

我们制作的TabControlEx是作为它所在窗体的子控件存在的,为了获得包含TabControlEx的窗体(的引用),可以调用TabControlEx的FindForm()方法(从Control继承)。FindForm()可以获取容纳该控件的顶层窗体,在我们的例子里,就是我们的山寨Safari窗体。

为了在TabControlEx刚刚加入父控件的时候(也就是窗体初始化的时候)就能够顺利「劫持」到窗体的引用,并修改它的标题(否则显示Tab0的时候会发现窗体的标题还未改变),我们重写一下TabControlEx的ParentChanged事件。

  1. // 对父窗体的引用
  2. Form oldman;
  3. protected override void OnParentChanged(EventArgs e)
  4. {
  5. // 如果没有劫持到,则搜索
  6. if (oldman == null)
  7. oldman = this.FindForm();
  8. oldman.Text = this.TabPages[0].Text;
  9. }

这样,我们就可以在启动时就修改父窗体标题了。我们最终的目的是每次切换标签时都改变父窗体标题,现在我们拿到了窗体的引用,只需要重写TabControlEx的Selected事件。

  1. protected override void OnSelected(TabControlEventArgs e)
  2. {
  3. parent.Text = e.TabPage.Text;
  4. }

下面是完成之后的效果

自动调整窗体大小

完成了杂项工作,现在要进入今天的重点:自动调整大小。在开始之前,先来回顾一下这个闷骚的功能。

下面来好好分析一下到底发生了什么事。

注意,大家发现右下角那个问号没有?根据观察,那个问号始终是保持在窗体右下角的,这就好办了,直接Anchor到Right和Bottom就行。因此下面的分析中直接无视它了。

从本质上来看,因为切换的标签内容高度不同,所以窗体高度也发生了改变。但不管怎么变,窗体的底部到最下面一个控件的距离Δ没有变化,参考分析图。

所以,动画就是在H1-H2这段距离内发生的。另外,值得注意的是,Safari是在窗体动画完成,调整大小到位以后,才显示新标签的控件,这样做可以显得很有动感,而且留下了足够的时间加载控件。所以,动画应该在标签的Selecting事件里解决,而显示控件留到Selected事件。

下面来分析大小调整的算法

山寨算法:从不追求精确还原

通过慢镜头分析,可以看到在相同时间差内窗体大小的运动距离是不同的,这说明窗体大小不是匀速改变的。

为了不让算法影响我们的设计进度,将算法写在单独的方法里(最正规的应该是写成委托,直接传递方法,但你认为一个山寨货有必要吗)。

  1. private double getHeight(double time)
  2. {
  3. // (略)
  4. }

既然这样,那么算法的问题我们稍后再来讨论,现在研究怎样让窗体动起来。

由于动画过程较长,将近1秒,那么我们实现的时候应当尽量以不影响主线程为前提。除了动不动就多线程这种有点大炮打蚊子太2的方法外,我们还可以用系统自带的Timer。在每个Timer.Tick事件里挪一步,合起来就成了动画。

  1. // Δ常量
  2. int FORM_DELTA = 20;
  3. // 动画用Timer
  4. Timer timer;
  5. // 经历时间计数器
  6. int elapsed = 0;
  7. // 构造函数
  8. public TabControlEx()
  9. {
  10. // (略)
  11. // 初始化Timer
  12. timer = new Timer();
  13. timer.Interval = 100;
  14. timer.Enabled = false;
  15. timer.Tick += new EventHandler(timer_Tick);
  16. }
  17. // Timer tickle
  18. void timer_Tick(object sender, EventArgs e)
  19. {
  20. if (parent == null)
  21. return;
  22. elapsed++;
  23. parent.Height = getHeight(elapsed, FORM_DELTA);
  24. }

现在我们可以填写刚才分析的Selecting和Selected事件了。

http://blog.csdn.net/conmajia/article/details/7596718

算法尝试

目前流行的加减速函数有很多,最简单的从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

最新文章

  1. php用redis保存session
  2. mapReduce编程之auto complete
  3. SQL Server 2014 SP2发布下载:数十项更新修复
  4. 常用的CentOS 7系统yum源集合
  5. Redhat Linux /etc/profile 与 /etc/bashrc 的区别
  6. 使用PL/SQL编写存储过程访问数据库
  7. PHP filesystem attack vectors
  8. MyBatis XML 映射配置文件
  9. python学习笔记-day4笔记 常用内置函数与装饰器
  10. 2016阿里巴巴校招offer面经
  11. C++ 牛人博客(不断更新中...)
  12. Android吧数据保存成xml文件
  13. The &#39;_imaging&#39; module for the PIL could not be imported: DLL load failed: The specified module could not be found
  14. (简单) HDU 5154 Harry and Magical Computer,图论。
  15. 记一次 net 使用 data.oracleclient 使用错误
  16. 微服务框架——SpringCloud
  17. turtle库的学习笔记
  18. OC学习2——C语言特性之函数
  19. 还需要注册的是我们还有一个是“交差集” cross join, 这种Join没有办法用文式图表示,因为其就是把表A和表B的数据进行一个N*M的组合,即笛卡尔积。表达式如下:
  20. PS设计漂亮网站主页图片的实例教程

热门文章

  1. LeetCode--015--三数之和(python)
  2. POST接口测试的请求方式
  3. 华为云服务器centos7.3 安装jdk
  4. React Native 中 跨页面间通信解决方案之 react-native-event-bus
  5. ASP.NET上传一个文件夹
  6. luogu 4147 玉蟾宫 悬线DP
  7. SQL 查询表字段长度, 名称, 类型, 存储过程创建和修改时间
  8. luogu P1428 小鱼比可爱 x
  9. 使用visual studio配置和运行《opengl圣经》的第一个案例
  10. Java Interger类,两对整数明明完全一样,为何一个输出true,一个输出false