一個仿windows泡泡屏保的實現
有天看到有人在百度知道上問windows 泡泡屏保該怎么用C#做,一時有趣,就做了一個出來,對於其中幾個要點總結如下:
一,屏保程序的制作要求
屏保程序的擴展名是.scr, 但其實還是一個exe文件,只要把編譯好的exe文件擴展名改為.scr,就變成了一個屏保了。
但做為屏保程序,也對之有一定的要求如下:
1.應該是一個全屏的、無邊框的程序。
2.退出機制應該符合屏保的操作習慣,如動鼠標就退等。(我在這個例子里是用esc做退出。)
3.支持以下命令行參數:
/c , 顯示選項對話框。
/p, 顯示預覽。
/s, 或不加參數:正常運行。
提醒注意一點: 當程序擴展名修改之后,.config配置文件也需要同時改名。比如原來叫popo.exe, 配置文件就是popo.exe.config, 當你把popo.exe改為popo.scr時,也要把配置文件改為popo.scr.config。
二,程序運行原理
接下來,就是要做一個全屏的泡泡程序了。原理如下:
1.程序啟動時,先把當前的屏幕位圖保存下來備用。
2.設計泡泡對象,記錄自己的位置、大小、速度、顏色等。在設計時,我認為泡泡怎么動,怎么畫都是泡泡自己該知道的,所以,把運行計算方法與繪制方法都放在泡泡類里了。另外,在計算時,泡泡需要參考環境因素來決定自己的運動狀態,因此還帶進去了一些環境參數。我這里就把屏幕大小和整個泡泡集合做為兩個參數放進去了,如果需要的環境變量太多,可以做個環境類,把這些都包到一個環境對象里傳遞,或是做為全局變量來用。
3.做一個無框無內容、最大化、自繪制的form。
4.設置一個定時器。在觸發時,逐一更新泡泡的運動狀態。然后通知form進行重繪。
5.在form重繪時,先把保存下來的屏幕位圖畫到屏幕上,再調用泡泡自己的繪制方法,用gdi+作圖。
三、gdi+繪圖
繪圖部分很簡單,我就只畫了個圓做泡泡,並把圓里面用線性漸變畫刷刷了個從有顏色到完全透明色的效果,看起來也挺漂亮的。請看截屏。
如果用路徑漸變畫刷繪圖,還可以做到園心透明的效果。但我沒有繼續改進這塊兒。
關於透明色,可以用Color.FromArgb()方法來得到,第一個參數就是不透明度,只要設為0就是完全透明了,255為完全不透明。
四,兩種計時器的不同
為了簡化時間處理,我決定采用定時器的方法來做主循環。 一開始,我使用了一個system.form.timer, 也就是從工具欄里直接拖出來的那個timer,結果發現無論我怎么把interval搞多小,每秒都只會產生大約18次多一點的tick事件,屏幕更新率上也就最多18fps. 后來我想起來之前有看到過說這個定時器最大精度只有55ms,設再短都是55ms.
於是改用system.timer.timer做定時器。這個定時器不能從工具框里拉出來,只能在form里代碼定義,在form_load里設置屬性、委托事件,並啟動。測試后發現它的最小精度似乎是15ms,不過這樣也能達到每秒Elapsed上60多次,遠超過人眼需要的24fps刷新率了。
五,winform的重繪效率
1.Form的OnPaint()調用機制
我是在定時器Elapsed時,對所有泡泡進行一次狀態計算,然后調用form.Invalidate(),要求重繪整屏。進行測試后發現,60次計算是真實發生了,但OnPaint只重繪了40多次,也就是40fps,其中有20次Invalidate並沒有產生重繪消息。這應該是windows對wm_paint消息的重復出現時的一種處理方式——上次paint沒搞完,這次paint就又來了時,系統就放棄這次的paint消息了。
2.double buffered比位圖復制的效率高
在用VC做繪圖程序時,如果你直接在Paint事件里給你的Graphics句柄上繪圖,這個繪圖的過程就會被直接顯示到屏幕上,一是造成閃耀,二是由於要向硬件發送繪制信息,繪圖的效率也會變低(低得多!),所以習慣上會創建一個與所給的Graphics設備兼容的“內部”Bitmap對象,在這個內部Bitmap上進行繪制,完成之后再整個復制到Graphics句柄上去。由於是全覆蓋,所以對原Graphics的句柄都不需要Clear()。
因此,在用C#的winform繪圖時,我也習慣地用了這個方法。在最后優化幀率時卻發現,這個方法並不是最快的!
在form對象里有個屬性做doublebuffered,就是是否采用雙顯示緩沖區。如果設為True,.net就會給form維護兩個顯示緩沖區,當請你響應wm_Paint繪圖時,給你一個后台的緩沖請你畫,畫完了,把這個畫好的屏幕切換到前台來顯示,把原來的前台切到后台備用於下一次paint。
有了這個機制,其實就不用自己來創建那個內部bitmap了,只要啟用雙緩沖方式,你就可以直管向onPaint方法里送來的那個Graphics上繪制就好,繪制的過程不會被實時顯示到屏幕上。當你結束了OnPaint時,.net會把你畫好的這個屏幕“翻”到前面去。也不會有閃爍。
實際的測試發現,使用單緩沖 + 自己維護內部bitmap繪圖與復制的辦法,要比啟用雙緩沖 + 直接在Graphics上繪圖的效率要低上差不多30%。這應該是因為自己的內部bitmap最后要Draw到Graphics的過程是個位圖復制過程,整屏內容數據量還是很大的。而雙緩沖時,.net只是交換一個指針值就可以了。
六,碰撞算法
泡泡的運動狀態參數,我用了Vx, Vy兩個值來表示在水平方向和豎直方向上的運行速度,速度分為正負。這個設計在一些情況下簡化了運動狀態的計算處理。
1.撞邊
撞邊的檢測很簡單,就是看邊緣座標是否超過屏幕大小,或是小於0。撞上下邊時,修改Vy的值為-Vy; 撞左右邊時,Vx = -Vx;
2.泡泡互撞
這個很頭大,但終於自行搞定了。
我在一個泡泡計算運動時,逐個計算與其他泡泡兩兩碰撞的情況,原理就是計算兩個園心的距離(d =( (x1 -x2) ^ 2 + (y1 - y2)^2) ^ 0.5)是否大於兩個泡泡半徑之和,如果不大於,d <= r1 + r2, 那么說明與這個泡泡發生碰撞。
碰撞之后,如果發現兩個泡泡已經相交了,d < r1 + r2, 第一件事情就是先把兩個泡泡移開到誰不挨誰的位置上,為了看起來正確一些,我決定按兩圓心的延長線,把兩個泡泡各向后移動重疊部分( r1 + r2 - d) 的一半距離。這種情況在兩個時候很容易有,一是剛開始,泡泡的位置是隨機的,有大量擠一起的,二是在運動中,一個泡泡如果速度太快,在一次計算周期里它移動的位置很可能已經讓它超過另一泡泡的邊界了。我現在想到只是移回到邊界處也不是很對,應該彈回對應的距離才符合實際。
后來發現,各自回退一半距離時,有時會有某個圓的回退方向上會碰到其他圓,其他圓又會把它擠回原位置,這個循環引起了一些泡泡間的奇怪行為,還造成一些誤差積累的問題。最后,我把規則改為只移動其中一個泡泡,這樣就消除了這種問題。
然后,要計算兩個泡泡的碰撞對兩方產生的影響。
我先規定這些泡泡的重量都是相同的,以簡化問題。然后,我推測自然界里的碰撞過程應該是一個動量(速度矢量 * 質量)交換的過程,交換的法則應該符合平行四邊形法則,結合我Vx,Vy速度分量的設計,我分析了在兩個正交方向上向對方輸出動量的計算方法,比如下圖是B圓的Vy動量向A圓Vy和Vx輸出的分析:
如圖:Vy向A圓的輸出動量大小,應該是Vy在法線上的投影ObP, 這個投影是個矢量,正指向A圓圓心,也就是法線的方向。這個矢量被分解到兩個正交方向上,就是Py和Px。
同樣,Vx也要如此分解為兩個方向上的分量(Px2, Py2)。
Px+Px2, Py + Py2, 需要都從Vx和Vy里減出去,加到圓A的Vx和Vy上。
以此類推,圓A的動量也要如此計算出要分給圓B的部分,並從自己的兩個速度與動量中減出,加到圓B的兩個動量里去。
最后的效果,看起來和現實情況比較符合。為了測試,我做了一個下面的布置,把5個球放一排,然后讓兩個球從左邊撞過去,看看碰撞的效果如何,結果是右邊也彈出了兩個球,與現實相當符合:
3.多個泡泡同時撞到一起
兩兩相撞計算后,看起來也差不多了。沒做更多處理。
4.效率問題
由於要兩兩計算,每一輪的計算量就是n*n次。一開始比較擔心,但設置了30個泡,運行起來之后,發現大量的cpu時間是在繪圖部分的,900次的碰撞檢查幾乎不占什么CPU比例,於是就算了。
5.難以徹底處理的問題
程序運行中,發現有一些誤差被積累,或是在很多球球時,它們之間的相互作用就變得很復雜,行為有時有點怪異。仔細分析后,歸結為兩大原因:
一、 現實中,物體的運行狀態是依時間連續發生的,實時的。而在計算機里,只能在離散的時間點上計算狀態,這就會錯過去很多事件,不得不加了一些糾正。這種事件錯失與糾正行為會讓物體表現得不太自然。
二、 現實中,物體間的相互作用是並行發生的,但在計算里,只能兩兩考慮,逐個處理,這也造成計算結果與現實環境不太一致。
這兩大原因都是目前不太好完全解決的,最多只是精度修正,不可能完全消除。
七,如何實現不妨礙工作的屏幕泡泡?
最后,拋出一個問題:在這個程序里,背景部分是抓屏后繪出來的假屏幕。有沒有可能讓泡泡們在真正的屏幕上飄動,而且不影響當前的工作?
我想到把每個泡泡都作為一個真正的窗體來處理,以Top most方式運行,並設置window的剪裁區域為自己的形狀,但是這樣的話,這些窗口泡泡就會載到在它們上面發生的windows消息,似乎會影響下面的程序工作,比如點擊到它們時,就讓下面的窗口失去了焦點。
用系統鈎子如何呢?
下載源碼在這里: http://files.cnblogs.com/haoxiaobo/PopScreenSaver.rar