QDateEdit, QStandardItemModel and empty date fields

classic Classic list List threaded Threaded
5 messages Options
Reply | Threaded
Open this post in threaded view
|

QDateEdit, QStandardItemModel and empty date fields

Sibylle Koczian-3
How can I get a QDateEdit to show an empty date, or how can I clear the
date it shows?

Background: my application puts data from a MS Access database table
into a QStandardItemModel. One of the table columns contains dates and
those dates may be NULL. This is no problem for the QTableView that
shows all the data: these cells are empty.

For editing data several input fields are connected to the model using a
QDataWidgetMapper. The date column is connected to a QDateEdit.

If a record contains NULL in its date column, then the QDateEdit shows
the date of the last selected record with a valid date. I can change
this to another valid date, but it should be changed to None. The
QTableView with the correct empty cell is in the same window as the
input fields or I would save wrong dates without noticing.

Of course I can replace the QDateEdit with a QLineEdit and work with a
string representation of the date column throughout. Is that the only
solution?

I wrote a minimal test application that takes a date string from a
QLineEdit, converts it into a QDate and puts this into a QDateEdit - and
vice versa. I see that the QDateEdit simply keeps its last value, if the
date it gets is invalid (empty or invalid for other reasons). No exception.

Using PyQt5 5.11.3, Python 3.7.2, Windows 10.

Thank you for advice,
Sibylle
_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
Reply | Threaded
Open this post in threaded view
|

Re: QDateEdit, QStandardItemModel and empty date fields

Maurizio Berti
Il giorno sab 2 mar 2019 alle ore 21:02 Sibylle Koczian <[hidden email]> ha scritto:
How can I get a QDateEdit to show an empty date, or how can I clear the
date it shows? 
[...]
Of course I can replace the QDateEdit with a QLineEdit and work with a
string representation of the date column throughout. Is that the only
solution?

QDateTimeEdit and its inherited QDateEdit/QTimeEdit all inherit from QAbstractSpinBox, which contains a "private" QLineEdit.
While that is not publicly accessible, you can just use findChild() to get its reference.
Here's a small example you can implement in a model view by using a delegate and checking for the data in the setEditorData() method:

class ClearableDateEdit(QtWidgets.QDateEdit):
    def __init__(self, *args, **kwargs):
        QtWidgets.QDateEdit.__init__(self, *args, **kwargs)
        self.lineEdit = self.findChild(QtWidgets.QLineEdit)
        self.clear()

    def clear(self):
        self.lineEdit.setText('')


class Widget(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        layout = QtWidgets.QGridLayout()
        self.setLayout(layout)
        dateEdit = ClearableDateEdit()
        layout.addWidget(dateEdit)
        clearButton = QtWidgets.QPushButton('Clear date')
        layout.addWidget(clearButton)
        clearButton.clicked.connect(dateEdit.clear)

There are some things you've to take into account, anyway, most importantly whenever the focus is get or lost or the cell is changed, which will require some control over the DateEdit widget *changed() signals and methods like dateTimeFromText(), and finally check everything before using setModelData.

Maurizio

-- 
È difficile avere una convinzione precisa quando si parla delle ragioni del cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
Reply | Threaded
Open this post in threaded view
|

Re: QDateEdit, QStandardItemModel and empty date fields

J Barchan

On Sun, 3 Mar 2019 at 15:57, Maurizio Berti <[hidden email]> wrote:
Il giorno sab 2 mar 2019 alle ore 21:02 Sibylle Koczian <[hidden email]> ha scritto:
How can I get a QDateEdit to show an empty date, or how can I clear the
date it shows? 
[...]
Of course I can replace the QDateEdit with a QLineEdit and work with a
string representation of the date column throughout. Is that the only
solution?

QDateTimeEdit and its inherited QDateEdit/QTimeEdit all inherit from QAbstractSpinBox, which contains a "private" QLineEdit.
While that is not publicly accessible, you can just use findChild() to get its reference.
Here's a small example you can implement in a model view by using a delegate and checking for the data in the setEditorData() method:

class ClearableDateEdit(QtWidgets.QDateEdit):
    def __init__(self, *args, **kwargs):
        QtWidgets.QDateEdit.__init__(self, *args, **kwargs)
        self.lineEdit = self.findChild(QtWidgets.QLineEdit)
        self.clear()

    def clear(self):
        self.lineEdit.setText('')


class Widget(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        layout = QtWidgets.QGridLayout()
        self.setLayout(layout)
        dateEdit = ClearableDateEdit()
        layout.addWidget(dateEdit)
        clearButton = QtWidgets.QPushButton('Clear date')
        layout.addWidget(clearButton)
        clearButton.clicked.connect(dateEdit.clear)

There are some things you've to take into account, anyway, most importantly whenever the focus is get or lost or the cell is changed, which will require some control over the DateEdit widget *changed() signals and methods like dateTimeFromText(), and finally check everything before using setModelData.

Maurizio

-- 
È difficile avere una convinzione precisa quando si parla delle ragioni del cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net
_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt

You cannot have an empty QDateEdit, as per my topic long ago at https://forum.qt.io/topic/86749/qdateedit-qabstractspinbox-blank-empty-value-problem.

I had to deal with this.  I did not take @Maurizio's approach.  I do not know what his solution does about QDateEdit.setData/data() for an empty date, and other things.  Until now I didn't know you could get at its QLineEdit.  For my part I chose to have a QLineEdit plus an associated ... button to its right which leads to a modal dialog.  I paste extracts below, feel free to cannibalise it if you want my approach.  If @Maurizio wishes to criticize mine, that's fine, I will read and consider!

class JDateEdit(QWidget):
# Class to display an "editable date" widget
# It consists of a QLineEdit plus a "..." QPushButton
# which brings up a (modal) dialog to pick a date to copy into the line edit
# We have to use our own class and not the native Qt "QDateEdit"+setCalendarPopup(True)
# because that does not allow a "blank/empty" date in any form,
# and we do need blank all over the place

# class variable for "editingFinished" signal
editingFinished = QtCore.pyqtSignal(name='editingFinished')

def __init__(self, parent=None):
super().__init__(parent)

self.minimumDate = None
self.maximumDate = None

# lineEdit holds the date
self.lineEdit = JLineEdit(self)
# "..." button, when clicked will bring up a dialog with a QCalendarWidget
self.button = JDotDotDotButton(self)

# connect self.lineEdit.editingFinished signal to self.editingFinished signal
self.lineEdit.editingFinished.connect(self.editingFinished)

layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self.lineEdit)
layout.addWidget(self.button)
layout.addStretch(1)
self.setLayout(layout)

self.button.clicked.connect(self.doDialog)
self.dialog = None

def doDialog(self):
self.dialog = JCalendarDialog(self.button)
if self.minimumDate:
self.dialog.calendar.setMinimumDate(self.minimumDate)
if self.maximumDate:
self.dialog.calendar.setMaximumDate(self.maximumDate)
date = self.date()
if date is not None:
self.dialog.setSelectedDate(pyDateToQDate(date))
pos = self.button.rect().topLeft()
pos = self.button.mapToGlobal(pos)
self.dialog.move(pos)
if self.dialog.exec():
date = qDateToPyDate(self.dialog.selectedDate())
# copy the selected date to the line edit
self.setDate(date)
# and emit the editingFinished signal
self.lineEdit.editingFinished.emit()
self.dialog = None

def setReadOnly(self, ro: bool):
self.lineEdit.setReadOnly(ro)
if ro:
self.button.hide()
else:
self.button.show()

def setDate(self, date: typing.Union[datetime.date, None]):
# Set the date
# Accepts None for the date, and sets it blank
if date is None:
self.lineEdit.setText("")
else:
self.lineEdit.setText(dateToStr(date))

def date(self) -> typing.Union[datetime.date, None]:
# Return the date
# If the date is blank, or if it fails to parse in parseDateSoft(), return None
text = self.lineEdit.text()
if text == "":
return None
return parseDateSoft(text)

def dateOrWarn(self) -> typing.Union[datetime.date, None]:
# Return the date, which *should* be filled in
# If the date is blank, or if it fails to parse,
# put up an ErrorMsgBox() and then return None
text = self.lineEdit.text()
return parseDateHard(text, self.lineEdit)

def dateOrError(self) -> datetime.date:
# Return the date, which *must* be filled in
# If the date is blank, or if it fails to parse,
# put up an ErrorMsgBox() and then raise an exception
text = self.lineEdit.text()
date = parseDateHard(text, self.lineEdit)
if date is None:
raise ValueError("Valid date required")
return date

def clear(self):
self.lineEdit.clear()

def setText(self, a0: str):
self.lineEdit.setText(a0)

def text(self) -> str:
return self.lineEdit.text()

def setMinimumDate(self, date: typing.Union[datetime.date, None]):
self.minimumDate = date

def setMaximumDate(self, date: typing.Union[datetime.date, None]):
self.maximumDate = date



--
Kindest,
Jonathan

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
Reply | Threaded
Open this post in threaded view
|

Re: QDateEdit, QStandardItemModel and empty date fields

Sibylle Koczian-3
Many thanks to you both for your solutions. I'll need a little time to
try them out - and this looks like the first time I might put an
additional widget into the designer. That is documented, I think, but
I'll have to look at it.

Am 05.03.2019 um 09:43 schrieb J Barchan:

>
> On Sun, 3 Mar 2019 at 15:57, Maurizio Berti <[hidden email]
> <mailto:[hidden email]>> wrote:
>
>     QDateTimeEdit and its inherited QDateEdit/QTimeEdit all inherit from
>     QAbstractSpinBox, which contains a "private" QLineEdit.
>     While that is not publicly accessible, you can just use findChild()
>     to get its reference.
>     Here's a small example you can implement in a model view by using a
>     delegate and checking for the data in the setEditorData() method:
>
>     class ClearableDateEdit(QtWidgets.QDateEdit):
>          def __init__(self, *args, **kwargs):
>              QtWidgets.QDateEdit.__init__(self, *args, **kwargs)
>              self.lineEdit = self.findChild(QtWidgets.QLineEdit)
>              self.clear()
>
>          def clear(self):
>              self.lineEdit.setText('')
>
>
>     class Widget(QtWidgets.QWidget):
>          def __init__(self):
>              QtWidgets.QWidget.__init__(self)
>              layout = QtWidgets.QGridLayout()
>              self.setLayout(layout)
>              dateEdit = ClearableDateEdit()
>              layout.addWidget(dateEdit)
>              clearButton = QtWidgets.QPushButton('Clear date')
>              layout.addWidget(clearButton)
>              clearButton.clicked.connect(dateEdit.clear)
>
>     There are some things you've to take into account, anyway, most
>     importantly whenever the focus is get or lost or the cell is
>     changed, which will require some control over the DateEdit widget
>     *changed() signals and methods like dateTimeFromText(), and finally
>     check everything before using setModelData.
>
>     Maurizio
>
>
> You cannot have an empty QDateEdit, as per my topic long ago at
> https://forum.qt.io/topic/86749/qdateedit-qabstractspinbox-blank-empty-value-problem.

Or rather, you cannot have an empty QDate, as this very interesting
discussion shows. Happily I'm not plagued with pernickety users, but
just writing something I intend to use myself. So possibly I'll just use
a QLineEdit with an input mask.

>
> I had to deal with this.  I did not take @Maurizio's approach.  I do not
> know what his solution does about QDateEdit.setData/data() for an empty
> date, and other things.  Until now I didn't know you could get at its
> QLineEdit.  For my part I chose to have a QLineEdit plus an associated
> ... button to its right which leads to a modal dialog.  I paste extracts
> below, feel free to cannibalise it if you want my approach. If @Maurizio
> wishes to criticize mine, that's fine, I will read and consider!
>
I think in Maurizios approach much depends on the delegate that's used.

Greetings,
Sibylle

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
Reply | Threaded
Open this post in threaded view
|

Re: QDateEdit, QStandardItemModel and empty date fields

Maurizio Berti
Il giorno mar 5 mar 2019 alle ore 16:31 Sibylle Koczian <[hidden email]> ha scritto:
> Am 05.03.2019 um 09:43 schrieb J Barchan:
> > I had to deal with this.  I did not take @Maurizio's approach.  I do not
> > know what his solution does about QDateEdit.setData/data() for an empty
> > date, and other things.  Until now I didn't know you could get at its
> > QLineEdit.  For my part I chose to have a QLineEdit plus an associated
> > ... button to its right which leads to a modal dialog.  I paste extracts
> > below, feel free to cannibalise it if you want my approach. If @Maurizio
> > wishes to criticize mine, that's fine, I will read and consider!

Well, Jonathan, your implementation seems fine, and, as long as it works, who am I to judge? ;-)
The only issue in this case might be that you wouldn't be able to take advantage of the "automatic" systems used to fill/read the editor of a delegate, since the lineEdit is not publicly exposed, thus its methods won't be too.
By using parenting, you'll be able to keep all original methods of the QLineEdit and still have that extra button for "advanced" editing. This is useful even in non-delegate scenarios, as it will make coding easier by directly using the QLineEdit subclass.
Here's what I'd do (this is just a simple QLineEdit, obviously without the date implementation):

