经过前面的几篇文章,相信大家对于Qt Creator这个IDE以及Qt基本的使用方法有所了解。这篇文章我们使用已经了解过的技术简单的定制一个计算器。
这里我们基于Qt Creator中定制GUI程序04(国际化多语言)中提供的工程在继续往下开发。
相信大家都用过Windows下的计算器,作为一个计算器,我们需要一个用来输入和显示结果的文本框(QTextEdit),我们还需要一些数字符号的按钮(QPushButton)。这些都是常用的控件,我们可以直接在Qt Creator上拖出来。
我们这里对整个窗口使用垂直布局,最上方放置了一个文本框,下方我们放置了一个QGridLayout(珊格)布局在布局中放置了很多个按钮分别表示数字0-9和加减乘除等按钮。
放置后我们需要调整标识控件的名称(objectName属性),控件显示的内容(比如QPushButton中的text属性),控件自动缩放的属性(sizePolicy),并为每个按钮控件设置快捷键(shortcut属性)。
设置完成后结果如下图所示。

如果我们想对用户按下按钮作出反应,常规的做法是,为每个按钮注册Clicked的信号。不过很显然我们一个一个注册实在是太麻烦了,这里我们使用了一个偷懒的办法,按钮组(QButtonGroup)。如果我们需要处理界面上多个类似功能的按钮,我们可以把这些按钮组成一个组,然后为一个组设置一个响应函数就可以了。

