(原创)[c#] gdi+ 之鼠标交互:原理、示例、一步步深入、性能优化_leslie_xin-编程思维

一、前言

“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】-

版权声明:本文版权归作者所有,遵循 CC 4.0 BY-SA 许可协议, 转载请注明原文链接
https://www.cnblogs.com/lesliexin/p/16554752.html

深入浅出wpf(binding篇1)_.net有点帅-编程思维

Binding在业界的使用一直是音译而来的,称为"Binding"。Binding的源是逻辑数据对象,目标则是UI层上面的控件对象。数据通过Binding送达UI层,被UI层展示出来,也就完成了数据驱动UI的过程了。 下面通过一个很简单的列子来引入我们最原始的Binding: <Window x:Class="

abp中的数据过滤器_阿升1990-编程思维

  本文首先介绍了ABP内置的软删除过滤器(ISoftDelete)和多租户过滤器(IMultiTenant),然后介绍了如何实现一个自定义过滤器,最后介绍了在软件开发过程中遇到的实际问题,同时给出了解决问题的一个未必最优的思路。 一.预定义过滤器   ABP中的数据过滤器源码在Volo.Abp.Data[2]包中,官

基于abp的appuser对象扩展_阿升1990-编程思维

  在ABP中AppUser表的数据字段是有限的,现在有个场景是和小程序对接,需要在AppUser表中添加一个OpenId字段。今天有个小伙伴在群中遇到的问题是基于ABP的AppUser对象扩展后,用户查询是没有问题的,但是增加和更新就会报"XXX field is required"的问题。本文以AppUser表扩展

[c#] stringformat详解之文本方向、对齐_leslie_xin-编程思维

在使用GDI方式处理文本时,往往会用到StringFormat。里面的某些点有点反直觉,不够直观,所以本篇就通过图文的方式去讲解一下。 本篇内容仅涉及到文本方向、对齐的相关内容。 如有错误、不妥之处,欢迎大家指正。 一、相关属性 与文本方向、对齐相关的属性,主要与三个属性有关: Alignment、LineAlignm

三角函数与缓入缓出动画及c#实现(图文讲解)_leslie_xin-编程思维

日常经常能看到缓入缓出的动画效果,如: 1,带缓入缓出效果的滚动条: 2,带缓入缓出效果的呼吸灯: 像上面这种效果,就是用到了三角函数相关的知识,下面将从头开始一步步去讲解如何实现这种效果。     一、基础知识 (一)三角函数 常用的三角函数有正弦函数(sin)、余弦函数(cos)、正切函数(tan)。在动画效

[c#] (原创)一步一步教你自定义控件——01,trackbar_leslie_xin-编程思维

一、前言 技术没有先进落后之分,只有合不合适。 WinForm有着非常多的优点,在使用WinForm久了之后,难免会觉得WinForm自带的某些控件外观上有些许朴素、或者功能上有些不如意,自然而然便想去美化这些控件,或者给控件添加一些额外功能,而这便是自定义控件的意义所在。 自定义控件的难度并不大,但是却处在一个比较尴

imagelist半透明,alpha通道bug处理。_timandy-编程思维

由于ImageList的先天障碍,对alpha通道支持不好。虽然到xp有所改善,但瑕疵依然存在。 通过reflactor发现ImageList通过windows api来进行读写的。写入数据时会对原始图像进行处理,等到读取时已经获取不到原始图像。鉴于此,只能另起炉灶重新编写一个ImageList,也就是缩略图集合类。

windows下gdi编程注意事项 - 编程思维

在Windows PC上编程,GDI是一个很重要的技术点。很多程序在运行一段时间后出现异常,导致程序崩溃,除了众所周知的内存泄露以外,GDI资源泄露也是一个很直接的原因。下面是我列出的一些注意事项。 Create出来的GDI对象,一定要用DeleteObject来释放,释放顺序是先Create的后释放,后Create的