PDA

View Full Version : Stuck with QNetworkManager and Q_INVOKABLE



KeineAhnung
6th December 2015, 14:29
Hi,

I am really stuck with my code. My plan is to write a c++ class for my mobile app (Sailfish OS) that looks if a xml file is stored on the device, if so if is up to date by comparing it with one on a web server. When not up to date than to download it. The result shall be display with QML.
The class is done and does what I want but I have trouble to integrate it into QML. One thing is that some functions are accessible from QML and others are not and I do not see why. Accessible are load(), replyFinished() and getHeaders(). The other is that the return of load() is emitted before the network things are done. How can I change that?

.h


#ifndef XMLFILEHANDLER_H
#define XMLFILEHANDLER_H

#include <QObject>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QStandardPaths>
#include <QTextStream>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QDateTime>
#include <QDebug>

class XmlFileHandler : public QObject
{
Q_OBJECT
public:
explicit XmlFileHandler(QObject *parent = 0);
Q_PROPERTY(int fileStat READ fileStat WRITE setFileStat NOTIFY fileStatChanged)

Q_INVOKABLE int load();
Q_INVOKABLE int fileStat();
Q_INVOKABLE QString filePath();

// Q_INVOKABLE bool setFilePath(const QString &file);

void setFileStat(int curStat);

QString fileName;

signals:
void fileStatChanged(int stat);

public slots:
void replyFinished (QNetworkReply *reply);
void getHeaders (QNetworkReply *reply);

private:
int stat;
QDir fileLocation;
QUrl url;
QFileInfo xmlInfo;
QNetworkAccessManager *manager;
QNetworkAccessManager *headManager;
};

#endif // XMLFILEHANDLER_H

.cpp


#include "xmlfilehandler.h"

XmlFileHandler::XmlFileHandler(QObject *parent) : QObject(parent){
fileLocation = QDir(QStandardPaths::writableLocation(QStandardPat hs::DataLocation));
url = QUrl("http://myURI/my.xml");
xmlInfo = QFileInfo(fileLocation.filePath("my.xml"));

stat = 0;
}

int XmlFileHandler::load(){
manager = new QNetworkAccessManager(this);
headManager = new QNetworkAccessManager(this);
connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*)));
connect(headManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(getHeaders(QNetworkReply*)));

if (xmlInfo.exists()) {
// Check if local file is up to date
headManager->head(QNetworkRequest(url));
} else {
qDebug() << "Need to download something here";
qDebug() << QDir().mkpath(fileLocation.path());
manager->get(QNetworkRequest(url));
}
qDebug() << stat;
return stat;
}

void XmlFileHandler::replyFinished(QNetworkReply *reply){
if(reply->error()) {
qDebug() << "ERROR!";
qDebug() << reply->errorString();
stat = -1;
fileStatChanged(stat);
}else{
QFile *file = new QFile(fileLocation.filePath(my.xml"));
if(file->open(QIODevice::ReadWrite | QIODevice::Truncate)){
QTextStream out(file);
out << reply->readAll();
qDebug() << out.status();
file->close();
stat = 2;
fileStatChanged(stat);
}else{
qDebug() << "Error opening file" << file->errorString();
stat = -1;
fileStatChanged(stat);
}
}
reply->deleteLater();
}

void XmlFileHandler::getHeaders(QNetworkReply *reply){
if (reply->operation() == QNetworkAccessManager::HeadOperation){
qDebug() << reply->header(QNetworkRequest::LastModifiedHeader).toDate Time();
qDebug() << xmlInfo.lastModified().toLocalTime();
if(xmlInfo.lastModified().toLocalTime()>reply->header(QNetworkRequest::LastModifiedHeader).toDate Time()){
qDebug() << "No need for Update";
stat = 1;
fileStatChanged(stat);
}else{
qDebug() << "Update required";
manager->get(QNetworkRequest(url));
}
}else{
stat = -1;
fileStatChanged(stat);
}
reply->deleteLater();
}

QString XmlFileHandler::filePath(){
return fileLocation.filePath("ausbildung.xml");
}
int XmlFileHandler::fileStat(){
return stat;
}

void XmlFileHandler::setFileStat(const int curStat){
stat = curStat;
fileStatChanged(stat);
}


main.qml


import QtQuick 2.0
import Sailfish.Silica 1.0
import "pages"

import harbour.Test 1.0

ApplicationWindow
{
initialPage: Component { FirstPage { } }
cover: Qt.resolvedUrl("cover/CoverPage.qml")
allowedOrientations: Orientation.All
_defaultPageOrientations: Orientation.All

XmlFileHandler {
id: handler;
}
}


page.qml


import QtQuick 2.0
import Sailfish.Silica 1.0


Page {
id: page

// To enable PullDownMenu, place our content in a SilicaFlickable
SilicaFlickable {
anchors.fill: parent

// PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView
PullDownMenu {
MenuItem {
text: qsTr("Check for Update")
onClicked: {
handler.load()
console.log(handler.fileStat()) //<- Not working
}

}
}

// Tell SilicaFlickable the height of its content.
contentHeight: column.height

// Place our content in a Column. The PageHeader is always placed at the top
// of the page, followed by our content.
Column {
id: column

width: page.width
spacing: Theme.paddingLarge
PageHeader {
title: qsTr("UI Template")
}

Text {
id: stat
color: Theme.secondaryHighlightColor
font.pixelSize: Theme.fontSizeSmall
width: parent.width-Theme.paddingLarge
wrapMode: Text.Wrap
text: "hier:"+handler.filePath() // <- not picked up by auto complete but works
}
}
}
}


Thanks

anda_skoa
6th December 2015, 16:22
QNetworkAccessManager's operations are asynchronous.
I would recommend to use that instead of trying to make it look synchronous.

How do you process the XML file?

Also:
- don't create the QFile on the heap if you don't need it across methods. currently you are leaking it
- don't create the network access managers in load without checking if you have already done so, otherwise you create new ones every time you call load()
- you could just use one network access manager if you connect to the reply's finished signal
- call setFileStat instead of manually setting the variable and manually emitting the change signal.
- make setFileStat check if it actually has a change before emitting the change signal

Cheers,
_

KeineAhnung
6th December 2015, 17:14
Thanks for the hints.
I read that the network manager works asynchronous but I do not know what to look for to make it work the way I need it to. Can I build in a wait or progress feature bevor return is sent? Is it possible to build in a onStatChanged method that QML can see?
Currently I am not processing the xml file. This part is missing at the moment but will be done within QML. I have done it once using XmlListModel. I hope I can set a local path as source. But I need to make sure that I have to updated file first.

1.) I don't understand this. QFile is only created when I want to write something
2.) It would be better to put them in XmlFileHandler::XmlFileHandler(QObject *parent) : QObject(parent){?
3.) I have tried that but then get and header was triggered on load. How can I separate this?

