Создание popup-календаря для qLineEdit в PyQt5
Для моей программы нужно сделать два поля QLineEdit, в которые пользователь будет вводить дату, используя для этого виджет QCalendarWidget, либо вводить дату вручную. Т.к. главное окно программы имеет адаптивные размеры, то простая вставка календаря в сетку не представляется возможной из-за того, что когда календарь скрыт, то окно программы выглядит нормально, а когда вызван календарь, то окно программы растянется на высоту календаря-это неприемлемо. По этой причине было решено сделать popup-календарь. Использование QDateEdit не подходит по причине того, что в этот виджет нельзя установить placeholder (ДД.ММ.ГГГГ).
Копаясь который день в интернете, я нашел интересный вариант создания popup-календаря с применением QToolButton и QMenu (в моем примере это реализовано). Для того, чтобы календарь открывался по клику на QLineEdit, в программе было определено действие для этого поля при фокусе по нему (полю) self.lineEdit.installEventFilter(self).
То, чего я бы хотел добиться, но у меня не получается:
- При клике по полю
QLineEditдолжен открыться календарь, но курсор должен находится в поле ввода и пользователь может вводить дату с клавиатуры (календарь не закрывается при ручном вводе), при этом также доступен ввод с календаря; - После того как пользователь ввел дату с календаря, в поле устанавливается выбранная дата, НО при этом курсор остается в поле для даты. Нужно, чтобы после ввода поле даты было без фокуса и без установленного курсора (при этом задумано срабатывание сигнала
editingFinished, вызывающего функцию валидации введенной даты); - Если пользователь захочет удалить или отредактировать дату, то нажатием на поле даты откроется календарь и будет возможность редактировать дату вручную или с помощью календаря (аналогично п.1);
- Если пользователь захочет удалить дату используя
ClearButtonвQLineEdit, то нажав на эту кнопку откроется календарь и дата будет удалена.
В веб-приложениях модуль popup-календаря реализован из-коробки, хотелось бы, чтобы в моей программе поведения календаря и поля даты было таким же.
Как дополнительный вопрос: как можно в QCalendarWidget установить дату самому, именно не текущую дату, а произвольную (наподобие команды setDate), чтобы если пользователь введет дату вручную, то при успешной валидации эта дата установилась в календаре, и открыв повторно календарь, пользователь бы видел именно эту дату?
Еще вопрос до кучи: можно ли в QMainWindow накладывать несколько сеток QGridLayout одна на другую?
Минимально-воспроизводимый код:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.Qt import *
from datetime import datetime, date, time
class Menu(QMenu):
def showEvent(self, event):
if self.isVisible():
button = self.parentWidget()
if button is not None:
pos = button.mapToGlobal(button.rect().bottomLeft())
self.move(pos - self.rect().topLeft())
super().showEvent(event)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.resize(400, 200)
self.layout = QHBoxLayout(self)
self.label = QLabel("Testing LineEdit Popup")
self.label.setFocus()
self.toolbutton = QToolButton(popupMode=QToolButton.InstantPopup)
self.toolbutton.setFocus()
self.widget = QCalendarWidget()
self.lineEdit = QLineEdit()
self.lineEdit.setFocusPolicy(QtCore.Qt.ClickFocus)
self.lineEdit.setClearButtonEnabled(True)
self.lineEdit.setPlaceholderText("ДД.ММ.ГГГГ")
self.widgetAction = QWidgetAction(self.lineEdit)
self.widgetAction.setDefaultWidget(self.widget)
self.widgetMenu = Menu(self.lineEdit)
self.widgetMenu.addAction(self.widgetAction)
self.toolbutton.setMenu(self.widgetMenu)
self.widget.clicked.connect(self.vrfDate)
self.layout.addWidget(self.label)
self.layout.addWidget(self.lineEdit)
#self.layout.addWidget(self.toolbutton)
self.vrfdate = self.lineEdit.text()
self.lineEdit.installEventFilter(self)
self.lineEdit.editingFinished.connect(self.dateValidator)
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.FocusIn:
if obj == self.lineEdit:
self.toolbutton.showMenu()
elif event.type() == QtCore.QEvent.HoverEnter:
print('Hover')
#self.label.setFocus()
elif event.type() == QtCore.QEvent.FocusOut:
print('FocusOut')
return super(Example, self).eventFilter(obj, event)
def vrfDate(self):
self.label.setFocus()
self.widgetMenu.close()
self.lineEdit.setText(self.widget.selectedDate().toString("dd.MM.yyyy"))
self.vrfdate = self.lineEdit.text()
def dateValidator(self):
sender = self.lineEdit
print('sender.text()', sender.text())
if sender.text() == '':
sender.setText('')
if sender == self.lineEdit:
self.vrfdate = sender.text()
else:
try:
self.formattedDate = datetime.strptime(sender.text(),'%d.%m.%Y').strftime('%d.%m.%Y')
if sender == self.lineEdit:
self.vrfdate = self.formattedDate
sender.setText(self.vrfdate)
self.vrfdate.split('.')
self.widget.setSelectedDate(QDate(int(self.vrfdate[0]), int(self.vrfdate[1]), int(self.vrfdate[2]))) # как установить в календарь введенную пользователем дату?
except ValueError:
if sender == self.lineEdit:
sender.setText(self.vrfdate)
def mousePressEvent(self, event):
self.dateValidator()
self.label.setFocus()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Example()
window.show()
sys.exit(app.exec_())
Ответы (2 шт):
sorry, слишком много текста и вопросов.
Попробуйте пример ниже и скажите, что не так.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.Qt import *
class SimpleDateValidator(QtGui.QValidator):
def validate(self, text, pos):
if not text:
return self.Acceptable, text, pos
fmt = self.parent().format()
_sep = set(fmt.replace('d', '').replace('M', '').replace('y', ''))
for l in text:
# убедитесь, что набранный текст представляет собой цифру или разделитель
if not l.isdigit() and l not in _sep:
return self.Invalid, text, pos
years = fmt.count('y')
if len(text) <= years and text.isdigit():
return self.Acceptable, text, pos
if QtCore.QDate.fromString(text, fmt).isValid():
return self.Acceptable, text, pos
return self.Intermediate, text, pos
class DateEdit(QtWidgets.QWidget):
customFormat = 'dd.MM.yyyy'
def __init__(self, parent=None):
super().__init__(parent)
self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.lineEdit = QtWidgets.QLineEdit()
layout.addWidget(self.lineEdit)
self.lineEdit.setMaxLength(len(self.format()))
self.validator = SimpleDateValidator(self)
self.lineEdit.setValidator(self.validator)
self.dropDownButton = QtWidgets.QToolButton()
layout.addWidget(self.dropDownButton)
self.dropDownButton.setIcon(
self.style().standardIcon(QtWidgets.QStyle.SP_ArrowDown))
self.dropDownButton.setMaximumHeight(self.lineEdit.sizeHint().height())
self.dropDownButton.setCheckable(True)
self.dropDownButton.setFocusPolicy(QtCore.Qt.NoFocus)
self.calendar = QtWidgets.QCalendarWidget()
self.calendar.setWindowFlags(QtCore.Qt.Popup)
self.calendar.installEventFilter(self)
# connect
self.dropDownButton.pressed.connect(self.showPopup)
self.dropDownButton.released.connect(self.calendar.hide)
self.lineEdit.editingFinished.connect(self.editingFinished)
self.calendar.clicked.connect(self.setDate)
self.calendar.activated.connect(self.setDate)
self.setDate(QtCore.QDate.currentDate())
def editingFinished(self):
if self.calendar.isVisible():
return
if not self.isValid():
self.lineEdit.setText('')
def format(self):
return self.customFormat or QtCore.QLocale().dateFormat(QtCore.QLocale.ShortFormat)
def setFormat(self, format):
# принимать только числовые форматы даты
if format and 'MMM' in format or 'ddd' in format:
return
self.customFormat = format
self.setDate(self.calendar.selectedDate())
self.calendar.hide()
self.lineEdit.setMaxLength(self.format())
self.validator.setFormat(self.format())
def text(self):
return self.lineEdit.text()
def date(self):
if not self.isValid():
return None
date = QtCore.QDate.fromString(self.text(), self.format())
if date.isValid():
return date
return int(self.text())
def setDate(self, date):
self.lineEdit.setText(date.toString(self.format()))
self.calendar.setSelectedDate(date)
self.calendar.hide()
def setDateRange(self, minimum, maximum):
self.calendar.setDateRange(minimum, maximum)
def isValid(self):
text = self.text()
if not text:
return False
date = QtCore.QDate.fromString(text, self.format())
if date.isValid():
self.setDate(date)
return True
try:
year = int(text)
start = self.calendar.minimumDate().year()
end = self.calendar.maximumDate().year()
if start <= year <= end:
return True
except:
pass
return False
def hidePopup(self):
self.calendar.hide()
def showPopup(self):
pos = self.lineEdit.mapToGlobal(self.lineEdit.rect().bottomLeft())
pos += QtCore.QPoint(0, 1)
rect = QtCore.QRect(pos, self.calendar.sizeHint())
self.calendar.setGeometry(rect)
self.calendar.show()
self.calendar.setFocus()
def eventFilter(self, source, event):
# нажмите или отпустите кнопку, когда календарь отображается / скрывается
if event.type() == QtCore.QEvent.Hide:
self.dropDownButton.setDown(False)
elif event.type() == QtCore.QEvent.Show:
self.dropDownButton.setDown(True)
return super().eventFilter(source, event)
def keyPressEvent(self, event):
if event.key() in (QtCore.Qt.Key_Down, QtCore.Qt.Key_F4):
if not self.calendar.isVisible():
self.showPopup()
super().keyPressEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = DateEdit()
window.show()
sys.exit(app.exec_())
Update
Мне нужно: 1.чтобы в поле даты по-умолчанию был указан плейсхолдер (ДД.ММ.ГГГГ).
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.Qt import *
class SimpleDateValidator(QtGui.QValidator):
def validate(self, text, pos):
if not text:
return self.Acceptable, text, pos
fmt = self.parent().format()
_sep = set(fmt.replace('d', '').replace('M', '').replace('y', ''))
for l in text:
# убедитесь, что набранный текст представляет собой цифру или разделитель
if not l.isdigit() and l not in _sep:
return self.Invalid, text, pos
years = fmt.count('y')
if len(text) <= years and text.isdigit():
return self.Acceptable, text, pos
if QtCore.QDate.fromString(text, fmt).isValid():
return self.Acceptable, text, pos
return self.Intermediate, text, pos
class DateEdit(QtWidgets.QWidget):
customFormat = 'dd.MM.yyyy'
def __init__(self, parent=None):
super().__init__(parent)
self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.lineEdit = QtWidgets.QLineEdit(placeholderText="ДД.ММ.ГГГГ") # +++ placeholderText
layout.addWidget(self.lineEdit)
self.lineEdit.setMaxLength(len(self.format()))
self.validator = SimpleDateValidator(self)
self.lineEdit.setValidator(self.validator)
self.dropDownButton = QtWidgets.QToolButton()
layout.addWidget(self.dropDownButton)
self.dropDownButton.setIcon(
self.style().standardIcon(QtWidgets.QStyle.SP_ArrowDown))
self.dropDownButton.setMaximumHeight(self.lineEdit.sizeHint().height())
self.dropDownButton.setCheckable(True)
self.dropDownButton.setFocusPolicy(QtCore.Qt.NoFocus)
self.calendar = QtWidgets.QCalendarWidget()
self.calendar.setWindowFlags(QtCore.Qt.Popup)
self.calendar.installEventFilter(self)
# connect
self.dropDownButton.pressed.connect(self.showPopup)
self.dropDownButton.released.connect(self.calendar.hide)
self.lineEdit.editingFinished.connect(self.editingFinished)
self.calendar.clicked.connect(self.setDate)
self.calendar.activated.connect(self.setDate)
# self.setDate(QtCore.QDate.currentDate()) # ---
def editingFinished(self):
if self.calendar.isVisible():
return
if not self.isValid():
self.lineEdit.setText('')
def format(self):
return self.customFormat or QtCore.QLocale().dateFormat(QtCore.QLocale.ShortFormat)
def setFormat(self, format):
# принимать только числовые форматы даты
if format and 'MMM' in format or 'ddd' in format:
return
self.customFormat = format
self.setDate(self.calendar.selectedDate())
self.calendar.hide()
self.lineEdit.setMaxLength(self.format())
self.validator.setFormat(self.format())
def text(self):
return self.lineEdit.text()
def date(self):
if not self.isValid():
return None
date = QtCore.QDate.fromString(self.text(), self.format())
if date.isValid():
return date
return int(self.text())
#
def setDate(self, date):
self.lineEdit.setText(date.toString(self.format()))
self.calendar.setSelectedDate(date)
self.calendar.hide()
def setDateRange(self, minimum, maximum):
self.calendar.setDateRange(minimum, maximum)
def isValid(self):
text = self.text()
if not text:
return False
date = QtCore.QDate.fromString(text, self.format())
if date.isValid():
self.setDate(date)
return True
try:
year = int(text)
start = self.calendar.minimumDate().year()
end = self.calendar.maximumDate().year()
if start <= year <= end:
return True
except:
pass
return False
def hidePopup(self):
self.calendar.hide()
def showPopup(self):
pos = self.lineEdit.mapToGlobal(self.lineEdit.rect().bottomLeft())
pos += QtCore.QPoint(0, 1)
rect = QtCore.QRect(pos, self.calendar.sizeHint())
self.calendar.setGeometry(rect)
self.calendar.show()
self.calendar.setFocus()
def eventFilter(self, source, event):
# нажмите или отпустите кнопку, когда календарь отображается / скрывается
if event.type() == QtCore.QEvent.Hide:
self.dropDownButton.setDown(False)
elif event.type() == QtCore.QEvent.Show:
self.dropDownButton.setDown(True)
return super().eventFilter(source, event)
def keyPressEvent(self, event):
if event.key() in (QtCore.Qt.Key_Down, QtCore.Qt.Key_F4):
if not self.calendar.isVisible():
self.showPopup()
super().keyPressEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = DateEdit()
window.show()
sys.exit(app.exec_())
я еще раз перечитал второе предложение вашего описания, а именно: "Т.к. главное окно программы имеет адаптивные размеры, то простая вставка календаря в сетку не представляется возможной из-за того, что когда календарь скрыт, то окно программы выглядит нормально, а когда вызван календарь, то окно программы растянется на высоту календаря-это неприемлемо." и не могу с ним согласиться. Это не так.
Класс Dialog - это первое что мне попалось под руки, чтобы заполнить главное окно.
А т.к. QDateEdit уже предоставляет QCalendarWidget, то календарь никуда вставлять не надо!
Данный ответ имеет совершенно другое решение и публикуется отдельным ответом. Я считаю, что это решение предпочтительнее и рекомендую вам его использовать.
Попробуйте:
import sys
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.Qt import *
class Dialog(QDialog):
NumGridRows = 3
NumButtons = 4
def __init__(self):
super(Dialog, self).__init__()
self.createHorizontalGroupBox()
self.createGridGroupBox()
self.createFormGroupBox()
bigEditor = QTextEdit()
bigEditor.setPlainText("Этот виджет занимает все оставшееся пространство "
"в макете верхнего уровня.")
buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
mainLayout = QVBoxLayout()
mainLayout.addWidget(self.horizontalGroupBox)
mainLayout.addWidget(self.gridGroupBox)
mainLayout.addWidget(self.formGroupBox)
mainLayout.addWidget(bigEditor)
mainLayout.addWidget(buttonBox)
self.setLayout(mainLayout)
self.setWindowTitle("Basic Layouts")
def createHorizontalGroupBox(self):
self.horizontalGroupBox = QGroupBox("Horizontal layout")
layout = QHBoxLayout()
for i in range(Dialog.NumButtons):
button = QPushButton("Button %d" % (i + 1))
layout.addWidget(button)
self.horizontalGroupBox.setLayout(layout)
def createGridGroupBox(self):
self.gridGroupBox = QGroupBox("Grid layout")
layout = QGridLayout()
for i in range(Dialog.NumGridRows):
label = QLabel("Line %d:" % (i + 1))
lineEdit = QLineEdit()
layout.addWidget(label, i + 1, 0)
layout.addWidget(lineEdit, i + 1, 1)
self.smallEditor = QTextEdit()
self.smallEditor.setPlainText("Этот виджет занимает около двух третей "
"макета сетки. \n Смотрим соотношение `setColumnStretch`!")
layout.addWidget(self.smallEditor, 0, 2, 5, 1) # 0, 2, 4, 1
layout.setColumnStretch(1, 10)
layout.setColumnStretch(2, 20)
self.gridGroupBox.setLayout(layout)
def createFormGroupBox(self):
self.formGroupBox = QGroupBox("Form layout")
layout = QFormLayout()
layout.addRow(QLabel("Line 1:"), QLineEdit())
layout.addRow(QLabel("Строка 2, длинный текст:"), QComboBox())
layout.addRow(QLabel("Line 3:"), QSpinBox())
self.formGroupBox.setLayout(layout)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.dateEdit = QtWidgets.QDateEdit(calendarPopup=True)
self.menuBar().setCornerWidget(self.dateEdit, QtCore.Qt.TopLeftCorner)
self.dateEdit.setDateTime(QtCore.QDateTime.currentDateTime())
centralWidget = QtWidgets.QWidget()
self.setCentralWidget(centralWidget)
dialog = Dialog()
grid = QtWidgets.QGridLayout(centralWidget)
grid.addWidget(dialog, 0, 0)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Update
Вы не заметили в тексте самого главного: "Использование QDateEdit не подходит по причине того, что в этот виджет нельзя установить placeholder (ДД.ММ.ГГГГ)." А в остальном меня этот виджет вполне устраивает.
Да дело в том, что я не понимаю зачем вам ДД.ММ.ГГГГ.
Ну и ладно, надо так надо. Попробуйте:
import sys
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.Qt import *
class Dialog(QDialog):
NumGridRows = 3
NumButtons = 4
def __init__(self):
super(Dialog, self).__init__()
self.createHorizontalGroupBox()
self.createGridGroupBox()
self.createFormGroupBox()
bigEditor = QTextEdit()
bigEditor.setPlainText("Этот виджет занимает все оставшееся пространство "
"в макете верхнего уровня.")
buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
mainLayout = QVBoxLayout()
mainLayout.addWidget(self.horizontalGroupBox)
mainLayout.addWidget(self.gridGroupBox)
mainLayout.addWidget(self.formGroupBox)
mainLayout.addWidget(bigEditor)
mainLayout.addWidget(buttonBox)
self.setLayout(mainLayout)
self.setWindowTitle("Basic Layouts")
def createHorizontalGroupBox(self):
self.horizontalGroupBox = QGroupBox("Horizontal layout")
layout = QHBoxLayout()
for i in range(Dialog.NumButtons):
button = QPushButton("Button %d" % (i + 1))
layout.addWidget(button)
self.horizontalGroupBox.setLayout(layout)
def createGridGroupBox(self):
self.gridGroupBox = QGroupBox("Grid layout")
layout = QGridLayout()
for i in range(Dialog.NumGridRows):
label = QLabel("Line %d:" % (i + 1))
lineEdit = QLineEdit()
layout.addWidget(label, i + 1, 0)
layout.addWidget(lineEdit, i + 1, 1)
self.smallEditor = QTextEdit()
self.smallEditor.setPlainText("Этот виджет занимает около двух третей "
"макета сетки. \n Смотрим соотношение `setColumnStretch`!")
layout.addWidget(self.smallEditor, 0, 2, 5, 1) # 0, 2, 4, 1
layout.setColumnStretch(1, 10)
layout.setColumnStretch(2, 20)
self.gridGroupBox.setLayout(layout)
def createFormGroupBox(self):
self.formGroupBox = QGroupBox("Form layout")
layout = QFormLayout()
layout.addRow(QLabel("Line 1:"), QLineEdit())
layout.addRow(QLabel("Строка 2, длинный текст:"), QComboBox())
layout.addRow(QLabel("Line 3:"), QSpinBox())
self.formGroupBox.setLayout(layout)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.dateEdit = QtWidgets.QDateEdit(calendarPopup=True)
self.menuBar().setCornerWidget(self.dateEdit, QtCore.Qt.TopLeftCorner)
self.dateEdit.setDateTime(QtCore.QDateTime.currentDateTime())
self.dateEdit.setToolTip('<b>ДД.ММ.ГГГГ</b>') # +++
self.dateEdit.installEventFilter(self) # +++
centralWidget = QtWidgets.QWidget()
self.setCentralWidget(centralWidget)
dialog = Dialog()
grid = QtWidgets.QGridLayout(centralWidget)
grid.addWidget(dialog, 0, 0)
# +++ vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
self.label = QLabel('ДД.ММ.ГГГГ', self.dateEdit)
self.label.setObjectName('label')
self.label.setVisible(True)
def eventFilter(self, source, event):
if source == self.dateEdit and event.type() == QtCore.QEvent.Enter:
self.label.setVisible(False)
elif source == self.dateEdit and event.type() == QtCore.QEvent.Leave:
self.label.setVisible(True)
return super().eventFilter(source, event)
# +++ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
# +++ vvvvvvvvvvvvv <<<---------------------------------------------------- !!!
app.setStyleSheet("""
QLabel#label {
background-color: #fff;
padding: 1px 0px 1px 0px;
font-size: 9.5pt;
}
""")
w = MainWindow()
w.show()
sys.exit(app.exec_())





