PDA

View Full Version : [QT 5.0] Downloading from a URL then saving to client-defined path/filename?



Salads
20th January 2013, 06:28
I'm trying to make a program that downloads an image and saves it to the disk. Using C++ I've followed a reference for the downloading of a file, but how would I save it to disk? My code as I have it now gives me allot of unresolved external symbols. When I comment out lines 40 - 44 everything compiles successfully..:(

My Code: (I ended up putting everything under the button clicked function)


#include "minecraftskindownloader.h"
#include "ui_minecraftskindownloader.h"
#include <QWidget>
#include <QObject>
#include <QtNetwork/QNetworkAccessManager>
#include <QFileDialog>
#include <C:\Qt\Qt5.0.0\5.0.0\msvc2010\include\QtNetwork\QNe tworkRequest>
#include <QDebug>
#include <QUrl>
#include <QFile>

MinecraftSkinDownloader::MinecraftSkinDownloader(Q Widget *parent) :
QMainWindow(parent),
ui(new Ui::MinecraftSkinDownloader)
{
ui->setupUi(this);
this->setWindowTitle("Minecraft Skin Downloader");
setFixedSize(400, 200);
}

MinecraftSkinDownloader::~MinecraftSkinDownloader( )
{
delete ui;
}

void MinecraftSkinDownloader::on_actionClose_triggered( )
{
qApp->quit();
}
//http://s3.amazonaws.com/MinecraftSkins/USERNAMEHERE.png

void MinecraftSkinDownloader::on_pushButton_clicked()
{
QString prefix = "http://s3.amazonaws.com/MinecraftSkins/";
QString url = prefix += ui->lineEdit->text();
url += ".png";

ui->textEdit->setText(url); //Just testing the link part

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

manager->get(QNetworkRequest(QUrl(url)));

QString fileName = QFileDialog::getSaveFileName(this, tr("Save"),"/Documents",tr("Images (*.png)"));

}

ChrisW67
20th January 2013, 07:27
You are probably missing:


QT += network

in your pro file.

Get your desired file name and open a file for writing. Store the QFile instance as a member variable.
Connect the QNetworkReply object readyRead() signal to a slot that can handle it.
In your readyRead() handler write all received bytes to the file.
In your finished() handler: If there has been no error then simply close the file, otherwise delete the partial file and report the error.

Salads
20th January 2013, 11:00
Thanks for the reply, but I'm having a bit of trouble understanding the pseudo-code.. Where do I start with the readyRead() signal? I also decided to add a separate button for a "Save As" function.
Which is the QNetworkReply Object? I was thinking about storing everything in a QByteArray and then streaming that to the filebut I don't have an idea how to do that since I don't know what variable the downloaded data is stored..
I might as well and come out that I'm almost a complete beginner, starting QT only a couple days ago..

Here's my code so far:


#ifndef MINECRAFTSKINDOWNLOADER_H
#define MINECRAFTSKINDOWNLOADER_H

#include <QMainWindow>

namespace Ui {
class MinecraftSkinDownloader;
}

class MinecraftSkinDownloader : public QMainWindow
{
Q_OBJECT

public:
explicit MinecraftSkinDownloader(QWidget *parent = 0);
~MinecraftSkinDownloader();

private slots:
void on_actionClose_triggered();

void on_pushButton_clicked();

void on_saveAs_clicked();

private:
Ui::MinecraftSkinDownloader *ui;

};

#endif // MINECRAFTSKINDOWNLOADER_H





#include "minecraftskindownloader.h"
#include <QApplication>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MinecraftSkinDownloader w;
w.show();

return a.exec();
}





#include "minecraftskindownloader.h"
#include "ui_minecraftskindownloader.h"
#include <QWidget>
#include <QObject>
#include <QtNetwork/QNetworkAccessManager>
#include <QFileDialog>
#include <QtNetwork/QNetworkRequest>
#include <QDebug>
#include <QUrl>
#include <QFile>

MinecraftSkinDownloader::MinecraftSkinDownloader(Q Widget *parent) :
QMainWindow(parent),
ui(new Ui::MinecraftSkinDownloader)
{
ui->setupUi(this);
this->setWindowTitle("Minecraft Skin Downloader");
setFixedSize(400, 200);
}

MinecraftSkinDownloader::~MinecraftSkinDownloader( )
{
delete ui;
}

void MinecraftSkinDownloader::on_actionClose_triggered( )
{
qApp->quit();
}
//http://s3.amazonaws.com/MinecraftSkins/USERNAMEHERE.png

void MinecraftSkinDownloader::on_pushButton_clicked()
{
QString prefix = "http://s3.amazonaws.com/MinecraftSkins/";
QString url = prefix += ui->lineEdit->text();
url += ".png";

ui->textEdit->setText(url); //Just testing the link part

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

manager->get(QNetworkRequest(QUrl(url)));
}

void MinecraftSkinDownloader::on_saveAs_clicked()
{

QString filename = QFileDialog::getSaveFileName(this, tr("Save As..."),
"C:/Users/",
tr("*.png"));

}

Salads
20th January 2013, 21:00
Sorry, yeah I just ran qmake and everything started magically working. Thanks for the += network tip, wouldn't have gotten it otherwise.

ChrisW67
20th January 2013, 21:33
There are several ways to achieve what you want. Each has its advantages/disadvantages. In all cases the QNetworkAccessManager get() or post() returns a QNetworkReply object (pointer) that you can use to access downloaded data, errors etc. This object will emit readyRead() signals when data arrives and could be written to file, a finished() signal when the transaction finishes (error or not), and two error signals. See the QNetworkAccessManager docs for how these are typically connected.

You can look at the Network Download Manager Example
and Network Download Example for a complete downloader (no UI).

Salads
21st January 2013, 03:49
Thanks, that helped me allot. But a weird problem came since then. I tried to make a Save As button, but now when I run it it doesn't do anything. I tried looking in the header file for anything that would be amiss, and even tried to use a connect statement. But I keep getting the error code "QMetaObject::connectSlotsByName: No matching signal for on_saveAs_clicked(QNetworkReply*)"

What's weird is that "No matching signal for on_saveAs_clicked(QNetworkReply*)" does not have to same arguments as the function in my .cpp/.h file. There, it's on_saveAs_clicked(QNetworkReply *reply).

Have a look: (.cpp)


void MinecraftSkinDownloader::on_pushButton_clicked()
{
QString prefix = "http://s3.amazonaws.com/MinecraftSkins/";
QString usrl = prefix += ui->lineEdit->text() += ".png";

QNetworkAccessManager *manager = new QNetworkAccessManager(this);
connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(slot_netwManagerFinished(QNetworkReply*)));
connect(ui->saveAs, SIGNAL(clicked()), this, SLOT(on_saveAs_clicked(QNetworkReply*)));


QUrl url(usrl);
QNetworkRequest request(url);

manager->get(request);

}

void MinecraftSkinDownloader::slot_netwManagerFinished( QNetworkReply *reply)
{

if (reply->error() != QNetworkReply::NoError)
{
qDebug() << "Error in" << reply->url() << ":" << reply->errorString();
NoUser mDiag;
mDiag.setModal(true);
mDiag.exec();
return;
}

QByteArray jpegData = reply->readAll();
QPixmap pixmap;
pixmap.loadFromData(jpegData);
QPixmap scaled = pixmap.scaled(128,64,Qt::IgnoreAspectRatio, Qt::FastTransformation);
ui->preview->setPixmap(scaled);
}

void MinecraftSkinDownloader::on_saveAs_clicked(QNetwor kReply *reply)
{
QString filename = QFileDialog::getSaveFileName(this, tr("Save Skin"), "C:/Users", tr("PNG Image (*.png)"));
QFile file (filename);
file.open(QIODevice::WriteOnly);
QDataStream out (&file);
out << reply;
file.close();
}

.h


#ifndef MINECRAFTSKINDOWNLOADER_H
#define MINECRAFTSKINDOWNLOADER_H

#include <QMainWindow>
#include <QtNetwork/QNetworkReply>

namespace Ui {
class MinecraftSkinDownloader;
}

class MinecraftSkinDownloader : public QMainWindow
{
Q_OBJECT

public:
explicit MinecraftSkinDownloader(QWidget *parent = 0);
~MinecraftSkinDownloader();

private slots:

void on_pushButton_clicked();

void slot_netwManagerFinished(QNetworkReply *reply);

void on_saveAs_clicked(QNetworkReply *reply);

private:
Ui::MinecraftSkinDownloader *ui;

};

