项目一 计算器

目录

学习目标

1.1准备工作

1.1.1搭建QT开发环境

1.2用户界面UI设计

1.2.1显示模块

1.2.2按键模块

1.3功能实现

1.3.1输入输出反馈

1.3.2退格与清除功能

1.3.3浏览历史记录与历史记录清除功能

1.3.4后缀表达式的转化

1.3.4.1合法性判断

1.3.4.2中缀表达式转为后缀表达式

1.3.5后缀表达式计算

1.3.6答案格式化

1.3.7输入自定义

1.3.8浏览历史功能改进


学习目标:

  • 通过本次实习熟悉基于QT的GUI开发流程
  • 能更加熟练的运用C++语言编程,同时加深对数据结构等知识的理解
  • 了解软件工程的基本知识

1.1准备工作

1.1.1搭建QT开发环境

        前往官网下载QT5.14.2版本,安装选项中选中MINGW64等编译工具,待安装完成后进入初始界面。在Projects一栏中选择新建一个项目,选择Qt Widgets Application模板,输入恰当的文件名和文件地址,选择默认的qmake编译系统,选择基类QWidget,命名子类名为calc_widget,选择中文语言,选择可行的编译套件,默认不添加新的子项目,点击完成后便创建了第一个项目。


1.2用户界面UI设计

1.2.1显示模块

        在项目的Forms文件夹下,找到calc_widget.ui文件,双击进入设计界面。在左侧菜单栏中可以找到Line Edit对象。选中该选项并将其拖入设计面板,即可创建第一个可编辑的行文本框。在界面右下角可以找到与之对应的属性

1.2.2按键模块

       在左侧菜单栏中的可以找到Push Button对象,将其拖入设计面板中即可实现按钮的创建,在我计划的计算器设计中,需要有0~9数字键、小数点键、加减乘除键、清除键、历史记录键、清除历史记录键、退格键以及等号键。将若干模块进行简单排序后可以选择栅格工具将其进行网状排列,以使得整个界面看起来整齐有序。

         如上图,至此计算器界面最基本的几个元素已经实现。进入调试状态下的计算器呈现如下:


1.3功能实现

1.3.1输入输出反馈

        为实现在点击0~9数字键、小数点键、百分号键、运算符键时能够在显示界面输出相对应的字符,需要对按键和显示界面进行关联。对每个按键,针对进行点击建立一个槽函数,以实现在一次点击下产生的结果。以数字键1为例,在头文件calc_widget.h中的私有插槽中声明它的槽函数。

private slots:
    void on_one_button_clicked();

同时建立QString类私有成员experssion,以表示当前界面中的字符串。

private:
    Ui::calc_Widget *ui;
    QString experssion;  //表示当前显示界面中的字符串

在calc_widget.cpp文件中补充其内容,在每次点击后,显示界面中的字符串接上字符“1”,同时将其显示在界面中。

void calc_Widget::on_one_button_clicked()
{
    experssion+="1";
    ui->screen_lineedit->setText(experssion);
}

其他类似按键以此类推,最终呈现结果如图

 可以实现基本的字符输入输出反馈。

1.3.2退格与清除功能

        退格操作需要实现删除当前字符串中最后一个字符的操作。基本流程类同上例,只是槽函数的功能方面有些许变化。

在私有插槽中声明

private slots:
    void on_delete_button_clicked(); 
    void on_clear_button_clicked();

定义槽函数

void calc_Widget::on_delete_button_clicked()
{
    experssion.chop(1);                       //每次点击都删除字符串最后一位
    ui->screen_lineedit->setText(experssion);
}
void calc_Widget::on_clear_button_clicked()
{
    experssion.clear();                       //清除字符串
    ui->screen_lineedit->clear();
}

1.3.3浏览历史记录与历史记录清除功能

        基本流程同上,历史记录用栈结构来存储,在浏览时从栈底开始浏览。

定义私有成员history_ans

private:
    Ui::calc_Widget *ui;
    QString experssion;  //表示当前显示界面中的字符串
    QStack<QString> history_ans;  //存储历史记录

定义槽函数

void calc_Widget::on_history_button_clicked()
{
    QString h_ans;
    for(auto ans:history_ans)
    {
        h_ans+=ans;
        h_ans+="\t";    //每条记录之间隔开
    }
    ui->screen_lineedit->setText(h_ans);
}
void calc_Widget::on_clearall_button_clicked()
{
    experssion.clear();
    history_ans.clear();
    ui->screen_lineedit->clear();
}

1.3.4后缀表达式的转化

        我考虑先将表达式从中缀到后缀的转化。但在这之前,需要对字符串做一系列预处理工作。1)将字符串的连续数字合并成一整个字符串,如256,需要将其合并为三位数“256”而非三个个位数字,另将操作符独自合并成一个字符串(其实还可以考虑将小数点也一起合并,这样就不用考虑后续有关小数点的格式合法性处理了)。2)考虑单独出现负号的情况,措施为在所有紧跟在左括号后面的负号前补0,若负号位于开头同理。3)考虑首位字符的合法情况,在出现非法字符或格式时,直接报错。4)遍历每个数字字符串,剔除前导0情况(排除前面为小数点情况)。5)剔除间隔出现小数点情况(如输入8.8.8)。

