(原創)[C#] GDI+ 之鼠標交互:原理、示例、一步步深入、性能優化

一、前言

「GDI+」與「鼠標交互」,乍一聽好像不可能,也無從下手,但是實現原理比想像中要簡單很多。
基於「GDI+」的「交互」,應用場景也很多,比如:流程圖、數據圖表、思維導圖等等。

本篇文章就通過多個示例來講解一下 GDI+ 與鼠標交互的原理,以及如何去實現。
每一個示例實現後,都會對示例進行優化,主要是解決一些在實際應用中比較常見的問題,比如:閃爍、資源佔用高等等。
而在最後,會基於實際的應用場景——在背景圖上繪製圖形並進行鼠標交互——編寫一個示例。
接着會使用實際應用場景內必備的、也是核心的「局部刷新」技術對示例進行優化。

相信看完的你,一定會有所收穫!

本文地址://www.cnblogs.com/lesliexin/p/16554752.html


二、基本原理

GDI+ 與鼠標交互的原理非常簡單:判斷鼠標是否在 GID+ 圖形上,然後根據鼠標的不同狀態,執行不同的效果。

估計很多人看到這句話就直接恍然大悟了。確實,原理就是這麼簡單。

下面,我們首先來簡單實現一個簡單的交互效果:可以用鼠標拖動的矩形。


三、示例1:可以用鼠標拖動的矩形

(一)設計器界面

程序界面如下:

image

我們的繪製及交互區域就是 panel1,所以為 panel1 綁定以下幾個鼠標相關的事件:

image

(二)代碼實現

1,添加全局變量

為了與鼠標交互,我們需要以下兩個全局變量:

image

其中,rectShape 是我們所繪製矩形的位置和尺寸;pointLast 是上次鼠標的位置。

2,繪製矩形方法

繪製矩形很簡單,直接在背景上畫一個矩形即可。
GDI+ 中繪製矩形的方法如下:
(下圖來自MSDN)
image

不過為了防止殘留,我們在畫矩形前需要先清空一下背景。
(下圖來自MSDN)
image

原理示意如下:

image

對應的代碼如下:

image

3,鼠標交互操作實現

(1)當鼠標在 panel1 中點擊時,我們要判斷鼠標點擊的位置是否處於我們繪製的矩形內。

如果是,則記錄當前鼠標的位置;
如果不是,則清空記錄的鼠標位置;

image

(2)當按着鼠標按鍵並拖動鼠標時,我們要判斷是否有記錄過之前鼠標的位置。

如果滿足條件,就證明是現在鼠標是按着所繪製的矩形進行拖動了。
所以,我們要計算一下這次鼠標的位移量,並計算矩形的新位置,然後重新在新位置繪製矩形
這一步,就是交互效果的核心。在拖動的過程中,我們會根據鼠標的位置不斷的計算並重新繪製新的矩形。在視覺效果上,就是我們拖動着矩形在動。

image

因為不斷在重新繪製矩形,所以這裡是最能體現 GDI+ 性能的地方,不同的寫法,性能相差很大,這也是後續所要優化的地方。

(3)當鬆開鼠標按鍵時,將記錄的鼠標位置清空。

上面的 MouseMove 事件會因為不滿足條件,而結束重繪。

image

(三)效果演示

編譯運行程序,我們會發現已經可以使用鼠標拖動矩形了。

image

我們會發現,拖動矩形時會出現閃爍的情況。而且窗口越大,閃爍越明顯。
這是因為我們是先清空背景、然後再繪製矩形,這個清空再繪製的過程,就會閃爍

下面,我們就來優化一下,解決閃爍的問題。

(四)「閃爍」問題優化

解決「閃爍」,我們最先想到的就是開啟「雙緩衝」,不過在這裡,開啟「雙緩衝」效果不大,因為閃爍的原因在於我們自己不斷的清空再繪製。
所以,我們優化的核心就是不再清空背景。
開啟雙緩衝的方式如下:

image

我們會發現,在兩次拖動變化之間,可以看作是先將原矩形填充為背景色,再在新位置繪製一個新的矩形

示意圖如下:

image

我們按照示意圖編寫代碼如下:

image

(五)優化後效果演示

編譯運行程序,我們再次拖動矩形,會發現不再有閃爍的情況。

image


四、示例2:可以用鼠標拖動的圓形

在實現了可以被鼠標拖動的矩形後,我們再來實現可以被鼠標拖動的圓形。
因為圓形和矩形是不一樣的:圓形既有可見區域,也有不可見區域
如圖所示:

image

我們本節就看一下在實現上都有哪些不同。

(一)設計器界面

設計器界面同上,增加一個按鈕用來添加圓形。

image

(二)代碼實現

1,添加全局變量

因為 GDI+ 中繪製圓形的參數和矩形是一樣的,都是一個 Rectangle ,所以我們可以復用之前的全局變量,不用進行修改。
(下圖來自MSDN)
image

2,繪製圓形方法

這裡,我們直接採用上節優化後的方法去實現,即:將舊矩形填充背景色,再在新位置繪製新圓形

原理示意見上節,具體代碼如下:

image

3,鼠標交互操作實現

這裡與上節繪製矩形的原理一樣,只需要在 MouseMove 事件中將繪製矩形的方法改為繪製圓形的方法即可。
代碼修改如下:

image

(三)效果演示

編譯運行,可以發現我們可以正常使用鼠標拖動繪製的圓形。
【註:我們會發現,同樣是優化後的方法,在繪製「矩形」時不會閃爍,但是在繪製「圓形」時會閃爍,這是因為繪製圓形會更加消耗性能,關於如何解決閃爍的問題,參見下面:「六、使用「局部刷新」技術對【示例3】進行優化」。因為本節內容的重點不在於此,所以未在此節解決閃爍問題。】

image

在拖動的時候,我們會發現一個問題:就是我們的鼠標即不在圓形上,而是在圓的四個邊角處,也能正常拖動圓形。
如下:

image

這是因為圓形和矩形不一樣,圓形是有可見區域(即顯示的圓形)和不可見區域(即非圓形區域),雖然不可見,但仍然是存在的,所以仍然會正常捕獲到鼠標的點擊。
這裡,我們在繪製圓形時將真正的範圍填充上顏色,效果會很明顯。

image

下面,我們就針對這個鼠標捕獲區域的問題進行優化。

(四)鼠標捕獲區域優化

首先,最關鍵的地方就是在鼠標點擊的時候,也就是 MouseDown 事件。

image

我們判斷鼠標是否落在圓形內,不能再通過當前的方法。因為這個只能判斷矩形。我們要判斷鼠標是否在圓形內,通過通過 Region 去判斷。
(下圖來自MSDN)
image

首先,我們添加一條和圓形同尺寸的圓形路徑,然後基於此路徑創建 Region ,接着判斷鼠標是否在此 Region 內。
具體的代碼如下:

image

(五)優化後效果演示

我們再次編譯運行程序,會發現只能我們的鼠標點擊在圓形內,才能正常拖動圓形。
為了更明顯的演示,我們為非圓形區域填充上顏色,再次操作如下:

image


五、示例3:可以用鼠標拖動的圓形,但背景圖不受影響

上面的示例看下來,似乎已經沒有問題了。但是在實際應用過程中,卻有一個不可忽視的元素:背景圖(此處的背景圖是廣義上的背景圖,可指圖片、其它GDI+ 圖形等等,但原理都是一樣的)。

因為前面的示例背景都是純色,所以我們看不出來,現在我們為 panel1 加上背景圖,再次運行程序,我們看下效果:

image

可以看到,拖動過的地方背景直接被擦了。這還是優化後的代碼,如果是最開始的「先清除背景再繪製圖形」,則在第一次拖動的時候,整個背景圖就都沒了。

本節,我們就來看一下:如何在用鼠標拖動圓形時,背景圖還正常顯示不受影響。

(一)設計器界面

設計器界面同上,不作變化。

(二)代碼實現

1,生成背景圖

首先,我們寫一個方法,生成一張背景圖,當然也可以使用現成的圖片。
然後將這張背景圖保存為全局變量,以供後續使用。

image

2,修改繪製圓形方法

既然背景圖受到影響,我們想到的最直接方法便是在每次繪製圓形時,都重新將背景圖繪製一遍。
不過將整個背景圖完整的重繪一遍會太過消耗資源,所以我們可以採取之前的優化思路,就是填充原矩形、繪製背後矩形,不過這裡的填充不再是背景色,而是背景圖

首先,我們需要計算一下原矩形在背景圖中對應的位置和尺寸,然後將這塊背景繪製上去,接着再繪製新的矩形。
我們使用這個重載方法進行背景圖的繪製:
(下圖來自MSDN)
image

具體的代碼如下:

image

(三)效果演示

編譯運行,可以發現背景確實不受影響了。

image

不過上節中出現的在繪製圓形閃爍的問題也更嚴重了。
那麼下面,我們就從根本上來解決一下閃爍的問題。


六、使用「局部刷新」技術對【示例3】進行優化

在前面的示例中,使用同樣的優化方式,在繪製矩形時不閃爍,而在繪製圓形時卻會閃爍,雖說是因為繪製圓形更耗性能,但也說明了前面的優化還遠遠不足。
而問題的根源,就在於刷新的面積太大了。所以我們的優化方向,就在於怎麼將這個「刷新面積」減小,也就是所謂的「局部刷新」技術。

下面,我們就以【示例3】為例來演示下如何使用「局部刷新」技術。

(一)「剪輯區域」

與「局部刷新」所對應的,就是「剪輯區域」,顧名思義,就是專門剪輯出來用來重繪的區域。

在計算「剪輯區域」時,為了方便計算和演示,我們直接將拖動時剛好包含「原矩形」和「新矩形」的矩形區域當成「剪輯區域」。

image

(二)修改繪製圓形方法

在繪製圓形時,我們首先要計算剪輯區域,然後獲取剪輯區域所對應的背景圖,接着設置剪輯區域,並繪製新矩形。

image

(三)效果演示

編譯運行程序,可以看到在拖動圓形時,不會再出現閃爍的問題,同時各種資源的佔用也很低。

image


七、「局部刷新」技術在實際場景中的應用

在實際應用場景中,並不是簡單的一個背景一個圖形。在需要用到 GDI+ 交互的場景,往往都會在同一個區域內有好多個不同的 GDI+ 圖形。

這種場景的基本繪製流程一般如下:
1,將諸多 GDI+ 圖形保存到一個集合內,一般是以類的形式,類裏面包含圖形類型、繪製此圖形所需要的參數、附加參數等。
2,在繪製時,將背景圖(如果有的話)和圖形集合繪製到一個臨時Bitmap 上,然後將此臨時Bitmap 繪製到窗口上。
3,釋放臨時Bitmap等資源。

在這種流程下,如果按照「局部刷新」的方式,就不免會出現閃爍、CPU內存佔用高等問題。

所以,這種時候就必然要用到「局部刷新」技術。我們不用再將全部的圖形集合和背景圖繪製到一張臨時Bitmap上,而是先計算剪輯區域,然後判斷圖形集合內有哪些圖形在剪輯區域內,之後僅重新繪製這些圖形即可。


八、源代碼下載

本文演示的程序源代碼如下:

//files.cnblogs.com/files/lesliexin/GdiInteractive.7z


九、總結

在這個新技術層出不窮的時代,GDI+ 已經被冠上諸如「上個時代的技術、落後的技術、性能很差的技術」等等名詞。

但是 GDI+ 的效率並不低下,只是很少有能夠發揮出 GDI+ 的正常性能,更別說觸摸到 GDI+ 的極限了。
當然,本人的水平也有限,只能說勉強夠用而已。

新技術,給了我們更多的選擇,不過技術是沒有先進落後之分的,只有合適與不合適之別

所以請對自己掌握的技術多一些信心,多一些耐心。
在此,作者與諸君共勉!

本人水平有限,文章難免有所疏漏,歡迎大家評論指正。


-【END】-