Adding a control (or sizer) between two other controls (or sizers). Topic is solved
Adding a control (or sizer) between two other controls (or sizers).
I just want to confirm and ask these two things:
1. What's the purpose of SetClientData and GetClientData? Is it my own user data? I couldn't find much information on it. Can it be a wxWidgets object or user data?
2. Can I dynamically add a button between two buttons in a sizer? In the same way, can I dynamically place a sizer between two sizers?
1. What's the purpose of SetClientData and GetClientData? Is it my own user data? I couldn't find much information on it. Can it be a wxWidgets object or user data?
2. Can I dynamically add a button between two buttons in a sizer? In the same way, can I dynamically place a sizer between two sizers?
Re: Adding a control (or sizer) between two other controls (or sizers).
The methods SetClientData/GetClientData are for void pointers. It's your responsibility to destroy them. Set/GetClientObject expect a pointer to a wxClientData and the respective object usually takes ownership (= destroys it). (Yes, the naming of the methods is a little confusing.)What's the purpose of SetClientData and GetClientData? Is it my own user data? I couldn't find much information on it. Can it be a wxWidgets object or user data?
You can use both types for any purpose. Personally i've never used these methods except for container classes, e.g. wxListBox where you can assign custom data to a list item.
Yes. Look at the wxSizer::Insert methods.Can I dynamically add a button between two buttons in a sizer? In the same way, can I dynamically place a sizer between two sizers?
Use the source, Luke!
Re: Adding a control (or sizer) between two other controls (or sizers).
Thanks, doublemax!
If I pass a wxTextCtrl as client data, then it really becomes my responsiblity to destroy the control or wxWidgets will free its memory when program terminates as usual?
If I pass a wxTextCtrl as client data, then it really becomes my responsiblity to destroy the control or wxWidgets will free its memory when program terminates as usual?
Re: Adding a control (or sizer) between two other controls (or sizers).
Client data is stored in wxClientDataContainer, where the documentation says (emphasis mine)
Out of curiosity, where do you want set the user data (which class method)?
In other words, using a wxTextCtrl for client data should not change anything for you and the control will be destroyed as usual when needed by its parent.This data can either be of type void - in which case the data container does not take care of freeing the data again or it is of type wxClientData or its derivatives. In that case the container will free the memory itself later. Note that you must not assign both void data and data derived from the wxClientData class to a container.
Out of curiosity, where do you want set the user data (which class method)?
Re: Adding a control (or sizer) between two other controls (or sizers).
That sounds like a bad idea in any case. I don't know what you're trying to do, but i'm sure there is a better way.If I pass a wxTextCtrl as client data
Use the source, Luke!
Re: Adding a control (or sizer) between two other controls (or sizers).
FWIW, below is an example of code demonstrating one of the ways how to create a relationship between a button and a sizer , using std::map with buttons as keys to sizers (for simplicity sake, the code does not bother setting the correct tab order). I think that applications allowing such flexibility are not very common, aside from form designers and such. If you wish, try to run the code verbatim to see what it does and then look how it is written...
Code: Select all
#include <map>
#include <wx/wx.h>
#include <wx/numdlg.h>
class MyFrame : public wxFrame
{
public:
MyFrame(size_t textCtrlSizerCount) : wxFrame(NULL, wxID_ANY, "Test", wxDefaultPosition, wxSize(800, 600))
{
wxScrolledWindow* scrolledPanel = new wxScrolledWindow(this);
wxBoxSizer* scrolledPanelSizer = new wxBoxSizer(wxVERTICAL);
wxFlexGridSizer* buttonSizer = new wxFlexGridSizer(2);
wxFlexGridSizer* textCtrlSizersSizer = new wxFlexGridSizer(textCtrlSizerCount);
for ( size_t i = 0; i < textCtrlSizerCount; ++i )
{
wxStaticBoxSizer* textCtrlSizer;
wxButton* button;
textCtrlSizer = new wxStaticBoxSizer(wxVERTICAL, scrolledPanel, wxString::Format("Sizer %zu", i + 1));
textCtrlSizersSizer->Add(textCtrlSizer, wxSizerFlags().Expand().Border());
button = new wxButton(scrolledPanel, wxID_ANY, wxString::Format("Add a wxTextCtrl to Sizer %zu", i + 1));
button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnAddTextCtrl, this);
buttonSizer->Add(button, wxSizerFlags().Expand().Border());
button2sizerMap[button] = textCtrlSizer;
button = new wxButton(scrolledPanel, wxID_ANY, wxString::Format("Remove all wxTextCtrls from Sizer %zu", i + 1));
button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnRemoveTextCtrls, this);
button2sizerMap[button] = textCtrlSizer;
buttonSizer->Add(button, wxSizerFlags().Expand().Border());
}
scrolledPanelSizer->Add(buttonSizer, wxSizerFlags().Border());
scrolledPanelSizer->Add(textCtrlSizersSizer, wxSizerFlags().Border());
scrolledPanel->SetSizer(scrolledPanelSizer);
scrolledPanel->FitInside();
scrolledPanel->SetScrollRate(15, 15);
}
private:
std::map<wxButton*, wxStaticBoxSizer*> button2sizerMap;
wxStaticBoxSizer* Button2Sizer(wxButton* button)
{
std::map<wxButton*, wxStaticBoxSizer*>::iterator it = button2sizerMap.find(button);
wxCHECK(it != button2sizerMap.end(), NULL);
return it->second;
}
void OnAddTextCtrl(wxCommandEvent& event)
{
wxStaticBoxSizer* sizer = Button2Sizer(dynamic_cast<wxButton*>(event.GetEventObject()));
sizer->Add(new wxTextCtrl(sizer->GetStaticBox(), wxID_ANY,
wxString::Format("wxTextCtrl %zu", sizer->GetItemCount() + 1)),
wxSizerFlags().Border());
sizer->GetContainingWindow()->FitInside();
}
void OnRemoveTextCtrls(wxCommandEvent& event)
{
wxSizer* sizer = Button2Sizer(dynamic_cast<wxButton*>(event.GetEventObject()));
sizer->Clear(true);
sizer->GetContainingWindow()->FitInside();
}
};
class MyApp : public wxApp
{
public:
bool OnInit()
{
long sizerCount = wxGetNumberFromUser("Enter number of sizers with wxTextCtrls",
"Number (1-10)", "Number of sizers", 5, 1, 10);
if ( sizerCount == -1 )
return false;
(new MyFrame(sizerCount))->Show();
return true;
}
}; wxIMPLEMENT_APP(MyApp);
Re: Adding a control (or sizer) between two other controls (or sizers).
Here's what I am trying to do:
The red boxes are BoxSizers.
So, I click minus button, it should completely the sizer in which controls are placed. That is easy to do as suggested by @PB i.e. by using std::map to maintain button and sizer relationship. So, when the minus button is clicked, I find the sizer and simply delete it. Earlier, I tried to use GetClientData until std::map was suggested. What's the purpose of GetClientData if I have to use a std::map?
But the thing I can't figure out is how to handle the plus button. When it is clicked, sizer with new controls(textfield, minus and plus button) should be added just below that sizer where the click is made.
To achieve that action, I need to maintain "indexes" so I can insert a sizer below that index.
I can get the sizer associated with plus button but how do I get location where the new sizer needs to be added? Just saving 'index' number is not enough because the index order changes as sizers are added and removed continuously. I need the best way to handle it.
The red boxes are BoxSizers.
So, I click minus button, it should completely the sizer in which controls are placed. That is easy to do as suggested by @PB i.e. by using std::map to maintain button and sizer relationship. So, when the minus button is clicked, I find the sizer and simply delete it. Earlier, I tried to use GetClientData until std::map was suggested. What's the purpose of GetClientData if I have to use a std::map?
But the thing I can't figure out is how to handle the plus button. When it is clicked, sizer with new controls(textfield, minus and plus button) should be added just below that sizer where the click is made.
To achieve that action, I need to maintain "indexes" so I can insert a sizer below that index.
I can get the sizer associated with plus button but how do I get location where the new sizer needs to be added? Just saving 'index' number is not enough because the index order changes as sizers are added and removed continuously. I need the best way to handle it.
Re: Adding a control (or sizer) between two other controls (or sizers).
You certainly do not need to use a map, it was just a generic example of relating a button to a sizer when I knew nothing about your code. However, I would not use client data, I believe they are not well suited for this.purplex88 wrote:What's the purpose of GetClientData if I have to use a std::map?
Here is an example of another solution, relying on indices and sizer ownership, does basically what you needed to do
Code: Select all
#include <wx/wx.h>
class MyFrame : public wxFrame
{
public:
MyFrame() : wxFrame(NULL, wxID_ANY, "Test")
{
m_scrolledPanel = new wxScrolledWindow(this);
wxBoxSizer* m_scrolledPanelSizer = new wxBoxSizer(wxVERTICAL);
m_display = new wxTextCtrl(m_scrolledPanel, wxID_ANY);
m_scrolledPanelSizer->Add(m_display, wxSizerFlags().Expand().Border());
m_controlTripletSizersSizer = new wxBoxSizer(wxVERTICAL);
m_scrolledPanelSizer->Add(m_controlTripletSizersSizer, wxSizerFlags().Expand().Border());
AddControlTriplet(NULL);
m_scrolledPanel->SetSizer(m_scrolledPanelSizer);
m_scrolledPanel->FitInside();
m_scrolledPanel->SetScrollRate(15, 15);
}
private:
wxScrolledWindow* m_scrolledPanel;
wxTextCtrl* m_display;
wxBoxSizer* m_controlTripletSizersSizer; // contains sizers with the three controls
void AddControlTriplet(wxSizer* afterSizer)
{
static size_t tripletCount = 1; // for debugging, to identify a triplet
// Create a control triplet
wxTextCtrl* textCtrl = new wxTextCtrl(m_scrolledPanel, wxID_ANY, wxString::Format("Triplet Id: %zu", tripletCount++));
wxButton* addTripletButton = new wxButton(m_scrolledPanel, wxID_ANY, "+");
wxButton* removeTripletButton = new wxButton(m_scrolledPanel, wxID_ANY, "-");
addTripletButton->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnAddControlTriplet, this);
removeTripletButton->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnRemoveControlTriplet, this);
wxFlexGridSizer* controlTripletSizer = new wxFlexGridSizer(3);
controlTripletSizer->Add(textCtrl, wxSizerFlags().Proportion(1).Expand());
controlTripletSizer->Add(addTripletButton);
controlTripletSizer->Add(removeTripletButton);
// insert into m_controlTripletSizersSizer
size_t insertIndex = 0;
if ( afterSizer )
{
// find the index of afterSizer in its parent sizer
for ( size_t i = 0; i < m_controlTripletSizersSizer->GetItemCount(); i++ )
{
if ( m_controlTripletSizersSizer->GetItem(i)->GetSizer() == afterSizer )
{
insertIndex = i + 1;
break;
}
}
}
m_controlTripletSizersSizer->Insert(insertIndex, controlTripletSizer, wxSizerFlags().Expand().Border());
// Adjust the tab order
const size_t removeTripletButtonIndexInSizer = 2; // 0 = textCtrl, 1 = addTripletButton
wxWindow* tabAfter = NULL;
if ( afterSizer )
tabAfter = afterSizer->GetItem(removeTripletButtonIndexInSizer)->GetWindow();
else
tabAfter = m_display;
textCtrl->MoveAfterInTabOrder(tabAfter);
addTripletButton->MoveAfterInTabOrder(textCtrl);
removeTripletButton->MoveAfterInTabOrder(addTripletButton);
}
void OnAddControlTriplet(wxCommandEvent& event)
{
wxWindow* eventWindow = dynamic_cast<wxWindow*>(event.GetEventObject());
AddControlTriplet(eventWindow->GetContainingSizer());
m_scrolledPanel->FitInside();
}
void OnRemoveControlTriplet(wxCommandEvent& event)
{
if ( m_controlTripletSizersSizer->GetItemCount() == 1 )
{
wxLogMessage("Cannot remove the only control triplet.");
return;
}
wxButton* eventButton = dynamic_cast<wxButton*>(event.GetEventObject());
wxSizer* controlTripletSizer = eventButton->GetContainingSizer();
controlTripletSizer->Clear(true);
m_controlTripletSizersSizer->Remove(controlTripletSizer);
m_scrolledPanel->FitInside();
}
};
class MyApp : public wxApp
{
public:
bool OnInit()
{
(new MyFrame())->Show();
return true;
}
}; wxIMPLEMENT_APP(MyApp);
EDIT
I have added another, perhaps more realistic and even maybe simpler, example of a bit different approach. The code above uses individual wxFlexGridSizers for each control triplet (1 wxTextCtrl + 2 wxButtons) stored in a vertical wxBoxSizer. The code below instead uses a single wxFlexGridSizer
Code: Select all
#include <wx/wx.h>
class MyFrame : public wxFrame
{
public:
MyFrame() : wxFrame(NULL, wxID_ANY, "Test")
{
m_scrolledPanel = new wxScrolledWindow(this);
wxBoxSizer* m_scrolledPanelSizer = new wxBoxSizer(wxVERTICAL);
m_display = new wxTextCtrl(m_scrolledPanel, wxID_ANY);
m_scrolledPanelSizer->Add(m_display, wxSizerFlags().Expand().Border());
m_controlTripletsSizer = new wxFlexGridSizer(3);
m_controlTripletsSizer->AddGrowableCol(0, 3);
m_scrolledPanelSizer->Add(m_controlTripletsSizer, wxSizerFlags().Expand().Border());
AddControlTriplet(NULL);
m_scrolledPanel->SetSizer(m_scrolledPanelSizer);
m_scrolledPanel->FitInside();
m_scrolledPanel->SetScrollRate(15, 15);
}
private:
wxScrolledWindow* m_scrolledPanel;
wxTextCtrl* m_display;
wxFlexGridSizer* m_controlTripletsSizer;
// eventAddButton is the button that generated the add event
// the new control triplet will be added after the triplet
// which contains this button.
void AddControlTriplet(wxWindow* eventAddButton)
{
static size_t tripletCount = 1; // for debugging, to identify a triplet
// Create a control triplet
wxTextCtrl* textCtrl = new wxTextCtrl(m_scrolledPanel, wxID_ANY, wxString::Format("Triplet Id: %zu", tripletCount++));
wxButton* addTripletButton = new wxButton(m_scrolledPanel, wxID_ANY, "+");
wxButton* removeTripletButton = new wxButton(m_scrolledPanel, wxID_ANY, "-");
addTripletButton->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnAddControlTriplet, this);
removeTripletButton->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnRemoveControlTriplet, this);
// insert into m_controlTripletsSizer
wxWindow* eventRemoveButton = NULL;
long insertIndex = 0;
if ( eventAddButton )
{
eventRemoveButton = dynamic_cast<wxButton*>(eventAddButton->GetNextSibling());
const long eventRemoveButtonIdx = GetIndexOfWindowInSizer(m_controlTripletsSizer, eventRemoveButton);
wxASSERT(eventRemoveButtonIdx != wxNOT_FOUND);
insertIndex = eventRemoveButtonIdx + 1;
}
m_controlTripletsSizer->Insert(insertIndex++, textCtrl, wxSizerFlags().Proportion(1).Expand());
m_controlTripletsSizer->Insert(insertIndex++, addTripletButton);
m_controlTripletsSizer->Insert(insertIndex++, removeTripletButton);
// Adjust tab order
wxWindow* tabAfter = NULL;
if ( eventRemoveButton )
tabAfter = eventRemoveButton;
else
tabAfter = m_display;
textCtrl->MoveAfterInTabOrder(tabAfter);
addTripletButton->MoveAfterInTabOrder(textCtrl);
removeTripletButton->MoveAfterInTabOrder(addTripletButton);
}
void OnAddControlTriplet(wxCommandEvent& event)
{
AddControlTriplet(dynamic_cast<wxButton*>(event.GetEventObject()));
m_scrolledPanel->FitInside();
}
// Removes the control triplet which contains the remove button
// which generated the remove event.
void OnRemoveControlTriplet(wxCommandEvent& event)
{
if ( m_controlTripletsSizer->GetEffectiveRowsCount() == 1 )
{
wxLogMessage("Cannot remove the only control triplet.");
return;
}
// Delete the triplet
wxButton* removeButton = dynamic_cast<wxButton*>(event.GetEventObject());
wxWindow* addButton = removeButton->GetPrevSibling();
wxWindow* textCtrl = addButton->GetPrevSibling();
textCtrl->Destroy();
addButton->Destroy();
removeButton->Destroy();
m_scrolledPanel->FitInside();
}
static long GetIndexOfWindowInSizer(wxSizer* sizer, const wxWindow* window)
{
for ( size_t i = 0; i < sizer->GetItemCount(); ++i )
{
const wxSizerItem* sizerItem = sizer->GetItem(i);
if ( sizerItem->IsWindow() && sizerItem->GetWindow() == window )
return i;
}
return wxNOT_FOUND;
}
};
class MyApp : public wxApp
{
public:
bool OnInit()
{
(new MyFrame())->Show();
return true;
}
}; wxIMPLEMENT_APP(MyApp);
Last edited by PB on Tue Jan 08, 2019 9:08 pm, edited 2 times in total.
Re: Adding a control (or sizer) between two other controls (or sizers).
Here's what i would do:
First of all, create a new class deriving from wxPanel that wraps the textcontrol and the two buttons.
When clicking "-" you just destroy it. Nothing else needed.
For the "+":
Strangely enough there seems to be no method in wxSizer that returns the index of an item in the sizer. So i'd write a small helper function that takes a wxSizer* and a wxWindow* as parameter and returns the index of that window inside the sizer (or wxNOT_FOUND). You can wxSizer::GetItem(size_t index) and loop over all items until you found the window.
Then when clicking "+", you find the index of the clicked window inside the sizer and use that (+1) with wxSizer::Insert to insert a new instance after the current one. This should not happen inside the newly created class, but somewhere higher up in the hierarchy. Ideally by sending an event upwards. But depending on your purpose that might be overkill.
BTW: Is it really necessary to insert a new item behind the current one? Usually you'd have only one "+" button to add a new entry at the end.
First of all, create a new class deriving from wxPanel that wraps the textcontrol and the two buttons.
When clicking "-" you just destroy it. Nothing else needed.
For the "+":
Strangely enough there seems to be no method in wxSizer that returns the index of an item in the sizer. So i'd write a small helper function that takes a wxSizer* and a wxWindow* as parameter and returns the index of that window inside the sizer (or wxNOT_FOUND). You can wxSizer::GetItem(size_t index) and loop over all items until you found the window.
Then when clicking "+", you find the index of the clicked window inside the sizer and use that (+1) with wxSizer::Insert to insert a new instance after the current one. This should not happen inside the newly created class, but somewhere higher up in the hierarchy. Ideally by sending an event upwards. But depending on your purpose that might be overkill.
BTW: Is it really necessary to insert a new item behind the current one? Usually you'd have only one "+" button to add a new entry at the end.
Use the source, Luke!
Re: Adding a control (or sizer) between two other controls (or sizers).
Thanks. It is indeed very simple to do that with wxFlexGridSizer. On the top of that, no data structures like std::map were required.
I combined the code with suggestions given by @doublemax as well and used a wxPanel.
Yes, actually I admit that it is an overkill for such small dialog to add '+' buttons everywhere but I guess that's how I imagined and wanted it.
I combined the code with suggestions given by @doublemax as well and used a wxPanel.
Yes, actually I admit that it is an overkill for such small dialog to add '+' buttons everywhere but I guess that's how I imagined and wanted it.