#endif // MINECRAFTSKINDOWNLOADER_H



Screenshot of error:http://i.imgur.com/zNHfnzG.png

ChrisW67
21st January 2013, 05:09
The name you have chosen is being picked up by QMetaObject::connectSlotsByName() in your UI generated code. It goes looking for an object in the UI called "saveAs" with a slot called "clicked" and finds that, while the saveAs button has one, its arguments do not match your slot. You need to rename the slot or rethink why you want the QNetworkReply* in that slot.

As an aside, you certainly do not want QDataStream involved in writing a PNG file retrieved from a QIODevice. Use QIOdevice::write() directly.

anda_skoa
21st January 2013, 19:25
And you cannot connect a signal with no arguments (clicked()) to a slot with arguments.

Cheers,
_

Salads
21st January 2013, 21:14
Thanks for the help, I was able to fix the Save As dialog from not appearing.. But now how would I pass data from my slot_netwManagerFinished(QNetworkReply *reply) function to the on_saveAs_clicked() function?
I want to either pass pixmap or jpegData to the SaveAs function so I can actually save it. (Adding variables to the function made it break) Would adding the button in manually do the job as well because of the variables? I still would prefer if I could just add the argument in..

Code:


void MinecraftSkinDownloader::slot_netwManagerFinished( QNetworkReply *reply)
{

if (reply->error() != QNetworkReply::NoError)
{
qDebug() << "Error in" << reply->url() << ":" << reply->errorString();
NoUser mDiag;
mDiag.setModal(true);
mDiag.exec();
return;
}

QByteArray jpegData = reply->readAll(); //or this
QPixmap pixmap; //<-- This one here
pixmap.loadFromData(jpegData);
QPixmap scaled = pixmap.scaled(128,64,Qt::IgnoreAspectRatio, Qt::FastTransformation);
ui->preview->setPixmap(scaled);

}

void MinecraftSkinDownloader::on_saveAs_clicked()
{
QString filename = QFileDialog::getSaveFileName(this, tr("Save Skin"), "C:/Users/", ("PNG Image (*.png)"));

QFile file(filename);
file.open(QIODevice::WriteOnly);
file.write(pixmap); //<-- Want to use here
file.close();

}

ChrisW67
21st January 2013, 21:28
If you want jpegData to be available to other member functions of the class then make it a member variable in the class rather than a local variable of the function.

Salads
22nd January 2013, 01:40
Thanks! Sorry I should have known that one from the beginning. (Maybe I'll go finish off C++ Primer before I do more QT eh? :p) After I fixed my code for using a class variable the connectSlotsByName problem came up again. I read that changing the function name to onsaveAs_clicked() will fix the problem as long as I add a connect to it, and it did! (Apparrently changing the naming convention will stop it from going to moc_ to automatically connect things) Everything is working perfectly now. Thanks so much for your help :D

Just one more thing.. I'm trying to test out the background image for my photo-shopper buddy but when I try this->setStyleSheet("background-image: url(C:/Users/David/desktop/bg.png); "
"background-position: top left;"); it sets all of my stuff to it. (Buttons, textEdits, Labels included.) How would I separate the background from the rest of the widgets? (the image is 400, 200 as well)

Code snippet:


MinecraftSkinDownloader::MinecraftSkinDownloader(Q Widget *parent) :
QMainWindow(parent),
ui(new Ui::MinecraftSkinDownloader)
{
ui->setupUi(this);
this->setWindowTitle("Minecraft Skin Downloader");
setFixedSize(400, 200);
this->setStyleSheet("background-image: url(C:/Users/David/desktop/bg.png); " //<-- here it is
"background-position: top left;");
connect(ui->save, SIGNAL(clicked()), this, SLOT(onsaveAs_clicked()));
}


Image of the app Before and After
http://imgur.com/XWRBeXF

ChrisW67
22nd January 2013, 02:21
Specify the class or object name the the style is to apply to, something like one of these:


setStyleSheet("QMainWindow { background-image: url(C:/Users/David/desktop/bg.png); } "

setStyleSheet("QMainWindow#objectname { background-image: url(C:/Users/David/desktop/bg.png); } "

anda_skoa
22nd January 2013, 08:11
Two other things:

- you don't need a new QNetworkManager for every request, just one will do fine (can handle multiple requests in parallel)
- your code becomes the owner of the returned QNetworkReply so your code has to delete it. The easiest way to do that is to call reply->deleteLater() in the finished slot.

Cheers,
_

Salads
23rd January 2013, 07:17
So if I put the following code where QMainWindow function is it won't have to make a new one every time? Would this help with performance?
EDIT: What if I set it as a public class definition? I have QNetworkAccessManager *manager; but the program crashes when I try to send the request. (When I press the pushButton originally set to send it)


QNetworkAccessManager *manager = new QNetworkAccessManager(this);


Here's my complete .cpp file for reference:


#include "minecraftskindownloader.h"
#include "ui_minecraftskindownloader.h"
#include "nouser.h"
#include <QWidget>
#include <QObject>
#include <QtNetwork/QNetworkAccessManager>
#include <QFileDialog>
#include <QtNetwork/QNetworkRequest>
#include <QDebug>
#include <QUrl>
#include <QFile>
#include <QPixmap>
#include <QMessageBox>

MinecraftSkinDownloader::MinecraftSkinDownloader(Q Widget *parent) :
QMainWindow(parent),
ui(new Ui::MinecraftSkinDownloader)
{
ui->setupUi(this);
this->setWindowTitle("Minecraft Skin Downloader");
setFixedSize(400, 200);
setStyleSheet("QMainWindow { background-image: url(:/bg.png); } ");

connect(ui->save, SIGNAL(clicked()), this, SLOT(onsaveAs_clicked()));
}

MinecraftSkinDownloader::~MinecraftSkinDownloader( )
{
delete ui;
}

//http://s3.amazonaws.com/MinecraftSkins/USERNAMEHERE.png

void MinecraftSkinDownloader::on_pushButton_clicked()
{
QString prefix = "http://s3.amazonaws.com/MinecraftSkins/";
QString usrl = prefix += ui->lineEdit->text() += ".png";

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

QUrl url(usrl);
QNetworkRequest request(url);

manager->get(request);

}

void MinecraftSkinDownloader::slot_netwManagerFinished( QNetworkReply *reply)
{

if (reply->error() != QNetworkReply::NoError)
{
qDebug() << "Error in" << reply->url() << ":" << reply->errorString();
NoUser mDiag;
mDiag.setModal(true);
mDiag.exec();
return;
}

MinecraftSkinDownloader::pngData = reply->readAll();
QPixmap pixmap;
pixmap.loadFromData(MinecraftSkinDownloader::pngDa ta);
QPixmap scaled = pixmap.scaled(128,64,Qt::IgnoreAspectRatio, Qt::FastTransformation);
ui->preview->show();
ui->preview->setStyleSheet("QLabel { background-color: white; }");
ui->preview->setPixmap(scaled);
reply->deleteLater();

}

void MinecraftSkinDownloader::onsaveAs_clicked()
{
QString filename = QFileDialog::getSaveFileName(this, tr("Save Skin"), "C:/Users/", ("PNG Image (*.png)"));

QFile file(filename);
file.open(QIODevice::WriteOnly);
file.write(MinecraftSkinDownloader::pngData);
file.close();
}

void MinecraftSkinDownloader::on_pushButton_2_clicked()
{
ui->lineEdit->clear();
ui->preview->hide();
}

anda_skoa
23rd January 2013, 13:06
[COLOR="#D3D3D3"]
EDIT: What if I set it as a public class definition? I have QNetworkAccessManager *manager; but the program crashes when I try to send the request. (When I press the pushButton originally set to send it)

I am not sure what you mean with that, the QNetworkManager creation in the code you posted seems to be unchanged.

Btw, you'll also have to deleted the network reply in the error handling case :)

Cheers,
_

Salads
23rd January 2013, 20:02
Got the error part thanks :)

I'll update my code:

.h File:


#ifndef MINECRAFTSKINDOWNLOADER_H
#define MINECRAFTSKINDOWNLOADER_H

#include <QMainWindow>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkAccessManager>

namespace Ui {
class MinecraftSkinDownloader;
}

class MinecraftSkinDownloader : public QMainWindow
{
Q_OBJECT

public:
explicit MinecraftSkinDownloader(QWidget *parent = 0);
~MinecraftSkinDownloader();
QByteArray pngData;
QNetworkAccessManager *manager;

private slots:

void slot_netwManagerFinished(QNetworkReply *reply);

void onsaveAs_clicked();

void on_clear_clicked();

void on_pushButton_clicked();

private:
Ui::MinecraftSkinDownloader *ui;


};

#endif // MINECRAFTSKINDOWNLOADER_H


.cpp File


#include "minecraftskindownloader.h"
#include "ui_minecraftskindownloader.h"
#include <QWidget>
#include <QObject>
#include <QtNetwork/QNetworkAccessManager>
#include <QFileDialog>
#include <QtNetwork/QNetworkRequest>
#include <QDebug>
#include <QUrl>
#include <QFile>
#include <QPixmap>
#include <QMessageBox>

MinecraftSkinDownloader::MinecraftSkinDownloader(Q Widget *parent) :
QMainWindow(parent),
ui(new Ui::MinecraftSkinDownloader)
{
ui->setupUi(this);
this->setWindowTitle("Minecraft Skin Downloader");
QIcon icon(":/ico");
setWindowIcon(icon);
setFixedSize(400, 200);
setStyleSheet("QMainWindow { background-image: url(:/bg.png); } ");

connect(ui->save, SIGNAL(clicked()), this, SLOT(onsaveAs_clicked()));

}

MinecraftSkinDownloader::~MinecraftSkinDownloader( )
{
delete ui;
}

//http://s3.amazonaws.com/MinecraftSkins/USERNAMEHERE.png

void MinecraftSkinDownloader::on_pushButton_clicked()
{
QString prefix = "http://s3.amazonaws.com/MinecraftSkins/";
QString usrl = prefix += ui->lineEdit->text() += ".png";

//QNetworkAccessManager *manager = new QNetworkAccessManager(this);
connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(slot_netwManagerFinished(QNetworkReply*)));

QUrl url(usrl);
QNetworkRequest request(url);

manager->get(request);

}

