使用Qt實現一個必應壁紙客戶端
概要
必應的每日壁紙很好看,但是看不到一周以前的壁紙圖片,日前使用python開發了必應壁紙收集站,可惜這樣的收集站只能在線瀏覽,我在想要是有一款軟體能夠下載每日必應壁紙,並應用到windows的桌面不是更好,必應出過一款叫「必應繽紛桌面」的軟體,這款軟體功能很簡單並且不好用,我的win7系統下載下來安裝還要安裝net 4.0的支援,不是很方便。市面上還有其他幾款關於設置必應桌面壁紙的軟體,也都一一看過,軟體不是功能很簡陋,就是python做的腳本,要安裝python環境,安裝一些支援庫,腳本才能跑起來。這對不懂程式的人來說無疑是很困難的。既然沒有心儀的軟體,那乾脆自己花時間做一個好了。於是有了這篇博文。軟體使用Qt開發,理論上應該可以運行在所有windows系統,不需要安裝其他的依賴庫。下面總結一下開發過程,給有需要要使用Qt開發軟體的同學一些參考。軟體使用QtCreator工具開發,可以點擊這裡下載體驗:必應壁紙PC客戶端工具,其介面效果如下:

去掉傳統標題欄,自定義最小化關閉按鈕,拖動窗體
首先軟體介面要漂亮的話,不能使用傳統的標題欄,窗口按鈕了,現在主流的軟體,QQ音樂,微信,360,金山基本上都是這種模式。這塊在Qt中是很容易做到的。去掉傳統標題欄用如下程式碼即可:
//去掉軟體標題欄,自己來實現 Qt::FramelessWindowHint
//設置窗體透明,但裡面的控制項不透明,這個可以用來做不規則的窗體效果
//如果是規則的矩形窗體這個可以不用
//setAttribute(Qt::WA_TranslucentBackground,true);
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
setWindowIcon(QIcon(":/images/title/icon_32.ico"));//可執行程式圖標
這裡面 Qt::FramelessWindowHint 就是創建無邊框窗口,使用 setWindowIcon() 去設置軟體圖標(該圖標就是在任務欄上看到的圖標),這裡面還有一個比較常用的屬性設置,setAttribute(Qt::WA_TranslucentBackground,true) 這個用於設置整個窗體透明,一般用來做不規則的窗口,比如圓形或者其他。我們知道windows的控制項,包括窗口肯定都是矩形,當設置窗體透明之後,給窗體設置一個非矩形的背景圖片,比如一個圓形的png作為背景,那麼由於png的透明部分的效果,使得窗體看起來是一個圓形。通過這種方式,只要設計好背景png圖片,理論上可以做出任何效果的窗體。當然本例中,我們不需要做非矩形的窗體也就不設置這個屬性。
當去掉傳統的標題欄之後,相應的窗口拖動功能就沒有了,因為窗口拖動都是基於傳統的標題欄進行拖動,那麼這部分功能就需要自己去實現。做起來也很簡單。在這之前,先說說我們的整個布局,我們的整個窗口是一個QMainWindow窗體,然後在這個窗體中拖入了一個QWidget,為QMainWindow設置一個布局(水平和垂直布局都可以)使得這個QWidget完全填充滿整個QMainWindow。然後我們再給這個QWidget設置一個垂直布局,之後的所有控制項都是基於這個QWidget的。為什麼要在最外面的QMainWindow中加一個QWidget作為整個布局容器,而不直接使用QMainWindow作為直接的布局容器呢,因為在使用過程中發現,在設置QSS時,無論是直接為QMainWindow設置QSS,還是給他定義一個objeceName用這個objectName設置QSS都沒有效果。而通過在他裡面添加一個QWidget,給這個QWidget設置一個objectName之後,在QSS文件中通過 #objectName 的方式設置樣式就可以。目前這個原因不得而知。我們整個軟體的布局如下:

在使用控制項布局的時候,用QFrame做容器,用的最多的就是垂直布局和水平布局,再加上垂直和水平的Spacer控制項,基本上可以搞定所有布局效果,這裡要注意的是,在QtCreator中為控制項設置布局的Layout屬性是有默認值的,包括Margin和Spacing,導致布局控制項的子控制項之間有空隙,所以這裡最好是手工都設置為0,按照上面的布局,我們最外層是一個QMainWindow,在其頭文件和源文件中重寫滑鼠的按下、移動、釋放按鈕如下:
//mainwindow.h 頭文件
protected:
virtual void mousePressEvent(QMouseEvent *event);
virtual void mouseMoveEvent(QMouseEvent *event);
virtual void mouseReleaseEvent(QMouseEvent *event);
重新實現滑鼠的這3個事件程式碼如下:
//重寫滑鼠按下事件
void MainWindow::mousePressEvent(QMouseEvent *event)
{
mMoveing = true;
//記錄下滑鼠相對於窗口的位置
//event->globalPos()滑鼠按下時,滑鼠相對於整個螢幕位置
//pos() this->pos()滑鼠按下時,窗口相對於整個螢幕位置
mMovePosition = event->globalPos() - pos();
QWidget::mousePressEvent(event);
}
//重寫滑鼠移動事件
void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
//(event->buttons() & Qt::LeftButton)按下是左鍵
//滑鼠移動事件需要移動窗口,窗口移動到哪裡呢?就是要獲取滑鼠移動中,窗口在整個螢幕的坐標,然後move到這個坐標,怎麼獲取坐標?
//通過事件event->globalPos()知道滑鼠坐標,滑鼠坐標減去滑鼠相對於窗口位置,就是窗口在整個螢幕的坐標
if (mMoveing && (event->buttons() & Qt::LeftButton)
&& (event->globalPos()-mMovePosition).manhattanLength() > QApplication::startDragDistance())
{
move(event->globalPos()-mMovePosition);
mMovePosition = event->globalPos() - pos();
}
QWidget::mouseMoveEvent(event);
}
//滑鼠釋放
void MainWindow::mouseReleaseEvent(QMouseEvent *event)
{
mMoveing = false;
QWidget::mouseReleaseEvent(event);
}
這樣就實現了無邊框窗體的拖動效果,除此之外我們要重寫關閉按鈕的功能,在關閉窗體的時候不去close,這個程式碼就不展示了,百度一下就有,下面我們看看如何自定義QListWidget
自定義QListWidget項目
默認的QListWidget項目很簡單,每一個QListWidget中的項目是一個QListWidgetItem對象,該對象提供 setText() 為該項目設置文字,setIcon() 用於為項目設置圖標。如果僅僅是這樣的話,這是完全滿足不了需求的,我們需要設置圖片,並且需要在圖片上顯示圖片的描述,日期,地址等資訊。我們可以在QtCreator中創建一個ui文件(包括頭文件和對應的cpp文件),在這個ui文件中,我們以QWidget作為最頂層的容器(不像窗體用的QMainWindow),他就像我們任何自定義的ui控制項一樣,我們可以利用QtCreator的布局功能做任何複雜的布局,最後這個ui文件對應的頭文件與cpp文件實際上就是一個C++類,我們在需要的地方使用這個類就可以了。我們為這個QListWidget的每一項定義的ui介面布局如下:

可以看到我們這個ui布局中黑色實線的矩形框一共有4個都是用QFrame,最外面的QFrame0是整個ui的容器他是被一個QWidget包裹,每一個項目的圖片被設置為QFrame0的背景圖片,該項目的區域被分為上下兩部分,上面是QFrame1,下面是QFrame3,其中QFrame1裡面嵌套了一個小的QFrame2,這個QFrame2中有2個水平布局的按鈕,一個是預覽(軟體中的放大鏡),一個是設置當前圖片為桌面壁紙(軟體中的顯示器圖標)。默認情況下這兩個按鈕不顯示,當滑鼠移動到QFrame0上的時候,整個QFrame1將以動畫的形式從上切入,並且有一個透明度的變化。如何將我們自定義的ui類,設置為該QListWidget的項呢,可以用下面的程式碼:
void MainWindow::initListWigdet(){
QFile file2(":/qss/listwidget.scrollbar3.qss");
file2.open(QFile::ReadOnly);
ui->listWidget->verticalScrollBar()->setStyleSheet(file2.readAll());
file2.close();
//初始化QListWidget
//ui->listWidget->setIconSize(QSize(300,225));
ui->listWidget->setResizeMode(QListView::Adjust);
ui->listWidget->setViewMode(QListView::IconMode);
ui->listWidget->setMovement(QListView::Static);
ui->listWidget->setSpacing(10);
ui->listWidget->horizontalScrollBar()->setDisabled(true);
ui->listWidget->verticalScrollBar()->setDisabled(true);
// 創建單元項
// 這裡創建的Item使用的背景圖是樣式表中的默認背景圖
for (int i = 0; i<6; ++i)
{
QListWidgetItem *item = new QListWidgetItem;
ImageInfoItem2 *widget = new ImageInfoItem2;
item->setSizeHint(QSize(288,180));
ui->listWidget->addItem(item);
ui->listWidget->setSizeIncrement(150,190);
ui->listWidget->setItemWidget(item,widget);//最重要的是這句將Item設置為一個Widget,而這個Widget就是自定義的ui
}
//給item綁定真實數據
updateListWidget(1); // page 從1開始
}
我們首先為這個QListWidget載入了一個樣式表,然後設置了一些參數,最後為其設置了6個item項目,每個item項目是一個Widget對象,也就是我們自定義的ui類(ImageInfoItem2),其實際上是繼承自QWidget類的。最重要的是使用QListWidget::setItemWidget()成員函數設置item為一個QWidget,設置好item對象之後,最後有一個函數去設置每個item的數據,例如背景圖片、圖片描述、日期、地址資訊等。當我們去遍歷該QListWidget的每一個item並將item轉換為當初設置的QWidget對象,調用該ui對象的成員方法來更新其上的數據即可:
//將圖片列表更新為第page頁的圖片數據
void MainWindow::updateListWidget(int page){
//獲取第page頁面的數據當在imageInfoList中
QList<BingImageDataInfo> imageInfoList = DataManager::GetImageInfoList(page);
//先初始化listWidget列表的每一項為空數據
for (int i = 0; i<ui->listWidget->count(); ++i)
{
QListWidgetItem *item = ui->listWidget->item(i);
QWidget * widget = ui->listWidget->itemWidget(item);
ImageInfoItem2* imageInfoItem = dynamic_cast<ImageInfoItem2*>(widget);
if(imageInfoItem!=NULL){
BingImageDataInfo info;//空數據
imageInfoItem->updateImageInfo(info);
}
}
//根據實際上得到的imageInfoList初始化listWidget
for (int i = 0; i<imageInfoList.size(); ++i)
{
//qDebug()<<imageInfoList[i].Url<<endl;
QListWidgetItem *item = ui->listWidget->item(i);
QWidget * widget = ui->listWidget->itemWidget(item);
ImageInfoItem2* imageInfoItem = dynamic_cast<ImageInfoItem2*>(widget);
if(imageInfoItem!=NULL){
imageInfoItem->updateImageInfo(imageInfoList[i]);
}
}
}
上面的程式碼中,先用空數據初始化了一遍item,然後用真實數據填充,因為這裡按分頁獲取的數據也許個數並不滿足一頁的數據項6個(比如最後一頁也許只有5個),這裡先通過QListWidget::itemWidget()得到一個綁定在該item上的QWidget對象,然後將其轉換成我們真實的 ImageInfoItem2 對象(就是那個ui類對象),並通過該對象的函數去更新item的數據。
QListWidget滾動條樣式表設置
本例中我們實際上並沒有允許滾動條出現,不過在最開始的時候確實考慮過使用滾動條,也就是讓每頁顯示不止6個圖片,但是發現對於這個軟體的這種布局使用滾動條比較奇怪,實際上滾動條通過QSS設置出來的效果還是很好的,在這裡將QSS放出來給需要的人。
/* QSS中不能用雙斜杠的注釋這會導致qss無效 */
QScrollBar:vertical {
border: 0px;
background: #202020;
width: 8px; /*設置一個非0的寬度讓滾動條顯示*/
margin: 0;
padding:0;
}
/*滑塊的樣式*/
QScrollBar::handle:vertical {
border: 0; /*設置border-radius屬性並不需要border屬性有值*/
background: rgba(70,70,70,85%);
min-height: 20px;
border-radius:4px; /*坑:這個圓角要注意,不能超過寬度的一半,否則沒有圓角效果*/
}
/*滑鼠放上滑塊的樣式,顏色透明度變一下*/
QScrollBar::handle:vertical:hover {
border: 0;
background: rgba(70,70,70,100%);
min-height: 20px;
border-radius:4px;
}
QScrollBar::add-line:vertical,QScrollBar::sub-line:vertical {
border: 0;
background: #202020;
height: 20px;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
/*這兩個屬性必須都要設置否則背景不會繼承基礎設置中的背景*/
QScrollBar::add-page:vertical,QScrollBar::sub-page:vertical
{
background:#202020;
}
其顯示的滾動條效果如下:

這裡在對QScrollBar的滾動條的QSS屬性加以說明,以便使用:

為方便,我這裡是自己在草稿紙上畫的,沒有用作圖工具,就將就著看一下吧。
分頁效果
分頁做起來很簡單,也是在QtCreator中自定義了一個ui介面類,將這個ui整體當作一個分頁控制項來用,在類裡面提供成員函數來設置各個按鈕的樣式,這個ui介面裡面就是一些分頁按鈕,上一頁,下一頁,一個輸入文本框,以及一個GO按鈕,根據當前分頁是第幾頁高亮表示當前分頁的按鈕,根據頁數的多少隱藏或顯示某些多餘的按鈕。每次點擊上一頁,下一頁或者在文本框中輸入數字轉到某一頁之後,分頁ui中的一系列的數字按鈕要重新初始化,並設置樣式。弄一個成員函數去統一更新就可以了。其餘的都是在這個ui類的構造函數中載入QSS樣式文件去設置其介面,這些都是最基本的樣式沒有什麼複雜的。當分頁控制項中的當前頁改變之後,我們可以發射一個訊號出去,通知主窗體上進行QListWidget的更新。這部分的邏輯沒什麼好說的,就不放程式碼了。在QtCreator看起來布局是這樣:
![]()
應用QSS之後的效果是這樣的:

Qt中讀取資料庫
在Qt中讀寫資料庫是很方便的,這裡以Sqlite為例子,主要是通過 QSqlDatabase 去連接資料庫,通過 QSqlQuery 去做查詢並獲取結果。這裡在查詢數據的時候,我們可以做一下簡單的封裝,我們首先需要有一個 QSqlDatabase 的對象實例,調用該實例的 open() 函數去打開資料庫連接,程式碼如下:
//得到一個QSqlDatabase的實例
SqliteDBAOperator::SqliteDBAOperator(QString dbName)
{
this->dbName = dbName;
if (QSqlDatabase::contains(dbName))
{
//這個類裡面應該有一個靜態的數據結構存儲 連接名 -> QSqlDatabase 對象的映射
db = QSqlDatabase::database(dbName);
}
else
{
//建立和sqlite數據的連接
db = QSqlDatabase::addDatabase("QSQLITE",this->dbName);
//設置資料庫文件的名字
QString dbFilePath = QCoreApplication::applicationDirPath() +QString("/")+ this->dbName;
db.setDatabaseName(dbFilePath);
}
}
//打開資料庫連接,之後就可以用這個 db 對象了
bool SqliteDBAOperator::openDb(void)
{
//打開資料庫
if(db.open() == false){
qDebug() << "connect db fail";
return false;
}
qDebug() << "connect db success";
return true;
}
這樣我們就得到了一個 QSqlDatabase 的對象變數 db ,之後就可以用這個QSqlQuery 去查詢:
QList<QMap<QString, QVariant> > SqliteDBAOperator::queryData(QString& str)
{
QSqlQuery query(this->db);
bool res = query.exec(str);
QList<QMap<QString, QVariant> > list;
if(res==true){
while(query.next()){
QSqlRecord record = query.record();
QMap<QString, QVariant> info;
for(int i=0;i<record.count();i++){//遍歷該record的所有列
QString field = record.fieldName(i);//得到欄位名,作為該map的key
QVariant var = record.value(field);
info[field] = var;
}
list<<info;
}
}
return list;
}
我們將查詢出來的數據放到了一個QList中,其每一個元素是一個QMap,實際上就是表中的一行數據,用QMap可以非常方便的通過表的欄位來引用該行數據的某一列的數據值,這裡的值是 QVariant 類型,那麼在使用的時候可以像下面這樣用:
QString sql = QString("select aa,bb from table ");
QList<QMap<QString, QVariant> > listMap = sqliteAdapter->queryData(sql);
QList<BingImageDataInfo> list;
if(listMap.size()>0){
for(int i=0;i<listMap.size();i++){
QMap<QString, QVariant> &row = listMap[0];
BingImageDataInfo info;
info.Url = row["url"].toString();
info.Time = row["time"].toInt();
list<<info;
}
}
使用表的欄位名來引用該欄位的值看起來是不是很直觀方便,另外要使用Sql功能,我們還需要在項目的pro文件中加上 QT += sql
自定義菜單項
我們這裡所說的自定義菜單,不是給菜單項添加一個圖標,在Qt中,可以使用QSS給每個菜單項設置樣式,比如邊框,背景顏色,菜單項文字前面的圖標等。但是我們這裡不僅限於此,我們的軟體中有一個菜單按鈕,點開之後是如下的菜單:

首先這個菜單是圓角的,其次這個菜單看起來不是常規的每個菜單項就是一行文字(最多前面加個圖標修飾一下),這裡面有複選框,有文字,還有按鈕,其實這個框框中的3個複選框,以及3個QLabel,還有一個按鈕,他們僅僅包含在一個菜單項中,也就是說這個菜單只有一個菜單項,這個菜單項是我們自定義的,同樣的這個菜單項,是我們自定義的一個ui文件類(包含了對應的頭文件以及cpp文件),當我們以自定義的ui文件類的方式來定義菜單項的時候,我們可以想做多複雜就做多複雜,像很多音樂播放器的任務托盤上的菜單有播放,調整音量,有的甚至有專輯封面,這些都是小菜一碟。首先截圖中的菜單按鈕(信封按鈕的右邊)是一個QPushButton,在Qt中可以為某一個按鈕關聯一個彈出菜單的,只需要調用這個按鈕的 setMenu() 函數傳遞一個 QMenu 就可以將這個 QMenu 與這個按鈕關聯起來,默認情況下這個 QMenu 彈出的位置是與這個QPushButton沿左邊線對齊的,可以看到,上面的截圖顯然不是,上面的截圖中,彈出的菜單看起來與按鈕是居中對齊的(這裡左對齊不好看),為了改變這種左對齊的默認行為,我們自己定義了一個繼承自QMenu的類,叫做 PopMenu ,在這個類中重寫了QMenu的 showEvent 函數,在此函數中,重新調整菜單顯示的位置,程式碼如下:
PopMenu::PopMenu(QPushButton* button, QWidget* parent) : QMenu(parent), b(button)
{
this->b = button; //保存其關聯的按鈕
}
void PopMenu::showEvent(QShowEvent* event)
{
//根據按鈕的位置,調整菜單的位置
QPoint p = this->pos();
int diff = (this->width() - b->width())/2;
int newx = p.x()-diff;
int newy = p.y()+5;
this->move(newx,newy);
}
與菜單按鈕關聯的菜單實際上是 PopMenu 其也是一個 QMenu 對象,那麼我們自定義的表示菜單項的ui類(下面程式碼中的 MenuSetting 類)如何設置到這個 PopMenu 菜單裡面的呢,
void MainWindow::initMenuSetting()
{
//構造 PopMenu 對象
this->menuSetting = new PopMenu(ui->btnMenu,this);
//構造自定義的菜單項的ui類對象,實際上是一個繼承自QWidget的子類
MenuSetting *menuSettingWidget = new MenuSetting(this);
//下面兩句是關鍵,這裡定義的菜單項不是QAction 而是 QWidgetAction 就是用來將菜單項設置為一個Wdiget的,這個QWidgetAction實際上是QAction的子類
QWidgetAction *action = new QWidgetAction(this->menuSetting);
//將這個Action設置為一個QWdiget,就是我們自定義的ui類對象
action->setDefaultWidget(menuSettingWidget);
//為這個菜單添加Action
this->menuSetting->addAction(action); //這個一定要有
//這個寫在主樣式表裡面沒效果,要寫在程式碼中才行
this->ui->btnMenu->setStyleSheet("QPushButton#btnMenu::menu-indicator{image:none;background-color:transparent;}");
//給按鈕綁定此彈出菜單
ui->btnMenu->setMenu(this->menuSetting);
//這裡為什麼需要,因為菜單作為一個獨立的存在,其地位跟窗口是一樣的,菜單並不是被包含在主窗口的任何控制項中
//沒有任何控制項是菜單的容器,他其實就是一個獨立的窗口,只不過菜單是一種比較特殊的窗口,所以我們需要單獨為其設置窗體標誌,設置背景透明
menuSetting->setWindowFlags(Qt::Popup|Qt::FramelessWindowHint);
menuSetting->setAttribute(Qt::WA_TranslucentBackground);
}
至於菜單的圓角效果,那就更簡單了,其實是我們自定義菜單項中的ui類對象中的容器QFrame,我們設置QSS的時候設置成圓角就可以。這裡有一點要注意的是,一旦一個QPushButton 設置了一個關聯的彈出菜單之後,該QPushButton的行為就有了變化,單擊這個按鈕的時候他的行為就是彈出其關聯的彈出菜單,他不會再響應其 click 槽函數,這點是需要注意的,此時這個按鈕的唯一功能就是單擊之後,彈出菜單。
Qt中托盤程式的實現
在Qt中實現托盤程式也比較容易,Qt中的托盤程式主要是通過 QSystemTrayIcon 以及其關聯的上下文菜單對象來實現的,具體程式碼如下:
void MainWindow::initTraySystem(){
//初始化托盤圖標對象
this->trayIcon = new QSystemTrayIcon( this );
//設定托盤圖標
this->trayIcon->setIcon( QIcon( QPixmap( ":/images/title/icon_16.ico" ) ) );
//設置提示文字
this->trayIcon->setToolTip(QString(WINDOW_TITLE));
//讓托盤圖標顯示在系統托盤上
this->trayIcon->show();
//連接訊號與槽,實現單擊圖標恢復窗口的功能,槽是自定義的槽函數TrayIconAction
connect( this->trayIcon, SIGNAL( activated( QSystemTrayIcon::ActivationReason ) ), this, SLOT( TrayIconAction( QSystemTrayIcon::ActivationReason ) ) );
//初始化托盤菜單及功能,這裡定義一個菜單,並定義各個菜單項
this->trayMenu = new QMenu(this);//初始化菜單
this->trayMenu->setObjectName("trayMenu");
this->trayActionOpen = new QAction(this);//打開主窗口菜單
this->trayActionOpen->setText( "主窗口" );
connect(this->trayActionOpen, SIGNAL(triggered()), this, SLOT(showNormal()));//菜單中的顯示窗口,單擊顯示窗口
this->trayActionquit = new QAction(this);//退出菜單
this->trayActionquit->setText( "退出" );
connect(this->trayActionquit, SIGNAL( triggered()), qApp, SLOT(quit()));// 菜單中的退出程式,單擊退出
this->trayActionAbout = new QAction(this);//關於
this->trayActionAbout->setText("關於");
connect(this->trayActionAbout, SIGNAL( triggered()), this, SLOT(TriggerAbout()));// 彈出關於窗口
//將定義好的菜單與托盤圖標關聯起來
this->trayIcon->setContextMenu(this->trayMenu);
this->trayMenu->addAction(this->trayActionOpen);
this->trayMenu->addAction(this->trayActionAbout);
this->trayMenu->addAction(this->trayActionquit);
//為菜單設置樣式
QFile file(":/qss/traymenu.qss");
file.open(QFile::ReadOnly);
QTextStream filetext(&file);
QString stylesheet = filetext.readAll();
this->trayMenu->setStyleSheet(stylesheet);
file.close();
}
這裡還要注意一點,由於Qt程式中,當所有的窗口都關閉之後,程式就退出了,但是我們希望窗口都關閉之後,程式仍然還在運行,進程是不能退出的,因為托盤圖標還在,這個時候就需要在Qt程式的main函數中,設置一下如下:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
app.setQuitOnLastWindowClosed(false);
.... 其他程式碼省略 ....
}
Qt單進程的實現
我們希望在已經有一個進程打開的情況下,用戶再雙擊打開程式直接觸發之前打開的程式窗口,而不是同時打開多個程式,就像目前這個程式,如果用戶已經有一個進程最小化到了任務托盤中,當用戶再次打開程式的時候,直接將當前運行在任務托盤的程式觸發其顯示主窗口即可。要實現單進程,就必須有一種方案,能在進程啟動的時候標識一個全局數據,在進程退出之後自動取消標識。基於共享記憶體、文件鎖等方式的實現不是很完美,總會有一些問題。我們這裡使用 QLocalServer 的方式來實現,實現程式碼如下:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//MainWindow w;
/*
* MainWindow的定義不能放在這裡,如果已經有一個實例在運行的情況下,這裡勢必會進入if分支
* 然後調用 return -1 我們預期的情況下當前這個進程應該會退出,但是實際實驗中發現,windows 的任務管理器中
* 會多出一個進程出來,在已經運行一個客戶端的情況下,後面的每雙擊一下程式,任務管理器都會多出一個進程
* 很奇怪,通過日誌列印這裡也確實是進入了if分支,並且確實是return -1 也就是這個main函數已經退出了
* 但是進程就在任務管理器裡面,並且沒有任何介面(都沒有走到下面的w.show()不會有窗口顯示)
*/
//初始化一個LocalServer
LocalServer server;
if(!server.init(LOCAL_SERVER_NAME)){
// 初使化Server失敗, 說明已經有一個在運行了,通過客戶端連接這個已經運行的Server
// 並給他發個消息讓已經運行的那個進程顯示主窗口,自己則退出
LocalClient::ConnectSendMessage(LOCAL_SERVER_NAME,ACTIVE_MESSAGE);
qDebug()<<"localserver init false exit -1"<<endl;
a.quit();
return -1;
}
//初始化數據
if(!DbDataManager::Init()){
qDebug()<<"DbDataManager::Init() false exit -1"<<endl;
return -1;
}
/*
* 這個定義放在這裡就不會有上面描述的後打開的進程不退出的問題,不知道為什麼在上面那種情況下
* main函數都return了進程卻不退出。以為是MainWindow中的構造函數中有初始化任務托盤可能跟任務托盤有關係
* 但是注釋掉初始化任務托盤的相關函數之後還是一樣的情況,具體原因還不清楚
*/
MainWindow w;
//LocalServer 初始化成功,當server收到消息之後用一個槽函數處理
//收到激活消息之後顯示主窗口
QObject::connect(&server, &LocalServer::newMessage, [&](const QString &message){
if(message == ACTIVE_MESSAGE){
qDebug()<<"recv active message show normal window"<<endl;
w.showNormal();
}
});
//所有窗口都關閉之後不退出程式,不設置的話如果關閉主窗口之後,在任務托盤上打開"關於"窗口之後,關閉"關於"窗口
//會導致進程退出(可能是所有可見窗口都關閉了所以進程自動退出了)
a.setQuitOnLastWindowClosed(false);
//根據命令行參數來決定程式啟動的時候是否顯示主窗口,還是直接最小化到任務托盤
//一般如果程式是隨系統自啟動的情況下,讓他直接最小化到任務托盤
//如果是用戶自己雙擊啟動的情況,就顯示主窗口
//判斷程式是隨系統自啟動還是用戶雙擊啟動的區別,就是有沒有命令行參數
//用戶雙擊啟動是不會有命令行參數的
bool showMainForm = true;
if(argc >= 2){
QString twoParam = argv[1];
if(twoParam==CMD_PARAM_AUTO_RUN){
showMainForm = false;
}
}
if(showMainForm){
w.show();
}
return a.exec();
}
上面的程式碼中 LocalServer,LocalClient 是我們基於 QLocalServer,QLocalSocket 包裝的本地服務和客戶端。客戶端的主要程式碼如下:
//localclient.cpp
void LocalClient::ConnectSendMessage(QString serverName,QString message){
QLocalSocket ls;
ls.connectToServer(serverName);
if (ls.waitForConnected()){
QTextStream ts(&ls);
ts << message;
ts.flush();
ls.waitForBytesWritten();
}
}
服務端的主要程式碼如下:
//localserver.cpp
LocalServer::LocalServer(QObject *parent) : QObject(parent)
{
m_server = 0;
}
LocalServer::~LocalServer()
{
if (m_server)
{
delete m_server;
}
}
bool LocalServer::init(const QString & servername)
{
// 如果已經有一個實例在運行了就返回0
if (isServerRun(servername)) {
return false;
}
m_server = new QLocalServer;
// 先移除原來存在的,如果不移除那麼如果
// servername已經存在就會listen失敗
QLocalServer::removeServer(servername);
// 進行監聽
m_server->listen(servername);
connect(m_server, SIGNAL(newConnection()), this, SLOT(newConnection()));
return true;
}
// 有新的連接來了
void LocalServer::newConnection()
{
QLocalSocket *newsocket = m_server->nextPendingConnection();
connect(newsocket, SIGNAL(readyRead()), this, SLOT(readyRead()));
}
// 可以讀數據了
void LocalServer::readyRead()
{
// 取得是哪個localsocket可以讀數據了
QLocalSocket *local = static_cast<QLocalSocket *>(sender());
if (!local)
return;
QTextStream in(local);
QString readMsg;
// 讀出數據
readMsg = in.readAll();
// 發送收到數據訊號
emit newMessage(readMsg);
}
// 判斷是否有一個同名的伺服器在運行
bool LocalServer::isServerRun(const QString & servername)
{
// 用一個localsocket去連一下,如果能連上就說明
// 有一個在運行了
QLocalSocket ls;
ls.connectToServer(servername);
if (ls.waitForConnected(1000)){
// 說明已經在運行了
ls.disconnectFromServer();
ls.close();
return true;
}
return false;
}
這樣就通過 QLocalServer 和 QLocalSocket 實現了單進程,實際上這裡的服務端並沒有監聽一個埠,在windows的實現中是通過命名管道的,這個管道在我們程式啟動之後被打開,在進程退出之後就自動沒有了。當程式啟動的時候啟動服務端,如果另一個程式也啟動,他會發現啟動通過一個名字的服務端失敗了,說明之前已經有一個服務端已經啟動了,這個時候他只需要以客戶端的方式連接之前啟動的服務端,給他發一個消息,那個服務端收到消息之後顯示其主窗口即可。
Qt寫註冊表實現自啟動
我們通過在註冊表項中添加項目來實現程式的自啟動,在Qt中寫註冊表是很簡單的,主要程式碼如下:
//開機自啟動註冊表鍵
#define REG_RUN "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"
//設置程式開機自啟動
void Tools::setAutoRunning(bool is_auto_start)
{
QString application_name = QApplication::applicationName();
QSettings *settings = new QSettings(REG_RUN, QSettings::NativeFormat);
if(is_auto_start)
{
//程式執行文件的路徑 F:\\xxx\\xxx\zz.exe
QString application_path = QApplication::applicationFilePath();
application_path += " " ;
//加上命令行參數 F:\\xxx\\xxx\zz.exe autorun
application_path += CMD_PARAM_AUTO_RUN;
settings->setValue(application_name, application_path.replace("/", "\\"));
}
else
{
settings->remove(application_name);
}
delete settings;
}
效果是這樣:

在執行緒中請求http或者https資源
我們需要通過網路去請求json數據,以及下載壁紙圖片,在Qt中是通過 QNetworkAccessManager 來實現的,執行緒的功能程式碼如下:
//定時任務讀取json數據,並從解析出的json數據中獲取要下載的圖片url
//進一步下載圖片,這裡面刪除了一些無關的程式碼,保留了關鍵的程式碼
void DownloadThread::run(){
QNetworkAccessManager* manager = new QNetworkAccessManager();
while(true){
QThread::sleep(TIME_INTERVAL_MINS*60);//secs
//輸出當前Qt支援的ssl版本情況
//qDebug()<<"QSslSocket="<<QSslSocket::sslLibraryBuildVersionString();
//輸出當前Qt是否支援ssl,如果支援這裡返回true,否則返回false
//qDebug() << "OpenSSL支援情況:" << QSslSocket::supportsSsl();
QEventLoop loop;
QObject::connect(manager, &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
//request對象
QNetworkRequest request;
//設置UserAgent頭
request.setHeader(QNetworkRequest::UserAgentHeader, USER_AGENT_FIREFOX);
//設置需要請求的資源url,例如 //www.abc.com/xx.jpg
request.setUrl(QUrl(BING_JSON_URL));
//調用 QNetworkAccessManager 的get函數發送http請求
QNetworkReply *reply = manager->get(request);
loop.exec();
//當請求完成之後通過 QNetworkReply 的readAll()讀取響應結果
//readAll()函數返回的是一個 QByteArray 如果響應是一個文本,我們可以賦值給一個QString,比如這裡是一個json文本
QString json = reply->readAll();
//刪除該reply對象
reply->deleteLater();
BingImageDataInfo info;
//解析json數據,這裡面通過 QJsonDocument 相關的類來解析json數據,很簡單,就不展示 parseJson() 的實現了
bool parseRes = parseJson(json,info);
qDebug()<<"parse"<<endl;
if(parseRes){ // added to db and mem
//待下載的圖片的url路徑
QString downloadUrl = BING_DOMAIN+info.Url;
//重新設置request的url
request.setUrl(QUrl(downloadUrl));
//發送請求獲取圖片內容
QNetworkReply *replyDownload = manager->get(request);
loop.exec();
QFile fDownload(imageFilePath);
if(fDownload.open(QIODevice::WriteOnly)){
fDownload.write(replyDownload->readAll());//讀取響應結果並寫入磁碟中
fDownload.close();
}
replyDownload->deleteLater();
}
}
} // end while true
}
當在執行緒中使用 QNetworkAccessManager 來請求http資源的時候,有幾個問題需要注意,第一個我們的QNetworkAccessManager變數最好是定義在DownloadThread執行緒的run函數中,就像上面的程式碼中一樣,不能定義他的構造函數中,否則當我們在主執行緒中實例化這個執行緒類DownloadThread並啟動他的時候,QNetworkAccessManager 對象由於是在DownloadThread的構造函數中初始化,而DownloadThread的構造函數實際上是在主執行緒中運行的,所以實際上最終 QNetworkAccessManager 對象可以認為是屬於主執行緒。然而我們卻在 DownloadThread::run() 中通過 connect() 函數為這個manager綁定訊號槽,也就是說我們綁定訊號槽的語句是在 DownloadThread 這個執行緒中做的,那麼這裡就出問題了。我們在一個執行緒中定義一個對象,卻在另一個執行緒中為這個對象綁定訊號槽。所以這裡我們需要將 QNetworkAccessManager 對象也定義在 DownloadThread::run() 中,這樣這個manager對象就屬於 DownloadThread執行緒,而不是主執行緒了。
另外一個問題,從上面的程式碼中可以看到,我們在連接manager對象的訊號槽之前,通過語句 QEventLoop loop; 定義了一個局部的事件循環,在我們的Qt程式中,主執行緒(也就是ui執行緒)會啟動一個事件循環,而我們的訊號的觸發,對應的槽函數被調用,與這個事件循環是有關係的,可以認為當一個訊號被觸發之後,其被放到當前事件循環的隊列中,當事件循環檢測到訊號的時候,去查找該訊號綁定的槽函數並執行之。如果沒有事件循環,那就不會有訊號槽的運行。所以我們需要在執行緒 DownloadThread 中啟動一個局部的事件循環,否則manager對象的槽函數不會被觸發。這裡會報錯:QNetworkAccessManager without finished signal
使用事件循環的另一個好處是可以將這裡的非同步轉成同步,因為在執行緒中我們需要同步去下載,如果是非同步的話,這裡不是很好跟執行緒結合在一起使用。當調用 loop.exec() 之後程式就阻塞在調用處,直到 manager 的 finished 訊號被觸發的時候,調用之前綁定的 QEventLoop::quit 槽函數,從而退出事件循環,loop.exec() 從調用處返回,後面的語句繼續執行。這樣就達到了類似同步請求http資源的效果了。
在Qt中想要請求// 的資源,還要做一些別的設置,不做任何設置的情況下,請求https資源是沒有任何效果的,我們首先可以在程式中列印出 QSslSocket::sslLibraryBuildVersionString() 以及 QSslSocket::supportsSsl() 來查看當前Qt版本支援ssl的情況。如果不支援OpenSSL 那麼我們去下載並安裝Windows的OpenSSL,下載介面如下:

根據自己使用的QT編譯器是32位還是64位,下載相應的安裝包。將下載的安裝包進行安裝,安裝之後,找到安裝目錄下的bin目錄中的兩個文件(libcrypto-1_1.dll 和libssl-1_1.dll),拷貝到QT編譯器目錄下即可。例如我的OpenSSL安裝在 C:\Program Files (x86)\OpenSSL-Win32\bin 將裡面的兩個dll 拷貝到我的Qt的32位編譯器的目錄 D:\Qt\Qt5.14.2\5.14.2\mingw73_32\bin 下面,這兩個dll文件在最後部署程式的時候也要放到部署包中。
Qt程式的部署與發布
Qt程式在寫好之後,如何部署呢,在QtCreator中我們一般是在Debug模式下編譯調試程式,當程式寫完之後,我們切換到Release模式下,重新編譯項目,最後會在相應的Release目錄下生成一個可執行文件,但是僅僅只有一個exe文件直接雙擊肯定是運行不了的,好在Qt為我們提供了工具,獲得這個exe文件運行所依賴的其他文件。按照下面的步驟操作就可以了:
1.首先把我們編譯出來的Release最終程式拷貝到任意一個文件夾,例如下面這樣:

我這裡是拷貝到 F:\bingimg\bingimg.exe 然後我們需要打開Qt提供的命令行環境,比如我的電腦上是這麼打開: 開始->所有程式->Qt 5.14.2->5.14.2->MinGW 7.3.0(32-bit)->Qt 5.14.2 (MinGW 7.3.0 32-bit) 如下圖所示,不同的電腦,不同的版本安裝之後可能略有差別:

因為我的程式編譯的是32位的,所以我這裡打開的是32位的命令行環境,打開Qt命令行環境之後,我們切換目錄到剛剛拷貝編譯程式的那個目錄 F:\bingimg 下面,之後運行命令 windeployqt.exe bingimg.exe 這樣這個部署命令會自動檢測程式 bingimg.exe 所依賴的庫以及其他文件,並將其依賴的文件都拷貝到其所在的目錄 F:\bingimg 下面。然後雙擊我們的程式就可以運行了:

可以看到運行 windeployqt.exe 命令之後,後面的工作都是自動的,到命令全部執行完成,我們在查看我們的目錄 F:\bingimg 可以看到如下:

所有需要的庫文件都已經拷貝到目標程式所在的目錄,不過這裡我們還需要將openssl的那兩個dll拷貝到這個目錄下,至此必應壁紙程式的編寫以及發布全部完成。
