在分析完main.cpp和calqlatr.qml之後,就到了計算器的具體邏輯部分,即當用戶在UI上點擊數字或操作符時,是如何存儲數據、計算結果以及顯示在UI上?本篇通過分析calculator.js來詳細展開。
入口函數
用戶在UI上只有兩種操作,分別是:點擊數字和點擊運算符,產生的操作就會觸發下述處理:
function operatorPressed(operator) { CalcEngine.operatorPressed(operator) }
function digitPressed(digit) { CalcEngine.digitPressed(digit) }
而operatorPressed(operator)和digitPressed(digit)函數實際上是在Button.qml中進行觸發的:
onClicked: {
if (operator)
window.operatorPressed(parent.text)
else
window.digitPressed(parent.text)
}
至於一個Button具體是不是operator則是在NumberPad.qml中創建Button的時候設置的標記:
...
Button { text: "2" }
Button { text: "3" }
Button { text: "0" }
Button { text: "±"; color: "#6da43d"; operator: true }
Button { text: "−"; color: "#6da43d"; operator: true }
Button { text: "+"; color: "#6da43d"; operator: true }
...
這樣整體就串起來了:
- 在NumberPad.qml中創建所有的Button,並根據實際需要設置Button的operator屬性值
- 當Button被鼠標點擊時,根據operator屬性值來判斷是調用window.operatorPressed(parent.text)函數還是window.digitPressed(parent.text)函數,並傳入當前Button的text
- window的operatorPressed(operator)和digitPressed(digit)函數沒有做任何特殊處理,直接透傳調用了CalcEngine(即calculator.js)響應的函數
disabled(op)函數
打開calculator.js文件,第一個函數就是disabled(op),而且在digitPressed(op)和operatorPressed(op)中都有調用到該函數。下面先分析一下這個函數的作用:
function disabled(op) {
if (op == "." && digits.toString().search(/\./) != -1) {
return true
} else if (op == window.squareRoot && digits.toString().search(/-/) != -1) {
return true
} else {
return false
}
}
上面的代碼else if部分是有問題的,正確的應該是:
} else if (op == "√" && digits.toString().search(/-/) != -1) {
原因在於,在上一章講到,UI中顯示的操作符(即Button的text字段)是一個UTF-8編碼的特殊字符,因此這裏判斷的時候也需要使用該特殊字符進行判斷。
disabled(op)函數的具體作用是:
- 如果當前字符是“.”,並且當前的輸入中已經包含一個"."了,那麼肯定是不允許在輸入字符"."了
- 如果當前字符是"√",並且當前的數值是負數(即第一個字符是"-"),那麼肯定是允許進行開方運算的
- 其它的都屬於正常情況,由operatorPressed(operator)和digitPressed(digit)函數分別進行處理
digitPressed(op)函數
每次點擊數字時就會觸發digitPressed(op)函數,具體的處理如下:
function digitPressed(op)
{
if (disabled(op))
return
if (digits.toString().length >= 14)
return
if (lastOp.toString().length == 1 && ((lastOp >= "0" && lastOp <= "9") || lastOp == ".") ) {
digits = digits + op.toString()
display.appendDigit(op.toString())
} else {
digits = op
display.appendDigit(op.toString())
}
lastOp = op
}
其中disabled(op)調用已在上一節中描述,如果是一個非法的數字點擊(多個.或對負數開方),則直接返回。下一個判斷是,如果是輸入的字符長度大於14個時,則不允許繼續輸入。
下面的一個if else判斷是針對首次輸入和非首次輸入的處理:
if (lastOp.toString().length == 1 && ((lastOp >= "0" && lastOp <= "9") || lastOp == ".") ) {
//非首次輸入,因爲lastOp.toString().length == 1
//這裏判斷是lastOp是否合法,實際上是有意義,主要作用是判斷上一個字符是數字還是操作符,如果是操作符,則也需要走else部分,重新給digits賦值
digits = digits + op.toString()
display.appendDigit(op.toString())
} else {
//首次輸入,因爲lastOp的初始長度爲0
//將op賦值給digits,並且將op顯示在輸出結果上
digits = op
display.appendDigit(op.toString())
}
不過,上面判斷缺少了下面一個針對op的有效性判斷(畢竟在輸入的數字區域也佈局了一個text爲" "的Button):
if (!((op >= "0" && op <= "9") || op == ".") )
return
當然,處理完digit後,記得更新lastOp:
lastOp = op
operatorPressed(op)函數
首先,需要去除代碼中無用的部分,如對操作符:"1/x"、"x^2"、"Abs"、"Int"、"mc"、"m+"、"mr"、"m-"、window.leftArrow、"Off"和"AC"這些根本在UI上不存在的操作符處理部分。經過裁剪後的代碼如下:
function operatorPressed(op)
{
if (disabled(op))
return
lastOp = op
if (previousOperator == "+") {
digits = Number(digits.valueOf()) + Number(curVal.valueOf())
} else if (previousOperator == "−") {
digits = Number(curVal) - Number(digits.valueOf())
} else if (previousOperator == "×") {
digits = Number(curVal) * Number(digits.valueOf())
} else if (previousOperator == "÷") {
digits = Number(curVal) / Number(digits.valueOf())
} else if (previousOperator == "=") {
}
if (op == "+" || op == "−" || op == "×" || op == "÷") {
previousOperator = op
curVal = digits.valueOf()
display.displayOperator(previousOperator)
return
}
if (op == "=") {
display.newLine("=", digits.toString())
}
curVal = 0
previousOperator = ""
if (op == "±") {
digits = (digits.valueOf() * -1).toString()
} else if (op == "√") {
digits = (Math.sqrt(digits.valueOf())).toString()
} else if (op == "C") {
display.clear()
}
}
我們將上面的代碼拆分一下,逐塊進行分析。
第一部分
if (disabled(op))
return
lastOp = op
在operatorPressed(op)函數開始同樣要判斷disable(op),根據上面對disable(op)函數的分析,這裏主要是判斷是否是對負數進行開方操作。
如果操作符合法,則在處理下面之前先將lastOp更新爲當前的操作符。這裏的修改主要是影響到digitPressed(op)函數中對lastOp的判斷,從而確認是一個新的數字串輸入。
第二部分
if (previousOperator == "+") {
digits = Number(digits.valueOf()) + Number(curVal.valueOf())
} else if (previousOperator == "−") {
digits = Number(curVal) - Number(digits.valueOf())
} else if (previousOperator == "×") {
digits = Number(curVal) * Number(digits.valueOf())
} else if (previousOperator == "÷") {
digits = Number(curVal) / Number(digits.valueOf())
} else if (previousOperator == "=") {
}
注意,這裏進行判斷的是previousOperator,即當輸入"+−×÷"時,需要等到下一個運算符時,上一個運算符的結果纔會計算出來。如2+3=5,實際是輸入"="時,纔會計算出2+3的值
第三部分
if (op == "+" || op == "−" || op == "×" || op == "÷") {
previousOperator = op
curVal = digits.valueOf()
display.displayOperator(previousOperator)
return
}
結合上面第二部分的分析,我們可以看到,當輸入"+−×÷"時,實際上是把op保存到previousOperator,使用curVal記錄下輸入"+−×÷"之前的digits值,並顯示操作符到界面上,然後return就OK了。
第四部分
if (op == "=") {
display.newLine("=", digits.toString())
}
curVal = 0
previousOperator = ""
當輸入=號時,因爲在第二部分的時候已經計算出來結果,所以此處只需要顯示”=“操作符和計算結果即可。並且計算完畢後,需要清空curVal和previousOperato(此處的清空處理很有必要,主要原因是在如果不清空,那麼在輸入操作符時,由於curVal和previousOperato均有值,則會再次執行一遍第二部分的處理,導致結果出錯)。
第五部分
if (op == "±") {
digits = (digits.valueOf() * -1).toString()
} else if (op == "√") {
digits = (Math.sqrt(digits.valueOf())).toString()
} else if (op == "C") {
display.clear()
}
最後處理就是針對幾個特殊的操作符,如"±"操作符相當於*(-1),而"√"的意思是開平方計算。而"C"操作符表示是清空輸出結果。
注意,雖然UI上沒有"AC"操作符,但是需要注意的是"AC"和"C"操作符的區別是:
else if (op == "AC") {
curVal = 0
memory = 0
lastOp = ""
digits ="0"
}
即,"C"操作符只是清空了輸出結果,但是緩存的數據並沒有清掉,仍可以繼續計算;而"AC"操作符則是清掉了所有的緩存數據,需要一切開始運算。
這裏有一個Bug,即當沒有輸入任何操作符時,是可以清空輸入結果的,但是一旦輸入了"="操作符,再按"C"就沒有反應了。
Display的操作
在calculator.js中共調用到了Display的四個函數,分別是:
- display.appendDigit(op.toString())
- display.displayOperator(previousOperator)
- display.newLine("=", digits.toString())
- display.clear()
appendDigit(digit)函數
function appendDigit(digit)
{
if (!enteringDigits)
listView.model.append({ "operator": "", "operand": "" })
var i = listView.model.count - 1;
listView.model.get(i).operand = listView.model.get(i).operand + digit;
enteringDigits = true
}
enteringDigits默認值是false,有兩處將其置爲false,分別是newLine(operator, operand)函數和clear()函數;同樣也有兩處將其置爲true,分別是displayOperator(operator)函數和appendDigit(digit)函數。
這裏的判斷的意思是如果enteringDigits爲false則添加一個新行,那麼什麼情況下才是false呢?默認值的情況肯定不用講,第一次輸入肯定是要添加新行的。newLine(operator, operand)函數和clear()函數之後需要添加新行,講到這兩個函數的時候會詳細講解。
剩下的動作就是將當前輸入的數字添加到其operand字段,並置enteringDigits爲true(即下一個數字輸入時無需創建新行)。
displayOperator(operator)函數
function displayOperator(operator)
{
listView.model.append({ "operator": operator, "operand": "" })
enteringDigits = true
}
首先需要注意的是,在calculator.js文件中只有當處理"+−×÷"操作符時纔會調用到該函數。
那麼這個函數就很好理解了,因爲"+−×÷"都是雙目運算符,當輸入這些運算符後,只需要(另起一行)顯示運算符,然後等待用戶輸入下一個操作數即可。而在我們從小在本子上進行演算時採用的就是運算符和第二個操作數在一行上,因此這裏就把enteringDigits置爲了true,意味着下一個數字輸入時無需在重新開始一行。
newLine(operator, operand)函數
function newLine(operator, operand)
{
listView.model.append({ "operator": operator, "operand": operand })
enteringDigits = false
listView.positionViewAtEnd()
}
同樣需要注意的是,在calculator.js文件中有當處理"="操作符時纔會調用到該函數。
而在調用時已經得到了計算結果,所以此處只需要另起一行顯示"="和運算結果,而且由於本次計算過程完畢,下一個數字輸入時需要在新行輸入,所以置enteringDigits爲false。
最後調用的positionViewAtEnd()函數其作用是讓listView重新佈局一下,其作用是當listView的行數過長時,顯示最後的幾行以保證結果能夠正常顯示。實際上這個操作應該放到每一個需要創建新行時,即在listView.model.append()之後調用。
clear()函數
function clear()
{
if (enteringDigits) {
var i = listView.model.count - 1
if (i >= 0)
listView.model.remove(i)
enteringDigits = false
}
}
這一段的代碼只是處理了當輸入狀態爲數字時(即連續輸入數字和輸入"+−×÷"操作符後),此時按"C"運算符纔會清除掉最後一項,但是實際上這個處理是有很多問題的。其意義完全沒有搞清楚,因此寫的代碼也完全不合邏輯。
而參考Windows自帶的計算器,上面有"CE"和"C"兩個清除鍵,其作用分別是(來自於百度知道上hangling1981的回答《計算器上的C和CE的區別》
- C的功能是將之前所輸入的數字、計算結果以及儲存等信息全部歸零,相當於把一塊寫滿字的黑板一下子擦乾淨了,不留任何痕跡。
- CE的功能是清除當前輸入的數據或符號,例如:想要計算14*2,不小心輸入14*3,這時發現輸錯了。此時按CE鍵,將3清除,再輸入2,即可計算出結果。
總結
這一個示例是我分析的第一個在qml中調用js的程序,而這種將js和qml以及c++代碼結合起來的形式非常有新意,一下子將GUI編程的大門對JS開發人員敞開了。但是整個程序仍然需要編譯後運行而不能做到動態加載和執行(如果是的話那不就是瀏覽器了嗎?)
不過通過上面的分析也看出來這個示例程序完全不是一個好的產品,無論從UI設計還是到具體的代碼邏輯都存在很大的問題。再次證明了:盡信書不如無書,盡信碼不如無碼,自己的碼纔是真的碼。