先看看效果吧:
很炫酷吧?
想不想要?
想要吧.
當然作者知道你們肯定想.
不然也不會點進來對不對.
好.進入正題.
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的是有磨砂效果的,這個作者還不會....
最後再上幾個圖,看看效果(由於動圖大小的限制只是簡單的表達式...):
希望你們也有一個屬於自己的計算器!