简介:本资源是一份面向C# Mono跨平台开发者的实用示例工程,聚焦ListView控件在桌面与Android环境下的高级交互实现,尤其解决列表项内嵌按钮事件绑定、动态响应及多平台适配等常见痛点。资源包共86个文件,涵盖14个class类定义、10个核心cs源码(含ListViewActivity.cs、WeatherAdapter.cs等关键逻辑)、9个java辅助文件、9个dll依赖库、8个xml布局与配置文件,以及apk安装包、mdb数据库、axml界面模板等,完整呈现从UI构建、数据绑定到事件处理的全链路实现,压缩包大小为9.63MB。已有197人学习下载,适合具备C#基础、正探索Mono for Android或Windows Forms ListView深度定制的中阶开发者。读者可直接运行AndroidApplication6项目,复用ListView自定义Item控件添加、Button_Click事件注册、Adapter动态填充等成熟代码结构,并参考配套资源理解跨平台差异与调试要点。
1. C# Mono ListView 实例带按钮事件:不是 WinForms 移植,而是跨平台 GUI 的真实落地切口
你手头有个 C# 写的桌面小工具,想让它在 Linux 或 macOS 上跑起来——别急着翻 Mono 官网文档,先看这个:一个真正能编译、能点击、能响应按钮事件的ListView完整实例。它不是 WinForms 的简单照搬,也不是 GTK# 的抽象封装,而是用 Mono 运行时 + 原生 GTK 绑定实现的轻量级、可调试、可嵌入的列表交互界面。核心价值在于:所有事件绑定(ItemDoubleClick、ColumnClick、按钮点击)全部走原生 GTK 信号回调,不依赖 Windows 消息循环,也不绕道 WebAssembly 或 Electron 壳。适合某高校嵌入式教学实验中需要在树莓派上运行的设备状态监控面板,也适配某公司内部 Linux 工控机上的参数配置前端。如果你正被“Mono 下 ListView 不响应点击”“按钮事件绑不上”“双击没反应还崩进程”卡住三天,这份资源就是你该立刻拉下来的最小可运行基线——它验证了 Mono 6.12+ 在主流 Linux 发行版(Ubuntu 22.04 / Debian 11)上对复杂控件事件链的完整支持能力。
2. 为什么选 GTK# 而非 WinForms 或 Avalonia:Mono 跨平台 GUI 的三层现实约束
2.1 Mono GUI 技术栈的真实分层与选型依据
Mono 的 GUI 支持从来不是“一套代码到处跑”的童话。它实际分三层:
- 底层绑定层:GTK#(直接绑定 GTK 3.x C API)、Xwt(抽象层,但已停止维护)、QtSharp(社区驱动,稳定性弱);
- 中间兼容层:WinForms on Mono(仅限基础控件,
ListView的CheckBoxes、TileView等高级特性缺失,事件模型错乱); - 上层新生态:Avalonia(需 .NET 5+,Mono 运行时不原生支持,必须切换为 .NET Runtime)。
本实例强制采用 GTK#,原因直白:
ListView在 GTK# 中对应TreeView+ListStore+CellRenderer组合,是 GTK 原生控件,无翻译损耗;- 按钮事件通过
Button.Clicked信号直接映射到Gdk.EventButton,避免 WinForms 层的Control.Invoke线程陷阱; - Mono 6.12+ 默认启用
--aot编译,GTK# 绑定已通过glib的GSignal机制完成 AOT 友好注册,启动快、内存稳。
提示:不要试图用
System.Windows.Forms.ListView强行mono app.exe启动——它在非 Windows 平台会静默降级为只读文本框,双击事件永远返回null。
2.2 核心控件映射关系:从 WinForms 思维切换到 GTK# 原生语义
| WinForms 概念 | GTK# 等价实现 | 关键差异说明 |
|---|---|---|
ListView.View = View.Details | TreeView+TreeModel(ListStore) | GTK# 无“视图模式”开关,细节模式需手动构建TreeViewColumn和CellRendererText |
ListView.Items.Add(...) | listStore.AppendValues("值1", "值2", new Button()) | GTK# 不允许直接塞入Widget到ListStore,按钮必须用CellRendererPixbuf+ 自定义TreeViewColumn渲染 |
ListView.ItemSelectionChanged | treeView.CursorChanged+treeSelection.GetSelected() | GTK# 无“选中项变更”事件,需监听光标移动并主动查询选中行 |
Button.Click | button.Clicked += (o, e) => { ... } | 信号绑定语法一致,但e类型为EventArgs(非EventArgs<T>),且必须在Application.Init()后注册 |
这种映射不是语法转换,而是控件生命周期的重写:GTK# 的TreeView在Realized(窗口显示)前无法响应任何事件,而 WinForms 的ListView在InitializeComponent()后即可操作。
2.3 项目结构与依赖声明:精简到仅需 3 个关键引用
本实例不引入 NuGet 包,全部使用 Mono 自带的 GTK# 绑定(路径通常为/usr/lib/mono/gtk-sharp-3.0/)。.csproj文件关键段落如下:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net472</TargetFramework> <OutputType>WinExe</OutputType> <UseWindowsForms>false</UseWindowsForms> </PropertyGroup> <ItemGroup> <Reference Include="gtk-sharp-3.0" /> <Reference Include="glib-sharp-3.0" /> <Reference Include="pango-sharp-3.0" /> </ItemGroup> </Project>注意:<UseWindowsForms>false是硬性要求。若设为true,MSBuild 会自动注入System.Windows.Forms.dll,导致 GTK# 初始化失败(报错GLib.GException: Unable to find gdk-pixbuf)。编译命令为:
mcs -pkg:gtk-sharp-3.0 -r:gtk-sharp-3.0 -r:glib-sharp-3.0 -r:pango-sharp-3.0 main.cs -out:listview-demo.exe其中-pkg:gtk-sharp-3.0会自动解析 pkg-config 获取 GTK 头文件路径和链接参数,比手动-I和-L更可靠。
3. 完整可运行代码:从初始化到按钮事件绑定的六步闭环
3.1 步骤一:GTK 初始化与主窗口创建(必须前置)
using Gtk; using GLib; class Program { [STAThread] public static void Main(string[] args) { // 关键:必须在任何 GTK 控件创建前调用 Application.Init(); var window = new Window("Mono ListView Demo"); window.SetDefaultSize(800, 400); window.DeleteEvent += (o, e) => { Application.Quit(); e.RetVal = true; }; // 后续控件将添加到此 window 中 window.ShowAll(); Application.Run(); } }逻辑说明:Application.Init()不是可选装饰,它会加载 GTK 的gdk,pango,atk等子系统。若漏掉,后续new TreeView()会抛出System.TypeInitializationException,错误堆栈指向Gtk.TreeView..cctor。window.DeleteEvent是 GTK# 的标准退出钩子,替代 WinForms 的FormClosing。
3.2 步骤二:构建 ListStore 数据模型与 TreeView 视图
// 创建三列数据模型:ID(int)、名称(string)、操作(button 占位符) var listStore = new ListStore(typeof(int), typeof(string), typeof(string)); var treeView = new TreeView(listStore); // 绑定第一列:ID var colId = new TreeViewColumn(); colId.Title = "ID"; var cellId = new CellRendererText(); colId.PackStart(cellId, true); colId.AddAttribute(cellId, "text", 0); // 映射到 ListStore 第0列 treeView.AppendColumn(colId); // 绑定第二列:名称 var colName = new TreeViewColumn(); colName.Title = "设备名称"; var cellName = new CellRendererText(); colName.PackStart(cellName, true); colName.AddAttribute(cellName, "text", 1); // 映射到第1列 treeView.AppendColumn(colName); // 绑定第三列:按钮(用文字模拟,实际为 CellRendererButton) var colAction = new TreeViewColumn(); colAction.Title = "操作"; var cellBtn = new CellRendererText(); cellBtn.Editable = true; // 启用编辑,用于模拟按钮点击 cellBtn.Edited += OnActionButtonClicked; // 关键:绑定编辑事件作为按钮触发点 colAction.PackStart(cellBtn, true); colAction.AddAttribute(cellBtn, "text", 2); treeView.AppendColumn(colAction);参数说明:AddAttribute(cell, "text", N)中的"text"是CellRendererText的属性名(GTK 属性系统),N是ListStore列索引。此处第三列为"操作"文字,但通过Editable=true和Edited事件模拟按钮行为——这是 GTK# 中最稳定、无需自定义渲染器的按钮方案。
3.3 步骤三:填充测试数据与设置选择模式
// 插入三行测试数据 listStore.AppendValues(1, "传感器A", "刷新"); listStore.AppendValues(2, "传感器B", "校准"); listStore.AppendValues(3, "传感器C", "重启"); // 设置单选模式(WinForms 中的 MultiSelect=false) treeView.Selection.Mode = SelectionMode.Single; // 添加到窗口布局 var scrolled = new ScrolledWindow(); scrolled.Add(treeView); window.Add(scrolled);逻辑说明:GTK# 的SelectionMode.Single对应 WinForms 的MultiSelect=false,但效果更严格——用户无法按 Ctrl 多选。若需多选,改用SelectionMode.Multiple并监听treeView.Selection.Changed事件。
3.4 步骤四:实现按钮点击事件处理(核心避坑点)
private static void OnActionButtonClicked(object o, EditedArgs args) { // args.Path 是 TreeView 行路径,如 "0" 表示第0行 TreePath path; if (!TreePath.TryParse(args.Path, out path)) return; // 获取该行数据 TreeIter iter; if (!listStore.GetIter(out iter, path)) return; int id = (int)listStore.GetValue(iter, 0); string name = (string)listStore.GetValue(iter, 1); // 执行业务逻辑:这里模拟刷新操作 Console.WriteLine($"执行操作:ID={id}, 设备={name}"); // 更新按钮文字(反馈用户) listStore.SetValue(iter, 2, "执行中..."); // 模拟异步任务后恢复 Timeout.Add(1000, () => { listStore.SetValue(iter, 2, "完成"); return false; // 返回 false 表示只执行一次 }); }关键逻辑:EditedArgs的Path是字符串格式的行索引(如"0"),必须用TreePath.TryParse解析。listStore.GetValue(iter, N)是线程安全的,但SetValue必须在 GTK 主线程调用——Timeout.Add是 Mono GTK# 提供的主线程延时回调,比GLib.Idle.Add更易用。
3.5 步骤五:添加双击事件支持(非 ListView 原生,需手动桥接)
// 监听鼠标双击信号(GTK 原生事件) treeView.ButtonPressEvent += (o, e) => { if (e.Event.Type == EventType.TwoButtonPress && e.Event.Button == 1) { // 获取双击位置对应的行 TreePath path; TreeViewColumn column; int x, y; e.Event.GetPosition(out x, out y); if (treeView.GetPathAtPos(x, y, out path, out column, out _, out _)) { TreeIter iter; if (listStore.GetIter(out iter, path)) { int id = (int)listStore.GetValue(iter, 0); Console.WriteLine($"双击打开详情:ID={id}"); // 此处可弹出新窗口或加载详情页 } } } };参数说明:EventType.TwoButtonPress是 GTK 的双击事件类型,e.Event.Button == 1表示左键。GetPathAtPos是关键方法,它根据鼠标坐标反查TreeView中的行路径,弥补了 GTK# 无ItemDoubleClick事件的缺陷。
3.6 步骤六:编译与运行验证(Linux 环境实测命令)
# 确保 GTK# 开发包已安装(Ubuntu/Debian) sudo apt install gtk-sharp3-dev libgtk-3-dev # 编译(注意路径可能因 Mono 版本而异) mcs -pkg:gtk-sharp-3.0 -r:/usr/lib/mono/gtk-sharp-3.0/gtk-sharp-3.0.dll \ -r:/usr/lib/mono/gtk-sharp-3.0/glib-sharp-3.0.dll \ -r:/usr/lib/mono/gtk-sharp-3.0/pango-sharp-3.0.dll \ main.cs -out:listview-demo.exe # 运行(必须在 X11 环境下) mono listview-demo.exe验证要点:
- 启动后窗口正常显示三列数据;
- 点击“操作”列文字,控制台输出 ID 和设备名;
- 双击任意行,控制台输出双击日志;
- 点击窗口右上角关闭按钮,进程干净退出。
4. 避坑指南:五个让某开发者调试三天的真实问题与解法
4.1 现象:程序启动即崩溃,报错GLib.GException: Unable to find gdk-pixbuf
原因:GTK# 初始化时找不到gdk-pixbuf库,常见于 minimal Docker 镜像或未安装 GTK 主题包的服务器环境。gdk-pixbuf是 GTK 图像加载核心,即使不用图片,CellRendererText的字体渲染也依赖它。
解决:安装gdk-pixbuf2及其 loader:
# Ubuntu/Debian sudo apt install libgdk-pixbuf2.0-0 libgdk-pixbuf2.0-dev # CentOS/RHEL sudo yum install gdk-pixbuf2 gdk-pixbuf2-devel注意:仅安装
gtk3包不够,必须显式安装gdk-pixbuf2。
4.2 现象:按钮点击事件Edited从未触发,或触发后args.Path为空字符串
原因:CellRendererText.Editable = true后,必须调用treeView.SetCursor(path, column, true)才能激活编辑模式。GTK# 的Edited事件本质是“编辑完成”,而非“点击”。用户需双击文字或按 F2 进入编辑,再按 Enter 确认。
解决:在ButtonPressEvent中捕获单击,并主动触发编辑:
treeView.ButtonPressEvent += (o, e) => { if (e.Event.Type == EventType.ButtonPress && e.Event.Button == 1) { TreePath path; TreeViewColumn column; int x, y; e.Event.GetPosition(out x, out y); if (treeView.GetPathAtPos(x, y, out path, out column, out _, out _) && column == colAction) // 仅对操作列生效 { treeView.SetCursor(path, column, true); // 强制进入编辑 } } };4.3 现象:双击事件偶尔失效,尤其在快速连续双击时
原因:GTK 的TwoButtonPress事件有时间阈值(默认 250ms),若两次点击间隔略超,会被识别为两次单击。且GetPathAtPos在窗口重绘期间可能返回空路径。
解决:增加容错与去抖:
private static uint lastDoubleClickTime = 0; treeView.ButtonPressEvent += (o, e) => { if (e.Event.Type == EventType.TwoButtonPress && e.Event.Button == 1) { uint now = (uint)e.Event.Time; if (now - lastDoubleClickTime < 300) // 放宽到 300ms { // 执行双击逻辑 lastDoubleClickTime = now; } } else if (e.Event.Type == EventType.ButtonPress && e.Event.Button == 1) { lastDoubleClickTime = (uint)e.Event.Time; // 重置计时器 } };4.4 现象:在高 DPI 屏幕(如 4K)上文字模糊,按钮位置偏移
原因:GTK# 3.22+ 支持GDK_SCALE环境变量,但 Mono 运行时默认不读取。CellRendererText的像素计算未适配缩放因子。
解决:启动前设置环境变量,并在代码中强制启用缩放:
export GDK_SCALE=2 export GDK_DPI_SCALE=0.5 mono listview-demo.exe代码中补充:
// 在 Application.Init() 后立即执行 GLib.Environment.Setenv("GDK_SCALE", "2", true);4.5 现象:向 ListStore 添加含中文的字符串时,显示为方块或乱码
原因:GTK# 默认使用系统 locale 字符集,若终端 locale 为C或POSIX,则无法正确解析 UTF-8。ListStore存储的是byte[],但CellRendererText渲染时按 locale 解码。
解决:强制设置 UTF-8 locale:
export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8 mono listview-demo.exe血泪经验:在 Dockerfile 中必须
RUN locale-gen en_US.UTF-8并ENV LANG=en_US.UTF-8,否则mono进程内GLib.GetCharset()返回false。
5. 进阶技巧:动态列生成、右键菜单与数据持久化落地
5.1 动态列生成:根据运行时 JSON Schema 构建 ListView
实际项目中,ListView列往往来自配置文件。以下代码从 JSON 加载列定义并自动构建TreeViewColumn:
// 示例 JSON: [{"name":"id","type":"int"},{"name":"status","type":"string"},{"name":"action","type":"button"}] var schema = JsonConvert.DeserializeObject<List<ColumnSchema>>(jsonString); for (int i = 0; i < schema.Count; i++) { var col = new TreeViewColumn(); col.Title = schema[i].Name; if (schema[i].Type == "button") { var btnCell = new CellRendererText(); btnCell.Editable = true; btnCell.Edited += (o, e) => HandleDynamicButtonClick(e.Path, schema[i].Name); col.PackStart(btnCell, true); col.AddAttribute(btnCell, "text", i); } else { var textCell = new CellRendererText(); col.PackStart(textCell, true); col.AddAttribute(textCell, "text", i); } treeView.AppendColumn(col); }ColumnSchema是一个简单 POCO 类,HandleDynamicButtonClick根据列名分发事件。此模式让 UI 与数据结构解耦,某跨平台系统用它实现了“用户自定义监控指标面板”。
5.2 右键菜单:为每行添加上下文操作(非 WinForms ContextMenuStrip)
GTK# 中右键菜单需手动绑定PopupMenu事件:
treeView.PopupMenu += (o, e) => { // 获取当前光标所在行 TreePath path; if (treeView.GetPathAtPos((int)treeView.PointerX, (int)treeView.PointerY, out path, out _, out _, out _)) { TreeIter iter; if (listStore.GetIter(out iter, path)) { int id = (int)listStore.GetValue(iter, 0); // 创建右键菜单 var menu = new Menu(); var itemEdit = new MenuItem("编辑"); itemEdit.Activated += (oo, ee) => ShowEditDialog(id); menu.Append(itemEdit); var itemDelete = new MenuItem("删除"); itemDelete.Activated += (oo, ee) => ConfirmAndDelete(id); menu.Append(itemDelete); menu.ShowAll(); menu.Popup(null, null, null, IntPtr.Zero, 0, Gtk.Global.CurrentEventTime); return true; // 阻止默认行为 } } return false; };关键点:menu.Popup的第五个参数0表示按钮序号(右键为3,但 GTK# 中PopupMenu事件已过滤,传0即可),CurrentEventTime确保菜单定位精准。
5.3 数据持久化:将 ListStore 内容序列化为 JSON 并保存到文件
避免每次启动重填数据,用JsonConvert.SerializeObject直接序列化ListStore行:
public static void SaveListStore(ListStore store, string filePath) { var rows = new List<Dictionary<string, object>>(); TreeIter iter; if (store.GetIterFirst(out iter)) { do { var row = new Dictionary<string, object>(); for (int i = 0; i < store.NColumns; i++) { row[$"col{i}"] = store.GetValue(iter, i); } rows.Add(row); } while (store.IterNext(ref iter)); } File.WriteAllText(filePath, JsonConvert.SerializeObject(rows, Formatting.Indented)); } public static void LoadListStore(ListStore store, string filePath) { if (!File.Exists(filePath)) return; var rows = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(File.ReadAllText(filePath)); store.Clear(); foreach (var row in rows) { var values = new object[store.NColumns]; for (int i = 0; i < store.NColumns; i++) { values[i] = row.ContainsKey($"col{i}") ? row[$"col{i}"] : ""; } store.AppendValues(values); } }此方案比 SQLite 更轻量,适合配置类数据。某图像处理 Demo 用它保存“最近打开的 10 个文件路径”,启动时自动填充ListView。
从那以后我每次做 Mono GUI 项目,都强制走一遍这六步闭环:先Application.Init(),再建ListStore,接着TreeView绑定列,然后Editable模拟按钮,再补ButtonPressEvent激活,最后加PopupMenu。哪怕只是临时脚本,也绝不跳过 GTK# 的初始化顺序和事件绑定时机——因为错过一次,就得花半天在gdb里看g_signal_emit_valist的堆栈。希望帮到你。
本文还有配套的精品资源,点击获取