WxSmith tutorial: Adding advanced properties into items
WARNING: NOT YET FINISHED
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 them:
Fully customizable properties
To create fully costomizable property we hve 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 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 ading properties (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 of wxChartPoints classes, probbly some structures describing points inside them and wxPGId values for each wxChartPoints class. Let's create some internal structure inside wxsChart class to handle this work:
struct ChartPointsDesc { wxPGId Id; }; WX_DEFINE_ARRAY(ChartPointDesc*,List); long m_Flags; List m_ChartPointsDesc; wxPGId m_ChartPointsCountId; };
Currently each chart points set is described only by property id, but it will be extended in future. Another property identifier is used for count of wxChartPoints classes and we will add this property and all the dynamics it gives in our custom 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) { Grid->SetTargetPage(0); m_ChartPointsCountId = Grid->Append(wxIntProperty(_("Number of data sets"),wxPG_LABEL, (int)m_ChartPointsDesc.Count())); for ( int i=0; i<(int)m_ChartPointsDesc.Count(); i++ ) { AppendPropertyForSet(Grid,i); } wxsWidget::OnAddExtraProperties(Grid); }
Note that we have to set our target page to initial one because it may have been switched to events (page number 1). We also use AppendPropertyForSet function which is not defined yet.
Now let's handle changes done in current properties:
void wxsChart::OnExtraPropertyChanged(wxsPropertyGridManager* Grid,wxPGId Id) { Grid->SetTargetPage(0); if ( Id == m_ChartPointsCountId ) { int OldValue = (int)m_ChartPointsDesc.Count(); int NewValue = Grid->GetPropertyValueAsInt(Id); if ( NewValue<0 ) { NewValue = 0; Grid->SetPropertyValue(Id,NewValue); } if ( NewValue > OldValue ) { // 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 ) { // 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); } NotifyPropertyChange(true); return; } 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. Note that we have to delete enteries in m_ChartPointsDesc manually because it's not object array (that would be created using WX_DEFINE_OBJARRAY). Because of that we also have to update desctuctor to avoid memory leaks:
wxsChart::~wxsChart() { for ( size_t i=0; i<m_ChartPointsDesc.Count(); i++ ) { delete m_ChartPointsDesc[i]; } m_ChartPointsDesc.Clear(); }
Now let's implement missing functions: AppendPropertyForSet and HandleChangeInSet. First one adds properties for one set, second one keeps set updated on property changes. But before we do that, let's 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's data and put it into chart data:
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); Desc->Id = Grid->Append(wxParentProperty(SetName,wxPG_LABEL)); 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 }; 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())); 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, put some basic properties there and add properties for points which are part of this set. It may look little bit complicated, but it is really easy indeed and require knowledge about wxPropertyGrid only.
Now let's create function which will react on data updates to properties inside set:
bool wxsChart::HandleChangeInSet(wxsPropertyGridManager* Grid,wxPGId Id,int Position) { ChartPointsDesc* Desc = m_ChartPointsDesc[Position]; bool Changed = false; bool Global = Id==Desc->Id; if ( Global || Id == Desc->TypeId ) { Desc->Type = (PointsType)Grid->GetPropertyValueAsInt(Desc->TypeId); Changed = true; } if ( Global || Id == Desc->NameId ) { Desc->Name = Grid->GetPropertyValueAsString(Desc->NameId); Changed = true; } 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; } if ( !Changed ) { for ( int i=0; i<(int)Desc->Points.Count(); i++ ) { if ( HandleChangeInPoint(Grid,Id,Desc,i,Global) ) { Changed = true; if ( !Global ) break; } } } if ( Changed ) { NotifyPropertyChange(true); return true; } return false; }
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 truw 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. When number of points changes, we have to additinaly add or remove extra points which is simillar to situation in number of sets. At the end we call function updating all points.
Now we have two more functions which operate on point level (don't worry, these two will be last ones). Firts 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))); }
and the second one:
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; }
And now we have full control over charts and it's 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.