65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2015年6月13日

CPOL

17分钟阅读

viewsIcon

26328

downloadIcon

1031

目前,所有主流的 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 几何对象/形状),并使用 XAMLRoma 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.RectangleSystem.Windows.Shapes.EllipseSystem.Windows.Shapes.PolylineSystem.Windows.Shapes.PolygonSystem.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,用于将子元素定位到其 TopLeftRightBottomFill(又称 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 的偏移量。默认情况下没有 RenderTransformRenderTransformOrigin 定义的偏移量是相对于形状边界的顶部和左侧测量的,并且量是形状边界宽度和高度的比例。典型值范围是 0.0 到 1.0。如果未定义 RenderTransformOrigin,则 RenderTransform 将应用于形状边界的中心。这相当于定义 RenderTransformOrigin="0.5,0.5"

Polyline 将由与 Line 相同的属性 NameStrokeStrokeThicknessStrokeDashArrayRenderTransformOrigin 定义,并另外由以下属性定义:

  • Points 属性将折线的插值点定义为多个由空格分隔的 **x,y** 坐标对。此属性是必需的。

Polygon 将由与 Line 相同的属性 NameStrokeStrokeThicknessStrokeDashArrayRenderTransformOrigin 定义,并另外由以下属性定义:

  • Points 属性将多边形的插值点定义为多个由空格分隔的 **x,y** 坐标对。此属性是必需的。多边形始终是闭合形状。如果第一个和最后一个插值点的坐标不同,则多边形将自动闭合。
  • Fill 属性定义了填充笔刷。目前只支持 SolidColorBrushImageBrush此属性是可选的。如果未定义 Brush,则该形状将不会被填充。SolidColorBrush 由其颜色定义。该属性的语法是十六进制值 #AARRGGBB#RRGGBB,其中 AA 代表 Alpha,RR 代表红色,GG 代表绿色,BB 代表蓝色。目前 Alpha 值尚未实现。ImageBrush 由其图像源定义。ImageBrush 的内联定义不受支持。

Rectangle 将由与 Polygon 相同的属性 NameStrokeStrokeThicknessStrokeDashArrayRenderTransformOriginFill 定义,并另外由以下属性定义:

  • RadiusX 属性定义了矩形圆角的 x 坐标。此属性是可选的。默认值为 0。
  • RadiusY 属性定义了矩形圆角的 y 坐标。此属性是可选的。默认值为 0。
  • Width 属性定义了矩形的 x 尺寸。此属性是必需的。
  • Height 属性定义了矩形的 y 尺寸。此属性是必需的。

Ellipse 将由与 Polygon 相同的属性 NameStrokeStrokeThicknessStrokeDashArrayRenderTransformOriginFill 定义,并另外由以下属性定义:

  • Width 属性定义了椭圆的 x 直径。此属性是必需的。
  • Height 属性定义了椭圆的 y 直径。此属性是必需的。

Path 将由与 Polygon 相同的属性 NameStrokeStrokeThicknessStrokeDashArrayRenderTransformOriginFill 定义,并另外由以下属性定义:

  • 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 绘制一条线到起点(关闭路径)。目前,每个移动和绘制命令都必须以其对应的字母(MLHVCQZ)开头。

由于 RenderTransform 的简写语法定义不受支持,因此 Line.RenderTransformPolyline.RenderTransformPolygon.RenderTransformRectangle.RenderTransformEllipse.RenderTransformPath.RenderTransform 将始终使用长格式语法和包含的 TransformGroup 定义。

TransformGroup 将由 RotateTransformScaleTransformTranslateTransformMatrixTransform 的任意组合定义。

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 属性目前仅支持 SolidColorBrushImageBrush
  • 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.csSelectedColorBorderBrus 属性,为了取消高亮显示旧选定的形状,将其 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 日。

© . All rights reserved.