前言
- Task:用QT实现一个可视化项目。
- Skill:已经学的C++控制台程序设计。
本次“C++控制台贪吃蛇移植QT贪吃蛇”是在已经有了C++控制台版本的贪吃蛇代码的情况下,将其移植到QT平台,做成一个可视化的项目。告别黑框框,进入白框框!
控制台部分任务分析
控制台源码
声明:这份源码不是博主写的,是博主的同学写的。博主的工作只是移植!
声明:这份源码不是博主写的,是博主的同学写的。博主的工作只是移植!
声明:这份源码不是博主写的,是博主的同学写的。博主的工作只是移植!
源码放在下面
#include<iostream>
#include<windows.h>
#include<conio.h>
#include<ctime>
#include<vector>
#include<string>
using namespace std;
#define high 25
#define width 50
int snake[high+5][width+5]={0};
int food_x,food_y;
int movedirection;
int score,sum;
void gotoxy(int x, int y)
{
HANDLE h;
COORD c;
c.X = x; c.Y = y;
h = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(h, c);
}
void hide_cursor()
{
HANDLE h_GAME = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info;
GetConsoleCursorInfo(h_GAME, &cursor_info);
cursor_info.bVisible = false;
SetConsoleCursorInfo(h_GAME, &cursor_info);
}
void start()
{
for(int i=0;i<high;i++)
{
snake[i][0]=-1;
snake[i][width-1]=-1;
}
for(int i=0;i<width;i++)
{
snake[0][i]=-1;
snake[high-1][i]=-1;
}
snake[high/2][width/2]=1;
for(int i=1;i<=4;i++)
snake[high/2][width/2-i]=i+1;
movedirection=4;srand(time(NULL));
food_x=rand()%(high-5)+2;
food_y=rand()%(width-5)+2;
snake[food_x][food_y]=-2;
snake[high+2][2]=0;sum=0;
}
void movesnake()
{
int max=0;
int oldtail_i,oldtail_j;
int oldhead_i,oldhead_j;
for(int i=1;i<high-1;i++)
{
for(int j=1;j<width-1;j++)
{
if(snake[i][j]>0)
{
snake[i][j]++;
if(max<snake[i][j])
{
max=snake[i][j];
oldtail_i=i;oldtail_j=j;
}
if(snake[i][j]==2)
{
oldhead_i=i;oldhead_j=j;
}
}
}
}
int newhead_i,newhead_j;
switch (movedirection)
{
case 1:
newhead_i=oldhead_i-1;
newhead_j=oldhead_j;
break;
case 2:
newhead_i=oldhead_i+1;
newhead_j=oldhead_j;
break;
case 3:
newhead_i=oldhead_i;
newhead_j=oldhead_j-1;
break;
case 4:
newhead_i=oldhead_i;
newhead_j=oldhead_j+1;
break;
}
if(snake[newhead_i][newhead_j]==-2)
{
snake[food_x][food_y]=0;
srand(time(NULL));
food_x=rand()%(high-5)+2;
food_y=rand()%(width-5)+2;
snake[food_x][food_y]=-2;
sum++;
snake[high+2][2]+=sum*2;
}
else
{
snake[oldtail_i][oldtail_j]=0;
}
if(snake[newhead_i][newhead_j]>0||snake[newhead_i][newhead_j]==-1)
{
cout<<"游戏失败"<<endl;
exit(0);
}
else
{
snake[newhead_i][newhead_j]=1;
}
}
void show()
{
gotoxy(0,0);
for(int i=0;i<high+3;i++)
{
for(int j=0;j<width;j++)
{
if(snake[i][j]==0&&i<high)
cout<<" ";
else if(snake[i][j]==-1&&i<high)
cout<<"#";
else if(snake[i][j]==1&&i<high)
cout<<"@";
else if(snake[i][j]>1&&i<high)
cout<<"*";
else if(snake[i][j]==-2&&i<high)
cout<<"F";
if(i==high+2&&j==2)
cout<<snake[i][j];
}
printf("\n");
}
Sleep(50);
}
void input()
{
char put;
if(kbhit())
{
put=getch();
if(put=='a')
movedirection=3;
if(put=='d')
movedirection=4;
if(put=='w')
movedirection=1;
if(put=='s')
movedirection=2;
}
}
int main()
{
start();
while(true)
{
hide_cursor();
input();
show();
movesnake();
}
return 0;
}
源码分析
假设我刚拿到这个代码,我将以这种状态对这个代码进行功能、实现上的分析。
- 首先分析主函数的内容
int main()
{
start();
//猜测这个是一个初始化函数,因为只执行一次
while(true)
//无限循环,应该是一个刷新界面的东西,不然不会无限循环的。
{
hide_cursor();
//字面意思,隐藏光标
input();
//字面意思,输入
show();
//字面意思,画图输出
movesnake();
//字面意思,移动蛇
}
return 0;
}
//总结,命名很好懂
//而且,如果你只想移植这个程序,分析到这里就可以了。
接下来我们一个一个函数进行分析,按照顺序搞下来。
- start函数
void start()
//目前猜测它是一个初始化函数
{
for(int i=0;i<high;i++)
//high,看一下前面的定义,是一个宏定义。应该是地图高度
//#define high 25
{
snake[i][0]=-1;
snake[i][width-1]=-1;
//把每个坐标的0和width-1都搞成了-1,应该是一个边界条件
}
for(int i=0;i<width;i++)
{
snake[0][i]=-1;
snake[high-1][i]=-1;
}
//这个分析同上,应该是处理另一个边界
snake[high/2][width/2]=1;
//取一个中间数,然后把它搞成1,应该是蛇头?
for(int i=1;i<=4;i++)
snake[high/2][width/2-i]=i+1;
//然后把宽度的后4格置为i+1,应该是一种特殊的标志。或许是蛇身?
movedirection=4;
//字面意思,移动方向
srand(time(NULL));
food_x=rand()%(high-5)+2;
food_y=rand()%(width-5)+2;
snake[food_x][food_y]=-2;
//这边应该是确定了第一个食物的位置。
snake[high+2][2]=0;
sum=0;
//应该是两个初始需要统计的变量
//综合上述的分析,snake数组应该是一个地图一样的东西
}
- hide_cursor()与gotoxy()
void gotoxy(int x, int y)
{
HANDLE h;
COORD c;
c.X = x; c.Y = y;
h = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(h, c);
}
void hide_cursor()
{
HANDLE h_GAME = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info;
GetConsoleCursorInfo(h_GAME, &cursor_info);
cursor_info.bVisible = false;
SetConsoleCursorInfo(h_GAME, &cursor_info);
}
//控制台代码,隐藏光标和移动光标到指定位置
- input
void input()
//我们猜测是相应输入的函数
{
char put;
if(kbhit())
//相应键盘事件
{
put=getch();
//读取到输入的字符(而且没有返回值)
if(put=='a')
movedirection=3;
//3是左方向
if(put=='d')
movedirection=4;
//4是右方向
if(put=='w')
movedirection=1;
//1是上方向
if(put=='s')
movedirection=2;
//2是下方向
}
}
- show
void show()
{
gotoxy(0,0);
//转到开头
for(int i=0;i<high+3;i++)
{
for(int j=0;j<width;j++)
{
//枚举每个snake数组的位置
if(snake[i][j]==0&&i<high)
cout<<" ";
//应该是空白地图
else if(snake[i][j]==-1&&i<high)
cout<<"#";
//边界
else if(snake[i][j]==1&&i<high)
cout<<"@";
//蛇头
else if(snake[i][j]>1&&i<high)
cout<<"*";
//蛇身
else if(snake[i][j]==-2&&i<high)
cout<<"F";
//食物
if(i==high+2&&j==2)
cout<<snake[i][j];
//暂时不知道是啥,但它在单独位置输出了一个数字,应该是一个计分表之类的东西?
}
printf("\n");
//朴实无华的换行
}
Sleep(50);
//朴实无华的等待
}
- movesnake
void movesnake()
//移动蛇,算法核心部分
{
int max=0;
int oldtail_i,oldtail_j;
int oldhead_i,oldhead_j;
//命名很好懂
for(int i=1;i<high-1;i++)
{
for(int j=1;j<width-1;j++)
{
//枚举这个每一个格子,好像是边界内的每一个格子
if(snake[i][j]>0)
{
//是蛇的部分,因为之前初始化看出来了
snake[i][j]++;
//让每个位置都+1,暂时不懂啥意思
if(max<snake[i][j])
{
//这个应该是找蛇尾,因为一开始蛇尾对应的int值是最大的,那么所有的都+1之后它还是最大的。
max=snake[i][j];
//更新一下
oldtail_i=i;
oldtail_j=j;
//旧蛇尾
}
if(snake[i][j]==2)
{
//这波应该是默认所有蛇头都是1了
oldhead_i=i;
oldhead_j=j;
}
}
}
}
int newhead_i,newhead_j;
//字面意思,新头
switch (movedirection)
{
case 1:
newhead_i=oldhead_i-1;
newhead_j=oldhead_j;
break;
case 2:
newhead_i=oldhead_i+1;
newhead_j=oldhead_j;
break;
case 3:
newhead_i=oldhead_i;
newhead_j=oldhead_j-1;
break;
case 4:
newhead_i=oldhead_i;
newhead_j=oldhead_j+1;
break;
}
//按照之前说的那个规则更新一下蛇头的信息
if(snake[newhead_i][newhead_j]==-2)
{
snake[food_x][food_y]=0;
srand(time(NULL));
food_x=rand()%(high-5)+2;
food_y=rand()%(width-5)+2;
snake[food_x][food_y]=-2;
sum++;
//这波应该是sum统计吃到的食物
snake[high+2][2]+=sum*2;
//应该是计分板吧
}
//这波是吃到食物了,但是我觉得srand应该去了。
else
{
snake[oldtail_i][oldtail_j]=0;
}
//不然就把旧的尾巴去了,吃到的话不去。
if(snake[newhead_i][newhead_j]>0||snake[newhead_i][newhead_j]==-1)
{
cout<<"游戏失败"<<endl;
exit(0);
}
//判定一波,不能撞界限和自己
else
{
snake[newhead_i][newhead_j]=1;
}
//更新一下头
}
QT部分分析
任务分析
- 显示部分,也就是show的部分。我们应该把它换成QT里面的定点输出。
- 键盘输入部分,也就是input的部分。应该换成QT的相应键盘函数。
- 计时器部分,考虑使用QT的计时器来完成这个工作。
组件挑选
-
显示部分
我们考虑使用QPainter来实现这个定点输出的问题,但是这样会出现不对应的问题。即QT中的xy与控制台并不是对应关系。是关于y=x对称的关系。
-
键盘输入部分
考虑使用Keypress函数,重写这个虚函数。就可以在每次按下按键的时候相应一次操作。完成input这个函数的功能。
-
计时器部分
考虑使用QTimer,然后设置一个计时时间间隔。这样每次到时间间隔之后将其与一个槽函数update关联。能够让QT的系统帮我们优化一波显示效果。
QT源码
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QPainter>
#include <QTime>
#include <QTimer>
#include <QString>
#include <string>
#include <QMessageBox>
#include <QKeyEvent>
#define high 25
#define width 50
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
void paintEvent(QPaintEvent *);
QTimer *ctime;
int snake[high+5][width+5];
int food_x,food_y;
int movedirection;
int score,sum,x;
void start();
void ready();
void keyPressEvent(QKeyEvent *event);
public slots:
void move();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
w.ready();
return a.exec();
}
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
start();
this->setFocusPolicy(Qt::StrongFocus);
ctime = new QTimer(this);
}
void MainWindow::ready(){
QMessageBox::about(this,"How to play","Press Space to start");
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::paintEvent(QPaintEvent *)
{
QPainter painter(this);
QFont font;
font.setPointSize(10);
painter.setFont(font);
for(int i=0;i<high+3;i++)
{
for(int j=0;j<width;j++)
{
if(snake[i][j]==-1&&i<high)
painter.drawText(10+10*i, 10+10*j, "#");
else if(snake[i][j]==1&&i<high)
painter.drawText(10+10*i, 10+10*j, "@");
else if(snake[i][j]>1&&i<high)
painter.drawText(10+10*i, 10+10*j, "*");
else if(snake[i][j]==-2&&i<high)
painter.drawText(10+10*i, 10+10*j, "F");
if(i==high+2&&j==2)
painter.drawText(10+10*i, 10+10*j, QString::number(snake[i][j]));
}
}
}
void MainWindow::start(){
for(int i=0;i<high;i++)
{
snake[i][0]=-1;
snake[i][width-1]=-1;
}
for(int i=0;i<width;i++)
{
snake[0][i]=-1;
snake[high-1][i]=-1;
}
snake[high/2][width/2]=1;
for(int i=1;i<=4;i++)
snake[high/2][width/2-i]=i+1;
movedirection=4;srand(time(NULL));
food_x=rand()%(high-5)+2;
food_y=rand()%(width-5)+2;
snake[food_x][food_y]=-2;
snake[high+2][2]=0;sum=0;
}
void MainWindow::move(){
int max=0;int oldtail_i,oldtail_j;
int oldhead_i,oldhead_j;
for(int i=1;i<high-1;i++)
{
for(int j=1;j<width-1;j++)
{
if(snake[i][j]>0)
{
snake[i][j]++;
if(max<snake[i][j])
{
max=snake[i][j];
oldtail_i=i;oldtail_j=j;
}
if(snake[i][j]==2)
{
oldhead_i=i;oldhead_j=j;
}
}
}
}
int newhead_i,newhead_j;
switch (movedirection)
{
case 1:newhead_i=oldhead_i-1;
newhead_j=oldhead_j;break;
case 2:newhead_i=oldhead_i+1;
newhead_j=oldhead_j;break;
case 3:newhead_i=oldhead_i;
newhead_j=oldhead_j-1;break;
case 4:newhead_i=oldhead_i;
newhead_j=oldhead_j+1;break;
}
if(snake[newhead_i][newhead_j]==-2)
{
snake[food_x][food_y]=0;
srand(time(NULL));
food_x=rand()%(high-5)+2;
food_y=rand()%(width-5)+2;
snake[food_x][food_y]=-2;
sum++;
snake[high+2][2]+=sum*2;
}
else
{
snake[oldtail_i][oldtail_j]=0;
}
if(snake[newhead_i][newhead_j]>0||snake[newhead_i][newhead_j]==-1)
{
QMessageBox::about(this, "See", "Game Over!");
exit(0);
}
else
{
snake[newhead_i][newhead_j]=1;
}
}
void MainWindow::keyPressEvent(QKeyEvent *event){
if(x == 0 && event->key() == Qt::Key_Space){
x = 1;
connect(ctime, SIGNAL(timeout()), this, SLOT(move()));
connect(ctime, SIGNAL(timeout()), this, SLOT(update()));
ctime->start(100);
}
switch (event->key()) {
case Qt::Key_W:
if(movedirection != 4)
movedirection=3;
break;
case Qt::Key_S:
if(movedirection != 3)
movedirection=4;
break;
case Qt::Key_A:
if(movedirection != 2)
movedirection=1;
break;
case Qt::Key_D:
if(movedirection != 1)
movedirection=2;
break;
default:
movedirection=movedirection;
break;
}
}
分部解析
-
算法核心部分move
没有改动,只是把它计时器ctime连接起来,使得每次达到计时周期的时候都能执行一次move函数,也就是更新一次坐标信息。
同样,在达到更新周期的时候,update刷新页面。
-
相应键盘操作
重写了虚函数keyPressEvent,然后利用其事件对应的key,来判断是按下了哪个按键,从而完成之前的kbhit+getch的操作。
-
画出指定的图形
利用了QPainter,和重写了虚函数paintEvent。利用每次update都会调用一次paintEvent来实现每次move后都paint一次。
-
计时器
利用QTimer,设置计时周期后,将周期到达时发出的timeout信号函数连接到move和update上,使得每次到达周期都能进行一次更新。
简单用法
-
keyPressEvent
利用其参数QKeyEvent *event,其中存储了按键的信息。我们利用
event->key()
就可以得到其键值。与QT基层的按键进行比较,即可完成之前的getch然后进行比较的工作。不过还要看第五条才能正常工作。 -
QPainter
QPainter打印字符(串)的用法是
QPainter::drawText(x,y,字符串)
,其中xy都是需要打印位置的左下角(如果看作是矩形的话)。QFont
可以设置字体,通过设置QFont的一些参数,然后QPainter :: setFont(font)
就可以把所设置的字体应用到对应的QPainter里面。关于QPainter画其他东西可以参考:Qt 之图形(QPainter 的基本绘图)
-
QMessageBox
这个是一个小弹窗,为了美化使用体验而制作的。最简单的用法如下:
QMessageBox::about(parent(经常用this就行),title_name,message)
其中title_name是说框框左上角的那个名字,message就是框中间的内容。 -
connect
是一个手动连接信号和槽的函数,最简单用法参考如下:
connect(name0,SIGNAL(name1()),name2,SLOT(name3()))
含义是当name0.name1()信号被发出的时候,name2.name3()这个函数就会被系统调用。具体实现原理是QT给做的。
-
关于键盘响应
只有在焦点的窗口才能接受键盘事件。而焦点设置,默认每个窗口都不是焦点。如果你想能够通过Tab和点击来将焦点确定在该窗口(就是说,点一下它,它就是焦点。Tab一下它,它就是焦点),那么你需要设置一个强聚焦。
QMainWindow::this->setFocusPolicy(Qt::StrongFocus)
。当然还有单独使用某种聚焦方式的,可以查阅相关参数(对应Qt::StrongFocus
)的部分。