有许多人喜欢手写板或者涂鸦板之类的东西,而并不怎么喜欢输入法,因此Microsoft专门有Ink这个东西用于处理鼠标画图。不得不说这个东西功能十分的强大,也让许多用户使用起来非常方便,用微软开发出来的很多Ink与Bitmap结合的API,即使不会用Photoshop的人也能轻松打开一张图片,然后在自己喜欢的地方写上一段话或者签个名什么的。这个功能,Winform上面有,WPF上面也有,当然Silverlight上也有,只不过作为起步不久的Silverlight版Ink,功能尚不够强大,目前能够开放给我们使用的,只有InkPresenter这一个控件。
也许开发WPF的人都没怎么听说过这个控件而只听说过InkCanvas——那是一个在WPF上对Ink功能封装得非常完善的控件,我们可以使用它进行画图和橡皮擦等一系列的事情。当然,如果你去研究过这个控件,你就可以发现其实它其实是通过DataBinding在InkPresenter上进行了进一步的封装。由于WPF与Silverlight不同的继承结构,恐怕在Silverlight上很难照搬WPF上那一套,即我们不能对其进行一对一的Port,所以如果要在Silverlight上实现一个InkCanvas,就要另辟蹊径。
如果你使用过WPF的InkCanvas控件,你将会发现它支持EraseByPoint,EraseByStroke,Ink三种模式,而且支持复制、粘贴,而且可以轻松地扩展出撤销与重做两个功能。但是后面的一系列功能,不是InkCanvas的核心功能,只要前三者得以实现,那么这个InkCanvas就可以正常的运作了。那么,我们首先从这三种模式中用于画图的Ink模式说起。
InkCanvas的核心,其实在于它内部的InkPresenter,在Silverlight中InkPresenter仅仅是Canvas的子类,只不过它多了Strokes这么一个属性用于存储和展示画上去的所有Stroke。因此,它把如何生成一个Stroke的问题完全留给了我们。先来看一下Stroke的定义:
// Represents a collection of points that correspond to a stylus-down, move,
// and stylus-up sequence.
public sealed class Stroke : DependencyObject
{
// Summary:
// Initializes a new instance of the System.Windows.Ink.Stroke class.
public Stroke();
//
// Summary:
// Initializes a new instance of the System.Windows.Ink.Stroke class with the
// specified System.Windows.Input.StylusPointCollection.
//
// Parameters:
// stylusPoints:
// A System.Windows.Input.StylusPointCollection that represents the System.Windows.Ink.Stroke.
public Stroke(StylusPointCollection stylusPoints);
// Summary:
// Gets or sets the properties of the stroke, such as System.Windows.Ink.DrawingAttributes.Height,
// System.Windows.Ink.DrawingAttributes.Width, System.Windows.Ink.DrawingAttributes.Color,
// or System.Windows.Ink.DrawingAttributes.OutlineColor.
//
// Returns:
// The System.Windows.Ink.DrawingAttributes of the stroke.
public DrawingAttributes DrawingAttributes { get; set; }
//
// Summary:
// Gets or sets the stylus points of the System.Windows.Ink.Stroke.
//
// Returns:
// The System.Windows.Input.StylusPointCollection that contains the stylus points
// that represent the current System.Windows.Ink.Stroke.
public StylusPointCollection StylusPoints { get; set; }
// Summary:
// Retrieves the bounding box for the System.Windows.Ink.Stroke object.
//
// Returns:
// A System.Windows.Rect structure defining the bounding box for the System.Windows.Ink.Stroke
// object.
public Rect GetBounds();
//
// Summary:
// Indicates whether a specified System.Windows.Input.StylusPointCollection
// intersects with a System.Windows.Ink.Stroke object.
//
// Parameters:
// stylusPointCollection:
// The System.Windows.Input.StylusPointCollection used to check for intersection
// with the System.Windows.Ink.Stroke object.
//
// Returns:
// true if the specified System.Windows.Input.StylusPointCollection intersects
// with the System.Windows.Ink.Stroke object; otherwise, false.
public bool HitTest(StylusPointCollection stylusPointCollection);
}
其中DrawingAttributes这个属性是用于描述画笔的颜色的,而StylusPoints描述了Stroke内点的集合。学过数学的人都知道,线是由点组成的,因此只要我们找到了应该插入到这个Stroke中所有的点,那么生成一个新的Stroke不在话下。所幸MouseEventArgs中,有一个StylusDevice只读属性,而它的一个公共方法public StylusPointCollection GetStylusPoints(UIElement relativeTo)可以在鼠标事件触发的时候,得到这些“点”的集合。我们只需要为InkPresenter加上MouseLeftButtonDown,MouseMove,MouseLeftButtonUp三个handler,那么我们就可以在鼠标进行轨迹上把那些点加到线上,并将这条线加入到InkPresenter这个“面”里。代码比较多,最后我会把工程放在下面,就不一段一段的贴了。
其实这个Ink模式,不算什么难点,而后面这个EraseByStroke也相对简单,最笨的方法就是遍历InkPresenter内所有的Stroke,然后一一检验它是否与我们的"Eraser"有交叉,如果有,则将它Remove。但是,最后这个EraseByPoint可没那么容易了,因为当橡皮将一条线拦腰截断的时候,不但要把擦掉的部分去掉,还要把余下的两段保留在Strokes这个StrokeCollection中,这才能达到一分为二的效果。我最初在实现这个功能的时候,由于设计的算法时间复杂度居高不下,造成如果相交的线过多,或者橡皮拖动太快,就会出现卡死的现象。在与微软silverlight开发小组的stefan swick交流之后,他决定实现这一功能,并且将其做成一个Custom Control。昨天他告诉我他把这个东西做好了,要我去他的Blog上下载。今天我仔细研究了他的算法,发现这个算法与我的算法有一个最大的不同之处就是:我在将一条线一分为二的过程中,完全是按照从前向后的顺序,将每个点一一挎贝并缓存,从前向后判断这个点是否被橡皮擦中,如果被擦中的话,马上生成一个新的Stroke,把旧的加入Strokes内,并对新的Stroke进行上述相同的操作。而stefan的算法则分为了两个部分,首先从前向后把前面没有被擦中的点取出来存到一个新的Stroke中,然后停止,再从后往前寻找后面的点,将没有被擦中的点加入到一个新的Stroke中,直到遇到被擦中的点停止。这样的话,可以保证一个Stroke可以被一分为二。
经过我的测试,执行并没有什么问题。但是由于我们向Stroke中插入点,完全依赖于MouseMove事件,如果我们的鼠标移动速度过快,那么被插入的这些本就离散的点,它们之前的间隔会变得更大。这在Ink模式下不会有什么问题,但是在EraseByPoint模式下,就会因被去掉的点附近没有其他的点,而一次性擦掉很大的一段,这是由于我们在插入点和擦除的时候没有做任何的优化造成的,希望这个问题能得到解决。
大家可以到http://blogs.msdn.com/swick/archive/2008/11/30/erasing-ink-in-silverlight-2.aspx去看stefan的原文,那里提供工程原件的下载,我就不再多此一举把它上传到博客园来浪费空间了。至于上面提到的问题,如果大家有什么优化的方式和算法,希望可以告诉我们,谢谢!
缥缈落花街 月圆月缺 望峦山平川 雁返君未还 怆然晚春残 忆天上人间