使用 Mono Develop 编程 Xlib - 第三部分:Motif 控件(概念验证)。





5.00/5 (2投票s)
如何从 Mono Develop C# 调用本机 Xm API,最终得到一个非常小的 Motif 小部件应用程序。
介绍
本文介绍如何使用 Mono Develop 从 C# 访问 Xm/Motif 小部件 API。许多 API 调用都是现成的、经过测试的,并且克服了一些挑战,例如使用窗口管理器协议信号、透明颜色图像、菜单回调和创建新的自定义对话框。此外,还集成了一个第三方小部件,并演示了多字体字符串。
本文旨在验证使用 C# 编程 Xm/Motif 小部件可以轻松实现(概念验证)。它提供了 32 位和 64 位示例应用程序的完整项目。
背景
这是本系列文章的第三篇,探讨使用 Mono Develop 从 C# 调用 X* API。
第一篇文章是 使用 Mono Develop 编程 Xlib - 第一部分:底层(概念验证),涉及 Xlib/X11。第二篇文章是 使用 Mono Develop 编程 Xlib - 第二部分:Athena 小部件(概念验证),涉及 Xt/Athena。本文将继续介绍 Xm/Motif 小部件,并展示如何从 C#/Mono 等现代语言/IDE 中使用它们。
本文说明了使用 Xm API 调用从 C# 编程 Xm/Motif 的容易程度。由于 Open Motif 使 Motif 可用源代码并在公共许可证下发布,几乎所有 XWindow 系统都可以免费处理 Motif 小部件。此外,与 Xlib/X11 甚至 Xt/Athena 相比,使用 Motif 创建 GUI 的工作量要小得多。这些可能是选择 Motif 的两个有力理由。
使用代码
示例应用程序使用 Mono Develop 2.4.1 为 Mono 2.8.1 在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面上编写。移植到任何旧版本或新版本应该都不是问题。但是,必须安装软件包 openmotif、openmotif-libs 和 openmotif-devel(已测试版本 2.3.2-2.8 和 2.3.4-25.1)。示例应用程序的解决方案包含五个项目(完整源代码可供下载)。
- X11Wrapper 定义了 Xlib/X11 调用到 libX11.so 的函数原型、结构和类型(本系列第一篇文章中已介绍)。
- XtWrapper 定义了 Xt 调用到 libXt.so 的函数原型、结构和类型(本系列第二篇文章中已介绍)。
- XmNative 包含少量本机 C 代码,用于访问 Motif 方法,以及一个展示如何集成第三方 Xm 小部件(Microline Progress 小部件)的示例 - 必须显式引用 /usr/lib/libXm.so 或 /usr/lib64/libXm.so 才能编译项目。
- XmWrapper 定义了 Xm 调用到 libXm.so 的函数原型、结构和类型,以及 Motif 方法调用到 libXmNative.so 的函数原型,加上 Microline Progress 小部件调用。
- Xm 包含基于 Motif 小部件的示例应用程序和一个示例第三方 Xm 小部件。
应用程序本身只有 1163 行代码。一些通用的 GUI 代码被释放到几个 Xm* 类中——不是为了用 C# 封装 Motif 小部件,而是为了组织可重用代码。Xm/Motif 的函数原型以及一些必要的结构/枚举定义在 Xmlib 类中。应用程序展示了:
- 一个带有 4 个按钮的菜单栏,其中第一个和第三个按钮有下拉菜单,第二个和第四个按钮有回调函数,
- 第一个和第三个菜单按钮都有一个带有多个条目的下拉菜单,
- 在第一个菜单按钮的下拉菜单中有一个 XmFileSelectionDialog,
- 在第二个菜单按钮中有一个扩展的 Motif 消息对话框,
- 在第三个菜单按钮的下拉菜单中,有一个基于 XmRowColumn 的手动创建对话框,一个基于 XmPanedWindow 的手动创建对话框,和一个基于 XmForm 的手动创建对话框,
- 一个绘图区域和三个按钮,用于绘制随机线、椭圆或矩形,
- 一个带有透明度的应用程序图标(并非所有最新的窗口管理器都能显示它,但 Xfce 可以),
- 一个基于第三方 Motif 小部件 Microline Progress 的进度条,
- 一个带有位图和标签的状态栏,
- 一个带有位图的 XmQuestionDialog,可以取消应用程序退出。
在 GNOME 桌面 2.30.0(32 位)和 Xfce 4.10(64 位)上的外观和感受。
管理 WM_DELETE_WINDOW 信号
Motif 提供了几个方便的 VendorShell 接口来访问窗口管理器协议,并最大程度地减少回调注册的代码行数。XmAddWMProtocolCallback()
和 XmAddProtocolCallback()
为窗口管理器协议添加客户端回调,并隐藏了操作记录和转换表。尽管 XmAddWMProtocolCallback()
在内部调用 XmAddProtocolCallback()
,但我无法使其工作。相反,XmAddProtocolCallback()
可以使用由 XmInternAtom()
或 XInternAtom()
创建的原子的工作。下一个代码示例说明了如何使用 XmAddProtocolCallback()
为 WM_DELETE_WINDOW 信号注册回调。
/* Hook the closing event from windows manager. */
// Turn off default delete response.
Arg[] shellArgs = { new Arg(XmNames.XmNdeleteResponse, (XtArgVal)DeleteResponse.XmDO_NOTHING) };
Xtlib.XtSetValues (_windowShell, shellArgs, (XCardinal) 1);
// Install delete callback.
IntPtr display = Xtlib.XtDisplay (_windowShell);
IntPtr wmDeleteMessage = Xmlib.XmInternAtom (display, "WM_DELETE_WINDOW", false);
IntPtr callbackUnmanagedPointer = CallBackMarshaler.Add (_windowShell, this.WmDeleteCB);
// This toesn't work.
// Xmlib.XmAddWMProtocolCallback(_windowShell, wmDeleteMessage, callbackUnmanagedPointer, IntPtr.Zero);
// Use this instead.
IntPtr wmProperty = Xmlib.XmInternAtom (display, "WM_PROTOCOLS", false);
Xmlib.XmAddProtocolCallback(_windowShell, wmProperty, wmDeleteMessage,
callbackUnmanagedPointer, IntPtr.Zero);
便捷类 CallBackMarshaler
的 Add()
方法提供了一个非托管回调指针——这在本系列第二篇文章中已介绍。
MessageWindow(XmMainWindow 的一部分)的改进
Motif 的 XmMainWindow
由四个区域组成(从上到下):菜单栏、命令窗口、工作区域和消息窗口。消息窗口分配了一个垂直方向的 XmRowColumn
小部件来管理其子项。
第一个子项是 XmLabel
小部件,显示多字体文本“This stringhas 3 fonts.",这只是演示了 Motif 的多字体功能。
第二个子项是 XmLProgress
小部件,展示了如何集成第三方小部件。
第三个子项是状态栏,由两个 XmLabel
小部件组成,一个用于显示图标,一个用于显示状态消息。两个标签小部件都由水平方向的 XmRowColumn
小部件组织。为了有效利用状态栏的空间并允许状态消息文本长度的增长/收缩,必须通过 XmNresizeHeight
和 XmNresizeWidth
资源显式打开 XmRowColumn
小部件的动态布局。
Arg[] statrowArgs = { new Arg(XmNames.XmNorientation, (XtArgVal)XmOrientation.XmHORIZONTAL),
new Arg(XmNames.XmNpacking, (XtArgVal)XmPacking.XmPACK_NONE),
// Don't refuse size requests from children.
new Arg(XmNames.XmNresizeHeight,(XtArgVal)0),
new Arg(XmNames.XmNresizeWidth, (XtArgVal)0) };
IntPtr _statusRow = Xmlib.XmCreateRowColumn (_msgBar, StatusRowWidgetName, statrowArgs,
(XCardinal)4);
Xtlib.XtManageChild(_statusRow);
现在,图标和状态消息的 XmLabel
小部件可以使用 XmNrecomputeSize
资源强制调整大小。
Arg[] statusIconArgs = { new Arg(XmNames.XmNlabelPixmap, _statusIconPixMap),
new Arg(XmNames.XmNlabelType, (XtArgVal)XmLabelType.XmPIXMAP),
new Arg(XmNames.XmNrecomputeSize, (XtArgVal)1)};
Xtlib.XtSetValues (_statusIcon, statusIconArgs, (XCardinal)3);
...
Arg[] statusLabelArgs = { new Arg(XmNames.XmNlabelString, xmStatusLabel),
new Arg(XmNames.XmNrecomputeSize, (XtArgVal)1)};
Xtlib.XtSetValues (_statusLabel, statusLabelArgs, (XCardinal)2);
要更新状态消息文本,可以使用静态类 Logger
的全局可访问静态方法 Log()
。
创建菜单并处理回调
有两种通用的方法可以创建菜单栏及其(级联)下拉菜单。
第一种方法提供了对所有菜单创建步骤的完全控制,例如菜单栏和级联按钮的创建、菜单 shell 和菜单按钮或菜单分隔符的创建,以及回调注册。它使用 XmCreateMenuBar()
创建一个(空的)菜单栏,使用 XmCreatePullbownMenu()
创建一个下拉菜单 shell,使用 XmCreateCascadeButtonGadget()
创建一个菜单栏项或下拉菜单项并将其与下拉菜单 shell 连接,使用 XtCreateManagedWidget()
创建菜单项,如 PushButtonGadget 或 SeparatorGadget,以及使用 XtAddCallback()
注册回调。这种技术足以编写菜单构建辅助函数。
第二种方法提供了高度的便捷性,它使用 XmCreateSimpleMenuBar()
创建一个包含级联按钮或按钮的菜单栏,以及使用 XmCreateSimplePulldownMenu()
创建一个包含级联按钮、按钮或分隔符的下拉菜单 shell。这种技术足以编写非常紧凑的代码。
下一个代码示例演示了菜单栏的创建。由于 XmCreateSimpleMenuBar()
创建了所有 XmNbuttonCount
个(4 个)XmNbuttons
并为它们分配了 XmNsimpleCallback
,因此所有菜单按钮共享同一个回调函数,并且必须在菜单栏小部件创建之前准备好回调函数。这就是 fakeWidgetID
发挥作用的地方。创建菜单栏小部件后,回调函数将从 fakeWidgetID
重新分配给 _menuBar
。
可以通过键盘使用 [Alt]+[<助记符>] 组合键将 XmNbuttonMnemonics
分配给用于调用菜单的级联按钮。助记符区分大小写。menuBtnNameList
和 menuMnemonicList
参数的长度必须相同,即等于 XmNbuttonCount
。下一个代码示例演示了高度便捷的菜单栏创建。
// The menu bar.
IntPtr fakeWidgetID = (IntPtr)9999;
IntPtr[] menuBtnNameList = { Xmlib.XmStringCreateDefaultCharsetLtoR ("File"),
Xmlib.XmStringCreateDefaultCharsetLtoR ("Message box"),
Xmlib.XmStringCreateDefaultCharsetLtoR ("Dialog"),
Xmlib.XmStringCreateDefaultCharsetLtoR ("Close application") };
TUint[] menuMnemonicList = { (TUint)'F', (TUint)'M', (TUint)'D', (TUint)'C' };
IntPtr menuCbUnmanagedPtr = CallBackMarshaler.Add (fakeWidgetID, this.MenuBarCB);
Arg[] menuBarArgs = { new Arg(XmNames.XmNscrolledWindowChildType,
(XtArgVal)XmScrolledWindowChildType.XmMENU_BAR),
new Arg(XmNames.XmNbuttonCount, (XtArgVal)4),
new Arg(XmNames.XmNbuttons, menuBtnNameList),
new Arg(XmNames.XmNbuttonMnemonics, menuMnemonicList),
new Arg(XmNames.XmNsimpleCallback, menuCbUnmanagedPtr),
// Don't extend the last column to the right edge of RowColumn.
new Arg(XmNames.XmNadjustLast, (XtArgVal)0) };
_menuBar = Xmlib.XmCreateSimpleMenuBar (_mainWindow, MenuBarWidgetName, menuBarArgs, (XCardinal)6);
// ### Callback to class context assignment: Part 1
// A "new Arg(XmNames.XmNsimpleCallback, ...)" must be set at a moment, neither menu bar widget ID nor
// menu item IDs are known. Therefore the callback function can't be assigned to a widget ID in advance.
// Since callbacks loose their class context at execution time, they must be re-associated with a class
// context at execution time to achieve object oriented behaviour. To realize reassignment of a callback
// function to it's class context, the CallBackMarshaler must associate the propper widget ID.
CallBackMarshaler.Reassign (fakeWidgetID, _menuBar);
// Clean up dynamic unmanaged resources.
for (int countMenuBtnNames = 0; countMenuBtnNames < menuBtnNameList.Length; countMenuBtnNames)
{
Xmlib.XmStringFree (menuBtnNameList[countMenuBtnNames]);
}
Xtlib.XtManageChild(_menuBar);
尽管所有菜单栏级联按钮都共享同一个回调,但可以通过 clientData
回调参数区分菜单项,该参数包含菜单项索引。下一个代码示例演示了由所有菜单栏项共享的这种回调。
/// <summary> The menu bar callback procedure. </summary>
/// <param name="widget"> The widget, that initiated the callback procedure.
/// <see cref="System.IntPtr"/> </param>
/// <param name="clientData"> Additional callback data from the client.
/// <see cref="System.IntPtr"/> </param>
/// <param name="callData"> Additional data defined for the call. <see cref="System.IntPtr"/> </param>
public void MenuBarCB (IntPtr widget, IntPtr clientData, IntPtr callData)
{
int menuItemIndex = (int) clientData;
switch (menuItemIndex)
{
// File
case 0:
break;
// Message box
case 1:
XmExtendedMessageDialog dlg = new XmExtendedMessageDialog (_windowShell,
ExtendedMessageDialogName, "Enter string", "Enter data string:");
dlg.Start ();
break;
// Dialog
case 2:
break;
// Close application
case 3:
WmDeleteCB (widget, clientData, callData);
break;
}
}
菜单栏的自动回调函数赋值(通过 XmNsimpleCallback
)的替代方法是,在菜单栏小部件创建后,为每个菜单栏子项单独手动分配回调函数。尽管这可以避免处理 fakeWidgetID
,但代码会不那么紧凑和优雅。for (TInt countChildren = Xtlib.XtCountChildren (_menuBar) - 1; countChildren >= 0; countChildren--)
{
IntPtr child = Xtlib.XtGetChild (_menuBar, countChildren);
// Either we assign a different callback to each child.
// Or we do it compatible to Motif and pass the child index.
switch ((int)countChildren)
{
// File
// Dialog
// Message box
// Close application
default:
Xtlib.XtAddCallback (child, XmNames.XmNactivateCallback,
CallBackMarshaler.Add (child, this.MenuBarCB), (IntPtr)((int)countChildren));
break;
}
}
下一个代码示例演示了下拉菜单的创建。由于 XmCreateSimplePulldownMenu()
创建了所有 XmNbuttonCount
个(5 个)XmNbuttons
并为它们分配了 XmNsimpleCallback
,因此所有按钮都共享同一个回调函数,并且必须在下拉菜单小部件创建之前准备好回调函数。这再次是 fakeWidgetID
发挥作用的地方。创建下拉菜单小部件后,回调函数将从 fakeWidgetID
重新分配给 _fileMenu
。
XmNpostFromButton
定义了此下拉菜单需要关联的菜单栏级联按钮的索引。XmNbuttonAccelerators
将加速器快捷键注册到下拉菜单按钮,XmNbuttonAcceleratorText
定义了下拉菜单按钮的加速器快捷键显示。
与菜单栏一样,下拉菜单也可以通过键盘使用 [<助记符>] 键(现在不与 [Alt] 键组合)来调用菜单的级联按钮,并分配 XmNbuttonMnemonics
。助记符区分大小写。fileBtnNameList
、fileBtnAccelList
、fileMnemonicList
和 fileBtnTypeList
参数的长度必须相同,即等于 XmNbuttonCount
。
// The file pull down menu.
IntPtr[] fileBtnNameList = { Xmlib.XmStringCreateDefaultCharsetLtoR ("Open..."),
Xmlib.XmStringCreateDefaultCharsetLtoR ("Save"),
Xmlib.XmStringCreateDefaultCharsetLtoR ("Save as ..."),
Xmlib.XmStringCreateDefaultCharsetLtoR (""),
Xmlib.XmStringCreateDefaultCharsetLtoR ("Exit") };
TUint[] fileMnemonicList = { (TUint)'O', (TUint)'S', (TUint)'a', (TUint)' ', (TUint)'x' };
IntPtr[] fileBtnAccelList = { Marshal.StringToHGlobalAuto ("Ctrl<Key>o\0"),
Marshal.StringToHGlobalAuto ("Ctrl<Key>s\0"),
Marshal.StringToHGlobalAuto ("Ctrl<Key>a\0"),
Marshal.StringToHGlobalAuto ("\0"),
Marshal.StringToHGlobalAuto ("Ctrl<Key>x\0") };
IntPtr[] fileBtnAccelTexts = { Xmlib.XmStringCreateDefaultCharsetLtoR ("Ctrl-O"),
Xmlib.XmStringCreateDefaultCharsetLtoR ("Ctrl-S"),
Xmlib.XmStringCreateDefaultCharsetLtoR ("Ctrl-A"),
Xmlib.XmStringCreateDefaultCharsetLtoR (""),
TChar[] fileBtnTypeList = { (TChar)XmButtonType.XmPUSHBUTTON,
(TChar)XmButtonType.XmPUSHBUTTON,
(TChar)XmButtonType.XmPUSHBUTTON,
(TChar)XmButtonType.XmSEPARATOR,
(TChar)XmButtonType.XmPUSHBUTTON};
IntPtr fileCbUnmanagedPtr = CallBackMarshaler.Add (fakeWidgetID, this.FileMenuCB);
// File menu button index (to start this pulldown for) is 0.
Arg[] fileMenuArgs = { new Arg(XmNames.XmNpostFromButton, (XtArgVal)0),
new Arg(XmNames.XmNbuttonCount, (XtArgVal)5) ,
new Arg(XmNames.XmNbuttons, fileBtnNameList),
new Arg(XmNames.XmNbuttonAccelerators, fileBtnAccelList),
new Arg(XmNames.XmNbuttonAcceleratorText, fileBtnAccelTexts),
new Arg(XmNames.XmNbuttonMnemonics, fileMnemonicList),
new Arg(XmNames.XmNbuttonType, fileBtnTypeList),
new Arg(XmNames.XmNsimpleCallback, fileCbUnmanagedPtr)};
_fileMenu = Xmlib.XmCreateSimplePulldownMenu (_menuBar, FileMenuWidgetName, fileMenuArgs, (XCardinal)7);
// ### See comments "Callback to class context assignment" part 1 & 2 on simple menu bar creation.
CallBackMarshaler.Reassign (fakeWidgetID, _fileMenu);
// Clean up dynamic unmanaged resources.
for (int countButtons = 0; countButtons < 5; countButtons++)
{
Xmlib.XmStringFree (fileBtnNameList[countButtons]);
Marshal.FreeHGlobal (fileBtnAccelList[countButtons]);
Xmlib.XmStringFree (fileBtnAccelTexts[countButtons]);
}
同样,所有下拉菜单按钮都共享同一个回调,并且可以通过 clientData
回调参数区分菜单项,该参数包含菜单项索引。/// <summary> The file menu callback procedure. </summary>
/// <param name="widget"> The widget, that initiated the callback procedure.
/// <see cref="System.IntPtr"/> </param>
/// <param name="clientData"> Additional callback data from the client.
/// <see cref="System.IntPtr"/> </param>
/// <param name="callData"> Additional data defined for the call.
/// <see cref="System.IntPtr"/> </param>
private void FileMenuCB (IntPtr widget, IntPtr clientData, IntPtr callData)
{
int menuItemIndex = (int) clientData;
switch (menuItemIndex)
{
// Open
case 0:
XmFileSelectionDialog dlg = new XmFileSelectionDialog (_windowShell, FileSelectionWidgetName,
"Open file ...");
dlg.Start ();
break;
// Save
case 1:
IntPtr saveMessage = Xmlib.XmStringCreateDefaultCharset ("'Safe' menu item not implemented yet.");
Arg[] saveDlgArgs = { new Arg (XmNames.XmNtitle, "Safe menu item"),
new Arg (XmNames.XmNmessageString, saveMessage) };
IntPtr saveDlg = Xmlib.XmCreateInformationDialog (_windowShell, ExtendedMessageDialogName,
saveDlgArgs, (XCardinal)2);
Xmlib.XmStringFree (saveMessage);
Xtlib.XtSetSensitive (Xtlib.XtNameToWidget (saveDlg, "Cancel"), (TBoolean)0);
Xtlib.XtSetSensitive (Xtlib.XtNameToWidget (saveDlg, "Help"), (TBoolean)0);
Xtlib.XtManageChild(saveDlg);
Xtlib.XtPopup (Xtlib.XtParent (saveDlg), XtGrabKind.XtGrabNone);
break;
// Save As
case 2:
IntPtr saveasMessage = Xmlib.XmStringCreateDefaultCharset ("'Safe As' menu item not implemented" +
" yet.");
Arg[] saveasDlgArgs = { new Arg (XmNames.XmNtitle, "Safe As menu item"),
new Arg (XmNames.XmNmessageString, saveasMessage) };
IntPtr saveasDlg = Xmlib.XmCreateInformationDialog (_windowShell, ExtendedMessageDialogName,
saveasDlgArgs, (XCardinal)2);
Xmlib.XmStringFree (saveasMessage);
Xtlib.XtSetSensitive (Xtlib.XtNameToWidget (saveasDlg, "Cancel"), (TBoolean)0);
Xtlib.XtSetSensitive (Xtlib.XtNameToWidget (saveasDlg, "Help"), (TBoolean)0);
Xtlib.XtManageChild(saveasDlg);
Xtlib.XtPopup (Xtlib.XtParent (saveasDlg), XtGrabKind.XtGrabNone);
break;
// Exit
case 3: // (Separators don't count!)
WmDeleteCB (widget, clientData, callData);
break;
}
}
修改标准的 Motif 对话框
接下来将展示对 Motif 标准 MessageDialog 的两个示例修改,两者都通过 XmExtendedMessageDialog
类实现。
- 显示带有透明度的自定义颜色符号,而不是默认符号。
- 集成附加的文本字段。
在修改中涉及非托管资源时,要做的第一件事是提供一种处置这些资源的方法。执行此操作的最佳时机是对话框的销毁。有两种方法可以挂钩对话框的销毁,这两种方法最终都会导致相同的行为和相同的信号使用。
要么可以使用 XmAddProtocolCallback()
为 WM_DELETE_WINDOW
在 WM_PROTOCOLS
上注册信号处理程序。这大约需要 5 行代码,并在示例应用程序的 XmWindows.Run()
方法中进行了演示,该方法已在“管理 WM_DELETE_WINDOW 信号”一章中讨论过。
XmNdeleteResponse
属性从 XmUNMAP
(默认行为)更改为 XmDESTROY
,并注册 XmNdestroyCallback
。因为对话框的属性已经设置,并且回调方法已经准备好,所以只需要另外两行代码。Arg[] messageDlgArgs = {
...
// Define action on delete on WM_DESTROY.
new Arg(XmNames.XmNdeleteResponse, (XtArgVal)DeleteResponse.XmDESTROY) };
...
// Send notification on WM_DESTROY do dialog's callback.
Xtlib.XtAddCallback (_dialogWidget, XmNames.XmNdestroyCallback, callbackUnmanagedPointer, (IntPtr)99);
使用一个回调方法处理所有对话框消息,并通过 clientData 或 callData 回调参数区分要执行的操作,这始终是一个好方法。对话框的回调方法可以被重用,并且还可以控制非托管资源的处置。
自定义颜色符号(带透明度)
要为标准的 Motif 对话框提供自定义颜色符号(包括透明度),需要四个步骤。
- 确定背景颜色。
- 创建透明度掩码位图。
- 创建带有背景颜色像素作为透明图像像素的符号位图。
- 将符号位图分配给对话框。
// Change icon.
// - Determine background color.
TPixel backgroundColorPixel = Xtlib.XtGetValueOfPixel (_dialogWidget, XmNames.XmNbackground);
// - Prepare and set symbol.
_display = Xtlib.XtDisplay (parentShell);
IntPtr window = Xtlib.XtWindow (parentShell);
TInt screen = Xtlib.XDefaultScreen (_display);
using (X11Graphic dialogIcon = new X11Graphic (_display, screen, IconPath))
{
_maskPixmap = dialogIcon.CreateIndependentMaskPixmap (_display, window);
_iconPixMap = dialogIcon.CreateIndependentPixmap (_display, window, backgroundColorPixel, _maskPixmap);
if (_iconPixMap != IntPtr.Zero)
{
Arg[] symbolDlgArgs = { new Arg(XmNames.XmNsymbolPixmap, _iconPixMap) };
Xtlib.XtSetValues (_dialogWidget, symbolDlgArgs, (XCardinal)1);
}
}
要确定背景颜色像素,可以使用 XtGetValueOfPixel()
便利方法。它隐藏了复杂的颜色像素值确定过程,该过程需要传入指向 TPixel
变量的引用,并且 TPixel
必须被 ReadInt32()
(对于 32 位)或 ReadInt64()
(对于 64 位 OS)识别。
最具挑战性的一步是创建带有背景颜色像素作为透明图像像素的符号位图。为实现这一点,X11Graphic
类(本系列第一篇文章 使用 Mono Develop 编程 Xlib - 第一部分:底层(概念验证) 中已介绍)已通过新方法 CreateIndependentPixmap()
进行了扩展。该方法需要位图句柄 maskPixmap
(作为参数)并返回一个位图句柄。位图句柄引用服务器端位图资源。
/// <summary> Provide a bitmap, containing the transparent graphic, that can be used
/// independent from this class. </summary>
/// <param name="display"> The display pointer, that specifies the connection to the X
/// server. <see cref="System.IntPtr"/> </param>
/// <param name="window"> The target window to create the pixmap for. <see cref="IntPtr"/>
/// </param>
/// <param name="backgroundColorPixel"> The background color behind any transparent
/// pixel. <see cref="TPixel"/> </param>
/// <param name="maskPixmap"> The mask pixmap to distinguish transparent from intransparent
/// pixel. <see cref="IntPtr"/> </param>
/// <returns> The pixmap on success, or IntPtr.Zero otherwise. <see cref="IntPtr"/> </returns>
public IntPtr CreateIndependentPixmap (IntPtr display, IntPtr window, TPixel backgroundColorPixel,
IntPtr maskPixmap)
{
if (_graphicXImage == IntPtr.Zero)
return IntPtr.Zero;
IntPtr pixmap = X11lib.XCreatePixmap (display, window, (TUint)_width, (TUint)_height,
(TUint)_graphicDepth);
// Fill pixmap with background color.
IntPtr bgGc = X11lib.XCreateGC (display, window, (TUlong)0, IntPtr.Zero);
X11lib.XSetForeground (display, bgGc, backgroundColorPixel);
X11lib.XFillRectangle (display, pixmap, bgGc, (TInt)0, (TInt)0, (TUint)_width, (TUint)_height);
X11lib.XFreeGC (display, bgGc);
bgGc = IntPtr.Zero;
// Overlay the image.
IntPtr pixmapGc = X11lib.XCreateGC (display, window, (TUlong)0, IntPtr.Zero);
if (pixmapGc != IntPtr.Zero)
{
if (maskPixmap != IntPtr.Zero)
{
// Prepare the clipping graphics context.
IntPtr graphicGc = X11lib.XCreateGC (display, window, (TUlong)0, IntPtr.Zero);
if (graphicGc != IntPtr.Zero)
{
X11lib.XSetClipMask (display, graphicGc, maskPixmap);
X11lib.XSetClipOrigin (display, graphicGc, (TInt)0, (TInt)0);
// Draw graphic using the clipping graphics context.
X11lib.XPutImage (display, pixmap, graphicGc, _graphicXImage, (TInt)0, (TInt)0,
(TInt)0, (TInt)0, (TUint)_width, (TUint)_height);
// Restore previous behaviour and clean up.
X11lib.XSetClipMask (display, graphicGc, IntPtr.Zero);
X11lib.XSetClipOrigin (display, graphicGc, (TInt)0, (TInt)0);
// Console.WriteLine (CLASS_NAME + "::Draw () Delete clipping image GC.");
X11lib.XFreeGC (display, graphicGc);
graphicGc = IntPtr.Zero;
}
else
{
Console.WriteLine (CLASS_NAME + "::Draw () ERROR: Can not create graphics " +
"context for transparency application.");
}
}
else
{
X11lib.XPutImage (display, pixmap, pixmapGc, _graphicXImage, (TInt)0, (TInt)0,
(TInt)0, (TInt)0, (TUint)_width, (TUint)_height);
// Console.WriteLine (CLASS_NAME + "::CreateIndependentGraphicPixmap () Delete " +
// "graphic image GC.");
X11lib.XFreeGC (display, pixmapGc);
pixmapGc = IntPtr.Zero;
}
}
else
{
Console.WriteLine (CLASS_NAME + "::CreateIndependentGraphicPixmap () ERROR: Can not create " +
"graphics context for graphic pixmap.");
}
return pixmap;
}
在对话框生命周期的最后,必须处置非托管资源。XmExtendedMessageDialog
类的 Dispose()
方法实现了对非托管图像资源、掩码位图和透明图像位图的处置。
/// <summary> IDisposable implementation. </summary>
public override void Dispose ()
{
Console.WriteLine (CLASS_NAME + "::Dispose ()");
this.DisposeByParent ();
}
/// <summary> Dispose by parent. </summary>
public override void DisposeByParent ()
{
// Dispose new ressources.
UnregisterCallbacks ();
if (_display != IntPtr.Zero)
{ if (_iconPixMap != IntPtr.Zero)
{
// Dispose symbol pixmap.
X11lib.XFreePixmap (_display, _iconPixMap);
_iconPixMap = IntPtr.Zero;
}
if (_maskPixmap != IntPtr.Zero)
{
// Dispose mask pixmap.
X11lib.XFreePixmap (_display, _maskPixmap);
_maskPixmap = IntPtr.Zero;
}
}
// Dispose base ressources.
base.DisposeByParent ();
}
Dispose()
方法将从下一个段落中显示的所有对话框消息的唯一回调方法 DialogCB()
调用。
附加的文本字段
将文本字段添加到 Motif 标准对话框是一项相当简单的任务。
// Inject a text widget.
IntPtr textField = Xmlib.XmCreateTextField (_dialogWidget, TextFieldWidgetName, Arg.Zero, 0);
Xtlib.XtManageChild (textField);
对话框确认后,应读取用户输入的文本字段中的文本。最佳位置是 DialogCB()
回调。它对任何按钮事件(帮助按钮未托管)以及销毁事件(所有回调注册都在构造函数中完成)都会被调用。下一个代码示例显示了 DialogCB()
回调的所有注册。
/// <summary> Register all callbacks (separetely from construction.) </summary>
private void RegisterCallbacks ()
{
IntPtr callbackUnmanagedPointer = CallBackMarshaler.Add (_dialogWidget, this.DialogCB);
Xtlib.XtAddCallback (_dialogWidget, XmNames.XmNokCallback,
callbackUnmanagedPointer, (IntPtr)0);
Xtlib.XtAddCallback (_dialogWidget, XmNames.XmNcancelCallback,
callbackUnmanagedPointer, (IntPtr)1);
// Send notification on WM_DESTROY do dialog's callback.
// Requires XmNames.XmNdeleteResponse set to DeleteResponse.XmDESTROY
// to be called from windows decorations as well.
Xtlib.XtAddCallback (_dialogWidget, XmNames.XmNdestroyCallback,
callbackUnmanagedPointer, (IntPtr)99);
}
由于文本将通过 XtGetValues()
方法确定,并以指向 char*
变量的引用形式返回——正如上面的背景颜色像素以指向 TPixel
变量的引用形式返回一样——因此也有必要对变量引用进行封送处理。因为文本字段的文本不是 Motif 复合字符串,所以可以使用 ReadIntPtr()
来返回文本。/// <summary> The question dialog command button callback procedure. </summary>
/// <param name="widget"> The widget, that initiated the callback procedure.
/// <see cref="System.IntPtr"/> </param>
/// <param name="clientData"> Additional callback data from the client.
/// <see cref="System.IntPtr"/> </param>
/// <param name="callData"> Additional data defined for the call. <see cref="System.IntPtr"/>
/// </param>
private void DialogCB (IntPtr widget, IntPtr clientData, IntPtr callData)
{
// Distinguish the action to take by callData.
XmAnyCallbackStruct cbStruct = (callData != IntPtr.Zero ? (XmAnyCallbackStruct)
Marshal.PtrToStructure (callData, typeof (XmAnyCallbackStruct)) :
new XmAnyCallbackStruct ());
// Distinguish the action to take by clientData.
// int buttonIndex = (int) clientData;
Xtlib.XtUnmanageChild (_dialogWidget);
DisposeResources ();
if (cbStruct.reason == (TInt)XmCallbackReason.XmCR_OK) // buttonIndex == 0)
{
IntPtr textField = Xtlib.XtNameToWidget (_dialogWidget, TextFieldWidgetName);
IntPtr pointer = Xtlib.XtGetValueOfPointer (textField, XmNames.XmNvalue);
string prompt = Marshal.PtrToStringAnsi (pointer);
Logger.Log (CLASS_NAME + "::DialogCB() OK ==> " + prompt);
Console.WriteLine (CLASS_NAME + "::DialogCB() OK ==> " + prompt);
Xtlib.XtDestroyWidget (_dialogWidget);
}
else if (cbStruct.reason == (TInt)XmCallbackReason.XmCR_CANCEL) // buttonIndex == 1)
{
Console.WriteLine (CLASS_NAME + "::DialogCB() Cancel");
Xtlib.XtDestroyWidget (_dialogWidget);
}
if ((int) clientData == 99)
{
// WM_DESTROY.
Dispose ();
}
}
DialogCB()
是整个对话框的唯一回调方法。它使用 callData
参数(通过 PtrToStructure()
转换为 XmAnyCallbackStruct
)来区分要执行的操作。这是处理对话框消息的首选方式。
区分操作的替代方法(使用 clientData
参数)也已显示但已禁用。
在按钮事件处理后,DialogCB()
调用 XtDestroyWidget()
,这将强制对话框销毁,进而调用 DialogCB()
来处置所有非托管资源。
从头开始设计一个新的自定义对话框
基于 XmRowColumn 的自定义对话框
从头开始构建一个(结构非常简单)的新自定义对话框的最简单方法是使用 XmRowColumn
小部件作为根管理器小部件,也作为操作区域小部件来布局 XmDialogShell
小部件的子项。这没什么不对,但应该提到的是,XmRowColumn
小部件的易用性可能会破坏与通用 Motif 对话框处理的兼容性——特别是键盘快捷键。
所有内置 Motif 对话框都使用 XmBulletinBoard
,它有几个默认的转换(有关详细信息,请参阅 Transltns.c 文件中的 _XmBulletinB_defaultTranslations),包括用于激活键(应用对话框)、取消键(转义对话框)和帮助键(调用帮助)的转换,以及用于处理 <Key>osfActivate
、<Key>osfCancel
和 <Key>osfHelp
键盘快捷键的 XmNdefaultButton
和 XmNcancelButton
资源。XmRowColumn
小部件既没有默认转换也没有处理键盘快捷键的资源。
为了实现基于 XmRowColumn
的新自定义对话框与 XmBulletinBoard
相同的用户体验,以下几点很重要:
- 扩展管理器小部件的转换。如果使用多个管理器小部件,可能需要扩展一个以上的管理器小部件的转换。
- 使用小部件而不是控件,至少对于(OK、Cancel、...)按钮,否则管理器小部件的转换将不会被注意到。
- 避免使用
XmText
和XmTextBox
小部件,或者为它们提供回调函数或转换。
XmHandMadeDialogOnRowColumn
类实现了一个基于 XmRowColumn
的新自定义对话框。
1. 使用“osf”keysyms 来扩展管理器小部件的转换始终是一个好方法。由于 ActionMarshaler
类的实现细节,每个事件类型只能注册一个操作过程。这就是为什么所有转换都调用 DlgKeyPressAction()
。为区分激活、取消和帮助的不同按键事件,会传递一个操作过程参数。
/// <summary> The translations to realize dialog key press response as expected. </summary>
public const string HandMadeDlgTranslations = "#override\n:<Key>osfActivate: DlgKeyPressAction(0)" +
"\n:<Key>osfCancel: DlgKeyPressAction(1)" +
"\n:<Key>osfHelp: DlgKeyPressAction(2)";
操作注册必须在转换注册之前完成。一旦完成操作注册,就可以在多个转换表中重复使用这些操作。
// Prepare generic action procedure to handle Enter, Escape and Help key press events.
// Don't forget to assign a method to a widget to enable the action procedure.
XtActionsRec[] actRec = { new XtActionsRec (X11.X11Utils.StringToSByteArray
("DlgKeyPressAction\0"), ActionMarshaler.ActionProc) };
Xmlib.XtAppAddActions (XmWindow.Instance.AppContext, actRec, (XCardinal)1);
解析后的转换不应通过 XtSetValues()
应用(这将完全替换已存在的窗口小部件转换),而应通过 XtOverrideTranslations()
应用(以将新转换合并到现有窗口小部件转换中)。// Enable the action procedure for the actionarea RowColumn widget.
// Use XtOverrideTranslations() instead of XtSetValues() to prevent a complete replacement.
ActionMarshaler.Add (_actionareaWidget, XEventName.KeyPress, this.DlgKeyPressAction);
IntPtr mgrTransl = Xtlib.XtParseTranslationTable (HandMadeDlgTranslations);
Xtlib.XtOverrideTranslations (_actionareaWidget, mgrTransl);
2. 尽管按钮小部件(不是控件!)通常支持加速器,并且加速器不得在应用程序级别安装,但启用此功能是一项艰巨的任务。有关详细信息,请参阅 Motif FAQ 132“为什么我不能在菜单外的按钮上使用加速器?”。至少,如果必须以某种方式安装支持的加速器,使其可以从对话框的所有可聚焦小部件/控件调用,则此方法会遇到麻烦。
3. 对话框中的文本小部件使转换处理变得复杂。根据 XmNeditMode
资源,文本小部件可以是 XmSINGLE_LINE_EDIT
或 XmMULTI_LINE_EDIT
。对于多行文本小部件,激活键需要创建新行。仅对于单行文本小部件,激活键可用于关闭对话框。
可以使用预定义资源 XmNactivateCallback
和 XmNhelpCallback
支持激活键(对于单行文本小部件)和帮助键,但没有合适的资源来支持取消键。
或者,因为管理器小部件的操作已在应用程序级别注册,所以可以在文本小部件上重用这些操作。通常,文本小部件的转换是管理器小部件转换的子集(多行文本小部件可能会省略激活键)。
/// <summary> The translations to realize text widget key press response as expected. </summary>
public const string TextWidgetTranslations = "#override\n:<Key>osfActivate: DlgKeyPressAction(0)" +
"\n:<Key>osfCancel: DlgKeyPressAction(1)" +
"\n:<Key>osfHelp: DlgKeyPressAction(2)";
...
// Enable the action procedure for the text TextField widget.
// Use XtOverrideTranslations() instead of XtSetValues() to prevent a complete replacement.
ActionMarshaler.Add (textField, XEventName.KeyPress, this.DlgKeyPressAction);
IntPtr textTransl = Xtlib.XtParseTranslationTable (TextWidgetTranslations);
Xtlib.XtOverrideTranslations (textField, textTransl);
估算
基于 XmRowColumn
的自定义对话框易于创建,并且可以设计得与通用 Motif 对话框处理完全兼容。最大的障碍是几乎静态的布局——对话框大小调整可能会完全混乱布局。
通过关闭窗口装饰/窗口功能来防止对话框大小调整是有疑问的,因为并非所有窗口管理器都支持 VendorShell
的 XmNmwmFunctions
和 XmNmwmDecorations
资源的所有 MWM 标志。
Arg[] shellArgs = { new Arg (XmNames.XmNtitle, caption),
new Arg (XmNames.XmNmwmFunctions, (XtArgVal)MWM_Functions.MWM_FUNC_ALL),
new Arg (XmNames.XmNmwmDecorations, (XtArgVal)MWM_Decorations.MWM_DECOR_ALL),
new Arg (XmNames.XmNdeleteResponse, (XtArgVal)DeleteResponse.XmDESTROY) };
_dialogShell = Xmlib.XmCreateDialogShell (parentShell, ShellResourceName, shellArgs, (XCardinal)4);
或者,根管理器小部件的大小可以固定。
XDimension height = Xtlib.XtGetValueOfDimension (_dialogWidget, XmNames.XmNheight);
XDimension width = Xtlib.XtGetValueOfDimension (_dialogWidget, XmNames.XmNwidth);
Arg[] cancelArgs = { new Arg (XmNames.XmNminWidth, (XtArgVal)width),
new Arg (XmNames.XmNmaxWidth, (XtArgVal)width),
new Arg (XmNames.XmNminHeight, (XtArgVal)height),
new Arg (XmNames.XmNmaxHeight, (XtArgVal)height) };
Xtlib.XtSetValues (_dialogShell, cancelArgs, (XCardinal)4);
基于 XmPanedWindow 的自定义对话框
几乎所有文献都展示了如何使用XmPanedWindow
小部件作为根管理器小部件,并使用 XmForm
作为操作区域小部件来布局 XmDialogShell
小部件的子项,从而从头开始构建一个新的自定义对话框。由于 XmForm
小部件继承自 XmBulletinBoard
小部件,因此比 XmRowColumn
小部件更容易实现与通用 Motif 对话框处理的兼容性。XmHandMadeDialogOnPanedWindow
类实现了一个新的自定义对话框,该对话框基于 XmPanedWindow
作为根管理器小部件,并使用 XmForm
作为操作区域小部件。
使用 XmForm
小部件非常简单。这个管理器小部件在一个网格上展开,具有大小相等的单元格,用于定位小部件。“OK”和“Cancel”按钮的操作区域应靠左和靠右对齐对话框边框。
OK (位置 1) | (位置 2) | (位置 3) | Cancel (位置 4) |
小部件也可以跨越多个网格位置。复杂的布局(几乎只能)通过广泛使用单元格跨度来实现。
(位置 1) OK (位置 2) | (位置 3) | (位置 4) Cancel (位置 5) |
网格单元的数量可以通过 XmNfractionBase
资源设置。
// ==== Action area, that holds all action buttons.
Arg[] actaArgs = { new Arg (XmNames.XmNfractionBase, (XtArgVal)(ActionAreaButtons + ActionAreaBlanks)),
new Arg (XmNames.XmNleftOffset, (XtArgVal)10),
new Arg (XmNames.XmNrightOffset, (XtArgVal)10) };
_actionareaWidget = Xmlib.XmCreateForm (_dialogWidget, ActionAreaResourceName, actaArgs, (XCardinal)3);
窗体小部件的子项的约束支持附加到窗体小部件边界、相邻小部件和网格位置。// ==== OK action button.
IntPtr okLabel = Xmlib.XmStringCreateDefaultCharset ("OK");
Arg[] okArgs = { new Arg (XmNames.XmNleftAttachment, (XtArgVal)XmAttachement.XmATTACH_POSITION),
new Arg (XmNames.XmNrightAttachment, (XtArgVal)XmAttachement.XmATTACH_POSITION),
new Arg (XmNames.XmNtopAttachment, (XtArgVal)XmAttachement.XmATTACH_FORM),
new Arg (XmNames.XmNbottomAttachment, (XtArgVal)XmAttachement.XmATTACH_FORM),
new Arg (XmNames.XmNleftPosition, (XtArgVal)0),
new Arg (XmNames.XmNrightPosition, (XtArgVal)1),
new Arg (XmNames.XmNlabelString, okLabel),
new Arg (XmNames.XmNshowAsDefault, (XtArgVal)1),
new Arg (XmNames.XmNdefaultButtonShadowThickness, (XtArgVal)1) };
_okbuttonWidget = Xmlib.XmCreatePushButtonGadget (_actionareaWidget, OkResourceName, okArgs, (XCardinal)9);
Xmlib.XmStringFree (okLabel);
Xtlib.XtManageChild (_okbuttonWidget);
如果窗体小部件的
XmNfractionBase
资源和窗体子小部件的 XmN*Position
资源不匹配,通常最后一个子小部件会平衡不匹配。
为实现典型的键盘快捷键行为,可以使用 **XmBulletinBoard
** 小部件的 XmNdefaultButton
和 XmNcancelButton
资源,以及 XmManager
小部件的 XmNhelpCallback
资源。
估算
基于 XmPanedWindow
和 XmForm
的自定义对话框易于创建,并且可以设计得几乎与通用 Motif 对话框处理兼容。调整大小是符合预期的。唯一的缺点是键盘遍历期间 XmPanedWindow
的窗格上的“不可见”制表符。
基于 XmBulletinBoard 的自定义对话框
由于 XmBulletinBoard
小部件默认没有内置的几何管理,因此从头开始使用 XmBulletinBoard
小部件作为根管理器小部件创建自定义对话框是一项非常艰巨的任务。通常,使用 XmBulletinBoard
的子类 XmForm
更有效。
使用 XmForm
的特别挑战在于定义所有小部件的最佳附件。XmHandMadeDialogOnBulletinBoard
类实现了一个简单的 XmBulletinBoard
小部件对话框,该对话框完全像 Motif MessageBox 对话框一样运行(大小调整、加速器等)。对于更复杂的对话框布局,可能不可避免地需要使用比单个根管理器小部件更多的管理器小部件。在这种情况下,可能会发生加速器必须分配给多个。
关注点
回调函数和非托管资源的处置
菜单栏和下拉菜单的回调注册必须使用一个技巧(fakeWidgetID
)来继续使用已引入的便捷类 CallBackMarshaler
。
所有 shell(主窗口、对话框)都继承自 XtShell
,因此通过 Dispose()
、DisposeByParent()
、RegisterCallbacks()
和 UnregisterCallbacks()
类方法标准化了非托管资源的处置、回调注册和回调注销。所有 shell 都必须将其删除响应行为从 XmUNMAP
更改为 XmDESTROY
,以确保正确的 WM_DESROY
消息处理。
通用功能
此示例应用程序还展示了一个符合 Motif 的第三方小部件的使用。有几个商业、开源或社区项目提供符合 Motif 的第三方小部件,例如 The Xg widget set、XmHTML 或 XRT,以及可用小部件的集合,例如 Widget.FAQ、motifdeveloper、ftp widget project collection 或 ICS MotifZone。尽管其中大部分已不再维护,但它们可能有助于完成 Motif 应用程序。
历史
- 2013 年 9 月 18 日,这是关于从 C# 和 Mono Develop 到 X11 API 的本机调用的第三篇文章。主要讨论 Xm/Motif 小部件。第一篇文章只涉及 Xlib。第二篇文章只涉及 Xlib。计划在后续文章中深入探讨 Xlib、Xt/Athena 或 Xm/Motif。