With UI standing for user interface, then by definition, ML::AbstractUI is a UI-concept independent UI-API in Standard ML currently with crossplatform classical windowing system implementations for 'WindowsNT/95/98', 'Linux/X-Windows' and other Unixes. (yes, that's a long definition... :). It is all distributed under the GNU General Public Library Licence (LGPL), which (in short) allows you to build commercial applications with it, as long as you distribute all the sourcecode and copyrightnotices for ML::AbstractUI in unmodified form along with your software.
There is also a GUI builder, called The Construct, available for ML::AbstractUI on this homepage.
It is concievable that ML::AbstractUI will
be merged with "Stuart Croy's" 'VisualML' toolkit.
This document gives a little overview of the current release
and it also outlines my current ideas about how everything should have been :).
User Interfaces in ML::AbstractUI
User Interfaces in ML::AbstractUI is represented as a directed graph. This graph is the client interface for creating and manipulating user interfaces.
Such a UI-graph can be created
and manipulated interactively using The Construct. This
is actually recommended for building user interfaces, since
The Construct will help you to
avoid mixing user interface code with application logic.
The Graph API
When building user interfaces from within an ML program you can use the DGNodes structure. It is defined in HardcoreProcessing/GraphToolkit/srcSML/DirectedGraph/DGNodes.sml in the distribution. The most useful functions in this structure has also got convenient infix operators defined in the structure DHShorthand in the file HardcoreProcessing/GraphToolkit/srcSML/DirectedGraph/DGShorthand.sml. You must of course call open DGShorthand before you can use these operators.
All necessary nodes are created by using other parts of the implementation. Specifically, you must always begin constructing a user interface by creating a MasterNode. This is done with the call uiCore.createMasterNode (). You create nodes for user interface controls you need by calling Reg.createA "Window". The argument Window here is the name of the user interface control to be created. For a list of the currently available controls, you should start The Construct and look at the buttons in the 'UI Palette' window. Alternatively you might call Reg.registered () to get a string list with the names of available controls.
To create the actual user interface you must first connect the nodes correctly. All nodes created as above has a node connected to it of type Children. To form a valid user interface, the Children node of MasterNode must be connected to any Window controls that you need and the Children nodes of these windows must in turn be connected to the other user interface controls that you want in those windows.
This means that you must be able to obtain the Children node connected from another node. This can be done by a call similar to val childrenNode = DGNodes.singleConnectedToType fromNode "Children" or more conveniently using the corresponding shorthand operator val childrenNode = fromNode +: "Children".
Now, to connect a node to a Children node you can either call DGNodes.connect childrenNode toNode or use the shorthand operator childrenNode '~+' toNode. The '~+' operator returns the node given as the left operand so that you may connect multiple nodes to the same node very conveniently like this fromNode '~+' node1 '~+' node2 '~+' node3.
This should allow you to build an entire user interface (however still unconfigured).
When done the actual user interface is constructed when you call
uiCore.createGUI masterNode. Afterwards you should
just leave the application running by calling
All the code that you have written for creating the user interface, starting
with the creation of the MasterNode and until the call to uiCore.runGUI app
should be put into a function of type unit -> unit. You should pass
this function (let's call it f here) as an argument to uiCore.runApp f.
This will start Concurrent ML correctly on the X-Windows/eXene
Setting up the user interface using protocols
To produce useful user interfaces you will need to configure your graph and setup eventhandlers etc. This is done by using protocols. Each node in the graph created contains a protocol. Nodes like the Children node described above is what will be referred to as a parameter node for a UI-control (or for the MasterNode). Each UI-control has several different parameternodes, such as PosX, PosY, EditableText etc. The Children node only has a Null protocol with no methods, but most other nodes have a useful protocol. For instance the position and size nodes have a protocol for reading and writing an integer and so on. To find out which parameternodes a given UI-control supports, you should start The Construct, create the UI-control in question and look at the 'Protocol accessor' window. Alternatively you can call the DGNodes.connected function to get a list of all the nodes connected to a node.
Protocols are implemented in structures which all begins with P for protocol. Examples are PString, PInt and PIntDrawable. These structures contain the methods that constitutes the protocols. Some common protocols are defined in HardcoreProcessing/GraphToolkit/srcSML/Protocols/DGPCommon. Some more special protocols are defined under HardcoreProcessing/MLAbstractUI/srcSML/UIProtocols/.
All methods in all protocols take the protocol itself as the first argument and any additional arguments as appropriate. For example, calling the write method in the protocol PString is done like this: PString.write protocol "Some string".
To obtain the protocol for a node you will have to typecast the node into the correct protocol type. For convenience, all protocols have a structure with the name of the protocol structure prefixed with DG for directed graph. This structure has a method called p for obtaining the protocol from a node. An example of typecasting a node to it's string protocol is val stringProtocol = DGPString.p someNode.
A more compact way of writing to a string protocol in a node would be like this:
PString.write (DGPString.p node) "Some string", but it may be a
good thing to keep a typecasted protocol around if you are going to call
many methods in the same parameter node. This will most likely be the case
if you are using the drawing protocol or a PStrings protocol
for the lines in a listbox.
The single valued protocols are PString, PInt, PReal and PBool. They all have a write method and a read method of the appropriate types for reading and writing a single value. They also have the methods addChangedNotifier, removeChangedNotifier and doNotifyChanged for adding event handlers for when the value changes. The event handlers generally have type DGNodes.t -> TypeOfTheValueChanged -> unit. The addChangedNotifier returns a notifier ID that you can use later for removing your notifier by calling removeChangedNotifier. The method doNotifyChanged just calls all registered notifiers in the node with the arguments given.
Other useful protocols include the PStrings protocol used in the TextLines node on the listbox. This protocol has methods like clear, count, add, remove, insertAt and deleteAt.
Then there are the notification protocols PNotify and
PPointerButtonNotify which have the methods addNotifier,
removeNotifier and doNotify similar to the methods in the
single valued protocols. Lastly there's the PIntDrawable
which is currently an incomplete (but very useful) drawing protocol using
integer coordinates. It is currently not implemented on the
There are a few things which I will try to give a very high priority:
As for further plans, it depends on what I will need and what you
will need. I will be very happy to recieve requests for certain features
that you might need. And of course, bugreports are highly encouraged :)