C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

一、前言

“GDI+”与“鼠标交互”,乍一听好像不可能,也无从下手,但是实现原理比想象中要简单很多。基于“GDI+”的“交互”,应用场景也很多,比如:流程图、数据图表、思维导图等等。.

本篇文章就通过多个示例来讲解一下 GDI+ 与鼠标交互的原理,以及如何去实现。每一个示例实现后,都会对示例进行优化,主要是解决一些在实际应用中比较常见的问题,比如:闪烁、资源占用高等等。

而在最后,会基于实际的应用场景——在背景图上绘制图形并进行鼠标交互——编写一个示例。接着会使用实际应用场景内必备的、也是核心的“局部刷新”技术对示例进行优化。

相信看完的你,一定会有所收获!

二、基本原理

GDI+ 与鼠标交互的原理非常简单:判断鼠标是否在 GID+ 图形上,然后根据鼠标的不同状态,执行不同的效果。

估计很多人看到这句话就直接恍然大悟了。确实,原理就是这么简单。

下面,我们首先来简单实现一个简单的交互效果:可以用鼠标拖动的矩形。

三、可以用鼠标拖动的矩形

1、设计器界面

程序界面如下:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

我们的绘制及交互区域就是 panel1,所以为 panel1 绑定以下几个鼠标相关的事件:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

2、代码实现

添加全局变量

为了与鼠标交互,我们需要以下两个全局变量:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

其中,rectShape 是我们所绘制矩形的位置和尺寸;pointLast 是上次鼠标的位置。

绘制矩形方法

绘制矩形很简单,直接在背景上画一个矩形即可。GDI+ 中绘制矩形的方法如下:(下图来自MSDN)C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

 

不过为了防止残留,我们在画矩形前需要先清空一下背景。(下图来自MSDN)

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

原理示意如下:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

对应的代码如下:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

鼠标交互操作实现

当鼠标在 panel1 中点击时,我们要判断鼠标点击的位置是否处于我们绘制的矩形内。

如果是,则记录当前鼠标的位置;如果不是,则清空记录的鼠标位置;

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

当按着鼠标按键并拖动鼠标时,我们要判断是否有记录过之前鼠标的位置。

如果满足条件,就证明是现在鼠标是按着所绘制的矩形进行拖动了。

所以,我们要计算一下这次鼠标的位移量,并计算矩形的新位置,然后重新在新位置绘制矩形。这一步,就是交互效果的核心。在拖动的过程中,我们会根据鼠标的位置不断的计算并重新绘制新的矩形。在视觉效果上,就是我们拖动着矩形在动。

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

因为不断在重新绘制矩形,所以这里是最能体现 GDI+ 性能的地方,不同的写法,性能相差很大,这也是后续所要优化的地方。

当松开鼠标按键时,将记录的鼠标位置清空。

上面的 MouseMove 事件会因为不满足条件,而结束重绘。

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

3、效果演示

编译运行程序,我们会发现已经可以使用鼠标拖动矩形了。

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

我们会发现,拖动矩形时会出现闪烁的情况。而且窗口越大,闪烁越明显。这是因为我们是先清空背景、然后再绘制矩形,这个清空再绘制的过程,就会闪烁

下面,我们就来优化一下,解决闪烁的问题。

4、闪烁问题优化

解决“闪烁”,我们最先想到的就是开启“双缓冲”,不过在这里,开启“双缓冲”效果不大,因为闪烁的原因在于我们自己不断的清空再绘制。所以,我们优化的核心就是不再清空背景。开启双缓冲的方式如下:


C# GDI+ 之鼠标交互:原理、一步步深入、性能优化C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

我们会发现,在两次拖动变化之间,可以看作是先将原矩形填充为背景色,再在新位置绘制一个新的矩形

示意图如下:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

我们按照示意图编写代码如下:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化
5、优化后效果演示

编译运行程序,我们再次拖动矩形,会发现不再有闪烁的情况。

四、可以用鼠标拖动的圆形

在实现了可以被鼠标拖动的矩形后,我们再来实现可以被鼠标拖动的圆形。因为圆形和矩形是不一样的:圆形既有可见区域,也有不可见区域。如图所示:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

我们本节就看一下在实现上都有哪些不同。

1、设计器界面

设计器界面同上,增加一个按钮用来添加圆形。


C# GDI+ 之鼠标交互:原理、一步步深入、性能优化C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

2、代码实现

 

添加全局变量

因为%20GDI+%20中绘制圆形的参数和矩形是一样的,都是一个%20Rectangle%20,所以我们可以复用之前的全局变量,不用进行修改。(下图来自MSDN)

 

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

绘制圆形方法

这里,我们直接采用上节优化后的方法去实现,即:将旧矩形填充背景色,再在新位置绘制新圆形

原理示意见上节,具体代码如下:

鼠标交互操作实现

这里与上节绘制矩形的原理一样,只需要在 MouseMove 事件中将绘制矩形的方法改为绘制圆形的方法即可。代码修改如下:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

3、效果演示

编译运行,可以发现我们可以正常使用鼠标拖动绘制的圆形。

【注:我们会发现,同样是优化后的方法,在绘制“矩形”时不会闪烁,但是在绘制“圆形”时会闪烁,这是因为绘制圆形会更加消耗性能,关于如何解决闪烁的问题,参见下面:“六、使用“局部刷新”技术对【示例3】进行优化”。因为本节内容的重点不在于此,所以未在此节解决闪烁问题。】

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

在拖动的时候,我们会发现一个问题:就是我们的鼠标即不在圆形上,而是在圆的四个边角处,也能正常拖动圆形。如下:


C# GDI+ 之鼠标交互:原理、一步步深入、性能优化C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

