QT 实操 - 疯狂的 ユウカ

11490DX Re: Master Lv.15

 

这个工程是一个计算器的可视化。效果图:

最上面的框是直接输出 double 结果值(有科学计数法),中间的框是输出最多保留 8 位小数的结果值(无科学计数法),下面的按键则是输入。

那么就可以一步一步地来实现这个疯狂的想法。

UI 部分

还是先点击 widget.ui,进入设计界面。

然后先把所有按钮、一个 TextEdit 和一个 LineEdit 给搞到上面。对于按钮的对齐,你并不需要像缝衣服穿针那样看的那么仔细,只需要先全选,然后点击最上面菜单上的那九个点的东西,这个其实就是 QGridLayout。一点它就可以自动像网格状排好版。

然后先给这些按钮的属性设置好。按钮的字体使用 Saira,TextEdit 的字体使用 Monocraft,LineEdit 的字体使用 Unifont。并且两个 Edit 全部设置为仅可读。再给所有对象命一个合理且规范的名字,这样的话 UI 部分基本上就写完了。

计算器逻辑部分

计算器,不外乎就是让你求一个中缀表达式的值。这个应该是普及到提高组的知识点。于是今日上午的 NOIP 联考我花了 1 个半小时将求中缀表达式全部写好并且调好(这玩意难调死了 qwq)。

我在考场上写的屎山代码,快来找 bug
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
QString s;
int getpri(QChar w){
if(w == '^') return 3;
else if(w == '*' || w == '/') return 2;
else return 1;
}
bool illegal = 0;
QStack<double> valstk; QStack<QChar> opestk;
QStack<int> bracket;
bool isope(QChar w){
return w == '^' || w == '+' || w == '-' || w == '*' || w == '/';
}
bool isnum(QChar w){
return ('0'<=w&&w<='9')||w=='.'||w=='x';
}
void process(){
double num2 = valstk.top(); valstk.pop();
double num1 = valstk.top(); valstk.pop();
QChar ope = opestk.top(); opestk.pop();
double res;
// std::cerr<<"PROCESS: "<<num1<<' '<<num2<<' '<<ope.toLatin1()<<'\n';
if(ope == '^') res = pow(num1, num2);
else if(ope == '+') res = num1 + num2;
else if(ope == '-') res = num1 - num2;
else if(ope == '*') res = num1 * num2;
else if(ope == '/') res = num1 / num2;
valstk.push(res);
}
bool ok;
void Widget::on_equalButton_clicked()
{
s = str;
int siz = s.size();
int am = 1, point = 0; double cur = 0, k = 1;
for(int i=0;i<siz;i++){
// cerr<<i<<'\n';
if(isnum(s[i])){
if(s[i] == 'x' && isnum(s[i+1])){illegal = 1; break;}
else if(s[i] == 'x') cur = lstval;
else if(point == 0 && s[i] == '.') point = 1, k /= 10;
else if(point == 0) cur = cur * 10, cur += s[i].digitValue();
else if(point == 1 && s[i] != '.') cur += k * s[i].digitValue(), k /= 10;
else if(point == 1 && s[i] == '.'){illegal = 1; break;}
}else if((i == 0 || isope(s[i-1]) || s[i-1] == '(') && s[i] == '-' && isnum(s[i+1])){
am = -1;
}else if((s[i] == '(' || isope(s[i])) && ((isope(s[i+1])&&s[i+1]!='-') || s[i+1] == ')')){
illegal = 1; break;
}else if(s[i] == '('){
bracket.push(opestk.size());
}else if(s[i] == ')'){
if(bracket.empty()){illegal = 1; break;}
if(isnum(s[i-1])){
cur *= am; valstk.push(cur*am); cur = 0, am = 1, point = 0, k = 1;
}
int curtop = bracket.top(); bracket.pop();
while(opestk.size() > curtop) process();
}else if(isope(s[i])){
// cerr<<"HERE\n";
cur = cur * am; am = 1, point = 0, k = 1;
if(i>0&&isnum(s[i-1])) valstk.push(cur);
if((opestk.empty()) || (bracket.size() && opestk.size() == bracket.top()) || getpri(opestk.top()) < getpri(s[i])){
opestk.push(s[i]);
}else{
process(); opestk.push(s[i]);
}
cur = 0;
}else{
illegal = 1; break;
}
}
if(isnum(s[siz-1])) cur *= am, valstk.push(cur), cur=0, am=1;
// cerr<<"JUMPOUT!\n";
// cerr<<opestk.size()<<' '<<valstk.size()<<'\n';
while(opestk.size() && valstk.size()>1){
process();
}
QString valdis;
if(opestk.size() || (valstk.size() > 1) || bracket.size()) illegal = 1;
if(illegal) str = "ILLEGAL!", valdis = "ILG";
else{ str = QString::number(valstk.top());
if(!isnum(str[0]) && str[0] != '-'){illegal = 1, str = "ERROR!", valdis = "ERR";}
else str = QString("%1").arg(valstk.top(), 0, 'f', 8), valdis = QString::number(valstk.top());
}
while(str.back() == '0') str.chop(1);
if(str.back() == '.') str.chop(1);
if(s.isEmpty()) str = "0", valdis = "0", lstval = 0;
ui->displayArea->setText(str); str = "x";
ui->valDisplay->setText(valdis);
if(illegal){
setAllable(false);
}
lstval = valstk.top();
illegal = 0; opestk.clear(); valstk.clear(); bracket.clear();
}

