20200603黑白棋
我们的实现目标如下图所示:(不是五子棋)
步骤:1画背景;2画线(横纵各9根);3画棋子(8*8=64个)
掌握:绘图技巧、封装思想
New Project:创建棋盘类Chess,不要 ✔ 界面(不能用设计模式,只能用代码绘制ui界面了)
(1)画背景
1.1 启用绘图事件
protected: void paintEvent(QPaintEvent *);
背景加载成功!
优先使用构造函数,也能实现该功能。
(1)对象被创建的时候,立马自动触发调用构造函数
(2)不用进行函数之间的切换,省了资源开销,提高效率
供别人随意改变背景:公共方法改变私有变量
头文件中增加:
private:
QString bg_filename;
void Init();
一般情况下,变量通常都是私有的。可以采用方法的形式让外界访问,这体现了面向对象的方式。
方法:void ChangeBGImage(const QString filename); //加const的原因: 防止传进去后被改变
方法的实现如下:驼峰命名法
(2)画线
2.1 设置颜色、样式、宽度等,提供一个统一的外部接口(方法)
初始化中给默认值
窗体一改变,立马给它赋值(按比例缩放),测试一下:
同理可得:横线和纵线组成的棋盘格。
(3)画棋子
先找规律
用棋子铺满整个格子:
把64个格子的数据(黑OR白)进行保存
public:
enum ChessType{Empty=0,White, Black}; //棋子类型:0不显示;1白色;2黑色
private:
int chessData[8][8]; //保存棋子数据的容器 //需要初始化,不然里面会有随机数
//(3)画棋子
QString chessFilename;//棋子名字
for(int i=0;i<8;i++){
for(int j=0;j<8;j++){
if(chessData[i][j] == White){
chessFilename = "../20200603/Images/white.PNG";
}
else if(chessData[i][j] == Black){
chessFilename = "../20200603/Images/black.PNG";
}
else{
chessFilename.clear();
continue;
}
painter.drawPixmap(startX + i* gridWidth, startY + j * gridHeight, gridWidth, gridHeight, QPixmap(chessFilename));
}
}
规则:中心四个点要占住
—————————分割线—————————分割线—————————分割线—————————
(4)接下来实现:鼠标点击事件,得出在哪里落棋子
备注:一定要刷新,不然鼠标点击后棋子不能及时出现。
同理 ChangeBGImage()和ChangeLine()中也要增加刷新语句 this->update();
(5)增加位点,增加界面:
5.1 先定义信号,并在鼠标点击事件里发送该信号。
signals:
void SignalSendChessData(int i,int j);//发送点击的坐标
5.2 改变棋盘的显示值
public:
void setChessStatus(void *p);//外面传一个数组进来
实现该方法:
void Chess::setChessStatus(void *p){
memcpy(chessData, p, sizeof(int)*8*8);//把外面的拷贝过来
this->update();
}
5.3 代码优化,将业务逻辑分开:初始化棋盘 void InitChess();————private:
转移 Init() 中的部分代码块,实现该方法,记得调用哦
void Chess::InitChess(){
//初始化棋盘数据数组
memset(chessData, 0, sizeof(int)*64);//memeset编译不出错,但这种方式不利于人理解。
//我们用for循环也可以实现数组的清零
for(int i = 0; i < 8;i++){
for(int j = 0; j < 8;j++){
chessData[i][j] = Empty;
}
}
chessData[3][3] = Black;//指定位置落棋子
chessData[4][3] = White;
chessData[3][4] = White;
chessData[4][4] = Black;
}
5.4 设计UI界面
右击 ---> 添加新文件,Qt设计师界面类QWidget
接下来:设计窗体
用到Frame容器和Grid Layout布局方式
Frame属性设置:frameShape为Box,frameShadow为Raised。
先把3个水平布局,整体再栅格布局。如果觉得太宽太高的话,可以先在属性里把最大宽度/最大高度定死 maximumSize,然后去布局。
把之前的棋盘类导入新界面中,成功实现如下:
QWidget 缩小这么多没有意义:可以在*.ui文件中改变minimumSize的值。
//黑白棋吃子规则,返回值是能吃子的个数
//x, y: 棋盘二维数组坐标位置 //chess: 棋子状态 //currentRole: 枚举变量(Empty, Black, White)
//eatChess: true代表吃子,false只是判断有子可吃,默认为true
int judgeRule(int x, int y, void *chess, Chess::ChessType currentRole, bool eatCehss);
int ChessForm::judgeRule(int x, int y, void *chess, Chess::ChessType currentRole, bool eatCehss){
//棋盘的八个方向
int dir[8][2] = {{1,0},{1,-1},{0,-1},{-1,-1},{-1,0},{-1,1},{0,1},{1,1}};
int temp_x = x, temp_y = y; //临时保存棋盘数组坐标位置
int i = 0, eatNum = 0; //初始化数据
typedef int (*p)[8]; //自定义类型
p chessFlag = p(chess); //类型转换
if(chessFlag[temp_x][temp_y] != Chess::Empty) //如果此方格内已有棋子,返回;
return 0;
//棋盘的八个方向
for(i = 0; i < 8; i++)
{
temp_x += dir[i][0]; temp_y += dir[i][1]; //准备判断相邻棋子
//如果没有出界,且相邻棋子是对方棋子,才有吃子的可能
if((temp_x <8 && temp_x >=0 && temp_y <8 && temp_y >=0)
&& (chessFlag[temp_x][temp_y] !=currentRole) && (chessFlag[temp_x][temp_y] != Chess::Empty))
{
temp_x += dir[i][0];
temp_y += dir[i][1];
while(temp_x < 8 && temp_x >=0 && temp_y <8 && temp_y >=0){
if(chessFlag[temp_x][temp_y] == Chess::Empty)//遇到空位,跳出
break;
if(chessFlag[temp_x][temp_y] == currentRole) //找到自己的棋子,代表可以吃子
{
if(eatCehss = true)//确定吃子
{
chessFlag[x][y] = currentRole;//开始点标志为自己的棋子
temp_x -=dir[i][0]; temp_y -=dir[i][1];//后退一步
while((temp_x != x)||(temp_y !=y)){ //只要没有回到开始的位置就执行
chessFlag[temp_x][temp_y] = currentRole;//标志为自己的棋子
temp_x -=dir[i][0]; temp_y -=dir[i][1];//继续后退一步
eatNum++;//累计
}
}
else //不吃子,只是判断这个位置能不能吃子
{
temp_x -=dir[i][0]; temp_y -=dir[i][1];//后退一步
while((temp_x != x)||(temp_y !=y)){ //只计算可以吃子的个数
temp_x -=dir[i][0]; temp_y -=dir[i][1];//继续后退一步
eatNum++;
}
}
break;//跳出循环
} //没有找到自己的棋子,就向前走一步
temp_x += dir[i][0]; temp_y += dir[i][1];//向前走一步
}
} //如果这个方向不能吃子,就换一个方向
temp_x = x; temp_y = y;
}
return eatNum; //返回能吃子的个数
}
加载窗体背景:
设计界面:Label, LCDNumber(digitCount=2), PushButton
谁先谁后显示:ComboBox9(白子先,黑子先),
初始化可以用上述方式实现,也可用下面的“方法”来实现:
void RoleInit(const QString whiteFilename, const QString blackFilename);
1)把界面初始化,分别实现。点击“人人对战”,设置谁先下。
void setRole(Chess::ChessType currentRole); //设置谁先下
Chess::ChessType currentRole;
2)把棋盘初始化
2.1 一开始默认情况下,棋盘是没有的。——注释相关语句
2.2 把chess.cpp中“初始化棋盘数据数组”的代码复制过来
int formChessData[8][8]; //窗体中的,跟棋盘中的不一样
void setChessInit(); //对棋盘初始化
运行代码Ctrl +R,结果显示如下:(LCD应显示2和2)
选择“白子先”,点击“人人对战”,如下:(LCD应显示2和2)
选择“黑子先”,点击“人人对战”,如下:(LCD应显示2和2)
绑定,处理信号
connect(myChess,SIGNAL(SignalSendChessData(int,int)),this,SLOT(doProcessChessData(int,int)));
上图说明:主窗体收到棋盘发送过来的信号
上图说明了吃子规则的实现
————————————————————分割线————————————————————
1. 角色切换:黑白子切换
2. 数据统计:白子多少个,黑子多少个
void ChessForm::RoleChange(){
if(currentRole == Chess::White){
currentRole = Chess::Black;
}
else{
currentRole = Chess::White;
}
//界面显示随变
if(currentRole == Chess::Black){
ui->label2->setVisible(true);
ui->label1->setVisible(false);
}
else{
ui->label1->setVisible(true);
ui->label2->setVisible(false);
}
}
结果如下:
接下来做数据统计,并显示在LCD上
void ChessForm::ChessShow(){
int blackCount=0, whiteCount=0;
for(int i=0; i<8; i++){
for(int j=0; j<8; j++){
if(formChessData[i][j] == Chess::White){
whiteCount++;
}
else if(formChessData[i][j] == Chess::Black){
blackCount++;
}
}
}
ui->lcdNum1->display(whiteCount);
ui->lcdNum2->display(blackCount);
}
注意:点击“人人对战”时,初始显示应为2和2
黑白棋子统计显示结果如下:
——————————分割线——————————分割线——————————
人机对战:Button转到槽(一开始,人先下白子)
enum PKType{PVP,PVC,NVN};
PKType currentPK;
机器下子:对空白处遍历,判断在哪里下子(LCD显示数目也要对)
后续:人下不了了就交给机器下,直到对方都下不了了,再根据黑白棋数目判断结果
弹出消息对话框告知用户。
等后面学了TCP/UDP后,来实现网络对战。