Streaming means the reconstruction of a GUI at runtime from a descriptive file. This file can be of arbitrary format - usually this will be an XML description (for human readability) or proprietary binary file (for optimized performance). Such a streaming file contains all required information to construct the GUI from it, such as:
This information must be present within the streaming file, but the way in which it is represented is variable. Therefore, the file may have any format - and in fact it is not limited to a physical file somewhere on a data storage, but could as well be read from a streaming source such as a network connection.
Another core concept of streaming is that each streamable object (i.e. each object within the GUI) is capable of reading and writing itself from/to a stream. In other words the object itself knows which attributes it must read/write in order to completely (re)store its current state. This has the advantage that the overall complexity of the system is greatly reduced, since each object encapsulates its own streaming details within its own code.
The main components involved in streaming are therefore the following:
Component | Guiliani class | description |
---|---|---|
Streamable object | CGUIStreamableObject | Base class for all object which are capable of reading/writing themselves from/to a stream. |
Stream Reader | CGUIStreamReader | Abstract interface for reading data from a stream. This offers for example APIs for reading integers, floats, string etc. from a stream and has to be re-implemented per file format. (e.g. one StreamReader for XML, another one for Binary...) |
Stream Writer | CGUIStreamWriter | Abstract interface for writing data to a stream. This offers for example APIs for writing integers, floats, string etc. to a stream and has to be re-implemented per file format. (e.g. one StreamWriter for XML, another one for Binary...) |
Factory | CGUIFactory | This component is responsible for dynamically creating objects at runtime. Before you can tell an object to read itself from a stream, you must obviously create this object first. This is done within the Factory, which instantiates an object of a given class. The class is specified by a dedicated identifier within the streaming file. |
Streaming can be divided into two parts: writing GUI definitions into a stream and reading GUI definitions from a stream.
A stream can be anything that can be represented by an eC_File, normally one stream reader or writer is responsible for one file currently streamed.
GUI definitions are the entirety of controls (GUI objects), their layouters, behaviours and commands, usually seen as one control which can be a CGUICompositeObject whose children are then part of the GUI definition.
For writing GUI definitions, the following steps must be performed after creating a GUI. The example uses the XML writer:
The method CGUIResourceManager::WriteDialogToFile creates a file with the given name, writes its own comment tags and then calls the supplied object's CGUIObject::WriteToStream method.
For reading (sometimes called streaming) GUI definitions, the following steps must be performed. The example uses the XML reader:
The method CGUIFactoryManager::LoadDialogFromFile opens the file and starts reading it, using the factories to create objects and calling ReadFromStream on the new objects.
The same mechanism can be used for writing and reading global settings like properties (see CGUIProperties::WriteGlobalPropertiesToFile and CGUIProperties::ReadGlobalPropertiesFromFile). ID mappings for images, fonts and sounds can be written and read by calling the appropriate methods of CGUIResourceManager.
CGUIStreamReader and CGUIStreamWriter are implemented in a stacked pattern. It is possible to create multiple stream readers or writers, i.e. the last created streamer becomes the active one. Is the last created streamer destroyed when it is not needed anymore, the old streamer becomes active again (is top of the stack).
This section will show you how to add support for Guiliani's streaming feature in your own controls. Adding streaming support is always done by implementing to methods which your object automatically inherited from CGUIStreamableObject, namely ReadFromStream and WriteToStream.
As the names already imply, ReadFromStream() will include be called whenever your object should read itself from a stream, while WriteToStream() will be called in order to let your object dump its current status to a stream. The two functions are therefore generally quite similar and once you have implemented one of them, it will be straightforward to write the other one accordingly.
Since you are going to re-implement the streaming methods within your own class, you will have to declare them within your header file. Note that the methods are typically encapsulated within the dedicated GUILIANI_STREAM_GUI and GUILIANI_WRITE_GUI defines, in order to allow the respective code section to be removed from the build, if they are not required.
Within the CPP file you will have to add the implementation of the ReadFromStream and WriteToStream methods, as well as some required includes and - although not necessarily required - it is strongly recommended to also add a define for the current version of your control and set a readable XML tag for your control's name.
Required includes
The following includes will be required so that you gain access to the framework's streaming API's.
Readable XML name tag
Setting an XML tag with your control's name is optional. It will enhance readability of your control's XML output. You should place the following code in your object's default constructor (since this will be used by the streaming factory to instantiate it). Obviously GUIImage is just an example - you can freely choose a name here.
If you want your streaming code to be downward compatible towards various versions of your control, you should add a define with your control's current version. You can then check for this version and have specific code paths in your streaming methods for varying versions.
This method will be called by the framework if an instance of your class has been instantiated while parsing a streaming file. First of all the default constructor of your class is being called by the respective factory (CGUIFactory), afterwards ReadFromStream() is being called on the newly created object in order to initialize it with the values found in the streaming file. Let's go through the demo code below line by line to see how it works.
In the first line you will see a call to a DeInit method. Per definition your control must be capable to deal with repetitive calls to the ReadFromStream method. That means it should not lead to any unexpected behaviour and/or memory leaks if ReadFromStream gets called several times. So if you have to do any clean-ups (like e.g. freeing previously requested resources), you should do this here.
The next line reads the control's class version from the streaming file. Noteworthy here are three things:
The next block checks the read class version against the current implementation's version define and throws an exception if it does not match. Here you might as well implement individual code paths for varying object versions (for example a newer implementation of a control might have more parameters than an older one). Note that the exception which is being thrown here is of type CGUIStreamingException, which allows you to hand over additional information, such as the line number within the streaming file which caused the exception (retrieved by using the CGUIStreamReader::GetCurLine() method).
Do not forget to call your base class streaming implementation. Skipping this will leave all your inherited attributes un-initialized! It is important to do this before streaming of your attributes to get a consistent order when listed in the GSE Editor or within the xml files.
The three lines following that block read this object's individual attributes from the stream. You can see that there are different interfaces available for reading varying types of data, such as image IDs, boolean values or integers. You should obviously use the one best matching the variable that you wish to set. Also make sure that you are using the same tag names (e.g. StretchBlit) for both reading and writing that value.
Fianlly you may want to put other required initialization code here as well, such as requesting an ImageID which you have just read from the stream. This is done in the next line of the demo code.
The ReadFromStream and WriteToStream methods of an object are always linked very closely and therefore usually look quite similar. All the attributes which you desire to read from a stream will also need to be written beforehand.
One general difference is the very first block within the WriteToStream() method, though. This is where you are writing your object's class ID to the stream. The class ID is an enumeration value which needs to be present in your UserControlResource.h file. This value will be read by the streaming factory later on and thus allow it to instantiate an object of this type. It is mandatory to have the blocks encapsulated by the if(bWriteClassID) statements in your WriteToStream implementation. What needs to be adapted though is the actual class ID (in the shown example GUI_IMAGE) to that of your own class.
Similarly to the GETINPUTSTREAM macro, there is also a GETOUTPUTSTREAM macro which grants you write access to the current stream without having to know whether you are currently writing XML or binary output. Once again, there are also matching APIs for the diverse types of data. A difference is that you need to supply two parameters, namely the value which you wish to write, and the corresponding tag name.
Just like within the ReadFromStream method, you must first call the base class implementation of the streaming method to write inherited attributes, too. The WriteToStream method ends by writing the closing tag with the object's name. This will appear within the XML file as the XML tag that you set earlier using the SetXMLTag() interface.
When implementing streaming methods, you need to respect four simple rules: