Phoenix icon indicating copy to clipboard operation
Phoenix copied to clipboard

GenericCalendarCtrl crashes with "double free or corruption (fasttop)"

Open reticulatus opened this issue 3 years ago • 8 comments

Operating system: Linux Mint 20.3 wxPython version & source: wxPython 4.1.1 gtk3 (phoenix) wxWidgets 3.1.5 (built from https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-20.04 using pip). Python version & source: Python 3.8.10 from Linux Mint distribution.

Description of the problem: I have been experimenting with highlighting certain days in a GenericCalendarCtrl.

My code creates a CalendarDateAttr with a bold font and applies it to the required days in calendar control.

If I create the CalendarDateAttr as an instance attribute, the program crashes with "double free or corruption (fasttop)" when you change the month or year in the calendar control or just exit the program (see example generic_calendar_exp_2.py below).

I have modified that program so that a new CalendarDateAttr is created just before being applied to the required days. This version of the program (see generic_calendar_exp_3.py) doesn't crash.

Is the CalendarDateAttr being deleted prematurely?

Code Example (click to expand)
# generic_calendar_exp_2.py
# This program crashes when you change month or year, or just exit.

import wx
import wx.adv
import calendar

special_dates = {
    (2022, 1) : (2,5,7,11,30),
    (2022, 2) : (6,12,13,20,27),
    (2022, 3) : (1,27)
}

class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.SetSize((400, 300))
        self.SetTitle("Calendar Test")

        self.panel_1 = wx.Panel(self, wx.ID_ANY)
        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        cal_style = wx.adv.CAL_MONDAY_FIRST | wx.adv.CAL_SHOW_SURROUNDING_WEEKS
        self.calendar_ctrl = wx.adv.GenericCalendarCtrl(self.panel_1, wx.ID_ANY, style=cal_style)
        self.calendar_ctrl.SetMinSize((216, 166))
        sizer_1.Add(self.calendar_ctrl, 0, wx.ALL, 8)
        self.panel_1.SetSizer(sizer_1)
        self.Layout()

        self.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.OnCalChanged, self.calendar_ctrl)

        attr = self.calendar_ctrl.GetClassDefaultAttributes()
        bold_font = attr.font.Bold()
        self.highlight_attr = wx.adv.CalendarDateAttr(font=bold_font)

        self.highlightDates()


    def highlightDates(self):
        date = self.calendar_ctrl.GetDate()
        year = date.year
        month = date.month + 1
        last_day = calendar.monthrange(year, month)[1]
        bold_days = special_dates.get((year, month), ())
        print("Bold days : ", bold_days)

        for d in range(1, last_day+1):
            attr = self.calendar_ctrl.GetAttr(d)
            if d in bold_days:
                if attr != self.highlight_attr:
                    self.calendar_ctrl.SetAttr(d, self.highlight_attr)

            else:
                if attr is not None:
                    self.calendar_ctrl.ResetAttr(d)

        self.calendar_ctrl.Refresh()


    def OnCalChanged(self, event):
        date = event.GetDate()
        print("Date changed :", date.Format("%d/%m/%Y"))
        self.highlightDates()


class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None, wx.ID_ANY, "")
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True


if __name__ == "__main__":
    app = MyApp(0)
    app.MainLoop()

# generic_calendar_exp_3.py
# This program doesn't crash when you change month or year, or exit

import wx
import wx.adv
import calendar

special_dates = {
    (2022, 1) : (2,5,7,11,30),
    (2022, 2) : (6,12,13,20,27),
    (2022, 3) : (1,27)
}

