問題描述
Python 事件集成中的 QML Calendar 和 Google Calendar API (QML Calendar and Google Calendar API in Python events integration)
我目前正在創建一個 QML 日曆,它可以理想地顯示來自 Google 日曆的事件。
這是我想在 python 中模擬的示例:https://doc.qt.io/qt‑5.9/qtquickcontrols‑calendar‑example.html
現在,一個 python 文件正在從 Google Calendar API 獲取日期和事件摘要,並將它們作為 {date: event summary} 的字典(python 代碼中的 tenevents 變量)返回. 我有一個非常簡單的 QML 窗口,顯示一個日曆和一個矩形。我想單擊日曆上的一個日期,並讓該日期的事件顯示在矩形中,並標記有事件的日期。我想我有我需要的大部分數據,我只是不知道如何使用它。我不確定如何在單擊時提取日期信息,也不確定如何傳遞 python 字典並顯示我想要的內容——我感謝任何有助於文檔的方向和/或指針!
我'已經在下麵包含了我的代碼!
getevents() 返回如下內容:
{'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