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);
            }
        }
    }

上面的类图中还有一处值得注意的是虽然我们有多种点(如FreePointPointOnLine等),但是我们只提供的一种行为PointCreator,这与我们现实是相符的,因为不管什么点,都是用“笔”这个工具来画的,所以PointCreator的内部实现可能就有点复杂甚至于分支的情况,我们要保证提供给用户的只有一个接口。

到目前位置,一切都是很顺利的,因此我们不妨先噼里啪啦地把各个类的代码写好,然后进行测试。

例如画点,我们是这样实现的:当鼠标按下时,根据当前位置,用HitTests()方法(这里我们仿照以前的HitTest()方法,对其进行了改装,让其可以返回符合条件的所有元素),取得鼠标下面所有的Shapes,通过判断Shapes来确定画什么点,逻辑是这样的:

  1. 如果Shapes是空,则画一个FreePoint
  2. 如果Shapes只有一个元素且是LineShape或者CircleShape的话,画一个PointOnLine或PointOnCircle。
  3. 如果有两个或者以上,就根据前面两个画交点。

一切看起来顺理成章,到了测试的时候才发现除了自由点外,其他的点画起来就没那么轻松了,主要有以下几个Bug:

  1. HitTest需要精确定位,如果线很细的话,点了很多次都Hit不到。
  2. 点击圆的内部会画出PointOnCircle,因为圆相对比较大,很容易Hit中,如果想在圆内部画一个FreePoint,那是无法实现的。
  3. 点击两圆的交叉处会画出两个交点。

由此看来,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一下运行截图(鼠标画平行线的示例),你可以看到我增加了行为的工具栏呢:

【源代码和演示地址】

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

 

posted @ 2012-12-04 17:33  地月银光  阅读(1109)  评论(0)    收藏  举报