1.3.4.1合法性判断

        考虑到判断用户输入的合法性,我罗列了8种情况以应对最基本的输入格式问题。

    /*
   1 小数点后必须为数字
   2 百分号后可接右括号、四则运算符、百分号、指数
   3 左括号后可接数字和左右括号,负号,ln
   4 右括号后可接百分号,四则运算符,右括号,指数
   5 数字后可接四则运算符,百分号,小数点,右括号,数字、指数
   6 四则运算符后跟数字和左括号,ln
   7 指数后接数字和左括号,ln
   8 ln后接左括号
   */

如上,如果在预处理中将小数点也合并,上述的情况1和情况5中有关小数点部分就可以省略。根据这8种情况,可以开一个长度为8的布尔数组,来作为每次循环时向下一层循环传达的信号。

    bool leg_input[8];
    memset(leg_input,true,sizeof leg_input);

初始状态都设置为真。在处理第一个字符串时,可以根据其类型将对应一位数组置为假,然后进入循环(例如第一个字符串为数字123,则将数组第5位元素置假)。

        在每次循环开始,先找出数组的8个元素中哪个为假,然后进入对应的分支,以小数点分支为例:

    if(!leg_input[0])        //上个字符为小数点,下一个为数字
    {
        if(pre_expression[idx][0]-'0'>=0&&pre_expression[idx][0]-'0'<10)  
        //如果是这一次循环到的是数字
        {
            leg_input[0]=true;  //将小数点的信号重新设为真
            leg_input[4]=false;  //将数字的信号设为假,表示下一层循环将循环数字分支
            num(pre_expression[idx++],operation);    //将数字进行入栈操作
            right(operation,opertor,brack);   //补上右括号
        }
        else return "ERROR(INVALID_INPUT_1)";  //如果不是数字,直接报错
    }

以此方式可以规避大多数情况下的输入格式问题,这是因为大多数操作符一般不会隔位影响,除了括号和小数点。对于小数点隔位输入的情况我在预处理中已经剔除,而对于左右括号可能出现的隔位错误,多半是由于左右括号数不匹配造成的。因此我新设一个变量用于判断左右括号数是否一致。

int brack=0;   //判断括号是否成对

在每次循环到左括号,进行入栈操作的同时,让变量增加1;循环到右括号时,先将变量减1,在对操作符栈进行出栈操作时,如果一直到栈空都没找到左括号,就直接将变量设为负无穷。循环结束后根据变量是否为0判断括号数是否匹配。

1.3.4.2中缀表达式转为后缀表达式

        我选择定义一个字符串队列来存储最终的后缀表达式。定义一个字符串栈来存各类运算符。当读到数字,直接将其入队列,读到某个运算符A,将其和运算符栈的栈顶比较,如果栈顶优先级大于等于A,则不停出栈直到优先级小于A,然后将A入栈。之前出栈的若干运算符依次入队列。如果A是左括号,直接将其入栈;若A是右括号,则将运算符从栈顶开始依次弹出入队列,直到遇到左括号,然后直接将左括号弹出,不入队。一些特殊的运算符,对于百分号,我定义其为一个单目运算符,所以直接入队;对于小数点,我采取人为给它套个括号的方式。对于自然对数ln,我选择在输入时就强制在其后面添上一个左括号,然后定义一个整型栈来存储每个ln的括号匹配情况。每当读到一个ln,就将1入栈,每当读到一个左括号,就将栈顶的数加1,每当读到一个右括号,就将栈顶的数减1,当栈顶的数为0时,将其出栈,同时将ln入运算符栈。

    else if(!leg_input[7])   //上一个为ln,下一位为数字和左括号,ln
    {
        //左括号
        if(pre_expression[idx][0]-'('==0)
        {
            dubln.push(1);   //将1入整型栈
            leg_input[7]=true;  
            leg_input[2]=false;
            left(opertor,brack);  //左括号入运算符栈
            idx++;  //指向下一个字符串
        }
        else return "ERROR(INVALID_INPUT_8)";
    }

1.3.5后缀表达式计算

        定义一个浮点型栈,用于存储最终产生的答案。将字符串从队列中依次出队,如果字符串全数字,将其转化成浮点数后入栈(要考虑小数点后允许有前导0的情况),如果是操作符,就将栈的前两个浮点数弹出,运算后再次入栈(需考虑除0等数学错误的情况)。对于单目运算符则只需弹出栈顶浮点数来运算即可。最后栈中应当只留下一个浮点数,即为最终答案。

1.3.6答案格式化

        由于最终是要将其转化成QString类型输出到窗口中,所以需将其转化为字符串。可以选择定义一个输出流对象,将其转化成指定格式输出。

std::ostringstream ost;
ost <<setiosflags(ios::fixed) << std::setprecision(3) <<std::fixed<<anssc;
return ost.str();
//返回保留三位小数字符串

        我通过这种方式利用QComboBox类控件实现可以让用户选择指定格式输出的功能。

 其中def格式默认最多以12位输出。sc表示科学计数法。这里提一句为什么只选择12位输出而不是更多。我一开始选用16位输出,当我调试到一些样例,如81.9/9,会产生如下结果

我不知道为什么会产生这种情况,但换成12位后这种现象似乎就消失了。

1.3.7输入自定义

        我参照CASIO的计算器设计了左右方向键这两个按键,用于控制光标的移动,达到对式子自由修改。但为保证显示屏上始终有光标闪烁,这就需要将显示屏始终保持在强聚焦状态下。在属性界面将其聚焦模式调整为强聚焦,将其他可以点击的按键调整为不可聚焦,这样在程序运行中显示屏就始终保持在强聚焦下了。

 接下来要设置左右方向键的功能。以右移键为例,可以利用如下方式实现右移

void calc_Widget::on_rightmove_button_clicked()
{
    ui->screen_lineedit->cursorForward(false,1);
}

 其中,QT帮助手册对cursorForward的描述如下:

 它指出,布尔型参数mark控制光标在移动时是否选中经过的字符,整型参数steps控制移动步长。

        接下来要考虑如何实现在光标位置进行输入或删除操作,这就需要用到我们先找到光标的位置。

int pos=ui->screen_lineedit->cursorPosition();  //光标位置

一句话即可得到当前光标所在位置。之后,可以利用QString类成员函数insert()对原字符串进行插入操作。insert()函数有多种重载形式,如下是我需要的格式

 position表示插入位置,str表示要插入的字符串。

QString calc_Widget::inp(QString expression,int pos,QString s)
{
    return expression.insert(min(pos,expression.size()),s);  //光标处插入
    /*要注意插入的光标位置要始终不超过字符串总长度*/
}

这里需要注意限制光标的位置。举个例子,由于我设定每次按下等于号后得到的答案会自动覆盖原先的字符串(为了方便连续计算),而且屏幕上会出现“= 答案”这样的形式,此时光标的位置应当为等号长度加空格长度加答案长度,此时一旦按下某个输入按键如+,屏幕上本应该显示“答案+”,但插入位置应当在光标处,而光标位置又比答案长度长2,这就相当于+号插入在一个未定义的位置,从而会导致程序报错。解决方案为将光标位置限制在不超过当前字符串长度,即可避免。

        当一个字符插入后,光标应当右移一格,这需要我重新设置光标位置。

void calc_Widget::on_e_button_clicked()  //按下e
{
    int pos=ui->screen_lineedit->cursorPosition();
    expression=inp(expression,pos,"e");
     ui->screen_lineedit->setText(expression);
    ui->screen_lineedit->setCursorPosition(pos+1);//设置光标位置右移一格
}

应当注意的是,将光标右移一格的操作必须在打印字符串操作之后,否则光标的位置会被重新覆盖。

1.3.8浏览历史功能改进

        我模仿CASIO计算器设计了上下方向键,用于调出历史记录。在之前的预习中我选择将历史记录以栈形式存储,后来发现这样的弊端很多,其中之一就是无法自由调用除栈顶以外的历史,除非将其弹出。我于是将其改为vector形式存储,这样可以通过下标找到对应的历史记录。以上方向键为例,

void calc_Widget::on_up_button_clicked()
{
    if(history_ans.empty());
    else if(h_idx>0)
        expression=history_ans[--h_idx],ui->screen_lineedit->setText(expression);
}

其中h_idx表示vector的索引。需要注意的是,在clear按键中,在清空后需要重新将h_idx更新为history_ans.size(),这是因为我将h_idx设为了全局的静态变量,在经过up键和down键操作后会改变其值。

        我还设置了HISTORY键用于浏览全局历史记录,我在计算器界面右侧加上一个QTextEdit控件用于输出历史,

每次按下等于号后自动在该控件上输出算式及对应答案。按下等号后部分代码如下:

 current_ans=QString::fromStdString(dub_ans);   //当前答案
 QString current_expersion=expression+"="+current_ans;            //历史展示整个式子
 history_ans.push_back(current_expersion);                             //存储历史
 ui->screen_lineedit->setText("= "+current_ans);
 expression=current_ans;                                        //当前值变为答案值
 ui->history_edit->append(current_expersion);
 h_idx=history_ans.size();                       //索引更新

为了美观,我设计了HISTORY键用于展开和收起历史记录页面,我先在构造函数中固定了窗口大小,以保证只出现按键的横屏,然后在HISTORY的槽函数中设计它展开后的尺寸。值得注意的是我是使用了retranslateUi()来访问整个窗口的属性,此时横屏中的式子会被清空,这需要我们重新再打印一次。

void calc_Widget::on_his_button_clicked()
{
    ui->retranslateUi(this);
    if(this->width()==525)this->setFixedWidth(996);
    else this->setFixedWidth(525);
    ui->screen_lineedit->setText(expression);  //重新打印式子
//    qDebug()<<this->width();
}

         为了便于调用最近一次的历史,我又设计了Ans键用于表示上次计算所得结果,在按下Ans键后可以直接获得上次计算的答案。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值