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

一步一步玩控件:自定义TabControl——从山寨Safari开始的更多相关文章

  1. [转]一步一步玩控件:自定义TabControl——从山寨Safari开始

    作者:野比 (conmajia@gmail.com) 时间:May, 2012 封面图片为野比原创,请勿未经允许私自引用 #1-1 嗯,各位,又是我,生物钟颠倒的家伙. 今天我要山寨的是大名鼎鼎的Ap ...

  2. 【Android开发日记】之入门篇(十四)——Button控件+自定义Button控件

        好久不见,又是一个新的学期开始了,为什么我感觉好惆怅啊!这一周也发生了不少事情,节假日放了三天的假(好久没有这么悠闲过了),实习公司那边被组长半强制性的要求去解决一个后台登陆的问题,结果就是把 ...

  3. 一步一步学android之控件篇——ScrollView

    一个手机的屏幕大小是有限的,那么我要显示的东西显示不下怎么办?这就会使用到ScrollView来进行滚动显示,他的定义如下: 可以看到ScrollView是继承于FrameLayout的,所以Scro ...

  4. DEV控件自定义排序实现

    一般的控件或者组件都支持按照某一列进行排序.但是,这种排序是根据数据源里的数据默认按照降序或升序排序的,同时这样的排序与字段的类型有关. 假设现在字段的类型是字符串类型 ,但是,存储的数据时数字加一些 ...

  5. C# 根据BackgroundWoker异步模型和ProgressBar控件,自定义进度条控件

    前言 程序开发过程中,难免会有的业务逻辑,或者算法之类产生让人能够感知的耗时操作,例如循环中对复杂逻辑处理;获取数据库百万乃至千万级数据;http请求的时候等...... 用户在使用UI操作并不知道程 ...

  6. WPF自定义控件与样式(5)-Calendar/DatePicker日期控件自定义样式及扩展

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 日历控 ...

  7. android控件---自定义带文本的ImageButton

    由于SDK提供的ImageButton只能添加图片,不能添加文字:而Button控件添加的文字只能显示在图片内部:当我们需要添加文字在图片外部时就不能满足我们的需求了,顾只能自己写个自定义ImageB ...

  8. silverlight 控件自定义样式 实现方法

    1:在app.xaml中加入需实现的样式,如: <Application.Resources> <Style x:Key="NodeStyle" TargetTy ...

  9. C#winform窗体用户控件自定义事件

    C#许多事情都和事件有关系,大部分的事情我们可以通过C#自己的事件来完成,但如果我们自己新建了一个自定义控件,我们该如何定义自己想要的事件呢?下面我就来为大家粗略的讲解一番. 假设我们自定义了一个控件 ...

随机推荐

  1. NOIP模拟赛(by hzwer) T2 小奇的序列

    [题目背景] 小奇总是在数学课上思考奇怪的问题. [问题描述] 给定一个长度为 n 的数列,以及 m 次询问,每次给出三个数 l,r 和 P, 询问 (a[l'] + a[l'+1] + ... + ...

  2. ajax请求成功,但是进入error

    ajax请求成功,这是因为json数据没有严格按json格式返回

  3. Eclipse 中的 parameter参数,property属性,preference首选项 区别

    parameter参数 1.配置框架 web.xml <init-param> <param-name>contextConfigLocation</param-name ...

  4. android 常用框架总结(转载)

    原文:https://blog.csdn.net/weixin_44702125/article/details/89886948

  5. 如何生成各种mif文件,绝对经典!!!

    mif文件生成模板,只需要5步,很简单!!!!! 先说明如何操作,1-2-3-4-5步,后面附上模板!!! 下面以汉字去模演示过程: 1.取模软件设置:注意这里是设置的输出数据的格式!!!!!!!!! ...

  6. POJ 2778 DNA Sequence ( AC自动机、Trie图、矩阵快速幂、DP )

    题意 : 给出一些病毒串,问你由ATGC构成的长度为 n 且不包含这些病毒串的个数有多少个 分析 : 这题搞了我真特么久啊,首先你需要知道的前置技能包括 AC自动机.构建Trie图.矩阵快速幂,其中矩 ...

  7. codevs 2038 香甜的黄油x+luogu P1828 x

    题目描述 Description 农夫John发现做出全威斯康辛州最甜的黄油的方法:糖.把糖放在一片牧场上,他知道N(1<=N<=500)只奶牛会过来舔它,这样就能做出能卖好价钱的超甜黄油 ...

  8. SpringCloud 教程 (七)服务注册(consul)

    一.consul 简介 consul 具有以下性质: 服务发现:consul通过http 方式注册服务,并且服务与服务之间相互感应. 服务健康监测 key/value 存储 多数据中心 consul可 ...

  9. TestingWhiz社区版2013版下载地址

    TestingWhiz社区版 https://sourceforge.net/projects/testingwhiz-community-edition/ https://sourceforge.n ...

  10. svn 服务器操作

    mkdir /var/svn/svnrepos/aaasvnadmin create /var/svn/svnrepos/aaasvnserve -d -r /var/svn/svnrepos #启动 ...