GUI Tree structure gui representations

Started by Max9403, December 03, 2015, 18:57:36

Previous topic - Next topic

Max9403

I'm stuck at a cross roads on how to implement my GUI tree, I have a map in which I store a name and a GUI element representation, now when I construct a complete GUI I would query from the map and store that in the created GUI tree however I'm unsure in what way I should store the GUI element representation (ether a class reference or an instance) ... I don't think I'm explaining it well.

The map structure is so that I know what GUI element <Panel> is mean to be, it's meant to allow for building the GUI up dynamically. It's essentially stores what I need to know to be able to convert the XML into a GUI.

When I launch my game it registers GUI elements in a map, this would ether be a class to instantiate later using reflection or an interface which defines a draw method.
GuiHandler.register("Panel", Panel.class);

or
GuiElement element = new GuiElement() {
    public void draw(Render render, HashMap properties) {}
};
GuiHandler.register("Panel", element);


Then when the GUI is constructed (from an XML file) it would grab the details, e.g.
GuiTree result = new GuiTree();
Node example = result;
String[] elements = new String[]{"Panel", "Example", "Label"}; // This is just for an example this would be gathered using Java's StAX
for (String element : elements) { // Replace with StAX parsing XML
    Node temp = (Node)GuiHandler.get(element).getConstructor(HashMap.class).newInstance(new HashMap());
    example.addChild(temp);
    example = temp;
}
return result;

or
GuiTree result = new GuiTree();
Node example = result;
String[] elements = new String[]{"Panel", "Example", "Label"}; // This is just for an example this would be gathered using Java's StAX
for (String element : elements) { // Replace with StAX parsing XML
    Node temp = new Node(GuiHandler.get(element), new HashMap());
    example.addChild(temp);
    example = temp;
}
return result;


And when rendering it would be like this (properties contain different items, from scripts for onClick to the width and height):
getNode().draw(render);

public void draw(Render render)

or
getNode().getElement().draw(render, getNode().getProperties());

public void draw(Render render, HashMap properties)


I am tempted to go with the first method but I'm unsure if there is a better way of constructing GUIs like this?

abcdef

Maybe you need to explain the rationale more, why do you not just build the gui directly when you read the xml. Why have a stored map structure at all?

Max9403

ahh the map structure is so that I know what GUI element <Panel> is mean to be, it's meant to allow for building the GUI up dynamically. It's essentially stores what I need to know to be able to convert the XML into a GUI.

abcdef

But why do you need a map structure? For example, if your xml says you need a panel when why not instantiate a Panel object. If your panel contains 2 buttons, why not add those 2 buttons as children to the panel. The panels draw method can then draw it self and then call the draw method of its children.

The map structure just adds all kinds of complication in my mind  :)

Max9403

The map is that I can figure out what a "Panel" is, the map is not there for rendering - the tree is, the map is there so I know what to put in the tree. It is mainly there so that who ever is using it doesn't have to type <com.some.package.Panel> but instead can type <Panel>, if you are asking why I'm passing a new HashMap into the GUI Element, it stores additional values e.g. width and height

<Panel width="40" height="20">


Actually I have idea, perhaps requiring a "createNewInstance(HashMap properties)" method (instead of using reflection), which would return a new instance of the GUI Element, though that would require there to be a factory class :/

Kai

You might just want to use a XML/Java-POJO serialization/mapping framework, such as XStream.
No need to hand-parse the XML with DOM/SAX/StAX or anything.
With XStream you can also alias your element/class names, like "Panel" instead of "com.some.package.Panel", as well as specify which members should map to XML attributes and which to XML elements.
Look at its documentation/tutorial: http://x-stream.github.io/tutorial.html
XStream even supports non-tree-like graph structures via XPath references.

But generally: don't think in terms of XML representations or the mechanics of reading/writing your objects/properties to/from XML. Think in terms of abstract data models with its elements and their associations.
You could just as well use JSON or Google's Protocol Buffers for serialization instead of XML. The serialization format is irrelevant.
What matter is the elements and associations of your data model.

If you have that down, it is just a matter of googling around and finding suitable frameworks that will help you with the serialization mechanics.

Max9403

I looked at XStream and it looks more complicated than I need, and I'm not sure how it deals with attributes/elements not present in the java class (the reason I'm using a HashMap). Well the use of XML in this case is indeed irrelevant as I could just as well be building aliases for JSON or Google's Protocol Buffers, I'm just using XML for it. Though that does help resolve my conflicting ideas on how I want the aliases to be registered (the XStream part, not so much the the mentality thing - I considered multiple syntaxes/languages for it).

Perhaps I should have said html/android/fxml like XML rather than just XML (I do have my own styling language as well, similar to basic CSS)

