1

I'm very new to QML and somewhat confused on how to properly connect more than one element across python and QML (I'm using python 2.7!). I have python script to print weather data, and a QML application that is supposed to take in the input as "cityname."

The user theoretically types a city into the textfield, hits the button, and python takes the input, finds the weather data, and prints it to the QML window. I'm struggling with how the connections with textfield+button+pythonfunction will work! The python function without QML works and the QML produces a window with a textinput and a button.

Here's my code:

QML (weather5.qml)

import QtQuick 2.0
import QtQuick.Controls 2.1
import QtQuick.Window 2.2

ApplicationWindow {
    title: qsTr("Test")
    width: 300
    height: 450
    visible: true
    Column {
        spacing: 20
            TextField {
                placeholderText: qsTr("City")
                echoMode: TextInput.City
                id: city
                selectByMouse: true
                }
            ListView{
                model: cityy
                id: hi
                delegate: Text { text: city.display }
            }
            Button {
                signal messageRequired
                objectName: "myButton"
                text: "Search"
                onClicked: {
                    print(hi)
                    }
            }
    }
    Connections {
        target: 
        }
}

and here's the python!! (pyweather.py)

import requests, json, os
from PyQt5.QtQml import QQmlApplicationEngine, QQmlEngine, QQmlComponent, qmlRegisterType
from PyQt5.QtCore import QUrl, QObject, QCoreApplication, pyqtProperty, QStringListModel, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QGuiApplication
import sys

class City(QObject):
    def __init__(self):
        QObject.__init__(self)

    enterCity = pyqtSignal(str, arguments=["weat"])
    @pyqtSlot(str)
    def weat(self, city_name):
        api_key = "key" #I've excluded my key for this post
        base_url = "http://api.openweathermap.org/data/2.5/weather?"
        complete_url = "http://api.openweathermap.org/data/2.5/weather?q=" + city_name + api_key
        response = requests.get(complete_url)
        x = response.json()


        if x["cod"] != "404":

            res = requests.get(complete_url)
            data = res.json()
            temp = data['main']['temp']
            description = data['weather'][0]['description']

            print('Temperature : {} degree Kelvin'.format(temp))

            rett = ['Temperature : ' + str(temp) + " degree Kelvin"]
            return rett
            self.enterCity.emit(rett)

        else:
            print(" City Not Found ")

app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
city = City()
engine.rootContext().setContextProperty("cityy", city)
engine.load(QUrl.fromLocalFile('weather5.qml'))
if not engine.rootObjects():
    sys.exit(-1)
sys.exit(app.exec_())

1 Answer 1

1

The logic is to return the information through a signal or a property, in this case I will show how to return the information through a property.

As it has to update to some element of the QML then it has to notify then it must have associated to a signal. On the other hand, you should not use requests since it can block the eventloop (and freeze the GUI).

Considering the above, the solution is:

main.py

from functools import cached_property
import json

from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QUrlQuery
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine

import logging

logging.basicConfig(level=logging.DEBUG)



class WeatherWrapper(QObject):
    BASE_URL = "http://api.openweathermap.org/data/2.5/weather"

    dataChanged = pyqtSignal()

    def __init__(self, api_key; str ="", parent: QObject = None) -> None:
        super().__init__(parent)
        self._data = dict()
        self._has_error = False
        self._api_key = api_key

    @cached_property
    def manager(self) -> QNetworkAccessManager:
        return QNetworkAccessManager(self)

    @property
    def api_key(self):
        return self._api_key

    @api_key.setter
    def api_key(self, key):
        self._api_key = key

    @pyqtProperty("QVariantMap", notify=dataChanged)
    def data(self) -> dict:
        return self._data

    @pyqtSlot(result=bool)
    def hasError(self):
        return self._has_error

    @pyqtSlot(str)
    def update_by_city(self, city: str) -> None:

        url = QUrl(WeatherWrapper.BASE_URL)
        query = QUrlQuery()
        query.addQueryItem("q", city)
        query.addQueryItem("appid", self.api_key)
        url.setQuery(query)

        request = QNetworkRequest(url)
        reply: QNetworkReply = self.manager.get(request)
        reply.finished.connect(self._handle_reply)

    def _handle_reply(self) -> None:
        has_error = False
        reply: QNetworkReply = self.sender()
        if reply.error() == QNetworkReply.NoError:
            data = reply.readAll().data()
            logging.debug(f"data: {data}")
            d = json.loads(data)
            code = d["cod"]
            if code != 404:
                del d["cod"]
                self._data = d
            else:
                self._data = dict()
                has_error = True
                logging.debug(f"error: {code}")
        else:
            self._data = dict()
            has_error = True
            logging.debug(f"error: {reply.errorString()}")
        self._has_error = has_error
        self.dataChanged.emit()
        reply.deleteLater()