class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.SetSize((400, 300))
        self.SetTitle("Calendar Test")

        self.panel_1 = wx.Panel(self, wx.ID_ANY)
        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        cal_style = wx.adv.CAL_MONDAY_FIRST | wx.adv.CAL_SHOW_SURROUNDING_WEEKS
        self.calendar_ctrl = wx.adv.GenericCalendarCtrl(self.panel_1, wx.ID_ANY, style=cal_style)
        self.calendar_ctrl.SetMinSize((216, 166))
        sizer_1.Add(self.calendar_ctrl, 0, wx.ALL, 8)
        self.panel_1.SetSizer(sizer_1)
        self.Layout()

        self.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.OnCalChanged, self.calendar_ctrl)

        attr = self.calendar_ctrl.GetClassDefaultAttributes()
        self.bold_font = attr.font.Bold()

        self.highlightDates()


    def highlightDates(self):
        date = self.calendar_ctrl.GetDate()
        year = date.year
        month = date.month + 1
        last_day = calendar.monthrange(year, month)[1]
        bold_days = special_dates.get((year, month), ())
        print("Bold days : ", bold_days)

        for d in range(1, last_day+1):
            attr = self.calendar_ctrl.GetAttr(d)
            if d in bold_days:
                highlight_attr = wx.adv.CalendarDateAttr(font=self.bold_font)
                if attr != highlight_attr:
                    self.calendar_ctrl.SetAttr(d, highlight_attr)

            else:
                if attr is not None:
                    self.calendar_ctrl.ResetAttr(d)

        self.calendar_ctrl.Refresh()


    def OnCalChanged(self, event):
        date = event.GetDate()
        print("Date changed :", date.Format("%d/%m/%Y"))
        self.highlightDates()


class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None, wx.ID_ANY, "")
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True


if __name__ == "__main__":
    app = MyApp(0)
    app.MainLoop()

reticulatus avatar Jan 08 '22 20:01 reticulatus

The issue is that SetAttr() on the C++ side takes control of the wxCalendarDateAttr that you pass in. If you pass in the same instance of wxCalendarDateAttr, then there is going to be an issue because the C++ side is going to try to delete it twice. As a workaround, do as you are doing and always pass a unique wxCalendarDateAttr into SetAttr(). We might be able to make this better on the Python side - internally create a copy of the wxCalendarDateAttr when you call SetAttr?

swt2c avatar Jan 08 '22 22:01 swt2c

It looks like it would be somewhat challenging to do what I proposed earlier - there's no copy constructor for wxCalendarDateAttr, nor is there an equality operator, so we'd have to implement those and maintain them in wxPython. So I'd suggest just always using a unique wxCalendarDateAttr when calling SetAttr().

swt2c avatar Jan 09 '22 01:01 swt2c

Thanks for your replies, Scott, they certainly clear up my confusion. The reason for my initial approach was that, in my real application, there is quite a lot of processing happening when the date in the calendar is changed, so I was trying not to add too much additional load for the highlighting of the special days. Hopefully, it won't be too much of an issue.

reticulatus avatar Jan 09 '22 08:01 reticulatus

It looks like it would be somewhat challenging to do what I proposed earlier - there's no copy constructor for wxCalendarDateAttr, nor is there an equality operator, so we'd have to implement those and maintain them in wxPython. So I'd suggest just always using a unique wxCalendarDateAttr when calling SetAttr().

Might a noob ask, how one would go about "using a unique wxCalendarDateAttr when calling SetAttr()"?

fullwehrer avatar Aug 24 '22 20:08 fullwehrer

It looks like it would be somewhat challenging to do what I proposed earlier - there's no copy constructor for wxCalendarDateAttr, nor is there an equality operator, so we'd have to implement those and maintain them in wxPython. So I'd suggest just always using a unique wxCalendarDateAttr when calling SetAttr().

Might a noob ask, how one would go about "using a unique wxCalendarDateAttr when calling SetAttr()"?

Click on the "Code Example (click to expand)" button in my original post above. This shows two versions of my demo program.

In the first version I created a single wxCalendarDateAttr object and tried to pass it to multiple calls of SetAttr(). This was causing the crash.

In the second version, I create a new wxCalendarDateAttr object before every call of SetAttr(). This stopped the crash from happening.

reticulatus avatar Aug 25 '22 06:08 reticulatus

This issue has been mentioned on Discuss wxPython. There might be relevant details there:

https://discuss.wxpython.org/t/wx-adv-genericcalendarctrl-crashes-the-frame-when-using-setattr/36096/2

RobinD42 avatar Aug 25 '22 06:08 RobinD42

This issue has been mentioned on Discuss wxPython. There might be relevant details there:

https://discuss.wxpython.org/t/a-wx-datepickerctrl-with-a-customisable-format/36295/13

RobinD42 avatar Aug 07 '23 16:08 RobinD42

This issue has been mentioned on Discuss wxPython. There might be relevant details there:

https://discuss.wxpython.org/t/a-wx-datepickerctrl-with-a-customisable-format/36295/24

RobinD42 avatar Aug 15 '23 11:08 RobinD42