Difference between revisions of "WxSmith tutorial: Adding advanced properties into items"
(7 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
− | + | [[Category:Code::Blocks Documentation]] | |
+ | [[Category:Developer Documentation]] | ||
+ | [[Category:wxSmith extensions]] | ||
+ | |||
+ | |||
== Preface == | == Preface == | ||
− | In previous tutorial we learned how to add some basic properties into our custom item. In this tutorial we will learn how to create fully customizable properties by operating directly on wxProperytGrid and xml structures. Let's take a look how wxSmith deals with | + | In previous tutorial we learned how to add some basic properties into our custom item. In this tutorial we will learn how to create fully customizable properties by operating directly on wxProperytGrid and xml structures. Note that we will use [http://wxpropgrid.sourceforge.net/ wxPropertyGrid] and [http://www.grinninglizard.com/tinyxml/ TinyXml] libraries here so I recommend you get familliar with them first. |
+ | |||
+ | Let's take a look how wxSmith deals with fully customizable properties: | ||
== Fully customizable properties == | == Fully customizable properties == | ||
Line 22: | Line 28: | ||
void OnAddExtraProperties(wxsPropertyGridManager* Grid); | void OnAddExtraProperties(wxsPropertyGridManager* Grid); | ||
− | Inside this class we can freely operate on given <tt>wxsPropertyGridManager</tt> (see [http://wxpropgrid.sourceforge.net/ wxPropertyGrid site] to learn more on how to operate on this class), but the operations should be limited to | + | Inside this class we can freely operate on given <tt>wxsPropertyGridManager</tt> (see [http://wxpropgrid.sourceforge.net/ wxPropertyGrid site] to learn more on how to operate on this class), but the operations should be limited to adding properties and responding for property change (it may be risky to delete or change existing ones). |
At the end of custom implementation of this function, we should also call the original function declared inside wxsWidget class: | At the end of custom implementation of this function, we should also call the original function declared inside wxsWidget class: | ||
Line 165: | Line 171: | ||
These implementations are just calling original functions to let wxSmith do it's work inside of them. | These implementations are just calling original functions to let wxSmith do it's work inside of them. | ||
− | Another thing we need before we start implementing new property is some place where we will store chart's data. It must of course be put into wxsChart class. We need some array | + | Another thing we need before we start implementing new property is some place where we will store chart's data. It must of course be put into wxsChart class. We need some array describing wxChartPoints classes and probaly some structures describing points. But now let's put wxPGId value for each wxChartPoints class. This id will point to parent property which will handle other wxChartPoints properties inside: |
''' struct ChartPointsDesc''' | ''' struct ChartPointsDesc''' | ||
Line 179: | Line 185: | ||
}; | }; | ||
− | Currently each chart points | + | Currently each set of chart points is described by property id, but it will be extended later. Another property id which was added here (m_ChartPointsCountId) is used to handle count of wxChartPoints classes. Now we will add this property into property browser and will implement all the dynamics it does (adding / removing other properties). |
First we have to update code generating custom properties, we do this by adding one property handling number of data sets and some properties per each set: | First we have to update code generating custom properties, we do this by adding one property handling number of data sets and some properties per each set: | ||
Line 185: | Line 191: | ||
void wxsChart::OnAddExtraProperties(wxsPropertyGridManager* Grid) | void wxsChart::OnAddExtraProperties(wxsPropertyGridManager* Grid) | ||
{ | { | ||
+ | // '''(1) | ||
Grid->SetTargetPage(0); | Grid->SetTargetPage(0); | ||
+ | // '''(2) | ||
m_ChartPointsCountId = Grid->Append(wxIntProperty(_("Number of data sets"),wxPG_LABEL, | m_ChartPointsCountId = Grid->Append(wxIntProperty(_("Number of data sets"),wxPG_LABEL, | ||
(int)m_ChartPointsDesc.Count())); | (int)m_ChartPointsDesc.Count())); | ||
+ | // '''(3) | ||
for ( int i=0; i<(int)m_ChartPointsDesc.Count(); i++ ) | for ( int i=0; i<(int)m_ChartPointsDesc.Count(); i++ ) | ||
{ | { | ||
Line 198: | Line 207: | ||
} | } | ||
− | + | In this code we switch to first page in property grid first ''(1)'' (this prevents adding properties to other pages because we can not be sure about current page), next we add number of sets ''(2)'' and then add properties for each set ''(3)'', wchich will be done in AppendPropertyForSet function. | |
Now let's handle changes done in current properties: | Now let's handle changes done in current properties: | ||
Line 204: | Line 213: | ||
void wxsChart::OnExtraPropertyChanged(wxsPropertyGridManager* Grid,wxPGId Id) | void wxsChart::OnExtraPropertyChanged(wxsPropertyGridManager* Grid,wxPGId Id) | ||
{ | { | ||
+ | // '''(1) | ||
Grid->SetTargetPage(0); | Grid->SetTargetPage(0); | ||
+ | // '''(2) | ||
if ( Id == m_ChartPointsCountId ) | if ( Id == m_ChartPointsCountId ) | ||
{ | { | ||
Line 211: | Line 222: | ||
int NewValue = Grid->GetPropertyValueAsInt(Id); | int NewValue = Grid->GetPropertyValueAsInt(Id); | ||
+ | // '''(3) | ||
if ( NewValue<0 ) | if ( NewValue<0 ) | ||
{ | { | ||
Line 219: | Line 231: | ||
if ( NewValue > OldValue ) | if ( NewValue > OldValue ) | ||
{ | { | ||
+ | // '''(4) | ||
+ | |||
// We have to generate new entries | // We have to generate new entries | ||
for ( int i=OldValue; i<NewValue; i++ ) | for ( int i=OldValue; i<NewValue; i++ ) | ||
Line 228: | Line 242: | ||
else if ( NewValue < OldValue ) | else if ( NewValue < OldValue ) | ||
{ | { | ||
+ | // '''(5) | ||
+ | |||
// We have to remove some entries | // We have to remove some entries | ||
for ( int i=NewValue; i<OldValue; i++ ) | for ( int i=NewValue; i<OldValue; i++ ) | ||
Line 238: | Line 254: | ||
} | } | ||
+ | // '''(6) | ||
NotifyPropertyChange(true); | NotifyPropertyChange(true); | ||
return; | return; | ||
} | } | ||
+ | // '''(7) | ||
for ( int i=0; i<(int)m_ChartPointsDesc.Count(); i++ ) | for ( int i=0; i<(int)m_ChartPointsDesc.Count(); i++ ) | ||
{ | { | ||
Line 250: | Line 268: | ||
} | } | ||
− | This function is much more complicated since it must dynamically add and remove properties from property grid. Note that we have to delete | + | This function is much more complicated since it must dynamically add and remove properties from property grid. In this code we first switch to base page as in previous function ''(1)'', then check if number of sets was changed ''(2)''. If yes, we read new number of sets and make sure it's not negative ''(3)'' because user may give any value including negative ones which could cause some crash. Then we check if new number of sets is greater than current one and add new enteries if necessary ''(4)'' and also check if we have to remove some sets when new number of sets is smaller than current one ''(5)''. If we have updated proserties sets, we call <tt>NotifyPropertyChange(true);</tt> which will update editor's area, regenerate source code and update all stuff in wxSmith. |
+ | |||
+ | If it was not number of sets that changed, we try to update individual property sets by calling <tt>HandleChangeInSet</tt> function. | ||
+ | |||
+ | Note that when we remove sets in ''(5)'' we have to call <tt>delete m_ChartPointsDesc[i];</tt> to avoid memory leaks because it won't be deleted automatically. Because of same reason, we also have to alter destructor which will free memory used by wxsChart class: | ||
wxsChart::~wxsChart() | wxsChart::~wxsChart() | ||
Line 261: | Line 283: | ||
} | } | ||
− | Now let's implement | + | Now let's implement functions functions opreating on individual property sets: <tt>AppendPropertyForSet</tt> and <tt>HandleChangeInSet</tt>. But before we do that, we need to update ChartPointsDesc structure a little bit. |
− | Each chart set has few global properties: name, type, colour and switch for showing label. Right now let's just add name and type since they are required to build wxChartPoints class. Also let's build structure for one point | + | Each chart points set has few global properties: name, type, colour and switch for showing label. Right now let's just add name and type since they are required to build wxChartPoints class. Also let's build structure for data of one point: |
''' struct PointDesc | ''' struct PointDesc | ||
Line 324: | Line 346: | ||
wxString SetName = wxString::Format(_("Set %d"),Position+1); | wxString SetName = wxString::Format(_("Set %d"),Position+1); | ||
+ | // '''(1) | ||
Desc->Id = Grid->Append(wxParentProperty(SetName,wxPG_LABEL)); | Desc->Id = Grid->Append(wxParentProperty(SetName,wxPG_LABEL)); | ||
+ | // '''(2) | ||
static const wxChar* Types[] = | static const wxChar* Types[] = | ||
{ | { | ||
Line 338: | Line 362: | ||
}; | }; | ||
+ | // '''(3) | ||
Desc->TypeId = Grid->AppendIn(Desc->Id,wxEnumProperty(_("Type"),wxPG_LABEL,Types,Values,Desc->Type)); | Desc->TypeId = Grid->AppendIn(Desc->Id,wxEnumProperty(_("Type"),wxPG_LABEL,Types,Values,Desc->Type)); | ||
Desc->NameId = Grid->AppendIn(Desc->Id,wxStringProperty(_("Name"),wxPG_LABEL,Desc->Name)); | Desc->NameId = Grid->AppendIn(Desc->Id,wxStringProperty(_("Name"),wxPG_LABEL,Desc->Name)); | ||
Desc->PointsCountId = Grid->AppendIn(Desc->Id,wxIntProperty(_("Number of points"),wxPG_LABEL,(int)Desc->Points.Count())); | Desc->PointsCountId = Grid->AppendIn(Desc->Id,wxIntProperty(_("Number of points"),wxPG_LABEL,(int)Desc->Points.Count())); | ||
+ | // '''(4) | ||
for ( int i=0; i<(int)Desc->Points.Count(); i++ ) | for ( int i=0; i<(int)Desc->Points.Count(); i++ ) | ||
{ | { | ||
Line 348: | Line 374: | ||
} | } | ||
− | In this function we first create parent property which will group properties of this set, | + | In this function we first create parent property which will group properties of this set ''(1)'', generate arrays which will be used for chart type proeprty ''(2)'', generate properties for set ''(3)'' and add properties for points which are part of this set ''(4)''. It may look little bit complicated now, but it is really easy and mostly we operate on wxPropertyGridManager class. |
Now let's create function which will react on data updates to properties inside set: | Now let's create function which will react on data updates to properties inside set: | ||
Line 354: | Line 380: | ||
bool wxsChart::HandleChangeInSet(wxsPropertyGridManager* Grid,wxPGId Id,int Position) | bool wxsChart::HandleChangeInSet(wxsPropertyGridManager* Grid,wxPGId Id,int Position) | ||
{ | { | ||
+ | // '''(1) | ||
ChartPointsDesc* Desc = m_ChartPointsDesc[Position]; | ChartPointsDesc* Desc = m_ChartPointsDesc[Position]; | ||
− | |||
bool Changed = false; | bool Changed = false; | ||
bool Global = Id==Desc->Id; | bool Global = Id==Desc->Id; | ||
+ | // '''(2) | ||
if ( Global || Id == Desc->TypeId ) | if ( Global || Id == Desc->TypeId ) | ||
{ | { | ||
Line 365: | Line 392: | ||
} | } | ||
+ | // '''(3) | ||
if ( Global || Id == Desc->NameId ) | if ( Global || Id == Desc->NameId ) | ||
{ | { | ||
Line 371: | Line 399: | ||
} | } | ||
+ | // '''(4) | ||
if ( Global || Id == Desc->PointsCountId ) | if ( Global || Id == Desc->PointsCountId ) | ||
{ | { | ||
Line 408: | Line 437: | ||
} | } | ||
− | if ( !Changed ) | + | // '''(5) |
+ | if ( !Changed || Global ) | ||
{ | { | ||
for ( int i=0; i<(int)Desc->Points.Count(); i++ ) | for ( int i=0; i<(int)Desc->Points.Count(); i++ ) | ||
Line 415: | Line 445: | ||
{ | { | ||
Changed = true; | Changed = true; | ||
+ | // '''(6) | ||
if ( !Global ) break; | if ( !Global ) break; | ||
} | } | ||
Line 422: | Line 453: | ||
if ( Changed ) | if ( Changed ) | ||
{ | { | ||
+ | // '''(7) | ||
NotifyPropertyChange(true); | NotifyPropertyChange(true); | ||
return true; | return true; | ||
Line 429: | Line 461: | ||
} | } | ||
− | That function may require some explanation. It returns true when any property in this set has changed and false if not. At the beginning we extract right description and check if id of changed property matches id of global parent property for whole set. If yes, we set Global flag to | + | That function may require some explanation. It returns true when any property in this chart points set has changed and false if not. |
+ | At the beginning we extract right description ''(1)'' and check if id of changed property matches id of global parent property for whole set. If yes, we set Global flag to true which says that we will update all properties in set. This may be not necessary in new property grid versions but it's better to handle it this way to prevent any possible bugs and data looses. Next we test individual properties for change ''(2), (3)'', and monitor number of points property in same way as we did react to changes of number of point sets ''(4)''. Last thing is to forward information about property change into point data ''(5)'' (we do this only when there was no change so far or if we update whole set). Note here that we don't stop processing points in loop only when there's no global flag set and some point reported that it's content changed ''(6)''. This have to be done this way because we have to be sure that all properties including points are updated when Global flag is set. | ||
− | + | ||
+ | Those two functions introduced two more functions which operate at point level. First one is AppendProperyForPoint and the second one is HandleChangeInPoint. Implementation of these functions will be quite simple: | ||
void wxsChart::AppendPropertyForPoint(wxsPropertyGridManager* Grid,ChartPointsDesc* SetDesc,int Position) | void wxsChart::AppendPropertyForPoint(wxsPropertyGridManager* Grid,ChartPointsDesc* SetDesc,int Position) | ||
Line 444: | Line 478: | ||
} | } | ||
− | and | + | We simply add one parent property for point and add one child property for each point's member. |
+ | Second function has following implementation: | ||
bool wxsChart::HandleChangeInPoint(wxsPropertyGridManager* Grid,wxPGId Id,ChartPointsDesc* SetDesc,int Position,bool Global) | bool wxsChart::HandleChangeInPoint(wxsPropertyGridManager* Grid,wxPGId Id,ChartPointsDesc* SetDesc,int Position,bool Global) | ||
Line 474: | Line 509: | ||
} | } | ||
− | + | We just read properties if they were changed of when global flag is set. Worth mentioning here is that the Global flag propagates from previous function which called it (<tt>HandleChangeInSet</tt>) and may be updated to true if parentp property of this point was changed. | |
+ | |||
+ | Now we have full control over charts and their points. But it's not the end now. There are more four things to do: affect preview, affect source code, load from xml and store into xml. | ||
+ | |||
+ | In this tutorial we will cover only xml operations. Affecting preview and source code will be described in next tutorial. | ||
+ | |||
+ | === Updating XML structures === | ||
+ | |||
+ | |||
+ | wxSmith uses XML strucures to store data of resource in many places. Most obvious one is XRC file, another example is WXS file and the third, hidden one is that wxSmith uses XML format to store entries in undo/redo buffers. So without routines responsible for storing and reading our custom property into xml, we don't have any of those features. | ||
+ | |||
+ | First problem we will solve is to store data into xml. Each chart point set will be stored inside <chartpointset> node and each point of this set will be stored inside <point> node which will be added as child of <chartpointset>. Some example structure may look like this: | ||
+ | |||
+ | <object class="wxChartCtrl" name="ID_CHART1" variable="Chart1" member="no"> | ||
+ | <chartpointset name="" type="pie"> | ||
+ | <point name="Point 1" x="5.000000" y="6.000000" /> | ||
+ | <point name="Point 2" x="7.000000" y="8.000000" /> | ||
+ | <point name="Point 3c" x="9.000000" y="10.000000" /> | ||
+ | </chartpointset> | ||
+ | <chartpointset name="" type="line3d"> | ||
+ | <point name="Point 1" x="0.000000" y="0.000000" /> | ||
+ | <point name="Point 2" x="10.000000" y="10.000000" /> | ||
+ | </chartpointset> | ||
+ | </object> | ||
+ | |||
+ | So let's write function which will store data into xml: | ||
+ | |||
+ | bool wxsChart::OnXmlWrite(TiXmlElement* Element,bool IsXRC,bool IsExtra) | ||
+ | { | ||
+ | for ( size_t i=0; i<m_ChartPointsDesc.Count(); i++ ) | ||
+ | { | ||
+ | ChartPointsDesc* Desc = m_ChartPointsDesc[i]; | ||
+ | TiXmlElement* DescElem = Element->InsertEndChild(TiXmlElement("chartpointset")) | ||
+ | ->ToElement(); | ||
+ | |||
+ | DescElem->SetAttribute("name",cbU2C(Desc->Name)); | ||
+ | switch ( Desc->Type ) | ||
+ | { | ||
+ | case Bar: DescElem->SetAttribute("type","bar"); break; | ||
+ | case Bar3D: DescElem->SetAttribute("type","bar3d"); break; | ||
+ | case Pie: DescElem->SetAttribute("type","pie"); break; | ||
+ | case Pie3D: DescElem->SetAttribute("type","pie3d"); break; | ||
+ | case Points: DescElem->SetAttribute("type","points"); break; | ||
+ | case Points3D: DescElem->SetAttribute("type","points3d"); break; | ||
+ | case Line: DescElem->SetAttribute("type","line"); break; | ||
+ | case Line3D: DescElem->SetAttribute("type","line3d"); break; | ||
+ | case Area: DescElem->SetAttribute("type","area"); break; | ||
+ | case Area3D: DescElem->SetAttribute("type","area3d"); break; | ||
+ | } | ||
+ | |||
+ | for ( size_t j=0; j<Desc->Points.Count(); j++ ) | ||
+ | { | ||
+ | PointDesc* PDesc = Desc->Points[j]; | ||
+ | TiXmlElement* PointElem = DescElem->InsertEndChild(TiXmlElement("point")) | ||
+ | ->ToElement(); | ||
+ | PointElem->SetAttribute("name",cbU2C(PDesc->Name)); | ||
+ | PointElem->SetDoubleAttribute("x",PDesc->X); | ||
+ | PointElem->SetDoubleAttribute("y",PDesc->Y); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | return wxsWidget::OnXmlWrite(Element,IsXRC,IsExtra); | ||
+ | } | ||
+ | |||
+ | In this function we iterate through all sets and store xml data for it. Note that TinyXml can not operate on wxStirng nor unicode characters directly. So we have to use <tt>cbU2C</tt> macro provided by Code::Blocks which will convert unicode string into standard char* (maybe it's not best solution since it should always use wxConvUTF8 because that's default encoding in xml files used by wxSmith). | ||
+ | |||
+ | Now let's write reading function. It's very simillar to writing function: | ||
+ | |||
+ | bool wxsChart::OnXmlRead(TiXmlElement* Element,bool IsXRC,bool IsExtra) | ||
+ | { | ||
+ | for ( size_t i=0; i<m_ChartPointsDesc.Count(); i++ ) | ||
+ | { | ||
+ | delete m_ChartPointsDesc[i]; | ||
+ | } | ||
+ | m_ChartPointsDesc.Clear(); | ||
+ | |||
+ | for ( TiXmlElement* DescElem = Element->FirstChildElement("chartpointset"); | ||
+ | DescElem; | ||
+ | DescElem = DescElem->NextSiblingElement("chartpointset") ) | ||
+ | { | ||
+ | ChartPointsDesc* Desc = new ChartPointsDesc; | ||
+ | Desc->Name = cbC2U(DescElem->Attribute("name")); | ||
+ | wxString Type = cbC2U(DescElem->Attribute("type")); | ||
+ | |||
+ | if ( Type == _T("bar") ) Desc->Type = Bar; else | ||
+ | if ( Type == _T("bar3d") ) Desc->Type = Bar3D; else | ||
+ | if ( Type == _T("pie") ) Desc->Type = Pie; else | ||
+ | if ( Type == _T("pie3d") ) Desc->Type = Pie3D; else | ||
+ | if ( Type == _T("points") ) Desc->Type = Points; else | ||
+ | if ( Type == _T("points3d") ) Desc->Type = Points3D; else | ||
+ | if ( Type == _T("line") ) Desc->Type = Line; else | ||
+ | if ( Type == _T("line3d") ) Desc->Type = Line3D; else | ||
+ | if ( Type == _T("area") ) Desc->Type = Area; else | ||
+ | if ( Type == _T("area3d") ) Desc->Type = Area3D; else | ||
+ | Desc->Type = Bar; | ||
+ | |||
+ | for ( TiXmlElement* PointElem = DescElem->FirstChildElement("point"); | ||
+ | PointElem; | ||
+ | PointElem = PointElem->NextSiblingElement("point") ) | ||
+ | { | ||
+ | PointDesc* Point = new PointDesc; | ||
+ | Point->Name = cbC2U(PointElem->Attribute("name")); | ||
+ | (PointElem->QueryDoubleAttribute("x",&Point->X) == TIXML_SUCCESS) || (Point->X = 0.0); | ||
+ | (PointElem->QueryDoubleAttribute("y",&Point->Y) == TIXML_SUCCESS) || (Point->Y = 0.0); | ||
+ | |||
+ | Desc->Points.Add(Point); | ||
+ | } | ||
+ | |||
+ | m_ChartPointsDesc.Add(Desc); | ||
+ | } | ||
+ | |||
+ | return wxsWidget::OnXmlRead(Element,IsXRC,IsExtra); | ||
+ | } | ||
+ | |||
+ | In few places we used cbC2U which converts char* string into wxString. | ||
+ | |||
+ | |||
+ | Now if we test new chart, it stores and restores all data from xml structures (the easiest way to test it is to use undo/redo operations since they internally use xml structures). Now we can continue updating of wxChart item by generating better source code and preview. This will be covered in next tutorial. |
Latest revision as of 19:30, 14 October 2007
Preface
In previous tutorial we learned how to add some basic properties into our custom item. In this tutorial we will learn how to create fully customizable properties by operating directly on wxProperytGrid and xml structures. Note that we will use wxPropertyGrid and TinyXml libraries here so I recommend you get familliar with them first.
Let's take a look how wxSmith deals with fully customizable properties:
Fully customizable properties
To create fully costomizable property we have to handle few operations done on it:
- Adding property to property grid
- Reacting to change of this property
- Reading data from XML (XRC) structures
- Writing data to XML (XRC) structures
Each of these operations is done inside one virtual function of class which adds item into wxSmith (like wxsChart in our chart example). Overriding it and giving custom implementation allows us to freely operate on properties.
Adding custom property to property grid
To include our own properties in list of properties, we have to add following function into our class:
void OnAddExtraProperties(wxsPropertyGridManager* Grid);
Inside this class we can freely operate on given wxsPropertyGridManager (see wxPropertyGrid site to learn more on how to operate on this class), but the operations should be limited to adding properties and responding for property change (it may be risky to delete or change existing ones).
At the end of custom implementation of this function, we should also call the original function declared inside wxsWidget class:
void CustomClass::OnAddExtraProperties(wxsPropertyGridManager* Grid) { // Add custom properties here wxsWidget::OnAddExtraProperties(Grid); }
If it won't be called, item wont allow to edit any events because they are added inside wxsWidget::OnAddExtraProperties(Grid); call.
Reacting to change of custom properties
Processing changes of custom properties is simillar to creating them. We just have to override following function:
void OnExtraPropertyChanged(wxsPropertyGridManager* Grid,wxPGId Id);
It has extra parameter comparing to previous function - Id. It's id of property which was changed. It's advised that you compare value of Id with identifiers generated inside OnAddExtraProperties to avoid unnecessary reads from property grid. At the end of this function you should also call original one but only if you're sure that your properties have not changed:
void CustomClass::OnExtraPropertyChanged(wxsPropertyGridManager* Grid,wxPGId Id) { if ( Id == Property_Id ) { // Read value of property here NotifyPropertyChange(true); return; } wxsWidget::OnExtraPropertyChanged(Grid,Id); }
Note that there's also call to NorifyPropertyChange(true); at the end of value reading. This call updates all things related to current resource: regenerates source code and XRC files if necessary, updates the content of editor and does few more things to keep wxSmith's state up to date.
Reading data from XML (XRC) structures
Reading of our custom properties is done inside following function:
bool OnXmlRead(TiXmlElement* Element,bool IsXRC,bool IsExtra);
The Element argument is object represending xml node of current widget. To load custom properties we must locate child nodes and read data from them. Code::Blocks and wxSmith are using TinyXml to operate on xml structures so to get more informations on how to read and manipulate xml data, read TinyXml's documentation.
The IsXRC and IsExtra arguments require some better description. From the very beginning wxSmith tends to be compatible with XRC files which allow storing structure of window inside xml file. Usually XRC files are limited to widgets which are provided with wxWidgets library. And they don't provide some extra data used by wxSmith like enteries for event handlers and variable name. wxSmith does split data of widgets into two parts - the firs one is strict XRC data and the second one is extra data provided by wxSmith. If IsXRC parameter is true, this mean that function should read XRC data part, if IsExtra is true, this mean that it should read extra data (both arguments may be true in one call). Most of contrib items would have XRC support disabled so we should read data only when IsExtra is true:
bool CustomClass::OnXmlRead(TiXmlElement* Element,bool IsXRC,bool IsExtra) { if ( IsExtra ) { // Process xml structures here } return wxsTool::OnXmlRead(Element,IsXRC,IsExtra); }
At the end of this implementation we must call original function as usual ;)
Writing data to XML (XRC) structures
Writing data into xml structures is simillar to reading them. We have to implement following function:
bool CustomClass::OnXmlWrite(TiXmlElement* Element,bool IsXRC,bool IsExtra) { if ( IsExtra ) { // Store data into xml structure } return wxsTool::OnXmlRead(Element,IsXRC,IsExtra); }
The meaning of arguments is same as in case of reading data. The only difference is that we write data here instead of reading it.
Adding custom property into wxChart
Now it's time to do some really complex task. We will add property which adds some data into chart widget so it would be possible to show some content in editor, preview and of course in target application. But before we do this, we need some knowledge about wxChart's data managment.
How wxChartCtrl handle data for properties
Each wxChartCtrl widget can show few sets of data which create one chart shape (one pie, one line etc). Each of these sets is stored inside class derived from wxChartPoints and the class type depends on type of chart we want to show. This structure keep data of points, percentages or any other data required to build chart. Following chart types are available in wxChart:
- Bar
- Bar3D
- Pie
- Pie3D
- Points
- Points3D
- Line
- Line3D
- Area
- Area3D
Different data types may be stored inside one chart which will result in many charts shown inside one control.
Now let's add some property.
Adding dynamically changing properties set
First thing we need here is to provide dynamically changing list of data sets (wxChartPoints structures). Because it's not as easy as in case of base properties, we have to handle our properties manually. So first we add new functions which will manage our custom properties.
We add this code into wxsChart's declaration:
void OnAddExtraProperties(wxsPropertyGridManager* Grid); void OnExtraPropertyChanged(wxsPropertyGridManager* Grid,wxPGId Id); bool OnXmlRead(TiXmlElement* Element,bool IsXRC,bool IsExtra); bool OnXmlWrite(TiXmlElement* Element,bool IsXRC,bool IsExtra);
and the following initial implementations:
void wxsChart::OnAddExtraProperties(wxsPropertyGridManager* Grid) { wxsWidget::OnAddExtraProperties(Grid); } void wxsChart::OnExtraPropertyChanged(wxsPropertyGridManager* Grid,wxPGId Id) { wxsWidget::OnExtraPropertyChanged(Grid,Id); } bool wxsChart::OnXmlRead(TiXmlElement* Element,bool IsXRC,bool IsExtra) { return wxsWidget::OnXmlRead(Element,IsXRC,IsExtra); } bool wxsChart::OnXmlWrite(TiXmlElement* Element,bool IsXRC,bool IsExtra) { return wxsWidget::OnXmlWrite(Element,IsXRC,IsExtra); }
These implementations are just calling original functions to let wxSmith do it's work inside of them.
Another thing we need before we start implementing new property is some place where we will store chart's data. It must of course be put into wxsChart class. We need some array describing wxChartPoints classes and probaly some structures describing points. But now let's put wxPGId value for each wxChartPoints class. This id will point to parent property which will handle other wxChartPoints properties inside:
struct ChartPointsDesc { wxPGId Id; }; WX_DEFINE_ARRAY(ChartPointDesc*,List); long m_Flags; List m_ChartPointsDesc; wxPGId m_ChartPointsCountId; };
Currently each set of chart points is described by property id, but it will be extended later. Another property id which was added here (m_ChartPointsCountId) is used to handle count of wxChartPoints classes. Now we will add this property into property browser and will implement all the dynamics it does (adding / removing other properties).
First we have to update code generating custom properties, we do this by adding one property handling number of data sets and some properties per each set:
void wxsChart::OnAddExtraProperties(wxsPropertyGridManager* Grid) { // (1) Grid->SetTargetPage(0); // (2) m_ChartPointsCountId = Grid->Append(wxIntProperty(_("Number of data sets"),wxPG_LABEL, (int)m_ChartPointsDesc.Count())); // (3) for ( int i=0; i<(int)m_ChartPointsDesc.Count(); i++ ) { AppendPropertyForSet(Grid,i); } wxsWidget::OnAddExtraProperties(Grid); }
In this code we switch to first page in property grid first (1) (this prevents adding properties to other pages because we can not be sure about current page), next we add number of sets (2) and then add properties for each set (3), wchich will be done in AppendPropertyForSet function.
Now let's handle changes done in current properties:
void wxsChart::OnExtraPropertyChanged(wxsPropertyGridManager* Grid,wxPGId Id) { // (1) Grid->SetTargetPage(0); // (2) if ( Id == m_ChartPointsCountId ) { int OldValue = (int)m_ChartPointsDesc.Count(); int NewValue = Grid->GetPropertyValueAsInt(Id); // (3) if ( NewValue<0 ) { NewValue = 0; Grid->SetPropertyValue(Id,NewValue); } if ( NewValue > OldValue ) { // (4) // We have to generate new entries for ( int i=OldValue; i<NewValue; i++ ) { m_ChartPointsDesc.Add(new ChartPointsDesc()); AppendPropertyForSet(Grid,i); } } else if ( NewValue < OldValue ) { // (5) // We have to remove some entries for ( int i=NewValue; i<OldValue; i++ ) { Grid->Delete(m_ChartPointsDesc[i]->Id); delete m_ChartPointsDesc[i]; } m_ChartPointsDesc.RemoveAt(NewValue,OldValue-NewValue); } // (6) NotifyPropertyChange(true); return; } // (7) for ( int i=0; i<(int)m_ChartPointsDesc.Count(); i++ ) { if ( HandleChangeInSet(Grid,Id,i) ) return; } wxsWidget::OnExtraPropertyChanged(Grid,Id); }
This function is much more complicated since it must dynamically add and remove properties from property grid. In this code we first switch to base page as in previous function (1), then check if number of sets was changed (2). If yes, we read new number of sets and make sure it's not negative (3) because user may give any value including negative ones which could cause some crash. Then we check if new number of sets is greater than current one and add new enteries if necessary (4) and also check if we have to remove some sets when new number of sets is smaller than current one (5). If we have updated proserties sets, we call NotifyPropertyChange(true); which will update editor's area, regenerate source code and update all stuff in wxSmith.
If it was not number of sets that changed, we try to update individual property sets by calling HandleChangeInSet function.
Note that when we remove sets in (5) we have to call delete m_ChartPointsDesc[i]; to avoid memory leaks because it won't be deleted automatically. Because of same reason, we also have to alter destructor which will free memory used by wxsChart class:
wxsChart::~wxsChart() { for ( size_t i=0; i<m_ChartPointsDesc.Count(); i++ ) { delete m_ChartPointsDesc[i]; } m_ChartPointsDesc.Clear(); }
Now let's implement functions functions opreating on individual property sets: AppendPropertyForSet and HandleChangeInSet. But before we do that, we need to update ChartPointsDesc structure a little bit.
Each chart points set has few global properties: name, type, colour and switch for showing label. Right now let's just add name and type since they are required to build wxChartPoints class. Also let's build structure for data of one point:
struct PointDesc { wxString Name; double X; double Y; wxPGId Id; wxPGId NameId; wxPGId XId; wxPGId YId; }; WX_DEFINE_ARRAY(PointDesc*,PointList); enum PointsType { Bar, Bar3D, Pie, Pie3D, Points, Points3D, Line, Line3D, Area, Area3D }; struct ChartPointsDesc { wxPGId Id; wxPGId TypeId; wxPGId NameId; wxPGId PointsCountId; PointsType Type; wxString Name; PointList Points; ChartPointsDesc(): Type(Bar) {} ~ChartPointsDesc() { for ( size_t i=0; i<Points.Count(); i++ ) { delete Points[i]; } Points.Clear(); } };
Ok, now let's create code which will generate properties for set:
void wxsChart::AppendPropertyForSet(wxsPropertyGridManager* Grid,int Position) { ChartPointsDesc* Desc = m_ChartPointsDesc[Position]; wxString SetName = wxString::Format(_("Set %d"),Position+1); // (1) Desc->Id = Grid->Append(wxParentProperty(SetName,wxPG_LABEL)); // (2) static const wxChar* Types[] = { _T("Bar"), _T("Bar3D"), _T("Pie"), _T("Pie3D"), _T("Points"), _T("Points3D"), _T("Line"), _T("Line3D"), _T("Area"), _T("Area3D"), NULL }; static const long Values[] = { Bar, Bar3D, Pie, Pie3D, Points, Points3D, Line, Line3D, Area, Area3D }; // (3) Desc->TypeId = Grid->AppendIn(Desc->Id,wxEnumProperty(_("Type"),wxPG_LABEL,Types,Values,Desc->Type)); Desc->NameId = Grid->AppendIn(Desc->Id,wxStringProperty(_("Name"),wxPG_LABEL,Desc->Name)); Desc->PointsCountId = Grid->AppendIn(Desc->Id,wxIntProperty(_("Number of points"),wxPG_LABEL,(int)Desc->Points.Count())); // (4) for ( int i=0; i<(int)Desc->Points.Count(); i++ ) { AppendPropertyForPoint(Grid,Desc,i); } }
In this function we first create parent property which will group properties of this set (1), generate arrays which will be used for chart type proeprty (2), generate properties for set (3) and add properties for points which are part of this set (4). It may look little bit complicated now, but it is really easy and mostly we operate on wxPropertyGridManager class.
Now let's create function which will react on data updates to properties inside set:
bool wxsChart::HandleChangeInSet(wxsPropertyGridManager* Grid,wxPGId Id,int Position) { // (1) ChartPointsDesc* Desc = m_ChartPointsDesc[Position]; bool Changed = false; bool Global = Id==Desc->Id; // (2) if ( Global || Id == Desc->TypeId ) { Desc->Type = (PointsType)Grid->GetPropertyValueAsInt(Desc->TypeId); Changed = true; } // (3) if ( Global || Id == Desc->NameId ) { Desc->Name = Grid->GetPropertyValueAsString(Desc->NameId); Changed = true; } // (4) if ( Global || Id == Desc->PointsCountId ) { int OldValue = (int)Desc->Points.Count(); int NewValue = Grid->GetPropertyValueAsInt(Desc->PointsCountId); if ( NewValue<0 ) { NewValue = 0; Grid->SetPropertyValue(Desc->PointsCountId,NewValue); } if ( NewValue > OldValue ) { for ( int i=OldValue; i<NewValue; i++ ) { PointDesc* NewPoint = new PointDesc; NewPoint->X = 0.0; NewPoint->Y = 0.0; NewPoint->Name = wxString::Format(_("Point %d"),i+1); Desc->Points.Add(NewPoint); AppendPropertyForPoint(Grid,Desc,i); } } else if ( NewValue < OldValue ) { for ( int i=NewValue; i<OldValue; i++ ) { Grid->Delete((Desc->Points[i])->Id); delete Desc->Points[i]; } Desc->Points.RemoveAt(NewValue,OldValue-NewValue); } Changed = true; } // (5) if ( !Changed || Global ) { for ( int i=0; i<(int)Desc->Points.Count(); i++ ) { if ( HandleChangeInPoint(Grid,Id,Desc,i,Global) ) { Changed = true; // (6) if ( !Global ) break; } } } if ( Changed ) { // (7) NotifyPropertyChange(true); return true; } return false; }
That function may require some explanation. It returns true when any property in this chart points set has changed and false if not. At the beginning we extract right description (1) and check if id of changed property matches id of global parent property for whole set. If yes, we set Global flag to true which says that we will update all properties in set. This may be not necessary in new property grid versions but it's better to handle it this way to prevent any possible bugs and data looses. Next we test individual properties for change (2), (3), and monitor number of points property in same way as we did react to changes of number of point sets (4). Last thing is to forward information about property change into point data (5) (we do this only when there was no change so far or if we update whole set). Note here that we don't stop processing points in loop only when there's no global flag set and some point reported that it's content changed (6). This have to be done this way because we have to be sure that all properties including points are updated when Global flag is set.
Those two functions introduced two more functions which operate at point level. First one is AppendProperyForPoint and the second one is HandleChangeInPoint. Implementation of these functions will be quite simple:
void wxsChart::AppendPropertyForPoint(wxsPropertyGridManager* Grid,ChartPointsDesc* SetDesc,int Position) { PointDesc* Desc = SetDesc->Points[Position]; wxString Name = wxString::Format(_("Point %d"),Position+1); Desc->Id = Grid->AppendIn(SetDesc->Id,wxParentProperty(Name,wxPG_LABEL)); Desc->NameId = Grid->AppendIn(Desc->Id,wxStringProperty(_("Name"),wxPG_LABEL,Desc->Name)); Desc->XId = Grid->AppendIn(Desc->Id,wxStringProperty(_("X"),wxPG_LABEL,wxString::Format(_T("%lf"),Desc->X))); Desc->YId = Grid->AppendIn(Desc->Id,wxStringProperty(_("Y"),wxPG_LABEL,wxString::Format(_T("%lf"),Desc->Y))); }
We simply add one parent property for point and add one child property for each point's member. Second function has following implementation:
bool wxsChart::HandleChangeInPoint(wxsPropertyGridManager* Grid,wxPGId Id,ChartPointsDesc* SetDesc,int Position,bool Global) { PointDesc* Desc = SetDesc->Points[Position]; bool Changed = false; if ( Id == Desc->Id ) Global = true; if ( Global || Id == Desc->NameId ) { Desc->Name = Grid->GetPropertyValueAsString(Desc->NameId); Changed = true; } if ( Global || Id == Desc->XId ) { Grid->GetPropertyValueAsString(Desc->XId).ToDouble(&Desc->X); Changed = true; } if ( Global || Id == Desc->YId ) { Grid->GetPropertyValueAsString(Desc->YId).ToDouble(&Desc->Y); Changed = true; } return Changed; }
We just read properties if they were changed of when global flag is set. Worth mentioning here is that the Global flag propagates from previous function which called it (HandleChangeInSet) and may be updated to true if parentp property of this point was changed.
Now we have full control over charts and their points. But it's not the end now. There are more four things to do: affect preview, affect source code, load from xml and store into xml.
In this tutorial we will cover only xml operations. Affecting preview and source code will be described in next tutorial.
Updating XML structures
wxSmith uses XML strucures to store data of resource in many places. Most obvious one is XRC file, another example is WXS file and the third, hidden one is that wxSmith uses XML format to store entries in undo/redo buffers. So without routines responsible for storing and reading our custom property into xml, we don't have any of those features.
First problem we will solve is to store data into xml. Each chart point set will be stored inside <chartpointset> node and each point of this set will be stored inside <point> node which will be added as child of <chartpointset>. Some example structure may look like this:
<object class="wxChartCtrl" name="ID_CHART1" variable="Chart1" member="no"> <chartpointset name="" type="pie"> <point name="Point 1" x="5.000000" y="6.000000" /> <point name="Point 2" x="7.000000" y="8.000000" /> <point name="Point 3c" x="9.000000" y="10.000000" /> </chartpointset> <chartpointset name="" type="line3d"> <point name="Point 1" x="0.000000" y="0.000000" /> <point name="Point 2" x="10.000000" y="10.000000" /> </chartpointset> </object>
So let's write function which will store data into xml:
bool wxsChart::OnXmlWrite(TiXmlElement* Element,bool IsXRC,bool IsExtra) { for ( size_t i=0; i<m_ChartPointsDesc.Count(); i++ ) { ChartPointsDesc* Desc = m_ChartPointsDesc[i]; TiXmlElement* DescElem = Element->InsertEndChild(TiXmlElement("chartpointset")) ->ToElement(); DescElem->SetAttribute("name",cbU2C(Desc->Name)); switch ( Desc->Type ) { case Bar: DescElem->SetAttribute("type","bar"); break; case Bar3D: DescElem->SetAttribute("type","bar3d"); break; case Pie: DescElem->SetAttribute("type","pie"); break; case Pie3D: DescElem->SetAttribute("type","pie3d"); break; case Points: DescElem->SetAttribute("type","points"); break; case Points3D: DescElem->SetAttribute("type","points3d"); break; case Line: DescElem->SetAttribute("type","line"); break; case Line3D: DescElem->SetAttribute("type","line3d"); break; case Area: DescElem->SetAttribute("type","area"); break; case Area3D: DescElem->SetAttribute("type","area3d"); break; } for ( size_t j=0; j<Desc->Points.Count(); j++ ) { PointDesc* PDesc = Desc->Points[j]; TiXmlElement* PointElem = DescElem->InsertEndChild(TiXmlElement("point")) ->ToElement(); PointElem->SetAttribute("name",cbU2C(PDesc->Name)); PointElem->SetDoubleAttribute("x",PDesc->X); PointElem->SetDoubleAttribute("y",PDesc->Y); } } return wxsWidget::OnXmlWrite(Element,IsXRC,IsExtra); }
In this function we iterate through all sets and store xml data for it. Note that TinyXml can not operate on wxStirng nor unicode characters directly. So we have to use cbU2C macro provided by Code::Blocks which will convert unicode string into standard char* (maybe it's not best solution since it should always use wxConvUTF8 because that's default encoding in xml files used by wxSmith).
Now let's write reading function. It's very simillar to writing function:
bool wxsChart::OnXmlRead(TiXmlElement* Element,bool IsXRC,bool IsExtra) { for ( size_t i=0; i<m_ChartPointsDesc.Count(); i++ ) { delete m_ChartPointsDesc[i]; } m_ChartPointsDesc.Clear(); for ( TiXmlElement* DescElem = Element->FirstChildElement("chartpointset"); DescElem; DescElem = DescElem->NextSiblingElement("chartpointset") ) { ChartPointsDesc* Desc = new ChartPointsDesc; Desc->Name = cbC2U(DescElem->Attribute("name")); wxString Type = cbC2U(DescElem->Attribute("type")); if ( Type == _T("bar") ) Desc->Type = Bar; else if ( Type == _T("bar3d") ) Desc->Type = Bar3D; else if ( Type == _T("pie") ) Desc->Type = Pie; else if ( Type == _T("pie3d") ) Desc->Type = Pie3D; else if ( Type == _T("points") ) Desc->Type = Points; else if ( Type == _T("points3d") ) Desc->Type = Points3D; else if ( Type == _T("line") ) Desc->Type = Line; else if ( Type == _T("line3d") ) Desc->Type = Line3D; else if ( Type == _T("area") ) Desc->Type = Area; else if ( Type == _T("area3d") ) Desc->Type = Area3D; else Desc->Type = Bar; for ( TiXmlElement* PointElem = DescElem->FirstChildElement("point"); PointElem; PointElem = PointElem->NextSiblingElement("point") ) { PointDesc* Point = new PointDesc; Point->Name = cbC2U(PointElem->Attribute("name")); (PointElem->QueryDoubleAttribute("x",&Point->X) == TIXML_SUCCESS) || (Point->X = 0.0); (PointElem->QueryDoubleAttribute("y",&Point->Y) == TIXML_SUCCESS) || (Point->Y = 0.0); Desc->Points.Add(Point); } m_ChartPointsDesc.Add(Desc); } return wxsWidget::OnXmlRead(Element,IsXRC,IsExtra); }
In few places we used cbC2U which converts char* string into wxString.
Now if we test new chart, it stores and restores all data from xml structures (the easiest way to test it is to use undo/redo operations since they internally use xml structures). Now we can continue updating of wxChart item by generating better source code and preview. This will be covered in next tutorial.