乐趣区

从0开始自制计算器

先看看效果吧:

很炫酷吧?

想不想要?

想要吧.

当然作者知道你们肯定想.

不然也不会点进来对不对.

好. 进入正题.

1. 概述

这个是仿照 win10 自带的计算器制作的简化版本. 是用 Qt 做的, 直接把整个表达式输入然后得出计算结果.
主要分为三部分. 界面部分, 事件处理部分与表达式处理部分.

  • 界面部分就是看到的计算器, 包括标题栏, 中间的输出框, 还有各个按键.
  • 事件处理就是处理对应的鼠标与键盘事件.
  • 表达式处理部分就是处理整个输入的字符串, 返回计算的结果, 当然这个还支持错误判断功能.

2. 新建工程

选择 Widgets Application.

起名字.

一般只需 MinGW.

这里默认即可, 名字可以随便改

2. 界面

(1) 按键

按键的话, 基本上按着改就可以了. 改布局, 改颜色, 改字体, 主要就是这三个.
首先先打开.ui 文件:

a. 添加一个 Grid Layout, 调整好大小.

b. 拖入 Push Button 作为按键,sizePolicy 属性那里水平和垂直属性都选择 Expanding.

c. 调整好颜色, 设置 styleSheet 与字体

这里给出作者的参考 style:

border:1px groove rgb(220,220,220);
background-color:rgb(243,243,243);

字体:

这里按个人喜好调整即可.

d. 复制制作好的 button, 布好局

e. 改内容

这里不仅把里面的字符改变, 还要把相应的对象名也改变.

再细调每一个按键, 包括大小, 字体与颜色, 使总体效果更好.

数字要注意有 ” 加粗 ” 效果, 符号的话尽量 ” 精细 ” 一点.

f. 整体修改大小, 同时加上间隔

调整好间隔. 注意细节.

下面是 win10 自带的计算器:

看到间隔了没?

作者要的就是这种效果.

可以先运行看看.

两边的间隔的话一会配合 widget 的大小调整即可.

(2) 输出框

输出框很简单, 就是一个 QLineEdit.

a. 添加 QLineEdit

b. 调整好大小, 设置好背景颜色

作者的 qss:

border:0px groove rgb(243,243,243);
background-color:rgb(245,245,245);

c. 设置字体, 只读, 对齐

(3) 标题栏

标题栏其实也很简单, 一个 QBoxLayout

a. 新建 Horizontal Layout

b. 添加细节

QLabel 输入标题, 两个 QPushButton 表示最小化与关闭, 同时加入两个 Spacer, 让标题与左边空出一些距离.

其实就是模仿 win10 的标题栏的效果

这里就不做最大化了. 因为涉及到按钮的重新排布问题, 这个可以自己选择实现.

(4) 整体处理

a. 标题栏

把上一步做的标题栏移到合适的位置, 同时删除自带的 QMenuBar,QToolBar,QStatusBar.

b. 调整整体大小, 同时添加透明度

调整好后大概就那样, 透明度这里选择了 0.9.

真是完美啊!

3. 事件处理

(1) 光标事件

A. 标题栏

a. 拖动效果

首先把本来那个标题栏去掉.

setWindowFlags(windowFlags() | Qt::FramelessWindowHint);

再在 protected 中加入鼠标监听函数:

void mousePressEvent(QMouseEvent *);
void mouseMoveEvent(QMouseEvent *);

私有成员中加入两个 QPoint. 分别表示当前窗口坐标与光标的坐标.

QPoint mousePoint;
QPoint windowPoint;

第一个函数是鼠标按下时触发的, 根据 event->button() 判断是否是左键, 是的话获取 mouse 坐标, 在设置 window 坐标.

当触发第二个函数时, 即先判断是否按住左键不放, 使用 MainWindow 的 move 方法移动窗口.

event->globalPos() 获取坐标后减去原来光标的坐标得到 window 坐标的变化量, 再用原坐标加上这个变化量.

void MainWindow::mousePressEvent(QMouseEvent *event)
{if(event->button() == Qt::LeftButton)
    {mousePoint = event->globalPos();
        windowPoint = frameGeometry().topLeft();
    }
}

void MainWindow::mouseMoveEvent(QMouseEvent *event)
{if(event->buttons() & Qt::LeftButton)
    {move(windowPoint + event->globalPos() - mousePoint);
    }
}

