本文轉載自: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 = 0; 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(44, 55); // 設定每個標簽的尺寸
上面設置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 = 0; 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) / 2; 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(1, 1); if (index > -1) { icon = this.ImageList.Images[index]; } if (!string.IsNullOrEmpty(key)) { icon = this.ImageList.Images[key]; } e.Graphics.DrawImage( icon, bounds.X + (bounds.Width - icon.Width) / 2, 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[0].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 = 20; // 動畫用Timer Timer timer; // 經歷時間計數器 int elapsed = 0; // 構造函數 public TabControlEx() { // (略) // 初始化Timer timer = new Timer(); timer.Interval = 100; 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
(未完待續)