一、前言
上节课程我们实现了连接形状不同的连接点,但在实际使用中会发现很繁琐,需要分别指定开始形状和结束形状的连接点,这明显不符合操作经验逻辑。我们本节课程就来实现鼠标拖动完成连线,拖到哪个连接点就对哪个连接点完成连线,所见即所得,而且在拖动连线过程中,还会实时显示虚线箭头用于提示。
相信看完的你,一定会有所收获!
本文地址:https://www.cnblogs.com/lesliexin/p/19096107
二、先看效果
通过视频我们可以看到,我们本节课就要依次实现3种效果:
1,鼠标拖动连线、连线过程中有虚线箭头提示,且连线样式也变成了带箭头的样式。
2,形状所占区域的精准判断、连线点突出显示,特别是在形状背景色与连接同色时更好区分。
3,每个形状的连接点都不一样,和形状长得一样。
本节课代码已上传Gitee,读者可自行拉取编译。
Gitee地址:https://gitee.com/lesliexin/flowchartdemo
三、效果1:鼠标拖动连线、连线过程中有虚线箭头提示
这个效果乍看起来很复杂,无从下手。不用关键,我们先拆分一下,一点点的去实现。
(一)鼠标进入形状后,显示所有连接点
我们看了上面的视频,已经有了初步印象,连线最开始,是鼠标移动到形状上时,鼠标显示所有的连接点,如下图所示:

我们就先来实现这个效果。
1,形状基类调整
(1)获取连接点所占区域
要想显示所有的连接点,也就是画出来连接点,首先要有获取连接点所占区域的方法。
我们之前的形状基类已经有了获取连接点坐标点的方法:

那么我们连接点所占区域直接以此为基础,就是求一个以连接点为中心的矩形。
我们设定连接点的宽高都是10,那么就可以参照上述方法实现获取连接点区域的方法:

我们定义为虚方法,就是上节讲的一样,有些形状(如:平行四边形)的边并不在矩形区域边上,所以还要提供让派生形状自行计算的能力。
平行四边形:


(2)获取所有连接点区域
因为我们目前就支持左、上、右、下四个连接点,所以获取所有连接点所点区域直接依次调用上面的方法即可:

(3)绘制所有连接点
在获取到所有连接点后,我们就可以进行绘制了,我们同样定义为虚方法,默认就以填充矩形的方式进行绘制:

2,画布调整
我们的形状基类已经支持获取并绘制所有连接点了,接下来就要让画布支持这个功能了。
(1)增加连线模式
我们要实现的是所见即所得的拖动连线方式,所以增加一个'连线模式',此模式下就可以进行连线操作,而默认的就是'正常模式',可以拖动形状等等。
读者可以回想一下日常操作流程图、PPT等,一般都是点个连线样式的按钮,然后就可以拖动连线的,这就是进入了'连线模式'
我们定义一个枚举,来表示画布所支持的模式:

同时定义一个属性,来表示当前画布所处的模式:

(2)移除不需要的属性、字段、方法、事件
当我们实现了拖动添加连线后,原来有一些属性、字段、方法、事件等就不再需要了,所以我们将这些移除掉。
我们已经不需要再提示'请选择开始形状',然后在选择了一个形状后再提示'请选择结束形状'等等,所以直接移除掉:

这个开始形状和结束形状的选中连接点还是需要的,但是不再需要作为公共属性暴露出来,这个是在拖动选择时自动设置的,所以我们改成私有变量:


我们已经不再需要手动开始连线及中止连线了,所以我们直接将这两个公共方法移除掉:

(3)重绘事件,判断鼠标是否进入了形状中
我们要想在连线模式下,鼠标进入形状中就自动显示所有连接点,那么就首先需要在鼠标移动事件(MouseMove)中始终使控件重绘,而不是之前那样只在移动形状时才重绘。

然后在OnPaint事件中,我们在绘制完形状和连线后,判断当前有无形状在鼠标之下,有则绘制此形状的所有连接点:

这样,我们就实现了鼠标进入形状后,显示所有连接点的效果。
(二)聚焦鼠标所在的连接点
在能显示出所有的连接点后,下一步就是鼠标选择连接点,此时要有反馈效果,也就是令鼠标所在的连接点聚焦显示、高亮显示。效果如下:

有了上面的基础,这个就很好实现了,在显示所有连接点后,接着判断下鼠标是否在连接点上就行了。
我们下面来看一下如何实现。
1,形状基类调整
(1)确定当前鼠标是否在某一连接点上
我们要先判断当前鼠标是否在某一个连接点上:

同样,我们定义为虚方法,默认是以矩形区域进行判断的。
(2)获取鼠标所在连接点
当我们判断出了当前鼠标在某一连接点上之后,我们就要获取到鼠标所在连接点是哪一个:

