重繪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
  • 苦力:野比

 

分析,分析

山寨的靈魂在於分析,首先把剛才拍的高清果照扯過來分解了。

所以,我把他分解成這幾個部分:

 

  1. 根據標簽不同修改窗體標題
  2. 導航標簽
  3. 標簽面板
  4. 自動縮放

 

組件設計

分析了其中的功能,那么就要想想怎么來實現。

從功能來看,這個窗口實際上是由多個子面板切換來實現的,最多他加了點自動縮放。所以從本質來說,還是一個標簽切換的窗口。

我最早想到的就是大名鼎鼎卻又丑得無以復加的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條需要山寨的部分,如下。

 

  1. 根據標簽不同修改窗體標題
  2. 導航標簽
  3. 標簽面板
  4. 自動縮放

通過昨天的努力,我們已經搞定了第2、3條,所以,今天的任務,就只剩下兩條

 

 

  1. 根據標簽不同修改窗體標題
  2. 導航標簽
  3. 標簽面板
  4. 自動縮放

 

 

修改窗體標題

我們參考下圖,

我們制作的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

 

 

(未完待續)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM