最近在b站上学了下GUI开发 (实在不想看黑框了 ),做出来一个翻金币的小游戏。虽然写完了但是不会打包,一直也没有给别人帮我debug。在我会打包之前现在这里复盘一下程序是怎么写出来的。
说实话,Qt的学习确实让我有点了解了程序的运行机制。最简单的就是connect函数只走一遍,不是发一遍信号走一遍。而且不是发信号的时候才走这句话。
资源添加
开发一个程序不可避免的需要图片和音效。虽然我现在还没加音效 由于我用的visual studio开发,vs自动给我创建了一个.qrc文件,资源全放在这里面。
添加的文件的话,你需要先把你的资源先打包一下放到工程文件夹下面。下面的prefix是添加一个前缀。
把文件放qrc文件的目的是可以使用相对路径,对于需要移植的程序来说非常重要。
像我这个引用的时候(":/source/name…") 如果有前缀在前面加前缀即可。
程序主窗口
thefinalgame.cpp
主窗口我做的比较粗糙,1280*720的界面就放了一个“开机按钮”,简单的设置了一下标题栏,起名“Coin Turning”。 菜单栏只放了一个关闭按钮。
程序的关闭并连接警告对话框
这个程序一共有两个正常途径将程序关闭
第一个是菜单栏上的关闭,第二个是直接点击右上角叉号。
简单说下菜单栏上关闭,他是QAction 类的,点击它会产生triggered信号,用connect连接一个lambda表达式关闭窗口即可。
我们关闭窗口会产生关闭事件,这个在主窗口父类是一个virual函数,说明我们可以在主窗口类重写。
void TheFinalGame::closeEvent(QCloseEvent* ce)
{
//生成警告对话框
int res = QMessageBox::question(this , "Warning" , "Sure to quit?" , QMessageBox::Yes | QMessageBox::No , QMessageBox::Yes);
//如果点的是确定 退出
if (res == QMessageBox::Yes) this->close();
//如果不是 不理会
else if (res == 65536 || res == QMessageBox::No) ce->ignore();
}
载入背景图片
用paintevent载入背景图片
用QPixmap作为载体
void TheFinalGame::paintEvent(QPaintEvent*)
{
QPainter painter(this);
QPixmap pix;
pix.load(":/source/ground.jpeg");
painter.drawPixmap(QRect(QPoint(0 , 23) , QPoint(1280 , 720)) , pix);
/*
绘制背景图片
先在Qpixmap类中加载一张图片
用画家在选定的矩形区域画出即可
*/
}
创建按钮类(开始 返回 关卡选择)
mypushbutton.h/mypushbutton.cpp
想做一个按一下就能变色(其实是换图片)的按钮,所有构造函数改了一下。
传入两张图片的路径,不按显示其中一个,按了显示另一个。
myPushButton::myPushButton(QString NormalPath , QString PressPath = "")
//第二个是按下时显示的图片 默认是空 因为有的按钮不需要变色
{
this->NormalImgPath = NormalPath; //不按按钮的路径
this->PressImgPath = PressPath; //按了按纽的路径
QPixmap pix;
pix.load(NormalImgPath);
this->setFixedSize(QSize(pix.width() , pix.height())); //设置按钮大小
this->setStyleSheet("QPushButton{border:0px;}"); //去掉图片周围白色
this->setIcon(QIcon(pix)); //设置按钮上的图标
this->setIconSize(QSize(pix.width() , pix.height())); //设置按钮中图片的大小
}
按钮动画
实现点一下按钮,按钮上下跳动的动画。
在按钮类里定义两个函数,一个实现按钮往下,另一个实现往上
void myPushButton::zoom1()
{
QPropertyAnimation* anime = new QPropertyAnimation(this , "geometry");
//设置事件间隔
anime->setDuration(200);
//起始位置
anime->setStartValue(QRect(this->x() , this->y() , this->width() , this->height()));
//结束位置
anime->setEndValue(QRect(this->x() , this->y() + 10 , this->width() , this->height()));
//设置弹跳曲线
anime->setEasingCurve(QEasingCurve::OutBounce);
anime->start();
}
void myPushButton::zoom2()
{
QPropertyAnimation* anime = new QPropertyAnimation(this , "geometry");
//设置事件间隔
anime->setDuration(200);
//起始位置
anime->setStartValue(QRect(this->x() , this->y() + 10 , this->width() , this->height()));
//结束位置
anime->setEndValue(QRect(this->x() , this->y() , this->width() , this->height()));
//设置弹跳曲线
anime->setEasingCurve(QEasingCurve::OutBounce);
anime->start();
}
用connect函数连接lambda表达式,点击就启动
加入了暂停300ms 防止执行过快,观感不好
connect(btn_start , &QPushButton::clicked , [=] () {
btn_start->zoom1();
btn_start->zoom2();
QTimer::singleShot(300 , this , [=] () {
this->hide();
chooselevel->show();
});
});
选择关卡窗口
chooselevel.h/chooselevel.cpp
设置窗口各种参数和主窗口是同样的方式
设置返回按钮
返回按钮比开始按钮只多了一个功能,就是按下变色(实际上是图片切换)。只需要在鼠标按下事件里加一点东西。
void myPushButton::mousePressEvent(QMouseEvent* me)
{
if(this->PressImgPath != "")
this->setIcon(QIcon(this->PressImgPath));
//通过pressimgpath判断这个按钮是不是需要变色
QPushButton::mousePressEvent(me);
//如果不拦截这个信号 交给父亲处理
}
还有一个问题 就是按了返回之后如何返回上一个场景。我们在选关窗口里并没有上一个窗口的对象。但是我们在上一个窗口有这个窗口的对象。
我们可以在chooselevel.h里手动实现一个信号
因为在主窗口里我们有选关窗口对象,我们可以接收到chooselevel发出的信号。
thefinalgame.cpp
connect(chooselevel , &ChooseLevel::back() , [=](){
this->show();
})
back()
是实现在chooselevel.h
中的信号
设置选关按钮
让我对connect了解更深的地方
for(int i = 1 ; i <= 4 ; i ++)
for (int j = 1; j <= 5; j ++)
{
myPushButton* btn_level;
btn_level = new myPushButton(":source/Unchoose.png");
btn_level->setParent(this);
btn_level->setIconSize(QSize(0.8 * btn_level->size()));
btn_level->move(QPoint((j - 1) * 0.18 * this->width() , (i - 1) * 0.25 * this->height()));
Levels* levels = new Levels(j + (i - 1) * 5);
connect(btn_level , &QPushButton::clicked , [=] () {
btn_level->zoom1();
btn_level->zoom2();
QTimer::singleShot(300 , this , [=] () {
this->hide();
levels->show();
qDebug() << levels->number;
});
});
//这里很多人可能会问 申请这么多,只用一个指针存。调用怎么办
//其实这里是申请之后立马connect了,只是用一下这个地址,这个地址其实就用不到了,没必要再用新的指针存下一个了。
QLabel* label_level = new QLabel(btn_level);
label_level->setText(QString().number((i - 1) * 5 + j));
btn_level->move(QPoint((j - 1) * 0.18 * this->width() , (i - 1) * 0.25 * this->height()));
label_level->setFixedSize(QSize(btn_level->size()));
label_level->setAlignment(Qt::AlignCenter);
QFont font_level;
font_level.setPointSize(35);
label_level->setFont(QFont(font_level));
label_level->setStyleSheet("color:red;");
//这里是label 显示关卡号的,注意父亲要设置为按钮,如果设置为窗体会导致按钮按不了
connect(levels , &Levels::back , [=] () {
this->show();
});
//下一个窗口返回这里需要的connect
}
关卡窗口
选关之后就正式进关了
我们需要创建金币类,一关是4*4的金币,难点是如何翻动附近的金币。
这里我没跟着教程做,凭借算法竞赛的经验,自己口胡出来了一个方法。
for(int i = 1 ; i <= 4 ; i ++)
for (int j = 1; j <= 4; j ++)
{
QLabel* label = new QLabel(this);
QPixmap pix(":/source/coinbackground.png");
pix = pix.scaled(QSize(0.8 * pix.size()));
label->setGeometry(0 , 0 , pix.width() , pix.height());
label->setPixmap(pix);
label->move(250 + pix.width() * (j - 1) , 53 + pix.height() * (i - 1));
//贴图部分
myCoin* coin = new myCoin(lev[this->number - 1][i - 1][j - 1] , this);
v[this->number - 1].push_back(coin);
coin->move(230 + pix.width() * (j - 1) , 30 + pix.height() * (i - 1));
//创建金币类,每关一个vector, 16个金币地址按顺序存进vector
}
int f = 0;
for (QVector<myCoin*>::iterator it = v[this->number - 1].begin(); it != v[this->number - 1].end(); it ++)
{
f ++;
connect(*it , &QPushButton::clicked , [=] () {
(*it)->turn();
if (f > 4) v[this->number - 1][f - 4 - 1]->turn();
if (f <= 12) v[this->number - 1][f + 4 - 1]->turn();
if (f % 4 != 1) v[this->number - 1][f - 1 - 1]->turn();
if (f % 4 != 0) v[this->number - 1][f + 1 - 1]->turn();
isWin();
});
}
金币是线性存储的,怎么应用的4*4行列里呢
当前数-4就是上一行 注意判断是不是第一行
同理+4是下一行 注意判断是不是最后一行
前后的话好判断 注意判断是不是在头和尾 如果%4 = 0 是最后一列 %4 = 1 是前一列