基于C++的植物生长模拟系统(Qt6实现)
摘要
这是软件课布置的一个模拟植物向阳生长的小作业,附上了源码并记录了实现过程。
作业要求
现实中植物都有向着阳光生长的特性。 请在软件中模拟这一特性。
- 系统绘图区底部有一个植物幼苗在慢慢长出;
- 系统绘图区上部某一位置放置一个太阳
- 该植物向上生长时,不断向上长出越来越多的枝条,这些枝条均偏向太阳位置生长
- 用户可在生长过程中,改变太阳的位置。 则新长出的枝条,也会偏向太阳新的位置生成;
实现思路
该系统的主要功能就是模拟植物的向阳生长,主要实现思路:
- 利用鼠标移动事件来实现对太阳的移动。
- 利用QVector(动态数组)实现新增分枝,存储分枝(位置信息,向阳角度信息等)。
- 计时器定时触发界面重绘,重绘前通过增加植物高度,新增分枝来实现动画效果。
界面预览
源码获取
网盘链接:https://pan.baidu.com/s/1biwJ7cWR5GjL5wi__C6GTA?pwd=yfks
提取码:yfks
代码详解
主窗口类Widget,树枝类Branch,太阳类Sun
主窗口类属性和方法声明
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void paintbk();
//angle 角度 x,y 矩形右下角坐标 ,w,h 矩形宽高
void paintRect(int angle,int x ,int y,int w,int h);
//绘制太阳
void paintSun(Sun mysun);
//绘制树干
void paintTrunk();
//绘制一级树枝
void paintBranch(int brangle,int pos,int brheight);
//绘制二级树枝(树枝上的小树枝)
void paintBranch2(int brangle,int pos,int brheight);
private slots:
//“开始模拟”按钮,“暂停模拟”按钮,“重置”按钮的槽函数
void on_btnstar_clicked();
void on_btnwait_clicked();
void on_btnreset_clicked();
private:
Ui::Widget *ui;
Sun *sun;//声明太阳类对象指针,该类封装了太阳的三个属性,中心点横纵坐标和半径。
notice notice; //提示窗口类 触发到边界条件时进行弹窗提示
//以下一些关于树的信息可以封装成一个树类,会更清晰一些
int rootx,rooty;//树的根部位置信息
int trheight;//树的高度 tree height的缩写
QVector<Branch> mybranch;//存储一级树枝
QVector<Branch> mybranch2;//存储二级树枝(树枝上的小树枝)
int counter;//计数器,用于记录计时器触发次数
//重写绘图事件的声明,对页面绘图事件进行重写。基本上大多数绘图事件都要通过重写这个方法来实现
void paintEvent(QPaintEvent *event);
//重写鼠标移动事件的声明,这些内置的方法在重写时,不要随便改变函数名称和参数,返回值。
void mouseMoveEvent(QMouseEvent *event);
//声明定时器
QTimer *tim;
public slots:
void onTimeOut();
};
主窗口类的构造函数
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//初始化太阳(横坐标,纵坐标,半径)
sun = new Sun(100,174,50);
//初始化树根部坐标
rootx=375;
rooty=666;
//初始化树的高度trheight(treeheight)
trheight=45;
//计数器,用于记录定时器触发的次数。
counter=0;
//初始化计时器,触发间隔为0.5s
tim = new QTimer();
tim->setInterval(500);
//计时器信号和对应的自定义槽函数,在onTimeOut中定义定时信号发生后触发的行为
connect(tim,&QTimer::timeout,this,&Widget::onTimeOut);
}
太阳类Sun及构造函数
class Sun
{
public:
Sun();
sun(int x, int y, int radius);
int sun_x,sun_y,sun_radius;
};
//定义有参构造函数,sun_x和sun_y为太阳的中心点坐标,radius为半径。
sun::sun(int x, int y, int radius)
{
sun_x=x;
sun_y=y;
sun_radius=radius;
}
树枝类branch
class Branch
{
public:
Branch();
Branch(int angle,int h,int pos)
{
brangle=angle;
brheight=h;
brpos=pos;
}
public:
//br是branch的缩写
int brangle;//存储树枝 向阳生长的角度
int brheight; //存储树枝的长度
int brpos;//存储树枝根部在树干上的位置
};
类函数的功能详解
绘制倾斜矩形
因为Qt没有内置的绘制倾斜矩形的功能,于是实现了一个,该函数可以在指定位置生成一个指定倾斜度的矩形。原理就是在绘制前旋转一下坐标系。
右图为正常的坐标系,左图为该坐标系逆时针旋转30度,绘制矩形的结果。我们定义x,y轴正方向(箭头方向)为第一象限,按理说我们旋转坐标系后(左图)的矩形应该也在第一象限生成。但是我们设置了宽和高的负值,所以就实现左图的情况。
关于绘制倾斜矩形的方法,可以详细了解一些设置坐标原点translate(),旋转坐标系rotate()的方法。
//绘制"倾斜"矩形的方法 (水平角度,矩形右下角的x,y坐标,矩形的宽高w,h)
void Widget::paintRect(int angle,int x ,int y,int w,int h)
{
QPainter painter(this);
painter.setPen(QPen(Qt::green,1));
painter.translate(x, y);//把x,y作为坐标系原点
painter.rotate(angle);//从目标角度逆时针旋转到angle度,
QRect rect;
//注意现在虽然是从(0,0)点绘制,但实际会在translate的参数指定的(x,y)位置画出
rect.setX(0);
rect.setY(0);
//设置矩形的宽高,这里用了负值,是经过多次测试得出的。
//这样矩形在第一象限位置和水平的角度为0到90度,在第二象限的方向和水平的角度为0到-90度
//既可以通过角度来判断矩形在第一象限(树干右边)还是第二象限(树干左边)
rect.setHeight(-h);
rect.setWidth(-w);
painter.fillRect(rect, QBrush(QColorConstants::Green));//绘制矩形
painter.resetTransform();//绘图结束,重置坐标轴
}
绘制背景,绘制树干,绘制树枝
就是简单画个750*650矩形,矩形的左上角坐标为20,20.
//绘制背景bk background的缩写
void Widget::paintbk()
{
//声明一个画家对象
QPainter painter(this);
//给画家设置画笔(颜色,粗细)
painter.setPen(QPen(Qt::black,4));//设置画笔形式
//在20,20位置上画一个宽为750 高为650的矩形作为背景
painter.drawRect(20,20,750,650);//画矩形
}
//绘制树干
void Widget::paintTrunk()
{
//旋转角度为0度,(该度数为矩形和垂直线的夹角) , 右下角坐标为rootx,rooty,树高为trheight。
paintRect(0,rootx,rooty,5,trheight);
}
//绘制树枝 brangle树枝的角度,pos树枝在qt正常坐标系下的纵坐标,brheight树枝长度
void Widget::paintBranch(int brangle,int pos,int brheight)
{
//树枝的横坐标和树干是相等的,因为是在树干上某个高度的点上进行一级树枝的绘制
//参数:树枝角度,树枝生成的位置横坐标rootx,纵坐标pos,树枝宽度 10 树枝高度 brheight
paintRect(brangle,rootx,pos,10,brheight);
}
绘制二级树枝
在一级树枝的brheight等于某个值的时候进行二级树枝的生成,二级树枝的生成点通过一级树枝生成点的坐标和brheight,倾斜角度进行计算,具体原理如图。
在二级树枝生成后,一级树枝还会继续随着时间变长,所以图像中的brheight只是二级树枝生成时,一级树枝的长度。
void Widget::paintBranch2(int brangle, int pos, int brheight)
{
qreal VAR_PI=180/M_PI; //这是一个角度转弧度的一个转换算子,角度/该算子=弧度,下面使用三角函数时会用到
float x;//保存二级树枝生成点的水平偏移量
int bias;//角度偏差,设置二级树枝和一级树枝的角度差值
//判断一级树枝在左侧还是右侧,若一级树枝在左侧,则二级树枝的生产点横坐标向左偏移,角度向逆时针偏转一些。
if(brangle<0){//如果一级树枝角度小于0,说明一级树枝在树干左边。
x=brheight*qSin(qAbs(brangle)/VAR_PI);//Sin函数求二级树枝生成点横坐标
bias=20;
}
else{//如果一级树枝角度大于0,说明一级树枝在树干右边。
x=-brheight*qSin(qAbs(brangle)/VAR_PI);
bias=-20;
}
float y=brheight*qCos(qAbs(brangle)/VAR_PI);
qDebug()<<"brheight,brx,bry:"<<brheight<<","<<x<<","<<y;
//绘制二级树枝 参数:
//带有偏差的二级树枝角度:brangle-bias 二级树枝生产点横坐标:rootx-x
//二级树枝生成点纵坐标:pos-y 二级树枝宽度:3 二级树枝长度:brheight
paintRect(brangle-bias,rootx-x,pos-y,3,brheight);
}
实现鼠标拖动太阳
//鼠标移动事件 用于拖动太阳
void Widget::mouseMoveEvent(QMouseEvent *event)
{
//为了节省内存,qt中鼠标移动事件只有在按住鼠标左键时候才会记录鼠标位置信息
//记录鼠标按下时的坐标
int mouse_x = event->pos().rx();
int mouse_y = event->pos().ry();
//判断鼠标按压时是否处于太阳内部时
if (qPow(mouse_x-sun->sun_x,2)+qPow(mouse_y-sun->sun_y,2)<qPow(sun->sun_radius,2))
{
//我们需要把太阳的中心点坐标设置在鼠标按压的位置来实现拖动,但是在此之前进行一次越界判断,判断太阳是否会因此出界。
if( //如果越界了,什么都不做
(mouse_x-sun->sun_radius<20)
||(mouse_y-sun->sun_radius<20)
||(mouse_x+sun->sun_radius>(750+20))
||(mouse_y+sun->sun_radius>300))//禁止太阳拖到界面下半部分
{
//什么都不做
}
else //没越界,将鼠标坐标赋值给太阳坐标并update重绘界面
{sun->sun_x=event->pos().rx();
sun->sun_y=event->pos().ry();
update();}
}
}
通过计时器更新树干,树枝等信息,实现植物生长的动画效果
这个比较重要,代码详细注释了。
//计时器的槽函数,每0.5s触发一次。间隔可定义,本例为0.5s.
void Widget::onTimeOut()
{
//先判断树是否已经长到400 就停止及时,提示模拟完成。
if(trheight>=400)
{
tim->stop();//定时停止
notice.show();//提示界面弹出
}
//每次计时,计数器+1
counter+=1;
//每次计时,树长高5个单位
trheight+=5;
//设置时刻的太阳与树根的夹角,太阳是动态的,这个夹角在我们每次通过计时器重绘时都要计算一下
int angle=qAtan2(rooty-sun->sun_y,rootx-sun->sun_x)*(180.0/3.1415926)-90;
//计数器为1或者10的倍数时,生成一个一级树枝
if(counter%10==0 || counter==1)
{
// angle:角度 10:初始树枝长度 rooty-counter*5-30:一级树枝生产点纵坐标
//第一次生成树枝时,rooty-1*5-35 说明在树根的上方35的距离处生成树枝。想想第二次生成树枝的时候,是在树根上方什么位置生成树枝?
//生成树枝,即把生成一个树枝对象并加入到一级树枝数组当中,该对象包括了树枝的一些信息(树枝生成时候的向阳角度,初始长度,纵坐标)
mybranch.append(Branch(angle,10,rooty-30-counter*5));
}
//遍历一级树枝数组
QVector<Branch>::iterator iter;
for (iter=mybranch.begin();iter!=mybranch.end();iter++)
{
//如果一级树枝长度小于等于75,让他增加4的长度
if((*iter).brheight<=75)
(*iter).brheight+=4;
//如果一级树枝长度等于50,生成一个二级树枝对象,并添加到二级树枝数组当中。
if((*iter).brheight==50)
{
//二级数枝对象存储了一级树枝的向阳角度,初始长度:10,一级树枝的纵坐标信息,根据这些信息可以算出二级树枝生成点的位置
mybranch2.append(Branch((*iter).brangle,10,(*iter).brpos));
}
}
//遍历二级树枝数组
for (iter=mybranch2.begin();iter!=mybranch2.end();iter++)
{
//如果二级树枝长度小于等于25,让他增加4的长度
if((*iter).brheight<=25)
(*iter).brheight+=4;
}
//数据更新完成,重新绘制界面,实现动画效果
update();
}
界面按钮的槽函数
//开始按钮,点击则开始计时
void Widget::on_btnstar_clicked()
{
tim->start();
}
//暂停按钮,点击则暂停计时
void Widget::on_btnwait_clicked()
{
tim->stop();
}
//重置按钮,点击则进行数据还原成最初的状态
void Widget::on_btnreset_clicked()
{
//清空一级树枝和二级树枝
mybranch.clear();
mybranch2.clear();
//计时器归零
counter=0;
//树干还原成最初的高度
trheight=45;
//太阳位置还原为最初的位置
sun->sun_x=100;
sun->sun_y=174;
//重新绘图
update();
//暂停计时,等待手动点击开始按钮开始
tim->stop();
}