b. 最小化与关闭

这里以最小化为例, 关闭也一样的, 改一下函数调用即可.
在最小化按钮中右键选择 Go to slot:

选择 clicked()

添加一个最小化函数:

下面是关闭的函数:

B. 按键

按键的鼠标事件包括两个:

  • 光标移入与移出事件, 为按键添加阴影, 加深颜色等
  • 单击事件, 在输出框中增减对应的字符

a. 移入与移出事件

这里的实现方式是通过事件过滤器实现的. 增加一个 eventFilter() 函数

 bool eventFilter(QObject *,QEvent *);

首先通过 event->type() 判断事件类型, 如果是光标悬停, 再判断对应的各个对象增加阴影效果.

addNumButtonEffet():

void MainWindow::addNumButtonEffect(QPushButton *button,QGraphicsDropShadowEffect *shadow)
{shadow->setEnabled(true);
    button->setStyleSheet("border:1px groove rgb(220,220,220);"
        "background-color:rgb(193,193,193);"
    );
}

这里 QGraphicsDropShadowEffect *shadow 事先初始化好了.

然后在添加事件过滤器:

这里可以对比一下有没有阴影的效果:

没有阴影:

加上阴影:

呃 …. 这里可能是截图工具的问题, 看不来多大的效果, 但是直接在机器上看是有比较大的区别的, 建议还是加上阴影.

b. 单击事件

单击事件就是单击了某个按键然后用户可以在输出框中看到对应的反应.

依次选择按键, 右键 Go to slot:

选择 clicked()

然后添加处理函数, 作者这里自己实现了一个添加文本与清除焦点的函数, 添加文本就是对应按键被光标单击后添加到输出框, 至于为什么要清除焦点 ….

因为 …

因为空格.

因为作者的 ” 良好 ” 习惯, 习惯在运算符前后加上空格

单击后会把焦点保留在这个按钮上, 键盘上敲空格默认会帮你 ” 按一次 ” 这个按钮, 因此如果不清除焦点的话, 在光标单击了某个按钮, 比如 7, 按空格就会在输出框上输出 7, 光标单击了 8 后, 按空格就会在输出框上输出 8.

这里添加文本时还要注意默认的起提示作用的 0.

void MainWindow::appendText(const QString &s)
{if(ui->box->text() == "0")
        ui->box->setText(s);
    else
        ui->box->setText(ui->box->text()+s);
}

void MainWindow::appendTextAndClearFocus(QPushButton *button, const QString &s)
{appendText(s);
    button->clearFocus();}

(2) 键盘事件

键盘事件就是主要处理各个按键按下时的阴影与输出框添加输出.
键盘事件通过以下两个函数处理:

void keyPressEvent(QKeyEvent *);
void keyReleaseEvent(QKeyEvent *);

第一个是按键按下时触发的, 第二个是松开按键触发的.

A. 添加阴影

在按键按下时添加上阴影与颜色加深效果.

通过 event->key() 依次判断各个键.

键位可以看这里

然后添加在 keyRealeseEvent() 中把对应的阴影去掉:

void MainWindow::keyReleaseEvent(QKeyEvent *event)
{switch (event->key())
    {
        case Qt::Key_0:
        case Qt::Key_1:
        case Qt::Key_2:
        case Qt::Key_3:
        case Qt::Key_4:
        case Qt::Key_5:
        case Qt::Key_6:
        case Qt::Key_7:
        case Qt::Key_8:
        case Qt::Key_9:
        case Qt::Key_Plus:
        case Qt::Key_Minus:
        case Qt::Key_Asterisk:
        case Qt::Key_Slash:
        case Qt::Key_AsciiCircum:
        case Qt::Key_Percent:
        case Qt::Key_ParenLeft:
        case Qt::Key_ParenRight:
        case Qt::Key_BraceLeft:
        case Qt::Key_BraceRight:
        case Qt::Key_BracketLeft:
        case Qt::Key_BracketRight:
        case Qt::Key_Backspace:
        case Qt::Key_Space:
        case Qt::Key_Period:
        case Qt::Key_Escape:
        case Qt::Key_Equal:
        case Qt::Key_Return:
            removeNumButtonEffect(ui->num0,num0_shadow);
            removeNumButtonEffect(ui->num1,num1_shadow);
            removeNumButtonEffect(ui->num2,num2_shadow);
            removeNumButtonEffect(ui->num3,num3_shadow);
            removeNumButtonEffect(ui->num4,num4_shadow);
            removeNumButtonEffect(ui->num5,num5_shadow);
            removeNumButtonEffect(ui->num6,num6_shadow);
            removeNumButtonEffect(ui->num7,num7_shadow);
            removeNumButtonEffect(ui->num8,num8_shadow);
            removeNumButtonEffect(ui->num9,num9_shadow);
            removeSignButtonEffect(ui->plus,plus_shadow);
            removeSignButtonEffect(ui->minus,minus_shadow);
            removeSignButtonEffect(ui->mutiply,mutiply_shadow);
            removeSignButtonEffect(ui->divide,divide_shadow);
            removeSignButtonEffect(ui->pow,pow_shadow);
            removeSignButtonEffect(ui->percent,percent_shadow);
            removeSignButtonEffect(ui->parentheses,parentheses_shadow);
            removeSignButtonEffect(ui->parentheses,parentheses_shadow);
            removeSignButtonEffect(ui->brace,brace_shadow);
            removeSignButtonEffect(ui->brace,brace_shadow);
            removeSignButtonEffect(ui->bracket,bracket_shadow);
            removeSignButtonEffect(ui->bracket,bracket_shadow);
            removeSignButtonEffect(ui->backspace,backspace_shadow);
            removeSignButtonEffect(ui->blank,space_shadow);
            removeSignButtonEffect(ui->dot,dot_shadow);
            removeSignButtonEffect(ui->C,c_shadow);
            removeSignButtonEffect(ui->equal,equal_shadow);
            break;
    }
}

这里之所以没有一个个按键去判断是因为有可能同时多个按键按下, 然后同时松开后发现某个按键还存在阴影, 因此统一当其中一个按键释放时去除所有按键的阴影.

B. 添加输出

在输出框中添加输出, 调用一个函数即可:

4. 整体细节再处理

(1) 淡入效果

看看效果:

这里实际使用了 Qt 的动画, 针对透明度改变的动画.

void MainWindow::fadeIn(void)
{QPropertyAnimation * changeOpacity = new QPropertyAnimation(this,"windowOpacity");
    changeOpacity->setStartValue(0);
    changeOpacity->setEndValue(0.91);
    changeOpacity->setDuration(2500);
    changeOpacity->start();}

第一行表示改变的是透明度, 第二三行设置起始值与结束值, 接下来设置动画时间 (单位 ms), 然后开始动画.

(2) 设置固定尺寸

这里可以不设置最大尺寸, 但一定要设置最小尺寸.

设置这个实际上禁止了拖拽去改变大小.

(3) 淡出效果

淡出效果与淡入效果类似.

不同的时需要添加计时处理, 不能直接在 exit(0) 前调用 fadeOut() 函数, 因为动画会在另一个线程启动, 所以需要在主线程休眠指定秒数, 等待淡出效果完成后, 主线程再调用 exit(0);

void MainWindow::fadeOut(void)
{QPropertyAnimation * changeOpacity = new QPropertyAnimation(this,"windowOpacity");
    changeOpacity->setStartValue(0.9);
    changeOpacity->setEndValue(0);
    changeOpacity->setDuration(2500);
    changeOpacity->start();

    QTime start = QTime::currentTime().addMSecs(2500);
    while(QTime::currentTime() < start)
        QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}

其中 addMSecs() 表示要延迟的秒数,while 循环体中表示处理本线程的事件, 其中 100 表示处理事件最多 100ms 就返回本语句.

这里就不放淡出效果的图片了.

5. 表达式处理

由于这是整个字符串作为表达式进行输入, 需要先进行判断再计算. 所以分为判断与计算两部分.

这里使用了一个新开的控制台工程, 后面会把这个整合起来.

(1) 判断

使用了一个 check 类判断, 由于只有 10 个数字按键, 加减乘除, 小数点, 求余, 求次幂, 大中小括号, 空格, 所以可以分成这几类进行判断.

a. 去除所有空格

