Adding a control (or sizer) between two other controls (or sizers). Topic is solved

If you are using the main C++ distribution of wxWidgets, Feel free to ask any question related to wxWidgets development here. This means questions regarding to C++ and wxWidgets, not compile problems.
Post Reply
purplex88
Filthy Rich wx Solver
Filthy Rich wx Solver
Posts: 235
Joined: Mon Feb 24, 2014 3:14 pm

Adding a control (or sizer) between two other controls (or sizers).

Post by purplex88 » Tue Jan 08, 2019 6:48 am

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?

User avatar
doublemax
Moderator
Moderator
Posts: 14314
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: Adding a control (or sizer) between two other controls (or sizers).

Post by doublemax » Tue Jan 08, 2019 7:03 am

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?
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.)

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.
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?
Yes. Look at the wxSizer::Insert methods.
Use the source, Luke!

purplex88
Filthy Rich wx Solver
Filthy Rich wx Solver
Posts: 235
Joined: Mon Feb 24, 2014 3:14 pm

Re: Adding a control (or sizer) between two other controls (or sizers).

Post by purplex88 » Tue Jan 08, 2019 10:29 am

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?

PB
Part Of The Furniture
Part Of The Furniture
Posts: 2094
Joined: Sun Jan 03, 2010 5:45 pm

Re: Adding a control (or sizer) between two other controls (or sizers).

Post by PB » Tue Jan 08, 2019 12:05 pm

Client data is stored in wxClientDataContainer, where the documentation says (emphasis mine)
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.
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.

Out of curiosity, where do you want set the user data (which class method)?

User avatar
doublemax
Moderator
Moderator
Posts: 14314
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: Adding a control (or sizer) between two other controls (or sizers).

Post by doublemax » Tue Jan 08, 2019 12:22 pm

If I pass a wxTextCtrl as client data
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.
Use the source, Luke!

PB
Part Of The Furniture
Part Of The Furniture
Posts: 2094
Joined: Sun Jan 03, 2010 5:45 pm

Re: Adding a control (or sizer) between two other controls (or sizers).

Post by PB » Tue Jan 08, 2019 1:35 pm

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);

purplex88
Filthy Rich wx Solver
Filthy Rich wx Solver
Posts: 235
Joined: Mon Feb 24, 2014 3:14 pm

Re: Adding a control (or sizer) between two other controls (or sizers).

Post by purplex88 » Tue Jan 08, 2019 3:48 pm

Here's what I am trying to do:
sizer.png
sizer.png (49.82 KiB) Viewed 594 times
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.

PB
Part Of The Furniture
Part Of The Furniture
Posts: 2094
Joined: Sun Jan 03, 2010 5:45 pm

Re: Adding a control (or sizer) between two other controls (or sizers).

Post by PB » Tue Jan 08, 2019 5:43 pm

purplex88 wrote:What's the purpose of GetClientData if I have to use a std::map?
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.

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);
But as I said, that is just one of possible ways how to approach the problem, I am not saying it is the best one, just what came to my mind ATM...

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);
But as doublemax below writes, in a real application you may be better of encapsulating the triplet in a separate class.
Last edited by PB on Tue Jan 08, 2019 9:08 pm, edited 2 times in total.

User avatar
doublemax
Moderator
Moderator
Posts: 14314
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: Adding a control (or sizer) between two other controls (or sizers).

Post by doublemax » Tue Jan 08, 2019 7:43 pm

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.
Use the source, Luke!

purplex88
Filthy Rich wx Solver
Filthy Rich wx Solver
Posts: 235
Joined: Mon Feb 24, 2014 3:14 pm

Re: Adding a control (or sizer) between two other controls (or sizers).

Post by purplex88 » Wed Jan 09, 2019 10:13 pm

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.

Post Reply