Kuina-chan

くいなちゃん2019年11月22日


プログラミング言語Kuin」の基本構文を学ぶチュートリアル6です。 今回は、電卓アプリを作ります。

1ウインドウの配置

1.1ウインドウの配置



今回は、前回と同じように、視覚的に配置する機能を使ってウインドウを作ってみます。
メニューの「編集」の「新しいファイルを追加」をクリックして、「ウインドウ」を選び、ファイル名を「wnd_calc」として追加してください(図1-1)。
ウインドウの追加
図1-1: ウインドウの追加
前回と同様の操作で、ウインドウを作ります(図1-2)。
ウインドウの配置
図1-2: ウインドウの配置
「Edit」コントロールを1個、「Btn」コントロールを16個配置し、位置を表1-1のように設定しています。
表1-1: コントロールの位置
コントロール名 位置
wndMain 0 × 0 - 195 × 220
edit1 10 × 10 - 175 × 19
btn1 10 × 35 - 40 × 40
btn2 55 × 35 - 40 × 40
btn3 100 × 35 - 40 × 40
btn4 145 × 35 - 40 × 40
btn5 10 × 80 - 40 × 40
btn6 55 × 80 - 40 × 40
btn7 100 × 80 - 40 × 40
btn8 145 × 80 - 40 × 40
btn9 10 × 125 - 40 × 40
btn10 55 × 125 - 40 × 40
btn11 100 × 125 - 40 × 40
btn12 145 × 125 - 40 × 40
btn13 10 × 170 - 40 × 40
btn14 55 × 170 - 40 × 40
btn15 100 × 170 - 40 × 40
btn16 145 × 170 - 40 × 40
多いですが、規則的です。 「Btn」を1列だけ配置してから、それを4回コピー&ペーストして4行並べると早く配置できます。
配置できたら、「Btn」を1つずつ選択して「プロパティの編集...」ボタンを押し、「text」の項目に表1-2のテキストを設定します。
表1-2: ボタンのテキスト
コントロール名 「text」の値
btn1 7
btn2 8
btn3 9
btn4 /
btn5 4
btn6 5
btn7 6
btn8 *
btn9 1
btn10 2
btn11 3
btn12 -
btn13 0
btn14 .
btn15 =
btn16 +
以上で見た目が出来上がりました。 Kuinエディタ上部の「コードを表示」を押すと、自動生成されたプログラムが確認できます。 「show」と「close」という関数が生成されています。 そこで、Kuinエディタの右にあるファイル一覧から「\main」を選び、今作ったウインドウを表示させるプログラムを書きます(図1-3)。
  1. func main()
  2.   do \wnd_calc@show()
  3.   while(wnd@act())
  4.   end while
  5.   do \wnd_calc@close()
  6. end func
図1-3: calc1.kn
2行目と5行目で、自動生成された「\wnd_calc@show()」と「\wnd_calc@close()」を呼んでいます。 実行すると、電卓らしい見た目のウインドウが表示されました(図1-4)。
実行結果1
図1-4: 実行結果1

1.2ボタンの押下イベント



今は実行中にボタンを押しても何も起こりませんので、ボタンが押されたら「Edit」コントロールに文字が入力されるようにします。
Kuinエディタ右のファイル一覧から「\wnd_calc」を選び、「デザイナを表示」を押して配置画面に戻します。 Kuinエディタ左のコントロール一覧から「btn15」と「edit1」以外を選びます(図1-5)。
コントロールの選択
図1-5: コントロールの選択
「Shift」キーを押しながらクリックするとまとめて選択でき、「Ctrl」キーを押しながらクリックすると1つずつ選択状態が切り替えられますので、活用すると早く選択できます。
このまま「プロパティの編集...」を押し、「onPush」の項目に「\main@btnOnPush」と書き込みます。 「onPush」にはボタンが押されたときに呼び出す関数が設定でき、今回は「\main」ファイルの「btnOnPush」関数を呼び出すようにしています。
設定できたらファイル一覧から「\main」を選び、図1-6のプログラムを追加します。
  1. func main()
  2.   do \wnd_calc@show()
  3.   while(wnd@act())
  4.   end while
  5.   do \wnd_calc@close()
  6. end func
  7.  
  8. +func btnOnPush(btn: wnd@Btn)
  9.   do \wnd_calc@edit1.setText(\wnd_calc@edit1.getText() ~ btn.getText())
  10. end func
図1-6: calc2.kn
ボタンが押されると、8から10行目のbtnOnPush関数が呼ばれます。 そこでbtnOnPush関数の行頭に「+」を書いて、他のソースファイルから呼び出せるようにしておきます。
btnOnPush関数の引数「btn」には、押されたボタンのインスタンスが入っており、この「btn」に設定されているテキストを、9行目で「edit1」のテキストに追加しています。
実行すると図1-7のように、ボタンを押すと数式が入力できます。
実行結果2
図1-7: 実行結果2