void MinecraftSkinDownloader::slot_netwManagerFinished( QNetworkReply *reply)
{

if (reply->error() != QNetworkReply::NoError)
{
qDebug() << "Error in" << reply->url() << ":" << reply->errorString();
QPixmap noUsr(":/voidUser.png"); // Void User path
ui->preview->show();
ui->preview->setStyleSheet("background: transparent;");
ui->preview->setPixmap(noUsr);
reply->deleteLater();
return;
}

pngData = reply->readAll();
QPixmap pixmap;
pixmap.loadFromData(pngData);
QPixmap scaled = pixmap.scaled(128,64,Qt::IgnoreAspectRatio, Qt::FastTransformation);
ui->preview->show();
ui->preview->setStyleSheet("QLabel { background-color: white; }");
ui->preview->setPixmap(scaled);
reply->deleteLater();

}

void MinecraftSkinDownloader::onsaveAs_clicked()
{
if(pngData.isEmpty())
{
ui->preview->show();
QPixmap error(":/noSaveData.png");
ui->preview->setPixmap(error);
return;
}

QString filename = QFileDialog::getSaveFileName(this, tr("Save Skin"), "C:/Users/", ("PNG Image (*.png)"));

QFile file(filename);
file.open(QIODevice::WriteOnly);
file.write(pngData);
file.close();
}

void MinecraftSkinDownloader::on_clear_clicked()
{
pngData.clear();
ui->preview->setStyleSheet("background: transparent;");
ui->lineEdit->clear();
ui->preview->hide();
return;
}


It crashes the moment I press that on_pushButton_clicked(), and doesn't even get to display the error message (voidUser.png) when I leave it blank. Same thing happens when I put a valid username in there too.

ChrisW67
24th January 2013, 02:52
If you ran the program in your debugger you would know exactly which line your program was crashing on (probably 42, maybe 47), which is more useful than the "somewhere in on_pushButton_clicked()" level of information you have now.

You have made a pointer to QNetworkAccessManager member variable but you never initialise that pointer. When you subsequently use it... BOOM! You need to move the equivalent of line 41 (also 42) into the constructor. A good habit to get into is: any time you add a member variable to a class immediately add an initialisation of the variable into the constructor. In the case of pointers that can be setting it to 0 or allocating a new object; either way it forces you to think about where it should be set.