这是因为圆形和矩形不一样,圆形是有可见区域(即显示的圆形)和不可见区域(即非圆形区域),虽然不可见,但仍然是存在的,所以仍然会正常捕获到鼠标的点击。

这里,我们在绘制圆形时将真正的范围填充上颜色,效果会很明显。

下面,我们就针对这个鼠标捕获区域的问题进行优化。

4、鼠标捕获区域优化

首先,最关键的地方就是在鼠标点击的时候,也就是 MouseDown 事件。


C# GDI+ 之鼠标交互:原理、一步步深入、性能优化C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

我们判断鼠标是否落在圆形内,不能再通过当前的方法。因为这个只能判断矩形。我们要判断鼠标是否在圆形内,通过通过%20Region%20去判断。(下图来自MSDN)

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

首先,我们添加一条和圆形同尺寸的圆形路径,然后基于此路径创建%20Region%20,接着判断鼠标是否在此%20Region%20内。具体的代码如下:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

5、优化后效果演示

我们再次编译运行程序,会发现只能我们的鼠标点击在圆形内,才能正常拖动圆形。

为了更明显的演示,我们为非圆形区域填充上颜色,再次操作如下:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

五、可以用鼠标拖动的圆形,但背景图不受影响

上面的示例看下来,似乎已经没有问题了。但是在实际应用过程中,却有一个不可忽视的元素:背景图(此处的背景图是广义上的背景图,可指图片、其它GDI+%20图形等等,但原理都是一样的)。

因为前面的示例背景都是纯色,所以我们看不出来,现在我们为%20panel1%20加上背景图,再次运行程序,我们看下效果:

可以看到,拖动过的地方背景直接被擦了。这还是优化后的代码,如果是最开始的“先清除背景再绘制图形”,则在第一次拖动的时候,整个背景图就都没了。

本节,我们就来看一下:如何在用鼠标拖动圆形时,背景图还正常显示不受影响。

1、设计器界面

设计器界面同上,不作变化。

2、代码实现

生成背景图

首先,我们写一个方法,生成一张背景图,当然也可以使用现成的图片。然后将这张背景图保存为全局变量,以供后续使用。

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

修改绘制圆形方法

既然背景图受到影响,我们想到的最直接方法便是在每次绘制圆形时,都重新将背景图绘制一遍。不过将整个背景图完整的重绘一遍会太过消耗资源,所以我们可以采取之前的优化思路,就是填充原矩形、绘制背后矩形,不过这里的填充不再是背景色,而是背景图

首先,我们需要计算一下原矩形在背景图中对应的位置和尺寸,然后将这块背景绘制上去,接着再绘制新的矩形。

我们使用这个重载方法进行背景图的绘制:(下图来自MSDN)

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

具体的代码如下:

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

3、效果演示

编译运行,可以发现背景确实不受影响了。

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

不过上节中出现的在绘制圆形闪烁的问题也更严重了。

那么下面,我们就从根本上来解决一下闪烁的问题。

六、使用“局部刷新”技术对【示例3】进行优化

在前面的示例中,使用同样的优化方式,在绘制矩形时不闪烁,而在绘制圆形时却会闪烁,虽说是因为绘制圆形更耗性能,但也说明了前面的优化还远远不足。

而问题的根源,就在于刷新的面积太大了。所以我们的优化方向,就在于怎么将这个“刷新面积”减小,也就是所谓的“局部刷新”技术。

下面,我们就以【示例3】为例来演示下如何使用“局部刷新”技术。

1、剪辑区域

与“局部刷新”所对应的,就是“剪辑区域”,顾名思义,就是专门剪辑出来用来重绘的区域。

在计算“剪辑区域”时,为了方便计算和演示,我们直接将拖动时刚好包含“原矩形”和“新矩形”的矩形区域当成“剪辑区域”。

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

2、修改绘制圆形方法

在绘制圆形时,我们首先要计算剪辑区域,然后获取剪辑区域所对应的背景图,接着设置剪辑区域,并绘制新矩形。

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

3、效果演示

编译运行程序,可以看到在拖动圆形时,不会再出现闪烁的问题,同时各种资源的占用也很低。

C# GDI+ 之鼠标交互:原理、一步步深入、性能优化

七、局部刷新技术在实际场景中的应用

在实际应用场景中,并不是简单的一个背景一个图形。在需要用到 GDI+ 交互的场景,往往都会在同一个区域内有好多个不同的 GDI+ 图形。

这种场景的基本绘制流程一般如下:

1、将诸多 GDI+ 图形保存到一个集合内,一般是以类的形式,类里面包含图形类型、绘制此图形所需要的参数、附加参数等。

2、在绘制时,将背景图(如果有的话)和图形集合绘制到一个临时Bitmap 上,然后将此临时Bitmap 绘制到窗口上。

3、释放临时Bitmap等资源。

在这种流程下,如果按照“局部刷新”的方式,就不免会出现闪烁、CPU内存占用高等问题。

所以,这种时候就必然要用到“局部刷新”技术。我们不用再将全部的图形集合和背景图绘制到一张临时Bitmap上,而是先计算剪辑区域,然后判断图形集合内有哪些图形在剪辑区域内,之后仅重新绘制这些图形即可。

八、源代码下载

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

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

九、总结

在这个新技术层出不穷的时代,GDI+ 已经被冠上诸如“上个时代的技术、落后的技术、性能很差的技术”等等名词。

但是 GDI+ 的效率并不低下,只是很少有能够发挥出 GDI+ 的正常性能,更别说触摸到 GDI+ 的极限了。当然,本人的水平也有限,只能说勉强够用而已。

新技术,给了我们更多的选择,不过技术是没有先进落后之分的,只有合适与不合适之别

所以请对自己掌握的技术多一些信心,多一些耐心。