使用 Mono Develop 编程 Xlib - 第一部分:低级(概念验证)。





5.00/5 (4投票s)
如何从 Mono Develop C# 调用原生 X11 API,最终创建一个非常小的应用程序。
介绍
本文展示了如何使用 Mono Develop 从 C# 访问 Xlib/X11 API。许多 API 调用已经定义并测试完毕,可以直接使用,并且一些挑战已经攻克,例如,使用透明位图显示或提供模态对话框。
本文旨在验证使用 C# 进行 Xlib/X11 编程可以轻松实现(概念验证)。它提供了一个示例应用程序的完整项目,包括 32 位和 64 位版本。
背景
至今,许多实用的 Unix/Linux 小工具仍然是使用 C/C++ 针对 Xlib/X11 编写的。迁移到 C# 可以简化维护并提供新的机会。但开发者往往固守其固有的开发环境。一方面,从 GUI 开发环境 Xlib/X11 转换到 GTK 或 Qt 有许多优势,如快速原型设计或现成的高级 GUI 元素。另一方面,这需要放弃旧习惯,拒绝用低级 API 实现的针对特定问题优化的解决方案,并失去对一些钟爱的 GUI 实现细节的控制。此外,程序员们看到 GUI 工具包层出不穷,尤其是在 Windows 平台上,因此对彻底的改变持谨慎态度。
那些愿意从像 C/GCC 这样的老式语言/IDE 转换到像 C#/Mono Develop 这样的现代语言/IDE 的人,可能会担心同时切换 GUI 开发环境所带来的工作量或不确定性。但改变语言并坚持使用成熟的 GUI 开发环境是可能的。
如果程序员不想失去对 GUI 细枝末节的控制,如果他依赖于速度和零开销成本,或者如果他只是觉得针对 Xlib/X11 编程很有趣或很舒服,他会发现这个小示例应用程序提供了一些有用的信息,用于 C# 与 Xlib/X11 之间的通信。
使用代码
该示例应用程序是在 OPEN SUSE 11.3 Linux 32 位英文版和 GNOME 桌面上使用 Mono Develop 2.4.1 for Mono 2.8.1 编写的。移植到任何更旧或更新的版本应该都不是问题。该示例应用程序的解决方案包含两个项目(提供完整源代码下载):
- X11Wrapper 定义了对 libX11.so 进行 Xlib/X11 调用的函数原型、结构和类型。
- X11 包含了基于一个非常轻量级的部件集——“Roma Widget Set”(Xrw) 的示例应用程序。
C# 的限制
由于 C# 是一种动态解释型语言,它不支持预处理器为编译型语言 C/C++ 添加的功能。这主要影响类型声明,比如特定的 X11 类型名称,无法被充分模拟。
#typedef unsigned long Pixel;
我找到的折衷方法是定义枚举,例如:
public enum TPixel : int {};
但是没有自动类型转换,所有的转换操作都必须显式指定,例如:
TPixel p1 = (TPixel)0;
目前,所有的类型转换都是应用程序代码的一部分。一种避免大量转换的方法是封装 Xlib/X11 API 调用,并在封装函数内部进行转换。但这只对那些在 32 位和 64 位操作系统之间位大小不变的类型才能无障碍地工作。
32 位 vs. 64 位
此外,C/C++ 和 C# 对不同位大小的类型使用相同的类型说明符。为了避免开发过程中的类型不匹配,那些可能因名称相同但位大小不同,或名称不同但位大小相同而引起混淆的类型,也被定义为枚举。一个例子是 long
,它在 C# 中始终是 8 字节,但在 C/C++ 中,32 位系统下可能是 4 字节,64 位系统下是 8 字节。
public enum TLong : int {}; // X11 32 Bit: 4 Bytes: 0 to 65535
//public enum TLong : long {}; // X11 64 Bit: 8 Bytes: -9223372036854775807 to 9223372036854775807
public enum TUlong : uint {}; // X11 32 Bit: 4 Bytes: -2147483647 to 2147483647
//public enum TUlong : ulong {}; // X11 64 Bit: 8 Bytes: 0 to 18446744073709551615
public enum TPixel : uint {}; // X11 32 Bit: 4 Bytes: -2147483647 to 2147483647
//public enum TPixel : ulong {}; // X11 64 Bit: 8 Bytes: 0 to 18446744073709551615
public enum XtArgVal : int {}; // X11 32 Bit: 4 Bytes: 0 to 65535
//public enum XtArgVal : long {}; // X11 64 Bit: 8 Bytes: -9223372036854775807 to 9223372036854775807
public enum XtVersionType : uint {}; // X11 32 Bit: 4 Bytes: -2147483647 to 2147483647
//public enum XtVersionType : ulong {}; // X11 64 Bit: 8 Bytes: 0 to 18446744073709551615
该示例应用程序还通过了 Mono Develop 3.0.6 在 OPEN SUSE 12.3 Linux 64 位 DE 和 GNOME 桌面、IceWM、TWM 和 Xfce 上针对 Mono 3.0.4 的测试。
示例应用程序
大部分通用 GUI 代码被发布到多个 Xrw* 类中,以提供更高的聚合级别并组织可重用代码。Xlib/X11 的函数原型和一些必要的结构/枚举定义在 X11lib 类中。该应用程序展示了:
- 一个带有 4 个按钮的菜单栏,所有按钮都有回调函数,
- 一个有两项的下拉菜单,
- 窗口管理器的应用程序装饰,带有标题和图标标题,
- 一个带透明度的应用程序图标,
- 一个状态栏,以及
- 一个模态消息框,包含带透明度的图标。
在 GNOME 桌面 32 位上的原始外观 & 感觉 以及在 Xfce 64 位上的外观
更新后的应用程序展示了:
- 一个有三个页面的笔记本——一个菜单页、一个单选按钮测试页和一个切换按钮测试页,
- 一个带有 4 个按钮的菜单栏,所有按钮都有回调函数,其中三个带有透明图标,
- 一个有两项的下拉菜单,包含透明图标,
- 窗口管理器的应用程序装饰,带有标题、图标标题和透明图标,
- 一个带有透明左右图标的状态栏,以及
- 一个模态消息框,包含用于标签和按钮的透明图标。
在 GNOME 桌面 32 位上的更新后外观 & 感觉 以及在 Xfce 64 位上的外观
在托管/非托管代码之间传递数据
尽管在 Mono 中通过 [MarshalAs(UnmanagedType.*)]
进行的内存管理非常可靠,但在少数情况下,程序员会(或必须)控制其行为。关于处理 System.Runtime.InteropServices.Marshal
类,我们可以陈述一些简单的规则:
- 所有类型都在非托管内存和托管内存之间进行封送(即内容的深拷贝)——绝对没有安全的方法可以从托管代码访问非托管内存数据。
System.IntPtr
类型包装了非托管内存指针——同样,不能直接访问非托管内存,但可以使用System.Runtime.InteropServices.Marshal
的解引用方法。Marshal.AllocHGlobal()
可以成功地与 Xlib/X11 原生调用结合使用。
将数据从 Xlib/X11 封送到 C#
第一个演示在托管和非托管代码之间传递复杂结构的一种可能解决方案的例子是 X11lib.XGetClassHint()
。此调用成功时返回 XClassHint
结构,调用者负责在使用后释放分配的内存——换句话说:C# 必须释放在 X11lib.XGetClassHint()
调用期间分配的内存。
// Tested: O.K.
/// <summary> Return the class hint of the specified window to the members of the structure.
/// If the data returned by the server is in the Latin Portable Character Encoding, then the
/// returned strings are in the Host Portable Character Encoding. Otherwise, the result is
/// implementation dependent. To free res_name and res_class when finished with the strings,
/// use XFree() on each individually. </summary>
/// <param name="x11display"> The display pointer, that specifies the connection to the X
/// server. <see cref="System.IntPtr"/> </param>
/// <param name="x11window"> The window to get
/// the class hint for. <see cref="System.IntPtr"/>
/// </param>
/// <param name="classHint"> The class hint
/// to get. <see cref="X11._XClassHint"/> </param>
/// <returns> Nonzero on success, or zero otherwise. <see cref="X11.TInt"/> </returns>
[DllImport("libX11", EntryPoint = "XGetClassHint")]
extern private static TInt _XGetClassHint (IntPtr x11display, IntPtr x11window,
ref _XClassHint classHint);
// Tested: O.K.
/// <summary> Return a XClassHint structure of the specified window to the members of the
/// structure. If the data returned by the server is in the Latin Portable Character Encoding,
/// then the returned strings are in the Host Portable Character Encoding. Otherwise, the result
/// is implementation dependent. </summary
/// <param name="x11display"> The display pointer, that specifies the connection to the X server.
/// <see cref="System.IntPtr"/> </param>
/// <param name="x11window"> The window to get the class hint for. <see cref="System.IntPtr"/>
/// </param>
/// <param name="classHint"> The class hint to get. <see cref="X11.XClassHint"/> </param>
/// <returns> Nonzero on success, or zero otherwise. <see cref="System.Int32"/> </returns>
public static int XGetClassHint (IntPtr x11display, IntPtr x11window, out XClassHint classHint)
{
_XClassHint _classHint = _XClassHint.Zero;
if (_XGetClassHint (x11display, x11window, ref _classHint) == (TInt)0)
{
// Clean up unmanaged memory.
// --------------------------
// Typically: _classHint.res_name == IntPtr.Zero
// Freeing a IntPtr.Zero is not prohibited.
// Use first member (at offset 0) to free the structure itself.
XFree (_classHint.res_name);
classHint = XClassHint.Zero;
return 0;
}
else
{
classHint = new XClassHint();
// Marshal data from an unmanaged block of memory to a managed object.
classHint.res_name = (string)Marshal.PtrToStringAnsi (_classHint.res_name );
classHint.res_class = (string)Marshal.PtrToStringAnsi (_classHint.res_class);
// Clean up unmanaged memory.
// --------------------------
// Freeing a IntPtr.Zero is not prohibited.
// First structure member (at offset 0) frees the structure itself as well.
XFree (_classHint.res_name);
XFree (_classHint.res_class);
return 1;
}
}
我选择的解决方案将调用分为两部分。C# 方法 XGetClassHint()
通过重命名的函数 _XGetClassHint()
调用原生代码,并对返回的结构进行后处理。为了能够释放分配的内存,从原生代码返回的结构定义使用了两个内存指针 res_name
和 res_class
。该结构本身将由托管代码创建,封送到非托管代码,并提供指向 res_name
和 res_class
的两个内存指针(调用者负责释放),然后再封送回托管代码并由垃圾回收器销毁。
/// <summary> Internal memory mapping structure for XClassHint structure. </summary>
/// <remarks> First structure element is on offset 0.
/// This can be used to free the structute itself. </remarks>
[StructLayout(LayoutKind.Sequential)]
private struct _XClassHint
{
/// <summary> The application name (might be changed during runtime). </summary>
/// <remarks> Must be freed separately. </remarks>
public IntPtr res_name;
/// <summary> The application class name (should be constant during runtime). </summary>
/// <remarks> Must be freed separately. </remarks>
public IntPtr res_class;
public static _XClassHint Zero = new _XClassHint ();
}
而返回给应用程序的结构则使用两个同名的托管字符串进行定义。
[StructLayout(LayoutKind.Sequential)]
public struct XClassHint
{
[MarshalAs(UnmanagedType.LPStr)] public string res_name;
[MarshalAs(UnmanagedType.LPStr)] public string res_class;
public static XClassHint Zero = new XClassHint ();
}
C# 方法 XGetClassHint()
显式管理转换和内存释放,并提供对两个内存指针 res_name
和 res_class
的完全控制。
将数据从 C# 封送到 Xlib/X11
我要展示的第二个例子是 XrwGraphic
类的实现,它为应用程序提供了非常易于处理且支持透明度(实际上基于白色像素 #FFFFFF)的位图图形。将透明位图应用到可绘制对象(drawable)的实现过程包括:
- 从位图图形文件创建彩色图形的
XImage
结构。 - 从同一文件创建透明度蒙版的
XImage
结构(使用白色像素)。 - 创建透明度蒙版的
XPixmap
结构。 - 将透明度蒙版的
XImage
结构应用到透明度蒙版的XPixmap
结构。 - 基于透明度蒙版的
XImage
结构创建一个剪裁图形上下文。 - 使用剪裁图形上下文将彩色图形的
XImage
应用到应显示该图形的可绘制对象上。 - 释放所有过时的资源。
尽管 XrwGraphic
类没有针对速度、内存使用或不同颜色深度进行优化(目前仅在 24 位真彩色显示器上测试过),但这个类是所有提供的类中花费最长时间才使其正常工作的——因为关于要实现的过程和需要考虑的细节信息不足。
示例应用程序多次使用 XrwGraphic
类,并为应用程序图标以及标签、命令切换、单选和菜单小部件显示透明图形。为了给应用程序图标提供透明图形,上述某些步骤由 X11lib.XSetWMHints()
自动完成,但总体过程是相同的。
从 System.Drawing.Bitmap
创建用于彩色图形和透明度蒙版的两个 XImage
结构时,使用托管的 byte[]
在托管代码中处理所有计算,并使用 Marshal.AllocHGlobal
和 Marshal.Copy
将转换结果传输到非托管内存。
// ### Allocate bitmap conversion data.
// The bitmap color data. Use a temporary managed byte array for speed.
byte[] graphicData = new byte[_height * _width * colorPixelBytes];
// The bitmap transparency data. Use a temporary managed byte array for speed.
byte[] maskData = new byte[_height * maskLineBytes];
// Quick access to current line's color pixel.
int graphicPixelIndex = 0;
// Quick access to current line's mask pixel.
int maskPixelIndex = 0;
// Reduce slow calls to bmp.GetPixel (x,y).
Color pixelColor = Color.Black;
// Determine whether transparency is required.
bool transparency = false;
// ### Convert bitmap.
for (int y = 0; y < _height; y++)
{
for (int x = 0; x < _width; x++)
{
graphicPixelIndex = (y * _width + x) << 2;
maskPixelIndex = y * maskLineBytes + (x >> 3);
pixelColor = bmp.GetPixel (x,y);
graphicData[graphicPixelIndex + 0] = pixelColor.B; // B
graphicData[graphicPixelIndex + 1] = pixelColor.G; // G
graphicData[graphicPixelIndex + 2] = pixelColor.R; // R
graphicData[graphicPixelIndex + 3] = pixelColor.A; // A
if (pixelColor. B == 255 && pixelColor.G == 255 && pixelColor.R == 255)
{
byte summand = (byte)(1<<(x % 8));
maskData[maskPixelIndex] = (byte)(maskData[maskPixelIndex] + summand);
transparency = true;
}
}
}
// ### Create XImage.
// The bitmap color data.
IntPtr graphicDataHandle = Marshal.AllocHGlobal(graphicData.Length);
// Allocate not movable memory.
Marshal.Copy (graphicData, 0, graphicDataHandle, graphicData.Length);
// Client side XImage storage.
_graphicXImage = X11lib.XCreateImage (_display, visual, (TUint)_graphicDepth,
X11lib.TImageFormat.ZPixmap, (TInt)0,
graphicDataHandle, (TUint)_width, (TUint)_height,
IMAGE_PADDING, ASSUME_CONTIGUOUS);
if (_graphicXImage == IntPtr.Zero)
throw new OperationCanceledException ("Image creation for graphic failed.");
if (transparency == true)
{
IntPtr maskDataHandle = Marshal.AllocHGlobal(maskData.Length);
// The bitmap bitmap transparency data.
Marshal.Copy (maskData, 0, maskDataHandle, maskData.Length);
// Allocate not movable memory.
// Client side storage.
_transpXImage = X11lib.XCreateImage (_display, visual,
(TUint)_clipDepth, X11lib.TImageFormat.XYBitmap,
(TInt)0, maskDataHandle, (TUint)_width, (TUint)_height,
IMAGE_PADDING, ASSUME_CONTIGUOUS);
if (_transpXImage == IntPtr.Zero)
throw new OperationCanceledException (
"Image creation for transparency mask failed.");
}
用于彩色图形和透明度蒙版的托管 byte[]
由垃圾回收器处理。像 XImage
、XPixmap
和 GC
这样的 Xlib/X11 资源必须通过 X11lib.XDestroyImage()
、X11lib.XFreePixmap()
和 X11lib.XFreeGC()
手动销毁。
小部件集
为了防止代码大量混杂并遵循“不要重复自己”的原则,通用的 GUI 代码被发布到多个 Xrw* 类中。小部件的名称和功能遵循“Athena Widget Set”的思想,并受到“Motif Widget Set”和 GTK 的一些影响。
这个小部件集既不完整,功能也不完善。小部件层次结构显示了 Xrw 0.10 版本的情况。要获得完整的概述或最新信息,请参阅关于从 C# 和 Mono Develop 到 X11 API 的原生调用的第五篇文章,编程 Roma 小部件集 (C# X11) - 一个零依赖的 GUI 应用程序框架。后续的增强将在那里继续。小部件层次结构如下:XrwObject:IDisposable | 最终基对象[1, ∅] |
⌊ XrwRectObject | 带几何信息的基础基对象[1, ∅] |
⌊ XrwVisibleRectObject | 带绘制功能的基础基对象[1, ∅] |
⌊ XrwCore | 通用基础小部件/控件[2/3, ∅] |
⌊ XrwComposite | 容器[2/3, ∅],管理多个子控件 |
| ⌊ XrwConstraint | 容器[2/3, ∅],具有几何管理功能 |
| | ⌊ XrwBox | 容器[2/3],水平/垂直排列子部件 |
| | | ⌊ XrwNotebook | 容器[2/3],将子部件排列在页面上 |
| | | ⌊ XrwRadioBox | 小部件[2],水平/垂直排列 XrwRadio 子控件 |
| | | ⌊ XrwSpinBox | 小工具[2],将不可见的子部件切换为可见 |
| | ⌊ XrwPorthole | 小工具[2],在任何时候只显示***一个***子部件 |
| | ⌊ XrwViewport | 容器[2/3],使***一个***子部件能够滚动 |
| ⌊ XrwShell | 基础 shell 小部件[3] |
| ⌊ XrwOverrideShell | 基础弹出 shell,不与窗口管理器交互[4] |
| | ⌊ XrwSimpleMenu | 弹出菜单 shell,处理 XrwSme 小工具[2] |
| ⌊ XrwWmShell | 基础 shell,与窗口管理器 WM 交互[4] |
| ⌊ XrwApplicationShell | X11 应用程序的通用代码 |
| ⌊ XrwTransientShell | 弹出窗口的基类,与 WM 交互[4] |
| ⌊ XrwDialogShell | 对话框的基类,与 WM 交互[4] |
⌊ XrwLabelBase | 基础静态标签[2/3, ∅] |
| ⌊ XrwLabel | 静态标签[2/3] |
| | ⌊ XrwCommand | 命令按钮小部件[3] |
| | | ⌊ XrwMenuButton | 命令按钮小部件[3],弹出一个简单菜单 |
| | ⌊ XrwSme | 简单菜单项小部件[2] |
| ⌊ XrwToggle | 切换按钮控件[3] |
| ⌊ XrwRadio | 单选按钮控件[3] |
⌊ XrwSimple | 通用小部件[3] |
⌊ XrwList | 列表小部件[3] |
⌊ XrwScrollbar | 滚动条控件[3] |
⌊ XrwText | 单行文本编辑控件[3] |
⌊ XrwTree | 树形小部件[3] |
[1] 对象 = 不可见且无窗口,不使用***父***窗口或***自有***窗口
[2] 小部件 = 使用***父***窗口而不是***自有***窗口,比控件节省资源,但只能接收其包含控件转发的事件
[3] 控件 = 拥有***自有***窗口,可直接接收窗口管理器的事件[4]
[4] WM = 窗口管理器
[∅] 不要实例化
几何管理
几何管理始于 shell 的 ConfigureEvent
事件,该事件包含给定的 shell 几何信息。
XrwApplicationShell
将事件传递给自己的 OnConfigure()
方法,该方法存储给定的几何信息,并为每个子部件调用 SetAssignedGeometry()
方法并传入给定的几何信息。
子小部件的 SetAssignedGeometry()
方法存储给定的几何信息,减去边框,然后用新的几何信息调用自己的 CalculateChildLayout()
方法。
CalculateChildLayout()
方法为每个子部件调用 PreferredSize()
方法,以确定所有子部件的总首选尺寸,并在此基础上,通过调用 SetAssignedGeometry()
方法将给定的几何信息分配给子部件。
在复合小部件上调用 PreferredSize()
方法会传递给其子部件,并返回所有子部件的总首选尺寸。这个调用可以理解为递归调用。
SetAssignedGeometry()
方法调用 CalculateChildLayout()
方法,而后者又为所有子部件调用 SetAssignedGeometry()
方法。这个调用序列可以理解为递归调用。
事件循环与事件
应用程序的事件循环在 XrwApplicationShell
类中实现。我称之为“实验性的”,但它对于当前的需求工作得很好。事件循环分为无限循环 RunMessageLoop()
和单个事件处理 DoEvent()
。
/// <summary> Run the application's message loop. </summary>
protected void RunMessageLoop ()
{
bool proceed = true;
while(proceed == true)
{
try
{
proceed = DoEvent();
}
catch (Exception e)
{
Console.WriteLine (CLASS_NAME + ":: RunMessageLoop () ERROR: " + e.StackTrace);
}
}
return;
}
/// <summary> Process the topmost event and remove it from the event queue. </summary>
/// <returns> True if efent processing must contionue, false otherwise. </returns>
public bool DoEvent()
{
...
}
DoEvent()
方法目前处理 ConfigureNotify
, Expose
, KeyPress
, KeyRelease
, ButtonPress
, ButtonRelease
, FocusIn
, FocusOut
, EnterNotify
, LeaveNotify
, MotionNotify
和 ClientMessage
事件。
每个有窗口的小部件都可以接收通过 X11lib.XSelectInput()
为其底层窗口注册的 X11 事件。目前,XrwSimple
(包括其派生的简单小部件)、XrwOverrideShell
(包括其派生的 XrwSimpleMenu
)、XrwTransientShell
(包括其派生的 XrwDialogShell
和 XrwMessageBox
)以及 XrwApplicationShell
都会为其底层窗口注册 X11 事件。XrwTransientShell
和 XrwApplicationShell
与窗口管理器交互,并注册了 11 种 X11 事件。
X11lib.XSelectInput(_display, _window,
EventMask.StructureNotifyMask | EventMask.ExposureMask |
EventMask.ButtonPressMask | EventMask.ButtonReleaseMask |
EventMask.KeyPressMask | EventMask.KeyReleaseMask |
EventMask.FocusChangeMask | EventMask.EnterWindowMask |
EventMask.LeaveWindowMask | EventMask.PointerMotionMask |
EventMask.SubstructureNotifyMask);
XrwSimple
和 XrwOverrideShell
注册了相同的 X11 事件,除了 EventMask.StructureNotifyMask
。它们不需要接收 ConfigureNotify
X11 事件,因为所有的配置,主要是尺寸调整,都是通过以下方式完成的:
- 从与窗口管理器交互并接收
ConfigureNotify
事件的 shell 小部件开始, - 向前遍历整个小部件层次结构——包括所有
XrwSimple
或XrwOverrideShell
及其派生小部件,以及所有无窗口小部件——通过调用PreferredSize()
来计算所有小部件的首选尺寸, - 然后返回遍历该 shell 的整个小部件层次结构,通过调用
SetAssignedGeometry()
来设置分配的几何尺寸。
DoEvent()
方法确定 X11 事件的最佳接收者,并将 X11 事件转换为 C# 事件并触发它。ClientMessage
事件是这个过程的一个简单例子。
else if (xevent.type == XEventName.ClientMessage &&
xevent.ClientMessageEvent.ptr1 == _wmDeleteMessage)
{
// Window menager is clowing a window.
bool childAddressed = false;
// ClientMessageEvent events might go to associated transient shells.
foreach (XrwTransientShell transientShell in _associatedTransientShells)
{
if (xevent.ClientMessageEvent.window == transientShell.Window)
{
transientShell.OnClose (new XrwClientMessageEvent (ref xevent.ClientMessageEvent));
childAddressed = true;
break;
}
}
if (childAddressed == true)
return true;
// Subsequent ClientMessageEvent events might go to windowed children.
if (xevent.ClientMessageEvent.window == this.Window)
OnClose (new XrwClientMessageEvent (ref xevent.ClientMessageEvent));
}
事件处理首先检查事件是否发往应用程序的任何临时 shell。否则,它会为应用程序 shell 处理该事件。从 X11 事件到 C# 事件的转换是通过实例化相应的包装类来完成的,例如 new XrwClientMessageEvent (ref xevent.ClientMessageEvent)
,然后将该实例传递给 C# 事件的调用,例如 transientShell.OnClose()
。
为了确保应用程序的所有临时 shell 都被事件处理捕获,注册临时 shell 是至关重要的。目前,无论是 XrwTransientShell
类还是任何派生类都不会自动执行此操作,因此程序员有责任来完成它。
// <summary> Handle the ButtonPress event. </summary>
/// <param name="source"> The widget, the ButtonPress event
/// is assigned to. <see cref="XrwRectObj"/> </param>
/// <param name="e"> The event data. <see cref="XawButtonEvent"/> </param>
/// <remarks> Set XawButtonEvent. Set result to nonzero to stop further event processing. </remarks>
void HandleMessageBoxButtonPress (XrwRectObj source, XrwButtonEvent e)
{
...
this.AddTransientShell (messageBox);
...
}
与窗口管理器交互的小部件的特性
所有与窗口管理器交互的 shell 小部件都会被一个框架装饰,并且可以通过窗口标题栏左侧的应用程序图标菜单项“关闭”或右侧的关闭按钮来关闭。
为了确保受控且完整的 shell 关闭,包括释放非托管资源,shell 必须被通知由框架触发的关闭事件。为实现这一点,XrwTransientShell
和 XrwApplicationShell
注册了 _wmDeleteMessage
。
/* Hook the closing event from windows manager. */
_wmDeleteMessage = X11lib.XInternAtom (_display,
X11Utils.StringToSByteArray ("WM_DELETE_WINDOW\0"), false);
if (X11lib.XSetWMProtocols (_display, _window, ref _wmDeleteMessage, (X11.TInt)1) == 0)
{
Console.WriteLine (CLASS_NAME + "::InitializeApplicationShellResources () WARNING: " +
"Failed to register 'WM_DELETE_WINDOW' event.");
}
实际上,窗口管理器支持三种协议:WM_TAKE_FOCUS
、WM_SAVE_YOURSELF
、WM_DELETE_WINDOW
。X11lib.XInternAtom()
获取协议,并使用 X11lib.XSetWMProtocols()
将其注册到 shell 的底层窗口。
通用小部件事件处理
所有使用 X11lib.XSelectInput()
注册 X11 事件的小部件都会收到由 DoEvent()
方法从 X11 事件转换而来的 C# 事件的通知。如果应用程序想要处理事件,可以将回调方法注册到由 XrwRectObj
小部件引入的事件处理程序中。
/// <summary> Register expose event handler. </summary>
public event ExposeDelegate Expose;
/// <summary> Register KeyPress event handler. </summary>
public event KeyPressDelegate KeyPress;
/// <summary> Register KeyRelease event handler. </summary>
public event KeyReleaseDelegate KeyRelease;
/// <summary> Register ButtonPress event handler. </summary>
public event ButtonPressDelegate ButtonPress;
/// <summary> Register KeyPress event handler. </summary>
public event ButtonReleaseDelegate ButtonRelease;
/// <summary> Register FocusIn event handler. </summary>
public event FocusInDelegate FocusIn;
/// <summary> Register FocusOut event handler. </summary>
public event FocusOutDelegate FocusOut;
/// <summary> Register Enter event handler. </summary>
public event EnterDelegate Enter;
/// <summary> Register Leave event handler. </summary>
public event LeaveDelegate Leave;
/// <summary> Register Motion event handler. </summary>
public event MotionDelegate Motion;
现在,像在 C# 中一样获取事件通知就变得很简单了,例如 cbw1.ButtonPress += HandleCloseMenuButtonPress
。
不与窗口管理器交互的小部件的特性
一些小部件有预定义/内置的事件处理器。例如,XrwSimpleMenu
小部件使用 FocusOut
和 ButtonRelease
事件来隐藏其覆盖 shell,使用 Motion
事件来为菜单项设置/取消鼠标悬停效果,并使用 ButtonPress
事件来启动菜单操作。Motion
、ButtonPress
和 ButtonRelease
事件由 XrwSimpleMenu
小部件处理,而不是由 XrwSme
小部件处理,因为 XrwSme
小部件没有自己的窗口(为了节省资源),因此它们不接收事件。
再举一个例子,XrwCommand
小部件使用 Enter
和 Leave
事件来设置/取消鼠标悬停效果。
/// <summary> Initialize local ressources for all constructors. </summary>
private void InitializeCommandRessources ()
{
...
Enter += HandleEnter;
Leave += HandleLeave;
}
触发 X11 事件
为了实现鼠标悬停效果,XrwSme
、XrwCommand
和 XrwMenuButton
小部件会改变填充颜色。为了使这种变化可见,必须发送一个 X11 Expose
事件。XrwObject
小部件的静态便利方法 SendExposeEvent()
使此操作非常容易处理。
/// <summary> Send an expose event to the indicated window. </summary>
/// <param name="display">The display pointer, that specifies
/// the connection to the X server. <see cref="IntPtr"/> </param>
/// <param name="receiverWindow"> The window to adress
/// the expose event to. <see cref="IntPtr"/> </param>
/// <param name="dispatcherWindow"> The window to send
/// the expose event to. <see cref="IntPtr"/> </param>
/// <returns> Nonzero on success, or zero otherwise (if the conversion
/// to wire protocol format failed). <see cref="X11.TInt"/> </returns>
public static TInt SendExposeEvent (IntPtr display, IntPtr receiverWindow,
IntPtr dispatcherWindow)
{
XEvent eE = new XEvent();
eE.ExposeEvent.count = 0;
eE.ExposeEvent.display = display;
eE.ExposeEvent.window = receiverWindow;
eE.ExposeEvent.type = XEventName.Expose;
return X11lib.XSendEvent (display, dispatcherWindow, (TBoolean)1,
(TLong)EventMask.ExposureMask, ref eE);
}
模态对话框
将事件循环划分为 RunMessageLoop()
和 DoEvent()
,使得模态 shell 可以通过实现自己的无限循环来接管事件处理,例如 XrwMessageBox
。
/// <summary> Run the message box modal. </summary>
/// <returns> The pressed button. <see cref="XrwDialogShell.Result"/> </returns>
/// <remarks> To use the message box non-modal register at least event handler
/// to WmShellClose and to DialogShellEnd and call Show(). </remarks>
public XrwDialogShell.Result Run ()
{
this.Show();
// Take over the event loop processing.
while (_result == XrwDialogShell.Result.None)
ApplicationShell.DoEvent ();
return _result;
}
关注点
重走 Qt、GTK 或 wxWidget 开发者多年前走过的路,既富有挑战性,又富于启发性,也很有趣。它为从 C/C++ 到 C# 的迁移提供了一条巧妙的路径,开发者可以保留他们不想失去的成果,同时开拓新的领域。
“Roma Widget Set” (Xrw) 展示了在旧的 Xlib/X11 C 编程接口之上实现面向对象编程是多么容易。
历史
- 2013 年 4 月 30 日,这是第一篇关于从 C# 和 Mono Develop 到 X11 API 的原生调用的文章。它只涉及 Xlib。计划后续文章将涉及 Xt/Athena 和 Xt/Motif。
- 2013 年 7 月 8 日,第二篇关于从 C# 和 Mono Develop 到 X11 API 的原生调用的文章 使用 Mono Develop 进行 Xlib 编程 - 第 2 部分:Athena 小部件(概念验证) 已发布。
- 2013 年 9 月 18 日,第三篇关于从 C# 和 Mono Develop 到 X11 API 的原生调用的文章 使用 Mono Develop 进行 Xlib 编程 - 第 3 部分:Motif 小部件(概念验证) 已发布。
- 2013 年 11 月 7 日,第四篇关于从 C# 和 Mono Develop 到 X11 API 的原生调用的文章 使用 Mono Develop 进行 Xlib 编程 - 第 4 部分:FWF Xt 小部件 已发布。
- 2013 年 11 月 18 日,本文更新。新特性包括:
- 明确区分了边框(border)和框架(frame)。
- 新增了“Chiseled”和“Ledged”两种框架类型。
- 在所有 OnExpose 调用中维护 XrwExposeEvent.Result。
- XrwOverrideShell 现在基于一个独立的窗口,而不是 XrwApplicationShell 的子窗口。
- 新增小部件 XrwToggle、XrwRadio、XrwRadioBox、XrwPorthole 和 XrwNotebook。
- 2013 年 1 月 20 日,本文被拆分——小部件相关的引用已移至第五篇关于从 C# 和 Mono Develop 到 X11 API 的原生调用的文章,编程 Roma 小部件集 (C# X11) - 一个零依赖的 GUI 应用程序框架。后续的增强将在那里继续。要获得完整的概述或最新信息,请参阅本系列的第五篇文章。此外,还修复了一些错误并添加了许多新功能,例如:
XEvent
的成员time
原始类型/位大小为ulong
(C),对应于uint
(C# 32位)。- 弹出菜单的位置现在使用
XGetWindowAttributes()
和XTranslateCoordinates()
来计算。 - 通用的主题支持,包括两个预定义的主题
XrwTheme.GeneralStyle.Win95
和XrwTheme.GeneralStyle.Gtk2Clearlooks
。