2電卓の計算

2.1逆ポーランド記法



ここからは、「=」のボタンを押したときの計算処理に取りかかります。
ファイル一覧から「\wnd_calc」を選び、「btn15」を選んで「プロパティの編集...」を押し、「onPush」の項目に「\main@btnEqualOnPush」と書きます。
ファイル一覧から「\main」を選び、長いですが図2-1を追加します。
  1. const excptCalcErr: int :: 0x0001
  2.  
  3. func main()
  4.   do \wnd_calc@show()
  5.   while(wnd@act())
  6.   end while
  7.   do \wnd_calc@close()
  8. end func
  9.  
  10. +func btnOnPush(btn: wnd@Btn)
  11.   do \wnd_calc@edit1.setText(\wnd_calc@edit1.getText() ~ btn.getText())
  12. end func
  13.  
  14. +func btnEqualOnPush(btn: wnd@Btn)
  15.   try
  16.     var formula: [][]char :: @parse(\wnd_calc@edit1.getText())
  17.     do \wnd_calc@edit1.setText(@calc(formula))
  18.   catch
  19.     do \wnd_calc@edit1.setText("Error")
  20.   end try
  21. end func
  22.  
  23. func parse(formula: []char): [][]char
  24.   var output: list<[]char> :: #list<[]char>
  25.   var tmp: stack<char> :: #stack<char>
  26.   var numBuf: []char :: ""
  27.   for i(0, ^formula - 1)
  28.     var c: char :: formula[i]
  29.     if('0' <= c & c <= '9' | c = '.')
  30.       do numBuf :~ c.toStr()
  31.     else
  32.       if(numBuf <> "")
  33.         do output.add(numBuf)
  34.         do numBuf :: ""
  35.       end if
  36.       while(^tmp > 0 & priority(tmp.peek()) >= priority(c))
  37.         do output.add(tmp.get().toStr())
  38.       end while
  39.       do tmp.add(c)
  40.     end if
  41.   end for
  42.   if(numBuf <> "")
  43.     do output.add(numBuf)
  44.     do numBuf :: ""
  45.   end if
  46.   while(^tmp > 0)
  47.     do output.add(tmp.get().toStr())
  48.   end while
  49.   ret output.toArray()
  50.  
  51.   func priority(c: char): int
  52.     switch(c)
  53.     case '+', '-'
  54.       ret 10
  55.     case '*', '/'
  56.       ret 20
  57.     default
  58.       throw @excptCalcErr
  59.     end switch
  60.   end func
  61. end func
  62.  
  63. func calc(formula: [][]char): []char
  64.   ret formula.join(" ")
  65. end func
図2-1: calc3.kn
14から21行目までのbtnEqualOnPush関数が、「=」ボタンが押されたときの処理です。
15から20行目までの「try」「catch」「end try」は、例外(エラー)を処理する構文です。 try文では、「try」から「catch」までが順番に処理されていき、その途中で例外が発生すると「catch」から「end try」までが実行されます。 例外が発生しなければ「catch」から「end try」まではスキップされます。
try内の16、17行目では、数式を「@parse」して「@calc」したものをEditコントロールに表示しています。 この@parse関数で数式を解析し、@calc関数で実際に計算する予定です。 途中で例外が発生すると、19行目で「Error」と表示されます。
23から61行目が@parseの中身で、このあと詳しく説明しますが、数式を解析しています。 63から65行目の@calcでは、解析した数式を計算する予定ですが、今は単に「join」メソッドにより、スペース区切りで文字列を連結しているだけです。
ちなみに1行目では、発生させる例外のコード「0x0001」を「excptCalcErr」という名前で別名定義しています。 0x0001と直接書いても良いですが、プログラムが読みにくくなるため、このように「const」文で別名を定義すると便利です。

2.2数式の解析



