(原創)[C#] GDI+ 之滑鼠互動:原理、範例、一步步深入、效能優化

2022-08-08 12:01:23

一、前言

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

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

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

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


二、基本原理

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

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

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


三、範例1:可以用滑鼠拖動的矩形

(一)設計器介面

程式介面如下:

我們的繪製及互動區域就是 panel1,所以為 panel1 繫結以下幾個滑鼠相關的事件:

(二)程式碼實現

1,新增全域性變數

為了與滑鼠互動,我們需要以下兩個全域性變數:

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

2,繪製矩形方法

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

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

原理示意如下:

對應的程式碼如下:

3,滑鼠互動操作實現

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

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

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

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

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

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

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

(三)效果演示

編譯執行程式,我們會發現已經可以使用滑鼠拖動矩形了。

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

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

(四)「閃爍」問題優化

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

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

示意圖如下:

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

(五)優化後效果演示

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


四、範例2:可以用滑鼠拖動的圓形

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

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

(一)設計器介面

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

(二)程式碼實現

1,新增全域性變數

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

2,繪製圓形方法

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

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

3,滑鼠互動操作實現

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

(三)效果演示

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

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

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

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

(四)滑鼠捕獲區域優化

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

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

首先,我們新增一條和圓形同尺寸的圓形路徑,然後基於此路徑建立 Region ,接著判斷滑鼠是否在此 Region 內。
具體的程式碼如下:

(五)優化後效果演示

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


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

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

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

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

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

(一)設計器介面

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

(二)程式碼實現

1,生成背景圖

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

2,修改繪製圓形方法

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

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

具體的程式碼如下:

(三)效果演示

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

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


六、使用「區域性重新整理」技術對【範例3】進行優化

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

下面,我們就以【範例3】為例來演示下如何使用「區域性重新整理」技術。

(一)「剪輯區域」

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

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

(二)修改繪製圓形方法

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

(三)效果演示

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


七、「區域性重新整理」技術在實際場景中的應用

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

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

在這種流程下,如果按照「區域性重新整理」的方式,就不免會出現閃爍、CPU記憶體佔用高等問題。

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


八、原始碼下載

本文演示的程式原始碼如下:

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


九、總結

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

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

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

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

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


-【END】-