Search code examples
pythonc++wxpythonwxwidgets

wxpython Segmentation Fault when deleting item from ListCtrl


Whenever the program reach self.DeleteItem it crashes and print Segmentation Fault (Core Dumped). After removing self.DeleteItem it does not crash anymore but we lose the ability to remove a row. I am not sure what is wrong with this as it is only one line that is broken. Can you help me see if anything is wrong?

Code

import wx
from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
from scrapy.crawler import CrawlerProcess

from spider.spider import WIPOSpider
from settings import settings
from processor import process_xls

class MainFrame(wx.Frame):

    TITLE = 'Neko'
    FRAME_MIN_SIZE = (820, 220)
    DEFAULT_BORDER_SIZE = 10

    ADD_LABEL = 'Add'
    REMOVE_LABEL = 'Remove'
    START_LABEL = 'Start'

    FILE_LABEL = 'File'
    SIZE_LABEL = 'Size'
    PERCENT_LABEL = 'Percent'
    ETA_LABEL = 'ETA'
    SPEED_LABEL = 'Speed'
    STATUS_LABEL = 'Status'
    STATUS_LIST = {
        'filename': (0, FILE_LABEL, 300, False),
        'size': (1, SIZE_LABEL, 80, False),
        'percent': (2, PERCENT_LABEL, 80, False),
        'eta': (3, ETA_LABEL, 80, False),
        'speed': (4, SPEED_LABEL, 80, False),
        'status': (5, STATUS_LABEL, 80, False),
    }

    def __init__(self, *args, **kwargs):
        super(MainFrame, self).__init__(*args, **kwargs)

        self.index = 0

        self.SetTitle(self.TITLE)
        self.SetSize(self.FRAME_MIN_SIZE)

        self._panel = wx.Panel(self)
        self._vertical_box = wx.BoxSizer(wx.VERTICAL)

        # Status List
        self._status_list = ListCtrl(self.STATUS_LIST,
                                parent=self._panel,
                                style=wx.LC_REPORT | wx.LEFT | wx.RIGHT)
        self._horizontal_box_status_list = wx.BoxSizer(wx.HORIZONTAL)
        self._horizontal_box_status_list.Add(self._status_list,
                                        proportion=1, flag=wx.EXPAND)

        status_list_buttons_data = {
            ('Add', self.ADD_LABEL, (-1, -1), self._on_add, wx.Button),
            ('remove', self.REMOVE_LABEL, (-1, -1), self._on_remove, wx.Button),
            ('start', self.START_LABEL, (-1, -1), self._on_start, wx.Button),
        }

        # Create buttons vertically
        self._vertical_control_box = wx.BoxSizer(wx.VERTICAL)
        for item in status_list_buttons_data:
            name, label, size, evt_handler, parent = item

            button = parent(self._panel, size=size)

            if parent == wx.Button:
                button.SetLabel(label)

            if evt_handler is not None:
                button.Bind(wx.EVT_BUTTON, evt_handler)

            self._vertical_control_box.Add(button,
                                        flag=wx.LEFT|wx.BOTTOM|wx.EXPAND,
                                        proportion=1,
                                        border=self.DEFAULT_BORDER_SIZE)

        self._horizontal_box_status_list.Add(self._vertical_control_box)
        self._vertical_box.Add(self._horizontal_box_status_list,
                            flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.TOP,
                            border=self.DEFAULT_BORDER_SIZE)

        # Set Sizer
        self._panel.SetSizerAndFit(self._vertical_box)

    def _on_add(self, event):
        file_dialog = wx.FileDialog(self)
        file_dialog.Show()

        if file_dialog.ShowModal() == wx.ID_OK:
            file_path = file_dialog.GetPath()
            self._status_list.InsertItem(self.index, file_path)

            self.index += 1

    def _on_remove(self, event):
        selected = self._status_list.get_selected()

        if selected:
            self._status_list.remove_row(selected)

    def _on_start(self, event):
        for item_count in range(0, self._status_list.GetItemCount()):
            item = self._status_list.GetItem(item_count, col=0)
            data = process_xls(item.GetText())

            # Start spider
            if data:
                process = CrawlerProcess(settings['BOT_SETTINGS'])
                process.crawl(WIPOSpider(data))
                process.start()

# Copied from
# https://github.com/MrS0m30n3/youtube-dl-gui/blob/57eb51ccc8e2df4e8c766b31d677513adb5c86cb/youtube_dl_gui/mainframe.py#L1095
class ListCtrl(wx.ListCtrl, ListCtrlAutoWidthMixin):

    """Custom ListCtrl widget.
    Args:
        columns (dict): See MainFrame class STATUSLIST_COLUMNS attribute.
    """

    def __init__(self, columns, *args, **kwargs):
        super(ListCtrl, self).__init__(*args, **kwargs)
        ListCtrlAutoWidthMixin.__init__(self)
        self.columns = columns
        self._list_index = 0
        self._set_columns()

    def remove_row(self, row_number):
        self.DeleteItem(row_number)
        self._list_index -= 1

    def get_selected(self):
        return self.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)

    def _set_columns(self):
        """Initializes ListCtrl columns.
        See MainFrame STATUSLIST_COLUMNS attribute for more info. """
        for column_item in sorted(self.columns.values()):
            self.InsertColumn(column_item[0], column_item[1], width=wx.LIST_AUTOSIZE_USEHEADER)

            # If the column width obtained from wxLIST_AUTOSIZE_USEHEADER
            # is smaller than the minimum allowed column width
            # then set the column width to the minumum allowed size
            if self.GetColumnWidth(column_item[0]) < column_item[2]:
                self.SetColumnWidth(column_item[0], column_item[2])

            # Set auto-resize if enabled
            if column_item[3]:
                self.setResizeColumn(column_item[0])

Solution

  • Clearly, there is an issue either with your sub-classing of ListCtrl or in ListCtrlAutoWidthMixin.
    If you drop your sub-class and use a standard ListCtrl, the code works.
    i.e.

    self._status_list = wx.ListCtrl(self, -1,
                            style=wx.LC_REPORT)
    for column_item in self.STATUS_LIST.values():
        self._status_list.InsertColumn(column_item[0], column_item[1], width=column_item[2])
    

    Then the function _on_remove becomes:

    def _on_remove(self, event):
        selected = self._status_list.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
        if selected >= 0:
            self._status_list.DeleteItem(selected)
    

    Note: you are currently defining status_list_buttons_data as a set, your buttons would display more consistently if this was a list. I believe that sets are un-ordered.