Changes from Version 1 of documentation/tutorials/DatagridTree

Show
Ignore:
Author:
robertc (IP: 122.57.36.227)
Timestamp:
01/03/08 11:32:20 (10 years ago)
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • documentation/tutorials/DatagridTree

    v0 v1  
     1[[PageOutline(1-5, Contents)]] 
     2= Datagrid Tree Tutorial = 
     3 
     4This tutorial expects that you've got a solid grounding in C++ and know the basics of RML and RCSS, and know the basics of datagrids (ie, completed the first [wiki:documentation/tutorials/Datagrid datagrid tutorial].) 
     5 
     6== Step 1: The Plan == 
     7 
     8So we've got a pretty nice high scores chart, but we want to add some pep. One way to do that would be to show how many of each type of alien each person on the chart has destroyed. If we had this much information for everyone shown all the time there we would get info overload, so we'll develop it so the user can expand each row of the chart and see the kills that way. 
     9 
     10=== Tree structure === 
     11 
     12Each row in a datagrid has the ability to possess child rows - each of which can also have child rows, and so on. This is specified by the data source returning the name of the data source and table that the row fetches its children from - in the same way that a datagrid definition specifies the data source and table to fetch the root rows from. 
     13 
     14A row's child rows are displayed directly underneath the row itself. By default a row starts with its child rows hidden, so you'll only see the top level rows when you view a datagrid for the first time. Fortunately there are a few tools that you can use to automatically add tree functionality to your datagrid. More on these later - next, let's look at the changes we've made to the existing datagrid tutorial: 
     15 
     16=== The revised HighScores === 
     17 
     18Take a look at the HighScores class and see its changes from the first datagrid tutorial. The changes to the definition are the addition of the NUM_ALIEN_TYPES variable and the addition of the alien_kills variable to the SubmitScore function and the Score struct: 
     19 
     20{{{ 
     21const int NUM_ALIEN_TYPES = 3; 
     22 
     23... 
     24 
     25void SubmitScore(const EMP::Core::String& name, const EMP::Core::Colourb& colour, int wave, int score, int alien_kills[]); 
     26 
     27... 
     28 
     29struct Score 
     30{ 
     31        EMP::Core::String name; 
     32        EMP::Core::Colourb colour; 
     33        int score; 
     34        int wave; 
     35 
     36        int alien_kills[NUM_ALIEN_TYPES]; 
     37}; 
     38}}} 
     39 
     40The alien_kills array stores how many of each type of alien the player killed to get his score. This is what we'll be basing the new part of the chart from. The cpp file contains changes to the SubmitScore and LoadScores function to reflect the new data on the alien kills. 
     41 
     42== Step 2: Improving the Data Source == 
     43 
     44=== Specifying the child data source === 
     45 
     46So first of all we need to make the data source aware of the child rows. The best way to do this is to add more tables to the HighScores data source: one table for each row. So in the GetRow function, when we're checking what the columns are, we need to add one more check in - the check for the child data source. I added this code in after the "wave" check: 
     47 
     48{{{ 
     49else if (columns[i] == EMP::Core::DataSource::CHILD_SOURCE) 
     50{ 
     51        row.push_back(EMP::Core::String(24, "high_scores.player_%d", row_index)); 
     52} 
     53}}} 
     54 
     55This tells the calling datagrid that this row's child data source is "high_scores" (the same source that we're editing) and the table is called player_X. So now at the top of the GetRow and GetNumRows functions we now have to check for that table as well. 
     56 
     57You may be wondering where the EMP::Core::DataSource::CHILD_SOURCE variable came from! It's one of three predefined variables that can be used as column names and in the fields attribute in the col definition. The other two are DEPTH and NUM_CHILDREN. DEPTH returns the depth of the row and can be used by data formatters to draw something different for each row. NUM_CHILDREN returns the number of children under each row, and is most often used to know when to draw a '+' button to expand the row. We'll be using NUM_CHILDREN later. 
     58 
     59=== Adding the player tables === 
     60 
     61So now we need to add the extra tables into the data source. This can be done by adding an else statement in the GetRow and GetNumRows functions to catch all calls to tables that aren't "scores", then reading off the name which row it is exactly that's being queried. 
     62 
     63{{{ 
     64else 
     65{ 
     66        int player_index; 
     67        if (sscanf(table.CString(), "player_%d", &player_index) == 1) 
     68        { 
     69                // Code goes in here. 
     70        } 
     71} 
     72}}} 
     73 
     74For the GetNumRows function I wanted it only to return the number of alien types with actual kills, so if the player hasn't killed any of a certain type then we don't return that. Then is the code I ended up with: 
     75 
     76{{{ 
     77int player_index; 
     78if (sscanf(table.CString(), "player_%d", &player_index) == 1) 
     79{ 
     80        int num_alien_types = 0; 
     81        for (int i = 0; i < NUM_ALIEN_TYPES; i++) 
     82        { 
     83                if (scores[player_index].alien_kills[i] > 0) 
     84                { 
     85                        num_alien_types++; 
     86                } 
     87        } 
     88        return num_alien_types; 
     89} 
     90}}} 
     91 
     92The GetRows function is, of course, a bit more complicated. In the "name" column we want it to display the name of the alien vessel. In the "score" column we can put the score of the alien that was destroyed. Under the "colour" column we'll want to display the image of the bad guy - we can upgrade the defender decorator to handle this. And finally the "wave" column will show how many of the little blighters the player managed to frag. Here's my code: 
     93 
     94{{{ 
     95else 
     96{ 
     97        int player_index; 
     98        if (sscanf(table.CString(), "player_%d", &player_index) == 1) 
     99        { 
     100                // Translate the row_index to the actual index into the alien_kills array - as there might be gaps in the 
     101                // array we may have to skip those entries. 
     102                int alien_kills_array_index = row_index; 
     103                for (int i = 0; i < NUM_ALIEN_TYPES && i <= alien_kills_array_index; i++) 
     104                { 
     105                        if (scores[row_index].alien_kills[i] == 0) 
     106                                alien_kills_array_index++; 
     107                } 
     108 
     109                for (size_t i = 0; i < columns.size(); i++) 
     110                { 
     111                        if (columns[i] == "name") 
     112                        { 
     113                                row.push_back(ALIEN_NAMES[row_index]); 
     114                        } 
     115                        else if (columns[i] == "score") 
     116                        { 
     117                                row.push_back(EMP::Core::String(8, "%d", ALIEN_SCORES[row_index])); 
     118                        } 
     119                        else if (columns[i] == "colour") 
     120                        { 
     121                                EMP::Core::String colour_string; 
     122                                EMP::Core::TypeConverter< EMP::Core::Colourb, EMP::Core::String >::Convert(EMP::Core::Colourb(255, 255, 255), colour_string); 
     123                                row.push_back(colour_string); 
     124                        } 
     125                        else if (columns[i] == "wave") 
     126                        { 
     127                                int num_kills = scores[player_index].alien_kills[alien_kills_array_index]; 
     128                                if (num_kills == 1) 
     129                                        row.push_back(EMP::Core::String("1 kill")); 
     130                                else 
     131                                        row.push_back(EMP::Core::String(16, "%d kills", num_kills)); 
     132                        } 
     133                } 
     134        } 
     135} 
     136}}} 
     137 
     138Run this and you should see: nothing different. All the rows are there, just hidden away, and we've no way to expand the rows! 
     139 
     140== Step 3 : Adding the expand button == 
     141 
     142We need to make a new column in the datagrid, and into that column add a button if the row has any child rows. 
     143 
     144=== Adding the column === 
     145 
     146Open the tutorial.rml file in the data folder, and look at where the datagrid is defined. We have to squeeze an extra col element in there. We won't give this column a title, and it'll have a formatter to create the button we need. The only bit of information that the formatter needs is how many child rows the row has - if it has 0 children then we don't create a button, 1 or more children then we do create it. So it should look something like this: 
     147 
     148{{{ 
     149<datagrid source="high_scores.scores"> 
     150        <col fields="#num_children" formatter="expand_button" width="10%"></col> 
     151        <col fields="name" width="30%">Pilot:</col> 
     152        <col fields="colour" formatter="ship" width="20%">Ship:</col> 
     153        <col fields="wave" width="20%">Wave:</col> 
     154        <col fields="score" width="20%">Score:</col> 
     155</datagrid> 
     156}}} 
     157 
     158Of course we've no formatter called "expand_button", we'll have to create that later. The "#num_children" field corresponds to the EMP::Core::DataSource::NUM_CHILDREN string - we use this to ask the data source about its number of children. I took 10% width out of the Pilot column to make room. So, fire this up and see what we get: 
     159 
     160[[Image(tutorial_datagrid_tree_1.gif, nolink)]] 
     161 
     162As you can see, we haven't got our "expand_button" formatter yet so it falls back to displaying the raw text output, in this case the number of children. 
     163 
     164=== Creating the data formatter === 
     165 
     166Naturally, the next step is to create the data formatter! This one is pretty simple - all it does it read the first entry of the raw_data array to see how many children there are. If there is at least one child, then we return a <datagridexpand> element, otherwise we return nothing: 
     167 
     168{{{ 
     169void ExpandButtonFormatter::FormatData(EMP::Core::String& formatted_data, const EMP::Core::StringList& raw_data) 
     170{ 
     171        // Data format: 
     172        // raw_data[0] is the number of children that this row has. 0 means no button, more than 0 mean a button. 
     173 
     174        int num_children = 0; 
     175        EMP::Core::TypeConverter< EMP::Core::String, int >::Convert(raw_data[0], num_children); 
     176         
     177        if (num_children > 0) 
     178        { 
     179                formatted_data = "<datagridexpand />"; 
     180        } 
     181        else 
     182        { 
     183                formatted_data = ""; 
     184        } 
     185} 
     186}}} 
     187 
     188The datagridexpand element is an element that listens to the onclick events and toggles the visibility of the child rows of its parent row. In the RML there's been styling added to make it look like a +/- button - you'll have to style one yourself just like a button if you plan on using trees in your own application. 
     189 
     190Don't forget to call the formatter the right name, and to instance the formatter in main.cpp. Once that's done, fire it up and you'll see the expand buttons beside each row, and be able to expand the child rows: 
     191 
     192[[Image(tutorial_datagrid_tree_2.gif, nolink)]] 
     193 
     194== Step 4: Styling == 
     195 
     196All that remains is upgrading the defender decorator to support displaying different images. The project's already been set up in a way so that if we give the defender element the classes of alien_1, alien_2 or alien_3 then it'll display with the image of that alien instead of the defender. We should probably rename that element too, as it's a bit of misnomer now, but we'll leave that till later. :) If we make a new field - type - and pass that into the defender decorator as well, we can get then defender to allocate the right class to get it to display the corresponding image. So, in tutorial.rml: 
     197 
     198{{{ 
     199<col fields="colour,type" formatter="ship" width="20%">Ship:</col> 
     200}}} 
     201 
     202And in the GetRow function in HighScores.cpp, for the "scores" table, right after the "colour" check: 
     203 
     204{{{ 
     205else if (columns[i] == "type") 
     206{ 
     207        rows.push_back("0"); 
     208} 
     209}}} 
     210 
     211And in the same function, for the "player_X" table: 
     212 
     213{{{ 
     214else if (columns[i] == "type") 
     215{ 
     216        row.push_back(EMP::Core::String(8, "%d", alien_kills_array_index + 1)); 
     217} 
     218}}} 
     219 
     220And then finally the formatter needs updating to use the new field we're sending through to it: 
     221 
     222{{{ 
     223void HighScoresShipFormatter::FormatData(EMP::Core::String& formatted_data, const EMP::Core::StringList& raw_data) 
     224{ 
     225        // Data format: 
     226        // raw_data[0] is the colour, in "%d, %d, %d, %d" format. 
     227        // raw_data[1] is the type. 0 means a defender, else N means alien type N. 
     228 
     229        EMP::Core::Colourb ship_colour; 
     230        EMP::Core::TypeConverter< EMP::Core::String, EMP::Core::Colourb >::Convert(raw_data[0], ship_colour); 
     231        EMP::Core::String colour_string(32, "%d,%d,%d", ship_colour.red, ship_colour.green, ship_colour.blue); 
     232 
     233        int ship_type; 
     234        EMP::Core::TypeConverter< EMP::Core::String, int >::Convert(raw_data[1], ship_type); 
     235        EMP::Core::String class_string = ""; 
     236        if (ship_type > 0) 
     237        { 
     238                class_string = EMP::Core::String(32, "class=\"alien_%d\"", ship_type); 
     239        } 
     240 
     241        formatted_data = "<defender " + class_string + " style=\"color: rgb(" + colour_string + ");\" />"; 
     242} 
     243}}} 
     244 
     245Which gives us the following: 
     246 
     247[[Image(tutorial_datagrid_tree_3.gif, nolink)]]