And I have to be honest creating an automated parser for this isn't that hard, I'll post the code for how I'm parsing later if you're interested
Here you go (I don't think the code highlighter likes the angle brackets in my code, if you want it correctly you might need to copy it from a quote when writing a reply):
/* 
 * This program is free software. It comes without any warranty, to
 * the extent permitted by applicable law. You can redistribute it
 * and/or modify it under the terms of the Do What The f*ck You Want
 * To Public License, Version 2, as published by Sam Hocevar. See
 * http://www.wtfpl.net/ for more details.
 */
package com.bjd.main.util.ui;

import com.bjd.main.util.Node;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.stream.*;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Iterator;

/**
 * @Author Benjamin Claassen
 * @Version 1.0.0
 */
public class GuiLoader {
    private static Logger logger = LoggerFactory.getLogger(GuiLoader.class);

    private static final XMLInputFactory factory = XMLInputFactory.newInstance();
    private static HashMap<String, Class> aliases = new HashMap<>();

    /**
     * Parses a valid XML file from stream
     *
     * @param stream    The Stream that contains valid XML data
     * @return  The constructed GUI
     * @throws XMLStreamException If the XML cannot be parsed
     */
    public static Panel loadGUI(InputStream stream) throws XMLStreamException {
        Panel result = new Panel();
        Panel current = result;
        XMLEventReader reader = factory.createXMLEventReader(new InputStreamReader(stream));
        while (reader.hasNext()) {
            XMLEvent event = reader.nextEvent();
            switch (event.getEventType()) {
                case XMLStreamConstants.START_ELEMENT:
                    StartElement element = event.asStartElement();
                    String name = element.getName().getLocalPart();
                    Class panel;
                    if (aliases.containsKey(name)) {
                        panel = aliases.get(name);
                    } else {
                        try {
                            panel = Class.forName(name);
                        } catch (ClassNotFoundException e) {
                            logger.error("Could not find class: {}\n{}", name, e);
                            return null;
                        }
                    }
                    if (panel == null) {
                        logger.error("Could not find class: {}");
                        return null;
                    }

                    Panel temp;

                    HashMap<String, String> properties = new HashMap<>();

                    Iterator attributes = element.getAttributes();
                    while (attributes.hasNext()) {
                        Attribute next = (Attribute) attributes.next();
                        properties.put(next.getName().getLocalPart(), next.getValue());
                    }

                    try {
                        temp = (Panel) panel.getConstructor(HashMap.class).newInstance(properties);
                    } catch (NoSuchMethodException e) {
                        try {
                            temp = (Panel) panel.newInstance();
                        } catch (InstantiationException e1) {
                            logger.error("Could not load GUI: {}\n{}", name, e);
                            return null;
                        } catch (IllegalAccessException e1) {
                            logger.error("\"Could not load GUI: {}\n{}", name, e);
                            return null;
                        }
                    } catch (IllegalAccessException e) {
                        logger.error("Could not load GUI: {}\n{}", name, e);
                        return null;
                    } catch (InstantiationException e) {
                        logger.error("Could not load GUI: {}\n{}", name, e);
                        return null;
                    } catch (InvocationTargetException e) {
                        logger.error("Could not load GUI: {}\n{}", name, e);
                        return null;
                    }
                    if (temp == null) {
                        logger.error("Could not load GUI: {}", name);
                        return null;
                    }
                    if (!current.addChild(temp)) {
                        logger.error("Could not add GUI: {}", name);
                    } else {
                        temp.setParent(current);
                        current = temp;
                    }
                    break;
                case XMLStreamConstants.END_ELEMENT:
                    Node node = current.getParent();
                    if (node == null) {
                        break;
                    }
                    if (node instanceof Panel) {
                        current = (Panel) node;
                    } else {
                        logger.error("Parent of {} is illegal", current.getClass());
                        throw new IllegalStateException("Gui Element must extend Panel: "+ current.getClass());
                    }
            }
        }

        return result;
    }


    /**
     * Sets an alias for a GUI Element type, if type is null {@link #removeAlias(String)} is called instead
     *
     * @param name  The name that is being aliased
     * @param type  The type the aliased belongs, this the type of GUI element created
     * @param <T>   To ensure only valid GUI Elements are added
     */
    public static <T extends Panel> void setAlias(String name, Class<T> type) {
        if (type == null) {
            removeAlias(name);
        } else {
            aliases.put(name, type);
        }
    }

    /**
     * Removes an alias
     *
     * @param name    The Alias to be removed
     */
    public static void removeAlias(String name) {
        aliases.remove(name);
    }
}

Kai

Benjamin,

thanks for your code. Yes, like you showed it is of course just that simple to create a parser which builds a DOM tree out of SAX events.
But then I would just use a javax.xml DOM tree from the start. It offers you the exact same value:
- arbitrary tree structures of elements
- with untyped string attributes in an associative array (i.e. HashMap)
The value of a serialization framework like XStream lies in type-safe mapping between XML and Java structures/classes.
You only add the knowledge (and statically typed with instanceof and casts I might add) that a "Panel" means a Panel.
And your subtypes of Panel currently map to no more than javax.xml.Node elements with a different name, since the name is the only thing there that needs to act as a discriminator to differentiate the types.

Maybe leave the whole parsing stage for a second and tell a bit more about how your subsequent logic layer will actually interface with that data model. If your data model is a simple DOM with untyped string attributes then you will lose yourself in a forrest of `x instanceof That` and `Integer.parseInt(x); Float.parseFloat(y)` and static casts in your logic layer.

Maybe you could also tell more about why you actually need arbitrary attributes "which are not present in the Java classes."
How would your GUI framework make use of unknown attributes, which it knows no meaning to?
Take for example the attribute "width." When your logic layer reads your data model it needs to know that "width" means the width of a GUI element and forward that value to the layout engine and the rendering.

Cheers,
Kai

Max9403

Ok, the way the "GUI tree" would be processed is (assuming there is time for the entire document to be parsed):

  • Program requests GUI X
  • Method loadGUI starts processing
  • loadGUI returns tree
  • Program gets top node
  • Program only understands Panel class with width and height
  • Program allocates texture and creates render object
  • Program calls the top nodes render method passing in the render object
  • Top node starts render
  • Top node calls child nodes' render method
  • Renders finish
  • Program renders GUI to screen

The way the "GUI tree" would be processed is (assuming there is not time for the entire document to be parsed, there DOM goes out the window):

  • Program requests GUI X
  • Method loadGUI starts processing
  • loadGUI is periodicity called till end of document (e.g. every 10 frames)
  • loadGUI returns tree
  • Program gets top node
  • Program only understands Panel class with width and height
  • Program allocates texture and creates render object
  • Program calls the top nodes render method passing in the render object
  • Top node starts render
  • Top node calls child nodes' render method
  • Renders finish
  • Program renders GUI to screen

Also about the "forest of 'x instanceof That'" I highly doubt (especially if you mean 'x instanceof SpecialPanelX'), the only reason there is an instanceof in that code is because if a subclass of Panel does not properly implement the Node getParent() method to return a panel object (and yes I have checked this does allow for subclasses to be created and used)

Also the fact that just putting all the attributes into a HashMap means that no additional lookup that needs to be done make sure that the item is indeed for that Element means less leg work and less caching (as using reflection is expensive especially without caching) also the additional attributes are there also for anything that uses the GUI structure and defines something not for the GUI its self but for some other component elsewhere. (it also reminds me of my attempts with Apache and GSON for parsing JSON, I ended up using GSON the problem with it that I had was the need to create X amount of classes just to describe the content, and since they were small (ish) and a lot I ended putting all those classes as internal classes of a JSONTypes class, of coarse this was fun at first became very tedious later on, which made me want to move back to the more dynamic system that Apache uses with getString() and co)

Kai

Dude, what help were you actually seeking with your initial post?
Of course, with a statically typed language like Java you would have a class with specific fields for each kind of GUI element. Why wouldn't you?
I was also not referring to the single instanceof in your shown code.
I was referring to the many instanceof's, casts and Integer/Float-parsings that are sure to come once you being with the logic layer (layouting/rendering) of your application.
And honestly I have to say that most of what you wrote simply makes no sense.
Parsing the XML tree every 10 frames and assuming you woudn't have enough time for such a once-and-once-only action? Why?
Quoteputting all the attributes into a HashMap means that no additional lookup that needs to be done make sure that the item is indeed for that Element means less leg work and less caching
What? Nothing in that sentence makes any sense.
Quoteas using reflection is expensive especially without caching
Who said anything about reflection being used in such an excess that it would be a performance bottleneck? Again: Parsing your serialized GUI description is a one-time action.
Quotealso the additional attributes are there also for anything that uses the GUI structure and defines something not for the GUI its self but for some other component elsewhere
Yes, sure. But you are envisioning use cases for your GUI which do not exist and which are also unlikely to exist in the future. Stick with what you need and know currently. Don't play with thoughts and if's and when's. A software that is planned based on imagined use-cases is worthless and especially overengineered.

Max9403

Trying to respond to each line individually:

What I was look for in my original question was to use this:
GuiHandler.addAlias("Panel", Panel.class);

aliases.get(name).getConstructor(HashMap.class).newInstance(new HashMap());

panel.render(render);


or

GuiHandler.addAlias("Panel", new Panel());

new Node(aliases.get(name), new HashMap());

node.getPanel().render(render, node.getProperties());


Not sure exactly what you're trying to say here, do you mean each Panel that has sub-Panels will declare that they have said Panel?

Ok, fair enough

There is a thing called abstraction

I figured we are having different trains of thought

Sure that's fine when its a simple GUI, but then I prefer not to have a game where my fps drops 1 when trying to open a chest, rather than streaming in the content of the GUI while the animation for opening the chest is playing, remember this is just for loading/creating the layout not for any additional logic. Not to mention if any GUIs are streamed from an external internet source.

Well yes it does, the lookup in question is the need to lookup field of an unknown, potentially external/3rd party "Panel", the expression "leg work" was used to mean that the need to walk through the class to find said field, and caching refers to the need to cache said field to avoid having to do it again.

Well why should I add additional loading time?

Great now give me a reason to strip away what who ever made the GUI when converting it from XML

I'm also guessing this more of a top-down approach rather than a bottom-up