Avalonia Canvas 绘图实战:用纯 XAML 画出一个跨平台速度表盘
【免费下载链接】AvaloniaDevelop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The future of .NET UI项目地址: https://gitcode.com/GitHub_Trending/ava/Avalonia
面向 .NET 新手的一节 Avalonia 图形绘制练习:只写 XAML,在 Canvas 上画 2D 图形,最后得到一个可拖动的速度表盘。全程不写 C# 逻辑代码,同一份文件在 Windows、macOS、Linux 上渲染结果一致。
先把 Canvas 坐标系想清楚
Canvas 画 2D 图形前,先约定四件事,后面所有数字都按这套规则算:
| 约定 | 规则 |
|---|---|
| 原点 | 画板左上角是(0,0) |
| x 轴 | 向右增大,150,150就是"向右 150、向下 150" |
| y 轴 | 向下增大(和数学坐标系相反,按屏幕习惯) |
| 单位 | DIP(设备无关像素),支持小数 |
Canvas 本身不做任何自动排列——它只认四个附加属性,把每个子元素"钉"在指定坐标上;子元素多宽由它自己的Width或内容决定,Canvas 不管。
| 附加属性 | 锚定方向 | 行为 |
|---|---|---|
Canvas.Left | 左边缘 | 元素左边距画板左边的像素数 |
Canvas.Top | 上边缘 | 元素顶边距画板上边的像素数 |
Canvas.Right | 右边缘 | 元素右边距画板右边;宽度变化时位置跟着滑 |
Canvas.Bottom | 下边缘 | 适合"贴底"元素 |
冲突规则只有一条:Left 和 Right 都写时 Left 赢,Top 和 Bottom 都写时 Top 赢;全都不写,元素落在(0,0)。另外 Canvas 测量自己时返回0,0,不占父容器空间,可以单独叠在任何面板上当纯绘图层。这些定义在 src/Avalonia.Controls/Canvas.cs 里,读一遍源码十分钟能看完。
该画哪种轮廓,就选哪个图形控件
选型不用背,对着这张表走:
| 你想画 | 用这个控件 | 必须写的属性 |
|---|---|---|
| 一条直线(刻度、指针) | Line | StartPoint、EndPoint |
| 直角矩形 | Rectangle | Width、Height |
| 圆角矩形 | Rectangle | 同上,再加RadiusX、RadiusY |
| 圆或椭圆 | Ellipse | Width、Height(两者相等即正圆) |
| 一条折线 | Polyline/Polygon | Points |
| 任意轮廓、曲线 | Path | Data(命令字符串) |
这五个控件(外加Arc、Sector)都在 src/Avalonia.Controls/Shapes/ 目录下。它们共享同一套外观属性:Fill管内色,Stroke管边框色,StrokeThickness管边框粗细。Line默认边框粗细是 1,想画粗线记得显式写。
Path的Data是几何迷你语言的字符串,记住四个命令就够画大多数轮廓:
| 命令 | 含义 | 例子 |
|---|---|---|
M | 画笔移到起点,不画线 | M 50,100 |
L | 从当前位置画直线到指定点 | L 150,100 |
C | 三次贝塞尔曲线:两个控制点 + 终点 | C 100,50 130,150 200,100 |
Z | 直线连回起点并闭合,闭合影响Fill | Z |
动手:把表盘画出来
目标:300×300 的表盘,中心(150,150),灰色外圈、白色盘面、8 条刻度、一根红指针,下面跟一个滑块和实时读数。
第一步:表盘底与边框。不画"圆环",而是叠两个Ellipse:灰色大圆直径 260,白色小圆直径 244,两层之间自然露出 8 像素灰边,省掉了画环形的任何额外工作。
<!-- 外圈灰底:距画板每边 20 --> <Ellipse Canvas.Left="20" Canvas.Top="20" Width="260" Height="260" Fill="#D3D3D3"/> <!-- 白色盘面:再内缩 8px,两圈之间形成环形边框 --> <Ellipse Canvas.Left="28" Canvas.Top="28" Width="244" Height="244" Fill="White"/>第二步:指针和中心轴。指针是从中心(150,150)指向右上 45° 的一根红线,终点取半径 105 处的(224,76)。注意这里指针写在刻度之前——默认"后写在上",指针会被后面的刻度盖住,所以显式给它ZIndex="1"抬回来。中心再压一个 16×16 小红点,盖住线条汇聚处,看起来像转轴。
<!-- 指针:中心到 45° 方向,ZIndex=1 保证压过后面写的刻度 --> <Line StartPoint="150,150" EndPoint="224,76" Stroke="Red" StrokeThickness="5" ZIndex="1"/> <!-- 中心轴:盖住线条汇聚点 --> <Ellipse Canvas.Left="142" Canvas.Top="142" Width="16" Height="16" Fill="Red" ZIndex="2"/>第三步:刻度、读数、滑块。8 条刻度沿 0°/45°/90°… 摆开,每条从半径 120 画到半径 105(长 15),45° 方向的坐标用150 ± r×0.707心算即可。读数不放属性里,而是绑定给滑块:滑块取值 0–200,初始 60,StringFormat把数字格式化成带单位的文本。把Value="60"改成 120,启动时读数就是 120 km/h。
完整案例(新建 Avalonia 项目后,把MainWindow.axaml里<Window>标签内的内容整体换成下面这段即可运行):
<StackPanel Margin="20" Spacing="12" HorizontalAlignment="Center"> <!-- ① 表盘:Canvas 只负责定位,不做排列 --> <Canvas Width="300" Height="300"> <Ellipse Canvas.Left="20" Canvas.Top="20" Width="260" Height="260" Fill="#D3D3D3"/> <Ellipse Canvas.Left="28" Canvas.Top="28" Width="244" Height="244" Fill="White"/> <!-- ② 指针:写在刻度前,靠 ZIndex 抬到最上层 --> <Line StartPoint="150,150" EndPoint="224,76" Stroke="Red" StrokeThickness="5" ZIndex="1"/> <Ellipse Canvas.Left="142" Canvas.Top="142" Width="16" Height="16" Fill="Red" ZIndex="2"/> <!-- ③ 8 条刻度:每 45° 一条,半径 120 → 105 --> <Line StartPoint="150,30" EndPoint="150,45" Stroke="Black" StrokeThickness="2"/> <Line StartPoint="235,65" EndPoint="224,76" Stroke="Black" StrokeThickness="2"/> <Line StartPoint="270,150" EndPoint="255,150" Stroke="Black" StrokeThickness="2"/> <Line StartPoint="235,235" EndPoint="224,224" Stroke="Black" StrokeThickness="2"/> <Line StartPoint="150,270" EndPoint="150,255" Stroke="Black" StrokeThickness="2"/> <Line StartPoint="65,235" EndPoint="76,224" Stroke="Black" StrokeThickness="2"/> <Line StartPoint="30,150" EndPoint="45,150" Stroke="Black" StrokeThickness="2"/> <Line StartPoint="65,65" EndPoint="76,76" Stroke="Black" StrokeThickness="2"/> </Canvas> <!-- ④ 读数:绑定滑块,拖动实时刷新 --> <TextBlock Text="{Binding ElementName=speedSlider, Path=Value, StringFormat='{}{0:0} km/h'}" FontSize="18" HorizontalAlignment="Center"/> <!-- ⑤ 滑块:0–200,初始 60 --> <Slider x:Name="speedSlider" Width="260" Minimum="0" Maximum="200" Value="60" HorizontalAlignment="Center"/> </StackPanel>跑起来拖动滑块,读数跟着跳——图形、文字、控件各管各的,绑定只牵线。
画完第一张后最常问的 4 个问题
指针被后面的元素挡住了,怎么办?XAML 里"后写的默认在上"。想打破这个顺序就写ZIndex,它定义在Visual上(见 src/Avalonia.Base/Visual.cs),所有图形和控件都能用,数值越大越靠上。把指针的1改成-1再运行,能亲眼看到它被刻度盖住。
怎么让图形跟着滑块动?三个要素:ElementName找到源控件、Path=Value取哪个属性、StringFormat控制显示格式。案例里读数绑的是Text;换成绑Ellipse的Width和Height,就能让圆跟着滑块变大变小:Width="{Binding ElementName=speedSlider, Path=Value, FallbackValue=80}"。
小图形一多就卡,怎么提速?合并。上百条短刻度别用上百个Line,一个Path就能装下全部子路径:
<!-- 一条 Data 装多条子路径,渲染时只当一个图形处理 --> <Path Stroke="Black" StrokeThickness="2" Data="M 150,30 L 150,45 M 270,150 L 255,150 M 30,150 L 45,150"/>绘制对象数量降下来了,重复的小元素尤其受益。
ZIndex明明设了,层序却像没变?ZIndex只在同一父容器内的兄弟元素之间重新排序。两个元素分属不同容器时,彼此完全不受影响——把其中一个挪进另一个的父级下,或者调整嵌套结构,层序才会按预期变化。
带走三句话,再往哪走
带走三句话:
- 原点在左上角,x 向右、y 向下,单位 DIP;
Canvas.Left/Top钉位置,Left/Top 冲突时赢。 - 直线用
Line、矩形用Rectangle(圆角加RadiusX/RadiusY)、圆用Ellipse、其余一切交给Path。 - 默认后写的在上,要打破顺序就
ZIndex,且只在同父级兄弟间生效。
继续往哪走:
- 让指针真的转起来:给
Line挂一个RotateTransform,用RotateTransform.AngleProperty做DoubleAnimation,再加一个十几行的IValueConverter把滑块值映射成角度,表盘就从"静态图"变成"活的仪表"。 - 复杂轮廓:命令字符串拼不动时,改用
StreamGeometry在代码里逐段构造路径,XAML 只负责展示。 - 动画细节:补间、关键帧、缓动函数都在 src/Avalonia.Base/ 的 Animation 目录里,照着声明式写法加到表盘上即可。
同一套 XAML,所有平台行为一致——这大概就是跨平台绘图的全部卖点。
【免费下载链接】AvaloniaDevelop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The future of .NET UI项目地址: https://gitcode.com/GitHub_Trending/ava/Avalonia
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考