POSTS
wxWidgets - Removing Things From Sizers
This is the first time I’ve attempted a technical write-up of something I debugged. I want to get better at doing this because it’s important to be able to effectively communicate a process and solution, especially when other people may be depending on your explanation. I think next time, I might keep some notes on the side as to what I did instead of recalling all from memory, and probably preserve all of the testing code I write as I debug as well so that I can give more visual examples.
Not much meat here, but it is something I blew almost a whole day on. I’m using wxWidgets for a project I’ve been working on for some friends. I have a couple sections where items are displayed in a scrollable list. These items can be added, edited, and deleted.
I ran into an issue when trying to delete items - deleted items would not be removed from the screen, and attempting to delete again threw a segfault. The fact that it was segfaulting suggested that something was in fact being deleted, it just was either not what I wanted to be deleted or it was what I wanted and the GUI just wasn’t updating.
I have these items provide their own widget by returning a sizer that the main window sticks into the scrolling window. The items also return the id of their delete button. This and a pointer to the sizer are stored in a map with the delete button id as key, and when the delete is clicked I get the sizer from the map. This is then removed from the scrolling window’s sizer as well as the map and the source item is deleted.
So, somewhere in that deletion process something was going wrong. I put together some test code separate from the main application that did basically the same thing, and started poking around in the docs.
First, I verified some basic stuff. In my test code, I verified that the sizer and item were in fact being deleted. May seem unnecessary but when troubleshooting I like to experimentally verify that things are operating the way I think they are. These were tested individually, and it’s important to note here that in this case I did not attempt to delete the item again after clicking the delete button - I was just dumping state to std out after the button click and I never triggered the segfault.
The deletions were occurring properly. I then also verified that a second click with all deletions enabled did in fact cause the segfault, just to validate that this code in isolation produced the same result as the code in the context of the whole application. Reassured that what I was trying to fix was in fact broken, I took a look at how I was removing the sizer from the display. This was the only component that had not yet been verified as working, therefore it unequivocally had to be the culprit. I was able to verify that the sizer was indeed being removed, but the scroller showed no decrease in the number of children - somehow, the sizer was vanishing but the windows it held were not.
Digging through the docs, I discovered that sizers and windows do not behave the same when removed. When you remove a sizer from a sizer, any windows in the removed sizer do not get automatically destroyed. You have to tell the sizer to destroy the windows it holds, like so:
void TestingScroll::deleteSizer(wxEvent& evt){
m_sizerRemove->DeleteWindows();
m_scrollSizer->Remove(m_sizerRemove);
}
It is important to note that a removed sizer DOES in fact get destroyed - so if you want to get rid of the windows in the sizer, you have to call DeleteWindows()
on the sizer that you are removing, THEN remove the sizer.
When you are removing a window from a sizer, you have to use Detach()
- this is because the overload Remove(wxWindow * window)
is deprecated due to the differing behaviour - the window is not destroyed, since it is owned by its parent and not the sizer. You must detach the window and then manually destroy it - its children will automatically be destroyed as well.
So, the final solution: The sizer was properly being removed and destroyed, but because the windows inside it were never destroyed they remained visible and continued to emit events. Thus, the delete button remained clickable and the second click would attempt to destroy a sizer that was already destroyed.
Below is the full testing code I used to demonstrate this:
#include "./TestingScroll.h"
TestingScroll::TestingScroll(): wxFrame(NULL, wxID_ANY, "foo"){
m_rootPanel = new wxPanel(this);
m_rootSizer = new wxBoxSizer(wxVERTICAL);
scrollWin = new wxScrolledWindow(m_rootPanel, wxID_ANY, wxDefaultPosition, wxSize(300,100), wxVSCROLL);
m_scrollSizer = new wxBoxSizer(wxVERTICAL);
scrollWin->EnableScrolling(false, true);
m_sizerRemove = new wxBoxSizer(wxVERTICAL);
wxStaticText * sizertext = new wxStaticText(scrollWin, wxID_ANY, "added to scroller");
m_sizerRemove->Add(sizertext);
wxButton * deleteSizerButton = new wxButton(m_rootPanel, wxID_ANY, "Delete Sizer");
m_scrollSizer->Add(m_sizerRemove);
m_rootSizer->Add(scrollWin);
m_rootSizer->Add(deleteSizerButton, 0, wxALL, 10);
m_rootPanel->SetSizerAndFit(m_rootSizer);
scrollWin->SetSizerAndFit(m_scrollSizer);
m_rootPanel->SetSizerAndFit(m_rootSizer);
scrollWin->FitInside();
scrollWin->SetScrollRate(0,5);
m_rootPanel->Show();
Bind(wxEVT_BUTTON, &TestingScroll::deleteSizer, this, deleteSizerButton->GetId());
}
void TestingScroll::deleteSizer(wxEvent& evt){
m_sizerRemove->DeleteWindows();
m_scrollSizer->Remove(m_sizerRemove);
}
Yes, using new is discouraged, but for some reason, using anything other than raw pointers results in nothing being displayed. There are clearly some holes in my knowledge - even though I’ve been writing C++ for some time, I only recently started digging into the modern features. The issue is probably related to ownership or scope issues, but I will be doing some digging into this because I very much like what shared_ptr and unique_ptr can do compared to raw.