Kivy と  Python で電卓を作る3：基本部分の作成

## 目次
[Kivy と Python で電卓を作る:概要](https://qiita.com/ShrikeWasHere/items/07c879b4485a612eee33)  
[Kivy と Python で電卓を作る:方針](https://qiita.com/ShrikeWasHere/items/b45d8ebb7ed23600f45c)   
[Kivy と Python で電卓を作る:基本部分の作成]()←イマココ  
[Kivy と Python で電卓を作る:電卓機能の追加]()

## 3. ボタンと表示部分の実装

今回から実装.

* 必要なボタン類の作成
* 数字や記号の入力ができる
* 表示が更新される

今回, 作成するのはここまで.
![image_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3099743/92349258-2883-da09-994f-9501d3ee5062.png)

完成したらこんなふうになります.

### 3-1. ファイル構成

```
Kivy_Calculator  
|- Documents：原稿入れ
|- Layout：レイアウト関連のファイル
|  |- Calculator.kv
|  `- CalculatorMain.py
|- Params：数値の設定ファイル
|  `- CalcParams.py
|- Button.py
`- CalculatorApp.py：電卓メイン App
```
レイアウトと数値の設定は別ファイルに分ける.  
数値を直書き（マジックナンバーね）しないようにすると後々の修正が楽にできる.  

### 3-3. CalculatorApp.py

電卓アプリ起動に使うファイル.  
CalculatorMain.py と分けたのは, 初期起動時に色々設定したい！　となったときに分けた方がシンプルになるため.  
つまり, 今回は分ける意味はそこまででもない.

```python
from kivy.app import App
from kivy.core.window import Window
from kivy.lang import Builder

from Layout.CalculatorMain import CalculatorMain

Window.size = (500, 900)
Builder.load_file('Layout/Calculator.kv')

class CalculatorApp(App):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def build(self):
        return CalculatorMain()

if __name__ == '__main__':
    CalculatorApp().run()
```
### 3-3. Calculator.kv

レイアウトを決定する kivy ファイル.
数字を表示する DisplayArea と ButtonArea を分けてみた. 
分けた方がコードの見た目は良くなるのだが, 別 class となるため実装が少し面倒になる.
```
#:kivy 1.2.0
#:import MainParam Params.CalcParams.MainParam
#:import ButtonAreaParams Params.CalcParams.ButtonAreaParams
#:import NumberButton Button.NumberButton
#:import Operator Params.CalcParams.Operator

<CalculatorMain>:
    canvas.before:
        Color:
            rgba: MainParam.background_color
        Rectangle:
            pos: self.pos
            size: self.size
    CalculatorLayout:
        orientation: 'vertical'
        size_hint: MainParam.calc_size_hint
        DisplayArea:
            id: display_area
            size_hint: MainParam.display_area_size_hint
            Label:
                id: display_num
                text: self.parent.display_num
        ButtonArea:
            id: button_area
            size_hint: MainParam.button_area_size_hint
                

<ButtonArea>:
    padding: ButtonAreaParams.outer_padding
    GridLayout:
        size_hint: ButtonAreaParams.button_upper_area_size_hint
        cols: 4
        rows: 5
        ACButton:
            text: 'AC'
            on_press: root.reset_number()
        PlusMinusButton:
            text: '±'
            on_press: root.reverse_number()
        DeleteButton:
            text: 'del'
            on_press: root.delete_number()
        OperatorButton:
            text: '+'
            on_press: root.set_operator(Operator.PLUS)
        NumberButton:
            text: '7'
            on_press: root.add_number(self.text)
        NumberButton:
            text: '8'
            on_press: root.add_number(self.text)
        NumberButton:
            text: '9'
            on_press: root.add_number(self.text)
        OperatorButton:
            text: '-'
            on_press: root.set_operator(Operator.MINUS)
        NumberButton:
            text: '4'
            on_press: root.add_number(self.text)
        NumberButton:
            text: '5'
            on_press: root.add_number(self.text)
        NumberButton:
            text: '6'
            on_press: root.add_number(self.text)
        OperatorButton:
            text: '×'
            on_press: root.set_operator(Operator.MULTIPLICATION)
        NumberButton:
            text: '1'
            on_press: root.add_number(self.text)
        NumberButton:
            text: '2'
            on_press: root.add_number(self.text)
        NumberButton:
            text: '3'
            on_press: root.add_number(self.text)
        OperatorButton:
            text: '÷'
            on_press: root.set_operator(Operator.DIVISION)
        NumberButton:
            text: '0'
            on_press: root.add_number(self.text)
        DecimalSeparatorButton:
            text: '.'
            on_press: root.add_decimal_separator()
        OperatorButton:
            text: '='
```

### 3-4. CalculatorMain.py

レイアウト関連は, Calculator.kv に記載し, 処理はこっちに書く.

```python
from kivy.logger import Logger
from kivy.properties import StringProperty
from kivy.uix.screenmanager import Screen
from kivy.uix.boxlayout import BoxLayout

from Params.CalcParams import MainParam


class CalculatorMain(Screen):

    pass


class CalculatorLayout(BoxLayout):
    """電卓のメイン部分.
    """
    pass


class DisplayArea(BoxLayout):
    """電卓の表示部分.
    """
    first_num = 0
    second_num = 0
    display_num = StringProperty('')
    is_number_decimal = False
    is_plus = True

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def add_number(self, num):
        """数値を入力する処理.

        Args:
            num (str): 入力された数値
        """
        # 桁数を制限する処理
        if len(self.display_num) < MainParam.max_digits:
            # display_num が 0 で 0 が入力された場合への対処
            if self.display_num == '0' and num != 0:
                self.display_num = num
            else:
                self.display_num += num

    def add_decimal_separator(self):
        """小数点を入力する処理.
        """
        # 小数点が複数入力されないようにする処理
        if not self.is_number_decimal:
            self.display_num += '.'
            self.is_number_decimal = True

    def delete_number(self):
        """数値を削除する処理.
        """
        # 数値が全て削除された時に 0 を表示する処理
        if len(self.display_num) == 2 and self.display_num[0] == '-':
            self.display_num = '0'
        elif len(self.display_num) != 1:
            self.display_num = self.display_num[:-1]
        else:
            self.display_num = '0'

    def reset_number(self):
        """数値を 0 にリセットする処理.
        """
        self.display_num = '0'

    def reverse_number(self):
        """数値のプラスとマイナスを反転する処理.
        """
        if self.is_plus:
            self.is_plus = False
            self.display_num = '-' + self.display_num
        else:
            self.is_plus = True
            self.display_num = self.display_num[1:]

    def set_operator(self, operator):
        """演算子を設定する処理.

        Args:
            operator (Enum): 演算子の種類を示す.
        """
        Logger.info("Operator {} Pressed".format(operator))


class ButtonArea(BoxLayout):
    """電卓の入力・計算ボタン部分.
    """

    def add_number(self, text):
        """数字の入力処理.
        """
        Logger.info("Number Button {} Pressed".format(text))
        self.parent.children[1].add_number(text)

    def add_decimal_separator(self):
        """小数点の追加処理.
        """
        Logger.info("Decimal Separator Button Pressed")
        self.parent.children[1].add_decimal_separator()

    def delete_number(self):
        """削除ボタン.
        """
        Logger.info("Delete Button Pressed")
        self.parent.children[1].delete_number()

    def reset_number(self):
        """数字のリセット処理.
        """
        Logger.info("AC Button Pressed")
        self.parent.children[1].reset_number()

    def reverse_number(self):
        """プラス・マイナスの反転処理.
        """
        Logger.info("Reverse Button Pressed")
        self.parent.children[1].reverse_number()

    def set_operator(self, operator):
        """演算子の設定処理.

        Args:
            operator (Enum): 演算子
        """
        Logger.info("Operator Button Pressed")
        self.parent.children[1].set_operator(operator)
```

```
self.parent.children[1]
```

は, self (ButtonArea) の親 (CalculatorLayout) の2番目の子供 (DisplayArea) という意味.  
この辺りの関係は, [素人がpython(kivy)でブロック崩しを作ってみた話②](https://qiita.com/guraragu0010/items/826bd4ebc8de672ccf99)を参考にした.  
Button.py に各処理を記載しても良いのだが, 入れ子構造が増えるので, self.parent.parent.parent と繋がってダサくなってしまう. 
上手い書き方をご存じの方がいましたらコメントお願いします.

### 3-5. Button.py

Button クラスを継承して数字を入力する NumberButton と 小数点を入力する DecimalSeparatorButton を作る.

```python
from kivy.uix.button import Button


class NumberButton(Button):
    """数字の入力ボタン."""
    pass


class DecimalSeparatorButton(Button):
    """小数点の入力ボタン."""
    pass


class DeleteButton(Button):
    """削除ボタン."""
    pass


class ACButton(Button):
    """電源ボタン."""
    pass


class PlusMinusButton(Button):
    """プラスマイナス反転ボタン."""
    pass


class OperatorButton(Button):
    """演算子を設定するボタン."""
    pass
```

### 3-6. CalcParams.py

設定値を記載しているだけのファイル. フォントサイズや色をまとめて書いておくと, 後々変更する時に一括で変更できて楽.

```python
from kivy.metrics import dp

from enum import Enum


class MainParam:
    # 電卓の全体に適用される設定
    calc_size_hint = (1., 1.)
    display_area_size_hint = (1., 280. / 900)
    button_area_size_hint = (1., 620. / 900)
    background_color = (58. / 255, 64. / 255, 71./ 255, 1.)
    max_digits = 10


class ButtonAreaParams:
    # 電卓のボタンに関する設定
    outer_padding = (dp(20), dp(20), dp(20), dp(20))
    button_upper_area_size_hint = (1., 500. / 620.)
    button_lower_area_size_hint = (1., 120. / 620.)


class Operator(Enum):

    NONE = 0
    PLUS = 1
    MINUS = 2
    MULTIPLICATION = 3
    DIVISION = 4
```

```python
outer_padding = (dp(20), dp(20), dp(20), dp(20))
```
としているのは「20px padding してね！」と書いてもディスプレイの解像度によって padding が 20px にならないことがあるため. 

## 4. 実行結果

実行結果はこんな感じ.
各種数値の入力はいい感じですね.
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3099743/524de54d-b124-d1a5-9ced-3c0e5d4946de.gif" width="500">

ログもイイ感じに出ています.
![image_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3099743/b5713ce0-122a-d1d9-996a-1c7d2f077d1f.png)

## 5. 次回予告

次回はいよいよ, 計算処理と状態遷移を追加してゆきます.
前回：[Kivy と Python で電卓を作る2：設計](https://qiita.com/ShrikeWasHere/items/b45d8ebb7ed23600f45c)
次回：