それでは@parse関数の中身を説明します。
例えば「2-3*4*5」という数式の場合、足し算より掛け算を先に計算するルールがあるため、最初に「3*4」を計算しなければなりません。 これは複雑なため、計算の順番を考慮して数式を並べた「ぎゃくポーランド記法きほう」という表記にいったん変換することを考えます。
「2-3*4*5」を逆ポーランド記法で書くと、「2 3 4 * 5 * -」となります(図2-2)。
逆ポーランド記法
図2-2: 逆ポーランド記法
つまり、計算の順番を表した木構造を作り、その木構造をたどりながら折り返すときにノードを拾っていったものが逆ポーランド記法になります。 逆ポーランド記法になっていれば、先頭から順番に読んでいって、「+」や「*」などの演算子が現れるたびに計算すれば正しい結果が得られるため便利です。
今回は数式から逆ポーランド記法に変換する方法として、「操車場そうしゃじょうアルゴリズム」という手法を使っています。 操車場アルゴリズムとは、演算子をスタックに溜めていき、それらよりも優先順位が低い演算子が現れたらスタックから取り出して出力する手法です。
ではもう一度@parse関数の中身を再掲します(図2-3)。
  1. func parse(formula: []char): [][]char
  2.   var output: list<[]char> :: #list<[]char>
  3.   var tmp: stack<char> :: #stack<char>
  4.   var numBuf: []char :: ""
  5.   for i(0, ^formula - 1)
  6.     var c: char :: formula[i]
  7.     if('0' <= c & c <= '9' | c = '.')
  8.       do numBuf :~ c.toStr()
  9.     else
  10.       if(numBuf <> "")
  11.         do output.add(numBuf)
  12.         do numBuf :: ""
  13.       end if
  14.       while(^tmp > 0 & priority(tmp.peek()) >= priority(c))
  15.         do output.add(tmp.get().toStr())
  16.       end while
  17.       do tmp.add(c)
  18.     end if
  19.   end for
  20.   if(numBuf <> "")
  21.     do output.add(numBuf)
  22.     do numBuf :: ""
  23.   end if
  24.   while(^tmp > 0)
  25.     do output.add(tmp.get().toStr())
  26.   end while
  27.   ret output.toArray()
  28.  
  29.   func priority(c: char): int
  30.     switch(c)
  31.     case '+', '-'
  32.       ret 10
  33.     case '*', '/'
  34.       ret 20
  35.     default
  36.       throw @excptCalcErr
  37.     end switch
  38.   end func
  39. end func
図2-3: calc4.kn
2、3行目で出力用のリスト「output」と、演算子を一時的に格納するスタック「tmp」を作成しています。 4行目のnumBufは、数値を格納するバッファです。
5から19行目で数式を1文字ずつ解析しています。 数値の場合は、例えば「12」という数が「1」と「2」に分かれないように、numBufにいったん溜めてから11行目や21行目でoutputに出力します。 演算子の場合は29行目の「priority」関数で優先順位を求めてから、スタック内よりも低い優先順位の演算子が現れたらスタックから取り出して、15行目や25行目で出力しています。 処理が終わったら27行目で、出力リストを配列に変換して返しています。
実行して「2-3*4*5」と入力して「=」ボタンを押すと、逆ポーランド記法に変換された「2 3 4 * 5 * -」が出力されます(図2-4)。
実行結果3
図2-4: 実行結果3

2.3数式の計算



最後に、得られた逆ポーランド記法の数式を、計算しましょう。 @calc関数の中身を図2-5のように書き換えます。
  1. func calc(formula: [][]char): []char
  2.   var nums: stack<num@BigFloat> :: #stack<num@BigFloat>
  3.   for i(0, ^formula - 1)
  4.     switch(formula[i])
  5.     case "+"
  6.       var a: num@BigFloat :: nums.get()
  7.       var b: num@BigFloat :: nums.get()
  8.       do b.add(a)
  9.       do nums.add(b)
  10.     case "-"
  11.       var a: num@BigFloat :: nums.get()
  12.       var b: num@BigFloat :: nums.get()
  13.       do b.sub(a)
  14.       do nums.add(b)
  15.     case "*"
  16.       var a: num@BigFloat :: nums.get()
  17.       var b: num@BigFloat :: nums.get()
  18.       do b.mul(a)
  19.       do nums.add(b)
  20.     case "/"
  21.       var a: num@BigFloat :: nums.get()
  22.       var b: num@BigFloat :: nums.get()
  23.       do b.div(a)
  24.       do nums.add(b)
  25.     default
  26.       var num: num@BigFloat :: num@makeBigFloat()
  27.       do num.fromStr(formula[i])
  28.       do nums.add(num)
  29.     end switch
  30.   end for
  31.   if(^nums <> 1)
  32.     throw @excptCalcErr
  33.   end if
  34.   ret nums.get().toStr()
  35. end func
図2-5: calc5.kn
float型で計算しても良いですが、電卓は計算精度が高いほうが望ましいため、100桁の小数が扱える「num@BigFloat」型を使います。
2行目でこの「num@BigFloat」型の値が入るスタック「nums」を作成し、3から30行目までで逆ポーランド記法の数式を順番に解釈しています。
数値が来たら25から28行目のように「nums」のスタックに追加します。 「+」「-」「*」「/」が来た場合は5から24行目のように、numsから2つ値を取り出して各演算を行い、結果をnumsに戻します。 これを繰り返すと、最後に計算結果がnumsに残ります。
処理が終わったら34行目で、numsに残った計算結果を返しています。 実行して「2-3*4*5」と入力し「=」を押すと、正しく「-58」という結果が表示されました(図2-6)。
実行結果4
図2-6: 実行結果4
以上で電卓アプリの完成です。 次回は、3Dゲームを作ります!
1574406393jaf