void removeAllBlank(void)
{
    size_t i = 0;
    while((i = s.find(' ',i)) != string::npos)
        s.erase(i,1);
}

首先把所有空格去除, 避免之后的判断.

b. 分类判断

把表达式中的所有字符分成 5 类:

  • 数字
  • 小数点
  • 运算符号 + – * / ^ %
  • 左括号类 ([ {
  • 右括号类 ) ] }

然后就是针对每个类型判断它的下一个字符是否是允许的类型, 不是的话返回 false.

比如碰上了一个 (或 [ 或 {

则它的下一个不能是运算符号或者小数点, 当然允许 - 与 +, 因为有
(-7) (+234)

这种情况.

然后把这个符号保存下来判断后面是否是对应的右括号.

if(isLeftBrace(i))
{if(isSignOrDot(i+1))
    {if(s[i+1] != '-' && s[i+1] != '+')
            return false;
    }
    braces.push(s[i]);
}

整个判断函数如下:

bool valid(void)
{if(isSignOrDot(0) || isRightBrace(0))
        return false;
    len = s.size();
    stack<char> braces;
    for(size_t i=0;i<len;++i)
    {if(isLeftBrace(i))
        {if(isSignOrDot(i+1))
            {if(s[i+1] != '-' && s[i+1] != '+')
                    return false;
            }
            if(isRightBrace(i+1))
                return false;
            braces.push(s[i]);
        }
        else if(isRightBrace(i))
        {if(isDot(i+1) || isDigit(i+1) || isLeftBrace(i+1))
                return false;
            if(isRightBrace(i+1))
            {stack<char> braces_copy(braces);
                if(braces_copy.empty())
                    return false;
                braces_copy.pop();
                if(braces_copy.empty())
                    return false;
            }
            if(braces.empty())
                return false;
            char brace = braces.top();
            if((brace == '(' && s[i] != ')') || (brace == '[' && s[i] != ']') || (brace == '{' && s[i] != '}'))
                return false;
            braces.pop();}
        else if(isSign(i))
        {if(isSign(i+1) || isDot(i+1) || isRightBrace(i+1))
                return false;
        }
        else if(isDot(i))
        {if(isSignOrDot(i+1) || isBrace(i+1))
                return false;
        }
        else if(isDigit(i))
        {if(isRightBrace(i+1))
            {if(braces.empty())
                    return false;
                char brace = braces.top();
                if((brace == '(' && s[i+1] != ')') || (brace == '[' && s[i+1] != ']') || (brace == '{' && s[i+1] != '}'))
                    return false;
            }
        }
    }
    return braces.empty();}

特别要注意下的就是碰到右括号的情况, 除了要判断是否是单独存在的右括号, 还有判断是否与前一个左括号匹配.

c. 加 0

这是针对单目运算符 - 的情况, 比如 (-7), 然后把它转化为 (0-7):

string getResult(void)
{size_t len = s.size();
    for(size_t i = 0;i<len;++i)
    {if(s[i] == '(' && (s[i+1] == '-' || s[i+1] == '+'))
            s.insert(i+1,"0");
    }
    return s;
}

在左小括号后判断是否是 - 或 +, 是的话对应位置插入 0.

(2) 计算

a.calc 辅助类

calc 辅助类中使用了两个栈, 运算符栈与操作数栈.

private:
    stack<char> operators;
    stack<double> operands;

其中有两个重要的方法:

bool canCalculate(char sign);
void calculate(void);

第一个方法将下一个准备进入的符号作为参数, 判断是否可以计算操作数栈的前两个数, 如果可以的话, 使用第二个函数进行计算.

calculate() 会将出栈两个操作数与一个运算符, 得出结果后在将其压回操作数栈.

void calculate(void)
{double post = popAndGetNum();
    char sign = popAndGetSign();
    double pre = popAndGetNum();
    double result = 0.0;
    switch (sign)
    {
        case '+':
            result = pre+post;
        break;
        case '-':
            result = pre-post;
        break;
        case '*':
            result = pre*post;
        break;
        case '/':
            if(fabs(post) < 1e-6)
            {
                cout<<"Error.Divisor is 0.";
                exit(EXIT_FAILURE);
            }
            else
                result = pre / post;
        break;
        case '^':
            result = pow(pre,post);
        break;
        case '%':
            result = static_cast<int>(pre) % static_cast<int>(post);
        break;
    }
    push(result);
}

bool canCalculate(char sign)
{if(sign == '(' || sign == '[' || sign == '{' || operators.empty())
        return false;
    char t = getSign();
    if(t == '^')
        return true;
    switch (t)
    {
        case '+':
        case '-':
            return sign == '+' || sign == '-';
        case '*':
        case '/':
        case '%':
            return sign == '+' || sign == '-' || sign == '*' || sign == '/' || sign == '%';
    }
    return false;
}

下面是 calc 类:

class calc
{
private:
    stack<char> operators;
    stack<double> operands;

    char getSign(void)
    {return operators.top();
    }

    double getNum(void)
    {return operands.top();
    }

    void popSign(void)
    {operators.pop();
    }

    void popNum(void)
    {operands.pop();
    }

    double popAndGetNum(void)
    {double num = getNum();
        popNum();
        return num;
    }

    char popAndGetSign(void)
    {char sign = getSign();
        popSign();
        return sign;
    }
public:
    void push(double num)
    {operands.push(num);
    }

    void push(char sign)
    {operators.push(sign);
    }

    char get(void)
    {return getSign();
    }

    void pop(void)
    {popSign();
    }

    double result(void)
    {return getNum();
    }

    void calculate(void)
    {double post = popAndGetNum();
        char sign = popAndGetSign();
        double pre = popAndGetNum();
        double result = 0.0;
        switch (sign)
        {
            case '+':
                result = pre+post;
            break;
            case '-':
                result = pre-post;
            break;
            case '*':
                result = pre*post;
            break;
            case '/':
                if(fabs(post) < 1e-6)
                {
                    cout<<"Error.Divisor is 0.";
                    exit(EXIT_FAILURE);
                }
                else
                    result = pre / post;
            break;
            case '^':
                result = pow(pre,post);
            break;
            case '%':
                result = static_cast<int>(pre) % static_cast<int>(post);
            break;
        }
        push(result);
    }

    bool canCalculate(char sign)
    {if(sign == '(' || sign == '[' || sign == '{' || operators.empty())
            return false;
        char t = getSign();
        if(t == '^')
            return true;
        switch (t)
        {
            case '+':
            case '-':
                return sign == '+' || sign == '-';
            case '*':
            case '/':
            case '%':
                return sign == '+' || sign == '-' || sign == '*' || sign == '/' || sign == '%';
        }
        return false;
    }

    bool empty(void)
    {return operators.empty();
    }
};

private 封装了一些简单的对两个栈进行操作的工具方法, 公有的 pop() 与 get() 是对运算符栈进行的操作. 因为外部不需要对操作数栈进行操作, 由 calculate() 进行操作, 公有的 push 重载了, 可以 push 到操作数栈或运算符栈.

b. 计算部分

计算部分在这里直接放在了 main 中:

int main(void)
{
    check chk;
    while(!chk.inputAndCheck())
        cout<<"Error!Please input again.\n";
    string s = chk.getResult();
    size_t len = s.size();
    calc c;
    for(size_t i=0;i<len;++i)
    {if(isdigit(s[i]))
        {
            double num;
            size_t i1 = i+1;
            while(i1 < len && (isdigit(s[i1]) || s[i1] == '.'))
                ++i1;
            istringstream input(s.substr(i,i1));
            input>>num;
            i = i1-1;
            c.push(num);
        }
        else if(s[i] == '}' || s[i] == ']' || s[i] == ')')
        {
            char sign;
            char start = (s[i] == '}' ? '{' : ( s[i] == ']' ? '[' : '('));
            while((sign = c.get()) != start)
                c.calculate();
            c.pop();}
        else                          //s[i]  is  [ ( {  + - * / ^ %
        {while(c.canCalculate(s[i]))
                c.calculate();
            c.push(s[i]);
        }
    }
    while(!c.empty())
        c.calculate();
    cout<<"result is"<<c.result()<<endl;
    return 0;
}

对表达式的每个字符逐个处理, 若是数字, 提取出来并压栈.
若是右括号类, 不断从运算符栈中提取直到把这段括号内的表达式计算完成.

否则若是左括号或者是运算符, 当可以计算的时候一直计算, 提取两个操作数运算并压栈, 再把新的运算符压栈.

最后使用 result() 获取结果.

c. 测试

这里就显示几个很长的例子算了

当然作者测试了很多的例子

6.6/{2.3+34.3*2.22-5%2+22%4*[2+3.4/5-(4.3+3.2*33.3)]+34.3} + 7.8*{2.4-6/6+0-0*[23.4-3.4/6+4*(2.2+3)]}+0 - 0 + 0.0 
= 10.8569

3.4 - (+3.34) + 34.3 * (-2) / 3.34 + {[(-3.4)^2/3.4+3.4/3]-3.32+[3*(-3)]}
= -28.2656

9^5-34.4^2.3+5%6-34+66%78-78%4 + (-3)*3.4 / {3*(-7)+[3*(-8)+3*(3.4+4.34)/9.3-3.2 + 0.0 - 0]+0.0 - 0}+3.4^4/6.888 
= 55683.2

不信的话可以手工计算一下.

6. 整合

这部分把界面部分与表达式处理部分整合起来.

(1) 设置界面的调用进程, 并获取输出结果

计算表达式的程序叫 MyCalc.exe, 注意把它放在对应的工程文件夹下面, 然后使用 QProcess 调用.

使用 execute 执行, 表达式先去除所有的空格, 然后作为命令行参数传递给计算程序, 然后计算程序把计算结果写入到 result.txt 文件,Qt 读取这个文件, 如果读到 #表示表达式输入错误, 否则, 则是正确的计算结果.

对于结果因为在计算程序中设置了 fixed 格式, 因此对于

1+2

也会返回

3.000000

这步把多余的 0 去掉, 还要注意小数点的情况.

(2) 修改一些细节地方

a. 鼠标键盘修改事件

修改 setText 的内容, 把结果传递过去.

b.exe 中设置数字的格式

设置 fixed 格式, 否则的话显示的是科学计数法, 对小数位数有要求的话可以设置 setprecision.

c. 设置错误提示

这里出现错误时, 输出 ”#”, 然后主程序读取到就会提示 ” 表达式错误, 请重新输入.”

还有除数为 0 的错误提示, 这个要注意一下:

d. 可以考虑把错误处理整合过来

比如输入了一个点, 不能继续输入点, 输入了一个乘号或者除号不能再继续输入另一个符号:

7. 打包发布

(1) 首先去下载 Enigma Virtual Box

(2) 添加环境变量

把 Qt 文件夹下的如图所示的 bin 添加到 Path 环境变量,

(3) 打包库文件

使用 windeployqt 打包, 首先把程序调成 release 模式, 运行一次, 生成 release 的 exe, 然后把 exe 复制到一个单独的文件夹, 再用命令行进入到这个文件夹, 运行

windelpoyqt xxx.exe

这个命令把需要的 dll 复制到当前所在文件夹.

(4) 生成单个 exe

打开 Enigma Virtual Box, 选择

第一个选择 release 的 exe, 第二个选择打包之后的文件夹, 然后选择添加文件, 选择递归添加, 添加上一步生成的所有文件 (夹).

这里选择压缩文件.
然后选择压缩等待完成即可.

(5) 测试

点击运行.

大功告成!!

8. 源码

  • 1.github(里面包含完整可执行的单个 exe)
    注: 由于使用了 lfs 上传大文件, 所以 clone 的时候请使用

    git lfs clone
  • 2. 码云

9. 参考链接

1.Qt 淡入

2.Qt 按键

3.Qt 标题栏

4. 事件过滤器

5.Qt 鼠标事件

6.Qt 延时处理

7.Qt 文件读写

8.Qt 打包成单文件

10. 最后

这个简单的计算器有很大的改进空间, 比如可以添加各种 ” 数 ”:
正弦函数, 余弦函数, 正切函数, 反正弦函数, 指数函数, 对数函数, 高阶导数, 抽象函数, 复合函数. 心里没数

等等. 另外还可以改进矩形的按钮, 可以改成圆角矩形或者椭圆形.
另外, 对于阴影的处理可以添加淡入淡出效果.

最后就是磨砂. 因为 win10 的是有磨砂效果的, 这个作者还不会 ….
最后再上几个图, 看看效果 (由于动图大小的限制只是简单的表达式 …):

希望你们也有一个属于自己的计算器!

退出移动版