这里我们将判断与获取分成了两个方法,为什么明明一个方法就能既判断又获取,还要拆分成两个方法呢?除了所谓的责任划分外,最主要的原因是判断的时候多,而且一般都在遍历中,分开更好一些。
(3)绘制聚集状态连接点
我们取到了连接点,那么直接取连接点的区域绘制即可:

2,画布调整
这里就很简单了,判断鼠标是否在连接点上,在的话就调用聚焦绘制连接点的方法绘制即可:

到此我们就实现了鼠标移动到连接点上时,连接点聚焦或高亮显示的效果了。
(三)鼠标拖动实时显示半透明虚线箭头
我们接着来看一下拖动连线过程中的效果,就是实时显示一条从开始形状点击的连接到到当前鼠标的半透明虚线箭头。效果如下:

这个本质上就是实时绘制一条从开始形状连接点到鼠标点的连线,原理很简单,我们来看一下具体如何实现。
1,形状基类调整
我们可以看到,虚线箭头的起点就是开始形状的连接点,所以我们要提供一个绘制选中连接点的方法:

同样,定义为虚方法,默认以矩形方式填充。
2,画布调整
那么,起点怎么确定的呢?
(1)鼠标点击确定起点
我们在上一小节里,为了判断鼠标是否在某一连接点上时,实现了一个方法:IsHitLinkRect,所以我们要在鼠标点击时,也就是MouseDown事件中,判断鼠标是否点在了连接点区域内,如果点在其中则设置开始形状和开始连接点信息,并设置进入了连线中的状态:

(2)鼠标移动中绘制虚线箭头
在有了起点,我们就要实时获取当前鼠标信息,并发起重绘。我们前面的小节里已经改造了MouseMove事件,始终触发重绘,所以我们只需要记录一下鼠标的当前坐标,并在重绘事件中绘制虚线箭头即可。


(3)带箭头的虚线
上面一小节还是有个小的知识点的,就是如何绘制一条带箭头的虚线。这其实完全是由画笔(Pen)来实现的,具体请参考MSDN,本文不再赘述:

到此,我们就实现了拖动连线时实时显示起点到当前鼠标点的虚线箭头。
(四)鼠标松开完成添加连线、或取消连线
还是上一小节的图示,我们在松开鼠标时,要达到的效果只有两个:添加连线和取消连线。
而判断标准就是:松开鼠标时,鼠标是否在另一个形状的某一个连接点区域内。
我们在鼠标松开事件MouseUp中进行判断,并根据判断结果添加连线或取消连线:

至此,我们的效果1也就实现了,经过我们一点点的拆解,可以看到复杂的效果也是由一个个基础的效果组合而成的,当一个个基础效果实现之后,复杂效果也就自然而然的实现了。
四、效果2:形状区域精确判断、连接点突出显示
上面效果1的效果不错,但是还是有些细节需要考虑:一是非矩形时鼠标还未移动到形状内,连接点就全显示出来了;二是连接点是红色的,如果形状颜色也是红色的,就是导致连接点分不出来。我们本小节就来优化这两点。
(一)形状区域精确判断
这个不仅是本节所遇到的,在前面的课程里其实也有这个问题,就是非矩形时,我们点击非形状内时,仍可以拖动,就像下面这样:

这是因为我们在判断时是直接按形状所在矩形区域(Rect属性)来判断的,而不是严格按照形状区域进行判断:

同理,我们看文章开头的演示视频也可以看到,当鼠标还没进入形状中时,所有的连接点就显示出来了:

原因同样是因为我们是按照形状所在矩形区域(Rect属性)来判断的:

问题定位到了,我们就针对性的对修改即可。
1,形状基类调整
我们增加一个虚方法,来判断鼠标点是否在当前形状中:

同样提供默认实现,就是按矩形区域去判断。
2,派生形状调整
基类调整后,我们要对矩形外的其它派生形状都重写判断方法,来判断鼠标点是否在形状内。
(1)圆形
很简单,我们构造一个闭合圆形路径,然后判断即可:

(2)菱形
菱形则比较简单,因为我们的菱形就是以路径实现的:

所以我们把这生成路径的部分提出来,给重写判断方法即可:



(3)平行四边形
同菱形,不再赘述:



(4)圆角矩形
同上,不再赘述:



3,画布调整
我们修改画布中判断鼠标点是否在形状内的地方,改为调用我们的判断方法:
(1)判断是否点击在了形状内

(2)判断是否移动鼠标到了形状内

至此,我们就实现了形状区域的精准判断。
(二)连接点高亮显示
在上一小节的效果1中,我们会发现,当形状颜色是红色时,由于连接点也是红色,所以会导致连接点和形状混合在了一起,不明显,如下图所示:

所以我们就来为连接点增加高亮颜色,就像聚焦时显示黄色边框那样。
代码也很简单,我们就像聚焦里那样,在绘制连接点时,直接增加上一个白框即可:

