Download complete project for 32 bit (zip)
Download complete project for 64 bit (zip)
Introduction
This article shows how to access Xm/Motif widgets API from C# using Mono Develop. A lot of API calls are ready-to-use defined and tested and some challenges are mastered, e.g., using windows manager protocol signals, transparent color image, menu callbacks and creating new custom dialogs. Moreover a third party widgets is incorporated and a multifont string is demonstrated.
This article is aimed to verify that programming Xm/Motif widgets with C# can be easyly achieved (prove of concept). It provides a sample application's complete project for 32 bit and 64 bit.
Background
This is the third article in a series of articles examining X* API calls from C# using Mono Develop.
The first article was Programming Xlib with Mono Develop - Part 1: Low-level (proof of concept) and dealed with Xlib/X11. The second article was Programming Xlib with Mono Develop - Part 2: Athena widgets (proof of concept) and dealed with Xt/Athena. The current article goes ahead to the Xm/Motif widgets and shows how to use them from a modern language/IDE like C#/Mono.
This article illustrates how easy programming Xm/Motif using Xm API calls from C# is to achieve. Since Open Motif made Motif available with source code and under a public license, almost every XWindow system can deal with the Motif widgets royalty free. Moreover the effort to create a GUI with Motif is a fraction compared to Xlib/X11 and even to Xt/Athena. These might be two strong reasons to go in for Motif.
Using the code
The sample application was written with Mono Develop 2.4.1 for Mono 2.8.1 on OPEN SUSE 11.3 Linux 32 bit EN and GNOME desktop. Neither the port to any older nor to any newer version should be a problem. However the software packsges openmotif, openmotif-libs and openmotif-devel (versions 2.3.2-2.8 and 2.3.4-25.1 are tested) must be installt. The sample application's solution consists of five projects (the complete sources are provided for download):
- X11Wrapper defines the function prototypes, structures and types for Xlib/X11 calls to the libX11.so (already known from the first article of this series)
- XtWrapper defines the function prototypes, structures and types for Xt calls to the libXt.so (already known from the second article of this series)
- XmNative contains a very little amount of native C code to access Motif methods and one sample that shows how to incorporate third party Xm widgets (Microline Progress widget) - /usr/lib/libXm.so or /usr/lib64/libXm.so must be explicitly referenced to compile the project
- XmWrapper defines the function prototypes, structures and types for Xm calls to the libXm.so and the function prototypes for Motif methods calls to the libXmNative.so plus Microline Progress widget calls
- Xm contains the sample application based on Motif widgets and one exemplary third party Xm widget
The sample application is also tested with Mono Develop 3.0.6 for Mono 3.0.4 on OPEN SUSE 12.3 Linux 64 bit DE and GNOME desktop, IceWM, TWM und Xfce. The only difference between the 32 bit and the 64 bit solution is the definition of some types, as already described in the first article of this series.
The application itself has 1163 lines of code only. Some generic GUI code is released to several Xm* classes - not to wrap Motif widgets with C#, but to organize re-usable code. The function prototypes of the Xm/Motif and some necessary structures / enumerations are defined in the Xmlib class. The application shows:
- a menu bar with 4 buttons, where the first and third button have a dropdown menu and the second and fourth button have callbacks,
- a dropdown menu with multiple entries at the first and third menu button,
- a XmFileSelectionDialog at the first menu button's dropdown menu,
- an extended Motif message dialog at the second menu button,
- a hand made dialog based on XmRowColumn, a hand made dialog based on XmPanedWindow and a hand made dialog based on XmForm at the third menu button's dropdown menu,
- a drawing area and three buttons to draw random lines, ellipses or rectangles,
- an application icon with transparency (not all up to date windows managers display it, but Xfce does),
- a progress bar based on third party Motif widget Microline Progress,
- a status bar with bitmap and label and
- a XmQuestionDialog with bitmap, that can cancel application quit.
Look & feel on GNOME desktop 2.30.0 (32 bit) and on Xfce 4.10 (64 bit)
Manage the WM_DELETE_WINDOW signal
Motif provides several convenience interfaces for the VendorShell to access the windows manager protocol and to minimize the number of code lines for callback registration. XmAddWMProtocolCallback()
and XmAddProtocolCallback()
add client callbacks for a windows manager protocol and hide the handling of action records and translation tables. Although XmAddWMProtocolCallback()
is a internally calling XmAddProtocolCallback()
, i couldn't get it to work. Counterwise XmAddProtocolCallback()
does its job with atoms created by XmInternAtom()
as well by XInternAtom()
. The next code sampe illustrates how XmAddProtocolCallback()
can be used to register a callback for the WM_DELETE_WINDOW signal.
Arg[] shellArgs = { new Arg(XmNames.XmNdeleteResponse, (XtArgVal)DeleteResponse.XmDO_NOTHING) };
Xtlib.XtSetValues (_windowShell, shellArgs, (XCardinal) 1);
IntPtr display = Xtlib.XtDisplay (_windowShell);
IntPtr wmDeleteMessage = Xmlib.XmInternAtom (display, "WM_DELETE_WINDOW", false);
IntPtr callbackUnmanagedPointer = CallBackMarshaler.Add (_windowShell, this.WmDeleteCB);
IntPtr wmProperty = Xmlib.XmInternAtom (display, "WM_PROTOCOLS", false);
Xmlib.XmAddProtocolCallback(_windowShell, wmProperty, wmDeleteMessage,
callbackUnmanagedPointer, IntPtr.Zero);
The convenience class CallBackMarshaler
method Add()
provides an unmanaged callback pointer - already known from the second article of this series.
MessageWindow (of XmMainWindow) refinements
A Mofti XmMainWindow
consists of four areas (from top to bottom) MenuBar, CommandWindow, WorkRegion an MessageWindow. The MessageWindow has assigned a vertical oriented XmRowColumn
widget to manage its children.
The first child is a XmLabel
widget displaying the multiple font text "This string has 3 fonts." , that only demonstrates the multi font capabilities of Motif.
The second child ist a XmLProgress
widget, that shows how to incorporate a third party widget.
The third child is a status bar, that consists of two XmLabel
widgets, one to display an icon and one to display the status message. Both label widgets are organized by a horizontal oriented XmRowColumn
widget. To use the available space of the status bar effectively and to enable growth/shrinkage of the status message text length, the dynamic layout for the XmRowColumn
widget has to be switched on explicitly - using the XmNresizeHeight
and XmNresizeWidth
resources
.
Arg[] statrowArgs = { new Arg(XmNames.XmNorientation, (XtArgVal)XmOrientation.XmHORIZONTAL),
new Arg(XmNames.XmNpacking, (XtArgVal)XmPacking.XmPACK_NONE),
new Arg(XmNames.XmNresizeHeight,(XtArgVal)0),
new Arg(XmNames.XmNresizeWidth, (XtArgVal)0) };
IntPtr _statusRow = Xmlib.XmCreateRowColumn (_msgBar, StatusRowWidgetName, statrowArgs,
(XCardinal)4);
Xtlib.XtManageChild(_statusRow);
Now the XmLabel
widgets for icon and status message can force resize using the XmNrecomputeSize
resource.
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);
To update the status message text, the globally accessible static method Log()
of the static class Logger
can be used.
Create a menu and handle the callbacks
There are two general ways to create a menu bar and it's (cascade) pulldown menus.
The first one provides full control over all menu creation steps like menu bar and cascade button creation, menu shell and menu button or menu separator creation as well as callback registration. It uses XmCreateMenuBar()
to create an (empty) menu bar, XmCreatePullbownMenu()
to create a pulldown menu shell, XmCreateCascadeButtonGadget()
to create a menu bar item or pulldown menu item and to connect it with a pulldown menu shell, XtCreateManagedWidget()
to create menu items like a PushButtonGadget or a SeparatorGadget and XtAddCallback()
to register callbacks. This technique is adequate to write menu construction helper functions.
The second one provides high convenience and uses XmCreateSimpleMenuBar()
to create a menu bar including it's cascade buttons or push buttons and XmCreateSimplePulldownMenu()
to create a pulldown menu shell including it's cascade buttons, push buttons or separators. This technique is adequate to write very compact code.
The next code sample illustrates the creation of a menu bar. Since XmCreateSimpleMenuBar()
creats all XmNbuttonCount
four XmNbuttons
and assigns the XmNsimpleCallback
to them, all menu buttons share the same callback function and the callback function must already be prepared before the menu bar widget creation. This is where fakeWidgetID
comes into play. After menu bar widget creation the callback function will be reassigned from fakeWidgetID
to _menuBar
.
It is possible to assign XmNbuttonMnemonics
to invoke the menu's cascade buttons via keyboard using [Alt]+[<mnemonic>] key combination. The mnemonics are case sensitive. The menuBtnNameList
and menuMnemonicList
parameter must have the same length, that ist identical to XmNbuttonCount
. The next code sample illustrates the hight concenience menu bar creation.
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),
new Arg(XmNames.XmNadjustLast, (XtArgVal)0) };
_menuBar = Xmlib.XmCreateSimpleMenuBar (_mainWindow, MenuBarWidgetName, menuBarArgs, (XCardinal)6);
CallBackMarshaler.Reassign (fakeWidgetID, _menuBar);
for (int countMenuBtnNames = 0; countMenuBtnNames < menuBtnNameList.Length; countMenuBtnNames)
{
Xmlib.XmStringFree (menuBtnNameList[countMenuBtnNames]);
}
Xtlib.XtManageChild(_menuBar);
Although all menu bar cascade buttons share the same callback, the menu entries can be distinguished by clientData
callback parameter, that contains the menu entry index. The next code sample illustrates such a callback, shared by all menu bar items.
public void MenuBarCB (IntPtr widget, IntPtr clientData, IntPtr callData)
{
int menuItemIndex = (int) clientData;
switch (menuItemIndex)
{
case 0:
break;
case 1:
XmExtendedMessageDialog dlg = new XmExtendedMessageDialog (_windowShell,
ExtendedMessageDialogName, "Enter string", "Enter data string:");
dlg.Start ();
break;
case 2:
break;
case 3:
WmDeleteCB (widget, clientData, callData);
break;
}
}
An alternative to the menu bar's automatical callback function assignment via
XmNsimpleCallback
is to assign the callback function manually for each menu bar's child separately after menu bar widget creation. Although this would prevent juggling with
fakeWidgetID
, the code will be less compact and elegent.
for (TInt countChildren = Xtlib.XtCountChildren (_menuBar) - 1; countChildren >= 0; countChildren--)
{
IntPtr child = Xtlib.XtGetChild (_menuBar, countChildren);
switch ((int)countChildren)
{
default:
Xtlib.XtAddCallback (child, XmNames.XmNactivateCallback,
CallBackMarshaler.Add (child, this.MenuBarCB), (IntPtr)((int)countChildren));
break;
}
}
The next code sample illustrates the creation of a pulldown menu. Since XmCreateSimplePulldownMenu()
creats all XmNbuttonCount
five XmNbuttons
and assigns the XmNsimpleCallback
to them, all push buttons share the same callback function and the callback function must already be prepared before the pulldown menu widget creation. This is again where fakeWidgetID
comes into play. After pulldown menu widget creation the callback function will be reassigned from fakeWidgetID
to _fileMenu
.
The XmNpostFromButton
defines the menu bar's cascade button index, this pulldown menu has to be associated to. The XmNbuttonAccelerators
registers the accelrotor shortcuts to the pulldown menu push buttons and the XmNbuttonAcceleratorText
defines the display of accellerator shortcuts to the pulldown menu push buttons.
As for a menu bar it is also possible for a pulldown menu to assign XmNbuttonMnemonics
to invoke the menu's cascade buttons via keyboard using [<mnemonic>] key (now without the combination of [Alt] key). The mnemonics are case sensitive. The fileBtnNameList
, fileBtnAccelList
, fileMnemonicList
and fileBtnTypeList
parameter must have the same length, that ist identical to XmNbuttonCount
.
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);
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);
CallBackMarshaler.Reassign (fakeWidgetID, _fileMenu);
for (int countButtons = 0; countButtons < 5; countButtons++)
{
Xmlib.XmStringFree (fileBtnNameList[countButtons]);
Marshal.FreeHGlobal (fileBtnAccelList[countButtons]);
Xmlib.XmStringFree (fileBtnAccelTexts[countButtons]);
}
Again all pulldown menu push buttons share the same callback and the menu entries can be distinguished by
clientData
callback parameter, that contains the menu entry index.
private void FileMenuCB (IntPtr widget, IntPtr clientData, IntPtr callData)
{
int menuItemIndex = (int) clientData;
switch (menuItemIndex)
{
case 0:
XmFileSelectionDialog dlg = new XmFileSelectionDialog (_windowShell, FileSelectionWidgetName,
"Open file ...");
dlg.Start ();
break;
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;
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;
case 3: WmDeleteCB (widget, clientData, callData);
break;
}
}
Modify standard Motif dialogs
Two sample modifications to a Motif standard MessageDialog should be shown next, both exemplary implemented in the XmExtendedMessageDialog
class.
- The display of a custom color symbol with transparency instead of the default symbol.
- The integration of an additional TextField.
The first thing to do, if unmanaged resources are involved in the madifications, is to provide a way to displose those resources. The best point in time doing this is the dialog's destruction. There are two ways to hook up the dialog's destruction, that both end up in the same behaviour and usage of the same signals.
Either XmAddProtocolCallback()
can be used to register a signal handler for WM_DELETE_WINDOW
at WM_PROTOCOLS
. This takes about 5 lines of code and is demonstrated in the XmWindows.Run()
method of the sample application, already discussed in chapter Manage the WM_DELETE_WINDOW signal.
Or the dialog's
XmNdeleteResponse
attribute is changed from
XmUNMAP
(which is the default behaviour) to
XmDESTROY
and a
XmNdestroyCallback
is registered. Because the dialog's attributes are set anyway and the callback method is already prepared, only two additional lines of code are required.
Arg[] messageDlgArgs = {
...
new Arg(XmNames.XmNdeleteResponse, (XtArgVal)DeleteResponse.XmDESTROY) };
...
Xtlib.XtAddCallback (_dialogWidget, XmNames.XmNdestroyCallback, callbackUnmanagedPointer, (IntPtr)99);
It is always a good approach to use one callback method for all dialog messages and to distinguish the action to take via the clientData or callData callback parameter. The dialog's callback method can be re-used and also control the displosal of unmanaged resources.
A custom color symbol with transparency
There are four steps necessary to provide a standard Motif dialog with a custom color symbol including transparency.
- Determination of the background color.
- Creation of the transparency mask pixmap.
- Creation of the symbol pixmap with background color pixel at transparent image pixel.
- Assignment of the symbol pixmap to the dialog.
TPixel backgroundColorPixel = Xtlib.XtGetValueOfPixel (_dialogWidget, XmNames.XmNbackground);
_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);
}
}
To determine the background color pixel, the XtGetValueOfPixel()
convenience method can be used. It hides the complicated color pixel value determination, that requires marhaling of a reference to a TPixel
variable and the fact, that TPixel
must be recognized with ReadInt32()
for 32 bit or ReadInt64()
for 64 bit OS.
The most challenging step is the creation of the symbol pixmap with background color pixel at transparent image pixel. To achive this, the X11Graphic
class, already known from the first article Programming Xlib with Mono Develop - Part 1: Low-level (proof of concept), has been extended by the new method CreateIndependentPixmap()
.
This method requires the pixmap handle maskPixmap
(as parameter) and returns a pixmap handle. A pixmap handle referes to a server side pixmap resource.
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);
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;
IntPtr pixmapGc = X11lib.XCreateGC (display, window, (TUlong)0, IntPtr.Zero);
if (pixmapGc != IntPtr.Zero)
{
if (maskPixmap != IntPtr.Zero)
{
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);
X11lib.XPutImage (display, pixmap, graphicGc, _graphicXImage, (TInt)0, (TInt)0,
(TInt)0, (TInt)0, (TUint)_width, (TUint)_height);
X11lib.XSetClipMask (display, graphicGc, IntPtr.Zero);
X11lib.XSetClipOrigin (display, graphicGc, (TInt)0, (TInt)0);
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);
X11lib.XFreeGC (display, pixmapGc);
pixmapGc = IntPtr.Zero;
}
}
else
{
Console.WriteLine (CLASS_NAME + "::CreateIndependentGraphicPixmap () ERROR: Can not create " +
"graphics context for graphic pixmap.");
}
return pixmap;
}
At the end of the dialog's lifetime the unmanaged resources have to be disposed. The XmExtendedMessageDialog
class method Dispose()
implements the disposal of both unmanaged image resources, the mask pixmap and the transparent image pixmap.
public override void Dispose ()
{
Console.WriteLine (CLASS_NAME + "::Dispose ()");
this.DisposeByParent ();
}
public override void DisposeByParent ()
{
UnregisterCallbacks ();
if (_display != IntPtr.Zero)
{ if (_iconPixMap != IntPtr.Zero)
{
X11lib.XFreePixmap (_display, _iconPixMap);
_iconPixMap = IntPtr.Zero;
}
if (_maskPixmap != IntPtr.Zero)
{
X11lib.XFreePixmap (_display, _maskPixmap);
_maskPixmap = IntPtr.Zero;
}
}
base.DisposeByParent ();
}
The Dispose()
method will be called from the one and only callback method for all dialog messages DialogCB()
, that is shown next paragraph.
An additional TextField
Adding a TextField to a Motif standard dialog is a fairly simple task.
IntPtr textField = Xmlib.XmCreateTextField (_dialogWidget, TextFieldWidgetName, Arg.Zero, 0);
Xtlib.XtManageChild (textField);
TheTextField's text, entered by the user, shuld be readout after dialog confirmation. The best place to do this is the DialogCB()
callback. It is called for any button event (help button is unmanaged) as well as for the destroy event (all callback registrations are done in the constructor). The next code sample shows all registrations for DialogCB()
callback.
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);
Xtlib.XtAddCallback (_dialogWidget, XmNames.XmNdestroyCallback,
callbackUnmanagedPointer, (IntPtr)99);
}
Since the text will be determined by the
XtGetValues()
method and be returned as a reference to a
char*
variable - as well as the background color pixel above has been returned as a reference to a
TPixel
variable - here a marshaling of the variable reference is necessary too. Because the TextField's text isn't a Motif compound string,
ReadIntPtr()
can be used to return the text.
private void DialogCB (IntPtr widget, IntPtr clientData, IntPtr callData)
{
XmAnyCallbackStruct cbStruct = (callData != IntPtr.Zero ? (XmAnyCallbackStruct)
Marshal.PtrToStructure (callData, typeof (XmAnyCallbackStruct)) :
new XmAnyCallbackStruct ());
Xtlib.XtUnmanageChild (_dialogWidget);
DisposeResources ();
if (cbStruct.reason == (TInt)XmCallbackReason.XmCR_OK) {
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) {
Console.WriteLine (CLASS_NAME + "::DialogCB() Cancel");
Xtlib.XtDestroyWidget (_dialogWidget);
}
if ((int) clientData == 99)
{
Dispose ();
}
}
The DialogCB()
is the only callback method of the whole dialog. It uses the callData
parameter, converted to XmAnyCallbackStruct
via PtrToStructure()
, to distinguish the action to take. This is the preferred way to handle dialog messages.
The alternative way to distinguish the action to take, using the clientData
parameter, is shown as well but deactivated.
After button event processing DialogCB()
calls XtDestroyWidget()
that forces the dialog to destroy, which, in turn, calls DialogCB()
to dispose all unmanaged resources.
Design a new custom dialog from scratch
Custom dialogs based on XmRowColumn
The easiest way to build up a new - very simple structured - custom dialog from scratch is using XmRowColumn
widget as root manager widget and also as action area widget to layout the children of a XmDialogShell
widget. There is nothing wrong with that but it should be mentioned, that XmRowColumn
widget's ease of use might break compatibility to general Motif dialog handling - especially to the keyborad shortcuts.
All built-in Motif dialogs use a XmBulletinBoard
, that has several default translations (see Transltns.c file for details on _XmBulletinB_defaultTranslations) - including translations for activate key (apply the dialog), cancel key (escape the dialog) and help key (call help) - as well as the XmNdefaultButton
and XmNcancelButton
resources to handle <Key>osfActivate
, <Key>osfCancel
and <Key>osfHelp
keyborad shortcuts. The XmRowColumn
widget has neither default translations nor resources to handle keyborad shortcuts.
To achive the same user experience for a new custom dialog based on XmRowColumn
as of XmBulletinBoard
, some things are important:
- Extend the manager widget's translations. If multiple manager widgets are used, it might be necessary to extend more than one manager widget's translations.
- Use gadgets instead of widgets, at least for the (OK, Cancel, ...) buttons, otherwise the manager widget's translations are unnoticated.
- Avoid
XmText
and XmTextBox
widgets or provide them with callbacks or translations.
The XmHandMadeDialogOnRowColumn
class implements a new custom dialog based on XmRowColumn
.
1. It is always a good approach to use "osf" keysyms to extend the manager widget's translations. Due to the implementation specifics of the ActionMarshaler
class, only one action procedure can be registered per event type. That's why all translations call DlgKeyPressAction()
. To distinguish the different key press events for activate, cancel and help, an action procedure parameter is passed.
public const string HandMadeDlgTranslations = "#override\n:<Key>osfActivate: DlgKeyPressAction(0)" +
"\n:<Key>osfCancel: DlgKeyPressAction(1)" +
"\n:<Key>osfHelp: DlgKeyPressAction(2)";
The action registration must be done prior the translation registration. Once the action registration is done, the actions can be used in multiple translation tables.
XtActionsRec[] actRec = { new XtActionsRec (X11.X11Utils.StringToSByteArray
("DlgKeyPressAction\0"), ActionMarshaler.ActionProc) };
Xmlib.XtAppAddActions (XmWindow.Instance.AppContext, actRec, (XCardinal)1);
The parsed translations shouldn't be applied by
XtSetValues()
(this would completely replace already existing widget translations) but by
XtOverrideTranslations()
(to merge the new translations into the existing widget translations).
ActionMarshaler.Add (_actionareaWidget, XEventName.KeyPress, this.DlgKeyPressAction);
IntPtr mgrTransl = Xtlib.XtParseTranslationTable (HandMadeDlgTranslations);
Xtlib.XtOverrideTranslations (_actionareaWidget, mgrTransl);
2. Although button widgets (not gadgets!) generally support accelerators and accelerators must not be installed on application level, it is a hard job to enable this feature. See Motif FAQ 132 "Why can't I use accelerators on buttons not in a menu?" for details. At least if the accelerators to support must be installed in a manner that they can be called from all fucusable widgets/gadgets of the dialog, this approach runs into trouble.
3. Text widgets in a dialog make translation handling complicated. Dependent on XmNeditMode
resource a text widget can be XmSINGLE_LINE_EDIT
or XmMULTI_LINE_EDIT
. For multi line text widgets the activate key is required to create a new line. Only for single line text widgets the activate key can be used to close the dialog.
Either activate key (for single line text widgets) and help key can be supported using the predefined resources XmNactivateCallback
and XmNhelpCallback
, but there is no adequate resource to support the cancel key.
Or, because suitable actions for the manager widget(s) are already registered on application level, it is possible to re-use the actions on text widgets. Typically the text widget translations are a subset of the manager widget translations (the activate key migth be omitted for multi line text widgets).
public const string TextWidgetTranslations = "#override\n:<Key>osfActivate: DlgKeyPressAction(0)" +
"\n:<Key>osfCancel: DlgKeyPressAction(1)" +
"\n:<Key>osfHelp: DlgKeyPressAction(2)";
...
ActionMarshaler.Add (textField, XEventName.KeyPress, this.DlgKeyPressAction);
IntPtr textTransl = Xtlib.XtParseTranslationTable (TextWidgetTranslations);
Xtlib.XtOverrideTranslations (textField, textTransl);
Estimation
Custom dialogs based on XmRowColumn
are easy to create and can be designed fully compatible to general Motif dialog handling. The biggest handicap is the almost static layout - a dialog resize can completely confuse the layout.
To prevent a dialog resize by switching off the window decoration / window functions is doubtful, because not all window managers support all MWM flags of VendorShell
's XmNmwmFunctions
and XmNmwmDecorations
resources.
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);
Alernatively the size of the root manager widget can be fixed.
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);
Custom dialogs based on XmPanedWindow
Almost all literature shows how to build up a new custom dialog from scratch using
XmPanedWindow
widget as root manager widget and
XmForm
as action area widget to layout the children of a
XmDialogShell
widget. Since
XmForm
widget is inherited from
XmBulletinBoard
widget, it is much easier to achive compatibility to general Motif dialog handling than with
XmRowColumn
widget.
The XmHandMadeDialogOnPanedWindow
class implements a new custom dialog based on XmPanedWindow
as root manager widget and XmForm
as action area widget.
Using the XmForm
widget is very simple. This manager widget spans a grid if equally sized cells to position the widgets. The "OK" and "Cancel" buttons of the action area shall align left and right to the dialog borders.
OK (Position 1) | (Position 2) | (Position 3) | Cancel (Position 4) |
Widgets can span multiple grid positions as well. Complex layouts can (almost only) be achieved by an extensive use of cell spans.
(Position 1) OK (Position 2) | (Position 3) | (Position 4) Cancel (Position 5) |
The number of grid cells can be set via XmNfractionBase
resource.
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);
The constraints of the form widget's children support attachements to the form widget boundary, neighboring widgets and grid positions.
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);
If form widget's XmNfractionBase
resource and form child widget's XmN*Position
resources don't fit together, typically the last child widget balances the misfit.
To achive the typical keyborad shortcut behaviour, XmNdefaultButton
and XmNcancelButton
resources of XmBulletinBoard
widget and XmNhelpCallback
resource of XmManager
widget can be used.
Estimation
Custom dialogs based on XmPanedWindow
and XmForm
are easy to create and can be designed almost compatible to general Motif dialog handling. Resizing is expectation-conformal. The only drawback is the "invisible" tab stop on XmPanedWindow
's pane
during kyboart traversal.
Custom dialogs based on XmBulletinBoard
Because XmBulletinBoard
widget has no built-in geometry management by defaul, it is a very hard job to create a custom dialog from scratch using XmBulletinBoard
widget as root manager widget. Usually it's more efficient to use XmBulletinBoard
's subclass XmForm
.
The particular challenge using XmForm
will be to define the best attachements for all widgets. The XmHandMadeDialogOnBulletinBoard
class implements a simple XmBulletinBoard
widget based dialog, that exactly behaves like a Motif MessageBox dialog (resizing, accelerators, ...). For more complex dialog layouts it might be unavoidable to use more manager widgets than only one root manager widget. In this case it can happen, that acellerators must be assigned multiple.
Points of Interest
Callbacks and disposal of unmanaged resources
The callback registration for menu bar and pulldown menu must use a trick (fakeWidgetID
) to continue with the already introduced convenience class CallBackMarshaler
.
All shells (main window, dialogs) inherit from XtShell
and thus standardizes the disposal of unmanaged resources, callback registration and callback deregistration through Dispose()
, DisposeByParent()
, RegisterCallbacks()
and UnregisterCallbacks()
class methods. All shells have to change their delete response behaviour from XmUNMAP
to XmDESTROY
to ensure the correct WM_DESROY
message processing.
General functionality
This sample application also demonstrates the utilization of a Motif
compliant third party widget. There are several commercial, open source or community
projects, that provide Motif
compliant third party widgets, e. g. The Xg widget set, XmHTML or XRT as well as collections of available widgets like Widget.FAQ, motifdeveloper, ftp widget project collection or ICS MotifZone.
Although most of them are no longer maintained, they might help to complete a Motif application.
History
-
18. September 2013, This is the third article about native calls from C# and
Mono Develop to X11 API. It deals with the Xm/Motif widgets primarily. The first article was about the Xlib only. The second article was about the Xlib only. Further articles
are planned, that dive deeper into Xlib, Xt/Athena or Xm/Motif.