使用 Mono Develop 编程 Xlib - 第 2 部分:Athena 小部件(概念验证)






4.89/5 (4投票s)
如何从 Mono Develop C# 调用本地 Xt API,最终构建一个非常小的 Athena 小部件应用程序。
引言
本文介绍了如何使用 Mono Develop 从 C# 访问 Xt/Athena 小部件 API。许多 API 调用已准备就绪并经过测试,并且已解决了某些挑战,例如使用回调、整合第三方小部件或提供独占对话框。
本文旨在验证使用 C# 编程 Xt/Athena 小部件可以轻松实现(概念验证)。它提供了 32 位和 64 位应用程序的完整项目示例。
背景
这是本系列文章的第二篇,该系列文章使用 Mono Develop 从 C# 检查 X* API 调用。
第一篇文章是《使用 Mono Develop 编写 Xlib 程序 - 第一部分:底层(概念验证)》,仅涉及 Xlib/X11。现在我想继续研究 Xt/Athena 小部件,并展示如何从现代语言/IDE(如 C#/Mono)中使用它们。
一方面,本文表明无需害怕从 C# 调用 Xt API,另一方面,它也揭示了 Athena 小部件在视觉和功能上的不足。基于这些知识,不再推荐使用 Athena 小部件(Xaw)——即使它们仍由X.Org 基金会维护——除非是为了快速将 C/C++ 应用程序移植到 C#。虽然 Athena 3D 小部件(Xaw3d)可以改善视觉效果,但它们无法改善 Athena 小部件的功能不足。
使用代码
示例应用程序是用 Mono Develop 2.4.1 为 Mono 2.8.1 在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面上编写的。移植到任何旧版本或新版本都不应该有问题。该
示例应用程序的解决方案包含四个项目(完整源代码可供下载)
- X11Wrapper 定义了 Xlib/X11 调用到 libX11.so 的函数原型、结构和类型(在本系列的第一篇文章中已介绍)
- XawNative 包含少量本地 C 代码,用于访问 Athena 小部件类记录,并提供一个示例,演示如何整合第三方 Xt 小部件
- XtWrapper 定义了 Xt 调用到 libXt.so 的函数原型、结构和类型,以及 Athena 小部件调用到 libXawNative.so 的函数原型
- Xt 包含基于 Athena 小部件的示例应用程序和一个示例第三方 Xt 小部件
该示例应用程序还通过了 Mono Develop 3.0.6 在 OPEN SUSE 12.3 Linux 64 位 DE 和 GNOME 桌面、IceWM、TWM 和 Xfce 上针对 Mono 3.0.4 的测试。
32 位和 64 位解决方案之间的唯一区别是某些类型的定义,正如本系列第一篇文章中所述。
应用程序本身只有 645 行代码。一些通用的 GUI 代码被释放到几个 Xt* 类中——不是用 C# 包装 Athena 小部件,而是用于组织可重用代码。Xt/Athena 的函数原型和一些必需的结构/枚举定义在 Xtlib 类中。该应用程序演示了
- 一个带有 4 个按钮的菜单栏,其中前两个按钮有下拉菜单,后两个按钮有回调,
- 第一个菜单按钮上的一个经典下拉菜单,带有两个条目,以及第二个菜单按钮上的一个 Xt Release 4 简单菜单,带有两个条目,
- 第三个菜单按钮上的一个简单的独占抓取对话框,
- 一个绘图区域和三个按钮,用于绘制随机线条、椭圆或矩形,
- 一个带有透明度的应用程序图标(并非所有最新的窗口管理器都能显示它,但 Xfce 可以),
- 一个带有位图和标签的状态栏,
- 一个带有位图的独占抓取问题对话框,可以取消应用程序退出。
在 GNOME 桌面 2.30.0(32 位)和 Xfce 4.10(64 位)上的外观和感觉
初始化应用程序并设置备用资源
Xt 应用程序的大部分可重用代码被释放到 XtApplicationShell
类或其基类 XtWmShell
中,后者实现了 IDisposable
。
应用程序的初始化由 XtApplicationShell
的构造函数完成,并调用 XtAppInitialize()
,不考虑命令行参数。
/// <summary> The application's context. </summary>
protected IntPtr _appContext = IntPtr.Zero;
/// <summary> The shell widgets resource name. </summary>
private string _shellResourceName = "ApplicationShell";
...
TInt argc = 0;
_shell = Xtlib.XtAppInitialize (ref _appContext, _shellResourceName, IntPtr.Zero, 0, ref argc,
IntPtr.Zero, fallbackResources.Marshal (), IntPtr.Zero, 0);
第七个参数 fallbackRessources.Mashal()
必须作为非托管代码字符串数组传递,并在数组末尾包含一个 NULL 指针。
便捷封送 1
为了简化备用资源的处理,便捷类 XtFallbackRessources
负责定义(fallbackResources.Add()
)和转换(fallbackResources.Marshal()
)资源字符串。托管到非托管字符串的转换是通过对每个资源字符串调用 Marshal.StringToHGlobalAnsi()
来完成的。
Xt.XtFallbackRessources fallbackResources = new Xt.XtFallbackRessources ();
// Main application widgets.
fallbackResources.Add ("*RootForm.background: #F0F0F0");
fallbackResources.Add ("*MenuBox.orientation: horizontal");
...
fallbackResources.Add ("*FileMenuEntry2.translations: <EnterWindow>:highlight()\\n" +
"<LeaveWindow>:unhighlight()\\n" +
"<BtnDown>:set() notify() unset() XtMenuPopdown(FileMenu)");
在通过备用资源定义翻译表方面,有两点值得一提。
- 默认情况下,翻译表会被覆盖。
- 只有当小部件的
set()
翻译先前被调用过时,`notify()` 翻译才会调用注册到XtNcallback
的回调。
将字符串传递给非托管代码
示例应用程序使用三种不同的方法将字符串从托管代码传递到非托管代码
[MarshalAs(UnmanagedType.LPStr)]
,特别适用于仅作为输入参数的单个字符串 API 函数TChar[]
,特别适用于在托管和非托管代码之间封送的结构成员Marshal.StringToHGlobalAnsi()
,特别适用于要传递给非托管代码的字符串数组
使用 [MarshalAs(UnmanagedType.LPStr)]
生成的最低手动工作量,因为所有数据处理都由托管代码完成(构造、垃圾回收),并且非托管内存的分配、值传递和释放完全隐藏。
使用 TChar[]
可以更好地控制封送步骤,并由托管代码完全处理数据(构造、垃圾回收),它可以轻松地与便捷方法 X11Utils.StringToSByteArray()
一起使用,将托管字符串转换为 Xt API 友好的 sbyte[]
。
使用 Marshal.StringToHGlobalAnsi()
可以完全控制封送步骤,但数据不由托管代码处理,并且必须显式调用 Marshal.FreeHGlobal()
。
创建托管小部件
应用程序初始化后的下一步是创建小部件层次结构。在此阶段,小部件被创建(在内存中构建,包括所有资源初始化)并被管理(注册到其各自的父小部件),但既未实现(连接到窗口,除非它们是 gadget)也未映射(其窗口对屏幕可见)。
将小部件层次结构创建拆分为一步创建+管理和一步实现+映射,可以避免在初始几何和布局计算过程中小部件与其窗口之间进行大量通信,从而显着减慢应用程序启动速度。
示例应用程序仅使用 XtCreateManagedWidget()
来创建+管理小部件。第一次调用 XtCreateManagedWidget()
如下所示:
// Root composite: Use a Form widget, it supports rubber-band resizing
// and fromVert / fromHorz constraints.
rootForm = Xtlib.XtCreateManagedWidget(rootFormName,
Xtlib.XawFormWidgetClass(), _shell,
Arg.Zero, 0);
第一个参数 rootFormName
是小部件的资源名称。它控制资源分配,例如上面讨论的备用资源。第二个参数 Xtlib.XawFormWidgetClass()
是小部件的类记录,所有特定小部件类的实例都共享它。任何小部件类的类记录都是其实现文件的一部分,因此已经在 libXaw.so 中定义。示例应用程序**必须**使用 libXaw.so 中的小部件类记录,但 C# 无法访问非托管代码结构。
第三个参数 _shell
是小部件的父小部件,最后两个参数(Arg.Zero, 0
)提供小部件资源初始化,此处未使用。
额外的非托管代码
克服限制(应用程序**必须**使用 libXaw.so 中的小部件类记录,但 C# 无法访问非托管代码结构)的解决方法是通过少量非托管桥接代码访问小部件类记录。只需两行非托管代码即可提供对特定小部件类记录的访问。这三个示例取自解决方案的 XawNative.c 文件。
//Provide access to the widget's class record from C#.
void* XawAsciiTextWidgetClass ()
{ return (void*) asciiTextWidgetClass; }
void* XawBoxWidgetClass ()
{ return (void*) boxWidgetClass; }
void* XawCanvasWidgetClass ()
{ return (void*) canvasWidgetClass; }
...
此外,解决方案的 XawNative 项目包含第三方小部件 MoreWidgets/Canvas,以演示如何使用符合 Xt 标准的非 Athena 小部件。
初始化和操作小部件资源
XtCreateManagedWidget()
方法与最后两个参数 Args[] args
和 XCardinal numArgs
结合使用,是创建 Athena 小部件并同时设置资源的两种可能方法之一,并且在示例应用程序中得到了广泛测试。
第二种方法 XtVaCreateManagedWidget()
对参数没有类型安全性,因此未在示例应用程序中使用。
包含同时设置资源的一次 XtCreateManagedWidget()
调用如下所示:
// Menu bar: Use a Box widget, this is common.
Arg[] menuBoxArgs = { new Arg(XtNames.XtNwidth, (XtArgVal)CANVAS_WIDTH ),
new Arg(XtNames.XtNhSpace, (XtArgVal)2) };
menuBox = Xtlib.XtCreateManagedWidget(menuBoxName,
Xtlib.XawBoxWidgetClass(), rootForm,
menuBoxArgs, (XCardinal)2);
与在调用 XtCreateManagedWidget()
时使用 Args[] args
和 XCardinal numArgs
这两个参数的替代方案是,可以使用 XtGetValues()
和 XtSetValues()
方法来获取或设置 Athena 小部件资源。
使用 XtGetValues 和 XtSetValues
传递非托管代码占位符以接收 XtGetValues()
的返回值非常重要。调用 Marshal.AllocHGlobal()
会分配这样一个非托管代码占位符,并在使用后调用 Marshal.FreeHGlobal()
来释放它。要从非托管代码占位符获取值,必须应用 Marshal.Read*()
。
示例应用程序使用此技术来确定弹出菜单的位置
// Determine file menu command widget's height.
IntPtr hHeight = Marshal.AllocHGlobal (2); // XDimension is 2 bytes
Arg[] parentRetArgs = { new Arg(XtNames.XtNheight, hHeight) };
Xtlib.XtGetValues (parent, parentRetArgs, (XCardinal)1);
XDimension height = (XDimension)Marshal.ReadInt16 (hHeight);
Marshal.FreeHGlobal (hHeight);
menuY += (int)height;
应尽可能少地使用 XtSetValues()
调用,因为 Athena 小部件在资源值更改后不会进行任何几何更新。因此,必须显式进行几何更新。
在状态标签文本设置为新值后,示例应用程序会为状态标签小部件显式更新几何。
/// <summary> Implement the SetStatusLabel event handler. </summary>
/// <param name="label"> The label to set to the status bar. <see cref="System.String"/> </param>
public void HandleSetStatusLabel (string label)
{
if (!(Xtlib.XtIsRealized (_statusLabel) == (TBoolean)0))
{
Arg[] statusLabelArgs = { new Arg(XtNames.XtNlabel, X11.X11Utils.StringToSByteArray (label + "\0")) };
Xtlib.XtSetValues (_statusLabel, statusLabelArgs, (XCardinal)1);
XtWidgetGeometry preferred = new XtWidgetGeometry();
XtGeometryResult result = Xtlib.XtQueryGeometry (_statusLabel, IntPtr.Zero, ref preferred);
Xtlib.XtResizeWidget (_statusLabel, preferred.width, preferred.height, preferred.border_width);
}
}
调用 XtQueryGeometry()
获取新几何,调用 XtResizeWidget()
将新几何应用于刚刚更改了资源值状态标签小部件。但这不会影响父小部件的几何,并且可能需要显式遍历小部件层次结构回到根小部件。
管理 WM_DELETE_WINDOW 信号
大多数应用程序不仅会无疑问地关闭,还会请求保存未保存的数据,有时还会给用户提供中止关闭的可能性。要实现这一点,必须由应用程序接收和处理 WM_DELETE_WINDOW 信号。WM_DELETE_WINDOW 信号默认不会发送到窗口(可以在事件循环中处理),而只会在窗口管理器处发生。要由应用程序接收和处理该信号,必须将其从窗口管理器转移到应用程序的事件循环,并为顶层窗口设置一个到已注册操作的转换。
/// <summary> Register a "delete window action" to application context, translate the "delete
/// window action", add/overwrite the shell widget's translation table and set windows manager
/// protocol hook for the shell widget. </summary>
/// <param name="appContext"> The application's context to register the action to.
/// <see cref="System.IntPtr"/> </param>
/// <param name="deleteWindowAction"> The action to register to the application's context.
/// <see cref="XtActionProc"/> </param>
/// <remarks> This must be done *** AFTER *** XtRealizeWidget (). </remarks>
public void RegisterDeleteWindowAction (IntPtr appContext, XtActionProc deleteWindowAction)
{
try
{
// Register (instance method) action procedure to runtime action marshaler
// and let it map the signal to the (global static) action procedure..
IntPtr deleteWindowActionPtr = ActionMarshaler.Add (_shell,
X11.XEventName.ClientMessage, deleteWindowAction);
// Create an actions record to provide the application's context
// with a "action-name" to "action-procedure" translation.
XtActionsRec[] actionProcs= new XtActionsRec[] {
new XtActionsRec (X11Utils.StringToSByteArray (XtWmShell.DELETE_WINDOW_ACTION_NAME + "\0"),
deleteWindowActionPtr) };
// Register the actions record to the application's context.
Xtlib.XtAppAddActions (appContext, actionProcs, (XCardinal)1);
// Create a compiled translation table, to provide the widget with
// a "message" to "action-name" translation.
IntPtr translationTable = Xtlib.XtParseTranslationTable("<Message>WM_PROTOCOLS: " +
XtWmShell.DELETE_WINDOW_ACTION_NAME + "()");
// Merge new translations to the widget, overriding existing ones.
Xtlib.XtOverrideTranslations (_shell, translationTable);
/// The delete message from the windows manager. Closing an app via window
/// title functionality doesn't generate a window message - it only generates a
/// window manager message, thot must be routed to the window (message loop).
IntPtr wmDeleteMessage = IntPtr.Zero;
// Hook the closing event from windows manager.
// Must be done *** AFTER *** XtRealizeWidget () to determine display and window!
wmDeleteMessage = X11lib.XInternAtom (Xtlib.XtDisplay(_shell), "WM_DELETE_WINDOW", false);
if (X11lib.XSetWMProtocols (Xtlib.XtDisplay(_shell), Xtlib.XtWindow(_shell),
ref wmDeleteMessage, (X11.TInt)1) == 0)
{
Console.WriteLine (CLASS_NAME + "::RegisterDeleteWindowAction() " +
"WARNING: Failed to register 'WM_DELETE_WINDOW' event.");
}
}
catch (Exception e)
{
Console.WriteLine (e.Message);
Console.WriteLine (e.StackTrace);
}
}
便捷封送 2
在 Xt 中,所有回调和操作过程都被处理为全局函数,因为 Xt 不了解对象(以 C# 的方式)。为了简化回调和操作过程与类方法的映射,静态便捷类 CallBackMarshaler
和 ActionMarshaler
实现了回调过程或操作过程的字典,并提供了到相应类方法的自动映射。
静态方法 CallBackMarshaler.Add(...)
返回一个回调指针,该指针可用于通过 Xtlib.XtAddCallback(...)
注册小部件的回调过程。
Xtlib.XtAddCallback (closeCommand, XtNames.XtNcallback,
CallBackMarshaler.Add (closeCommand, this.ApplicationCloseClick), IntPtr.Zero);
静态方法 ActionMarshaler.Add(...)
返回一个操作指针,该指针可用于通过 XtActionsRec
和 Xtlib.XtAppAddActions(...)
注册操作过程。
IntPtr deleteWindowActionPtr = ActionMarshaler.Add (_shell,
X11.XEventName.ConfigureNotify, deleteWindowAction);
XtActionsRec[] actionProcs = new XtActionsRec[] {
new XtActionsRec (X11Utils.StringToSByteArray (XtWmShell.DELETE_WINDOW_ACTION_NAME + "\0"),
deleteWindowActionPtr) };
Xtlib.XtAppAddActions (appContext, actionProcs, (XCardinal)1);
管理根窗口的 CONFIGURE_NOTIFY 信号
要接收和处理 CONFIGURE_NOTIFY 信号(它报告窗口状态的变化 - 如大小、位置、边框和堆叠顺序),必须为顶层窗口设置一个到已注册操作的转换。
// Register (instance method) action procedure to runtime action marshaller and
// let it map the signal to the (global static) action procedure.
IntPtr configureNotifyActionPtr = ActionMarshaler.Add (_shell, X11.XEventName.ConfigureNotify,
configureNotifyAction);
// Create an actions record to provide the application's context with a "action-name" to
// "action-procedure" translation.
XtActionsRec[] actionProcs = new XtActionsRec[] {
new XtActionsRec (X11Utils.StringToSByteArray (XtWmShell.COFIGURE_NOTIFY_ACTION_NAME + "\0"),
configureNotifyActionPtr) };
// Register the actions record to the application's context.
Xtlib.XtAppAddActions (appContext, actionProcs, (XCardinal)1);
// Create a compiled translation table, to provide the widget with a "message" to "action-name" translation.
IntPtr translationTable = Xtlib.XtParseTranslationTable("<Configure>: " +
XtWmShell.COFIGURE_NOTIFY_ACTION_NAME + "()");
// Merge new translations to the widget, overriding existing ones.
Xtlib.XtOverrideTranslations (_shell, translationTable);
我们再次使用静态方法 ActionMarshaler.Add(...)
来注册操作过程,并使用静态类 ActionMarshaler
进行便捷的信号封送。
设置应用程序图标
创建应用程序图标的基本工作由 XrwGraphic
类完成,该类在本系列文章的第一部分“Marshaling data from C# to Xlib/X11”子章节中已有讨论。由于应用程序图标的图形和掩码是 XPixmap
,它们必须在 X 服务器上创建,这需要一个显示(与 X 服务器的连接)和一个与 shell 关联的窗口。换句话说,在 shell 小部件创建+管理之后,应用程序图标无法设置,必须在 shell 小部件实现+映射之后才能成功调用 Xtlib.XtDisplay()
和 Xtlib.XtWindow()
。
/// <summary> Load the icon from indicated path and set it as shell icon. </summary>
/// <returns> <c>true</c>, if icon was set, <c>false</c> otherwise. </returns>
/// <param name='iconPath'> The icon path. </param>
public bool SetShellIcon (string iconPath)
{
bool result = false;
if (_shell == IntPtr.Zero)
{
Console.WriteLine (CLASS_NAME + "::SetShellIcon() ERROR: Member attribute '_shell' null.");
return result;
}
if (string.IsNullOrEmpty (iconPath))
{
Console.WriteLine (CLASS_NAME + "::SetShellIcon() ERROR: Paramerter 'iconPath' null or empty.");
return result;
}
IntPtr display = Xtlib.XtDisplay (_shell);
IntPtr window = Xtlib.XtWindow (_shell);
TInt screen = Xtlib.XDefaultScreen (display);
using (X11Graphic appIcon = new X11Graphic (display, screen, iconPath))
{
IntPtr appGraphicPixMap = appIcon.CreateIndependentGraphicPixmap (display, window);
IntPtr appMaskPixMap = appIcon.CreateIndependentMaskPixmap (display, window);
if (appGraphicPixMap != IntPtr.Zero && appMaskPixMap != IntPtr.Zero)
{
X11lib.XWMHints wmHints = X11lib.XAllocWMHints ();
wmHints.flags = X11lib.XWMHintMask.IconPixmapHint |
X11lib.XWMHintMask.IconPositionHint |
X11lib.XWMHintMask.IconMaskHint;
wmHints.icon_pixmap = appGraphicPixMap;
wmHints.icon_mask = appMaskPixMap;
wmHints.icon_x = 0;
wmHints.icon_y = 0;
X11lib.XSetWMHints (display, window, ref wmHints);
result = true;
}
else
Console.WriteLine (CLASS_NAME + "::SetShellIcon () ERROR: Can not create application icon.");
}
return result;
}
最终的应用程序图标分配给 shell 是通过 X11lib.XSetWMHints()
完成的。
所有这些都集中在 XtWmShell
类的 SetShellIcon()
方法中。该方法可用于应用程序 shell...
Xtlib.XtRealizeWidget (_shell);
SetShellIcon (IconPath);
...以及弹出 shell...
Xtlib.XtPopup (_shell, XtGrabKind.XtGrabExclusive);
if (firstRun && XtApplicationShell.Instance != null)
{
SetShellIcon (XtApplicationShell.Instance.IconPath);
}
独占抓取对话框
示例应用程序包含两个独占抓取对话框:XtGrabExclusiveMessageBox
和 XtGrabExclusiveAthenaDialog
。两者都调用 XtPopup (xtPopupShell, grabKind)
并带 XtGrabKind.XtGrabExclusive
来实现独占事件处理。
关于颜色处理的最后说明
互联网上几乎 99% 的 Xt 示例代码都使用 XAllocColor
/ XFreeColors
进行颜色处理。如果 X 服务器使用未分解的颜色映射,这些函数就很好,例如 StaticGray
、GrayScale
、StaticColor
和 PseudoColor
。
但如今,大多数 X 服务器运行在 TrueColor
或 DirectColor
模式下,使用分解的颜色映射,而调用 XFreeColors
可能会混淆 X 服务器的颜色处理。静态类 X11Color
实现静态辅助方法 IsDirectColorVisual(...)
和 IsTrueColorVisual(...)
来识别具有分解颜色映射的模式。
现在,对于所有类型的 X 服务器颜色处理,都可以直接实现颜色处理。
IntPtr display = Xtlib.XtDisplay (_canvas);
TInt scrnID = X11lib.XDefaultScreen (display);
IntPtr colormap = X11lib.XDefaultColormap (display, scrnID);
bool fastCol = X11Color.IsDirectColorVisual (display, scrnID) ||
X11Color.IsTrueColorVisual (display, scrnID);
// Only TrueColor and DirectColor visuals can directly access colors via RGB,
// all other visuals must alloc/free colors.
if (fastCol)
{
_rectBuchColors[cont].pixel = (TPixel)((uint)((int)_rectBuchColors[cont].red)<<16) +
((uint)((int)_rectBuchColors[cont].green)<<8) +
((uint)_rectBuchColors[cont].blue);
}
else
{
string color = string.Format ("#{0,2:X}{1,2:X}{2,2:X}", _rectBuchColors[cont].red,
_rectBuchColors[cont].green, _rectBuchColors[cont].blue);
X11lib.XParseColor (display, colormap, X11Utils.StringToSByteArray(color + "\0"),
ref _rectBuchColors[cont]);
X11lib.XAllocColor (display, colormap, ref _rectBuchColors[cont]);
}
...
// Only TrueColor and DirectColor visuals can directly access colors via RGB,
// all other visuals must alloc/free colors.
if (!fastCol)
{
for (int cont = 0; cont < LINES_BUNCH_SIZE; cont++)
{
TPixel[] pixels = new TPixel[1];
pixels[0] = _lineBuchColors[cont].pixel;
X11lib.XFreeColors (display, colormap, pixels, (TInt)1, 0);
}
}
关注点
注册回调和操作过程
此示例应用程序表明,将 C/C++ Athena 小部件项目移植到 C# 可以非常直接。根据用于项目移植的小部件参数处理方式,它也可以是一项工作量较少(XtCreateManagedWidget()
)或工作量较大(必须将 XtVaCreateManagedWidget()
转换为 Arg[]
和 XtCreateManagedWidget()
)的任务。
真正的挑战可能是为移植提供面向对象的结构。便捷类 CallBackMarshaler
和 ActionMarshaler
展示了一种可能的实用方法,可以将 C# 对象和 Xt 全局函数对应起来。
视觉和功能上的不足
此示例应用程序还演示了如何使用符合 Xt 标准的非 Athena 小部件。有几个开源或社区项目正在重新开发 Athena 小部件,例如neXtaw、XawM、Xaw-Xpm 或 XawPlus,并且还有几个附加小部件可以与 Athena 小部件结合使用,例如Mowitz、EuroBridge 小部件集、Gridbox 以及 ftp://ftp.x.org/contrib/widgets 上的其他小部件,或 Siag Office。所有这些都试图克服视觉和功能上的不足,但尽管如此,使用 Xt/Athena 小部件创建现代风格的 GUI 仍然是一项艰巨的任务。
历史
- 2013 年 6 月 8 日,这是关于从 C# 和 Mono Develop 进行本地调用到 X11 API 的第二篇文章。它主要涉及 Xt/Athena 小部件。第一篇文章是关于 Xlib 的。计划撰写更多文章,涉及 Xt/Motif 并深入探讨 Xlib、Xt/Athena 或 Xt/Motif。