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

使用 MonoDevelop 和 Gtk3 在 Gtk# 中创建自定义模板 ComboBox 控件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2020 年 3 月 28 日

GPL3

8分钟阅读

viewsIcon

6456

downloadIcon

111

C# ComboBox 控件

引言

在本文中,我将介绍一个使用 C# 编写的 ComboBox 控件,它封装了一个 Gtk 3 的 ComboBox 控件,以便简化其使用,同时利用它提供的功能,例如它包含的 ListStore。该控件建立在我之前一篇文章 [1] 中介绍的 HComboBox 的开发经验之上。

目标

  • HComboBox 的目的是尽可能精简和简单,TComboBox 的目的是能够以一致的方式处理更复杂和多样的对象。
  • 通过分享这个例子,鼓励其他程序员使用 Mono / MonoDevelop / Gtk# 组合。
  • 使其易于使用,就像一个基本的 Gtk 小部件一样,同时增加尽可能少的开销。
  • 通过隐藏 Gtk 特定的实现细节,使其对 Windows / Forms / WPF 程序员更加易于访问。

致谢

感谢 Mono 和 MonoDevelop 团队在 Linux 上运行 C# 程序。还要感谢 Gtk# 团队提供的框架,特别是对于本文,感谢他们提供的 TreeView / ListView 实现。此外,GtkSharp 教程 [2] 也提供了巨大的帮助。

先决条件

虽然不是强制阅读,但我建议您查看我 HComboBox 文章 [3] 中的“备注”部分,我将在其中介绍 IListable 接口的概念及其存在的理由。

背景

我正在将我的一些 Windows C# WPF / MVVM 应用程序移植到 Linux / Mono / Gtk3。我正在边学边学习 Gtk 工具集,并且一直在尝试制作工具,以便尽可能多地利用我现有的代码。我学会使用的第一个 Gtk ComboBoxComboBoxText。当时,Gtk ComboBox 似乎有些令人生畏,而 ComboBoxText 则简单得多,因此 HComboBox 是围绕 ComboBoxText 构建的。

随着我对 Gtk 小部件,特别是 TreeViewListore 的熟悉,Gtk ComboBox 对我来说变得更加易于使用,因为它实际上是伪装的 TreeView

概念

如果将 ComboBox 的功能简化到最基本的形式,您会得出结论,为了完成其工作,它只需要

  • 一个整数“key”或索引,以便能够跟踪每个项目,
  • 以及一个与之关联的字符串,用于显示为该项目的描述。

这就是 HComboBox 控件背后的思想,它操作一个 KeyValuePair<int,String> 项目列表。对于非常简单的项目,这种方法效果很好,但对于更复杂的应用程序,它有一个非常严重的限制:只能通过 ComboBox 的“Selected Item key”来检索项目,因为这是我们拥有的全部。然而,在许多情况下,这还不够。我们需要实际的对象。我们必须能够将此 key 与底层对象关联起来。

HComboBox 可以设想与类似这样的内容一起工作

	public class ComboItem
	{
		public int		Key			{ get; set; }
		public String	Description 	{ get; set; }
	}

基于此,我们提出了 IListable 接口,如下所示

public interface IListable
{
	int    Key 			{ get; }
	String Description 	{ get; }
}

这具有我们刚才提到的缺点。

我们实际上需要的是更像这样的东西

	public class ComboItem
	{
		public int		Key			{ get; set; }
		public String	Description 	{ get; set; }
		public object  Tag			{ get; set; }
	}

这会产生一个看起来像这样的 IListable 接口

    public interface IListable
    {
		int       Key 		  { get; }
		String    Description { get; }
		IListable Record      { get; }
    }

我们需要再添加一个细节才能达到我们想要的状态,即对象能够定位自身的能力,所以最后我们有了这个

    public interface IListable : IEquatable<IListable>
    {
		int       Key 		  { get; }
		String    Description { get; }
		IListable Record      { get; }
    }

我花了这么多时间在 IListable 接口上,是因为它允许我们使用模板。而 Gtk ComboBox 控件可以与模板一起使用。

解决方案

以上所有内容都导致我们得到了本文介绍的 TComboBox 控件,它包含一个使用模板的 ComboBox,因此得名。

由于它与 HComboBox 相关(但不是派生自),因此暴露的属性和方法是相似的。

属性

  • 通过构造函数设置 Height / Width

方法

	void LoadItems( Ilist<IListable> lst ) 

将项目列表加载到 ComboBox 中。

	IListable GetSelection() 

获取选定的项目。IListable 的重要性现在显而易见了,因为这样我们就可以通过“Record”属性访问底层对象。

要设置选定的项目,我们现在有两个方法可用。

这个...

	void SetSelectionByKey( int nKey ) 

...与 HcomboBoxSetSelection(nKey) 方法一样,即通过列表中项目的索引,而...

	void SetSelectedItem( IListable item ) 

...通过实际对象设置选定的项目,类似于 WPF 的 SelectedItem 属性。

事件

它公开了 Changed 事件,客户端可以使用它来知道何时更改了选定的项目。

实现细节

我说 Gtk ComboBox 实际上是伪装的 TreeView 时,我并没有开玩笑。它是。

所有您可以用 TreeViews、ListStores 和 Renderers 做的事情,您也可以应用到 ComboBox。这对我是个启示。

您可以配置一个 ListStore 并将其分配给 Combo 的 Model 属性。您可以定义一个 Renderer 并将其附加到一个 CellRenderer,然后将其添加到 Combo。这是非常强大的功能!

我们的 TComboBox 控件,除了 ComboBox 成员外,还有一个 ListStore 成员。

我们将 ListStore 成员初始化为存储 IListable 对象

