基于C++的植物生长模拟系统(Qt6实现)

本项目使用Qt6实现了一个模拟植物向阳生长的系统。通过鼠标移动太阳位置,植物会根据太阳的位置调整生长方向。系统包含太阳、树干和树枝等元素,并通过计时器实现植物生长的动画效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

摘要

这是软件课布置的一个模拟植物向阳生长的小作业,附上了源码并记录了实现过程。

作业要求

现实中植物都有向着阳光生长的特性。 请在软件中模拟这一特性。

  1. 系统绘图区底部有一个植物幼苗在慢慢长出;
  2. 系统绘图区上部某一位置放置一个太阳
  3. 该植物向上生长时,不断向上长出越来越多的枝条,这些枝条均偏向太阳位置生长
  4. 用户可在生长过程中,改变太阳的位置。 则新长出的枝条,也会偏向太阳新的位置生成;

实现思路

该系统的主要功能就是模拟植物的向阳生长,主要实现思路:

  1. 利用鼠标移动事件来实现对太阳的移动。
  2. 利用QVector(动态数组)实现新增分枝,存储分枝(位置信息,向阳角度信息等)。
  3. 计时器定时触发界面重绘,重绘前通过增加植物高度,新增分枝来实现动画效果。

界面预览

在这里插入图片描述

源码获取

网盘链接: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();
}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值