class LineEditWithDotButton(QtWidgets.QLineEdit):
    advancedEditRequested = QtCore.Signal(str)

    def __init__(self, *args, **kwargs):
        QtWidgets.QLineEdit.__init__(self, *args, **kwargs)
        self.editBtn = QtWidgets.QToolButton(self)
        self.editBtn.setText('...')
        self.editBtn.setCursor(QtCore.Qt.ArrowCursor)
        self.editBtn.setFocusPolicy(QtCore.Qt.NoFocus)
        self.margin = 4
        l, t, r, b = self.getContentsMargins()
        self.setContentsMargins(l, t, r + self.editBtn.sizeHint().width() + self.margin, b)
        self.editBtn.clicked.connect(lambda: self.advancedEditRequested.emit(self.text())

    def showEvent(self, event):
        self.moveEditButton()

    def resizeEvent(self, event):
        self.moveEditButton()

    def moveEditButton(self):
        self.editBtn.setFixedHeight(self.height())
        self.editBtn.move(self.width() - self.getContentsMargins()[2] + self.margin, 0)

I've used a QToolButton as standard QPushButtons with text have the issue of fixed minimum size which could obviously be worked around, but since a QToolButton is fine enough for this case, there's no need to add more headaches. ;-)
The cursor is overridden, since the button is child of the line edit it would use the IBeamCursor and that's not very good for UX. There will be some space where the IBeamCursor is still active (the margin between the edit and the button), if you're not happy with it you can setMouseTracking(True) and use setCursor() in the mouseMoveEvent() implementation. I also used existing content margins, as the current style or (inherited) stylesheet might set them.

> I think in Maurizios approach much depends on the delegate that's used.

Yes, it is necessary to apply some "magic" to the delegate by letting it "talk" with the editor.
The basic idea is that if the setEditorData() argument contains an invalid date, it sets an internal bool property, which can be reset whenever the user types/select a valid date. That property can later be checked back in the setModelData, to avoid invalid submissions to the model.

Well, good luck ;-)

Maurizio

--
È difficile avere una convinzione precisa quando si parla delle ragioni del cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt