Home > Software design >  How to color disabled date in QcalendarWidget
How to color disabled date in QcalendarWidget

Time:01-08

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"
 }

CodePudding user response:

I am not sure of a way using css, it is possible using code though.

If you override the QCalenderWidget.paintCell method you can style each date individually.

For example:

class Calendar(QCalendarWidget):
    def __init__(self, parent) -> None:
        super().__init__(parent)
        self.start_date = QDate(2022, 12, 22)
        self.end_date = QDate(2023, 8, 1)

    def paintCell(self, painter, rect, date):
        if date.daysTo(self.end_date) > 0 and date.daysTo(self.start_date) < 0:
            painter.setPen("green")
            brush = painter.brush()
            brush.setColor("black") 
            brush.setStyle(Qt.SolidPattern)
            painter.setBrush(brush)
            painter.drawRect(rect)
            painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, str(date.day()))
        else:
            super().paintCell(painter, rect, date)

CodePudding user response:

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()
  • Related