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





5.00/5 (2投票s)
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 ComboBox
是 ComboBoxText
。当时,Gtk ComboBox
似乎有些令人生畏,而 ComboBoxText
则简单得多,因此 HComboBox
是围绕 ComboBoxText
构建的。
随着我对 Gtk 小部件,特别是 TreeView
和 Listore
的熟悉,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 )
...与 HcomboBox
的 SetSelection(nKey)
方法一样,即通过列表中项目的索引,而...
void SetSelectedItem( IListable item )
...通过实际对象设置选定的项目,类似于 WPF 的 SelectedItem
属性。
事件
它公开了 Changed
事件,客户端可以使用它来知道何时更改了选定的项目。
实现细节
我说 Gtk ComboBox
实际上是伪装的 TreeView
时,我并没有开玩笑。它是。
所有您可以用 TreeView
s、ListStore
s 和 Renderer
s 做的事情,您也可以应用到 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
类(事实证明 DayOfWeek
被 DateTime
使用),它看起来像这样
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
类包含三个 TComboBox
es,用于显示上述三个类的集合。
看看 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 日:初始版本