In this article we will learn something about GUI and in particular about TreeView in a real life example.
TreeView is one of most powerful GUI element we can use in Cinema4D sdk, but is also a little complicate a the beginning. But no worry we will see step by step how build and progressively improve it!
This is only the first part of tutorial, as the topic is very large and can have different level of complexity, so you will find some advanced article on the same topic and example in future.
Our Project
In our example we will build a new little manager that contain a TreeView that display all Materials and its Shader in the scene.
From this simple starting point we will add some nice hierarchy stuff, in future parts we will learn about customizing tree with bitmaps, Drag&Drop and selections.
The dialog
First of all we have to create a Dialog that is the window where we will attach the TreeView to.
All manager in Cinema4D like Object Manager, Layer Manager or Render Setting Window are Dialog and are class derived from GeDialog base class.
Create a new dialog is very simple so let we start to declare our class:
#include "c4d.h" #define ID_SHADER_BROWSER 1029405 // plugin id from Plugincafe //---------------------------------------------------------------------------------------- ///Dialog class //---------------------------------------------------------------------------------------- class ShaderBrowser : public GeDialog { public: ShaderBrowser() { } ~ShaderBrowser() { } // define dialog with some gadget build menus, title and so on virtual Bool CreateLayout(void) { // call class parent function Bool res = GeDialog::CreateLayout(); // set dialog title SetTitle("Shader Browser"); return res; } private: };
We are already ready to display the empty dialog, but as you can see we have not registered any plugin yet , so what we have to bear in mind when we work with dialog is that dialog can’t be registered or opened by itself.
Basically what is needed is a call by another plugin, In most of case we have to create a simple command Plugin (CommandData) that will open Dialog for us, the same operation can be done in a lot of mode like by double click on material open Material Editor and so on.
Anyhow here our simple Command PlugIn:
//---------------------------------------------------------------------------------------- ///Command Plugin to open the dialog //---------------------------------------------------------------------------------------- class ShaderBrowserCommand : public CommandData { public: virtual Bool Execute(BaseDocument *doc) { // opend dialog with 300x500 pixel dimension // DLG_TYPE_ASYNC mean dialog is not modal return dlg.Open(DLG_TYPE_ASYNC,ID_SHADER_BROWSER,-1,-1,300,500); } virtual LONG GetState(BaseDocument *doc) { return CMD_ENABLED; } virtual Bool RestoreLayout(void *secret) { // this allow cinema4d to restore a not modal dialog position/size and content if is closed or not visible return dlg.RestoreLayout(ID_SHADER_BROWSER,0,secret); } private: ShaderBrowser dlg; //shader browser dialog }; Bool RegisterShaderBrowser(void) { return RegisterCommandPlugin(ID_SHADER_BROWSER, "Shader Browser", 0 , NULL, "Open Shader Browser Manager", gNew ShaderBrowserCommand); }
at this time if you run your plugin you can see the dialog by choosing the command in plugin menu.
Then now we have the empty window and we can start to fill it with our Tree.
Prepare a simple TreeView
To create a nice tree ui we need the following 2 steps:
- Subclass TreeViewFunctions class that reppresent the behaviour of the tree in conjunction with the data.
- Attach to dialog a TreeViewCustomGui that represent the GUI elelement itself and connect TreeViewFunction to it.
Let we start with first step , so subclass TreeViewFunctions class and start to implement basic methods.
As we see before TreeViewFunctions define how the data should handled by the tree, but this have nothing to do with the GUI itself, basically we have to build the data model logic and then pass it to the GUI to display it with defined rules.
Bear in mind there are some “Mandatory” function to override in TreeViewFunctions otherwise we will fall in a pure virtual class error during compiling.
First important concept is understand wich will be the “Root” of our tree; the root can be any element that own child, in our example we will start to display materials and then shader so root will be the document that is the upper parent of it, if we would like to display tags of an object root will be the object itself and so on.
In this example for simplicity we will use an existing data set (BaseDocument, BaseMaterial, BaseShader….), in most of case you would like to implement your own data model and add it to Cinema4d Document but this involve some “advanced” tips like using SceneHook, NodeData and so on and maybe all this stuff can can be seen in another article.
You will see that most of functions use the same parameters or so, most commons are:
- void *root : root of data set;
- void *obj : the current element;
- void *userdata : free data so we can use it for our own specific needs, and we will see how use it later.
First of all we have to implement 3 method that define a basic hierarchy: GetFirst(), GetDown(), GetNext():
GetFirst(void *root,void *userdata) define how to find the first elelement in data set by using the root object, so simply cast root to correct type and get first element like this:
virtual void* GetFirst(void *root,void *userdata) { if (!root) return NULL; BaseDocument *doc = (BaseDocument*)root; return doc->GetFirstMaterial(); }
GetNext(void *root,void *userdata,void *obj) define how to find the next elelement to the passed obj:
virtual void* GetNext(void *root,void *userdata,void *obj) { if (!root || !obj) return NULL; BaseMaterial *mat = (BaseMaterial*)obj; return mat->GetNext(); }
GetDown(void *root,void *userdata,void *obj) define how to find the first chid of the current obj, just to start we will done a single level tree so simply return NULL for the moment:
virtual void* GetDown(void *root,void *userdata,void *obj) { return NULL; }
So After we defined the basic hierarchy we can add selection and opening functions:
IsSelected(void *root,void *userdata,void *obj) define if the current objet is selected or not;
IsOpened(void *root,void *userdata,void *obj) define if Childs of the current object should be visible or not (tree folding).
To start both will return FALSE so all element unselected and all folded:
virtual Bool IsSelected(void *root,void *userdata,void *obj) { return FALSE; } virtual Bool IsOpened(void *root,void *userdata,void *obj) { return FALSE; }
By default TreeView draw in the tree column tree graph and element name so we need a function to define the name to display.
GetName(void *root,void *userdata,void *obj) define the name to display for the current obj
virtual String GetName(void *root,void *userdata,void *obj) { if (!root || !obj) return String(); String name = String(); BaseMaterial *mat = (BaseMaterial*)obj; if (mat) name = mat->GetName(); return name; }
Last 2 “mandatory function are:
GetId(void *root,void *userdata,void *obj) return id for obj in this case simply cast to VLONG obj itself
GetDragType(void *root,void *userdata,void *obj) basic definition for allowed Drag&Drop within the tree return NOTOK that mean -1 to disallow Drag&Drop
virtual VLONG GetId(void *root,void *userdata,void *obj) { return (VLONG)obj; } virtual LONG GetDragType(void *root,void *userdata,void *obj) { return NOTOK; }
At the end first ShaderTree class implementation should appear like this:
//---------------------------------------------------------------------------------------- ///TreeView Functions Table class //---------------------------------------------------------------------------------------- class ShaderTree : public TreeViewFunctions { public: virtual void* GetFirst(void *root,void *userdata) { if (!root) return NULL; BaseDocument *doc = (BaseDocument*)root; return doc->GetFirstMaterial(); } virtual void* GetDown(void *root,void *userdata,void *obj) { return NULL; } virtual void* GetNext(void *root,void *userdata,void *obj) { if (!root || !obj) return NULL; BaseMaterial *mat = (BaseMaterial*)obj; return mat->GetNext(); } virtual Bool IsSelected(void *root,void *userdata,void *obj) { return FALSE; } virtual Bool IsOpened(void *root,void *userdata,void *obj) { return FALSE; } virtual String GetName(void *root,void *userdata,void *obj) { if (!root || !obj) return String(); String name = String(); BaseMaterial *mat = (BaseMaterial*)obj; if (mat) name = mat->GetName(); return name; } virtual VLONG GetId(void *root,void *userdata,void *obj) { return (VLONG)obj; } virtual LONG GetDragType(void *root,void *userdata,void *obj) { return NOTOK; } };
Now we have a basic ShaderTree implementation before to see it in Dialog we have to attach tree to a GUI element.
In Cinema4D SDK we have TreeViewCustomGui that do the work for us and is the real GUI element, so we need to add a pointer to a TreeViewCustomGui and a ShaderTree instance as private members of dialog and then modify create layout method.
//---------------------------------------------------------------------------------------- ///Dialog class //---------------------------------------------------------------------------------------- class ShaderBrowser : public GeDialog { public: ShaderBrowser() { _shaderTreeGui = NULL; } ~ShaderBrowser() { } // define dialog with some gadget build menus, title and so on virtual Bool CreateLayout(void) { // call class parent function Bool res = GeDialog::CreateLayout(); // set dialog title SetTitle("Shader Browser"); //attach treeview so the GUI element and define its parameter via treedata BaseContainer //no parameter at the moment BaseContainer treedata; //create customGui and assign it to ID 1000 _shaderTreeGui = (TreeViewCustomGui*)AddCustomGui(1000, CUSTOMGUI_TREEVIEW, String(), BFH_SCALEFIT|BFV_SCALEFIT, 0, 0, treedata); //define trees layout if (_shaderTreeGui) { BaseContainer layout; // add column 0 with type LV_TREE so default tree with name layout.SetLong(0, LV_TREE); // set column layout to the GUI element if (!_shaderTreeGui->SetLayout(1,layout)) return FALSE; // setting up the root element and pass the ShaderTree Function table if (!_shaderTreeGui->SetRoot(GetActiveDocument(),&_shaderTree,NULL)) return FALSE; // update tree GUI _shaderTreeGui->Refresh(); } return res; } private: ShaderTree _shaderTree; TreeViewCustomGui *_shaderTreeGui; };
As you can see now we add SetRoot() and Refresh() functions in CreateLayout() that is not ideal as you will see an updated tree only if you resize or move the dialog, as CreateLayout is called only if is needed so the dialog as to be rebuild.
The best approach for this is to create a little function in dialog and use it where is needed, here is the function:
Bool UpdateTree(BaseDocument *doc) { if (!doc || !_shaderTreeGui) return FALSE; // setting up the root element if (!_shaderTreeGui->SetRoot(doc, &_shaderTree, NULL)) return FALSE; // update tree GUI _shaderTreeGui->Refresh(); return TRUE; }
best places to update tree in this case are in InitValue() and CoreMessage() dialog Methods, we will implement so our dialog and tree will react and update when cinema notify some evenets, in particular InitValue() is called when the dialog is created so is a kind of default action then in CoreMessage() we have to react to EVMSG_CHANGE id that is called by EventAdd() to notify that some nodes is updated in the document:
Bool InitValues(void) { // call parent function if (!GeDialog::InitValues()) return FALSE; // get active document BaseDocument * doc = GetActiveDocument(); if (!doc) return FALSE; // build tree and set root if (!UpdateTree(doc)) return FALSE; return TRUE; } Bool CoreMessage(LONG id, const BaseContainer& msg) { if (id == EVMSG_CHANGE) { // get active document BaseDocument * doc = GetActiveDocument(); if (!doc) return FALSE; // build tree and set root if (!UpdateTree(doc)) return FALSE; } return GeDialog::CoreMessage(id, msg); }
At the end dialog class should be like this:
//---------------------------------------------------------------------------------------- ///Dialog class //---------------------------------------------------------------------------------------- class ShaderBrowser : public GeDialog { public: ShaderBrowser() { _shaderTreeGui = NULL; } ~ShaderBrowser() { } // define dialog with some gadget build menus, title and so on virtual Bool CreateLayout(void) { // call class parent function Bool res = GeDialog::CreateLayout(); // set dialog title SetTitle("Shader Browser"); //attach treeview so the GUI element and define its parameter via treedata BaseContainer //no parameter at the moment BaseContainer treedata; //create customGui and assign it to ID 1000 _shaderTreeGui = (TreeViewCustomGui*)AddCustomGui(1000, CUSTOMGUI_TREEVIEW, String(), BFH_SCALEFIT|BFV_SCALEFIT, 0, 0, treedata); //define trees layout if (_shaderTreeGui) { BaseContainer layout; // add column 0 with type LV_TREE so default tree with name layout.SetLong(0, LV_TREE); // set column layout to the GUI element if (!_shaderTreeGui->SetLayout(1,layout)) return FALSE; } return res; } // set tree root and update it Bool UpdateTree(BaseDocument *doc) { if (!doc || !_shaderTreeGui) return FALSE; // setting up the root element if (!_shaderTreeGui->SetRoot(doc, &_shaderTree, NULL)) return FALSE; // update tree GUI _shaderTreeGui->Refresh(); return TRUE; } // initialize dialog Bool InitValues(void) { // call parent function if (!GeDialog::InitValues()) return FALSE; // get active document BaseDocument * doc = GetActiveDocument(); if (!doc) return FALSE; // build tree and set root if (!UpdateTree(doc)) return FALSE; return TRUE; } // react toglobal notifications Bool CoreMessage(LONG id, const BaseContainer& msg) { if (id == EVMSG_CHANGE) { // get active document BaseDocument * doc = GetActiveDocument(); if (!doc) return FALSE; // build tree and set root if (!UpdateTree(doc)) return FALSE; } return GeDialog::CoreMessage(id, msg); } private: ShaderTree _shaderTree; TreeViewCustomGui *_shaderTreeGui; };
Great! now we have a nice tree and it show all materials in the scene by name, and should look like this:
Add the hierarchy layer
Now we are ready to add also shader to the tree by implementing GetDown() method we can add all hierarchy layer we need by simply traverse materials and then shaders.
While shades an materials are different node type we need a more “generic” way to traverse parents and Childs so the better way to do this is to go a little in a lower level and don’t use BaseMaterial or BaseShader in ShaderTree but BaseList2D that is a common Class that both are derived from, at the end we can rewrite ShaderTree like this:
//---------------------------------------------------------------------------------------- ///TreeView Functions Table class //---------------------------------------------------------------------------------------- class ShaderTree : public TreeViewFunctions { public: virtual void* GetFirst(void *root,void *userdata) { if (!root) return NULL; BaseDocument *doc = (BaseDocument*)root; return (BaseList2D*)doc->GetFirstMaterial(); } virtual void* GetDown(void *root,void *userdata,void *obj) { if (!root || !obj) return NULL; BaseList2D *mat = (BaseList2D*)obj; // if obj is a material use BaseList2d method GetFirstShader() if (mat->IsInstanceOf(Mmaterial)) return mat->GetFirstShader(); // otherwise is a shader so simply call GetDown() return mat->GetDown(); } virtual void* GetNext(void *root,void *userdata,void *obj) { if (!root || !obj) return NULL; BaseList2D *mat = (BaseList2D*)obj; return mat->GetNext(); } virtual Bool IsSelected(void *root,void *userdata,void *obj) { return FALSE; } virtual Bool IsOpened(void *root,void *userdata,void *obj) { return TRUE; } virtual String GetName(void *root,void *userdata,void *obj) { if (!root || !obj) return String(); String name = String(); BaseList2D *mat = (BaseList2D*)obj; if (mat) name = mat->GetName(); return name; } virtual VLONG GetId(void *root,void *userdata,void *obj) { return (VLONG)obj; } virtual LONG GetDragType(void *root,void *userdata,void *obj) { return NOTOK; } };
Don’t forget to return TRUE in the IsOpen Method otherwise al nodes will be folded so tree will not display them.
Result is nice now we have a complete Materials/Shader tree, and if you try to use shader like layer you will see the correct data structure.
Last nice thing we can do is to implement SetName() Method in ShaderTree so we will be able to rename elements in the tree by doubleclick like in other manager:
void SetName(void* root, void* userdata, void* obj, const String& str) { if (!root || !obj) return; BaseList2D *mat = (BaseList2D*)obj; if (mat) mat->SetName(str); }
Now to give the final touch we can change a parameter in GUI to display alternate row color in tree, in dialog CreateLayout() you can add this change:
//attach treeview so the GUI element and define its parameter via treedata BaseContainer //no parameter at the moment BaseContainer treedata; // set true tree parameter to show alternate background color treedata.SetBool(TREEVIEW_ALTERNATE_BG,TRUE); //create customGui and assign it to ID 1000 _shaderTreeGui = (TreeViewCustomGui*)AddCustomGui(1000, CUSTOMGUI_TREEVIEW, String(), BFH_SCALEFIT|BFV_SCALEFIT, 0, 0, treedata);
so final look should be like this:
So this is all for this chapter, thanks for reading and stay tuned for next parts.
Great timing!
I just learned how to create my first tree gui this weekend. And I’ve got mine set up very similar to yours.
But the one problem I can’t figure out is how to enable the arrows to dynamically open&collapse the hierarchy branches.
I’m not seeing that in your code either.
How do we collapse & uncollapse the branches in the tree GUI?
-ScottA
Hi Scott,
this will be a topic in a next article, anyhow you have to fill IsOpen() and Open() functions in your treefunctions class. the problem would be, where to store collapsing status for each node, so i would sugest you to use BaseList2D bits for that 🙂
but this depend a lot on the kind of data structure you are showing in the tree.
I’m looking forward to the second part then.
What I’ve done in my GeDialog plugin is create a copy of the OM using the tree gui so the user can stay inside the dialog and never have to leave it when working with objects.
Here is some of the code I’m using that allows the user to select an item from the tree gui. And it will act like the user is using the OM:
virtual Bool IsSelected(void* root, void* userdata, void* obj)
{
DebugNode *node = (DebugNode*)obj;
BaseObject *op = (BaseObject*)node;
BaseDocument *doc = op->GetDocument();
if (op->GetBit(BIT_ACTIVE))
{
GePrint(op->GetName());
return TRUE;
}
else return FALSE;
}
virtual void Select(void *root,void *userdata,void *obj,LONG mode)
{
DebugNode *node = (DebugNode*)obj;
BaseObject *op = (BaseObject*)node;
BaseDocument *doc = op->GetDocument();
if (doc) doc->SetActiveObject(op,mode);
EventAdd();
}
It works nicely.
But as you can imagine. The tree branches can quickly grow out of hand with them always open.
So it would be wonderful to learn how to make them collapse and uncollapse when clicking those little arrows.
-ScottA
i would sugest the same way, use in IsOpen and Open GetBit() SetBit(), BIT_OFOLD will do the work for you
Thanks for the hints. But I just can’t get it to work.
I’m hoping the next part of the tutorial will show the code to do this.
It would also be great to see how to implement dragging and dropping the tree items too.
-ScottA
See the brand new second part 2 of Francesco’s article: Treeview made simple – Part 2 – Folding & Selections 🙂