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


    一、前言

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

    本篇文章就通过多个示例来讲解一下 GDI+ 与鼠标交互的原理,以及如何去实现。
    每一个示例实现后,都会对示例进行优化,主要是解决一些在实际应用中比较常见的问题,比如:闪烁、资源占用高等等。
    而在最后,会基于实际的应用场景——在背景图上绘制图形并进行鼠标交互——编写一个示例。
    接着会使用实际应用场景内必备的、也是核心的“局部刷新”技术对示例进行优化。

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

    本文地址:(原创)[C#] GDI+ 之鼠标交互:原理、示例、一步步深入、性能优化 - leslie_xin - 博客园


    二、基本原理

    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+ 的极限了。
    当然,本人的水平也有限,只能说勉强够用而已。

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

    所以请对自己掌握的技术多一些信心,多一些耐心。
    在此,作者与诸君共勉!

  • 相关阅读:
    考研思想政治理论大纲
    Java 内置包装类——Object类
    react使用Map方法遍历列表不显示的问题
    CEC2015:动态多目标测试函数之FDA4、FDA5、FDA5_iso、FDA5_dec
    物联网微消息队列MQTT介绍-EMQX集群搭建以及与SpringBoot整合
    OpenAI 推出 DALL·E 3 配合 ChatGPT 无需复杂提示词即可作画
    jQuery中遍历元素each
    数据可视化分析工具DataEase
    Fashion MNIST与分类算法
    UFC765AE102 ABB数据密集型边缘人工智能
  • 原文地址:https://blog.csdn.net/sinat_40572875/article/details/128062548