Search code examples
pythoncssuser-interfacepyside2qcalendarwidget

How to color disabled date in QcalendarWidget


enter image description here

I'm trying to style my QcalendarWidget using CSS in PySide2, an set my maximum date to 22/12/2022. I'm able to change the color of text for next month to green and normal date to white, but is there any way to change the color for the date in between? (ie. from 22/12/2022 to 08/01/2023)

#qt_calendar_calendarview {
    outline: 0px;
    selection-background-color: #43ace6;
    alternate-background-color: #2c313c;
    background_color:rgb(170, 0, 0)
}

QCalendarWidget QAbstractItemView:!enabled { 
    color:"green"
 }

QCalendarWidget QAbstractItemView:enabled{ 
    color:"white"
 }


Solution

  • Unfortunately, it's not possible using style sheets nor the palette.

    There are some possible solutions, though.

    Override paintCell()

    This is the simplest possibility, as we can use paintCell() to draw the contents. Unfortunately, this has some limitations: we only get the painter, the rectangle and the date, meaning that it's our complete responsibility to choose how the cell and date would be drawn, and it may not be consistent with the rest of the widget (specifically, the headers).

    Set the date text format

    QCalendarWidget provides setDateTextFormat(), which allows setting a specific QTextCharFormat for any arbitrary date.

    The trick is to set the format for dates outside the range within the minimum/maximum month: the assumption is that the calendar is not able to switch to a month that is outside the available date range, so we only need to set the formats for these specific days of the month boundaries.

    class CustomCalendar(QCalendarWidget):
        def fixDateFormats(self):
            fmt = QTextCharFormat()
            # clear existing formats
            self.setDateTextFormat(QDate(), fmt)
    
            fmt.setForeground(QBrush(QColor('green')))
    
            for ref, delta in ((self.minimumDate(), -1), (self.maximumDate(), 1)):
                month = ref.month()
                date = ref.addDays(delta)
                while date.month() == month:
                    self.setDateTextFormat(date, fmt)
                    date = date.addDays(delta)
    
        def setDateRange(self, minimum, maximum):
            super().setDateRange(minimum, maximum)
            self.fixDateFormats()
    
        def setMinimumDate(self, date):
            super().setMinimumDate(date)
            self.fixDateFormats()
    
        def setMaximumDate(self, date):
            super().setMaximumDate(date)
            self.fixDateFormats()
    

    The only drawback of this is that it doesn't allow to change the color of the cells that belong to another month, and while it's possible to use the stylesheet as written by the OP, this doesn't cover the exception of weekends.

    Use a customized item delegate

    This solution is a bit too complex, but is also the most ideal, as it's completely consistent with the widget and style, while also allowing some further customization.

    Since the calendar is actually a composite widget that uses a QTableView to display the dates, this means that, just like any other Qt item view, we can override its delegate.

    The default delegate is a QItemDelegate (the much simpler version of QStyledItemDelegates normally used in item views). While we could manually paint the content of the cell by completely overriding the delegate's paint(), but at that point the first solution would be much simpler. Instead we use the default painting and differentiate when/how the actual display value is shown: if it's within the calendar range, we leave the default behavior, otherwise we alter the QStyleOptionViewItem with our custom color and explicitly call drawDisplay().

    class CalDelegate(QItemDelegate):
        cachedOpt = QStyleOptionViewItem()
        _disabledColor = None
        def __init__(self, calendar):
            self.calendar = calendar
            self.view = calendar.findChild(QAbstractItemView)
            super().__init__(self.view)
            self.view.setItemDelegate(self)
            self.dateReference = self.calendar.yearShown(), self.calendar.monthShown()
            self.calendar.currentPageChanged.connect(self.updateReference)
    
        def disabledColor(self):
            return self._disabledColor or self.calendar.palette().color(
                QPalette.Disabled, QPalette.Text)
    
        def setDisabledColor(self, color):
            self._disabledColor = color
            self.view.viewport().update()
    
        def updateReference(self, year, month):
            self.dateReference = year, month
    
        def dateForCell(self, index):
            day = index.data()
            row = index.row()
            if self.calendar.horizontalHeaderFormat():
                if row == 0:
                    return
                row -= 1
            col = index.column()
            if self.calendar.verticalHeaderFormat():
                if col == 0:
                    return
                col -= 1
            year, month = self.dateReference
            if row < 1 and day > 7:
                # previous month
                month -= 1
                if month < 1:
                    month = 12
                    year -= 1
            elif row > 3 and day < 15:
                # next month
                month += 1
                if month > 12:
                    month = 1
                    year += 1
            return QDate(year, month, day)
    
        def drawDisplay(self, qp, opt, rect, text):
            if self.doDrawDisplay:
                super().drawDisplay(qp, opt, rect, text)
            else:
                self.cachedOpt = QStyleOptionViewItem(opt)
    
        def paint(self, qp, opt, index):
            date = self.dateForCell(index)
            self.doDrawDisplay = not bool(date)
            super().paint(qp, opt, index)
            if self.doDrawDisplay:
                return
            year, month = self.dateReference
            if (
                date.month() != month 
                or not self.calendar.minimumDate() <= date <= self.calendar.maximumDate()
            ):
                self.cachedOpt.palette.setColor(
                    QPalette.Text, self.disabledColor())
            super().drawDisplay(qp, self.cachedOpt, 
                self.cachedOpt.rect, str(index.data()))
    
    
    app = QApplication([])
    cal = QCalendarWidget()
    delegate = CalDelegate(cal)
    delegate.setDisabledColor(QColor('green'))
    cal.setDateRange(QDate(2022, 12, 4), QDate(2023, 1, 27))
    cal.show()
    app.exec()