Writing objects objects with control windows

To provide the possibility of user interaction, each object can have an associated control window. This can be used to display information (like the
display object) and to get information from the user (see numeric input object for example). To allow event oriented programming, objects can create a new thread in response of a user interaction. The easiest thread creating object is the button object. This object creates a thread when the user presses the button.

Control windows are only created when a Vimms project runs within the GUI version of Vimms, i.e. it is not startet on the command line with the parameter --exec. When a program is started within the GUI version, a new process is created which loads the project file and executes it. Communication with the GUI is established via pipes. As control windows are displayed by the GUI, their class functions run in a different process than the Vimms object they belong to. All communication is done by sending data blocks back and forth between the GUI and the execution process.

Another fact must be taken care of: Each Vimms object can have multiple control windows. So between the class that represents the visible GUI window and the corresponding vimms object there exists another class that runs in the GUI process and acts as communication interface between the GUI windows and the Vimms object. So the Vimms object itself doesn't take care of how many GUI windows exist and only transmits its data to the communication interface in the GUI process which then distributes the information to all real existing GUI windows.

Creating a Vimms object with control windows at the beginning is similar to creating one without control windows: Use the template files viewertemplate.cpp and viewertemplate.h, copy them to a different name (we use myobject.cpp and myobject.h as example), and rename all occurrences of "TemplObject" into your favourite object name (we use "MyObject" again). Go on like writing an object without control windows as described in Writing objects. Now create the functionality for the control windows.

To indicate that a VimmsObject handles a control window it must provide a function hasViewer that returns true and a function CreateViewObject which returns a new instance of a class derived from the class ViewObject. This class actually represents the object that is inserted into the view window panel. During execution this class is responsible for creating the GUI windows and for the communication between the GUI windows and the VimmsObject.

The template file already provides the two classes on the GUI side, one class (MyObjectView in our example) which is derived from ViewObject and one class (MyObjectViewElem in our example) which is derived from both QWidget (or another class which itself is derived from QWidget) and TViewerGui. The last class actually provides the user interface. If the GUI window should be designed with Qt Assistant, don't derive this class from QWidget but from the class created with Qt Assistent.

Codeing the viewer functionality consists of the following steps:

  1. For sending data to control windows the VimmsObject class provides the following two functions: void NotifyViewers(int msg,void* data,int len,QWidget* src = NULL); and void NotifyViewers(int msg, QByteArray byteArray, QWidget* src = NULL) Both functions actually do the same job, sending data to the control windows. They differ in the form they accept the data: The first version accepts a pointer to a memory block data and a length parameter len. This memory block is transmitted as is to the corresponding ViewObject class. The second version transmits the data of a QByteArray object which should have been filled used QDataStream. This is normally done in the following way: QByteArray ba; QDataStream ds(ba,IO_WriteOnly); ds << somevar << ... //put some data into the datastream NotifyViewers(0,ds); //send data to viewers This is normally more easy than defining a memory structure, filling their elements and using the first version. When transmitting bigger amounts of data the first version is more efficient.

    The functions NotifyVewers can be called from within updateViewers, WantData or any other user defined action. updateViewers is called by Vimms when it knows that new GUI windows were created that must be completely initialized.

    The first parameter of NotifyViewers is a message number. This can be used to tell the other side what type of data is contained in the data block or the data stream. If writing complex objects sometimes it is unnessesary to transmit all kinds of information when only a small part has changed and the other information is already known. The other existing objects use the convention that message number 0 always contains a complete set of data.

    The last parameter is a pointer to QWidget and normally is zero. This pointer is discussed later.

  2. Now we come to the other side. If data was sent with NotifyViewers, on the GUI side ViewObject's functions ObjectMessage are called. As there exist two different NotifyViewers functions, there also exist two ObjectMessage functions: void ObjectMessage(int msg,int len, void*,QWidget* guiElement) and virtual void ObjectMessage(int msg,QDataStream& ds,QWidget* guiElement) If the first version of NotifyViewers was used to send a binary data block, implement the first version of ObjectMessage, when having sent a message using QDataStream (what normally is easier) use the second version.

    The parameter guiElement points to the GUI window (MyObjectViewElem in our example). If there exist multiple GUI windows for one object, this function is called once for each existing GUI window. In this function the data of the message should be used to update the contents of the GUI windows. The following example demonstrates this:

    void MyObject::updateViewers() { QString s("Hello"); QByteArray ba; QDataStream ds; ds << s; NotifyViewers(0,ba); } void MyObjectView::ObjectMessage(int /* msg nr.*/, QDataStream& ds, QWidget* guiElem) { MyObjectViewElem* ve = (MyObjectViewElem*)guiElem; //cast guiElem to our version QString s; ds >> s; ve->doSomething(s); } In the example the string "Hello" is sent to all GUI windows.

    Sometimes it is useful to store the received data common for all GUI windows in the class derived from ViewObject. In this case overwrite the functions

    bool ObjectMessage(int msg, int len, void* data) or virtual bool ObjectMessage(int,QDataStream&) {return false;}; These functions are only called once for each message. If the return value of these functions are true, the version with the guiElem parameter is not called afterwards. When storing data in the child class of ViewObject GUI windows can access the data when using the function getOwner() which return the ViewObject instance they belong to.
  3. Now we implement the functions that transmit data in the opposite direction. The GUI windows' parent class TViewerGui provides two functions that transmit data to their VimmsObject: void SendObjectData(int msg,QByteArray& data); andvoid SendObjectData(int msg,int len,void* data); Here we have again the two versions using a memory block or a QDataStream data transfer.
  4. Implement the function that receives data from the GUI window. We here also have the two versions: virtual TCallerBase* ViewerMessage(QWidget* source,int msg ,void* data,int len); and TCallerBase* ViewerMessage(QWidget* source,int msg,QDataStream& ds) The first parameter is the GUI window which created the message. This can be used in conjunction with the parameter src in the function NotifyViewers. As there can exist multiple GUI windows it is sometimes necessary to update all other GUI windows when one of the GUI windows reports a change. To prevent updating the GUI window that has reported the data change, the src parameter of NotifyViewer can be set to the value that has been recieved are parameter source in ViewerMessage. The return value specifies whether to create a thread as response to the ViewerMessage or not. If the return value is NULL, no thread is created. Otherwise the return value must be a pointer to the action that should be called after a new thread has been created. Passing data to the newly created thread can only be done via class variables.

    To create a thread and call the standard action after a message has been received use the following return statement:

    return (TCallerBase*)Actions[0];


Vimms user manual
F. Hitzel, 2003