一· 介紹
- 目的: 做一個標准型的計算器。用於學習Qt基礎學習。
- 平台: Qt 5.12.0
二· 結構框架設計
2.1最終產品樣式
界面的設計大體按照win系統自帶的計算器做模仿。左邊是win7 的界面。本次項目是為了熟悉Qt的界面設計,取消掉一些復雜的計算,和一些比較花樣的布局。最終實現右邊圖樣的界面。
2.2 架構規則
- 弱耦合,強內聚,業務邏輯與界面邏輯一定要分離
- 模塊之間使用接口進行關聯
- 模塊之間的關系必然是 單項依賴 ,最大力度避免循環依賴關系
2.3 框架結構
計算器一定包含界面類和 后台計算類。按照上述規則,設計如下:
-
一個界面類 IOCalculator;
-
一個后台計算的類 QCalculatorDec;
-
一個接口類,用來連接 界面類和后台業務計算類 QcalculatorUI;
-
一個 完整的計算機類 Qcalculator;(實現上述三個類的融合,對外就是一個計算器類)
三· 接口類的設計
接口類主要是實現界面類與業務邏輯的通信。最主要的有兩個內容
- 從界面類中得到 用戶輸入的計算公式。
- 由計算公式得到計算結果。
接口類只提供接口,UI 類使用該接口。業務邏輯類 負責具體的實現。
類的結構為:
class IOCalculator
{
public:
virtual bool expression(const QString& exp )=0;
virtual QString result()=0;
virtual ~IOCalculator() ;
};
四· 界面類的設計
4.1 界面布局設計
直接離別別人設計好的
從圖中可以看到 界面類用到的元素
- 一個窗口界面 (QWidget)
- 一個顯示欄(QLineEdit)
- 20個按鍵(QPushButton)
4.2 代碼設計
4.21 類的設計
UI類除了要實現界面,還用到接口。類的結構如下:
class QcalculatorUI : public QWidget
{
Q_OBJECT
private:
QPushButton * m_buttons[20];
QLineEdit* m_ledit;
IOCalculator* m_cal;
QcalculatorUI();
bool construct();
private slots:
void getEquationFoUser();
public:
static QcalculatorUI* NewInstance();
void show();
void importInterface(IOCalculator* cal );
~QcalculatorUI();
};
為了防止半成品的對象的生成,該類使用二階構造方式
4.22 具體實現
QcalculatorUI::QcalculatorUI() :QWidget(nullptr, Qt :: WindowMinimizeButtonHint | Qt:: WindowCloseButtonHint )
{
m_cal =nullptr;
}
void QcalculatorUI::getEquationFoUser()
{
QPushButton* binsig= dynamic_cast<QPushButton *>( sender());
if(binsig != nullptr)
{
QString gets=binsig->text();
if( "C"==gets )
{
m_ledit->setText("");
}
else if("<-"==gets )
{
QString ls = m_ledit->text();
if(ls.length()>0)
{
ls.remove(ls.length()-1,1);
}
m_ledit->setText(ls);
}
else if("="==gets)
{
if(m_cal != nullptr)
{
m_cal->expression(m_ledit->text());
m_ledit->setText(m_cal->result());
}
}
else
{
m_ledit->setText( m_ledit->text()+gets );
}
}
}
bool QcalculatorUI:: construct()
{
bool ret =true;
const char* btnText[20] =
{
"7", "8", "9", "+", "(",
"4", "5", "6", "-", ")",
"1", "2", "3", "*", "<-",
"0", ".", "=", "/", "C",
};
m_ledit = new QLineEdit( this);
if(m_ledit != nullptr)
{
m_ledit->move(10,10 );
m_ledit-> resize(240,30);
m_ledit->setReadOnly(true);
m_ledit->setAlignment(Qt::AlignRight);
}
else
{
ret =false;
}
for(int i=0;i< 5;i++)
for(int j =0;j<4;j++)
{
m_buttons[j*5 +i] = new QPushButton(this);
if(m_buttons[j*5+i] != nullptr)
{
m_buttons[j*5 +i]->setText (btnText[ j*5+i]);
m_buttons[j*5 +i]->move(10+i*50,50+j*50);
m_buttons[j*5+i]->resize(40,40);
connect(m_buttons[j*5+i] ,SIGNAL(clicked()),this ,SLOT(getEquationFoUser()));
}
else
{
ret = false;
continue;
}
}
return ret;
}
QcalculatorUI* QcalculatorUI:: NewInstance()
{
QcalculatorUI* ret = new QcalculatorUI() ;
if(ret == nullptr || ! ret->construct() )
{
delete ret;
ret =nullptr;
}
return ret;
}
void QcalculatorUI :: show()
{
QWidget ::show();
setFixedSize(this->width(),this->height());
}
void QcalculatorUI ::importInterface(IOCalculator* cal )
{
m_cal=cal;
}
4.23 主要代碼解釋
1 二結構造類
為了防止半成品的對象生成,所以使用二階構造模式。詳細請參考。
2 接口函數的使用
m_cal->expression(m_ledit->text()); 和 m_ledit->setText(m_cal->result());使用 接口對象,實現界面類和后台運算對象的交互。
五· 后台計算類
后台類主要實現計算器的計算,實現接口。
5.1 計算器算法
計算器的計算方式和我們人類的計算方式大不相同。計算機只會按照一條條指令來執行,人的那種整體性的去認知和分析在計算機世界里是很難完成。所以要按照計算機的思路設計算法。
5.11 元素分離
該類從UI界面類得到的表達式,對於計算器(准確的說是 C++編譯器)他只是一個字符串。想把該字符解釋成(當做)一個表達式。則該表達式中的元素有 四則遠算,數字,和正負號,還有括號符,每個元素根據前后的元素不同,就有不同的意義。例如 一個“+”元素,如果該元素前邊是一個數字,后邊是個數字,那該元素是加號,如果前邊元素是個符號,后邊是個數字元素,則是個正號。所以簡單的把字符串拆成每個字符是毫無意義。需要根據字符的前后特性,識別出每個表達式的元素,才能有后邊的計算的可能。所以先實現 元素分離。
隨便找個表達式:“1/2*(-2-1.123)+1”來分析。不看看出,如果以 符號為標志對每個字符分析,就能實現分離。
-
數字元素識別:
- 由於數字元素是多個字符組成。則要用字符串來存儲。
- 當前字符是數字字符或者小數點字符,則追加到到數字元素后邊。
-
符號元素的識別:
-
“*”,“/”,“(”, “)” 這些元素很好識別。直接可以識別出來。
-
如果遇到 “+”和“-”,如果① 該元素前是個符號且后邊為數字。②該元素位於當前表達式的首位。則該元素為 后邊的數字元素的正負號,存到下一個數字字符串中即可。
偽碼為:
// 用個對列分離出來的元素存起 exp= 表達式字符串 num 一個字符串的累加器 for(int i=0;i<exp.length();<i++) { if(exp[i] == 數字 || exp[i] == 小數點 ) num +=exp[i]; //追加 else { //到這里,前邊的數字元素已經結束 保存num; if( exp[i] == “*”,“/”,“(”, “)” ) 保存該元素; else if( exp[i] == “+” “-” ) { if( ( i>0 && exp[i-1] == 字符 ) || (i==0)) { 存入數字字符串; } else { error; } } } else { error; } }
-
5.22 中綴表達式轉后綴表達式
已經有了可以定性(知道該元素在表達式里代表什么意義)的元素,接下來就可以根據元素來做計算了。分析當前處境所遇到的問題是:計算機只能順序的執行指令,面對改變順序執行的 優先級的運算規則無法處理。則需要
-
四則運算符需要得到該運算符的優先級
-
用后綴表達式代替中綴表達式來解決 括號帶來的運算順序的改變問題。
-
當前元素為 左括號 : 入棧
-
當前元素為 右括號 :彈出棧的元素到出現 左括號並把左括號也彈出
-
當前元素為數字:輸出
-
當前元素為運算符:
1) 與棧頂元素比較優先級
2) 大於棧頂元素,入棧
3) 小於等於則把棧頂元素彈出,一直到大於棧頂元素,入棧
偽碼為:
// 中綴轉后綴,把轉好的元素按規則存到一個隊列里 //exp是個隊列,存放已經做了分離的元素 while(exp != 空) { QString s=exp.dequeue(); if(s==數字 ) { 放入 out隊列; } else if(s==運算符) { while(getPriority( s) <= getPriority( stack.top() ) ) stack.pop() 放入到out隊列; } else if(s== "(") 入棧 else if(s == ")") { while( "(" != stack.top() && ( stack.length()>0 ) ) { stack.pop() 放入到out隊列; } if(stack.length()>0) { //"("已經不需要了。丟到 stack.pop(); } else { // 說明棧里沒有 "(",表達式有誤 error; } } else { error; } }
-
5.23 后綴表達式的計算
經過前邊兩步的處理,用后綴表達式計算 該表達已經萬事俱備了。后綴表達式的計算規則為:
- 當前元素為 數字:入棧
- 當前元素為 運算符:
- 從棧中彈出 右操作數
- 從棧中彈出 左操作數
- 根據運算符,進行計算
- 把計算結果壓棧
5.2 后台計算類設計
前邊的5.1 章節做了計算器的算法處理,這只是該類的內部功能的實現。根據項目的整體結構設計。該類要是對接口的函數做具體的實現。則代碼如下:(私有函數是在具體代碼實現是才能確定,主要脈絡是對外的函數)
class QCalculatorDec :public IOCalculator
{
private:
QString m_exp;
QString m_result;
bool isNumberOrDec(QChar c );
bool isSymbol(QChar c);
bool isOperator(QString c);
bool isSign(QChar c);
bool isNumber(QString s);
int getPriority( QString& s);
QString getResult(const QString& var1,const QString& ver2,const QString& oper );
QQueue<QString> split(const QString& exp);
bool transverter(QQueue<QString>& exp , QQueue<QString>& output );
QString suffixCalculate(QQueue<QString>& exp);
public:
QCalculatorDec();
bool expression(const QString& exp );
QString result();
~QCalculatorDec();
};
5.3 具體實現
QCalculatorDec::QCalculatorDec()
{
}
bool QCalculatorDec:: isNumberOrDec(QChar c )
{
return ((c>='0'&& c<='9') || c=='.');
}
bool QCalculatorDec:: isSymbol(QChar c)
{
return(isOperator(c) ||c=='(' || c==')');
}
bool QCalculatorDec ::isOperator(QString c)
{
return(c =="+" || c =="-" || c =="*" || c =="/");
}
bool QCalculatorDec ::isSign(QChar c)
{
return(c =='+' || c =='-');
}
int QCalculatorDec:: getPriority( QString& s)
{
int ret=0;
if(s=="+" || s=="-")
{
ret =1;
}
else if(s=="*" || s=="/")
{
ret =2;
}
return ret;
}
bool QCalculatorDec:: isNumber(QString s)
{
bool ret =false;
s.toDouble( &ret);
return ret;
}
/* 元素 分離 */
QQueue<QString> QCalculatorDec::split(const QString& exp)
{
QQueue<QString> ret;
QString num;
QString pre;
for(int i=0; i<exp.length();i++)
{
if( isNumberOrDec(exp[i]))
{
num.append(exp[i]);
}
else if(isSymbol(exp[i]))
{
if(!num.isEmpty())
{
ret.enqueue(num);
num.clear();
}
if(isSign(exp[i]) &&( pre=="" || pre=="(" || isOperator( pre) ) )
{
num.append(exp[i]);
}
else //肯定是運算符
{
ret.enqueue(exp[i]);
}
}
pre = exp[i];
}
if(!num.isEmpty())
{
ret.enqueue(num);
num.clear();
}
return ret;
}
/*中綴變后綴*/
bool QCalculatorDec::transverter(QQueue<QString>& exp , QQueue<QString>& output )
{
bool ret= true;
QStack<QString> stack;
stack.clear();
// while(!exp.isEmpty())
for(int i=0;i<exp.length();i++)
{
if( isNumber(exp[i]))
{
output.enqueue(exp[i]);
}
else if(isOperator(exp[i]) )
{
while(!stack.isEmpty() && ( getPriority(exp[i])<= getPriority(stack.top() ) ) )
{
output.enqueue(stack.pop());
}
stack.push(exp[i]);
}
else if(exp[i]== "(")
{
stack.push(exp[i]);
}
else if(exp[i]== ")")
{
while(!stack.isEmpty() && ( "(" != stack.top() ) )
{
output.enqueue(stack.pop());
}
if(!stack.isEmpty() &&( "(" == stack.top()) )
{
stack.pop();
}
else // if ( stack.isEmpty() || ("(" != stack.top() ))
{
ret=false;
}
}
else
{
ret=false;
}
}
while(!stack.isEmpty())
{
if("(" != stack.top())
{
output.enqueue(stack.pop());
}
else
{
ret=false;
break;
}
}
if(! ret)
{
output.clear();
}
return ret;
}
/*單純的計算*/
QString QCalculatorDec::getResult( const QString& var1,const QString& var2,const QString& oper )
{
QString ret;
if(isNumber(var1) && isNumber(var2) )
{
double lp = var1.toDouble();
double rp = var2.toDouble();
if("+" == oper)
{
ret.sprintf("%f", lp + rp);
}
else if("-" == oper)
{
ret.sprintf("%f", lp - rp);
}
else if("*" == oper)
{
ret.sprintf("%f", lp * rp);
}
else if("/" == oper)
{
if(rp<0.000000000001 && rp> -0.000000000001 )
{
ret="error";
}
else
{
ret.sprintf("%f", lp / rp);
}
}
else
{
ret="error";
}
}
else
{
ret="error";
}
return ret;
}
/*后綴表達式的計算*/
QString QCalculatorDec::suffixCalculate(QQueue<QString>& exp)
{
QString ret;
QStack<QString> stack;
stack.clear();
for(int i= 0; i<exp.length();i++)
{
if(isNumber(exp[i]))
{
stack.push(exp[i]);
}
else if(isOperator(exp[i] ) )
{
if(!stack.isEmpty())
{
QString leftVar,rightVar;
rightVar=stack.pop();
if(!stack.isEmpty())
{
leftVar=stack.pop();
ret = getResult(leftVar,rightVar,exp[i]);
if("error"!= ret)
{
stack.push(ret);
}
}
else
{
ret="error";
}
}
else
{
ret="error";
}
}
else
{
ret="error";
}
}
if(1== stack.length())
{
ret =stack.pop();
}
else
{
ret ="error";
}
return ret;
}
/*計算結果*/
bool QCalculatorDec:: expression(const QString& exp )
{
bool ret=false;
m_exp= exp;
qDebug()<<"expression() is start " ;
QQueue<QString> tranin=split(m_exp) ;
QQueue<QString> tranOut;
if( transverter( tranin, tranOut ) )
{
m_result = suffixCalculate(tranOut);
ret = (m_result != "Error");
}
else
{
m_result= "error";
}
return ret;
}
QString QCalculatorDec :: result()
{
return m_result;
}
5..4 主要代碼解釋
1 浮點數與0的比較
在C/C++ 語言中,遇到 浮點數與0 的比較時,由於存儲特性。不能直接做相等比較,要與 很小的數字區間比較。
六· 計算類的實現
為了架構的整潔和使用的方便,把計算機的各個部分用一個類做個整體的封裝。對外只有對象的生產和顯示接口。
6.1 計算類的設計
class Qcalculator
{
private:
QcalculatorUI* m_ui;
QCalculatorDec m_dec;
Qcalculator();
bool construct();
public:
static Qcalculator * NewInstance();
void show();
};
6.2 具體實現
Qcalculator::Qcalculator()
{
}
bool Qcalculator::construct()
{
m_ui= QcalculatorUI::NewInstance();
if(m_ui != nullptr)
{
m_ui->importInterface(&m_dec);
}
return (m_ui != nullptr);
}
Qcalculator * Qcalculator:: NewInstance()
{
Qcalculator* ret = new Qcalculator;
if(ret ==nullptr || ! ret->construct())
{
delete ret;
ret = nullptr;
}
return ret;
}
void Qcalculator :: show()
{
m_ui->show();
}
6.3 主要代碼解釋
1各個模塊實現組合