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

为 X11 编写 XAML 计算器应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (5投票s)

2015年2月2日

CPOL

15分钟阅读

viewsIcon

24022

downloadIcon

951

目前,所有主流的 Linux/Unix (X11) GUI 应用程序框架(如 GTK+、KDE)都不支持基于 XAML 的应用程序开发。Moonlight 项目(包括 XAML 支持)已于 2012 年 5 月 29 日停止维护。本文介绍了一个具有基本菜单、剪贴板和验证功能的基于 XAML 的应用程序。

下载 XamlCalcApp_X11 V1_32.zip / 下载 XamlCalcApp_X11_V2_32.zip
下载 XamlCalcApp_X11_V1_64.zip / 下载 XamlCalcApp_X11_V2_64.zip
下载 XamlCalcApp_Win7_V1.zip / 下载 XamlCalcApp_Win7_V2.zip

引言

本文是一个案例研究,介绍如何使用 **Roma Widget Set** (Xrw) 编写一个基于 MVVM (Model View ViewModel) 设计模式、使用 XAML 的 X11 应用程序(包含基本菜单、剪贴板、默认验证、按键绑定和自定义验证器功能)。**Roma Widget Set** 是一个零依赖的 X11 GUI 应用程序框架(仅需要免费 Mono 标准安装的程序集和免费 X11 发行版的库;它不特别需要 GNOME、KDE 或商业库),完全用 C# 实现。

本文是继《为 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

  • 海量数据绑定可以提供有用的功能,并且
  • 定义一个零代码后端的应用程序,

本文将演示 XAML 可以实现:

  • 轻松设计简单的菜单,
  • 实现剪贴板文本交换,以及
  • 通过使用内置转换器异常显示输入错误的数据验证。

本文的更新版本也将展示

  • 自定义绑定验证和
  • 键盘快捷键绑定。

使用代码