在这里需要注意的是Qt Creator不能直接对buttonGroup组本身设置槽函数,所以需要手动创建一个槽函数并手动连接到信号。
这里我选择在界面初始化时connect buttonGroup的buttonClicked信号到一个MainWindow中的Button_clicked函数。
QObject::connect(ui->buttonGroup, (void (QButtonGroup::*)(QAbstractButton *))&QButtonGroup::buttonClicked , this , &MainWindow::Button_clicked);
这样组内的任意按钮产生Clicked事件都会调用Button_clicked这个函数,我们可以看到Button_clicked函数传入了一个QAbstractButton对象的指针,这个指针用来确定组中具体是哪个按钮被按下了。
在计算器上,如果用户按下的按钮是数字,则显示在文本框里,如果是符号,则展开清屏或者计算操作。
这里我直接给出实现。
void MainWindow::Button_clicked(QAbstractButton *ojbect_button) { QString button_text=ojbect_button->text(); QString display_text=ui->textEdit->toPlainText(); if(display_text.length()>20) { ui->textEdit->setText("ERROR"); return; } if(display_text=="ERROR") { ui->textEdit->setText(""); display_text.clear(); } if(button_text == "C") { ui->textEdit->setText(""); } else if(button_text == "back") { display_text.remove(display_text.length()-1,display_text.length()); ui->textEdit->setText(display_text); } else if(button_text == "=") { QString str_rpn_exp=calc_string_to_rpn_string(display_text); QString str_display=calc_rpn_string(str_rpn_exp); ui->textEdit->setText(str_display); } else { display_text+=button_text; ui->textEdit->setText(display_text); } }
我们简单的解释一下,通过ojbect_button指针我们可以调用text函数获取控件的text属性,通过text属性我们就可以知道是哪一个按钮被按下,如果是数字则直接添加到textEdit文本框中,如果是符号则进行相关处理。通过setText函数我们可以设置文本框显示的内容。
这里为了避免未知的问题,我限制了表达式的长度为20个字符,超过会显示ERROR,大家可以自行修改此处。
我们可以看到当等号按钮被按下时,这里调用了calc_string_to_rpn_string和calc_rpn_string这两个函数。这两个函数用于把我们常见的中缀表达式转换成逆波兰表达式(也叫后缀表达式),最后在计算逆波兰表达式得到结果。
把中缀表达式转换成逆波兰式是为了更方便我们进行计算,这方面网络上有非常多的资料,我不过多赘述,直接给出有关的实现。
bool is_number(char ch) { if(ch >= '0' && ch <= '9') return true; if(ch == '.') return true; return false; } QString calc_string_to_rpn_string(QString str_express) { QStack<char> stack_symbol; QString str_rpnexp = ""; for(int npos = 0; npos < str_express.length(); npos++) { char ch = str_express[npos].toLatin1(); if(is_number(ch) == true) { str_rpnexp += ch; npos++; while(npos < str_express.length()) { ch = str_express[npos].toLatin1(); if(is_number(ch) == true) { str_rpnexp += ch; npos++; } else { break; } } str_rpnexp += '#'; } //other symbool if(ch == '+' || ch == '-') { if(stack_symbol.isEmpty() != true && stack_symbol.top() != '(') { do { str_rpnexp += stack_symbol.pop(); } while(stack_symbol.isEmpty() != true && stack_symbol.top() != '('); } stack_symbol.push(ch); } else if(ch == '*' || ch == '/') { if(stack_symbol.isEmpty() != true && (stack_symbol.top() == '*' || stack_symbol.top() == '/') ) { do { str_rpnexp += stack_symbol.pop(); } while(stack_symbol.isEmpty() != true && stack_symbol.top() != '('); str_rpnexp += ch; } stack_symbol.push(ch); } else if(ch == '(') { stack_symbol.push(ch); } else if(ch == ')') { while(stack_symbol.isEmpty() != true) { ch = stack_symbol.pop(); if(ch != '(') { str_rpnexp += ch; } } } } while(stack_symbol.isEmpty() != true) { char ch = stack_symbol.pop(); if(ch != '(') { str_rpnexp += ch; } } return str_rpnexp; } QString calc_rpn_string(QString str_rpn_express) { QStack<double> stack_num; for(int npos = 0; npos < str_rpn_express.length(); npos++) { char ch = str_rpn_express[npos].toLatin1(); if(is_number(ch) == true) { QString str_num; str_num += ch; npos++; while(npos < str_rpn_express.length()) { ch = str_rpn_express[npos].toLatin1(); if(is_number(ch) == true) { str_num += ch; npos++; } else { break; } } bool b_conversion = false; double do_num = str_num.toDouble(&b_conversion); if(b_conversion == false) { return "ERROR"; } stack_num.push(do_num); } if(ch == '#') { continue; } else { if(stack_num.length() < 2) { return "ERROR"; } double b = stack_num.pop(); double a = stack_num.pop(); switch(ch) { case '+': stack_num.push(a + b); break; case '-': stack_num.push(a + b); break; case '*': stack_num.push(a * b); break; case '/': stack_num.push(a / b); break; default: return "ERROR"; } } } if(stack_num.isEmpty() == true) { return "ERROR"; } double calc = stack_num.pop(); return QString("").setNum(calc); }
这里我们简单的说明一下,QString这个类与std::string有一些区别。我们都知道std::string实际上就像个vector是一个单字节的字符串实现,很显然char类型容纳不了所有的汉字,为了能储存显示汉字(或者其他的语言)std::string在不同平台可能有不同的编码实现,比如在Linux上std::string中的内容采用的是UTF-8编码,而在Windows下使用的则是ANSI编码,对于中文版的Windows来说就是GBK编码。如果全都是UTF-8可能没有什么问题,但是对不同语言版本的Windows编码就成为了大难题,如果开发者想一劳永逸的解决各个语言版本的编码兼容性,Microsoft推荐大家使用UNIOCDE编码(对于高于Windows 2000的系统版本,就是UTF-16编码,至于低于Windows 2000的系统版本,我建议选择拒绝兼容)但是使用UTF-16编码则不能直接与Linux系统兼容。
多系统的兼容很显然是一件非常麻烦的事情,作为传统的开发方法,开发者必须考虑每个平台的特点作出取舍或者写上一堆转换编码的函数互相转换。在Qt中QString这个类为我们提供了统一的解决方案,QString内部采用了UNIOCDE编码,这样使得任何文字都能正确的储存在QString中,在Windows上也可以直接获得良好的兼容性。当然作为牺牲,如果想用我们熟悉的char来解决问题,需要使用toLatin1函数转换成Latin1编码。
QStack和std::stack类似,用来作为栈这种数据结构使用,熟悉栈的同学应该对pop,push,top等操作一目了然。
最后我们可以编译看运行结果。运行结果如下图所示。

当用户按下等号后将会计算出正确结果14.
工程源代码下载地址:
https://www.exvs.org/wp-content/uploads/public/Qt_Tutorial/qt_calculator_FCE2BED0.7z