1、前言
QT Creator5.9.9
近段時間學習了QT的一些設計基礎,忍不住設計了個五子棋小游戲項目進行實戰,從最開始的創建,到最后的整個游戲安裝包,經過磕磕絆絆,最終結果還算滿意。當然作為新手菜鳥,肯定存在一些問題,如果你恰好看到這篇文章,若有看到不當的地方,歡迎提及。
先來看下游戲界面整體效果:
實現的功能有:與電腦對弈(簡單的AI操作)、每步15秒倒計時、玩家信息顯示、下棋等的提示音、悔棋,重新開始,退出按鍵功能、簡易的信息提示、界面拖動
代碼已經托管至GitHub,有需要可以自己下載,傳送門:https://github.com/fuxiai/FiveChessGame
2、實現步驟
1、先設計一個棋盤類,即棋盤部件,能夠展現棋盤,鼠標移動提示下棋點,點擊下棋等功能,並位該類提供必要的接口。
2、設計整個游戲窗口,通過QT設計模式,進行ui設計,如標題 、玩家信息、按鍵位置、棋盤位置等,為這些功能提供部件支持和布局,然后在代碼當中對每個部件進行處理。
3、設計游戲運行的邏輯,如開始游戲、游戲先后手、對弈過程、結果判定、悔棋、重新開始與退出等功能。
4、對游戲界面進行美化,如添加背景,圖片信息,修改按鍵樣式,在最后去除窗口邊框。
5、設計圓圈進度條部件類,為游戲對弈添加進度條時間提示。
6、添加系統提示音,如開始游戲聲音、落子聲音、結束聲音。
7、升級電腦AI下棋算法。
8、為工程添加動態庫支持,使用inno setup軟件打包成一個安裝包程序。
2.1棋盤的實現
2.1.1創建部件
整一個棋盤就是一個窗口部件,直接創建一個app項目繼承於QWidget控件,此處不使用ui設計,以便最終可以得到該部件的類文件,方便移植到總窗口中。
工程創建完成后,就得到一個窗口啦!
2.1.2定義棋盤各類邊界和信息
此處定義的棋盤為15x15大小,此大小使用宏定義或定義成變量,后邊會經常使用。還需要定義多個間隔變量,如棋盤與邊界的距離、棋格的大小,這樣后期直接初始化中可以隨意修改而不影響棋盤架構。定義枚舉enum ChessType {noChess, blackChess, whiteChess};代表棋子類型,定義一塊ChessType類型的二維數組空間chessInfo[15][15];該空間代表當前棋盤上存在的棋子信息。在構造函數中為定義的變量賦初值,然后根據下圖示的變量信息設置窗口的大小,並固定窗口大小。
2.1.3繪制棋盤
QWidget部件具有繪畫事件virtual void paintEvent(QPaintEvent *event),是個虛函數,我們的類是由QWidget類繼承而來,能夠對繪畫事件進行重寫,當發生重新繪制或者更新窗口等繪制事件就會響應。
2.1.3.1重寫接口
類定義中添加:
1 protected: 2 void paintEvent(QPaintEvent *e); 3
接口實現內容:
A、定義一個畫家。
B、定義一只畫筆,給畫筆設置樣式(顏色、線條寬度、線條類型),並把畫筆給畫家。
C、調用畫家的drawPixmap方法繪制棋盤背景圖片。
D、通用畫家的drawLine方法依次畫橫豎各15條線組成棋盤網格。
E、查看當前棋盤上棋子信息,當存在棋子時,以網格坐標為准推導繪制位置,繪制相應的棋子。

1 void ChessBoard::paintEvent(QPaintEvent *e) 2 { 3 QPixmap chessBoardPic(QString(":/images/chessBoard.jpg")); 4 QPixmap blackChessPic(QString(":/images/black.png")); 5 QPixmap whiteChessPic(QString(":/images/white.png")); 6 QPainter painter(this); 7 QPen pen; 8 pen.setColor(Qt::black); 9 pen.setWidth(2); 10 pen.setStyle(Qt::SolidLine); 11 painter.setPen(pen); 12 painter.drawPixmap(0, 0, this->width(), this->height(), chessBoardPic); 13 14 for(int i = 0; i < chequerNumOfLine; i++) { 15 for(int j = 0; j < chequerNumOfLine; j++) { 16 painter.drawLine(topW, topH + j * chequerSide, 17 topW + (chequerNumOfLine-1) * chequerSide, topH + j * chequerSide); 18 painter.drawLine(topW + j * chequerSide, topH, 19 topW + j * chequerSide, topH + (chequerNumOfLine-1) * chequerSide); 20 } 21 } 22 for(int i = 0; i < chequerNumOfLine; i++) { 23 for(int j = 0; j < chequerNumOfLine; j++) { 24 if (chessInfo[i][j] == blackChess) { 25 painter.drawPixmap(topW + i * chequerSide - chessSide/2, topH + j * chequerSide - chessSide/2, 26 chessSide, chessSide, blackChessPic); 27 } 28 if (chessInfo[i][j] == whiteChess) { 29 painter.drawPixmap(topW + i * chequerSide - chessSide/2, topH + j * chequerSide - chessSide/2, 30 chessSide, chessSide, whiteChessPic); 31 } 32 } 33 } 34 //this->update(); 35 }
2.1.3.2添加資源文件
在我們項目當中,需要用到許多的圖片文件,需要把圖片放置到工程文件夾下,然后為工程添加資源文件,就能通過相對路徑的方式調用文件。
添加資源文件方法:
新建一個資源文件夾(文件->新建)。
然后工程目錄下會生成資源文件夾
選擇紅框中的.qr文件,又鍵選中Open in Editor進入編輯模式添加文件。
資源文件調用時的相對路徑是以:開頭的,或着當不識別時,可以在以qrc:開頭。
當我們paintEvent事件完成后,此時我們通過修改初始化時棋盤信息的內容就可以看到棋子的顯示啦。
2.1.4鼠標捕獲
我們下棋是用鼠標點擊,當鼠標移動到下棋點的時候,需要讓鼠標改變樣式(此處由箭頭變成手指),來框定是否是下棋點。具體方法是重寫鼠標移動事件virtual void mouseMoveEvent(QMouseEvent *event)。當我們移動鼠標的時候就會產生該事件。鼠標移動事件默認是需要按住鼠標才會產生事件的, 所以需要修改相關屬性。
打開鼠標的跟隨屬性:
1 this->setMouseTracking(true); // 在初始化時,打開鼠標跟蹤,則在窗口上只要移動鼠標就 //會產生鼠標移動事件。
代碼實現:

1 void ChessBoard::mouseMoveEvent(QMouseEvent *e) 2 { 3 // 去除邊界無效跟蹤 4 if ((e->x() < (topW - rangeCenter) || e->x() > (topW * 2 + chequerSide * (chequerNumOfLine-1) - rangeCenter)) || 5 e->y() < (topH - rangeCenter) || e->y() > (topH * 2 + chequerSide * (chequerNumOfLine-1) - rangeCenter) ) { 6 this->setCursor(Qt::ArrowCursor); 7 dropChessEnable = false; 8 return; 9 } 10 // 根據每個落棋點改變光標格式 11 int inChequerSideX = (e->x() - topW) % chequerSide; 12 int inChequerSideY = (e->y() - topH) % chequerSide; 13 dropW = (e->x() - topW) / chequerSide; 14 dropH = (e->y() - topH) / chequerSide; 15 if ((inChequerSideX < rangeCenter || inChequerSideX > (chequerSide - rangeCenter)) && 16 (inChequerSideY < rangeCenter || inChequerSideY > (chequerSide - rangeCenter))) { 17 this->setCursor(Qt::PointingHandCursor); 18 dropChessEnable = true; 19 // 落點位置進一格 20 if (inChequerSideX > (chequerSide - rangeCenter)) { 21 dropW++; 22 } 23 if (inChequerSideY > (chequerSide - rangeCenter)) { 24 dropH++; 25 } 26 } else { 27 this->setCursor(Qt::ArrowCursor); 28 dropChessEnable = false; 29 } 30 }
2.1.5下棋操作
通過2.1.4步的操作,我們已經能得到是否可以下棋的標志dropChessEnable和當前下棋的實時網格坐標(dropW,dropH)。那么同理的,鼠標按鍵按下也有對應的事件函數能夠重載--
virtual void mousePressEvent(QMouseEvent *event)。
當我們被允許下棋的時候,根據當前實時網格坐標,直接根據當前的棋子類型(黑/白),直接修改棋盤信息chessInfo[dropW][ dropH]內容給即可。每次下完后判斷當前是否五連。
代碼實現:
1 void ChessBoard::mousePressEvent(QMouseEvent *e) 2 { 3 if (dropChessEnable == false || dropedFlag == false) { 4 return; 5 } 6 if(e->button() == Qt::LeftButton) { 7 if (chessInfo[dropW][dropH] == noChess) { 8 chessInfo[dropW][dropH] = currentChess; 9 emit chessDroped(dropW, dropH); 10 dropedFlag = false; 11 if(isGameOver(dropW, dropH)) { 12 emit boardGameOver(); 13 } 14 this->update(); 15 } 16 } 17 }
到此,我們的棋盤就實現了,能夠顯示並且下棋。當然要想用到主窗口當中,為保證棋盤的封裝性,還需要提供各種接口供主窗口調用,例如獲取和修改棋盤信息,獲取棋盤的相關規格信息,修改一些需要處理的標志位……;還需要添加下棋和游戲結束判定的信號供給主窗口的槽函數響應。