五、效果3:再次抽象统一、每个形状的连接点样式都不一样
我们在上一小节有没有发现一些重复的代码逻辑?
没错!就是那些判断鼠标点是否在形状内时,除了矩形外,都要通过生成路径的方式去判断,而且除了圆形外其它形状不仅判断方法要用,绘制方法也在使用。
既然重复,那就存在抽象和统一的机会,我们下面就来看一下,如何再次抽象和统一。
(一)抽象、统一
1,形状基类修改
首先,我们上一节修改各形状时,除了矩形外都用到了生成路径操作,而且矩形也是支持用路径方式绘制的,所以我们先增加一个获取路径的方法:

我们定义为虚方法,默认生成矩形路径,各形状自行实现路径。
这里有一点需要注意的是,我们添加一个参数Rectangle rect,为什么不直接用属性Rect呢?这是为了实现让连接点样式与形状保持一致的效果,此时可能不太理解,因为这相当于二次抽象,只是不好再单开一小节讲这一点,就合一起了,下文随即就能看到了。
然后我们的形状基类的Draw方法就不需要再是抽象方法了,因为已经能获取到路径了,所以直接在基类里绘制即可:

虽然目前以我们的需求而言,并不再需要派生形状自行实现绘制了,但是仍定义为虚方法,以保持后续兼容,及特殊情况下仍支持形状自行绘制。
2,派生形状调整
(1)矩形
因为形状基类默认就是以矩形方式实现的,所以矩形不需要实现任何重写。

注:以路径方式实现,性能肯定不如直接绘制矩形的方式快,但是这点性能损耗几乎可以忽略不计,而且在代码结构会更简洁。
(2)圆形
前面的精准判断形状时,圆形已经实现了通过路径的方式来判断,所以我们直接将生成圆形路径的方法提出来改下就行了:

(3)菱形
这个更简单,因为已经有现成的生成路径的方法了,稍微改一下就行了:

(4)平行四边形
同上,不再赘述:

(5)圆角矩形
同上,不再赘述:

至此,所有派生形状均以改造完成,可以看到从上一小节变复杂后,又简洁了起来,这就是抽象和统一的魅力。
(二)每个形状的连接点都不一样
我们前面课程已经说了,我们将绘制连接点的方法定义为虚方法,就是能让各派生形状自行实现不同的连接点。但是在我们上面给抽象统一后,我们就不需要再让各派生形状去自行绘制连接点形状了,我们直接使用方法GetShapeGPath(Rectangle Rect),然后将连接点所占矩形区域传入进去就自动返回和形状一样的路径了,之后绘制即可。
所以我们本小节只需要修改形状基类即可。
1,通过路径绘制不同连接点
因为绘制连接点时,有绘制默认状态、聚焦状态等效果,但是核心区别就是边框的颜色不同,所以我也同样抽象统一一下,提供两个重载方法,可以通过参数绘制不同状态的效果。而内部就是调用GetShapeGPath(Rectangle Rect)获取连接点的样式路径:

2,绘制所有连接点方法修改
我们这里要改成遍历执行新增加的绘制连线点方法:

注意,我们这里仍定义为虚方法,因为不是所有的形状作连接点都合适或方便点击操作,所以我们仍保留自行实现连接点样式的能力。
3,绘制不同状态的连接点方法修改
同上,不再赘述:

4,判断鼠标点是否在连接点区域内方法修改
同上,不再赘述:

至此,我们就实现了让连接点样式变成和形状样式一致的效果了。
5,小问题优化
在实现时,我们会发现其它形状的连接点都正常,但是圆角矩形却不是:

这是因为我们在编写圆角矩形时,将圆角的直径固定为了20导致的:

修改也很简单,我们直接将半径改为宽度的10分之一即可,就是不使用固定值,而是根据形状所占区域动态变化:

我们再次运行,就可以看到是正常的了:

注:这个也是为后续的缩放效果做修复,如果不改成相对值,在缩放时就会出问题,而改成这样后,圆角就会随着缩放的大小而动态变化了。
六、结语
我们本节课实现了拖动完成连线,且连线过程中实时显示虚线箭头以提示正处连线中,同时还进行了再次抽象和统一,不仅代码再次简洁,还让我们只用极小的修改就实现连接点与形状保持一致的效果。这就是抽象的魅力。
估计不少读者在看到上节课程结语时说的本节课的效果时,都一头雾水,不知道该如何实现,但是真当我们一点点实现后,会发现并不难,甚至太过简单了。这并不是代码上的难,而是思考实现角度的难,本节课程,并不仅适用于GDI+,几乎所有的语言只要想实现类似的效果,基本逻辑都是一样的,具备普适性。
那么下节课,我们将实现另一个所见即所得的效果:拖动形状控制点来调整形状大小。读者们也请想想该如何实现,相信有了本节的基础,大家可以很容易的想到如何实现。
感谢大家的观看,本人水平有限,文章不足之处欢迎大家评论指正。
-[END]-