private void InitializeDetailList()
{
	m_dtlStore = new ListStore( typeof(IListable) );
} 

我们定义了一个名为 RenderDescription() 的方法,该方法渲染将要显示的 Text,并在 InitializeComponent() 方法中将所有内容连接起来

private void InitializeComponent()
{
	… 
	m_cbo.Model = m_dtlStore;
	var crt = new CellRendererText();
	m_cbo.PackStart( crt, true );
	m_cbo.SetCellDataFunc( crt, 
			new CellLayoutDataFunc( RenderDescription ) );
	...
}

这四行代码是此 ComboBox 控件的核心,但就我们所知,它们也可以初始化一个 TreeView

LoadItems() 非常简单,因为我们现在正在加载 ListStore

SetSelectionByKey() 方法同样简单,它只是设置 ComboBox 成员的 Active 属性。

IListable GetSelection() 方法是事情开始变得有趣的地方。它使用迭代器,并检索实际选定的对象,这正是我们想要实现的。

拼图的最后一块是 SetSelectedItem(IListable item) 方法,其中 IEquatable 的原因变得清晰:要将一个项目设置为选中状态,我们首先必须找到它。这是通过 TreeIter FindDetail(IListable item) 方法完成的,该方法的核心代码是这些行

IListable dt = ( model.GetValue( iter, nRecordCol ) as IListable );
// this is where the IEquatable implementation is applied.
if( dt == item )  // found!
    return iter ; 

找到项目后,FindDetail() 返回 Iterator,然后我们使用它来设置选定的项目...

IListable rec = ( m_cbo.Model.GetValue( iter, 0 ) as IListable )  ; 
if( rec != null )
  m_cbo.Active = rec.Key ; 

...就是这样。

示例用法

请参阅附件示例程序的 MainWindow 类,了解如何使用此控件的示例。

为了演示 IListable 接口如何使我们能够将 TComboBox 与不同的类一起使用,在附件的示例应用程序中,我提供了三个简单的类供我们使用。

HComboBox 示例应用程序中,我们使用了一个简单的 WeekDay 类(事实证明 DayOfWeekDateTime 使用),它看起来像这样

public class WeekDay
{
public int    Day  { get; set; }
public String Name { get; set; } 
}

而在这里它演变成这样

public class WeekDay : IListable
{
	public int    Day  { get; set; }
	public String Name { get; set; }

	public int       Key         { get { return Day ; } }
	public String    Description { get { return Name; } }
	public IListable Record      { get { return this; } }
}

同样,我们定义了一个 MonthOfYear

public class MonthOfYear : IListable
{
public int Month { get; set; }
public String Name  { get; set; }

public int   Key { get { return Month ; } }
public String Description { get { return Name; } }
public IListable Record  { get { return this; } }
}

最后是一个更接近真实世界领域对象的类

public class PersonTitle : IListable
{
public long  ID   { get; set; }          // assume this is a domain Primary Key
public int   Key  { get; set; }
public String Description  { get; set; }
public String Abbreviation { get; set; } // additional properties...

public IListable Record   { get { return this; } }
}

MainWindow 类包含三个 TComboBoxes,用于显示上述三个类的集合。

看看 InitializeLookupLists() 方法如何初始化组合框。请注意,现在不再需要 ConvertToComboList() 方法来转换我们的域对象。如果我们有返回实际对象的遗留方法,例如 IList<PersonTitle> GetPersonTitles(),LinQ 会来帮助我们进行打包

// notice how Linq lets us cast the PersonTitles to IListable on the fly
m_cbxTitles.LoadItems( 
       ( from x in GetPersonTitles() select ( x as IListable ) ).ToList() ); 

最后,请参阅 OnTitleSelectionChanged() 方法,这是最重要的部分

如何从选定的项目检索实际的域对象(即解包)。

// cast IListable to the actual class
PersonTitle sel = ( PersonTitle ) m_cbxTitles.GetSelection();
//
// apply PersonTitle-specific business logic here...
//
if( sel.Abbreviation.CompareTo( sel.Description ) != 0  ) ...

备注

我必须再次强调 TreeView / ListView 模式的强大功能。本文实现的 TComboBox 实际上是一个只有一列的 TreeView,它用于显示每个项目的 Description。它“只有一列”,因为我想模仿“经典”的 ComboBox 外观。

如果我们想添加更多列,例如在 Description 旁边显示图像,可以通过与向任何其他 TreeView / ListView 添加列相同的方式轻松完成。

给读者的练习

搜索 ListView

如果您查看我实现的 TreeIter FindDetail(IListable item) 函数,您会注意到我使用了暴力破解方法——我开始迭代列表直到找到我正在寻找的项目。我对这一点不满意。我唯一的安慰是 ComboBox 通常只有少量项目,因此影响很小,但仍然...

我理解 ListStore 是作为链表实现的,根据我在网上找到的信息,这似乎是唯一的方法。我非常希望能找到一种更有效的方法。如果有人有什么更好的建议,请分享您的智慧。

要求

  • Mono (目前我有 6.6.0.166 版本)
  • MonoDevelop (当前版本 7.8.4)
  • Gtk3 库(目前版本为 3.22.25.56)

提供的示例应用程序是一个 MonoDevelop 解决方案。它需要 Gtk 3 包。

由于这些是 DLL,因此不包含在示例项目中,但包括了 packages.config 文件。在 MonoDevelop 中打开解决方案后,如果未自动完成,请选择还原/更新程序包,NuGet 将会获取它们。

参考 / 另请参阅

GtkSharp TreeView 教程,其中对 TreeView / ListView 实现的模型、视图、控制器模式进行了非常好的描述,并且包含一些很好的示例来演示其功能。

历史

  • 2020 年 3 月 27 日:初始版本
© . All rights reserved.