然后就是设置每一个按钮的功能。这里我在 widget.hclass Widgetprivate 部分给整个 Widget 定义了一个表达式字符串,我定义为 QString str。然后这个 str 就可以在后面发挥出作用了。

  • 大部分按钮:在 后面添加一个字符。
  • 按钮【±】:更改最初 的第一个字符:即从没有变成负号、从负号变为没有。这个计算器在设计的时候并不兴【+x】的说法,而【-x】一般被这个计算器看作
  • 按钮【→】:退格按钮。即删除 的最后一个字符。可以使用 str.chop(1) 实现。(注意标准 C++ 的 string 并无 .chop() 操作!!!)
  • 按钮【C】:清除按钮。即将 变为空串。

完成了之后计算器的逻辑部分应该就是被设计好了。

输出部分

欲将算出来的结果显示出来并用于下一次的计算之中,我使用了一个 double lstval 来保证精度,以及两个 QString 表示显示在中间的答案和显示在右上角的答案。注意有以下几个细节:

  1. 无科学计数法的 double -> QString转换方式是 str = QString("%1").arg(val, 0, 'f', bit)val 是你欲转换的 double 变量,bit 是你欲保留几位小数。
  2. 特判当前 为空串时答案显示为
  3. 特判在式子不合法或者答案溢出的时候将输出设为 ILLEGAL! 或者是 ERROR! 并禁用除【C】以外的其它按键,按【C】后恢复。
  4. 显示了之后将原串的答案改为 x。即 。然后后面写表达式的时候就基于这个 x(中间显示的也是 x,但是右上角依然是上次答案的直接输出类型)。下一次计算答案的时候碰到 x 了就将当前数赋为 lstval,毕竟只有一位,就非常的方便。
  5. 多测不清空,爆零两行泪!

美化细节

到了这个时候,这个程序本应就这么做完了。但是我们依然遇到了一些问题:

字体的内嵌

就比如说,你把这个程序放到另外一个不打 Phigros 的人的电脑上跑。显然他不会安装 Saira 字体,就会导致你的字体被打回原形。为了解决这个问题,我们新介绍一个程序的资源储存库,即 Resources。如何向 Resources 里面添加东西呢?

  1. 右键项目,点击【添加新文件】或者【Add New】

  1. 选择 Qt -> Qt Resource File。即添加源文件。
  2. Location:填写名称。
  3. Summary:不用管,直接点击完成。