def main():
    import os
    import sys

    CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))

    app = QGuiApplication(sys.argv)

    API_KEY = "API_HERE"

    weather = WeatherWrapper()
    weather.api_key = API_KEY

    engine = QQmlApplicationEngine()
    engine.rootContext().setContextProperty("weather", weather)

    filename = os.path.join(CURRENT_DIR, "main.qml")
    engine.load(QUrl.fromLocalFile(filename))

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

main.qml

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

ApplicationWindow {
    title: qsTr("Weather App")
    width: 300
    height: 450
    visible: true
    ColumnLayout {
        anchors.fill: parent
        spacing: 20
        TextField {
            id: city_tf
            placeholderText: qsTr("City")
            Layout.alignment: Qt.AlignHCenter
            font.pointSize:14
            selectByMouse: true
        }
        Button {
            text: "Search"
            Layout.alignment: Qt.AlignHCenter
            onClicked: {
                weather.update_by_city(city_tf.text)
            }
        }
        Label{
            Layout.alignment: Qt.AlignHCenter
            id: result_lbl
        }
        Item {
            Layout.fillHeight: true
        }
    }

    Connections {
        target: weather
        function onDataChanged(){
            if(!weather.hasError()){
                var temperature = weather.data['main']['temp']
                result_lbl.text = "Temperature : " + temperature + " degree Kelvin"
            }
        }
    }
}

Python2 syntax:

Note: Install cached_property(python2.7 -m pip install cached_property)

from cached_property import cached_property
import json

from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QUrlQuery
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PyQt5.QtGui import QGuiApplication
from PyQt5.QtQml import QQmlApplicationEngine

import logging

logging.basicConfig(level=logging.DEBUG)


class WeatherWrapper(QObject):
    BASE_URL = "http://api.openweathermap.org/data/2.5/weather"

    dataChanged = pyqtSignal()

    def __init__(self, api_key="", parent=None):
        super(WeatherWrapper, self).__init__(parent)
        self._data = {}
        self._has_error = False
        self._api_key = api_key

    @cached_property
    def manager(self):
        return QNetworkAccessManager(self)

    @property
    def api_key(self):
        return self._api_key

    @api_key.setter
    def api_key(self, key):
        self._api_key = key

    @pyqtProperty("QVariantMap", notify=dataChanged)
    def data(self):
        return self._data

    @pyqtSlot(result=bool)
    def hasError(self):
        print(self._has_error)
        return self._has_error

    @pyqtSlot(str)
    def update_by_city(self, city):

        url = QUrl(WeatherWrapper.BASE_URL)
        query = QUrlQuery()
        query.addQueryItem("q", city)
        query.addQueryItem("appid", self.api_key)
        url.setQuery(query)

        request = QNetworkRequest(url)
        reply = self.manager.get(request)
        reply.finished.connect(self._handle_reply)

    def _handle_reply(self):
        has_error = False
        reply = self.sender()
        if reply.error() == QNetworkReply.NoError:
            data = reply.readAll().data()
            logging.debug("data: {}".format(data))
            d = json.loads(data)
            code = d["cod"]
            if code != 404:
                del d["cod"]
                self._data = d
            else:
                self._data = {}
                has_error = True
                logging.debug("error: {}".format(code))
        else:
            self._data = {}
            has_error = True
            logging.debug("error: {}".format(reply.errorString()))
        self._has_error = has_error
        self.dataChanged.emit()
        reply.deleteLater()


def main():
    import os
    import sys

    CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))

    app = QGuiApplication(sys.argv)

    API_KEY = "API_HERE"

    weather = WeatherWrapper()
    weather.api_key = API_KEY

    engine = QQmlApplicationEngine()
    engine.rootContext().setContextProperty("weather", weather)

    filename = os.path.join(CURRENT_DIR, "main.qml")
    engine.load(QUrl.fromLocalFile(filename))

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
Sign up to request clarification or add additional context in comments.

9 Comments

hi! thank you so much for your comprehensive response!! I'm actually working in python 2.7 because of a technical restriction, and im not sure if it's related but "->" is a syntax error. Could I ask if you knew any way to resolve this? thanks again!!
@sun Delete the -> Foo. I always recommend you indicate the version of SW you use since in general we will assume that the latest version of each SW is being used.
I'll definitely keep that in mind, thank you!! I'm so sorry to ask again but I'm also getting syntax errors on all the colons within the def parameters, (ie parent: QObject = None), as well as errors in all the places "reply" is referenced and I can't find any documentation as to why!
@sun Again, the python version points out, it is annoying to modify the code after writing it. In a few moments I will provide a compatible code like python2.7, which version of PySide2 do you use?
Ahh im so sorry, I truly appreciate it so much!! I'm using PyQt5, although I can also obtain/transition to PySide2 if needed!
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.