Any clue why not all methods are picked up in QML?

anda_skoa
6th December 2015, 17:29
I read that the network manager works asynchronous but I do not know what to look for to make it work the way I need it to. Can I build in a wait or progress feature bevor return is sent?

Yes, but that would be a hack. As I said I would suggest to let it work asynchronously as intended.
Also more declarative.



Is it possible to build in a onStatChanged method that QML can see?

That is already the case, a property has a change signal and each signal has a signal handler.



Currently I am not processing the xml file. This part is missing at the moment but will be done within QML. I have done it once using XmlListModel. I hope I can set a local path as source.

XmlListModel's source can be a local file.



1.) I don't understand this. QFile is only created when I want to write something

It is only needed within a certain block. No need to create it on the heap.
Especially not without deleting it.



2.) It would be better to put them in XmlFileHandler::XmlFileHandler(QObject *parent) : QObject(parent){?

Yes, that would work as well.



3.) I have tried that but then get and header was triggered on load. How can I separate this?

Connect to different slots.



Any clue why not all methods are picked up in QML?

What do you mean?

Cheers,
_

KeineAhnung
6th December 2015, 17:39
3.) They are already connected to different slots. But since the signal is the same both slots are triggered, aren't they?

Regarding the QML. Some function are picked up by auto complete and some not. I do not know what I did wrong that for example the stat value cannot be accessed by QML and therefore no onStatChanged...

ChrisW67
6th December 2015, 19:43
Editor autocomplete has nothing to do with whether the symbols are good for a compiler or the QML engine.
Do the symbols work if you simply type them?
Have you rerun qmake and rebuilt since you last changed the PROPERTY or Q_INVOKABLE macros?

KeineAhnung
6th December 2015, 20:21
I redid the project completely and more or even all methods are picked up in QML. Before I tried the clean and build all option from Qt bur this did not help. I did not try qmake.

Thanks for the help so far. I think I got the app working now. But I am not sure if all cases are covered. So in case there is no network connection etc.

If there are more suggestions on how to improve, I am thankful for everything =)

anda_skoa
6th December 2015, 21:41
3.) They are already connected to different slots. But since the signal is the same both slots are triggered, aren't they?

No, each reply object has its "own" signal, after all the very first argument of the connect() call is the sender object,.

Cheers,
_

KeineAhnung
7th December 2015, 14:35
Hi,

I still do net get how to do it with only one network manager. If I write the code like this:


manager = new QNetworkAccessManager(this);
connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*)));
connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(getHeaders(QNetworkReply*)));

I end up with an empty xml file after I reopen the app. I think this is because the replyFinished slot is triggered with the getHeaders SLOT. At least I that the console outputs of both functions. Since the file is opened with open(QIODevice::ReadWrite | QIODevice::Truncate) the xml file is cleared. Since I only asked for the headers the stream is empty and nothing is written to the file.
For me that is clear because I connected the same signal to two slots. How do I need to write this if I only want to use on network manager?

Thanks

anda_skoa
7th December 2015, 14:47
Hi,

I still do net get how to do it with only one network manager. If I write the code like this:


manager = new QNetworkAccessManager(this);
connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*)));
connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(getHeaders(QNetworkReply*)));


That will of course not work, this is the same sender/signal combination, so of course both slots are going to be executed upon emission.

Hence me writing:


- you could just use one network access manager if you connect to the reply's finished signal


Cheers,
_