Changes from Version 1 of documentation/tutorials/Datagrid

Show
Ignore:
Author:
lloydw (IP: 192.168.0.1)
Timestamp:
01/04/08 13:57:07 (10 years ago)
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • documentation/tutorials/Datagrid

    v0 v1  
     1[[PageOutline(1-5, Contents)]] 
     2= Datagrid Tutorial = 
     3 
     4This tutorial will take you through the process of upgrading an existing body of data to be a data source, then creating a datagrid to read from that source. 
     5 
     6This tutorial expects that you've got a solid grounding in C++ and know the basics of RML and RCSS. 
     7 
     8For a detailed description on how DataGrids work with DataSources please see the [wiki:documentation/C++Manual/Controls/DataGrid C++ Manual]. 
     9 
     10 
     11== Step 1: Getting started == 
     12 
     13Load up the Datagrid tutorial, compile and run it, you'll see a blank high scores window: 
     14 
     15[[Image(tutorial_datagrid_1.gif, nolink)]] 
     16 
     17If you take a look at the C++ code, you see that there's a HighScores class that loads and stores a set of high scores. At the moment it doesn't do much at all - the next step will be to turn this into data source so it can talk with the datagrid that we're going to add later. 
     18 
     19 
     20== Step 2: The data source == 
     21 
     22EMP::Core::DataSource is an abstract base class which simulates the interface to a database. Each data source contains one or more tables, and each table contains rows and columns. The columns specify the fields each row has. To inherit from EMP::Core::DataSource, you need to override two functions: 
     23 
     24{{{ 
     25virtual void GetRow(EMP::Core::StringList& row, const EMP::Core::String& table, int row_index, const EMP::Core::StringList& columns) = 0; 
     26virtual int GetNumRows(const EMP::Core::String& table) = 0; 
     27}}} 
     28 
     29GetNumRows is the easy one - the function is passed in the name of a table, and it returns how many rows are currently in that table. 
     30 
     31GetRow is the meat of the data source implementation. It takes a table, an index to a row within that table and a list of columns that are being queried. The function must look that row up and, for each column in the list, push the data into the EMP::Core::StringList ''row''. 
     32 
     33=== Implementation === 
     34 
     35So, let's convert the lackluster HighScores class to a fully-fledged EMP::Core::DataSource. First of all include EMP/Core/DataSource.h and have HighScores inherit from EMP::Core::DataSource. Take a quick look at the EMP::Core::DataSource's constructor: 
     36 
     37{{{ 
     38EMP::Core::DataSource(const EMP::Core::String& name = ""); 
     39}}} 
     40 
     41The name parameter that it takes is used to uniquely identify it. If this is set then we can use that name have a datagrid automatically look up our data source without any C++ code. If we don't pass in a name, then it'll default to the address of the data source object - much less useful! So it's best to call the constructor from HighScore's own constructor and pass in a name, like "high_scores". 
     42 
     43That's the basics covered, now we need to implement the two required functions. Add the GetRow and GetNumRows functions into HighScore. The implementation of these is fairly simple: GetNumRows returns the number of high scores we've got in the chart, and GetRow loops through the columns array and constructs a string for each requested column and pushes it into the passed-in string list. So you should end up with something like this: 
     44 
     45{{{ 
     46void HighScores::GetRow(EMP::Core::StringList& row, const EMP::Core::String& table, int row_index, const EMP::Core::StringList& columns) 
     47{ 
     48        if (table == "scores") 
     49        { 
     50                for (size_t i = 0; i < columns.size(); i++) 
     51                { 
     52                        if (columns[i] == "name") 
     53                        { 
     54                                row.push_back(scores[row_index].name); 
     55                        } 
     56                        else if (columns[i] == "score") 
     57                        { 
     58                                row.push_back(EMP::Core::String(32, "%d", scores[row_index].score)); 
     59                        } 
     60                        else if (columns[i] == "colour") 
     61                        { 
     62                                EMP::Core::String colour_string; 
     63                                EMP::Core::TypeConverter< EMP::Core::Colourb, EMP::Core::String >::Convert(scores[row_index].colour, colour_string); 
     64                                row.push_back(colour_string); 
     65                        } 
     66                        else if (columns[i] == "wave") 
     67                        { 
     68                                row.push_back(EMP::Core::String(8, "%d", scores[row_index].wave)); 
     69                        } 
     70                } 
     71        } 
     72} 
     73 
     74 
     75 
     76int HighScores::GetNumRows(const EMP::Core::String& table) 
     77{ 
     78        if (table == "scores") 
     79        { 
     80                for (int i = 0; i < NUM_SCORES; i++) 
     81                { 
     82                        if (scores[i].score == -1) 
     83                        { 
     84                                return i; 
     85                        } 
     86                } 
     87 
     88                return NUM_SCORES; 
     89        } 
     90 
     91        return 0; 
     92} 
     93}}} 
     94 
     95This should leave you with a compiling HighScores now that all the pure virtual functions have been implemented, and a data source that is perfectly usable. One more step remains however: 
     96 
     97=== Updating the data source === 
     98 
     99Half of the point of having data sources is allowing them to update dynamically - it's not much fun if the data can't change after it's been displayed by the datagrid. So EMP::Core::DataSource has a few protected functions that you can call to let it know that something's changed inside the source: 
     100 
     101{{{ 
     102// Tells all attached listeners that one or more rows have been added to the data source. 
     103void NotifyRowAdd(const String& table, int first_row_added, int num_rows_added); 
     104 
     105// Tells all attached listeners that one or more rows have been removed from the data source. 
     106void NotifyRowRemove(const String& table, int first_row_removed, int num_rows_removed); 
     107 
     108// Tells all attached listeners that one or more rows have been changed in the data source. 
     109void NotifyRowChange(const String& table, int first_row_changed, int num_rows_changed); 
     110 
     111// Tells all attached listeners that the row structure has completely changed in the data source. 
     112void NotifyRowChange(const String& table); 
     113}}} 
     114 
     115The first two are self-explanatory - call them after you've added or removed rows. The first RowsChanged function is to be called when you've altered some cell information in some rows, and the datagrid will refresh the contents of those rows. The second RowsChanged function should be called when everything in the data source has changed (or you don't know what's changed) - this will cause any attached datagrids to wipe their contents and start again. 
     116 
     117So, out of a sense of completeness, we should add in a call to NotifyRowAdd whenever we add a row. We'd do the same if we removed or changed rows, but in this sample we just add rows. Check out the HighScores chart in the Rocket Invaders from Mars to see how NotifyRowChange is used when the player enters their name. Anyway, here's the final SubmitScore function: 
     118 
     119{{{ 
     120void HighScores::SubmitScore(const EMP::Core::String& name, const EMP::Core::Colourb& colour, int wave, int score) 
     121{ 
     122        for (size_t i = 0; i < NUM_SCORES; i++) 
     123        { 
     124                if (score > scores[i].score) 
     125                { 
     126                        // If we've already got the maximum number of scores, then we have 
     127                        // to send a RowsRemoved message as we're going to delete the last 
     128                        // row from the data source. 
     129                        bool max_rows = scores[NUM_SCORES - 1].score != -1; 
     130 
     131                        // Push down all the other scores. 
     132                        for (int j = NUM_SCORES - 1; j > i; j--) 
     133                        { 
     134                                scores[j] = scores[j - 1]; 
     135                        } 
     136 
     137                        // Insert our new score. 
     138                        scores[i].name = name; 
     139                        scores[i].colour = colour; 
     140                        scores[i].wave = wave; 
     141                        scores[i].score = score; 
     142 
     143                        // Send the row removal message (if necessary). 
     144                        if (max_rows) 
     145                        { 
     146                                NotifyRowRemove("scores", NUM_SCORES - 1, 1); 
     147                        } 
     148 
     149                        // Then send the rows added message. 
     150                        NotifyRowAdd("scores", i, 1); 
     151 
     152                        return; 
     153                } 
     154        } 
     155} 
     156}}} 
     157 
     158== Step 3: The datagrid == 
     159 
     160Now on to the datagrid. To create one of these we make a Rocket element with the tag <datagrid>. Inside the datagrid we can define multiple <col>s - each <col> being a column in the grid. This is what it might look like: 
     161 
     162{{{ 
     163<datagrid source="high_scores.scores"> 
     164        <col fields="name" width="40%">Pilot:</col> 
     165        <col fields="colour" width="20%">Ship:</col> 
     166        <col fields="wave" width="20%">Wave:</col> 
     167        <col fields="score" width="20%">Score:</col> 
     168</datagrid> 
     169}}} 
     170 
     171The source attribute in the datagrid tag tells the datagrid where to fetch its data from. This is the in the format "datasource.table" - so this looks in the "scores" table, found in the "high_scores" data source. If you called your data source a different name then change this attribute. 
     172 
     173Each column has a "fields" attribute - this tells the column which fields it fetches from the data source to display. This is a list in CSV form, but in this tutorial each column only fetches one field. In the Rocket Invaders from Mars sample multiple fields per column are used. 
     174 
     175The "width" attribute in <col> instructs how much of the width of the datagrid that this column takes up. 
     176 
     177Anything in between the <col> and </col> tags is put in a header row, above all the other rows. Any RML can be put in here, and it can be left empty. 
     178 
     179Running the sample with the above code will give us the following output: 
     180 
     181[[Image(tutorial_datagrid_2.gif, nolink)]] 
     182 
     183Well, at least it's working. Time to pretty it up some: 
     184 
     185=== Data formatters === 
     186 
     187The third part of the datagrid system is the data formatter. A data formatter sits in between the data source and datagrid - it takes the raw field information and processes it into RML. So in this way you can turn the text returned by a column request into an icon, a button, an image with a caption, anything at all. Even another datagrid! To do this, first we go to the declaration of the datagrid in tutorial.rml and add the data formatter attribute to the ship col: 
     188 
     189{{{ 
     190<col fields="colour" formatter="ship" width="20%">Ship:</col> 
     191}}} 
     192 
     193This will tell the ship column to not just display the raw RGBA values that get sent back from the HighScore data source, but instead to send them to the "ship" formatter and display what that returns. Easy as that! Now all we have to do is write the ship formatter. 
     194 
     195A data formatter inherits from the class Rocket::Controls::DataFormatter. It has one function that needs overriding: 
     196 
     197{{{ 
     198virtual void FormatData(EMP::Core::String& formatted_data, const EMP::Core::StringList& raw_data) = 0; 
     199}}} 
     200 
     201This function takes a list of strings, which contains the fields from the data query. The string reference is used to return the final RML once the data has been formatted. It also has a constructor that takes a const char* (defaulting to "") in the same way as EMP::Core::DataSource. This is the name of the formatter, and is used to uniquely identify it to any datagrid columns that wish to use it. 
     202 
     203So to make a new formatter, first make a new class - I called mine "HighScoresShipFormatter". Have it inherit from Rocket::Controls::DataFormatter and define the FormatData function. In the .cpp file, call the Rocket::Controls::DataFormatter constructor called from the HighScoresShipFormatter constructor with the parameter "ship". This will give it the name that the datagrid column references it by. The next step is to write the FormatData function. Very handily there's a decorator which does exactly what we want, and it's mapped to the <defender> tag. The decorator reads the "colour" style applied to the <defender> tag and colours itself based on that. So all we have to do is read the raw colour information then construct a <defender> tag with a style with that colour. Here's my implementation: 
     204 
     205{{{ 
     206void HighScoresShipFormatter::FormatData(EMP::Core::String& formatted_data, const EMP::Core::StringList& raw_data) 
     207{ 
     208        EMP::Core::Colourb ship_colour; 
     209        EMPTypeConverter< EMP::Core::String, EMP::Core::Colourb >::Convert(raw_data[0], ship_colour); 
     210 
     211        EMP::Core::String colour_string(32, "%d,%d,%d", ship_colour.red, ship_colour.green, ship_colour.blue); 
     212        formatted_data = "<defender style=\"color: rgb(" + colour_string + ");\" />"; 
     213} 
     214}}} 
     215 
     216Then to tie it all together we need to instance the formatter. It'll automatically add itself to the formatter database, so in the main.cpp we only have to include the .h file and construct an instance after Rocket is initialised. 
     217 
     218So your class should look something like this: 
     219 
     220{{{ 
     221#include <Rocket/Controls/DataFormatter.h> 
     222 
     223class HighScoresShipFormatter : public Rocket::Controls::DataFormatter 
     224{ 
     225        public: 
     226                HighScoresShipFormatter(); 
     227                ~HighScoresShipFormatter(); 
     228 
     229                void FormatData(EMP::Core::String& formatted_data, const EMP::Core::StringList& raw_data); 
     230}; 
     231}}} 
     232 
     233{{{ 
     234#include <EMP/Core/TypeConverter.h> 
     235#include "HighScoresShipFormatter.h" 
     236 
     237HighScoresShipFormatter::HighScoresShipFormatter() : Rocket::Controls::DataFormatter("ship") 
     238{ 
     239} 
     240 
     241HighScoresShipFormatter::~HighScoresShipFormatter() 
     242{ 
     243} 
     244 
     245void HighScoresShipFormatter::FormatData(EMP::Core::String& formatted_data, const EMP::Core::StringList& raw_data) 
     246{ 
     247        // Data format: 
     248        // raw_data[0] is the colour, in "%d, %d, %d, %d" format. 
     249 
     250        EMP::Core::Colourb ship_colour; 
     251        EMP::Core::TypeConverter< EMP::Core::String, EMP::Core::Colourb >::Convert(raw_data[0], ship_colour); 
     252 
     253        EMP::Core::String colour_string(32, "%d,%d,%d", ship_colour.red, ship_colour.green, ship_colour.blue); 
     254 
     255        formatted_data = "<defender style=\"color: rgb(" + colour_string + ");\" />"; 
     256} 
     257}}} 
     258 
     259Run that and you should now see the correctly formatted ship colours: 
     260 
     261[[Image(tutorial_datagrid_3.gif, nolink)]] 
     262 
     263Excellent! Now to style the rest of the table. 
     264 
     265== Step 4: Styling the datagrid == 
     266 
     267The datagrid can be styled just like any other Rocket element. RCSS hooks are provided for: 
     268 - '''datagrid:''' The whole grid, including the header and all the visible rows. 
     269 - '''datagridheader:''' The top row that contains the headers for each of the columns. 
     270 - '''datagridbody:''' All the rows, excluding the header row. 
     271 - '''datagridrow:''' Each row underneath the header. 
     272 - '''datagridcell:''' Each cell inside a row. 
     273 
     274[[Image(tutorial_datagrid_4.gif, nolink)]] 
     275 
     276So, using these hooks, we can make the datagrid look pretty much any way we want it to. First, let's add a background to the body. The RCSS rule shown below will add an image background (using a tiled-box decorator) around the body, change the text to black and center it, and finally add some margins and padding to move the text within the new border: 
     277 
     278{{{ 
     279datagridbody 
     280{ 
     281        color: black; 
     282        text-align: center; 
     283                         
     284        margin-left: 4px; 
     285        margin-right: 3px; 
     286        padding: 0px 4px 4px 4px; 
     287                         
     288        background-decorator: tiled-box; 
     289        background-top-left-image: ../../../assets/invader.png 281px 275px 292px 284px; 
     290        background-top-right-image: ../../../assets/invader.png 294px 275px 305px 284px; 
     291        background-top-image: ../../../assets/invader.png stretch 292px 275px 293px 284px; 
     292        background-bottom-left-image: ../../../assets/invader.png 281px 285px 292px 296px; 
     293        background-bottom-right-image: ../../../assets/invader.png 294px 285px 305px 296px; 
     294        background-bottom-image: ../../../assets/invader.png stretch 292px 285px 293px 296px; 
     295        background-left-image: ../../../assets/invader.png stretch 281px 283px 292px 284px; 
     296        background-center-image: ../../../assets/invader.png stretch 292px 283px 293px 284px; 
     297} 
     298}}} 
     299 
     300So now it should look like this: 
     301 
     302[[Image(tutorial_datagrid_5.gif, nolink)]] 
     303 
     304Much better! Header still is a bit lacking. We'll add a rule to add a tiled-horizontal decorator as a background, then some padding to bring the text inside the background: 
     305 
     306{{{ 
     307datagridheader 
     308{ 
     309        width: auto; 
     310        height: 25px; 
     311        padding: 5px 10px 0px 10px; 
     312                 
     313        background-decorator: tiled-horizontal; 
     314        background-left-image: ../../../assets/invader.png 127px 192px 143px 223px; 
     315        background-center-image: ../../../assets/invader.png stretch 143px 192px 145px 223px; 
     316        background-right-image: ../../../assets/invader.png 145px 192px 160px 223px; 
     317} 
     318}}} 
     319 
     320So now we've got the following: 
     321 
     322[[Image(tutorial_datagrid_6.gif, nolink)]] 
     323 
     324Looking pretty good now. One more thing to add a bit more zing: we'll colour the even rows differently to make it easier to see the row delineations. The following rule will do exactly that: 
     325 
     326{{{ 
     327datagrid datagridrow:nth-child(even) 
     328{ 
     329        background: #FFFFFFA0; 
     330} 
     331}}} 
     332 
     333So finally, this is what we've come up with: 
     334 
     335[[Image(tutorial_datagrid_7.gif, nolink)]]