本系列所有文章可以在這里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
最近發覺學習Qt官方demo的好處越來越多,除了了解特定模塊與類的用法,更能學習一些平時注意不到的小的編程技巧,以及優秀的編程習慣,總之,好處多多啦~
考慮到學習是個往復的過程,好記性不如爛筆頭嘛,勤快點記下來免得日后忘記,也方便官方demo不在身邊的話也能隨時取得到,最后也為學習Qt的網友們做個淺顯的參考。
所以打算爭取將Qt5官方demo一一解析在此博客中。做此工作本意也是為了學習,能力所限,不妥之處還請各位多多指正~~以上,則是為Qt5官方Demo解析集創作的緣由。
/****************************************************************************************************************/
先從一個簡單的network例子開始吧~
Fortune Server/Client 由兩個程序構成?(Fortune Server Example)和(Fortune Client Example)。也就是我們所說的服務器和客戶端
首先看下Qt對這個例子的介紹:Demonstrates how to create a server for a network service.
fortuneserver一共就3個文件,先來看Server的main.cpp:
~~~
#include <QApplication>
#include <QtCore>
#include <stdlib.h>
#include "server.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
Server server;
server.show();
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
return app.exec();
}
~~~
前面一大段是版權所有,開源許可之類的東西,我們就不管它了。這段代碼唯一值得說的就是13行,可以理解為隨機種子的初始化。記得使用它要#include
server.h:
~~~
#ifndef SERVER_H
#define SERVER_H
#include <QDialog>
QT_BEGIN_NAMESPACE
class QLabel;
class QPushButton;
class QTcpServer;
class QNetworkSession;
QT_END_NAMESPACE
//! [0]
class Server : public QDialog
{
Q_OBJECT
public:
Server(QWidget *parent = 0);
private slots:
void sessionOpened();
void sendFortune();
private:
QLabel *statusLabel;
QPushButton *quitButton;
QTcpServer *tcpServer;
QStringList fortunes;
QNetworkSession *networkSession;
};
//! [0]
#endif
~~~
QT_BEGIN_NAMESPACE和QT_END_NAMESPACE宏在源代碼中是這樣定義的:
~~~
# define QT_BEGIN_NAMESPACE namespace QT_NAMESPACE {
# define QT_END_NAMESPACE }
~~~
也就是說,如果你定義以下內容:
~~~
QT_BEGIN_NAMESPACE
class QListView;
QT_END_NAMESPACE
~~~
那么,在編譯時就會變成這樣:
~~~
namespace QT_NAMESPACE {
class QListView;
}
~~~
僅當在編譯Qt時,加上-qtnamespace選項時,這兩個宏才會有作用,這時,Qt作為第三方庫,要使用用戶自定義的命名空間來訪問Qt中的類,如QListView *view = new QT_NAMESPACE::QListView
如果我們只需要聲明類的指針或者引用,使用前向聲明而不是#include可以減少頭文件之間的依賴。這在大型項目中減少編譯時間是很必要的。
QTcpServer *tcpServer;?
QNetworkSession *networkSession;
這是最核心的兩個類聲明了,前者提供了一個基于TCP的服務器,后者則提供了系統網絡接口控制。
好了,來看重頭Server.cpp,為了簡明部分注釋就直接加注釋在后面了
~~~
#include <QtWidgets>
#include <QtNetwork>
#include <stdlib.h>
#include "server.h"
Server::Server(QWidget *parent)
: QDialog(parent), tcpServer(0), networkSession(0) //初始化列表
{
statusLabel = new QLabel;
quitButton = new QPushButton(tr("Quit"));
quitButton->setAutoDefault(false);
QNetworkConfigurationManager manager;
if (manager.capabilities() & QNetworkConfigurationManager::NetworkSessionRequired) { // 檢測平臺是否具有網絡通訊能力
QSettings settings(QSettings::UserScope, QLatin1String("QtProject")); // QSettings用來將用戶配置保存到注冊表、INI文件中。這里是提取配置
settings.beginGroup(QLatin1String("QtNetwork"));
const QString id = settings.value(QLatin1String("DefaultNetworkConfiguration")).toString();
settings.endGroup();
QNetworkConfiguration config = manager.configurationFromIdentifier(id);
if ((config.state() & QNetworkConfiguration::Discovered) !=
QNetworkConfiguration::Discovered) {
config = manager.defaultConfiguration(); // 沒有取到用戶保存信息則使用默認配置
}
networkSession = new QNetworkSession(config, this); // 使用該配置新建網絡會話
connect(networkSession, SIGNAL(opened()), this, SLOT(sessionOpened()));
statusLabel->setText(tr("Opening network session."));
networkSession->open();
} else {
sessionOpened();
}
//! [2]
fortunes << tr("You've been leading a dog's life. Stay off the furniture.")
<< tr("You've got to think about tomorrow.")
<< tr("You will be surprised by a loud noise.")
<< tr("You will feel hungry again in another hour.")
<< tr("You might have mail.")
<< tr("You cannot kill time without injuring eternity.")
<< tr("Computers are not intelligent. They only think they are.");
//! [2]
connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));
//! [3]
connect(tcpServer, SIGNAL(newConnection()), this, SLOT(sendFortune()));
//! [3]
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch(1);
buttonLayout->addWidget(quitButton);
buttonLayout->addStretch(1);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(statusLabel);
mainLayout->addLayout(buttonLayout);
setLayout(mainLayout);
setWindowTitle(tr("Fortune Server"));
}
void Server::sessionOpened()
{
// 類似構造函數中的讀取配置,將網絡會話配置保存在注冊表中
if (networkSession) {
QNetworkConfiguration config = networkSession->configuration();
QString id;
if (config.type() == QNetworkConfiguration::UserChoice)
id = networkSession->sessionProperty(QLatin1String("UserChoiceConfiguration")).toString();
else
id = config.identifier();
QSettings settings(QSettings::UserScope, QLatin1String("QtProject"));
settings.beginGroup(QLatin1String("QtNetwork"));
settings.setValue(QLatin1String("DefaultNetworkConfiguration"), id);
settings.endGroup();
}
//! [0] //! [1]
tcpServer = new QTcpServer(this);
if (!tcpServer->listen()) { // 新建 Tcp 服務并開始監聽,這個監聽是基于所有地址,任意端口的<span style="font-family: Arial, Helvetica, sans-serif;"> </span>
QMessageBox::critical(this, tr("Fortune Server"),
tr("Unable to start the server: %1.")
.arg(tcpServer->errorString()));
close();
return;
}
//! [0]
QString ipAddress;
QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses(); // 這里返回的是所有主機能夠監聽到的IPv4的地址
for (int i = 0; i < ipAddressesList.size(); ++i) {
if (ipAddressesList.at(i) != QHostAddress::LocalHost &&
ipAddressesList.at(i).toIPv4Address()) { // 取出第一個非主機地址的IPv4地址,注意這里使用了toIPv4Address()進行轉換,因為原類型是QHostAddress的
ipAddress = ipAddressesList.at(i).toString();
break;
}
}
// 如果沒有發現則使用主機的IPv4地址
if (ipAddress.isEmpty())
ipAddress = QHostAddress(QHostAddress::LocalHost).toString();
statusLabel->setText(tr("The server is running on\n\nIP: %1\nport: %2\n\n"
"Run the Fortune Client example now.")
.arg(ipAddress).arg(tcpServer->serverPort())); // 端口號由serverPort()獲得
//! [1]
}
//! [4]
void Server::sendFortune()
{
//! [5]
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly); // QDataSteam作為二進制輸入輸出流,使用QTcpSocket傳輸數據的常用方法
out.setVersion(QDataStream::Qt_4_0); // 第(1)點
//! [4] //! [6]
out << (quint16)0; // 第(2)點
out << fortunes.at(qrand() % fortunes.size()); // (qrand() % fortunes.size())根據上文理解為隨機的0-6就好
out.device()->seek(0);
out << (quint16)(block.size() - sizeof(quint16)); // 第(3)點
//! [6] //! [7]
// 注意這個QTcpSocket對象由tcpSetver創建,這意味著clientConnection是作為tcpServer的子對象存在
QTcpSocket *clientConnection = tcpServer->nextPendingConnection();
connect(clientConnection, SIGNAL(disconnected()), // 確保失去連接后內存被釋放
clientConnection, SLOT(deleteLater()));
//! [7] //! [8]
clientConnection->write(block); // 數據的寫入是異步的
clientConnection->disconnectFromHost(); // 數據發送完畢后關閉Socket
//! [5]
}
//! [8]
~~~
第一點,很多地方都有講這個out.setVersion(QDataStream::Qt_4_0),就說編碼統一什么的,讓人聽的一頭霧水。明確的說,為了適應新的功能,一些Qt類的數據類的序列化格式已經在一些新版本的Qt發生了改變。也就是說,如果基于Qt5.2的數據發送到Qt4.0的客戶機上,很可能他取出的數據與你發送的數據不同。這樣就需要規定一個統一的數據流格式,這里將版本設置為Qt_4_0,也就是以4.0版本的數據流序列化格式發送數據。客戶機也只有設置了相同的版本才能保證數據的準確解析。
第二點,為了保證在客戶端能接收到完整的文件,我們都在數據流的最開始寫入完整文件的大小信息,這樣客戶端就可以根據大小信息來判斷是否接受到了完整的文件。而在服務器端,我們在發送數據時就要首先發送實際文件的大小信息,但是,文件的大小一開始是無法預知的,所以我們先使用了out<<(quint16) 0;在block的開始添加了一個quint16大小的空間,也就是兩字節的空間,它用于后面放置文件的大小信息。
第三點,當文件輸入完成后我們在使用out.device()->seek(0);返回到block的開始,加入實際的文件大小信息,也就是后面的代碼,它是實際文件的大小:out<<(quint16) (block.size() – sizeof(quint16));
好了,服務端大致這些,實際就我們一般的使用中,使用默認的網絡配置就可以了,所以核心程序還是很簡單的。
下面來看客戶端代碼吧~與fortuneServer一樣,fortuneClient中也只有3個文件,main.cpp保持了Qt一貫的簡約風格,我們就跳過它來看client.h:
~~~
#ifndef CLIENT_H
#define CLIENT_H
#include <QDialog>
#include <QTcpSocket>
QT_BEGIN_NAMESPACE
class QComboBox;
class QDialogButtonBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QTcpSocket;
class QNetworkSession;
QT_END_NAMESPACE
//! [0]
class Client : public QDialog
{
Q_OBJECT
public:
Client(QWidget *parent = 0);
private slots:
void requestNewFortune();
void readFortune();
void displayError(QAbstractSocket::SocketError socketError); // 注意到這個參數是QString型的枚舉量
void enableGetFortuneButton();
void sessionOpened();
private:
QLabel *hostLabel;
QLabel *portLabel;
QComboBox *hostCombo;
QLineEdit *portLineEdit;
QLabel *statusLabel;
QPushButton *getFortuneButton;
QPushButton *quitButton;
QDialogButtonBox *buttonBox;
QTcpSocket *tcpSocket;
QString currentFortune;
quint16 blockSize; // 這里的quint16為了封裝平臺間的差異,如果直接使用int在不同的平臺下可能長度不同
QNetworkSession *networkSession;
};
//! [0]
#endif
~~~
一般來講privat,private slots放在最下面,因為被查看的最少。
既然說的QTcpSocket,需要介紹一下它的兩種網絡編程的方式:
第一種是異步(non-blocking)方法。業務調度和執行時控制返回到Qt的事件循環。當操作完成時,QTcpSocket發出信號。例如,QTcpSocket::connectToHost()是立即返回的,而當連接已經建立后,QTcpSocket再發出connected()信號表明自己已成功連接。?
第二種是同步(blocking)的方法。在非圖形用戶界面和多線程應用程序,可以調用WAITFOR...()函數(例如,QTcpSocket:: waitForConnected())暫停直到操作完成。?
在這個Demo中,是以異步方式建立的網絡。Blocking Fortune Example中使用了同步的方法,這個下次來看。
client.cpp:
~~~
#include <QtWidgets>
#include <QtNetwork>
#include "client.h"
//! [0]
Client::Client(QWidget *parent)
: QDialog(parent), networkSession(0)
{
//! [0]
hostLabel = new QLabel(tr("&Server name:"));
portLabel = new QLabel(tr("S&erver port:"));
hostCombo = new QComboBox;
hostCombo->setEditable(true);
// find out name of this machine
QString name = QHostInfo::localHostName(); // 獲取主機名
if (!name.isEmpty()) {
hostCombo->addItem(name);
QString domain = QHostInfo::localDomainName(); // 獲取DNS域名。當然,個人PC機是沒有的
if (!domain.isEmpty())
hostCombo->addItem(name + QChar('.') + domain);
}
if (name != QString("localhost"))
hostCombo->addItem(QString("localhost"));
// find out IP addresses of this machine
QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();
// add non-localhost addresses
for (int i = 0; i < ipAddressesList.size(); ++i) { 第(1)點
if (!ipAddressesList.at(i).isLoopback())
hostCombo->addItem(ipAddressesList.at(i).toString());
}
// add localhost addresses
for (int i = 0; i < ipAddressesList.size(); ++i) { 第(2)點
if (ipAddressesList.at(i).isLoopback())
hostCombo->addItem(ipAddressesList.at(i).toString());
}
portLineEdit = new QLineEdit;
portLineEdit->setValidator(new QIntValidator(1, 65535, this)); // 輸入限制為1-65535的整型,限制輸入應該成為一個良好的習慣
hostLabel->setBuddy(hostCombo);
portLabel->setBuddy(portLineEdit);
statusLabel = new QLabel(tr("This examples requires that you run the "
"Fortune Server example as well."));
getFortuneButton = new QPushButton(tr("Get Fortune"));
getFortuneButton->setDefault(true); // 這個屬性設置意味著該按鍵將響應回車鍵
getFortuneButton->setEnabled(false);
quitButton = new QPushButton(tr("Quit"));
buttonBox = new QDialogButtonBox; // QDialogButtonBox中第一設置為ActionRole的按鈕將被設置為Default,它將響應回車按下事件
buttonBox->addButton(getFortuneButton, QDialogButtonBox::ActionRole);
buttonBox->addButton(quitButton, QDialogButtonBox::RejectRole);
//! [1]
tcpSocket = new QTcpSocket(this);
//! [1]
connect(hostCombo, SIGNAL(editTextChanged(QString)),
this, SLOT(enableGetFortuneButton()));
connect(portLineEdit, SIGNAL(textChanged(QString)),
this, SLOT(enableGetFortuneButton()));
connect(getFortuneButton, SIGNAL(clicked()),
this, SLOT(requestNewFortune()));
connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));
//! [2] //! [3]
connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(readFortune()));
//! [2] //! [4]
connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)),
//! [3]
this, SLOT(displayError(QAbstractSocket::SocketError)));
//! [4]
QGridLayout *mainLayout = new QGridLayout;
mainLayout->addWidget(hostLabel, 0, 0);
mainLayout->addWidget(hostCombo, 0, 1);
mainLayout->addWidget(portLabel, 1, 0);
mainLayout->addWidget(portLineEdit, 1, 1);
mainLayout->addWidget(statusLabel, 2, 0, 1, 2); // 參數分別表示,第2行,第0列,行跨度為1,列跨度為2,最后還有一個Qt::Alignment屬性
mainLayout->addWidget(buttonBox, 3, 0, 1, 2);
setLayout(mainLayout);
setWindowTitle(tr("Fortune Client"));
portLineEdit->setFocus();
QNetworkConfigurationManager manager; // 這里主要用來保存網絡設置,可以參見Server端代碼
if (manager.capabilities() & QNetworkConfigurationManager::NetworkSessionRequired) {
// Get saved network configuration
QSettings settings(QSettings::UserScope, QLatin1String("QtProject"));
settings.beginGroup(QLatin1String("QtNetwork"));
const QString id = settings.value(QLatin1String("DefaultNetworkConfiguration")).toString();
settings.endGroup();
// If the saved network configuration is not currently discovered use the system default
QNetworkConfiguration config = manager.configurationFromIdentifier(id);
if ((config.state() & QNetworkConfiguration::Discovered) !=
QNetworkConfiguration::Discovered) {
config = manager.defaultConfiguration();
}
networkSession = new QNetworkSession(config, this);
connect(networkSession, SIGNAL(opened()), this, SLOT(sessionOpened()));
getFortuneButton->setEnabled(false);
statusLabel->setText(tr("Opening network session."));
networkSession->open();
}
//! [5]
} //構造函數完
//! [5]
//! [6]
void Client::requestNewFortune()
{
getFortuneButton->setEnabled(false);
blockSize = 0;
tcpSocket->abort(); // 終止當前連接并重置socket,任何等待發送的數據都將被丟棄
//! [7]
tcpSocket->connectToHost(hostCombo->currentText(),
portLineEdit->text().toInt()); //連接到主機,第一個參數是IP地址,第二個參數是端口號,第三個參數默認值為讀寫
//! [7]
}
//! [6]
//! [8]
void Client::readFortune()
{
//! [9]
QDataStream in(tcpSocket);
in.setVersion(QDataStream::Qt_4_0); // 版本號需要保持一致
if (blockSize == 0) {
if (tcpSocket->bytesAvailable() < (int)sizeof(quint16))
return;
//! [8]
//! [10]
in >> blockSize; // 因為blockSize是quint16,所以只接收到in數據流前16個字節,而該數據則是整個數據流的長度
}
if (tcpSocket->bytesAvailable() < blockSize)
return; // 第(3)點
//! [10] //! [11]
QString nextFortune; //注意這個局部QString變量的作用
in >> nextFortune;
if (nextFortune == currentFortune) {
QTimer::singleShot(0, this, SLOT(requestNewFortune())); // 單次的定時器,重新請求新數據
return;
}
//! [11]
//! [12]
currentFortune = nextFortune; // 否則將這個值賦給全局變量
//! [9]
statusLabel->setText(currentFortune);
getFortuneButton->setEnabled(true);
}
//! [12]
//! [13]
void Client::displayError(QAbstractSocket::SocketError socketError) // 這個SocketError是抽象基類的枚舉對象,因此對所有套接字都是通用的
{
switch (socketError) {
case QAbstractSocket::RemoteHostClosedError:
break;
case QAbstractSocket::HostNotFoundError:
QMessageBox::information(this, tr("Fortune Client"),
tr("The host was not found. Please check the "
"host name and port settings."));
break;
case QAbstractSocket::ConnectionRefusedError:
QMessageBox::information(this, tr("Fortune Client"),
tr("The connection was refused by the peer. "
"Make sure the fortune server is running, "
"and check that the host name and port "
"settings are correct."));
break;
default:
QMessageBox::information(this, tr("Fortune Client"),
tr("The following error occurred: %1.")
.arg(tcpSocket->errorString()));
}
getFortuneButton->setEnabled(true);
}
//! [13]
void Client::enableGetFortuneButton() // 判斷語句封裝在函數體內,而不是每次調用時還要自己判斷
{
getFortuneButton->setEnabled((!networkSession || networkSession->isOpen()) && // 省 if 的方法
!hostCombo->currentText().isEmpty() &&
!portLineEdit->text().isEmpty());
}
void Client::sessionOpened() // 保存網絡配置,詳見Server端類似代碼
{
// Save the used configuration
QNetworkConfiguration config = networkSession->configuration();
QString id;
if (config.type() == QNetworkConfiguration::UserChoice)
id = networkSession->sessionProperty(QLatin1String("UserChoiceConfiguration")).toString();
else
id = config.identifier();
QSettings settings(QSettings::UserScope, QLatin1String("QtProject"));
settings.beginGroup(QLatin1String("QtNetwork"));
settings.setValue(QLatin1String("DefaultNetworkConfiguration"), id);
settings.endGroup();
statusLabel->setText(tr("This examples requires that you run the "
"Fortune Server example as well."));
enableGetFortuneButton();
}
~~~
第一點,注意下面兩個if 只有一個 ! 的區別。首先添加了環回網路,在我的機子上測試為192.168.1.106、192.168.23.1、192.168.19.1(均屬于本地局域網) ??
第二點,添加了環回網路,地址為127.0.0.1(Windows下的虛擬接口,永不會down~)
第三點,這點很關鍵,實際上,尤其在慢速網絡中,TCP基于數據流的發送方式會使客戶端在一次接收中無法收到一整包的數據。那么,程序在判斷接收到的數據長度小于隊首的數值時,說明這包數據并不完整,不過沒有關系,程序直接返回,等待接下來的片段包,而QTcpSocket會將這些片段緩存,當收到數據長度等于隊首長度數值時,再將這些緩存數據全部讀出。這也是為什么我們在Server端加入數據長度幀的原因所在。
好了,Fortune Server/Client例程大概就這些~看起來代碼很長,其實真正用在發送接收的也不過幾句話而已。當然我們自己在做項目的時候也應該像demo一樣把各種可能的異常盡量加在處理函數中,這對程序的健壯性是有好處的~
- 前言
- 1——Fortune Server/Client
- 2——Multicast Sender/Receiverz
- 3——Broadcast Sender/Receiver
- 4——Blocking Fortune Client
- 5——Threaded Fortune Server
- 5(總結)——Fortune例程的各個實現區別
- 6——Loopback Example
- 7——Analog Clock Example
- 8——Shaped Clock Example
- 9——Analog Clock Window Example
- 10——Qt Quick Particles Examples - Emitters
- 11——Qt Quick Particles Examples - Affectors
- 12——Qt Quick Particles Examples - CustomParticles
- 13——Qt Quick Particles Examples - Image Particles
- 14——Qt Quick Particles Examples - System
- 15——Chapter 1: Creating a New Type
- 16——Chapter 2: Connecting to C++ Methods and Signals
- 17——Chapter 3: Adding Property Bindings
- 18——Chapter 4: Using Custom Property Types
- 19——Chapter 5: Using List Property Types
- 20——Chapter 6: Writing an Extension Plugin
- 21——Extending QML - Adding Types Example
- 22——Extending QML - Object and List Property Types Example
- 23——Extending QML - Inheritance and Coercion Example
- 24——Extending QML - Default Property Example
- 25——Extending QML - Methods Example
- 26——Extending QML - Grouped Properties Example
- 27——Extending QML - Attached Properties Example
- 28——Extending QML - Signal Support Example
- 29——Extending QML - Property Value Source Example
- 30——Extending QML - Binding Example
- 31——StocQt
- 32——Qt Quick Examples - Threading
- 33——Qt Quick Examples - Window and Screen
- 34——Concentric Circles Example
- 35——Music Player
- 36——Wiggly Example
- 37——Vector Deformation