WPF开发日记—解决拖动行为附加到元素上的延迟
此文的前提是 结合ItemsControl在Canvas中动态添加控件的最MVVM的方式
上一篇博客讲到了我通过使用 ItemsControls 内部设置Canvs为布局面板
并在ItemContainerStyle中设置Canvs.Left, Canvas.Top。
来实现设置MVVM模式下自定义位置显示单项内容的功能。
接下来的一步要显示内容的拖动。
很抱歉的告诉大家,经过这两天的探索,我打算推翻上面的做法。
虽然可以这样用,学到了些东西,但是效果并不是很好。
原因在于,当后面我需要给 Item 加上一个可拖动的功能时,就出现问题了。
我起初是这样做的:创建了一个自定义的可拖动行为类(DragMoveBehavior),
我本是先要测试下通过行为设置 Canvas.Left, Canvas.Top 是否是可用的,
实现了以后,我新建了个测试窗体,放进去一个Canvas,里面再随便塞了一个矩形(Rectangle),给矩形添加了我自定义的拖动行为。
问题很快就出现了,当我拖动的太快,矩形根不上我的鼠标速度,就被丢下了,不再跟着我走。
理想状态下,应该是我拖动的再快,矩形再小,也应该无时差的一直跟着我的鼠标走才对啊。
虽然稍微慢点是可以跟着走的,但我无论如何不能忍受这样的操作体验。
我测试来测试去,发现Canvas.SetLeft是有细微的延迟的。
未经严格证实的猜测是鼠标移动和设置位置两个任务,在主线程的处理优先级是不一致的(DispatcherPriority)。
这个时候,鼠标每移动一个像素点,都需要不断的Canvas.SetLeft,重复不停的布局操作无疑不是很好的选择。
想到 Blend 里面的行为自带了 MouseDragElementBehavior 行为拖动效果,测试发现这个功能在操作上是没有瑕疵的。
然而我并不能直接拿来用。因为相关的位置数据是要读取和保存的,而不只是有一个拖动效果。
所以我直接用 dotPeek 反编译看下微软是怎么实现的。 dotPeek 果然强大,最大的特点是 每个方法都生成了非常完整的中文注释。甚至直接查看基类,也有完整的代码和中文注释。
而我同时用 Reflector 反编译没有注释还报错。 ILSpy 只有部分方法有中文注释。
哦,原来微软用的是 RenderTransform 呈现变形!好东西,我怎么又没想到呢?
不过微软写的比较复杂,600多行代码,用了矩阵变形,实现了变形深克隆,内置了X,Y的依赖属性等等。
依赖属性?难道可以直接用?试了下,不行,没有变化。
太困了,凌晨四点了,睡觉先。
趴在床上,准备睡觉,习惯性翻开mac爽爽vim,突然想到,苹果系统拖动效果是怎样的呢,测试了下,发现竟然有完全一样的问题,然而在鼠标瞬间脱离UI元素的时候,并没有失去鼠标捕获。捕获?哦,我懂了,如果用 MouseCapture 对元素进行捕获就可以了,鼠标不在UI元素上也没问题!放心了,睡觉。
——————————————————
今天着重把 MouseDragElementBehavior 类看明白了,并把源代码调整了下,基本可用了。
最终的使用方式看起来是这样的
<DataTemplate DataType="vm:MyItemViewModel"> <Border Width="120" Height="30" Background="Red"> <i:Interaction.Behaviors> <ib:MouseDragElementBehavior X="{Binding Left, Mode=TwoWay}" Y="{Binding Top, Mode=TwoWay}" /> </i:Interaction.Behaviors> <TextBlock HorizontalAlignment="Center" Foreground="White" VerticalAlignment="Center" Text="{Binding Show}" /> </Border> </DataTemplate>
然而这个时候,我是在 DataTemplete 中的Border根节点附加的 拖动行为。
总感觉哪里不对,哪里不对呢,拖动的时候有细微的晃动,并且Border不应该被用来干拖动这样的事情,另外没有有效的利用路由事件的优势。
这时我对使用 ItemsControl 这个控件产生了怀疑,跑去研究了下ListView,看了下官方ListView四种示例的Demo(地址)认真看完Demo的具体使用方式,发觉这和我想要的东西相差甚远,ListView主要的用途是可以以自定义视图展示可视项,想来想去还是回到ItemsControl 的怀抱上来吧,暂时先以 ItemsControl + Canvas 的组合前提下做调整吧。
仔细分析 MouseDragElementBehavior ,最后发现官方这个类的实现竟然和窗口复杂度有关!
里面有这样一个觉得不好的属性的实现:
/// <summary> /// 获得关联的对象所在的场景的根元素。 /// </summary> private UIElement RootElement { get { DependencyObject reference = AssociatedObject; for (DependencyObject dependencyObject = reference; dependencyObject != null; dependencyObject = VisualTreeHelper.GetParent(reference)) { reference = dependencyObject; } return reference as UIElement; } }
每产生一次Move事件,也就是每移动一个像素点,就要重新遍历去获取一次根元素···然而我的窗口并非一个只有一个跟节点的Demo,嵌套还是很多层的,虽然我是在UserControl 里面实现的,但最终它还是找到了最外层的Windows窗体元素。
如果窗口简单,那么拖动效果还可以,到了我的程序里面,这个拖动效果就有了一定的延迟。
类似的问题不少,我决定根据自己的思路重写这个行为,以达到优化的目的。
同时,我想到了,拖动效果不理想,拖动,会不会有更好的实现呢?Border不太合适?那我为何不用Thumb?测试了下,发现非常有局限性,Thumb虽然用起来简单,但是需要依赖Canvas做容器,并且不支持直接内容,我要想添加内容,只能想到覆盖一层装饰器了,不太合适。
还是继续按我原来的想法优化拖动行为吧!
首先,我删除掉了之前写的 DragMoveBehavior 行为的内容,参考MouseDragElementBehavior ,用 RenderTransform 来实现对鼠标拖动的响应机制。这个过程我发现 Blend 内置的拖动行为类,还有很多我并不需要处理的逻辑和需要优化的地方。当我写完需要的实现,500多行代码变成了100多行。
测试了一下,在我这个复杂的窗体中拖动延迟降低了不少。
可我还是觉得不对!因为拖动的时候有略重的感觉。
难道我优化过的行为实现的不对?
新建一个WPF窗口。放进去一个矩形,附加上我刚刚编写的行为,运行,拖动过程非常流畅!!!
什么情况?
两个窗体,一样的控件,使用效果大不相同,想原因。
对比区别,前者的特别之处是:
1. 我是把控件放在了 UserControl里面了。
2. 窗体本身是我重定义的,也就是去掉了Windows 系统默认的窗体标题栏,并重写了窗体模板。
3. 自定义程序窗体添加了Windows 窗体发光,窗体圆角,窗体透明
4. 解决自定义窗体最大化以及为了完全模拟Windows系统窗体的一系列效果,我使用了 HwndSource 对窗口消息进行了处理。
那么开始针对性查找问题根源:
首先仅不使用UserControl,没有变化,还是有延迟。仅删除窗体模板中的大量内容,依然有延迟。仅注释对窗口消息的处理入口,还是延迟!晕。
就剩下窗体效果了,改窗体的Style吧,唯一有关系的就剩下窗体透明了,AllowsTransparency 改成 False,流畅了!
原因找到了,但为什么会有这样的事情?苍天啊,设置为False,窗体发光和圆角以及窗体本身的透明都是受影响的啊。
去翻 文档 吧,AllowsTransparency 是个允许窗口本身上设置 Background 为透明颜色时,必须设置为 True,才会起作用的东西,同时可以帮助实现非矩形窗口的创建。
在 图形呈现层 一文中,可以了解到,非矩形窗口 又叫做 Layered windows,在Vista以上的操作系统上是硬件加速的。
这样等于开启了硬件加速?我用的win8.1,硬件也是主流配置,玩大型3D网游特效全开也不会卡,而我重新建一个WPF空白窗体,设置为允许透明,背景设置为非透明的白色,在这上面我就放一个矩形连拖动都不流畅,你告诉我这还硬件加速了?
我突然对使用WPF产生了深深的自卑感。
不甘心,跑到 stackoverflow 上搜搜看,类似的问题还真不少,然而并没有好的解决方案。基本的结论是:
1. 大多数性能问题都会因为AllowsTransparency 设置为True 而产生,尤其是动画类效果。
2. AllowsTransparency 和其他技术集成,比如 DirectShow 不能正常运行,再比如我以前遇到的 WebBrowser内容无法显示等,甚至 WindowsFromsHost 这种可以允许在WPF窗体中使用Winform控件的类都失去了作用···也就是说使用它会产生各种不可预期的BUG,而你可能一时间无法想到 这个 窗体透明的设置竟然会是元凶。
3. 经验表明,WPF中的硬件加速,并没有多么惊人的效果,至少它产生的问题与复杂程度让人对使用它优秀的一面产生了畏惧。
4. 如果是企业级应用,尽量避免使用 AllowsTransparency。如果只是一个简单的项目那么可以玩玩。
还是悲哀。为了性能,我还是决定去掉 AllowsTransparency,窗体圆角和发光没有了很遗憾,打算后面有时间换一种方式实现圆角和发光,比如双窗体。
最后,既然使用了RenderTransform,那事实上,ItemsPanel 设置的Canvas 已经没有实际意义了。然而也没有更加轻量的布局控件进行替代,暂时也没有必要使用自定义Panel,Canvas先用着。
最后的最后,附上 我重写的拖动行为的 Demo,特点:
1. 可以附加到任意FrameworkElement元素。
2. 父级元素可以为任意类型的布局控件。
3. X,Y为依赖属性可以设置初值或参与数据绑定。
4. 还没有扩展可拖动边界的计算。
本文原创,转载请注明出处。