sun
sun

Reputation: 49

Connecting QML and Python where text is sent by a button click to a python function, and the result prints in QML

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_())

Upvotes: 1

Views: 1595

Answers (1)

eyllanesc
eyllanesc

Reputation: 244132

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()

Upvotes: 1

Related Questions