然后你就会跳转到一个新 qrc 文件里面,右键这个 qrc 文件并且选择 Open in Editor 可以达到同样的效果。

你现在欲添加字体文件,那么先添加前缀,【Add Prefix】。然后自己定前缀,这个前缀会影响到时候引用的路径。然后添加文件,【Add Files】,就选你欲添加的字体文件,然后添加完了之后就可以了。

你添加了还不行,你要让这个程序引用,识别你的字体文件。那么就先 #include<QFontDatabase>,然后在 widget.cppWidget::Widget 里面添加如下语句:

1
QFontDatabase::addApplicationFont(":/new/prefix1/fonts/Monocraft.ttf"); QFontDatabase::addApplicationFont(":/new/prefix1/fonts/Saira.ttf"); QFontDatabase::addApplicationFont(":/new/prefix1/fonts/Unifont.ttf");

这里的 new/prefix1/ 就是你在 .qrc 文件里面设定的【前缀】。

之后你就可以正常使用你的字体了,无论哪里。

图片、文本的小优化

这个时候你点运行,你的左上角还是默认的程序框框,标题还是 Widget。你欲改成好玩的,怎么办?

首先把 Logo 文件按照和字体文件一样的方法插入到资源里面。然后进设计界面,点击最上面的 Widget 对象,改变其属性的 windowTitle 和 windowIcon 即可。

然后右上角的框框你要让它文字靠右,只需要在 Widget::Widget() 里面添加 ui->valDisplay->setAlignment(Qt::AlignRight) 即可。valDisplay 是你写的右上角的框框的对象名称。

最终程序的打包

马上要完成了。你去了运行目录去看了你生成出来的程序,发现有一抹多个文件,并且删掉其中任意一个都不行。你想把它们打包成一个 exe,以便可以拿回家给 ユウカ 用。该如何做?

这里分两步。第一步是将所有的程序所需文件集中到一起,第二步则是将程序打包。

首先打开 Qt x.x.x (MinGW x.x.x 64-bit) 应用程序,在 Windows 开始界面里面搜索即可搜到(因为我用的是 MinGW 所以自然也就用 MinGW 了)。

点开之后,输入指令 windeployqt exedirexedir 是你生成出来的程序的精确位置。

如果没错的话,这个终端运行了一会,最后生成一堆语言文件后就会停下来。这就代表第一步运行成功了。

接下来做第二步。首先下载 Enigma Virtual Box 应用程序。这里是链接

然后点开,选择【待封包的主程】,就是你刚刚的那个 exe。然后下面的另存为的路径就会自动给你生成出来。

接着,点击最下面的【增加…】,选择【增加文件夹(递归)(X)】,然后选择你的 release 文件夹。

最后点击最下面的【文件选项】,选择压缩文件可以减少你的文件体积。最后执行封包!大功告成!你可以将你的计算器程序发给 ユウカ 了!


后记

最后发现我的计算器非常的垃圾,但是因为这是我第一次写的工程,ユウカ 也好好保存了这个文件。

The End.

工程源文件 最终程序

感谢哈赤提供的千年 Logo,以及互联网上的 Unifont、Monocraft 和 Saira 字体文件。

07-07-29 upd:昨日我的脑子出了点事故。明明知道打包的时候会把整个文件夹下面的目录一起打包,这个文件夹就包括了这个 xxx_boxed.exe,结果我硬是打包了 998244352 道,硬生生地把一个几十多兆的文件给干成 118 MB,git 都传不上去。还差点导致我的博客以后都用不了了。今天终于解决了,已换源。

  • Title: QT 实操 - 疯狂的 ユウカ
  • Author: 11490DX
  • Created at : 2025-07-28 17:11:46
  • Updated at : 2025-07-30 16:08:06
  • Link: https://11490dx.net/2025/07/28/QT-R70728/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments