使用ItemsControl作为容器
想要在面板上显示图形,先把图形的VM都创建好,希望能传所有VM到一个列表(ObservableCollection
泛型容器)里,然后界面上就能显示出对应的图形。
这些图形VM都继承自ViewModelBase
,同时有一些是结点, 有一些是连线。结点是直接继承了NetworkItem_VM
,然后再间接继承ViewModelBase
的。
如果直接用ItemsControl
作为VM容器,即:
<ItemsControl Items="{Binding UserControlVMs}" Height="1000" Width="2000"/>
设置Style和数据模板:
<UserControl.Styles>
<!--ItemsControl的样式-->
<Style Selector="ItemsControl">
<!--使用Canvas作面板-->
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<!--这里需要为Canvas设置颜色才能在按下时相应鼠标事件-->
<Canvas Background="#EEEEEE" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<!--右键菜单行为-->
<Canvas.ContextMenu>
<ContextMenu>
<MenuItem Header="xxx" Command="{Binding xxx}"/>
</ContextMenu>
</Canvas.ContextMenu>
</Canvas>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<!--里面放图形的VM-->
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate DataType="vm:ViewModelBase">
<ContentControl Content="{Binding .}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<!--Avalonia的ItemsControl中没有ItemContainerStyle,用选择器+样式来绑定Canvas.Left等附加属性-->
<Style Selector="ItemsControl > ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</UserControl.Styles>
这种方法能实现需求,但是出来的图形会有些卡顿,仅仅在当前图形面板上操作时不太能感觉出来,但是在不同图形面板之间切换时就会有很明显的卡顿,创建12个结点,40多个控制点,然后所有这些50多个点连线在一起,切换回这个图形面板时大约需要3秒的时间(8G内存64位win10集成显卡)。
改用ListBox作为容器
在这篇问答中讨论的是WPF上的切换问题,Avalonia中也有这样的问题,然后答主意思是ListBox
支持UI虚拟化,代替原生的ItemsControl
效果会好一些。
改用ListBox
作为VM容器:
<ListBox Name="panel" Items="{Binding UserControlVMs}" Height="1000" Width="2000"/>
设置Style和数据模板:
<UserControl.Styles>
<!--ListBox的样式-->
<Style Selector="ListBox#panel">
<!--使用Canvas作面板-->
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<!--这里需要为Canvas设置颜色才能在按下时相应鼠标事件-->
<Canvas Background="#EEEEEE" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<!--右键菜单行为-->
<Canvas.ContextMenu>
<ContextMenu>
<MenuItem Header="xxx" Command="{Binding xxx"/>
</ContextMenu>
</Canvas.ContextMenu>
</Canvas>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<!--里面放图形的VM-->
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate DataType="vm:ViewModelBase">
<ContentControl Content="{Binding .}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<!--ListBox下的ListBoxItem项-->
<Style Selector="ListBox#panel ListBoxItem">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</UserControl.Styles>
这里用ListBox#panel
指明name的原因是图形里面有些也带ListBox
(状态机的转移边),这里让选择器只选择这个容器ListBox
。现在渲染速度比用ItemsControl
快了大概三倍,和上面一样多的图形大概要用1秒的时间。
但是这种方式又带来一个问题,就是因为ListBox
有指针停留和选中的问题,其中的项会产生灰色不透明和蓝色透明的背景,而且会影响下层内容的点击,问题效果和可视树如下:
如果用Enabled="False"
或者IsHitTestVisible="False"
确实可以解决这个问题,但是这样整个ListBox
就不响应鼠标操作了,对图形的拖拽和打开右键菜单都会失效。
这里加了一个Style来解决这个问题,判断是NetWorkItem_VM
的子类就设置IsHitTestVisible="True"
,否则就是图形连线,设置为False
。
<Style Selector="ListBox#panel ListBoxItem > ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="IsHitTestVisible" Value="{Binding IsNetWorkItemVM}"/>
</Style>
相应地,在ViewModelBase
类里要加个判断的属性:
namespace sbid._VM
{
public class ViewModelBase : ReactiveObject
{
// 判断这个ViewModelBase对象是不是一个NetWorkItem_VM
// 用于在状态机中区分"线"(直线/箭头)和"元素"(状态/转移块/控制点)
// 以将"线"设置不可点击也不会显示选中效果,防止影响到"元素"的显示和移动
public bool IsNetWorkItemVM { get => this is NetworkItem_VM; }
}
}