为 X11 编写 XAML 功能区应用程序





5.00/5 (7投票s)
目前,主要的 Linux/Unix (X11) GUI 应用程序框架 (GTK+, KDE) 都不支持基于 XAML 的应用程序开发。Moonlight 项目(包括 XAML 支持)于2012年5月29日被废弃。本文介绍了如何在 C# 中使用 Roma Widget Set (Xrw) 创建基于 XAML 的 Ribbon 应用程序。
引言
本文是一个实践教程,介绍了如何使用 Roma Widget Set (Xrw) 在 C# 中编写基于 MVVM(模型-视图-视图模型)设计模式的 X11 ribbon 命令界面应用程序(一个稍微复杂一点的 GUI)。Roma Widget Set 是一个零依赖的 X11 GUI 应用程序框架(它只需要免费 Mono 标准安装的程序集和免费 X11 发行版的库;它不需要 GNOME、KDE 或商业库),并且完全用 C# 实现。
本文延续了 为 X11 编写 XAML 对话框应用程序的工作。据我所知,这是在 Moonlight 被废弃后,首次尝试将 XAML 用于 X11 应用程序开发(利用 Xrw)。
Roma Widget Set 和 XAML 实现都不完整。这个示例应用程序旨在作为一个更复杂的“概念验证”,检查是否以及如何使用 XAML 创建基于 MVVM 设计模式的 X11 应用程序。
由于第二次尝试将 XAML 用于 X11 应用程序开发已经成功,因此接下来肯定会有更多关于使用 Roma Widget Set 在 X11 上使用 XAML 的文章。
背景
使用 XAML 进行 X11 应用程序开发的动机和通用概念已在为 X11 编写 XAML 对话框应用程序一文中解释。
焦点
第一篇文章演示了 XAML 可以实现:
- 定义一个包含一些控件的窗口,并且
- 将点击事件连接到按钮上,
本文将演示 XAML 可以实现:
- 定义一个包含 Ribbon 命令界面的窗口,
- 定义静态窗口资源(本示例有一个资源转换器和一个 ModelView),
- 将 ModelView 作为静态资源分配给控件的数据上下文,
- 将资源转换器作为静态资源应用于控件的属性,
- 通过“RelayCommand”方法将命令绑定到按钮,
- 将控件属性绑定到数据上下文,并且
- 通过 `INotifyPropertyChanged` 接口更新控件。
使用代码
示例应用程序使用 Mono Develop 2.4.1 在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面环境上为 Mono 2.8.1 编写。移植到任何更旧或更新的版本都应该没有问题。示例应用程序的解决方案包含两个项目(完整的源代码可供下载)
- XamlRibbonApp 包含示例应用程序的源代码。
- 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.7,它定义了 libX11.so 的 Xlib/X11 调用函数原型、结构和类型。该程序集是为 使用 Mono Develop 编程 Xlib - 第一部分:低级 (概念验证) 项目开发的,并在 编程 Roma Widget Set (C# X11) - 零依赖 GUI 应用程序框架 - 基础 项目期间得到改进。
GUI 框架基于 Xrw 程序集版本 0.7,它定义了 XAML 代码中使用的部件/小部件及其包装类(应尽可能接近 Microsoft® 原版)。该程序集是在 编程 Roma Widget Set (C# X11) - 零依赖 GUI 应用程序框架 - 基础 项目期间开发的。
建议:要使用 MonoDevelop 的类库文档快捷键 (F1),必须安装“mono-tools”软件包。
该图片展示了使用 `XrwTheme.GeneralStyle.Gtk2Clearlooks` 的示例应用程序。
示例应用程序(通过 Ribbon 的应用程序菜单项“打开...”)打开并逐行显示一个 CSV 文件(SampleData.csv 已包含在示例应用程序的源代码中)。
所有 `RibbonApplicationMenuItem` 和所有 `RibbonButton` 都连接到 `RelayCommand`,其中大部分仅用于演示目的,显示一个消息框。与连接到必须包含在视图代码隐藏文件中的事件委托的 `Click` 属性不同,`Command` 属性连接到必须包含在当前 `DataContext` 代码文件(通常是 ViewModel)中的命令类实例。这导致了更好的 MVVM 模式实现,没有代码隐藏(在 CodeProject 上搜索“RelayCommand”或参阅“在 XAML 中使用 EventTrigger 实现 MVVM – 无代码隐藏”以了解为何应避免代码隐藏)。
所有 `RibbonApplicationMenuItem` 和所有 `RibbonButton` 的图像均取自框架资源文件 (xmlns:properties="clr-namespace:X11.Properties"
),并通过 `BitmapResourceToImageConverter` 类转换为图像资源。
Windows 版本,与 XAML 代码并行开发,旨在尽可能接近 Microsoft® 原版,它从应用程序资源文件 (xmlns:properties="clr-namespace:XamlRibbonApp.Properties"
) 获取图像,并使用 `BitmapResourceToImageConverter` 类的不同实现将图像转换为图像资源。这种方法完全隐藏了 X11/Windows® 平台的特定细节。
逐步演示
项目设置、应用程序文件上下文(主题除外)和预处理器代码生成步骤与为 X11 编写 XAML 对话框应用程序中的完全相同。如果需要从头开始创建新解决方案,请参阅该文章。
主视图文件上下文
XAML (MainView.xaml)
首先是省略了 Ribbon 定义的 XAML 文件。
<RibbonWindow x:Class="XamlRibbonApp.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:XamlRibbonApp"
xmlns:properties="clr-namespace:X11.Properties"
Name="MainWindow" Title="XAML Tester" Height="600" Width="500"
MinWidth="300" MinHeight="250" Icon="XrwIcon16.bmp">
<Window.Resources>
<src:BitmapResourceToImageConverter x:Key="ImageConverter" />
<src:MainWindowViewModel x:Key="MainViewModel" />
</Window.Resources>
<Grid Name="MainGrid" DataContext="{StaticResource MainViewModel}">
<Grid.Resources>
<!-- <src:MainViewModel x:Key="mainViewDataSource" /> -->
</Grid.Resources>
<Grid.Datacontext>
<!-- <Binding Source="{StaticResource mainViewDataSource}"/> -->
</Grid.Datacontext>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<!-- The fixed height of 117 is the required heigth of a ribbon. -->
<!-- If a margin is set, the margin must be added to the height. -->
<RowDefinition Height="117"/>
<RowDefinition Height="1.0*"/>
<RowDefinition Height="28"/>
</Grid.RowDefinitions>
<!-- ATTENTION: (Microsoft compatibility issue) The ribbon out of the System.Windows.Controls. -->
<!-- Ribbon.dll needs a System.Windows.Controls.RibbonWindow as topmost container. -->
<!-- OTHERWISE: A "System.Windows.Data Error: 4 : Cannot find source for binding with -->
<!-- reference 'RelativeSource FindAncestor, AncestorType='Microsoft.Windows. -->
<!-- Controls.Ribbon.RibbonWindow', ... " occures. -->
<!-- APPARENTLY: The Microsoft.Windows.Controls.Ribbon namespace (Office 2010 ribbon control -->
<!-- from October 2010) is replaced by System.Windows.Controls.Ribbon namespace -->
<!-- (WPF ribbon control from .NET 4.5). -->
<Ribbon Name="MainRibbon" Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="0">
...
</Ribbon>
<ListView Name="FileContentListView" Grid.Row="1" ItemsSource="{Binding DocumentRows}" >
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Row" Width="40" DisplayMemberBinding="{Binding Row}" />
<GridViewColumn Header="Content" DisplayMemberBinding="{Binding Path=Content}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
<Label Name="StateLine" Content="{Binding CurrentFilePath}" Grid.Column="0" Grid.Row="2"
BorderThickness="1" />
</Grid>
</RibbonWindow>
完整的 XAML 代码与 Microsoft® 完全兼容。
与 为 X11 编写 XAML 对话框应用程序 相比,本文将只讨论增强功能和差异。
对于最顶层的容器控件,使用 `RibbonWindow` 而不是 `Window`。这是为了防止出现“System.Windows.Data Error: 4 : 无法找到绑定源,引用 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.Ribbon.RibbonWindow', AncestorLevel='1''. BindingExpression:Path=WindowState; DataItem=null; 目标元素是 'Ribbon' (Name='MainRibbon'); 目标属性是 'NoTarget' (type 'Object')”和“System.Windows.Data Error: 4 : 无法找到绑定源,引用 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.Ribbon.RibbonWindow', AncestorLevel='1''. BindingExpression:Path=IsActive; DataItem=null; 目标元素是 'Ribbon' (Name='MainRibbon'); 目标属性是 'NoTarget' (type 'Object')”错误。
xmln
s:properties
属性定义本地资源属性的命名空间。(Microsoft .NET 项目包含 Resources.resx 文件以保存语言相关的资源,如字符串和图像/图标。Xrw 通过简单地使用 C# 文件来模拟此技术。)如果任何属性值使用前缀properties:
作为引用,则建议使用或强制使用此属性。属性值的语法必须是clr-namespace:
<命名空间名称>。以这种方式定义的命名空间可以通过属性值通过前缀properties:
引用。MinWidth
属性定义了最小宽度限制。此属性是可选的。正整数被解释为像素宽度。Microsoft® 原版 WPF Ribbon 控件(.NET 4.5)在主窗口宽度小于 295 时会消失。因此,MinWidth
设置为 300。- `MinHeight` 属性定义了最小高度限制。此属性是可选的。正整数被解释为像素高度。
Window.Resources
节点现在被评估,并定义与Window
类实例相关的静态资源。此节点是可选的。节点的语法必须是<命名空间名称>:
<类名>x:Key="
<别名>"
。此处定义的静态资源在窗口类实例化期间创建,并与窗口类实例的生命周期绑定。由于静态资源的搜索算法(遍历控件层次结构),窗口类实例及其所有子控件实例可以通过其<别名>引用此处定义的资源。
定义与窗口类实例关联的静态资源很有用,因为它们可以被多次引用(但只需加载到内存中一次),并且可以被所有子控件实例引用。如果应用程序支持多个窗口,或者静态资源应该与应用程序类实例的生命周期绑定,而不是与单个窗口类实例的生命周期绑定,那么在 `App.xaml` 的 `Application.Resources` 节点中定义应用程序范围的静态资源可能会更好,该节点现在也已评估。在这种情况下,由于静态资源的搜索算法,所有窗口类实例及其所有子控件实例都可以通过其<别名>引用此处定义的资源。
Grid
是窗口的根布局管理器小部件。在 为 X11 编写 XAML 对话框应用程序 中已经讨论过的 Grid 用法没有增强。
Ribbon
将稍后讨论。
ListView
将通过以下方式定义
Name
属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。Grid.Column
属性定义了控件在网格中定位的基于零的列索引。默认值为 0。此属性对于网格子项是推荐的,但对于未定位在网格中第 0 列的控件是强制性的。索引不得超过可用网格行。Grid.Row
属性定义了控件在网格中定位的基于零的行索引。默认值为 0。此属性对于网格子项是推荐的,但对于未定位在网格中第 0 行的控件是强制性的。索引不得超过可用网格行。ItemsSource
属性定义了控件获取要显示项的数据源。数据源必须实现System.Collections.IEnumerable
。此属性通常是可选的,但要绑定数据以显示则必需。- `ListView.View` 节点提供对子对象的访问,该子对象定义了数据在 `ListView` 中如何样式化和组织。此节点是强制性的。它必须包含一个定义子控件的节点。目前仅支持 `GridView` 节点作为子节点。
GridView
将由以下内容定义
GridView.Columns
节点连接列定义。此节点是强制性的。
- `GridColumn` 节点定义一个网格视图列。此节点是强制性的,至少一次。
- `Header` 属性定义列标题显示文本。此属性是可选的。
-Width
属性定义了网格列的宽度。此属性是可选的。正整数被解释为像素宽度,并覆盖任何自动宽度计算。省略 width 属性将导致自动宽度计算。
- `DisplayMemberBinding` 属性将任何指定的 `ItemsSource` 元素的属性名与要在此列中显示的值相关联。此属性通常是可选的,但显示值时是必需的。
现在回到 `Ribbon`。这是带有 ribbon 定义的 XAML 文件摘录。(第一个 ribbon 选项卡的第二个 ribbon 组和第二个 ribbon 选项卡被省略了,它们使用了与第一个 ribbon 选项卡的第一个组相同的技术。)
<Ribbon Name="MainRibbon" Grid.Column="0" Grid.Row="0">
<Ribbon.ApplicationMenu>
<RibbonApplicationMenu>
<RibbonApplicationMenuItem Name="AppMenuItem0" Header="New"
Command="{Binding RibbonAppMenu_FileNew_Command}" CommandParameter="null"
ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_NEW_32_WIN},
Converter={StaticResource ImageConverter}}" />
<RibbonApplicationMenuItem Name="AppMenuItem1" Header="Open ..."
Command="{Binding RibbonAppMenu_FileOpen_Command}" CommandParameter="null"
ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_OPEN_32_WIN},
Converter={StaticResource ImageConverter}}" />
<RibbonSeparator />
<RibbonApplicationMenuItem Name="AppMenuItem2" Header="Save"
Command="{Binding RibbonAppMenu_FileSave_Command}" CommandParameter="null"
ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_SAVE_32_WIN},
Converter={StaticResource ImageConverter}}" />
<RibbonApplicationMenuItem Name="AppMenuItem3" Header="Save as ..."
Command="{Binding RibbonAppMenu_FileSaveAs_Command}" CommandParameter="null"
ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_SAVEAS_32_WIN},
Converter={StaticResource ImageConverter}}" />
<RibbonSeparator />
<RibbonApplicationMenuItem Name="AppMenuItem4" Header="Options"
Command="{Binding RibbonAppMenu_Options_Command}" CommandParameter="null"
ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_OPTIONS_32_WIN},
Converter={StaticResource ImageConverter}}" />
<RibbonApplicationMenuItem Name="AppMenuItem5" Header="Close"
Command="{Binding RibbonAppMenu_Close_Command}" CommandParameter="null"
ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_CLOSE_32_WIN},
Converter={StaticResource ImageConverter}}" />
<RibbonSeparator />
<RibbonApplicationMenuItem Name="AppMenuItem6" Header="Exit"
Command="{Binding RibbonAppMenu_Exit_Command}" CommandParameter="null"
ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_RIBBON_EXIT_32_WIN},
Converter={StaticResource ImageConverter}}" />
</RibbonApplicationMenu>
</Ribbon.ApplicationMenu>
<RibbonTab Name="RibbonTabEdit" Header="Edit">
<RibbonGroup Name="RibbonGroupClipboard" Header="Clipboard" >
<RibbonButton Name="RibbonButtonClipboardCut" Label="Cut"
Command="{Binding RibbonTabEdit_ClipboardCut_Command}"
LargeImageSource="{Binding Source={x:Static properties:Resources.IMAGE_CUT_32_WIN},
Converter={StaticResource ImageConverter}}"
SmallImageSource="{Binding Source={x:Static properties:Resources.IMAGE_CUT_16_WIN},
Converter={StaticResource ImageConverter}}" />
<RibbonButton Name="RibbonButtonClipboardCopy" Label="Copy"
Command="{Binding RibbonTabEdit_ClipboardCopy_Command}"
LargeImageSource="{Binding Source={x:Static properties:Resources.IMAGE_COPY_32_WIN},
Converter={StaticResource ImageConverter}}"
SmallImageSource="{Binding Source={x:Static properties:Resources.IMAGE_COPY_16_WIN},
Converter={StaticResource ImageConverter}}" />
<RibbonButton Name="RibbonButtonClipboardPaste" Label="Paste"
Command="{Binding RibbonTabEdit_ClipboardPaste_Command}"
LargeImageSource="{Binding Source={x:Static properties:Resources.IMAGE_PASTE_32_WIN},
Converter={StaticResource ImageConverter}}"
SmallImageSource="{Binding Source={x:Static properties:Resources.IMAGE_PASTE_16_WIN},
Converter={StaticResource ImageConverter}}" />
</RibbonGroup>
<RibbonGroup Name="RibbonGroupOrder" Header="Order">
...
</RibbonGroup>
</RibbonTab>
<RibbonTab Name="RibbonTabView" Header="View">
...
</RibbonTab>
<RibbonTab Name="RibbonTabHelp" Header="Help">
<RibbonGroup Name="RibbonGroupHelp" Header="Help">
<RibbonSplitButton Name="RibbonButtonHelp" Label="Help"
LargeImageSource="{Binding Source={x:Static properties:Resources.IMAGE_QUESTION_32},
Converter={StaticResource ImageConverter}}"
SmallImageSource="{Binding Source={x:Static properties:Resources.IMAGE_QUESTION_16},
Converter={StaticResource ImageConverter}}" >
<RibbonMenuItem Header="Local Help"
Command="{Binding RibbonTabHelp_LocalHelp_Command}"
ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_QUESTION_16},
Converter={StaticResource ImageConverter}}"/>
<RibbonMenuItem Header="Online Help"
Command="{Binding RibbonTabHelp_OnlineHelp_Command}"
ImageSource="{Binding Source={x:Static properties:Resources.IMAGE_QUESTION_16},
Converter={StaticResource ImageConverter}}"/>
</RibbonSplitButton>
<RibbonButton Name="RibbonButtonAbout" Label="About"
Command="{Binding RibbonTabHelp_About_Command}"
LargeImageSource="{Binding Source={x:Static properties:Resources.IMAGE_INFO_32},
Converter={StaticResource ImageConverter}}"
SmallImageSource="{Binding Source={x:Static properties:Resources.IMAGE_INFO_16},
Converter={StaticResource ImageConverter}}" />
</RibbonGroup>
</RibbonTab>
</Ribbon>
完整的 XAML 代码与 Microsoft® 完全兼容。
Ribbon
将通过以下方式定义
Name
属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。Grid.Column
属性定义了控件在网格中定位的基于零的列索引。默认值为 0。此属性对于网格子项是推荐的,但对于未定位在网格中第 0 列的控件是强制性的。索引不得超过可用网格行。Grid.Row
属性定义了控件在网格中定位的基于零的行索引。默认值为 0。此属性对于网格子项是推荐的,但对于未定位在网格中第 0 行的控件是强制性的。索引不得超过可用网格行。- `Ribbon.ApplicationMenu` 节点提供对子对象的访问,该子对象定义应用程序菜单。此节点通常是可选的,但要显示应用程序菜单则必需。它必须包含一个定义应用程序菜单的节点。目前仅支持 `RibbonApplicationMenu` 节点作为子节点。
- `RibbonApplicationMenu` 节点定义应用程序菜单。此节点是强制性的。它必须包含定义菜单项的节点。 RibbonTab
节点定义一个 Ribbon 选项卡,并包含定义子控件的节点。目前只支持RibbonGroup
节点作为子节点。
RibbonApplicationMenu
控件包含 `RibbonApplicationMenuItem` 控件作为子控件,它们将按以下方式定义
Name
属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。Header
属性定义要显示的文本。此属性是可选的。Command
属性定义要执行的“RelayCommand”。此属性是可选的。Command
相较于Click
的优点是,操作代码可以位于 ModelView 代码中,而不是视图的代码隐藏中。ImageSource
属性定义要显示的图标。此属性是可选的。属性的语法必须是<相对图像文件URI>或{Binding Source={x:Static
<命名空间名称>:
<资源名称>} Converter={StaticResource
<转换器类别名>}}
用于本地静态资源,以及{Binding Source={StaticResource
<资源别名>} Converter={StaticResource
<转换器类别名>}}
用于可引用的静态资源。Converter
是可选的。
{Binding ... }
语法提供了使用预编译资源而不是图像文件的可能性,因此也避免了在程序集之外提供大量图像文件。
RibbonApplicationMenu
控件包含 RibbonSeparator
控件作为子控件。Microsoft® 实现将完全独立处理此节点,而 X11/Xrw 实现会修改先前的 RibbonApplicationMenuItem
以显示分隔符。因此,分隔符不能在没有先前的 RibbonApplicationMenuItem
的情况下出现,也不能立即连续出现。
RibbonTab
控件包含 `RibbonGroup` 控件作为子控件,它们将按以下方式定义
Name
属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。Header
属性定义要显示的文本。此属性是可选的。RibbonButton
节点定义一个 Ribbon 按钮作为子控件。此节点是可选的。RibbonSplitButton
节点定义一个 Ribbon 拆分按钮作为子控件。此节点是可选的。
`RibbonGroup` 控件包含 `RibbonButton` 控件作为子控件,它们将按以下方式定义
Name
属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。Label
属性定义要显示的文本。此属性是可选的。Command
属性定义要执行的“RelayCommand”。此属性是可选的。Command
相较于Click
的优点是,操作代码位于 ModelView 代码中,而不是视图的代码隐藏中。LargeImageSource
属性定义了用于尺寸定义ImageSize="Large"
的大图标,而SmallImageSource
属性定义了用于尺寸定义ImageSize="Small"
的小图标。这些属性是可选的。属性的语法必须是<相对图像文件 URI>或{Binding Source={x:Static
<命名空间名称>:
<资源名称>} Converter={StaticResource
<转换器类别名>}}
用于此节点本地的静态资源,以及{Binding Source={StaticResource
<资源别名>} Converter={StaticResource
<转换器类别名>}}
用于可引用的静态资源。Converter
是可选的。
`RibbonGroup` 控件包含 `RibbonSplitButton` 控件作为子控件,它们将按以下方式定义
Name
属性定义了类实例名称,可用于唯一标识类实例。此属性是推荐的,如果此类的实例必须通过 C# 代码访问,则为强制性的。Label
属性定义要显示的文本。此属性是可选的。LargeImageSource
属性定义了用于尺寸定义ImageSize="Large"
的大图标,而SmallImageSource
属性定义了用于尺寸定义ImageSize="Small"
的小图标。这些属性是可选的。属性的语法必须是<相对图像文件 URI>或{Binding Source={x:Static
<命名空间名称>:
<资源名称>} Converter={StaticResource
<转换器类别名>}}
用于此节点本地的静态资源,以及{Binding Source={StaticResource
<资源别名>}}
用于可引用的静态资源。RibbonMenuItem
节点定义了 Ribbon 拆分按钮的一个菜单项。此节点是强制性的,至少一次。目前只支持RibbonMenuItem
节点作为子节点。
-Header
属性定义要显示的文本。此属性是可选的。
-Command
属性定义要执行的“RelayCommand”。此属性是可选的。Command
相较于Click
的优点是,操作代码位于 ModelView 代码中,而不是视图的代码隐藏中。
-ImageSource
属性定义要显示的图标。此属性是可选的。属性的语法必须是<相对图像文件URI>或{Binding Source={x:Static
<命名空间名称>:
<资源名称>} Converter={StaticResource
<转换器类别名>}}
用于本地静态资源,以及{Binding Source={StaticResource
<资源别名>} Converter={StaticResource
<转换器类别名>}}
用于可引用的静态资源。Converter
是可选的。
如果为 `RibbonButton` 或 `RibbonSplitButton` 定义了 `LargeImageSource` 属性,则默认情况下会以 `ImageSize="Large"` 显示(无论是否定义了 `SmallImageSource` 属性)。
如果省略了 `LargeImageSource` 属性,并且为 `RibbonButton` 或 `RibbonSplitButton` 定义了 `SmallImageSource` 属性,则默认情况下会以 `ImageSize="Small"` 和 `IsLabelVisible="True"` 显示。
如果 `RibbonButton` 或 `RibbonSplitButton` 既没有定义 `LargeImageSource` 属性,也没有定义 `SmallImageSource` 属性,那么 `Label` 属性将不再是可选的,而是强制性的,并且 `RibbonButton` 或 `RibbonSplitButton` 将只显示标签。
RibbonButton
和 RibbonSplitButton
的默认尺寸适配策略,例如,对于一个具有 LargeImageSource
属性和 SmallImageSource
属性且为所有 RibbonButton
和 RibbonSplitButton
定义的四按钮 RibbonGroup
,等同于
<RibbonGroup.GroupSizeDefinitions>
<RibbonGroupSizeDefinition>
<RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
<RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
<RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
<RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
</RibbonGroupSizeDefinition>
<RibbonGroupSizeDefinition>
<RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
<RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True"/>
<RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True"/>
<RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="True"/>
</RibbonGroupSizeDefinition>
<RibbonGroupSizeDefinition>
<RibbonControlSizeDefinition ImageSize="Large" IsLabelVisible="True"/>
<RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="False"/>
<RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="False"/>
<RibbonControlSizeDefinition ImageSize="Small" IsLabelVisible="False"/>
</RibbonGroupSizeDefinition>
<RibbonGroupSizeDefinition IsCollapsed="True"/>
</RibbonGroup.GroupSizeDefinitions>
XAML 语法的局限性
X11/Xrw 实现
- 目前,`ListView.View` 的子节点只支持 `GridView` 节点。
- 目前,`RibbonTab` 的子节点只支持 `RibbonGroup` 节点。
- 目前,`RibbonSplitButton` 的子节点只支持 `RibbonMenuItem` 节点。
- 目前,`RibbonGroup` 控件必须至少包含一个 `RibbonButton` 或 `RibbonSplitButton` 控件。不支持其他子节点。
- 目前不支持 `RibbonGroup.GroupSizeDefinitions` 节点。
Microsoft® 原版实现
- `RibbonDialogLauncher` for `RibbonGroup` 已从 System.Windows.Controls.Ribbon(用于 WPF .NET 4.5 的 ribbon)中移除,以避免潜在的专利侵权。奇怪的是,它仍然包含在 Microsoft.Windows.Controls.Ribbon(用于 WPF 2010 年 10 月 .NET 4.0)和 Office 2013 的最新下载中。
- WPF Ribbon 控件(.NET 4.5)在主窗口宽度小于 295 时会消失。
RelayCommand 的连接
要连接“RelayCommand”,必须满足以下几个要求。
- 控件或其父控件之一必须设置 `DataContext` 属性,例如:`DataContext="{StaticResource MainViewModel}"`
- 控件必须将 `Command` 属性绑定到作为数据上下文设置的类的属性,例如:`Command="{Binding RibbonTabHelp_LocalHelp_Command}"`
- 作为数据上下文设置的类必须提供一个属性来访问“RelayCommand”,例如
/// <summary>The relay command for Ribbon.TabHelp.GroupHelp.LocalHelp: Exit</summary>
private RelayCommand _ribbonTabHelp_LocalHelp_Command = null;
...
/// <summary>Provide the relay command for Ribbon.TabHelp.GroupHelp.LocalHelp</summary>
public System.Windows.Input.ICommand RibbonTabHelp_LocalHelp_Command
{ get { return _ribbonTabHelp_LocalHelp_Command; } }
- “RelayCommand”必须关联一个“Action”和一个“CanAction”回调,例如
_ribbonTabHelp_LocalHelp_Command = new RelayCommand(RibbonTabHelp_LocalHelp_Action,
RibbonTabHelp_LocalHelp_CanAction);
- Action 和 CanAction 回调必须实现,例如
/// <summary>The action for Ribbon.TabHelp.GroupHelp.LocalHelp</summary>
/// <param name="o">A parameter to use for any purpose.</param>
private void RibbonTabHelp_LocalHelp_Action(object o)
{
//System.Windows.Result result =
MessageBox.Show("To do: Fill the '" + CLASS_NAME + ".RibbonTabHelp_LocalHelp_Action' " +
"stub with meaningful code!", "Unfinished",
MessageBoxButton.OKCancel, MessageBoxImage.Information);
}
/// <summary>The can-action property for Ribbon.TabHelp.GroupHelp.LocalHelp</summary>
/// <param name="o">A parameter to use for any purpose.</param>
/// <returns>True, if action can happen, or false otherwise.</returns>
private bool RibbonTabHelp_LocalHelp_CanAction(object o)
{
return true;
}
代码隐藏 (MainView.xaml.cs)
主视图对应的 C# 代码文件是 MainView.xaml.cs
。它几乎是空的。
using System;
using X11;
using Xrw;
using XrwXAML;
namespace XamlRibbonApp
{
/// <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.RibbonWindow, IView
{
/// <summary>The default constructor.</summary>
public MainView ()
: base (-1, -1)
{
InitializeComponent ();
}
// =======================================================================
// ATTENTION: On an accurate implemented MVVM pattern this class is empty!
// =======================================================================
}
}
主视图模型文件上下文
MainWindowViewModel.cs
文件包含所有连接到应用程序菜单项和 Ribbon 按钮的“RelayCommand”。它们中的大多数只是存根。只有两个被实现,以展示如何调用 OpenFileDialog
和 SaveFileDialog
。
/// <summary>The action for Ribbon.ApplicationMenu.RibbonApplicationMenuItem.AppMenuItem1</summary>
/// <param name="o">A parameter to use for any purpose.</param>
private void RibbonAppMenu_FileOpen_Action(object o)
{
// https://codeproject.org.cn/Articles/36745/Showing-Dialogs-When-Using-the-MVVM-Pattern
OpenFileDialog ofd = new OpenFileDialog ();
ofd.Filter = "comma separated values CSV|*.csv";
ofd.FilterIndex = 0;
ofd.InitialDirectory = (new System.IO.FileInfo (
System.Reflection.Assembly.GetExecutingAssembly().Location)).Directory.FullName;
if (ofd.ShowDialog (Application.Current.MainWindow) == true)
{
int row = 1;
string line;
DocumentRows.Clear();
CurrentFilePath = ofd.FileName;
System.IO.StreamReader file = new System.IO.StreamReader(CurrentFilePath);
while ((line = file.ReadLine()) != null)
{
DocumentRows.Add (new DocumentLine (string.Format("{0:0000}", row), line));
row++;
}
file.Close();
}
}
/// <summary>The action for Ribbon.ApplicationMenu.RibbonApplicationMenuItem.AppMenuItem3</summary>
/// <param name="o">A parameter to use for any purpose.</param>
private void RibbonAppMenu_FileSaveAs_Action(object o)
{
// https://codeproject.org.cn/Articles/36745/Showing-Dialogs-When-Using-the-MVVM-Pattern
SaveFileDialog sfd = new SaveFileDialog ();
if (sfd.ShowDialog (Application.Current.MainWindow) == true)
{
CurrentFilePath = sfd.FileName;
}
}
此外,`RibbonAppMenu_FileOpen_Action()` 回调通过 `DocumentRows.Clear()` 和 `DocumentRows.Add(...)` 更新 ViewModel 背后的 MVVM 模型。
`DocumentRows` 是 ViewModel 类的一个属性
/// <summary>Get the item collection, contained in the cocument.</summary>
public ObservableCollection<DocumentLine> DocumentRows
{
get
{
if (_model != null)
return _model.DocumentContent;
else
{
SimpleLog.LogLine(TraceEventType.Error, CLASS_NAME +
"DocumentRows.get (): Missing the model, connected to the view-model.");
return new ObservableCollection<DocumentLine>();
}
}
set
{
if (_model != null)
{
_model.DocumentContent = value;
RaisePropertyChanged("DocumentRows");
}
else
SimpleLog.LogLine(TraceEventType.Error, CLASS_NAME +
"DocumentRows.set (): Missing the model, connected to the view-model.");
}
}
将文档行实现为 `ObservableCollection<DocumentLine>` 提供了 `INotifyCollectionChanged` 和 `INotifyPropertyChanged` 的实现。而且 `DocumentRows` 属性不仅方便访问文档内容,还通过 XAML 连接到 `ListView` 的项源:`ItemsSource="{Binding DocumentRows}"`。
因此,文档内容的更改会自动更新视图。
`CurrentFilePath` 是 ViewModel 类的另一个属性
/// <summary>Get or set the current file path, stored inside the underlaying model.</summary>
public string CurrentFilePath
{
get
{
if (_model != null)
return _model.DocumentFilePath;
else
{
SimpleLog.LogLine(TraceEventType.Error, CLASS_NAME +
"CurrentFilePath.get (): Missing the model, connected to the view-model.");
return "";
}
}
set
{
if (_model != null)
{
_model.DocumentFilePath = value;
RaisePropertyChanged("CurrentFilePath");
}
else
SimpleLog.LogLine(TraceEventType.Error, CLASS_NAME +
"CurrentFilePath.set (): Missing the model, connected to the view-model.");
}
}
由于 MVVM ViewModel 实现了 `INotifyPropertyChanged` 并且 `CurrentFilePath` 属性调用了 `RaisePropertyChanged`,所以 `Label` “StateLine”会收到更改通知,因为它的 `Content` 属性通过 XAML 连接到 `CurrentFilePath`:`Content="{Binding CurrentFilePath}"`
有两篇 Code Project 文章,我推荐阅读它们,它们与干净的 MVVM 实现相关:
关注点
尽管我对 XAML 持怀疑态度(又是微软®的一项新技术,在文档-视图范式和模型-视图-控制器范式之后又是新的范式,所有控件都是新的),但我不得不承认
- XAML 省去了大量的 GUI 编码!!!并且
- Linux/Unix (X11) 和 Windows 的并行开发竟然如此简单!!!
因为为 X11 编写 XAML ribbon 应用程序已经成功,这不会是最后一篇关于 XAML 编程的文章。
自2014年11月23日起,文章为 X11 编写具有海量数据绑定和零代码的 XAML 应用程序延续了 XAML 主题。
自2015年2月2日起,文章为 X11 编写 XAML 计算器应用程序延续了 XAML 主题。
历史
这是2014年10月29日的第一个版本。
这是2014年11月23日的修订版。