I am aiming to disable that users can click on days that aren't from the current month in a QCalendarWidget
, so I subclassed the widget in order to do it. So far I could make those days don't render any text at all (great). This is the code:
class QCustomCalendar(QCalendarWidget):
"""Create my own Calendar with my own options."""
def __init__(self, parent=None):
"""Initializing functions"""
QCalendarWidget.__init__(self, parent)
self.setEnabled(True)
self.setGeometry(QRect(0, 0, 320, 250))
self.setGridVisible(False)
self.setHorizontalHeaderFormat(QCalendarWidget.SingleLetterDayNames)
self.setVerticalHeaderFormat(QCalendarWidget.NoVerticalHeader)
self.setNavigationBarVisible(True)
self.setDateEditEnabled(True)
self.setObjectName("calendarWidget")
def paintCell(self, painter, rect, date):
"""Sub-class this and repaint the cells"""
# Render only this-month days
month = "{0}-{1}".format(str(self.yearShown()), str(self.monthShown()).zfill(2))
day = str(date.toPython())
if not day.startswith(month):
return
QCalendarWidget.paintCell(self, painter, rect, date)
However, if I click on a non-rendered day it still counts and triggers the clicked
event. Example: I photoshopped a red square where clicking in it, it would select June's 4th (even though we are in May in the screenshot).
How do I disable those days to not being selectables?
I tried setDateRange
on currentPageChanged
event, but it doesn't works as expected:
def __init__(self, parent=None):
# some code
self.currentPageChanged.connect(self.store_current_month)
self.clicked.connect(self.calendar_itemchosen)
def store_current_month(self):
self.CURRENT_MONTH = "{0}-{1}".format(str(self.yearShown()), str(self.monthShown()).zfill(2))
def calendar_itemchosen(self):
day = str(self.selectedDate().toPython())
print(day)
if day.startswith(self.CURRENT_MONTH):
selection = self.selectedDate()
# some code
self.close()
The result of clicking on that red square with this code is:
2018-06
2018-06-04
So I guess Qt first triggers the currentPageChanged
event when you select a date on another month. setDateRange
won't work because if I add it to limit selections to this month only, then the buttons at top of the calendar to "go to next or previous month" won't work, and I need the user to be able to change months. I just don't want the calendar to show days that don't belong to this month page.
One solution is to filter the mousePressEvent event of the QTableView that the QCalendarWidget has Internally. For this we use the event filter:
from PyQt5 import QtCore, QtWidgets
class CalendarWidget(QtWidgets.QCalendarWidget):
def __init__(self, parent=None):
super(CalendarWidget, self).__init__(parent, gridVisible=False,
horizontalHeaderFormat=QtWidgets.QCalendarWidget.SingleLetterDayNames,
verticalHeaderFormat=QtWidgets.QCalendarWidget.NoVerticalHeader,
navigationBarVisible=True,
dateEditEnabled=True)
self.setEnabled(True)
self.setGeometry(QtCore.QRect(0, 0, 320, 250))
self.clicked.connect(print)
self.table_view = self.findChild(QtWidgets.QTableView, "qt_calendar_calendarview")
self.table_view.viewport().installEventFilter(self)
self.setFirstDayOfWeek(QtCore.Qt.Monday)
def referenceDate(self):
refDay = 1
while refDay <= 31:
refDate = QtCore.QDate(self.yearShown(), self.monthShown(), refDay)
if refDate.isValid(): return refDate
refDay += 1
return QtCore.QDate()
def columnForDayOfWeek(self, day):
m_firstColumn = 1 if self.verticalHeaderFormat() != QtWidgets.QCalendarWidget.NoVerticalHeader else 0
if day < 1 or day > 7: return -1
column = day - int(self.firstDayOfWeek())
if column < 0:
column += 7
return column + m_firstColumn
def columnForFirstOfMonth(self, date):
return (self.columnForDayOfWeek(date.dayOfWeek()) - (date.day() % 7) + 8) % 7
def dateForCell(self, row, column):
m_firstRow = 1 if self.horizontalHeaderFormat() != QtWidgets.QCalendarWidget.NoHorizontalHeader else 0
m_firstColumn = 1 if self.verticalHeaderFormat() != QtWidgets.QCalendarWidget.NoVerticalHeader else 0
rowCount = 6
columnCount = 7
if row < m_firstRow or row > (m_firstRow + rowCount -1) or column < m_firstColumn or column > (m_firstColumn + columnCount -1):
return QtCore.QDate()
refDate = self.referenceDate()
if not refDate.isValid():
return QtCore.QDate()
columnForFirstOfShownMonth = self.columnForFirstOfMonth(refDate)
if (columnForFirstOfShownMonth - m_firstColumn) < 1:
row -= 1
requestedDay = 7*(row - m_firstRow) + column - columnForFirstOfShownMonth - refDate.day() + 1
return refDate.addDays(requestedDay)
def eventFilter(self, obj, event):
if obj is self.table_view.viewport() and event.type() == QtCore.QEvent.MouseButtonPress:
ix = self.table_view.indexAt(event.pos())
date = self.dateForCell(ix.row(), ix.column())
d_start = QtCore.QDate(self.yearShown(), self.monthShown(), 1)
d_end = QtCore.QDate(self.yearShown(), self.monthShown(), d_start.daysInMonth())
if d_start > date or date > d_end:
return True
return super(CalendarWidget, self).eventFilter(obj, event)
def paintCell(self, painter, rect, date):
d_start = QtCore.QDate(self.yearShown(), self.monthShown(), 1)
d_end = QtCore.QDate(self.yearShown(), self.monthShown(), d_start.daysInMonth())
if d_start <= date <= d_end:
super(CalendarWidget, self).paintCell(painter, rect, date)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = CalendarWidget()
w.show()
sys.exit(app.exec_())