Silverlight数学引擎(11)——鼠标行为示例
我们在上一节已经在理论上实现了尺规作图的各个功能,并通过代码创建了示例,如何脱离代码使我们的鼠标能够肩负起尺、规、笔三者的重任呢。这涉及到界面交互事件处理很多繁杂的事情,因此一般纯代码下不需要太注意的细节问题,在界面编程时就必须特别注意,细节决定成败。
其实我们之前已经有了一个鼠标行为——拖拽,如果不考虑鼠标右键的话,那么鼠标的委托有三个(MouseUp、MouseDown、MouseMove),我们在拖拽的行为中就是将拖拽行为托付给这些委托来实现的。其他的行为如画点、画线的原理也是如此,如何将这些行为封装使其互不干扰是我们要考虑的。行为不多,其结构也十分简单,如图:

在IBehavior中我们提供了Attach()、Detach()方法,其作用就是处理行为与鼠标委托的关系,这两个方法在行为被关联到它的宿主(我们这里是画板CoordinateSystem)时处理,保证一个宿主一次只关联一种行为,否则行为交叉就会出现又在画又在拖的情况:
partial class CoordinateSystem { private IBehavior _behavior; public IBehavior Behavior { set { if (_behavior != null) _behavior.Detach(); _behavior = value; _behavior.Attach(this); } } }
上面的类图中还有一处值得注意的是虽然我们有多种点(如FreePoint、PointOnLine等),但是我们只提供的一种行为PointCreator,这与我们现实是相符的,因为不管什么点,都是用“笔”这个工具来画的,所以PointCreator的内部实现可能就有点复杂甚至于分支的情况,我们要保证提供给用户的只有一个接口。
到目前位置,一切都是很顺利的,因此我们不妨先噼里啪啦地把各个类的代码写好,然后进行测试。
例如画点,我们是这样实现的:当鼠标按下时,根据当前位置,用HitTests()方法(这里我们仿照以前的HitTest()方法,对其进行了改装,让其可以返回符合条件的所有元素),取得鼠标下面所有的Shapes,通过判断Shapes来确定画什么点,逻辑是这样的:
- 如果Shapes是空,则画一个FreePoint
- 如果Shapes只有一个元素且是LineShape或者CircleShape的话,画一个PointOnLine或PointOnCircle。
- 如果有两个或者以上,就根据前面两个画交点。
一切看起来顺理成章,到了测试的时候才发现除了自由点外,其他的点画起来就没那么轻松了,主要有以下几个Bug:
- HitTest需要精确定位,如果线很细的话,点了很多次都Hit不到。
- 点击圆的内部会画出PointOnCircle,因为圆相对比较大,很容易Hit中,如果想在圆内部画一个FreePoint,那是无法实现的。
- 点击两圆的交叉处会画出两个交点。
由此看来,HitTest()在这里不太适合做命中测试,第一它太精准了,容不了一点误差,而鼠标单击要人性化一点自然是要允许一定的误差的,当然对其改进是一种方法,然而更好的还是专门提供一个IHitTest接口,让各个图形元素自己去实现各自的命中测试方法,这里我们把这个接口与ICoordinate合并,来看看PointShape是如何实现命中测试的:
public override bool HitTest(LogicalPoint p) { //计算当前鼠标单击的地方与PointShape中心得距离,如果距离在误差范围内,则认为命中。 return p.Distance(Center) < BehaviorBase.Setting.LogicalCursorTolerance; }
看看,实现自己的命中测试是不是也很简单,而且相当灵活,对于圆的话,我们只有当时在圆上单击才命中:
public override bool HitTest(LogicalPoint p) { //在圆上而不在园内 return (p.Distance(Center) - Radius.Length).Abs() < BehaviorBase.Setting.LogicalCursorTolerance; }
此外还有一个头痛的问题,就是两个交点的时候,鼠标点击不能确定是两个交点中的哪一个,只能将两个都画出来,要解决这个问题其实也很简单,只要判断哪个离单击的位置最近就可以了:
if (shapesUnderMouse[1] is CircleShape) { var p1 = cs.DrawIntersectionPoint1(shapesUnderMouse[0] as LineShape, shapesUnderMouse[1] as CircleShape); var p2 = cs.DrawIntersectionPoint2(shapesUnderMouse[0] as LineShape, shapesUnderMouse[1] as CircleShape); return position.Distance(p1.Center) < position.Distance(p2.Center) ? p1 : p2; }
这些细节是不是很麻烦啊?呵呵,其实也没有想象中那么麻烦,多花点功夫调试是必须的。最后我们看看画圆行为的实现,一点也不复杂吧:
public class CircleCreator : BehaviorBase { public override string Name { get { return "圆"; } } private LineShape foundRadius;//先找到半径,再找圆心 public override void MouseDown(object sender, MouseButtonEventArgs e) { if (foundRadius == null) { var found = FindLine(e); if (found != null) foundRadius = found; } else { var found = FindPoint(e, true); var cirlce= CS.DrawCircle(found, foundRadius); cirlce.Opacity = Setting.Opacity; foundRadius = null; } } }
上面代码中有一句:
cirlce.Opacity = Setting.Opacity;
是我为下节埋下的一个小小的伏笔,你可能已经猜到了,目前我们画的图都一个样式,辅助线和非辅助线否一个样式,混在一起纠缠不清,这和实际的几何作图是不符的,至少实际的作图会有虚线之类的吧。目前我们只以一个单独的Opacity来控制画图的样式,下节我们将丰富它,使图形插上多彩的翅膀。
最后来Show一下运行截图(鼠标画平行线的示例),你可以看到我增加了行为的工具栏呢:

你是否注意到切换行为与游戏中的切换武器有点类似呢?游戏中除了换武器还有换衣服换坐骑,功能多得去了,游戏更引人入胜的还是它的美轮美奂的界面,相比之下我们的界面是不是显得非常单调呢?那我们下节就来美化我们的各个元素,来给它们披上华丽的衣裳吧!
浙公网安备 33010602011771号