Xamarin.Mac 中的大纲视图
本文介绍如何在 Xamarin.Mac 应用程序中使用大纲视图。 本文介绍如何在 Xcode 和 Interface Builder 中创建和维护大纲视图,还描述了如何以编程方式使用它们。
在 Xamarin.Mac 应用程序中使用 C# 和 .NET 时,你可以访问的大纲视图与使用 Objective-C 和 Xcode 的开发人员访问的大纲视图相同。 由于 Xamarin.Mac 与 Xcode 直接集成,你可以使用 Xcode 的 Interface Builder 来创建和维护大纲视图(或选择直接使用 C# 代码创建)。
大纲视图是一种表类型,支持用户展开或折叠分层数据行。 与表视图一样,大纲视图显示一组相关项的数据,行代表单个项,列代表这些项的属性。 与表视图不同,大纲视图中的项不是在平面列表中,而是按层次结构组织的,就像硬盘上的文件和文件夹。
本文将介绍在 Xamarin.Mac 应用程序中使用大纲视图的基础知识。 强烈建议先阅读 Hello, Mac 一文,特别是 Xcode 和 Interface Builder 简介和输出口和操作部分,因为其中介绍了我们将在本文中使用的关键概念和技术。
你可能还需要查看 Xamarin.Mac 内部机制文档的向 Objective-C 公开 C# 类/方法部分,因为其中介绍了用于将 C# 类连接到 Objective-C 对象和 UI 元素的 Register
和 Export
命令。
大纲视图简介
大纲视图是一种表类型,支持用户展开或折叠分层数据行。 与表视图一样,大纲视图显示一组相关项的数据,行代表单个项,列代表这些项的属性。 与表视图不同,大纲视图中的项不是在平面列表中,而是按层次结构组织的,就像硬盘上的文件和文件夹。
如果大纲视图中的某个项包含其他项,用户可以展开或折叠该项。 可展开的项会显示一个开合三角形,当项折叠时,三角形指向右侧;当项展开时,三角形指向下。 单击开合三角形会导致项展开或折叠。
大纲视图 (NSOutlineView
) 是表视图 (NSTableView
) 的子类,因此其大部分行为都继承自父类。 因此,表视图支持的许多操作,如选择行或列、通过拖动列标题调整列的位置等,也受大纲视图的支持。 Xamarin.Mac 应用程序可控制这些功能,并可配置大纲视图的参数(在代码或 Interface Builder 中),以允许或禁止某些操作。
大纲视图不存储它自己的数据,而是依赖于数据源 (NSOutlineViewDataSource
) 来根据需要提供所需的行和列。
可以通过提供大纲视图委托 (NSOutlineViewDelegate
) 的子类来支持大纲列管理、键入以选择功能、行选择和编辑、自定义跟踪以及单个列和行的自定义视图来自定义大纲视图的行为。
由于大纲视图的大部分行为和功能都与表视图相同,因此在继续阅读本文之前,你可能需要先阅读我们的表视图文档。
在 Xcode 中创建和维护大纲视图
创建新的 Xamarin.Mac Cocoa 应用程序时,默认情况下会获得标准空白窗口。 此窗口在项目中自动包含的 .storyboard
文件中定义。 若要编辑窗口设计,请在“解决方案资源管理器”中双击 Main.storyboard
文件:
这将在 Xcode 的 Interface Builder 中打开窗口设计:
在“库检查器”的搜索框中键入 outline
,以便更轻松地查找大纲视图控件:
将大纲视图拖到“界面编辑器”的视图控制器上,使其填充视图控制器的内容区域,并将其设置为在“约束编辑器”中的窗口收缩和增长的位置:
选择“接口层次结构”中的大纲视图,“属性检查器”中提供以下属性:
- 大纲列 - 显示分层数据的表列。
- 自动保存大纲列 - 如果为
true
,大纲列将在应用程序运行之间自动保存和还原。 - 缩进 - 展开的项下对列的缩进量。
- 缩进跟随单元格 - 如果为
true
,缩进标记将随单元格一起缩进。 - 自动保存展开的项 - 如果为
true
,项的展开/折叠状态将在应用程序运行之间自动保存和还原。 - 内容模式 - 允许使用视图 (
NSView
) 或单元格 (NSCell
) 在行和列中显示数据。 从 macOS 10.7 开始,应使用视图。 - 浮点数组行 - 如果
true
,表视图将绘制分组单元格,就像它们浮动一样。 - 列 - 定义显示的列数。
- 标头 - 如果
true
,列将具有标头。 - 重新排序 - 如果
true
,用户将能够拖动对表中的列进行重新排序。 - 调整大小 - 如果
true
,用户将能够拖动列标题以调整列的大小。 - 列大小调整 - 控制表自动调整列大小的方式。
- 突出显示 - 控制选定单元格时表格使用的突出显示类型。
- 交替行 - 如果
true
,相邻的行将具有不同的背景色。 - 水平网格 - 选择水平单元格之间绘制的边框类型。
- 垂直网格 - 选择垂直单元格之间绘制的边框类型。
- 网格颜色 - 设置单元格边框颜色。
- 背景 - 设置单元格背景色。
- 选择 - 允许你控制用户可以如何选择表中的单元格,如下所示:
- 多个 - 如果
true
,用户可以选择多个行和列。 - 列 - 如果
true
,用户可以选择列。 - 类型选择 - 如果
true
,用户可以键入字符以选择行。 - 空 - 如果
true
,用户不需要选择行或列,表格允许完全不选择。
- 多个 - 如果
- 自动保存 - 表格式的名称会自动保存。
- 列信息 - 如果
true
,将自动保存列的顺序和宽度。 - 换行符 - 选择单元格如何处理换行符。
- 截断最后一个可见行 - 如果
true
,则单元格将被截断,因为数据无法在其范围内存储。
重要
除非要维护旧版 Xamarin.Mac 应用程序,否则应使用基于 NSView
的大纲视图,而不是基于 NSCell
的表视图。 NSCell
被视为旧版,今后可能不受支持。
在“接口层次结构”中选择一个表列,“属性检查器”中提供了以下属性:
- 标题 - 设置列的标题。
- 对齐 - 设置单元格中文本的对齐方式。
- 标题字体 - 选择单元格标题文本的字体。
- 排序键 - 用于对列中的数据进行排序的键。 如果用户无法对此列进行排序,请留空。
- 选择器 - 用于执行排序的操作。 如果用户无法对此列进行排序,请留空。
- 顺序 - 列数据的排序顺序。
- 调整大小 - 选择列的大小调整类型。
- 可编辑 - 如果
true
,用户可以编辑基于单元格的表中的单元格。 - 隐藏 - 如果
true
,列处于隐藏状态。
还可以通过向左或向右拖动列的手柄(垂直居中)调整列的大小。
让我们选择表视图中的每一列,并为第一列提供 Product
标题 ,第二列则为 Details
。
在“接口层次结构”中选择表单元格视图 (NSTableViewCell
) ,“属性检查器”中提供以下属性:
这些是标准视图的所有属性。 还可以在此处选择调整此列的行大小。
在“接口层次结构”中选择表视图单元(默认情况下,这是 NSTextField
),“属性检查器”中提供以下属性:
你将拥有要在此处设置的标准文本字段的所有属性。 默认情况下,标准文本字段用于显示列中单元格的数据。
在“接口层次结构”中选择表单元格视图 (NSTableFieldCell
) ,“属性检查器”中提供以下属性:
此处最重要的设置包括:
- 布局 - 选择此列中的单元格布局方式。
- 使用单行模式 - 如果
true
,单元格限制为单行。 - 第一个运行时布局宽度 - 如果
true
,单元格在首次运行应用程序时将首选为其设置的宽度(手动或自动)。 - 操作 - 控制何时为单元格发送编辑操作。
- 行为 - 定义单元格是否可选择或可编辑。
- 格式文本 - 如果
true
,单元格可以显示带格式的文本和样式文本。 - 撤消 - 如果
true
,单元格承担撤消行为的责任。
选择“接口层次结构”中表列底部的表单元格视图 (NSTableFieldCell
):
这样,您可以编辑用作为给定列创建的所有单元格的基模式的表格单元格视图。
添加操作和出口
与任何其他 Cocoa UI 控件一样,我们需要使用操作和出口(基于所需的功能)向 C# 代码公开大纲视图及其列和单元格。
对于要公开的任何大纲视图元素,此过程是相同的:
切换到“助理编辑器”并确保已选择
ViewController.h
文件:从“接口层次结构”中选择大纲视图,按住 Control 键单击并拖动到
ViewController.h
文件。为名为
ProductOutline
的大纲视图创建出口:为表列创建出口,也称为
ProductColumn
和DetailsColumn
:保存更改并返回到 Visual Studio for Mac 以与 Xcode 同步。
接下来,我们将编写代码,以便在运行应用程序时显示大纲的一些数据。
填充大纲视图
在 Interface Builder 中设计好大纲视图并通过出口公开后,接下来我们需要创建 C# 代码来填充大纲视图。
首先,让我们创建一个新的 Product
类,用于保存各个行和子产品组的信息。 在“解决方案资源管理器”中,右键单击项目并选择“添加”>“新建文件...”。选择“常规>“空类”,输入 Product
作为“名称”,然后单击“新建”按钮:
使 Product.cs
文件如下所示:
using System;
using Foundation;
using System.Collections.Generic;
namespace MacOutlines
{
public class Product : NSObject
{
#region Public Variables
public List<Product> Products = new List<Product>();
#endregion
#region Computed Properties
public string Title { get; set;} = "";
public string Description { get; set;} = "";
public bool IsProductGroup {
get { return (Products.Count > 0); }
}
#endregion
#region Constructors
public Product ()
{
}
public Product (string title, string description)
{
this.Title = title;
this.Description = description;
}
#endregion
}
}
接下来,我们需要创建一个 NSOutlineDataSource
子类,以便在请求表时为大纲提供数据。 在“解决方案资源管理器”中,右键单击项目并选择“添加”>“新建文件...”。选择“常规”>“空类”,输入 ProductOutlineDataSource
作为“名称”,然后单击“新建”按钮。
编辑 ProductTableDataSource.cs
文件,使其如下所示:
using System;
using AppKit;
using CoreGraphics;
using Foundation;
using System.Collections;
using System.Collections.Generic;
namespace MacOutlines
{
public class ProductOutlineDataSource : NSOutlineViewDataSource
{
#region Public Variables
public List<Product> Products = new List<Product>();
#endregion
#region Constructors
public ProductOutlineDataSource ()
{
}
#endregion
#region Override Methods
public override nint GetChildrenCount (NSOutlineView outlineView, NSObject item)
{
if (item == null) {
return Products.Count;
} else {
return ((Product)item).Products.Count;
}
}
public override NSObject GetChild (NSOutlineView outlineView, nint childIndex, NSObject item)
{
if (item == null) {
return Products [childIndex];
} else {
return ((Product)item).Products [childIndex];
}
}
public override bool ItemExpandable (NSOutlineView outlineView, NSObject item)
{
if (item == null) {
return Products [0].IsProductGroup;
} else {
return ((Product)item).IsProductGroup;
}
}
#endregion
}
}
此类包含大纲视图项的存储,并替代 GetChildrenCount
以返回表中的行数。 GetChild
返回一个特定的父项或子项(根据大纲视图请求),而 ItemExpandable
则将指定的项定义为父项或子项。
最后,我们需要创建一个 NSOutlineDelegate
的子类来提供大纲的行为。 在“解决方案资源管理器”中,右键单击项目并选择“添加”>“新建文件...”。选择“常规”>“空类”,输入 ProductOutlineDelegate
作为“名称”,然后单击“新建”按钮。
编辑 ProductOutlineDelegate.cs
文件,使其如下所示:
using System;
using AppKit;
using CoreGraphics;
using Foundation;
using System.Collections;
using System.Collections.Generic;
namespace MacOutlines
{
public class ProductOutlineDelegate : NSOutlineViewDelegate
{
#region Constants
private const string CellIdentifier = "ProdCell";
#endregion
#region Private Variables
private ProductOutlineDataSource DataSource;
#endregion
#region Constructors
public ProductOutlineDelegate (ProductOutlineDataSource datasource)
{
this.DataSource = datasource;
}
#endregion
#region Override Methods
public override NSView GetView (NSOutlineView outlineView, NSTableColumn tableColumn, NSObject item) {
// This pattern allows you reuse existing views when they are no-longer in use.
// If the returned view is null, you instance up a new view
// If a non-null view is returned, you modify it enough to reflect the new data
NSTextField view = (NSTextField)outlineView.MakeView (CellIdentifier, this);
if (view == null) {
view = new NSTextField ();
view.Identifier = CellIdentifier;
view.BackgroundColor = NSColor.Clear;
view.Bordered = false;
view.Selectable = false;
view.Editable = false;
}
// Cast item
var product = item as Product;
// Setup view based on the column selected
switch (tableColumn.Title) {
case "Product":
view.StringValue = product.Title;
break;
case "Details":
view.StringValue = product.Description;
break;
}
return view;
}
#endregion
}
}
创建 ProductOutlineDelegate
的实例时,还会传入提供大纲数据的 ProductOutlineDataSource
的实例。 GetView
方法负责返回视图(数据)以显示给定列和行的单元格。 如果可能,将重复使用现有视图来显示单元格(如果不是新视图)。
若要填充大纲,让我们编辑 MainWindow.cs
文件,使 AwakeFromNib
方法如下所示:
public override void AwakeFromNib ()
{
base.AwakeFromNib ();
// Create data source and populate
var DataSource = new ProductOutlineDataSource ();
var Vegetables = new Product ("Vegetables", "Greens and Other Produce");
Vegetables.Products.Add (new Product ("Cabbage", "Brassica oleracea - Leaves, axillary buds, stems, flowerheads"));
Vegetables.Products.Add (new Product ("Turnip", "Brassica rapa - Tubers, leaves"));
Vegetables.Products.Add (new Product ("Radish", "Raphanus sativus - Roots, leaves, seed pods, seed oil, sprouting"));
Vegetables.Products.Add (new Product ("Carrot", "Daucus carota - Root tubers"));
DataSource.Products.Add (Vegetables);
var Fruits = new Product ("Fruits", "Fruit is a part of a flowering plant that derives from specific tissues of the flower");
Fruits.Products.Add (new Product ("Grape", "True Berry"));
Fruits.Products.Add (new Product ("Cucumber", "Pepo"));
Fruits.Products.Add (new Product ("Orange", "Hesperidium"));
Fruits.Products.Add (new Product ("Blackberry", "Aggregate fruit"));
DataSource.Products.Add (Fruits);
var Meats = new Product ("Meats", "Lean Cuts");
Meats.Products.Add (new Product ("Beef", "Cow"));
Meats.Products.Add (new Product ("Pork", "Pig"));
Meats.Products.Add (new Product ("Veal", "Young Cow"));
DataSource.Products.Add (Meats);
// Populate the outline
ProductOutline.DataSource = DataSource;
ProductOutline.Delegate = new ProductOutlineDelegate (DataSource);
}
如果运行应用程序,将显示以下内容:
如果我们在大纲视图中展开节点,它将如下所示:
按列排序
让我们允许用户通过单击列标题对大纲中的数据进行排序。 首先,双击 Main.storyboard
该文件将其打开,以便在 Interface Builder 中编辑。 选择 Product
列,为“排序键”输入 Title
,为“选择器”compare:
输入 ,并为“顺序”选择 Ascending
:
保存更改并返回到 Visual Studio for Mac 以与 Xcode 同步。
现在,让我们编辑 ProductOutlineDataSource.cs
文件并添加以下方法:
public void Sort(string key, bool ascending) {
// Take action based on key
switch (key) {
case "Title":
if (ascending) {
Products.Sort ((x, y) => x.Title.CompareTo (y.Title));
} else {
Products.Sort ((x, y) => -1 * x.Title.CompareTo (y.Title));
}
break;
}
}
public override void SortDescriptorsChanged (NSOutlineView outlineView, NSSortDescriptor[] oldDescriptors)
{
// Sort the data
Sort (oldDescriptors [0].Key, oldDescriptors [0].Ascending);
outlineView.ReloadData ();
}
Sort
方法允许我们根据给定 Product
类字段按升序或降序对数据源中的数据进行排序。 每次使用单击列标题时,都会调用重写的 SortDescriptorsChanged
方法。 它将传递我们在 Interface Builder 中设置的键值以及该列的排序顺序。
如果我们运行应用程序并单击列标题,则行将按该列进行排序:
行选择
如果要允许用户选择单行,请双击 Main.storyboard
文件以在 Interface Builder 中编辑。 在“接口层次结构”中选择大纲视图,然后取消选中“属性检查器”中的“多个”复选框:
保存更改并返回到 Visual Studio for Mac 以与 Xcode 同步。
接下来,编辑 ProductOutlineDelegate.cs
文件并添加以下方法:
public override bool ShouldSelectItem (NSOutlineView outlineView, NSObject item)
{
// Don't select product groups
return !((Product)item).IsProductGroup;
}
这将允许用户选择大纲视图中的任何单行。 对于不希望用户能够选择的项,则针对 ShouldSelectItem
返回 false
,如果不希望用户能够选择所有项,则针对所有项返回 false
。
多行选择
如果希望允许用户选择多行,请双击 Main.storyboard
文件将其打开,以便在 Interface Builder 中编辑。 在“接口层次结构”中选择大纲视图,然后选中“属性检查器”中的“多个”复选框:
保存更改并返回到 Visual Studio for Mac 以与 Xcode 同步。
接下来,编辑 ProductOutlineDelegate.cs
文件并添加以下方法:
public override bool ShouldSelectItem (NSOutlineView outlineView, NSObject item)
{
// Don't select product groups
return !((Product)item).IsProductGroup;
}
这将允许用户选择大纲视图中的任何单行。 对于不希望用户能够选择的项,则针对 ShouldSelectRow
返回 false
,如果不希望用户能够选择所有项,则针对所有项返回 false
。
键入以选择行
如果想允许用户在选中大纲视图时键入字符,并选择包含该字符的第一行,双击 Main.storyboard
文件,打开它以便在 Interface Builder 中进行编辑。 选择“接口层次结构”中的大纲视图,并在“属性检查器”中选中“类型选择”复选框:
保存更改并返回到 Visual Studio for Mac 以与 Xcode 同步。
现在,让我们编辑 ProductOutlineDelegate.cs
文件并添加以下方法:
public override NSObject GetNextTypeSelectMatch (NSOutlineView outlineView, NSObject startItem, NSObject endItem, string searchString)
{
foreach(Product product in DataSource.Products) {
if (product.Title.Contains (searchString)) {
return product;
}
}
// Not found
return null;
}
GetNextTypeSelectMatch
方法采用给定的 searchString
,并返回第一个在其 Title
中包含该字符串的 Product
的项。
重新排序列
如果要允许用户在大纲视图中拖动重新排序列,请双击 Main.storyboard
文件将其打开,以便在 Interface Builder 中编辑。 在“接口层次结构”中选择大纲视图,并在“属性检查器”中选中“重新排序”复选框:
如果我们为“自动保存”属性提供值并检查“列信息”字段,则对表布局所做的任何更改将自动保存,并在下次运行应用程序时还原。
保存更改并返回到 Visual Studio for Mac 以与 Xcode 同步。
现在,让我们编辑 ProductOutlineDelegate.cs
文件并添加以下方法:
public override bool ShouldReorder (NSOutlineView outlineView, nint columnIndex, nint newColumnIndex)
{
return true;
}
ShouldReorder
方法应返回要允许重新排序到 newColumnIndex
的任何列的 true
,否则返回 false
;
如果运行应用程序,我们可以拖动列标题来重新排序列:
编辑单元格
如果希望允许用户编辑给定单元格的值,请编辑 ProductOutlineDelegate.cs
文件并更改 GetViewForItem
方法,如下所示:
public override NSView GetView (NSOutlineView outlineView, NSTableColumn tableColumn, NSObject item) {
// Cast item
var product = item as Product;
// This pattern allows you reuse existing views when they are no-longer in use.
// If the returned view is null, you instance up a new view
// If a non-null view is returned, you modify it enough to reflect the new data
NSTextField view = (NSTextField)outlineView.MakeView (tableColumn.Title, this);
if (view == null) {
view = new NSTextField ();
view.Identifier = tableColumn.Title;
view.BackgroundColor = NSColor.Clear;
view.Bordered = false;
view.Selectable = false;
view.Editable = !product.IsProductGroup;
}
// Tag view
view.Tag = outlineView.RowForItem (item);
// Allow for edit
view.EditingEnded += (sender, e) => {
// Grab product
var prod = outlineView.ItemAtRow(view.Tag) as Product;
// Take action based on type
switch(view.Identifier) {
case "Product":
prod.Title = view.StringValue;
break;
case "Details":
prod.Description = view.StringValue;
break;
}
};
// Setup view based on the column selected
switch (tableColumn.Title) {
case "Product":
view.StringValue = product.Title;
break;
case "Details":
view.StringValue = product.Description;
break;
}
return view;
}
现在,如果运行应用程序,用户可以编辑表视图中的单元格:
在大纲视图中使用图像
若要在单元格中包含NSOutlineView
图像,需要更改大纲视图NSTableViewDelegate's
GetView
方法返回数据的方式,以使用NSTableCellView
常规NSTextField
数据。 例如:
public override NSView GetView (NSOutlineView outlineView, NSTableColumn tableColumn, NSObject item) {
// Cast item
var product = item as Product;
// This pattern allows you reuse existing views when they are no-longer in use.
// If the returned view is null, you instance up a new view
// If a non-null view is returned, you modify it enough to reflect the new data
NSTableCellView view = (NSTableCellView)outlineView.MakeView (tableColumn.Title, this);
if (view == null) {
view = new NSTableCellView ();
if (tableColumn.Title == "Product") {
view.ImageView = new NSImageView (new CGRect (0, 0, 16, 16));
view.AddSubview (view.ImageView);
view.TextField = new NSTextField (new CGRect (20, 0, 400, 16));
} else {
view.TextField = new NSTextField (new CGRect (0, 0, 400, 16));
}
view.TextField.AutoresizingMask = NSViewResizingMask.WidthSizable;
view.AddSubview (view.TextField);
view.Identifier = tableColumn.Title;
view.TextField.BackgroundColor = NSColor.Clear;
view.TextField.Bordered = false;
view.TextField.Selectable = false;
view.TextField.Editable = !product.IsProductGroup;
}
// Tag view
view.TextField.Tag = outlineView.RowForItem (item);
// Allow for edit
view.TextField.EditingEnded += (sender, e) => {
// Grab product
var prod = outlineView.ItemAtRow(view.Tag) as Product;
// Take action based on type
switch(view.Identifier) {
case "Product":
prod.Title = view.TextField.StringValue;
break;
case "Details":
prod.Description = view.TextField.StringValue;
break;
}
};
// Setup view based on the column selected
switch (tableColumn.Title) {
case "Product":
view.ImageView.Image = NSImage.ImageNamed (product.IsProductGroup ? "tags.png" : "tag.png");
view.TextField.StringValue = product.Title;
break;
case "Details":
view.TextField.StringValue = product.Description;
break;
}
return view;
}
有关详细信息,请参阅使用图像文档中的在大纲视图中使用图像部分。
数据绑定大纲视图
通过在 Xamarin.Mac 应用程序中使用键值编码和数据绑定技术,可以大大减少必须编写和维护的代码量,以填充和使用 UI 元素。 还可以从前端用户界面 (Model-View-Controller)进一步分离支持数据(数据模型),从而更轻松地维护、更灵活的应用程序设计。
键值编码 (KVC) 是间接访问对象属性的机制,使用键(特殊格式的字符串)来标识属性,而不是通过实例变量或访问器方法 (get/set
) 访问它们。 通过在 Xamarin.Mac 应用程序中实现符合键值编码的访问器,可以访问其他 macOS 功能,例如键值观察 (KVO)、数据绑定、核心数据、Cocoa 绑定和可脚本性。
有关详细信息,请参阅“数据绑定和键值编码”文档的“大纲视图数据绑定”部分。
总结
本文详细介绍了如何使用 Xamarin.Mac 应用程序中的大纲视图。 我们了解了大纲视图的不同类型和用法,如何在 Xcode 的 Interface Builder 中创建和维护大纲视图,以及如何在 C# 代码中使用大纲视图。