该示例应用程序是使用 Mono Develop 2.4.1、Mono 2.8.1、OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面编写的。移植到任何更早或更新的版本应该都不是问题。该示例应用程序的解决方案包含两个项目(提供完整的源代码供下载)。

  • XamlCalcApp 包含示例应用程序的源代码。
  • 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.8,它定义了 libX11.so 的函数原型、结构和类型。该程序集是为《使用 Mono Develop 编程 Xlib - 第一部分:底层(概念验证)》项目开发的,并在《编程 Roma Widget Set (C# X11) - 一个零依赖的 GUI 应用程序框架 - 基础》项目中得到了改进。

GUI 框架基于 **Xrw** 程序集版本 0.8,它定义了小部件/小工具及其在 XAML 代码中使用的包装器类(应尽可能接近 Microsoft® 原始版本)。该程序集是在《编程 Roma Widget Set (C# X11) - 一个零依赖的 GUI 应用程序框架 - 基础》项目中开发的。

建议:要使用 MonoDevelop 的类库文档快捷键 (F1),必须安装“mono-tools”软件包。

所有图像都显示了相同状态下的示例应用程序:数字 12 被记住(M),应该与下一个值相乘(12 *),但输入的数字(#4)是错误的 - 无法转换为数字。这由 TextBox 周围的红色边框指示。

第一张图片显示了示例应用程序在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面上的样子。

第二张图片显示了示例应用程序在 OPEN SUSE 12.3 Linux 64 位 DE 和 Xfce 上的样子。

第三张图片显示了示例应用程序在 Windows® 7 64 位版本上的样子。

该示例应用程序提供了一个简单但功能齐全的计算器。它包含(从上到下):一个带有单个菜单项 **File** 和 **?** 的简单菜单栏、一个接受键盘输入的 **TextBox**、两个显示内存状态(**M** 表示已记住数字)和先前捕获的运算符(例如 **12 ***)的 **TextBlock**,以及多行多列按钮,包括一个跨行按钮和一个跨列按钮。

所有按钮都已在其 `Click` 属性上注册了回调以实现功能(对于这个简单的项目,`Click` 属性可以减少代码量,并且与《为 X11 编写 XAML 功能区应用程序》一文中讨论的 `Command` 属性和“RelayCommand”方法相比没有缺点)。

TextBox 会自动检查输入中的非数字字符,如果无法将输入转换为数字,它会在 TextBox 周围显示红色边框(如图像所示)。为了保持示例应用程序的简单性并演示在 X11 和 Windows 上使用内置转换器异常进行数据验证,没有自动更正输入错误。

该示例应用程序基于 CodeProject 文章《Calc# - Gtk# 入门介绍》中的思想。但本文的目的是介绍 XAML,而不是 Gtk#。

逐步演示

项目设置应用程序文件上下文(主题除外)和预处理器代码生成步骤与《为 X11 编写 XAML 对话框应用程序》中的完全相同。如果需要从头开始创建新解决方案,请参阅该文章。

主视图文件上下文

XAML (MainView.xaml)

首先是省略了按钮定义的 XAML 文件。

<Window         x:Class="XamlCalcApp.MainView"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:src="clr-namespace:XamlCalcApp"
                xmlns:validators="clr-namespace:XamlCalcApp"
                Name="MainWindow" Title="XAML calculator application"
                Width="300" Height="320" 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>
        <src:MainWindowViewModel x:Key="MainViewModel" />
        <!-- MainWindowViewModel : ViewModelCore<T>.ctr() automatically registeres
             itself as the initial Window.DataContext -->
    </Window.Resources>
    <Grid Name="MainGrid" Background="#E8E8E8" DataContext="{StaticResource MainViewModel}">
        <Grid.Resources>
            <!-- <src:MainViewModel x:Key="mainViewDataSource" /> -->
        </Grid.Resources>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.1*"/><!-- Left padding. -->
            <ColumnDefinition Width="1.0*"/>
            <ColumnDefinition Width="0.2*"/><!-- Col 1-2 padding. -->
            <ColumnDefinition Width="1.0*"/>
            <ColumnDefinition Width="0.2*"/><!-- Col 2-3 padding. -->
            <ColumnDefinition Width="1.0*"/>
            <ColumnDefinition Width="0.2*"/><!-- Col 3-4 padding. -->
            <ColumnDefinition Width="1.0*"/>
            <ColumnDefinition Width="0.2*"/><!-- Col 4-5 padding. -->
            <ColumnDefinition Width="1.0*"/>
            <ColumnDefinition Width="0.1*"/><!-- Right padding. -->
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="22"/><!-- Menu bar. -->
            <RowDefinition Height="32"/><!-- Editor bar. -->
            <RowDefinition Height="18"/><!-- Info bar. -->
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="0.8*"/>
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="1.0*"/>
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="1.0*"/>
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="1.0*"/>
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="1.0*"/>
            <RowDefinition Height="0.2*"/><!-- Row padding. -->
            <RowDefinition Height="1.0*"/>
            <RowDefinition Height="0.1*"/><!-- Bottom padding. -->
        </Grid.RowDefinitions>
        <Menu Name="MainMenu" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="9" >
            <MenuItem Name="MenuItemFile"  Header="File">
                <MenuItem Name="MenuItemFileCopy"   Header="Copy"
                          Click="MenuItemFileCopy_Click" />
                <MenuItem Name="MenuItemFilePaste"  Header="Paste"
                          Click="MenuItemFilePaste_Click" />
                <Separator/>
                <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>
        <!-- Short hand syntax. Doesn't support custom validation.
        <TextBox Name="Entry" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="9"
                 TextAlignment="Right" FontSize="18" BorderThickness="2"  BorderBrush="#CCCCCC"
                 Text="{Binding Path=CurrentValue, Mode=TwoWay,
                 UpdateSourceTrigger=PropertyChanged}" />
        -->
        <!-- Long hand syntax. -->
        <TextBox Name="Entry" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="9"
                 TextAlignment="Right" FontSize="18" BorderThickness="2" BorderBrush="#CCCCCC">
            <TextBox.Text >
                <Binding Path="CurrentValue" Mode="TwoWay"
                   UpdateSourceTrigger="PropertyChanged" />
            </TextBox.Text>
        </TextBox>
        <!-- Validation rule implementation for version 2 of XamlCalcApp.
        <TextBox Name="Entry" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="9"
                 TextAlignment="Right" FontSize="18" BorderThickness="2" BorderBrush="#CCCCCC">
          <TextBox.Text >
            <Binding Path="CurrentValue" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">                   
              <Binding.ValidationRules>
                  <validators:TextIsFloatingNumberValidationRule
                              ErrorMessage="The input must be a valid floating point number." />
              </Binding.ValidationRules>
            </Binding>
          </TextBox.Text>
        </TextBox>
        -->
        <TextBlock Name="Memory"           Grid.Row="2"  Grid.Column="1" Text="" />
        <TextBlock Name="Info"             Grid.Row="2"  Grid.Column="3" Text=""
                   Grid.ColumnSpan="7"     TextAlignment="Right" />

...

    </Grid>
</Window>

完整的 XAML 代码与 Microsoft® 完全兼容。

与《为 X11 编写具有大量数据绑定和零代码的 XAML 应用程序》及之前的文章相比,本文将只讨论增强功能和差异。

Application 将(除已知功能外)通过以下方式定义:

  • `xmlns:validators` 属性定义了自定义验证器类(通常派生自 `ValidationRule` 抽象类)使用的命名空间名称。*此属性已添加,用于与自定义 `TextIsFloatingNumberValidationRule` 类结合进行一些验证规则测试。即使 `TextIsFloatingNumberValidationRule` 类由示例项目提供,此属性目前在 X11 上尚未完全实现*。

Menu 将通过以下方式定义:

  • Name 属性定义了类实例的名称,可用于唯一标识该类实例。此属性是推荐的,如果该类实例需要通过 C# 代码访问,则是必需的。
  • `Grid.Column` 属性定义了控件在网格中应放置的零基列索引。默认值为 0。*对于网格子项,推荐使用此属性,但对于放置在网格中非第 0 列的控件,则是必需的*。索引不得超过可用网格列数。
  • `Grid.Row` 属性定义了控件在网格中应放置的零基行索引。默认值为 0。*对于网格子项,推荐使用此属性,但对于放置在网格中非第 0 行的控件,则是必需的*。索引不得超过可用网格行数。
  • `Grid.ColumnSpan` 属性定义了控件在网格中应跨越的列数。*对于网格子项,此属性是可选的,但对于跨越多列的控件,则是必需的*。省略此属性或将其设置为“0”或“1”是等效的。跨度不得超过可用网格列数。
  • `Grid.RowSpan` 属性定义了控件在网格中应跨越的行数。*对于网格子项,此属性是可选的,但对于跨越多行的控件,则是必需的*。省略此属性或将其设置为“0”或“1”是等效的。跨度不得超过可用网格行数。

Menu 控件包含 MenuItem 控件作为子项,它们将通过以下方式定义:

  • Name 属性定义了类实例的名称,可用于唯一标识该类实例。此属性是推荐的,如果该类实例需要通过 C# 代码访问,则是必需的。
  • `Header` 属性定义了菜单项的显示文本。*此属性是推荐的*。目前不支持热键标记(例如,`"_File"` 表示 [Alt]+[f] 热键)。
  • `Click` 属性定义了点击事件委托。*此属性是可选的*。目前,委托必须在 `Window`(代码后端)的类代码中定义,该控件是其子/孙子项。

TextBox 将(除已知功能外)通过以下方式定义:

  • `TextAlignment` 属性定义了要显示的文本的水平对齐方式。*此属性是可选的*。
  • `FontSize` 属性定义了要显示的文本的字体大小。*此属性是可选的*。
  • `BorderBrush` 属性定义了控件边框的颜色。*此属性是可选的*。仅当 `BorderThickness` 设置为大于 0 的值时,边框才可见。目前支持命名颜色和 HTML 颜色名称(“#RRGGBB”)。
  • `Text` 属性定义了要显示的文本。*此属性是推荐的*。属性语法可以是常量值 `"<value>"`,也可以是动态资源的 `{Binding <path>, ElementName=<control name>, Mode=<mode>, UpdateSourceTrigger=<Trigger>}`(简写)。动态数据源必须实现 `INotifyPropertyChanged`。现在,`Mode` 支持 `OneWay`、`TwoWay` 和 `OneWayToSource` 绑定模式。`UpdateSourceTrigger` 目前支持 `Default`、`LostFocus` 和 `PropertyChanged`。除了动态资源的简写形式外,现在还支持显式形式:`<TextBox ...><TextBox.Text ><Binding <BindingExpression> /></TextBox.Text></TextBox>`,其中 `<BindingExpression>` 可以由 `Path="<path>" Mode="<mode>" UpdateSourceTrigger="<trigger>"` 组成。

MainView.xaml 中包含三种替代的 `TextBox` 定义。第一个被注释掉了,它包含一个功能齐全的简写表示法(但简写表示法不支持自定义验证)。第二个是活动的,包含一个功能齐全的显式表示法。显式表示法的支持是 **Xrw** 程序集版本 0.8 的新功能。第三个被注释掉了,它包含一个实验性的显式表示法,包括自定义验证器。此功能在 XamlCalcApp 的第一个版本中尚未完全实现。(通过示例应用程序的 Windows 版本证明,对于计算器示例应用程序第一个版本的特定实现,它没有帮助)。

TextBlock(类似于 `Label`,但支持自动换行)将(除已知功能外)通过以下方式定义:

  • `TextAlignment` 属性定义了要显示的文本的水平对齐方式。*此属性是可选的*。

现在回到 `Button`。这是上面省略了按钮定义的 XAML 文件摘录。

...

    <Button Name="MemoryClear"               Grid.Row="4"  Grid.Column="1"
            Click="MemoryClear_Click"        Content="MC" />
    <Button Name="MemoryRecall"              Grid.Row="4"  Grid.Column="3"
            Click="MemoryRecall_Click"       Content="MR" />
    <Button Name="MemorySave"                Grid.Row="4"  Grid.Column="5"
            Click="MemorySave_Click"         Content="MS" />
    <Button Name="MemoryAdd"                 Grid.Row="4"  Grid.Column="7"
            Click="MemoryAdd_Click"          Content="M+" />
    <Button Name="MemorySubstract"           Grid.Row="4"  Grid.Column="9"
            Click="MemorySubstract_Click"    Content="M-" />
        
    <Button Name="EditDeleteLast"            Grid.Row="6"  Grid.Column="1"
            Click="EditDeleteLast_Click"     Content="←" />
    <Button Name="EditClearEntry"            Grid.Row="6"  Grid.Column="3"
            Click="EditClearEntry_Click"     Content="CE" />
    <Button Name="EditClearAll"              Grid.Row="6"  Grid.Column="5"
            Click="EditClearAll_Click"       Content="C" />
    <Button Name="CalculateDivide"           Grid.Row="6"  Grid.Column="7"
            Click="CalculateDivide_Click"    Content="/" />
    <Button Name="CalculateSqrt"             Grid.Row="6"  Grid.Column="9"
            Click="CalculateSqrt_Click"      Content="x ¹/²" />
        
    <Button Name="NumberSeven"               Grid.Row="8"  Grid.Column="1"
            Click="NumberSeven_Click"      ><TextBlock Text="7"  FontWeight="Bold" /> </Button>
    <Button Name="NumberEight"               Grid.Row="8"  Grid.Column="3"
            Click="NumberEight_Click"      ><TextBlock Text="8"  FontWeight="Bold" /> </Button>
    <Button Name="NumberNine"                Grid.Row="8"  Grid.Column="5"
            Click="NumberNine_Click"       ><TextBlock Text="9"  FontWeight="Bold" /> </Button>
    <Button Name="CalculateMultiply"         Grid.Row="8"  Grid.Column="7"
            Click="CalculateMultiply_Click"  Content="*" />
    <Button Name="CalculateSquare"           Grid.Row="8"  Grid.Column="9"
            Click="CalculateSquare_Click"    Content="x ²" />
        
    <Button Name="NumberFour"                 Grid.Row="10" Grid.Column="1"
            Click="NumberFour_Click"        ><TextBlock Text="4"  FontWeight="Bold" /> </Button>
    <Button Name="NumberFife"                 Grid.Row="10" Grid.Column="3"
            Click="NumberFife_Click"        ><TextBlock Text="5"  FontWeight="Bold" /> </Button>
    <Button Name="NumberSix"                  Grid.Row="10" Grid.Column="5"
            Click="NumberSix_Click"         ><TextBlock Text="6"  FontWeight="Bold" /> </Button>
    <Button Name="CalculateSubtract"          Grid.Row="10" Grid.Column="7"
            Click="CalculateSubtract_Click"   Content="-" />
    <Button Name="CalculateReciprocal"        Grid.Row="10" Grid.Column="9"
            Click="CalculateReciprocal_Click" Content="1/x" />
        
    <Button Name="NumberOne"                  Grid.Row="12" Grid.Column="1"
            Click="NumberOne_Click"         ><TextBlock Text="1"  FontWeight="Bold" /> </Button>
    <Button Name="NumberTwo"                  Grid.Row="12" Grid.Column="3"
            Click="NumberTwo_Click"         ><TextBlock Text="2"  FontWeight="Bold" /> </Button>
    <Button Name="NumberThree"                Grid.Row="12" Grid.Column="5"
            Click="NumberThree_Click"       ><TextBlock Text="3"  FontWeight="Bold" /> </Button>
    <Button Name="CalculateAdd"               Grid.Row="12" Grid.Column="7"
            Click="CalculateAdd_Click"        Content="+" />
    <Button Name="CalculateResult"            Grid.Row="12" Grid.Column="9"
            Click="CalculateResult_Click"     ontent="=" Grid.RowSpan="3" />
        
    <Button Name="NumberZero"                 Grid.Row="14" Grid.Column="1" Grid.ColumnSpan="3"
            Click="NumberZero_Click"        ><TextBlock Text="0"  FontWeight="Bold" /> </Button>
    <Button Name="NumberDecimalDot"           Grid.Row="14" Grid.Column="5"
            Click="NumberDecimalDot_Click"  ><TextBlock Text="."  FontWeight="Bold" /> </Button>
    <Button Name="CalculateNegate"            Grid.Row="14" Grid.Column="7"
             Click="CalculateNegate_Click"    Content="+/-" />
...

在第一个 **Xrw** XAML 版本中,`Button` 控件语法就已经包含了拥有 `TextBox` 子控件的可能性。但内部 `TextBox` 子控件没有被表示为 `TextBox` 控件,因为 `Button` 控件是基于 `XrwButton` 实现的。而是将 `TextBox` 子控件的 `Text` 属性内容赋给了 `Button` 控件的 `Content` 属性内容。现在,`Button` 控件总是包含一个 `TextBox` 子控件,因为 `Button` 控件是基于 `XrwFrame` 实现的。如果在 XAML 文件中定义了显式的 `TextBox` 子控件,则默认的 `TextBox` 子控件将被显式定义替换。

XAML 语法的局限性

X11/Xrw 实现

  • MenuItem 控件目前不支持热键标记(例如,`"_File"` 表示 [Alt]+[f] 热键)。
  • MenuItem 控件的嵌套深度目前限制为 2。
  • `Binding` 语法目前不支持绑定模式 `Mode=OneTime` 和触发器 `UpdateSourceTrigger=Explicit`。
  • `Binding` 语法不支持自定义验证器(在本文第一个版本中 - 第二个版本支持此功能)。

XamlCalcApp 的第二个 XAML 版本 (MainView.xaml)

XAML 文件已包含按键绑定。

    ...
</Window.Resources>
<Window.InputBindings>
    <KeyBinding Command="{Binding NumberZero}" Key="NumPad0"/>
    <KeyBinding Command="{Binding NumberZero}" Key="D0"/>
    <KeyBinding Command="{Binding NumberOne}" Key="NumPad1"/>
    <KeyBinding Command="{Binding NumberOne}" Key="D1"/>
    <KeyBinding Command="{Binding NumberTwo}" Key="NumPad2"/>
    <KeyBinding Command="{Binding NumberTwo}" Key="D2"/>
    <KeyBinding Command="{Binding NumberThree}" Key="NumPad3"/>
    <KeyBinding Command="{Binding NumberThree}" Key="D3"/>
    <KeyBinding Command="{Binding NumberFour}" Key="NumPad4"/>
    <KeyBinding Command="{Binding NumberFour}" Key="D4"/>
    <KeyBinding Command="{Binding NumberFife}" Key="NumPad5"/>
    <KeyBinding Command="{Binding NumberFife}" Key="D5"/>
    <KeyBinding Command="{Binding NumberSix}" Key="NumPad6"/>
    <KeyBinding Command="{Binding NumberSix}" Key="D6"/>
    <KeyBinding Command="{Binding NumberSeven}" Key="NumPad7"/>
    <KeyBinding Command="{Binding NumberSeven}" Key="D7"/>
    <KeyBinding Command="{Binding NumberEight}" Key="NumPad8"/>
    <KeyBinding Command="{Binding NumberEight}" Key="D8"/>
    <KeyBinding Command="{Binding NumberNine}" Key="NumPad9"/>
    <KeyBinding Command="{Binding NumberNine}" Key="D9"/>
    <!-- There is no Key definition for '.' or ','! Only the numerical pad decimal dot works. -->
    <KeyBinding Command="{Binding NumberDecimalDot}" Key="Decimal"/>
    <KeyBinding Command="{Binding CalculateDivide}" Key="Divide"/>
    <KeyBinding Command="{Binding CalculateDivide}" Key="D7" Modifiers="Shift"/> <!-- GERMAN
        KeyBoard! -->
    <KeyBinding Command="{Binding CalculateMultiply}" Key="Multiply"/>
    <KeyBinding Command="{Binding CalculateMultiply}" Key="OemPlus" Modifiers="Shift"/> <!--
        GERMAN KeyBoard! -->
    <KeyBinding Command="{Binding CalculateSubtract}" Key="Subtract"/>
    <KeyBinding Command="{Binding CalculateSubtract}" Key="OemMinus"/>
    <KeyBinding Command="{Binding CalculateAdd}" Key="Add"/>
    <KeyBinding Command="{Binding CalculateAdd}" Key="OemPlus"/>
    <KeyBinding Command="{Binding CalculateResult}" Key="D0" Modifiers="Shift"/> <!-- GERMAN
        KeyBoard! -->
</Window.InputBindings>
<Grid ...

Window.InputBindings 节点包含 KeyBinding 节点,它们将通过以下方式定义:

  • `Command` 属性定义要执行的“RelayCommand”。*此属性是必需的*。属性语法为 `{Binding <path>}`。
  • `Key=`<key> 定义了 `System.Windows.Input.Key` 枚举的任何值。*此语句是必需的*。
  • `[Modifiers=<modifiers>]` 定义了 `System.Windows.Input.ModifierKeys` 枚举的任何值。*此语句是可选的*。默认/回退值为 `System.Windows.Input.ModifierKey.None`。

XAML 语法的局限性

X11/Xrw 实现

  • `KeyBinding` 的 `Command` 目前仅支持当前 `DataContext` 的 `ICommand` 属性。
  • `KeyBinding` 目前不支持 `CommandParameter` 和 `CommandTarget`。
  • `KeyBinding` 的 `Key` 需要对键码进行本地化。

一些期望的按键绑定无法定义,例如,没有 `System.Windows.Input.Key` 值用于“.”或“,”。某些按键绑定在 GERMAN 键盘之外将无法工作。这是因为:

  • 键盘布局和
  • `System.Windows.Input.KeyConverter` 的 `KeyToKeySym()` 实现。

为了让 ENGLISH 键盘完美工作,可能需要更改/扩展两者。

代码隐藏 (MainView.xaml.cs)

主视图的对应 C# 代码文件是 `MainView.xaml.cs`。它包含实现值计算以及所有点击处理程序代码所需的所有结构和属性。

/// <summary>The main window of the application. This class must be derived from XrwXAML.Window.
/// It must be a partial class.
/// The second part of the class will be autogenerated and named '*.generated.cs'.</summary>
public partial class MainView : XrwXAML.Window, IView
{
    /// <summary>Provide the possible actions, that are to apply next.</summary>
    private enum   SecondArgumentAction
    {
        /// <summary>No specific action is to apply.</summary>
        None,
        /// <summary>Add a second argument to the last value.</summary>
        Add,
        /// <summary>Subtract a second argument from the last value.</summary>
        Substract,
        /// <summary>Multiply a second argument with the last value.</summary>
        Multiply,
        /// <summary>Divide the last value by second argument.</summary>
        Divide,
        /// <summary>Clear the current entry.</summary>
        Clear
    }

    /// <summary>Store a self-reference, that enables other classes to access this.</summary>
    private static MainView      _instance = null;

    /// <summary>Define the currently selected action.</summary>
    private SecondArgumentAction _operator = SecondArgumentAction.None;

    /// <summary>The last value.</summary>
    private double               _value = 0;

    /// <summary>The currently memorized value.</summary>
    private double               _memValue = 0;

    /// <summary>The language specific decimal delimiter.</summary>
    private string               _decimalDelimiter = 
        CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;

    /// <summary>The invariant culture number format.
    /// The number parsing relies on invariant culture.</summary>
    private NumberFormatInfo     _invariantNF = CultureInfo.InvariantCulture.NumberFormat;

    /// <summary>The default constructor.</summary>
    public MainView ()
        : base (-1, -1)
    {
        _instance = this;
        InitializeComponent ();
        // will be called after construction by generated code!
    }

    ...

    /// <summary>Process the "File.Copy" button click event.</summary>
    /// <param name="sender">The event source.<see cref="System.Object"/></param>
    /// <param name="e">The event data.<see cref="RoutedEventArgs"/></param>
    private void MenuItemFileCopy_Click(object sender, RoutedEventArgs e)   
    {   
        Clipboard.SetText (Entry.Text);
    }

    /// <summary>Process the "File.Paste" button click event.</summary>
    /// <param name="sender">The event source.<see cref="System.Object"/></param>
    /// <param name="e">The event data.<see cref="RoutedEventArgs"/></param>
    private void MenuItemFilePaste_Click(object sender, RoutedEventArgs e)   
    {   
        // Besause of the asynchronous processing of clipboard data(inter-application
        // message processing) a delegate to inject the result must be provided.
        Clipboard.GetText (this.ProcessClipboardPasteToEntry);
    }

    ...
}

代码块不包含所有点击处理程序 - 它们主要是重复的。尽管如此,有一件事应该指出:剪贴板文本传输。

`System.Windows` 静态类 `Clipboard` 包含 `SetText()` 和 `GetText()` 方法,它们在 **Xrw** 版本 0.8 中也有等效项。但由于 Windows 的桌面窗口管理器与 X11 窗口管理器之间的单体设计存在很大差异,因此无法完全实现与 Windows 兼容的 `GetText()` 方法(即无参)。`GetText()` 方法需要一个委托来处理异步提供的剪贴板结果。示例应用程序实现了 `ProcessClipboardPasteToEntry()` 来实现此目的(请参阅上面示例应用程序 X11 版本实现的源代码)。

示例应用程序的 Windows 版本实现 `MenuItemFilePaste_Click()` 如下:

    /// <summary>Process the "File.Paste" button click event.</summary>
    /// <param name="sender">The event source.<see cref="System.Object"/></param>
    /// <param name="e">The event data.<see cref="RoutedEventArgs"/></param>
    private void MenuItemFilePaste_Click(object sender, RoutedEventArgs e)
    {
        string rawPastable = Clipboard.GetText();
        ProcessClipboardPasteToEntry(rawPastable);
    }

示例应用程序 X11 版本的 `ProcessClipboardPasteToEntry()` 委托和 Windows 版本的 `ProcessClipboardPasteToEntry()` 方法共享完全相同的代码。

    /// <summary>Handle the ClipboardGetResult event.</summary>
    /// <param name="result">The clipboard get text result.<see cref="System.Object"/></param>
    private void ProcessClipboardPasteToEntry (object result)
    {
        if (result != null)
        {
            string rawPastable   = result.ToString ();
            string cleanPastable = "";

            for (int charIndex = 0; charIndex < rawPastable.Length; charIndex++)
            {
                if (char.IsDigit (rawPastable[charIndex]))
                    cleanPastable += rawPastable[charIndex];
                else
                {    string s = rawPastable.Substring (charIndex, 1);
                    if (_decimalDelimiter == s || "." == s)
                        cleanPastable += _decimalDelimiter;
                    else
                        break;
                }
            }
        
            if (!string.IsNullOrEmpty (cleanPastable))
            {
                double d = 0;
                if (double.TryParse (cleanPastable, out d) == true)
                {
                    Entry.Text = d.ToString ();
                }
            }
        }
    }

主视图模型文件上下文

除了初始状态外,`MainWindowViewModel.cs` 文件仅包含 `CurrentValue` 属性,`TextBox` **Entry** 绑定到该属性。

...

    #region Attributes
    
    ...
    
    /// <summary>The currently entered value.</summary>
    private string        _currentValue = 0;
    
    #endregion

...

    #region Properties
    
    ...

    /// <summary>Get or set the currently entered value.</summary>
    public string CurrentValue
    {    get
        {    return _currentValue;    }
        set
        {    if (_currentValue == value)
                return;
            
            _currentValue = value;
            RaisePropertyChanged("CurrentValue");
        }
    }
    
    #endregion

....

`CurrentValue` 属性与 `TextBox` **Entry** 的 `Text` 属性之间的绑定分三步完成:

  1. 静态资源 `MainViewModel` 通过 XML 节点 `` 绑定到 `MainWindowViewModel` 实例。
  2. Grid 将静态资源 `MainViewModel` 连接到其 `DataContext` 属性。
  3. TextBox *Entry* 将 `DataContext` 的 `CurrentValue` 属性绑定到其 `Text` 属性。
...

    <Window.Resources>
        <src:MainWindowViewModel x:Key="MainViewModel" />
    </Window.Resources>

...

    <Grid Name="MainGrid" Background="#E8E8E8" DataContext="{StaticResource MainViewModel}">

...

        <TextBox Name="Entry" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="9"
                 TextAlignment="Right" FontSize="18" BorderThickness="2" BorderBrush="#CCCCCC">
            <TextBox.Text >
                <Binding Path="CurrentValue" Mode="TwoWay"
                    UpdateSourceTrigger="PropertyChanged" />
            </TextBox.Text>
        </TextBox>

...

    </Grid>

XamlCalcApp 的第一个版本

绑定到 `MainWindowViewModel` 的 `CurrentValue` 属性没有特殊的关联功能。但由于它是 `System.Double` 类型,而 `TextBox` ***Entry*** 的 `Text` 属性是 `System.String` 类型,因此绑定需要自动类型转换。类型转换失败会抛出异常。对于示例应用程序图像中显示的 `TextBox` ***Entry*** 的 `Text` "#4",异常是:

System.Windows.Data Error: 7 : ConvertBack 不能将值 '#4'(类型 'String')转换为。BindingExpression:Path=CurrentValue; DataItem='MainWindowViewModel' (HashCode=44944057); target element is 'TextBox' (Name='Entry'); target property is 'Text' (type 'String') ...

此异常被数据绑定捕获,并导致 TextBox 周围出现红色边框,指示输入错误。目前没有自动更正错误输入的机制,以保持示例应用程序的简单性并演示数据验证。

下一项功能

接下来可以实现的功能是:

  • 集成到 `MainWindowViewModel` 的 `CurrentValue` 属性 setter 中的内置自动更正;以及
  • 高级键盘支持,模拟 [0]...[9]、[.]、[+]、[-]、[*]、[/] 和 [=] 的按钮按下事件。

XamlCalcApp 的第二个版本

`CurrentValue` 属性定义为 `System.Double` 数据类型有一个很大的缺点:无法输入浮点数。该示例应用程序的第二个版本将 `CurrentValue` 属性数据类型更改为 `System.String`。但是,现在不再需要自动类型转换(`TextBox` ***Entry*** 和 `MainWindowViewModel` 属性都为 `System.String` 类型),输入错误也不会再抛出 *System.Windows.Data Error* 异常(基于此,`TextBox` ***Entry*** 会通过 TextBox 周围的红色边框来指示错误)。

现在,有一个验证规则绑定到 `TextBox` ***Entry***,并且实现了自定义验证器。

<TextBox Name="Entry" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="9"
         TextAlignment="Right" FontSize="18" BorderThickness="2" BorderBrush="#CCCCCC">
    <TextBox.Text >
        <Binding Path="CurrentValue" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">  
            <Binding.ValidationRules>
                <validators:TextIsFloatingNumberValidationRule
                            ErrorMessage="The input must be a valid floating point number." />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

这是验证器类。错误消息文本通过 XAML 代码中的 `ErrorMessage` 属性注入。

/// <summary>Implement a validation rule for character input.</summary>
public class TextIsFloatingNumberValidationRule : ValidationRule
{
    /// <summary>The message to display in case of an error.</summary>
    private string _errorMessage;

    /// <summary>Create a new instance of the TextIsFloatingNumberValidationRule class.</summary>
    public TextIsFloatingNumberValidationRule ()
    {
    }

    /// <summary>Get or set the message to display in case of an error.</summary>
    public string ErrorMessage
    {
        get { return _errorMessage; }
        set { _errorMessage = value; }
    }

    /// <summary>Perform validation checks on a value.</summary>
    /// <param name="value">The value from the binding target
    /// to check.<see cref="System.Object"/></param>
    /// <param name="cultureInfo">The culture to use in this
    /// rule.<see cref="CultureInfo"/></param>
    /// <returns>A System.Windows.Controls.ValidationResult
    /// object.<see cref="ValidationResult"/></returns>
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (cultureInfo == null)
            cultureInfo = CultureInfo.CurrentCulture;

        string localDecimalSeparator =
           cultureInfo.NumberFormat.NumberDecimalSeparator;
        string invarDecimalSeparator =
           CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator;
        
        string inputString = (value ?? string.Empty).ToString();
        for (int charIndex = 0; charIndex < inputString.Length; charIndex++)
        {
            char c = inputString[charIndex];
            if (!char.IsNumber(c) &&
               localDecimalSeparator[0] != c &&
               invarDecimalSeparator[0] != c)
                return new ValidationResult(false, this.ErrorMessage);
        }
        return new ValidationResult(true, null);
    }
}

这为示例应用程序的第二个版本提供了相同的输入错误通知功能。

主模型文件上下文

MainModel.cs 文件保持初始状态。Model 未向应用程序提供任何功能。

关注点

为 X11 创建这个几乎完全兼容 Microsoft® 的 XAML 应用程序是一项有趣的工作。我参与 XAML 在 X11 和 Windows 上的并行开发的时间越长(这是第四个案例研究),我就越确信 XAML 的价值。这尤其适用于 100% 兼容的 GUI 定义以及创建 GUI 所节省的代码行。

XAML 所需的许多静态对象和属性(例如,大量的 `System.Windows.DependencyProperty` 实例)等负面因素正逐渐淡出视线。

历史

本文第一个版本写于 2015 年 2 月 2 日。

这是第二个版本,于 2015 年 2 月 16 日修订并扩展。

© . All rights reserved.