在分析完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设计还是到具体的代码逻辑都存在很大的问题。再次证明了:尽信书不如无书,尽信码不如无码,自己的码才是真的码。