问题
I'm currently working on creating a QML Calendar that will ideally display events from a Google calendar.
This is an example of what I'd like to emulate in python: https://doc.qt.io/qt-5.9/qtquickcontrols-calendar-example.html
Right now, a python file is taking dates and event summaries from the Google Calendar API and returning them as a dictionary (the tenevents variable in the python code) of {date: event summary}. I have a very simple QML window displaying a calendar and a rectangle. I would like to click a date on the calendar and have the events for that date show up in the rectangle, and mark the days that have events. I think I have most of the data I need, I'm just not sure how to use it. I'm not sure how to extract date info onClick, nor am I sure how to pass the python dictionary and display what I'd like—I appreciate any direction and/or pointers towards helpful documentation!
I've included my code below!
getevents() returns something like this:
{'2020-07-30T13:00:00-05:00': 'Buy a Single Olive', '2020-07-31T10:00:00-05:00': 'Jarvis', '2020-08-02': 'Peel an Apple', '2020-08-04T02:30:00-05:00': 'Eat Two Crisp Grapes'}
python.py:
from __future__ import print_function
import datetime
import pickle
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
import os.path
import os
from PySide2 import QtCore
from PySide2.QtCore import Property, Signal, Slot, QObject, QUrl, QUrlQuery
from PySide2 import QtGui
from PySide2 import QtQml
class CalendarModule(QtCore.QObject):
def __init__(self):
super(CalendarModule, self).__init__()
self.scopes = ['https://www.googleapis.com/auth/calendar']
self.service = self.use_token_pickle_to_get_service()
def use_token_pickle_to_get_service(self):
creds = None
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
service = build('calendar', 'v3', credentials=creds)
return service
@Slot(result='QVariant')
def getevents(self):
# Call the Calendar API
now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
events_result = self.service.events().list(calendarId='primary', timeMin=now,
maxResults=10, singleEvents=True,
orderBy='startTime').execute()
events = events_result.get('items', [])
tenevents = {}
if not events:
return 'No upcoming events found.'
for event in events:
start = event['start'].get('dateTime', event['start'].get('date'))
tenevents[start] = event['summary']
return tenevents
def main():
import os
import sys
CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
app = QtGui.QGuiApplication(sys.argv)
engine = QtQml.QQmlApplicationEngine()
filename = os.path.join(CURRENT_DIR, "calendar2.qml")
cal = CalendarModule()
engine.rootContext().setContextProperty("Cal", cal)
engine.load(QUrl.fromLocalFile(filename))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
if __name__ == '__main__':
print(CalendarModule().getevents())
main()
calendar2.qml:
import QtQuick 2.3
import QtQuick.Controls.Styles 1.2
import QtQuick.Layouts 1.11
ApplicationWindow {
title: qsTr("Calendar")
width: 700
height: 400
visible: true
RowLayout {
anchors.fill: parent
Rectangle {
id: tasks
Layout.fillWidth: true
Layout.fillHeight: true
height: 400
width: 150
color: "white"
Text {
anchors.horizontalCenter: parent.horizontalCenter
font.bold: true
font.pointSize:14
text: 'To-Do'
}
}
Calendar {
Layout.fillWidth: true
Layout.fillHeight: true
frameVisible: true
style: CalendarStyle {
gridVisible: false
dayDelegate: Rectangle {
gradient: Gradient {
GradientStop {
position: 0.00
color: styleData.selected ? "#111" : (styleData.visibleMonth && styleData.valid ? "#444" : "#666");
}
GradientStop {
position: 1.00
color: styleData.selected ? "#444" : (styleData.visibleMonth && styleData.valid ? "#111" : "#666");
}
GradientStop {
position: 1.00
color: styleData.selected ? "#444" : (styleData.visibleMonth && styleData.valid ? "#111" : "#666");
}
}
Label {
text: styleData.date.getDate()
anchors.centerIn: parent
color: styleData.valid ? "white" : "white"
}
Rectangle {
width: parent.width
height: 1
color: "#555"
anchors.bottom: parent.bottom
}
Rectangle {
width: 1
height: parent.height
color: "#555"
anchors.right: parent.right
}
}
}
}
}
}
回答1:
Before modifying any code you must understand the code and understand that it is different with what you want. And in your case there is a difference: The data is not accessed quickly but you have to make a request that is time consuming and can block the GUI.
So before the above it is better to download the events (perhaps save it in a database like sqlite and use the original example) to have it as a cache and thus minimize the amount of downloads.
Then you must separate the logic: Create a class that exports the information to QML, and another class that obtains it.
Since the information will be reloaded every time there is an event, it is better to use a Loader that redraws the GUI every time the cache is updated.
Note: I have used the official example
main.py
import logging
import os
import pickle
import sys
import threading
from PySide2 import QtCore, QtGui, QtQml
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"]
CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
logging.basicConfig(level=logging.DEBUG)
class CalendarBackend(QtCore.QObject):
eventsChanged = QtCore.Signal(list)
def __init__(self, parent=None):
super().__init__(parent)
self._service = None
@property
def service(self):
return self._service
def updateListEvents(self, kw):
threading.Thread(target=self._update_list_events, args=(kw,)).start()
def _update_list_events(self, kw):
self._update_credentials()
events_result = self.service.events().list(**kw).execute()
events = events_result.get("items", [])
qt_events = []
if not events:
logging.debug("No upcoming events found.")
for event in events:
start = event["start"].get("dateTime", event["start"].get("date"))
end = event["end"].get("dateTime", event["end"].get("date"))
logging.debug(f"From {start} - To {end}: {event['summary']}")
start_dt = QtCore.QDateTime.fromString(start, QtCore.Qt.ISODate)
end_dt = QtCore.QDateTime.fromString(end, QtCore.Qt.ISODate)
summary = event["summary"]
e = {"start": start_dt, "end": end_dt, "summary": summary}
qt_events.append(e)
self.eventsChanged.emit(qt_events)
def _update_credentials(self):
creds = None
if os.path.exists("token.pickle"):
with open("token.pickle", "rb") as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json", SCOPES
)
creds = flow.run_local_server(port=0)
with open("token.pickle", "wb") as token:
pickle.dump(creds, token)
self._service = build("calendar", "v3", credentials=creds)
class CalendarProvider(QtCore.QObject):
loaded = QtCore.Signal()
def __init__(self, parent=None):
super().__init__(parent)
self._cache_events = []
self._backend = CalendarBackend()
self._backend.eventsChanged.connect(self._handle_events)
@QtCore.Slot("QVariant")
def updateListEvents(self, parameters):
d = dict()
for k, v in parameters.toVariant().items():
if isinstance(v, QtCore.QDateTime):
v = v.toTimeSpec(QtCore.Qt.OffsetFromUTC).toString(
QtCore.Qt.ISODateWithMs
)
d[k] = v
self._backend.updateListEvents(d)
@QtCore.Slot(QtCore.QDate, result="QVariantList")
def eventsForDate(self, date):
events = []
for event in self._cache_events:
start = event["start"]
if start.date() == date:
events.append(event)
return events
@QtCore.Slot(list)
def _handle_events(self, events):
self._cache_events = events
self.loaded.emit()
logging.debug("Loaded")
def main():
app = QtGui.QGuiApplication(sys.argv)
QtQml.qmlRegisterType(CalendarProvider, "MyCalendar", 1, 0, "CalendarProvider")
engine = QtQml.QQmlApplicationEngine()
filename = os.path.join(CURRENT_DIR, "main.qml")
engine.load(QtCore.QUrl.fromLocalFile(filename))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
if __name__ == "__main__":
main()
main.qml
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Controls.Private 1.0
import QtQuick.Controls.Styles 1.1
import MyCalendar 1.0
ApplicationWindow {
visible: true
width: 640
height: 400
minimumWidth: 400
minimumHeight: 300
color: "#f4f4f4"
title: "Calendar Example"
SystemPalette {
id: systemPalette
}
CalendarProvider {
id: eventModel
onLoaded: {
// reload
loader.sourceComponent = null
loader.sourceComponent = page_component
}
Component.onCompleted: {
eventModel.updateListEvents({
calendarId: "primary",
timeMin: new Date(),
maxResults: 10,
singleEvents: true,
orderBy: "startTime",
})
}
}
Loader{
id: loader
anchors.fill: parent
sourceComponent: page_component
}
Component{
id: page_component
Flow {
id: row
anchors.fill: parent
anchors.margins: 20
spacing: 10
layoutDirection: Qt.RightToLeft
Calendar {
id: calendar
width: (parent.width > parent.height ? parent.width * 0.6 - parent.spacing : parent.width)
height: (parent.height > parent.width ? parent.height * 0.6 - parent.spacing : parent.height)
frameVisible: true
weekNumbersVisible: true
selectedDate: new Date()
focus: true
style: CalendarStyle {
dayDelegate: Item {
readonly property color sameMonthDateTextColor: "#444"
readonly property color selectedDateColor: Qt.platform.os === "osx" ? "#3778d0" : systemPalette.highlight
readonly property color selectedDateTextColor: "white"
readonly property color differentMonthDateTextColor: "#bbb"
readonly property color invalidDatecolor: "#dddddd"
Rectangle {
anchors.fill: parent
border.color: "transparent"
color: styleData.date !== undefined && styleData.selected ? selectedDateColor : "transparent"
anchors.margins: styleData.selected ? -1 : 0
}
Image {
visible: eventModel.eventsForDate(styleData.date).length > 0
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: -1
width: 12
height: width
source: "images/eventindicator.png"
}
Label {
id: dayDelegateText
text: styleData.date.getDate()
anchors.centerIn: parent
color: {
var color = invalidDatecolor;
if (styleData.valid) {
// Date is within the valid range.
color = styleData.visibleMonth ? sameMonthDateTextColor : differentMonthDateTextColor;
if (styleData.selected) {
color = selectedDateTextColor;
}
}
color;
}
}
}
}
}
Component {
id: eventListHeader
Row {
id: eventDateRow
width: parent.width
height: eventDayLabel.height
spacing: 10
Label {
id: eventDayLabel
text: calendar.selectedDate.getDate()
font.pointSize: 35
}
Column {
height: eventDayLabel.height
Label {
readonly property var options: { weekday: "long" }
text: Qt.locale().standaloneDayName(calendar.selectedDate.getDay(), Locale.LongFormat)
font.pointSize: 18
}
Label {
text: Qt.locale().standaloneMonthName(calendar.selectedDate.getMonth())
+ calendar.selectedDate.toLocaleDateString(Qt.locale(), " yyyy")
font.pointSize: 12
}
}
}
}
Rectangle {
width: (parent.width > parent.height ? parent.width * 0.4 - parent.spacing : parent.width)
height: (parent.height > parent.width ? parent.height * 0.4 - parent.spacing : parent.height)
border.color: Qt.darker(color, 1.2)
ListView {
id: eventsListView
spacing: 4
clip: true
header: eventListHeader
anchors.fill: parent
anchors.margins: 10
model: eventModel.eventsForDate(calendar.selectedDate)
delegate: Rectangle {
width: eventsListView.width
height: eventItemColumn.height
anchors.horizontalCenter: parent.horizontalCenter
Image {
anchors.top: parent.top
anchors.topMargin: 4
width: 12
height: width
source: "images/eventindicator.png"
}
Rectangle {
width: parent.width
height: 1
color: "#eee"
}
Column {
id: eventItemColumn
anchors.left: parent.left
anchors.leftMargin: 20
anchors.right: parent.right
height: timeLabel.height + nameLabel.height + 8
Label {
id: nameLabel
width: parent.width
wrapMode: Text.Wrap
text: modelData["summary"]
}
Label {
id: timeLabel
width: parent.width
wrapMode: Text.Wrap
text: modelData.start.toLocaleTimeString(calendar.locale, Locale.ShortFormat) + "-" + modelData.end.toLocaleTimeString(calendar.locale, Locale.ShortFormat)
color: "#aaa"
}
}
}
}
}
}
}
}
The full example is here
来源:https://stackoverflow.com/questions/62927504/qml-calendar-and-google-calendar-api-in-python-events-integration