为 X11 编写带几何对象(形状)的 XAML 应用程序





5.00/5 (7投票s)
目前,所有主流的 Linux/Unix (X11) GUI 应用程序框架(GTK+、KDE)都不支持基于 XAML 的应用程序开发。Moonlight 项目(包括 XAML 支持)已于 2012 年 5 月 29 日停止维护。本文将介绍一个基于 XAML 的应用程序,其中包含 WPF 几何对象(形状)。
下载 XamlGeometryApp_X11_32.zip 包含完整源代码和可执行文件的 Mono 项目
下载 XamlGeometryApp_X11_64.zip 包含完整源代码和可执行文件的 Mono 项目
下载 XamlGeometryApp_Win7.zip 包含完整源代码和可执行文件的 Visual Studio 2013 项目
引言
本文是一项案例研究,介绍如何使用 **Roma Widget Set** (Xrw) 编写一个基于 MVVM (Model View ViewModel) 设计模式的 X11 应用程序(利用 WPF 几何对象/形状),并使用 XAML。 Roma Widget Set 是一个无依赖关系的 X11 GUI 应用程序框架(仅需要免费 Mono 标准安装的程序集和免费 X11 发行版的库;它不特别需要 GNOME、KDE 或商业库),完全用 C# 实现。
本文继续了以下系列文章的工作:为 X11 编写 XAML 对话框应用程序、为 X11 编写 XAML 功能区应用程序、为 X11 编写具有海量数据绑定和零代码的 XAML 应用程序以及为 X11 编写 XAML 计算器应用程序。据我所知,这是在 Moonlight 停止维护后,首次尝试使用 XAML 进行 X11 应用程序开发(利用 Xrw)。
Roma Widget Set 和 XAML 实现均未完成。此示例应用程序旨在作为另一个“概念验证”,并检查是否以及如何可能使用 XAML 创建基于 MVVM 设计模式的 X11 应用程序。
由于这是第五次尝试使用 XAML 进行 X11 应用程序开发,并且已成功,因此肯定会继续发布有关在 X11 上使用 Roma Widget Set 的 XAML 的其他文章。
背景
使用 XAML 进行 X11 应用程序开发的**动机**和总体**概念**已在 为 X11 编写 XAML 对话框应用程序文章中进行了说明。
焦点
第一个文章 为 X11 编写 XAML 对话框应用程序 演示了使用 XAML
- 定义一个包含一些控件的窗口,并且
- 将点击事件连接到按钮上,
第二个文章 为 X11 编写 XAML 功能区应用程序 演示了使用 XAML
- 定义一个包含 Ribbon 命令界面的窗口,
- 定义静态窗口资源(本示例有一个资源转换器和一个 ModelView),
- 将 ModelView 作为静态资源分配给控件的数据上下文,
- 将资源转换器作为静态资源应用于控件的属性,
- 通过“RelayCommand”方法将命令绑定到按钮,
- 将控件属性绑定到数据上下文,并且
- 控件可以通过
INotifyPropertyChanged
接口进行更新,
第三个文章 为 X11 编写具有海量数据绑定和零代码的 XAML 应用程序 演示了使用 XAML
- 海量数据绑定可以提供有用的功能,并且
- 可以定义一个零代码后端的应用程序,
第四个文章 为 X11 编写 XAML 计算器应用程序 演示了使用 XAML
- 可以轻松设计简单的菜单,
- 可以实现剪贴板文本交换,并且
- 通过使用内置转换器的异常,数据验证可以显示输入错误,
第四篇文章的更新版本也进行了演示
- 自定义绑定验证和
- 键盘快捷键绑定,
本文将演示 XAML 可以实现:
- 可以创建简单的 WPF 几何对象(形状),如线、弧、矩形、圆角矩形、椭圆、折线、多边形和路径,以在画布上绘制,并且
- 可以删除或操作它们(填充、定位、旋转)。
使用代码
示例应用程序是用 Mono Develop 2.4.1、Mono 2.8.1、OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面编写的。移植到任何旧版本或任何新版本都不应该有问题。示例应用程序的解决方案包含两个项目(提供完整的源代码供下载)。
- XamlGeometryApp 包含示例应用程序的源代码。
- XamlPreprocessor 包含 XAML 预处理器的源代码。
该示例应用程序还通过了 Mono Develop 3.0.6 在 OPEN SUSE 12.3 Linux 64 位 DE 和 GNOME 桌面、IceWM、TWM 和 Xfce 上针对 Mono 3.0.4 的测试。
32 位和 64 位解决方案之间的唯一区别是某些 X11 特定数据类型的定义,正如在 使用 Mono Develop 编程 Xlib - 第一部分:底层(概念验证) 文章中所述。
Xlib/X11 窗口处理基于 **X11Wrapper** 程序集版本 0.9(此示例项目包含 0.9 版预览库),该程序集定义了 Xlib/X11 调用 libX11.so.1 的函数原型、结构和类型。该程序集是为 使用 Mono Develop 编程 Xlib - 第一部分:底层(概念验证) 项目开发的,并在 编程 Roma Widget Set (C# X11) - 一个无依赖关系的 GUI 应用程序框架 - 基础 项目中得到了改进。
GUI 框架基于 **Xrw** 程序集版本 0.9(此示例项目包含 0.9 版预览库),该程序集定义了 XAML 代码中使用的控件/小部件及其包装类(应尽可能接近 Microsoft® 原版)。该程序集是在 编程 Roma Widget Set (C# X11) - 一个无依赖关系的 GUI 应用程序框架 - 基础 项目中开发的。
建议:要使用 MonoDevelop 的类库文档快捷键 (F1),必须安装“mono-tools”软件包。
所有图像都显示了示例应用程序的相同状态:包含所有初始几何图形,没有从(左侧工具栏)添加新几何图形,也没有进行(右侧工具栏)任何操作。
第一张图片显示了示例应用程序在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面上的样子。
第二张图片显示了示例应用程序在 OPEN SUSE 12.3 Linux 64 位 DE 和 Xfce 上的样子。
第三张图片显示了示例应用程序在 Windows® 7 64 位版本上的样子。
示例应用程序提供了一个简单的画布来绘制几何图形。所有内置的 WPF 几何图形(System.Windows.Shapes.Line
、(圆角) System.Windows.Shapes.Rectangle
、System.Windows.Shapes.Ellipse
、System.Windows.Shapes.Polyline
、System.Windows.Shapes.Polygon
和 System.Windows.Shapes.Path
)都已测试。左侧工具栏没有用于创建新路径的工具按钮,但右下角的一个初始路径几何图形演示了路径的功能。
除了内置的 WPF 几何图形外,自制的 System.Windows.Shapes.Arc
类还完善了可用的几何图形。
该窗口使用 System.Windows.Controls.DockPanel
作为其根布局管理器。顶部停靠着一个带有单层菜单 **File** 和 **?** 的简单菜单栏,底部停靠着一个状态栏;两者都使用了完整的可用宽度。一个停靠在左侧的 System.Windows.Controls.Grid
用于组织创建新几何图形的工具按钮。另一个停靠在右侧的 System.Windows.Controls.Grid
用于组织操作当前所选几何图形的工具按钮。中心(停靠位置 fill)包含一个 System.Windows.Controls.Cancas
,周围有一个 System.Windows.Controls.Border
以实现几何对象(形状)的裁剪。(System.Windows.Controls.Cancas
本身不提供裁剪功能,而 XrwXAML.Canvas
则包装了一个具有自身窗口的 Xlib/X11 控件,并默认提供裁剪功能。)此示例应用程序的画布通过左键单击支持单个几何图形的选择。
所有工具按钮都已为其 Click
属性注册了回调,以实现功能(对于这个简单的项目,Click
属性比 Command
属性和“RelayCommand”方法(在 为 X11 编写 XAML 功能区应用程序 文章中讨论)更精简,且没有缺点)。
X11 和 Windows 7 版本示例应用程序之间唯一的实际区别是 X11 版本对“重新填充”工具按钮(位于右侧工具栏)增加了 the hatch brush 支持。这是因为一些 the hatch 位图集成在 X11 中(X11BrushInfo.HatchType.*
),而在 Windows 7 上定义 the hatch brush 需要额外的努力。
逐步演示
项目设置、应用程序文件上下文(主题除外)和预处理器代码生成步骤与 为 X11 编写 XAML 对话框应用程序 中的完全相同。如果需要从头开始创建新解决方案,请参考该文章。
主视图文件上下文
XAML (MainView.xaml)
首先是 XAML 文件,省略了画布内容。
<Window x:Class="XamlGeometryApp.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:XamlGeometryApp"
Name="MainWindow" Title="XAML geometry application"
Width="510" Height="380" Icon="XrwIcon16.bmp">
<!-- ATTENTION: To set an application icon, a Resources.resx file must
be created for the project on the windows platform. -->
<Window.Resources>
<ResourceDictionary>
<src:MainWindowViewModel x:Key="MainViewModel" />
<!-- MainWindowViewModel : ViewModelCore<T>.ctr() automatically registeres
itself as the initial Window.DataContext. -->
</ResourceDictionary>
</Window.Resources>
<DockPanel Name="MainDockPanel" Background="#E8E8E8"
DataContext="{StaticResource MainViewModel}">
<Menu Name="MainMenu" DockPanel.Dock="Top" >
<MenuItem Name="MenuItemFile" Header="_File ">
<MenuItem Name="MenuItemFileEdit" Header="Exit"
Click="MenuItemFileExit_Click" />
</MenuItem>
<MenuItem Name="MenuItemQmark" Header="_?">
<MenuItem Name="MenuItemQmarkHelp" Header="Help"
Click="MenuItemQmarkHelp_Click" />
<MenuItem Name="MenuItemQmarkAbout" Header="About"
Click="MenuItemQmarkAbout_Click" />
</MenuItem>
</Menu>
<Label Name="StateBar" Content="Ready." HorizontalContentAlignment="Left"
DockPanel.Dock="Bottom"/>
<Grid Name="LeftGrid" Background="#E8E8E8" DockPanel.Dock="Left">
<Grid.Resources>
<!-- <src:MainViewModel x:Key="mainViewDataSource" /> -->
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70" />
<!-- Right padding. -->
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="1.0*" />
<RowDefinition Height="1.0*" />
<RowDefinition Height="1.0*" />
<RowDefinition Height="1.0*" />
<RowDefinition Height="1.0*" />
<RowDefinition Height="1.0*" />
<RowDefinition Height="1.0*" />
</Grid.RowDefinitions>
<Button Name="ButtonNewLine" Grid.Row="0" Click="ToolNewLine_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="New Line" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonNewArc" Grid.Row="1" Click="ToolNewArc_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="New Arc" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonNewRect" Grid.Row="2" Click="ToolNewRect_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="New Rect" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonNewRRect" Grid.Row="3" Click="ToolNewRRect_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="New RRect" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonNewEllipse" Grid.Row="4" Click="ToolNewEllipse_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="New Ellipse" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonNewPolyline" Grid.Row="5" Click="ToolNewPolyline_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="New Polyline" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonNewPolygon" Grid.Row="6" Click="ToolNewPolygon_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="New Polygon" TextWrapping="Wrap"/>
</Button>
</Grid>
<Grid Name="RightGrid" Background="#E8E8E8" DockPanel.Dock="Right" Margin="0" >
<Grid.Resources>
<!-- <src:MainViewModel x:Key="mainViewDataSource" /> -->
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80" />
<!-- Right padding. -->
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="1.0*" />
<RowDefinition Height="1.0*" />
<RowDefinition Height="1.5*" />
<RowDefinition Height="1.5*" />
<RowDefinition Height="1.5*" />
<RowDefinition Height="1.5*" />
<RowDefinition Height="1.5*" />
<RowDefinition Height="1.5*" />
</Grid.RowDefinitions>
<Button Name="ButtonDelete" Grid.Row="0" Click="ToolDelete_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="Delete" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonRefill" Grid.Row="1" Click="ToolRefill_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="Re-fill" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonMoveUp" Grid.Row="2" Click="ToolModeUp_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="Move up" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonMoveDown" Grid.Row="3" Click="ToolModeDown_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="Move down" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonMoveLeft" Grid.Row="4" Click="ToolMoveLeft_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="Move left" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonMoveRight" Grid.Row="5" Click="ToolMoveRight_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="Move right" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonRotateClock" Grid.Row="6" Click="ToolRotateClock_Click"
BorderThickness="2" Margin="2" >
<TextBlock Text="Rotate clock" TextWrapping="Wrap"/>
</Button>
<Button Name="ButtonRotateCounterclock" Grid.Row="7"
Click="ToolRotateCounterclock_Click" BorderThickness="2" Margin="2" >
<TextBlock Text="Rotate counterclock" TextWrapping="Wrap"/>
</Button>
</Grid>
<Border Name="CanvasClipBorderForWindowsOnly" ClipToBounds="True" >
<Canvas Name="MainCanvas" Background="#ffff88" MouseUp="MainCanvas_MouseUp">
<!-- ... -->
</Canvas>
</Border>
</DockPanel>
</Window>
完整的 XAML 代码与 Microsoft® 完全兼容。
与 为 X11 编写 XAML 计算器应用程序 和之前的文章相比,只讨论了增强功能和区别。
DockPanel
将由以下方式定义:
Name
属性定义了类实例的名称,可用于唯一标识该类实例。此属性是推荐的,如果该类实例需要通过 C# 代码访问,则是必需的。Background
属性定义了背景颜色。此属性是可选的。该属性的语法是十六进制值#AARRGGBB
或#RRGGBB
,其中AA
代表 Alpha,RR
代表红色,GG
代表绿色,BB
代表蓝色。目前 Alpha 值尚未实现。DataContext
属性定义了此示例的默认/备用数据上下文,即StaticResource MainViewModel
(它引用Window.Resources
字典)。此属性是可选的。目前DataContext
属性未被评估。
DockPanel
提供子约束 DockPanel.Dock
,用于将子元素定位到其 Top
、Left
、Right
、Bottom
或 Fill
(又称 center)位置。
Border
将由以下方式定义:
Name
属性定义了类实例的名称,可用于唯一标识该类实例。此属性是推荐的,如果该类实例需要通过 C# 代码访问,则是必需的。ClipToBounds
属性定义了裁剪行为。此属性是可选的,但本示例需要它来裁剪包含的画布的内容。
Canvas
将由以下方式定义:
Name
属性定义了类实例的名称,可用于唯一标识该类实例。此属性是推荐的,如果该类实例需要通过 C# 代码访问,则是必需的。Background
属性定义了背景颜色。此属性是可选的。该属性的语法是十六进制值#AARRGGBB
或#RRGGBB
,其中AA
代表 Alpha,RR
代表红色,GG
代表绿色,BB
代表蓝色。目前 Alpha 值尚未实现。MouseDown
属性定义了鼠标按下事件委托。此属性是可选的。目前,委托必须在Window
(代码后端)的类代码内部定义。MouseUp
属性定义了鼠标释放事件委托。此属性是可选的。目前,委托必须在Window
(代码后端)的类代码内部定义。
由于原始 Microsoft 实现的 Canvas
完全不裁剪其内容,因此需要使用裁剪 Border
来防止几何对象(形状)覆盖画布外的控件。相反,X11 的 Canvas
总是进行裁剪,因为它基于一个具有自身 X11 窗口的控件。为了使 Windows 和 X11 的示例代码保持同步,X11 XAML 代码也使用了 Border
包裹 Canvas
。
Canvas
可以包含任何 UIElement
并将其放置在绝对位置。本示例专注于 System.Windows.Shapes.*
子元素。
Canvas
提供子约束 Canvas.Left
来定义子元素的 x 坐标偏移量,以及 Canvas.Top
来定义子元素相对于画布原点(左上角)的 y 坐标偏移量。
现在回到画布内容。这是上面省略了画布内容定义的 XAML 文件摘录。
<!-- Image filled geometry ... -->
<Rectangle Name="ImageRect" Width="278" Height="227" Canvas.Left="57" Canvas.Top="10">
<Rectangle.Fill>
<ImageBrush ImageSource="Bridge.bmp"/>
</Rectangle.Fill>
</Rectangle>
<!-- Simple geometries ... -->
<Line Name="Line0" Stroke="#5566AA" StrokeThickness="2"
X1="0" Y1="0" X2="50" Y2="30" Canvas.Left="10" Canvas.Top="242" />
<Polyline Name="Polyline0" Stroke="#55CC66" StrokeThickness="2"
Fill="#AAAA99" Points="0,0 10,10 20,0 30,10"
Canvas.Left="236" Canvas.Top="304" />
<Polygon Name="Polygon0" Stroke="#996655" StrokeThickness="2"
StrokeDashArray="3" Fill="#AAAA99" Points="0,0 20,20 40,0 60,20"
Canvas.Left="203" Canvas.Top="250" />
<Rectangle Name="Rectangle0" Stroke="#556699" StrokeThickness="2"
StrokeDashArray="3" Fill="#AAAA99" RadiusX="10" RadiusY="10" Width="50" Height="35"
RenderTransformOrigin="2.1,14.467"
Canvas.Left="148" Canvas.Top="242" />
<Ellipse Name="Ellipse0" Stroke="#556699" StrokeThickness="2"
Fill="#AAAA99" Width="50" Height="35" RenderTransformOrigin="2.1,14.467"
Canvas.Left="148" Canvas.Top="280" />
<Path Name="Path0" Stroke="#FF6699" StrokeThickness="2"
Fill="#AAAA99" Data="M 0,0 L 50,25 L 50,75 L 0,50 Z Q 40,0 40,40"
Canvas.Left="284" Canvas.Top="242" />
<!-- More path geometries ... -->
<Path Name="Path1" Fill="#c0dd89" Data="M 50,50 L 150,50 L 150,150 L 50,150 Z">
<Path.RenderTransform>
<TransformGroup>
<RotateTransform CenterX="65" CenterY="65" Angle="45" />
<ScaleTransform ScaleX="0.95" ScaleY="0.95" />
<TranslateTransform X="20" Y="20" />
</TransformGroup>
</Path.RenderTransform>
</Path>
<Path Name="Path2" Fill="#c01730" Data="M 160,160 L 260,160 L 260,260 L 160,260 Z"
Canvas.Left="-74" Canvas.Top="-73">
<Path.RenderTransform>
<TransformGroup>
<MatrixTransform Matrix="0.95,0.1, -0.1,0.95, -20,-20" />
</TransformGroup>
</Path.RenderTransform>
</Path>
完整的 XAML 代码与 Microsoft® 完全兼容。
Line
将由以下方式定义:
Name
属性定义了类实例的名称,可用于唯一标识该类实例。此属性是推荐的,如果该类实例需要通过 C# 代码访问,则是必需的。Stroke
属性定义了描边笔刷。目前只支持SolidColorBrush
。此属性是可选的。SolidColorBrush
由其颜色定义。颜色的语法是十六进制值#AARRGGBB
或#RRGGBB
,其中AA
代表 Alpha,RR
代表红色,GG
代表绿色,BB
代表蓝色。目前 Alpha 值尚未实现。StrokeThickness
属性定义了轮廓的宽度。此属性是可选的。StrokeDashArray
属性定义了轮廓的虚线和间隙的长度。此属性是可选的。X1
属性定义了线的起点 x 坐标。此属性是必需的。Y1
属性定义了线的起点 y 坐标。此属性是必需的。X2
属性定义了线的终点 x 坐标。此属性是必需的。Y2
属性定义了线的终点 y 坐标。此属性是必需的。RenderTransformOrigin
属性定义了应用于RenderTransform
的偏移量。默认情况下没有RenderTransform
。RenderTransformOrigin
定义的偏移量是相对于形状边界的顶部和左侧测量的,并且量是形状边界宽度和高度的比例。典型值范围是 0.0 到 1.0。如果未定义RenderTransformOrigin
,则RenderTransform
将应用于形状边界的中心。这相当于定义RenderTransformOrigin="0.5,0.5"
。
Polyline
将由与 Line
相同的属性 Name
、Stroke
、StrokeThickness
、StrokeDashArray
和 RenderTransformOrigin
定义,并另外由以下属性定义:
Points
属性将折线的插值点定义为多个由空格分隔的 **x,y** 坐标对。此属性是必需的。
Polygon
将由与 Line
相同的属性 Name
、Stroke
、StrokeThickness
、StrokeDashArray
和 RenderTransformOrigin
定义,并另外由以下属性定义:
Points
属性将多边形的插值点定义为多个由空格分隔的 **x,y** 坐标对。此属性是必需的。多边形始终是闭合形状。如果第一个和最后一个插值点的坐标不同,则多边形将自动闭合。Fill
属性定义了填充笔刷。目前只支持SolidColorBrush
和ImageBrush
。此属性是可选的。如果未定义Brush
,则该形状将不会被填充。SolidColorBrush
由其颜色定义。该属性的语法是十六进制值#AARRGGBB
或#RRGGBB
,其中AA
代表 Alpha,RR
代表红色,GG
代表绿色,BB
代表蓝色。目前 Alpha 值尚未实现。ImageBrush
由其图像源定义。ImageBrush
的内联定义不受支持。
Rectangle
将由与 Polygon
相同的属性 Name
、Stroke
、StrokeThickness
、StrokeDashArray
、RenderTransformOrigin
和 Fill
定义,并另外由以下属性定义:
RadiusX
属性定义了矩形圆角的 x 坐标。此属性是可选的。默认值为 0。RadiusY
属性定义了矩形圆角的 y 坐标。此属性是可选的。默认值为 0。Width
属性定义了矩形的 x 尺寸。此属性是必需的。Height
属性定义了矩形的 y 尺寸。此属性是必需的。
Ellipse
将由与 Polygon
相同的属性 Name
、Stroke
、StrokeThickness
、StrokeDashArray
、RenderTransformOrigin
和 Fill
定义,并另外由以下属性定义:
Width
属性定义了椭圆的 x 直径。此属性是必需的。Height
属性定义了椭圆的 y 直径。此属性是必需的。
Path
将由与 Polygon
相同的属性 Name
、Stroke
、StrokeThickness
、StrokeDashArray
、RenderTransformOrigin
和 Fill
定义,并另外由以下属性定义:
Data
属性定义了路径的移动和绘制命令序列。此属性是必需的。目前支持的移动和绘制命令有:M x,y 移动到指定的 x,y 坐标,L x,y 绘制一条线到指定的 x,y 坐标,H x 水平绘制一条线到指定的 x 坐标,V y 垂直绘制一条线到指定的 y 坐标,Q x1,y1 x2,y2 绘制一条二次贝塞尔曲线,其控制点为 x1,y1 坐标,终点为 x2,y2 坐标,C x1,y1 x2,y2 x3,y3 绘制一条三次贝塞尔曲线,其第一个控制点为 x1,y1 坐标,第二个控制点为 x2,y2 坐标,终点为 x3,y3 坐标,以及 Z 绘制一条线到起点(关闭路径)。目前,每个移动和绘制命令都必须以其对应的字母(M、L、H、V、C、Q 或 Z)开头。
由于 RenderTransform
的简写语法定义不受支持,因此 Line.RenderTransform
、Polyline.RenderTransform
、Polygon.RenderTransform
、Rectangle.RenderTransform
、Ellipse.RenderTransform
和 Path.RenderTransform
将始终使用长格式语法和包含的 TransformGroup
定义。
TransformGroup
将由 RotateTransform
、ScaleTransform
、TranslateTransform
和 MatrixTransform
的任意组合定义。
RotateTransform
将由以下方式定义:
CenterX
属性定义了旋转中心的 x 坐标。此属性是可选的。默认值为 0。CenterY
属性定义了旋转中心的 y 坐标。此属性是可选的。默认值为 0。Angle
属性以顺时针度数为单位定义了旋转角度。此属性是必需的。
ScaleTransform
将由以下方式定义:
ScaleX
属性定义了 x 坐标的缩放因子。此属性是可选的。默认值为 0。ScaleY
属性定义了 y 坐标的缩放因子。此属性是可选的。默认值为 0。
TranslateTransform
将由以下方式定义:
X
属性定义了 x 坐标的平移偏移量。此属性是可选的。默认值为 0。Y
属性定义了 y 坐标的平移偏移量。此属性是可选的。默认值为 0。
MatrixTransform
将由以下方式定义:
Matrix
属性以空格分隔的值对的形式定义了矩阵分量,顺序为 m11,m12 m21,m22 offsetX,offsetY。此属性是必需的。
XAML 语法的局限性
X11/Xrw 实现
Stroke
属性目前仅支持SolidColorBrush
。StrokeDashArray
属性目前仅支持等长度的虚线和间隙。Fill
属性目前仅支持SolidColorBrush
和ImageBrush
。ImageBrush
目前仅支持相对于执行程序集的图像源。Canvas
始终裁剪其内容,并且目前仅支持System.Windows.Shapes.*
子元素。Brush
颜色语法目前不支持命名颜色,并且 Alpha 值尚未实现。
Windows 实现
MessageBox
的文本参数不支持标记。
代码隐藏 (MainView.xaml.cs)
主视图对应的 C# 代码文件是 MainView.xaml.cs
。它包含实现几何对象(形状)操作以及菜单和按钮回调所需的所有必要属性和特性。
所有左侧工具栏按钮回调(用于创建新形状)的代码非常相似。创建新 Arc
形状的代码将作为示例显示(因为 Arc
不是内置的 WPF Shape,而是自制的扩展)。
/// <summary>Process the "NewArc" button click event.</summary>
/// <param name="sender">The event source.<see cref="System.Object"/></param>
/// <param name="e">The event data.<see cref="System.Windows.RoutedEventArgs"/></param>
private void ToolNewArc_Click(object sender, System.Windows.RoutedEventArgs e)
{
System.Windows.Shapes.Arc newArc = new System.Windows.Shapes.Arc();
newArc.Center = new Point(_randomizer.Next(60, 210), _randomizer.Next(60, 210));
newArc.RadiusVector = new Vector(_randomizer.Next(10, 50), _randomizer.Next(10, 50));
newArc.StartAngle = _randomizer.NextDouble() * 2 * Math.PI;
newArc.EndAngle = newArc.StartAngle + _randomizer.NextDouble() * 2 * Math.PI;
newArc.Stroke = new SolidColorBrush(RandomColor());
newArc.StrokeThickness = _randomizer.Next(1, 5);
newArc.Fill = RandomBrush();
MainCanvas.Children.Add(newArc);
}
Arc
展示了通过代码创建 System.Windows.Shapes.*
的典型方法 - 使用默认构造函数,逐个设置属性,最后将形状注册到画布。RandomColor()
方法从随机的红色、绿色和蓝色值创建随机颜色。RandomBrush()
方法创建一个随机的 SolidColorBrush
,或在 X11 上创建一个具有随机纹理的随机 ImageBrush
。
接下来的代码显示了 Canvas
的鼠标释放处理程序,该处理程序通过左键单击提供对几何图形(形状)的单一选择。
/// <summary>Process the "Canvas" mouse button up event.</summary>
/// <param name="sender">The event source.<see cref="System.Object"/></param>
/// <param name="e">The event data.<see cref="System.Windows.Input.MouseButtonEventArgs"/></param>
private void MainCanvas_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
System.Windows.Point mousePos = e.GetPosition(MainCanvas);
System.Windows.Media.PointHitTestResult hitTestResult =
(System.Windows.Media.PointHitTestResult)System.Windows.Media.VisualTreeHelper.HitTest
(MainCanvas, new System.Windows.Point(mousePos.X, mousePos.Y));
UIElement selectedUIElement = null;
bool selectionChanged = false;
bool foundSelection = false;
if (hitTestResult != null)
selectedUIElement = (UIElement)hitTestResult.VisualHit;
else
selectedUIElement = null;
for (int index = 0; index < MainCanvas.Children.Count; index++)
{
UIElement uiElement = MainCanvas.Children[index];
if (uiElement is Image)
{
/* Currently not supported. */
}
if (uiElement is Shape)
{
System.Windows.Shapes.Shape shape = uiElement as System.Windows.Shapes.Shape;
if (foundSelection == false && uiElement == selectedUIElement)
{
if (shape.GetSelected() != true)
{
shape.SetOriginalStrokeBush(shape.Stroke);
shape.SetSelected(true);
selectionChanged = true;
shape.Stroke = SelectedControlBorderBrush;
shape.StrokeThickness += 2;
}
foundSelection = true;
}
else
{
if (shape.GetSelected() != false)
{
shape.Stroke = shape.GetOriginalStrokeBush();
shape.SetSelected(false);
selectionChanged = true;
shape.StrokeThickness -= 2;
}
}
}
}
if (selectionChanged != false)
{
MainCanvas.InvalidateVisual();
OnCanvasChildSelectionChanged();
}
}
Canvas
的鼠标释放处理程序利用 VisualTreeHelper.HitTest()
并评估 PointHitTestResult
来确定新选定的形状。随后将释放旧的选择并设置新选择。如果旧选择和新选择不同,则设置 selectionChanged
并调用 OnCanvasChildSelectionChanged()
。此方法根据形状类型更新所有右侧工具栏按钮(用于操作当前所选形状)的 IsEnabled
属性(并非所有形状都支持所有操作)。
为了区分形状是否被选中,使用了 System.Windows.Shapes.Shape
类的扩展属性 SelectedProperty
的访问器 GetSelected()
和 SetSelected()
。
为了高亮显示新选定的形状,将其 Stroke
属性设置为 MainView.xaml.cs
的 SelectedColorBorderBrus
属性,为了取消高亮显示旧选定的形状,将其 Stroke
属性恢复为其原始 Stroke
笔刷。这得到了 System.Windows.Shapes.Shape
类的扩展属性 OriginalStrokeBushProperty
访问器 GetOriginalStrokeBush()
和 SetOriginalStrokeBush()
的支持。
这两个扩展展示了 WPF 依赖项机制的一个巨大优势 - 可以轻松地通过扩展现有类来实现,而无需继承。
代码以示例形式展示了右侧工具栏的形状操作回调。
/// <summary>Process the "ModeUp" button click event.</summary>
/// <param name="sender">The event source.<see cref="System.Object"/></param>
/// <param name="e">The event data.<see cref="System.Windows.RoutedEventArgs"/></param>
private void ToolModeUp_Click(object sender, System.Windows.RoutedEventArgs e)
{
for (int index = 0; index < MainCanvas.Children.Count; index++)
{
UIElement uiElement = MainCanvas.Children[index];
if (uiElement is Image)
{
/* Currently not supported. */
}
if (uiElement is Shape)
{
Shape shape = uiElement as Shape;
if (shape.GetSelected() == true)
{
Matrix m = shape.RenderTransform.Value;
m.Translate(0, -10);
shape.RenderTransform = new MatrixTransform (m);
return;
}
}
}
}
右侧工具栏的所有形状操作回调都通过其扩展属性 SelectedProperty
访问器 GetSelected()
来确定当前选定的形状,并相应地操作其 RenderTransform
。
主视图模型文件上下文
MainWindowViewModel.cs
文件保持初始状态。ViewModel 未向应用程序提供任何功能。
主模型文件上下文
MainModel.cs
文件保持初始状态。Model 未向应用程序提供任何功能。
关注点
这是另一个用于 X11 的 XAML 应用程序,几乎完全兼容 Microsoft®,展示了这种方法的最大优势:100% 兼容的 GUI 定义和节省用于创建 GUI 的代码行数。
而且我发现了另一个优势:WPF 依赖项机制允许通过扩展现有类来实现功能,这些功能可以轻松实现而无需继承。
历史
本文的第一版发布于 2015 年 6 月 16 日。