先看看效果吧:css
很炫酷吧?git
想不想要?github
想要吧.bash
固然做者知道大家確定想.app
否則也不會點進來對不對.less
好.進入正題.ide
這個是仿照win10自帶的計算器製做的簡化版本.是用Qt作的,直接把整個表達式輸入而後得出計算結果. 主要分爲三部分.界面部分,事件處理部分與表達式處理部分.函數
選擇Widgets Application.工具
起名字.oop
通常只需MinGW.
這裏默認便可,名字能夠隨便改
按鍵的話,基本上按着改就能夠了.改佈局,改顏色,改字體,主要就是這三個. 首先先打開.ui文件:
這裏給出做者的參考style:
border:1px groove rgb(220,220,220); background-color:rgb(243,243,243);
字體:
這裏按我的喜愛調整便可.
這裏不只把裏面的字符改變,還要把相應的對象名也改變.
再細調每個按鍵,包括大小,字體與顏色,使整體效果更好.
數字要注意有"加粗"效果,符號的話儘可能"精細"一點.
調整好間隔.注意細節.
下面是win10自帶的計算器:
看到間隔了沒?
做者要的就是這種效果.
能夠先運行看看.
兩邊的間隔的話一會配合widget的大小調整便可.
輸出框很簡單,就是一個QLineEdit.
做者的qss:
border:0px groove rgb(243,243,243); background-color:rgb(245,245,245);
標題欄其實也很簡單,一個QBoxLayout
QLabel輸入標題,兩個QPushButton表示最小化與關閉,同時加入兩個Spacer,讓標題與左邊空出一些距離.
其實就是模仿win10的標題欄的效果
這裏就不作最大化了.由於涉及到按鈕的從新排布問題,這個能夠本身選擇實現.
把上一步作的標題欄移到合適的位置,同時刪除自帶的QMenuBar,QToolBar,QStatusBar.
調整好後大概就那樣,透明度這裏選擇了0.9.
真是完美啊!
首先把原本那個標題欄去掉.
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); } }
這裏以最小化爲例,關閉也同樣的,改一下函數調用便可. 在最小化按鈕中右鍵選擇Go to slot:
選擇clicked()
添加一個最小化函數:
下面是關閉的函數:
按鍵的鼠標事件包括兩個:
這裏的實現方式是經過事件過濾器實現的.增長一個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事先初始化好了.
而後在添加事件過濾器:
這裏能夠對比一下有沒有陰影的效果:
沒有陰影:
加上陰影:
呃....這裏多是截圖工具的問題,看不來多大的效果,可是直接在機器上看是有比較大的區別的,建議仍是加上陰影.
單擊事件就是單擊了某個按鍵而後用戶能夠在輸出框中看到對應的反應.
依次選擇按鍵,右鍵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(); }
鍵盤事件就是主要處理各個按鍵按下時的陰影與輸出框添加輸出. 鍵盤事件經過如下兩個函數處理:
void keyPressEvent(QKeyEvent *); void keyReleaseEvent(QKeyEvent *);
第一個是按鍵按下時觸發的,第二個是鬆開按鍵觸發的.
在按鍵按下時添加上陰影與顏色加深效果.
經過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; } }
這裏之因此沒有一個個按鍵去判斷是由於有可能同時多個按鍵按下,而後同時鬆開後發現某個按鍵還存在陰影,所以統一當其中一個按鍵釋放時去除全部按鍵的陰影.
在輸出框中添加輸出,調用一個函數便可:
看看效果:
這裏實際使用了Qt的動畫,針對透明度改變的動畫.
void MainWindow::fadeIn(void) { QPropertyAnimation * changeOpacity = new QPropertyAnimation(this,"windowOpacity"); changeOpacity->setStartValue(0); changeOpacity->setEndValue(0.91); changeOpacity->setDuration(2500); changeOpacity->start(); }
第一行表示改變的是透明度,第二三行設置起始值與結束值,接下來設置動畫時間(單位ms),而後開始動畫.
這裏能夠不設置最大尺寸,但必定要設置最小尺寸.
設置這個實際上禁止了拖拽去改變大小.
淡出效果與淡入效果相似.
不一樣的時須要添加計時處理,不能直接在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就返回本語句.
這裏就不放淡出效果的圖片了.
因爲這是整個字符串做爲表達式進行輸入,須要先進行判斷再計算.因此分爲判斷與計算兩部分.
這裏使用了一個新開的控制檯工程,後面會把這個整合起來.
使用了一個check類判斷,因爲只有10個數字按鍵,加減乘除,小數點,求餘,求次冪,大中小括號,空格,因此能夠分紅這幾類進行判斷.
void removeAllBlank(void) { size_t i = 0; while((i = s.find(' ',i)) != string::npos) s.erase(i,1); }
首先把全部空格去除,避免以後的判斷.
把表達式中的全部字符分紅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(); }
特別要注意下的就是碰到右括號的狀況,除了要判斷是不是單獨存在的右括號,還有判斷是否與前一個左括號匹配.
這是針對單目運算符-的狀況,好比(-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.
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到操做數棧或運算符棧.
計算部分在這裏直接放在了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()獲取結果.
這裏就顯示幾個很長的例子算了
固然做者測試了不少的例子
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
不信的話能夠手工計算一下.
這部分把界面部分與表達式處理部分整合起來.
計算表達式的程序叫MyCalc.exe,注意把它放在對應的工程文件夾下面,而後使用QProcess調用.
使用execute執行,表達式先去除全部的空格,而後做爲命令行參數傳遞給計算程序,而後計算程序把計算結果寫入到result.txt文件,Qt讀取這個文件,若是讀到#表示表達式輸入錯誤,不然,則是正確的計算結果.
對於結果由於在計算程序中設置了fixed格式,所以對於
1+2
也會返回
3.000000
這步把多餘的0去掉,還要注意小數點的狀況.
修改setText的內容,把結果傳遞過去.
設置fixed格式,不然的話顯示的是科學計數法,對小數位數有要求的話能夠設置setprecision.
這裏出現錯誤時,輸出"#",而後主程序讀取到就會提示"表達式錯誤,請從新輸入."
還有除數爲0的錯誤提示,這個要注意一下:
好比輸入了一個點,不能繼續輸入點,輸入了一個乘號或者除號不能再繼續輸入另外一個符號:
把Qt文件夾下的如圖所示的bin添加到Path環境變量,
使用windeployqt打包,首先把程序調成release模式,運行一次,生成release的exe,而後把exe複製到一個單獨的文件夾,再用命令行進入到這個文件夾,運行
windelpoyqt xxx.exe
這個命令把須要的dll複製到當前所在文件夾.
打開Enigma Virtual Box,選擇
第一個選擇release的exe,第二個選擇打包以後的文件夾,而後選擇添加文件,選擇遞歸添加,添加上一步生成的全部文件(夾).
這裏選擇壓縮文件. 而後選擇壓縮等待完成便可.
點擊運行.
大功告成!!
1.github(裏面包含完整可執行的單個exe) 注:因爲使用了lfs上傳大文件,因此clone的時候請使用
git lfs clone
2.碼雲
1.Qt淡入
2.Qt按鍵
3.Qt標題欄
4.事件過濾器
5.Qt鼠標事件
6.Qt延時處理
7.Qt文件讀寫
8.Qt打包成單文件
這個簡單的計算器有很大的改進空間,好比能夠添加各類"數": 正弦函數,餘弦函數,正切函數,反正弦函數,指數函數,對數函數,高階導數,抽象函數,複合函數.內心沒數
等等.另外還能夠改進矩形的按鈕,能夠改爲圓角矩形或者橢圓形. 另外,對於陰影的處理能夠添加淡入淡出效果.
最後就是磨砂.由於win10的是有磨砂效果的,這個做者還不會.... 最後再上幾個圖,看看效果(因爲動圖大小的限制只是簡單的表達式...):
但願大家也有一個屬於本身的計算器!