issues with MonthGrid
-
Hi all -
I'm trying to use MonthGrid as part of a date picker I'm building. I'm having a couple problems. Here's the code:
MonthGrid { id: monthGrid delegate: Rectangle { id: dayDelegate property bool selected: false Rectangle { id: highlightRect visible: dayDelegate.selected } MouseArea { onClicked: (mouseEvent) => { dayDelegate.selected = !dayDelegate.selected } } } onClicked: (date) => { // call a C++ function dayDelegate.selected = !dayDelegate.selected // this doesn't work } }
I realize I can't have two onClicked() slots, but I've tried both and want to show them.
My first problem is when I select a date (by clicking on it), then change months, the highlighting remains in the grid. Looks like this:
Then, when I change months, the highlights remain:
It's as though the grid is really just a grid (7 columns and 6 rows) with no real "smarts" behind what it's displaying. Is this expected behavior?Second problem is, I can't access the delegate from the latter onClicked() code. I get an error:
ReferenceError: dayDelegate is not defined
I'm guessing this is a coding error on my part, but I don't know what to do to fix it.
Any help is appreciated. Thanks...
-
@mzimmers The documentation shows the properties of the delegate model:
https://doc.qt.io/qt-6/qml-qtquick-controls-monthgrid.html#delegate-propAs it is only a data representation of the dates, you have to store the selected date somewhere else and bind the properties:
property date selectedDate: new Date() MonthGrid { anchors.fill: parent delegate: Item { id: delegateItem // compare dates without time property bool isSelectedDay: ( model.year === selectedDate.getFullYear() && model.month === selectedDate.getMonth() && model.day === selectedDate.getDate() ) Text { color: delegateItem.isSelectedDay ? "green" : "red" text: model.day } } onPressed: function (date) { selectedDate = date } }
-
@JoeCFD I see what you mean (I think), but...what's the right way to do this? The MonthGrid seems a little odd in that it can take a delegate, but it's not apparent (to me anyway) what its model is. The docs list a few model properties, but it doesn't seem to behave the way other controls do.
If it truly is just a grid, then it seems that my highlighting should focus on the grid, not on the dates. So, how do I go about "clearing" the selected property when the month is changed?
-
@mzimmers The documentation shows the properties of the delegate model:
https://doc.qt.io/qt-6/qml-qtquick-controls-monthgrid.html#delegate-propAs it is only a data representation of the dates, you have to store the selected date somewhere else and bind the properties:
property date selectedDate: new Date() MonthGrid { anchors.fill: parent delegate: Item { id: delegateItem // compare dates without time property bool isSelectedDay: ( model.year === selectedDate.getFullYear() && model.month === selectedDate.getMonth() && model.day === selectedDate.getDate() ) Text { color: delegateItem.isSelectedDay ? "green" : "red" text: model.day } } onPressed: function (date) { selectedDate = date } }
-
@lemons thanks for the reply. It took me some experimentation, but I think I finally understand what you're doing.
Now, I'd like to complicate this slightly -- I'd like to allow the user to select multiple dates. I've added a property to store the selected dates:
property var selectedDates: []
(I had to use var because I couldn't figure out how to make an array of type date.)
My onClicked() logic looks like this:
onClicked: (dateClicked) => { var offset = dateClicked.getTimezoneOffset() var localDate = new Date(dateClicked.getTime() + (offset * 60000)) var dateStr = monthGrid.locale.toString(localDate) scheduleModel.enterDate(localDate, true) selectedDate = localDate const index = selectedDates.indexOf(dateStr) // console.log("DatePicker.qml: index is " + index) if (index >= 0) { selectedDates.splice(index, 1) // remove ("deselect") } else { selectedDates.push(dateStr) } console.log("DatePicker.qml: selectedDate is " + selectedDate + "\n\n") } }
(I needed to convert the date to a string because when I used dates, the indexOf() always returned (-1). I think this might be due to the dates being stored as pointers, but this is just a guess.)
So...if this seems logical so far, the remaining task is to modify the assignment to the property bool isSelectedDay. Any suggestions on this?
EDIT:
I've made some progress. I replaced the JS array with a QML list:
property list<date> selectedDates: []
so that eliminated the need for using a string representation of the date.
I also created a function to determine whether a date should be highlighted. It's not particularly efficient, but should be OK:
function isSelected(date) { var rc = false let len = selectedDates.length for (let i = 0; i < len; i++) { var entry = selectedDates[i] if ( entry.getDate() === date.getDate() && entry.getMonth() === date.getMonth() && entry.getFullYear() === date.getFullYear() ) { rc = true break } } return rc }
My delegate has this property:
property bool selected: isSelected(model.date)
And it's all working, except for one thing: the highlight is applied to the next date from the one I click on. I'm sure this is because the line above uses model.date instead of the local date. I do create the local date in my onClicked() slot:
var offset = dateClicked.getTimezoneOffset() var localDate = new Date(dateClicked.getTime() + (offset * 60000))
But, I don't know how to expose this JS variable to my QML so I can use it in the above property setting. Can anyone help with this?
-
@mzimmers I don't like dates and timezones. My approach would be something like this:
property list<int> selectedUnixDates function getLocaleUnix(date) { return (date.getTime() / 1000) + (date.getTimezoneOffset() * 60) } MonthGrid { id: monthGrid anchors.fill: parent delegate: Item { id: delegateItem property int localeUnix: getLocaleUnix(model.date) property bool isSelectedDay: selectedUnixDates.includes(localeUnix) Text { anchors.centerIn: parent color: delegateItem.isSelectedDay ? "green" : "red" font.bold: delegateItem.isSelectedDay text: model.day } } onClicked: date => { let localeUnix = getLocaleUnix(date) console.debug("UNIX:", localeUnix) console.debug("DATE:", new Date(localeUnix * 1000)) /* clicked on August 16 from client timezone -5 UNIX: 1692162000 DATE: Wed Aug 16 00:00:00 2023 GMT-0500 clicked on August 16 from client timezone +2 UNIX: 1692136800 DATE: Wed Aug 16 00:00:00 2023 GMT+0200 */ let matchIndex = selectedUnixDates.indexOf(localeUnix) if (matchIndex === -1) { selectedUnixDates.push(localeUnix) return } selectedUnixDates.splice(matchIndex, 1) } }
-
@lemons said in issues with MonthGrid:
I don't like dates and timezones.
Neither do I, but I think I need to find a solution that uses them. This is no longer a MonthGrid issue per se, so I'm going to mark this as solved. Thanks for the help.
EDIT:
I got it working with the following changes: first I added an offset parameter to my isSelected() function:
function isSelected(date, offset) { let rc = false let len = selectedDates.length date.setMinutes(date.getMinutes() + offset) for (let i = 0; i < len; i++) { let entry = selectedDates[i] if ( entry.getDate() === date.getDate() && entry.getMonth() === date.getMonth() && entry.getFullYear() === date.getFullYear() ) { rc = true break } } return rc }
and it's used like so:
MonthGrid { id: monthGrid readonly property int offset: new Date().getTimezoneOffset() property date localDate delegate: Rectangle { id: dayDelegate Rectangle { id: highlightRect visible: isSelected(model.date, monthGrid.offset) } }
Seems to work fine.
Editorial: JS is a maximum PITA. I didn't get to the bottom of this issue until I realized how blithely JS changes variable types at the drop of a hat. In my case, it was cheerfully changing objects to numbers to strings, when I didn't want any of that. I realize that to JS people, "it's not a bug, it's a feature," but to me, it's a nuisance.
-
-
@mzimmers I think I should have explained my previous example :D
To check if the delegate date is in the list of selected dates, you have to do the same formatting in the onClicked as within the delegate, as the delegate date property is the same as the one that gets emitted in the onClicked event.
I did the formatting with this function, which gives me the unix timestamp of the start of the date in the client timezone of that day (you might have to read this twice to understand what I want to say):
function getLocaleUnix(date) { return (date.getTime() / 1000) + (date.getTimezoneOffset() * 60) }
I personally try to avoid date objects in all languages and also store dates as unix timestamps in the databases, as working with integers is way easier and more uniform across different languages.
As I converted the dates to an integer, I use a list<int> to store the selected dates, which also offers me to use the JS includes() method, so there is no need of a hard-to-read and maybe more costly method to check if the date is selected.
property list<int> selectedUnixDates
id: delegateItem property int localeUnix: getLocaleUnix(model.date) property bool isSelectedDay: selectedUnixDates.includes(localeUnix)
Also note, your code might not work in all circumstances:
MonthGrid { id: monthGrid readonly property int offset: new Date().getTimezoneOffset()
e.g. we have winter and summer times, so our timezone offset varies by 1h, depending on the season.
→ If a user opens the calendar and selects a date in the other season, you might get the date of the day before at 11PM (or the same date but 1AM), as the timezone offset of the monthgrid is related to the current date (date of using the calendar input) and not the selected date / delegate date.EDIT:
The console.debug() in my previous example should show how to hook into your other logic by either passing a date or a unix timestamp to C++ or other JS methods.