Avalonia 使用ItemsControl示例以及问题记录
需求:
读取一个图片,点击就生成一个标记点并显示标记点序号。
设计思路:
1:由于不是winform那样可以设置图层(不知道行不行)。想法是用一个Grid包裹Image和ItemsControl,使其在同一范围内。
2:Image控件用点击事件调用vm的方法传递Point坐标。Image显示的图片需要Fill拉伸
3:ItemsControl中用Canvas做布局,在ItemsControl.ItemTemplate标签的<DataTemplate>标签中用一个Grid再次包裹标记点组合控件。
4:标记点组合控件用一个Ellipse显示圆圈,用一个TextBlock显示标记点序号文本。
5:采用伪MVVM,但是通过事件调用vm示例从而使用viewmodel的方法(想不明白怎么合理的获取x,y坐标。真没招了,不知道这样算不算破坏了MVVM)。
原错误代码:
<Grid Grid.Row="3" Background="Blue" Margin="0"><!-- Width="800" --><!-- Height="376" --><Border BorderBrush="Red" BorderThickness="0" Margin="0" Background="Aqua"><ImageStretch="Fill"Source="/Assets/V8.png"PointerPressed="MarkPoint_Click"></Image></Border><ItemsControlx:Name="MarkersItemsControl"ItemsSource="{Binding Markers}"><!-- 布局容器 Canvas/StackPanel/WrapPanel --><ItemsControl.ItemsPanel><ItemsPanelTemplate><Canvas x:Name="MarkersCanvas" /></ItemsPanelTemplate></ItemsControl.ItemsPanel><!-- 生成标记点 --><ItemsControl.ItemTemplate><DataTemplate><GridCanvas.Left="{Binding Mark_X,Mode=TwoWay}"Canvas.Top="{Binding Mark_Y,Mode=TwoWay }"><!-- Width="{Binding MarkSize, RelativeSource={RelativeSource AncestorType=vm:EditImagePositionViewModel} }" --><!-- Height="{Binding MarkSize, RelativeSource={RelativeSource AncestorType=vm:EditImagePositionViewModel}}" --><!-- 显示的大小绑定数据库里的,添加的按页面的标记点大小 --><EllipseWidth="{Binding FontSize}"Height="{Binding FontSize}"Stroke="Red"StrokeThickness="2"Fill="Transparent"><!-- 边框颜色,厚度,透明 --></Ellipse><!-- Sequence文本 --><TextBlockText="{Binding Sequence}"Foreground="Black"FontSize="{Binding FontSize}"HorizontalAlignment="Center"VerticalAlignment="Center"></TextBlock></Grid></DataTemplate></ItemsControl.ItemTemplate></ItemsControl></Grid>
这是一段有问题的代码:所有的标记点会生成至容器的左上角。新创建较小的标记点也无限趋近于左上角(0,0)应该就是位于原点。

猜测是Canvas的Canvas.Left(X轴原点像右为+)和Canvas.Top(Y轴-原点向下为+)的值没有正确赋值导致默认初始化为(0,0)
问题和修改方式:
直接说原因:这种情况就是定义ItemsControl控件时,忘记定义了ItemsControl.Style,去设置ContentPresenter 并 绑定x:DataType (实体类-不绑定的话,当前上下文是ViewModel(我页面标签头设置了x:DataType=vm的全局引用)而X,Y我需要绑定的是实体类字段,该实体类不在vm里的,导致空引用-无法解析报错)。
需要使用样式,来在Canvas中 定位 缝合了<Ellipse>和<TextBlock>的<Grid>。
官网文档链接:https://docs.avaloniaui.net/zh-Hans/docs/concepts/custom-itemspanel
注意,必须要在ItemsControl.Style 定义ContentPresent
<ItemsControl.Styles><!-- 设置容器中每个子控件的样式 --><Style Selector="ContentPresenter" x:DataType="models:ConfGuide"><!-- 硬编码测试:设置容器在Canvas中的位置 --><!-- <Setter Property="Canvas.Left" Value="600" /> --><!-- <Setter Property="Canvas.Top" Value="200" /> --><Setter Property="Canvas.Left" Value="{Binding Mark_X }"></Setter><Setter Property="Canvas.Top" Value="{Binding Mark_Y }"></Setter></Style></ItemsControl.Styles>
修改效果:
已经正常显示标记点

完整代码:
<Grid Grid.Row="3" Background="Blue" Margin="0"Width="800"Height="376"><Border BorderBrush="Red" BorderThickness="0" Margin="0" Background="Aqua"><ImageStretch="Fill"Source="/Assets/V8.png"PointerPressed="MarkPoint_Click"></Image></Border><ItemsControlx:Name="MarkersItemsControl"ItemsSource="{Binding Markers}"Width="800"Height="376"><!-- 布局容器 --><ItemsControl.ItemsPanel><ItemsPanelTemplate><Canvas x:Name="MarkersCanvas"Width="800"Height="376" /></ItemsPanelTemplate></ItemsControl.ItemsPanel><!-- 生成标记点 --><ItemsControl.ItemTemplate><DataTemplate><Grid><!-- Canvas.Left="{Binding Mark_X,Mode=TwoWay}" --><!-- Canvas.Top="{Binding Mark_Y,Mode=TwoWay }" --><!-- 显示的大小绑定数据库里的,添加的按页面的标记点大小 --><EllipseWidth="{Binding FontSize}"Height="{Binding FontSize}"Stroke="Red"StrokeThickness="2"Fill="Transparent"><!-- 边框颜色,厚度,透明 --></Ellipse><!-- Sequence文本 --><TextBlockText="{Binding Sequence}"Foreground="Red"FontSize="10"HorizontalAlignment="Center"VerticalAlignment="Center"></TextBlock></Grid></DataTemplate></ItemsControl.ItemTemplate><ItemsControl.Styles><!-- 设置容器中每个子控件的样式 --><Style Selector="ContentPresenter" x:DataType="models:ConfGuide"><!-- 硬编码测试:设置容器在Canvas中的位置 --><!-- <Setter Property="Canvas.Left" Value="600" /> --><!-- <Setter Property="Canvas.Top" Value="200" /> --><Setter Property="Canvas.Left" Value="{Binding Mark_X }"></Setter><Setter Property="Canvas.Top" Value="{Binding Mark_Y }"></Setter></Style></ItemsControl.Styles></ItemsControl></Grid>
AI分析:
ItemsControl的项容器(默认是ContentPresenter)可通过Styles中的Style Selector="ContentPresenter"或ItemContainerTheme进行样式设置,两种方式本质一致,均作用于项容器。
你通过ItemsControl.Styles中的Style Selector="ContentPresenter"设置Canvas.Left/Top能生效,核心原因是:ContentPresenter是ItemsControl为每个项自动创建的 “容器控件”,负责承载ItemTemplate的内容并决定其在Canvas中的位置—— 无论用ItemContainerTheme还是Style Selector="ContentPresenter",最终都是在配置这个容器的属性,目的完全一致。
为什么必须针对ContentPresenter设置?
ItemTemplate只负责 “内容”,不负责 “位置”:你在ItemTemplate中定义的Ellipse、TextBlock是项的 “内容”,但它们在Canvas中的位置不由自身的Canvas.Left/Top决定,而是由承载它们的容器(ContentPresenter)的Canvas.Left/Top决定。简单说:ContentPresenter是 “装内容的盒子”,盒子的位置决定了内容的位置,所以必须给 “盒子” 设置位置属性。
Canvas布局依赖容器的位置属性:你的ItemsPanel是Canvas,而Canvas布局的规则是:子元素的位置由其自身或其父容器的Canvas.Left/Top决定。对于ItemsControl生成的项,ContentPresenter是Canvas的直接子元素,因此Canvas会根据ContentPresenter的Left/Top来定位整个项,而非ItemTemplate内部的Grid或Ellipse。
你写的Style Selector="ContentPresenter"为什么能生效?
- 选择器精准定位容器:Selector="ContentPresenter"会匹配ItemsControl为每个项创建的ContentPresenter容器,相当于 “找到所有装内容的盒子”。
- x:DataType="models:ConfGuide"明确绑定上下文:这行声明告诉解析器,ContentPresenter的DataContext是ConfGuide类型,因此{Binding Mark_X}能正确找到ConfGuide中的属性,避免了之前 “上下文混淆” 的错误。
- 与ItemContainerTheme等价:ItemContainerTheme是 Avalonia 中专门用于配置项容器样式的语法,而Style Selector="ContentPresenter"是更通用的样式语法,两者最终都作用于ContentPresenter,效果完全一致。
