3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

こどものために計算ドリルを作ってみたpython編 ②Tkinterとreportlabを用いてPDF作成

Last updated at Posted at 2024-09-29

お子様がいるプログラマーのみなさん

はい。では前回作成した関数を使って計算プリントのPDFファイルをバシバシ出力していきましょう。

Tkinterとreportlabを使います

ここに一番時間がかかったのですが、正直ほかの記事を参照した方がいいと思います。

参考にした記事

今回の記事ではとりあえず実践例を載せちゃいます!!!

以下をコピペするだけで計算ドリルのPDF作成できちゃいます!!!

構成

parts.py ・・・ 問題作成関数とPDF作成関数をまとめたpy file
addition.py ・・・ 足し算を出力するpy file
subtraction.py ・・・ 引き算を出力するpy file
multiplication.py ・・・ 掛け算を出力するpy file
division.py ・・・ 割り算を出力するpy file

parts.py

前回作成した問題作成関数と、今回初出のPDF作成関数をまとめたものです。

parts.py
parts.py

import random
import os

from datetime import datetime

from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.pagesizes import A4, mm


# 繰り上がり判定機 
def k_up(N, M):
    d_N = len(str(N))
    d_M = len(str(M))
    
    if d_N > d_M: 
        l = d_M
    else: 
        l = d_N

    for i in range(1, l+1):
        if int(str(N)[-i]) + int(str(M)[-i]) >= 10:
            return False
        else:
            continue
    
    # 繰り上がりありならFalse、なしならTrueを返す
    return True

def generate_temp_addition(N, M):
    retern random.randint(10**(N-1), 10**(N)-1), random.randint(10**(M-1), 10**(M)-1)

def generate_addition(N, M, k):    
    # 繰り上がりなし
    if k is False:
        # Trueとなれば抜けられるようにする
        while k is False:
            n_N, n_M = generate_temp_addition(int(N), int(M))
            k = k_up(n_N, n_M)

    # 繰り上がりあり
    else:
        # Falseとなれば抜けられるようにする
        while k is True:
            n_N, n_M = generate_temp_addition(int(N), int(M))
            k = k_up(n_N, n_M)

    return {"N": n_N, "M": n_M, "ans": {"value": n_N+n_M}}


def k_down(N, M):

    for i in range(1, len(str(M))+1):
        if int(str(N)[-i]) < int(str(M)[-i]):
            return False
        else:
            continue

def generate_temp_asubtraction(N, M):
    retern random.randint(10**(N-1), 10**(N)-1), random.randint(10**(M-1), n_N)

def generate_subtraction(N, M, k):  
    # 繰り上がりなし
    if k is False:
        # Trueとなれば抜けられるようにする
        while k is False:
            n_N, n_M = generate_temp_subtraction(int(N), int(M))
            k = k_down(n_N, n_M)

    # 繰り上がりあり
    else:
        # Falseとなれば抜けられるようにする
        while k is True:
            n_N, n_M = generate_temp_subtraction(int(N), int(M))
            k = k_down(n_N, n_M)

    return {"N": n_N, "M": n_M, "ans": {"value": n_N-n_M}}
    

def generate_multiplication(N, M):

    n_N = random.randint(10**(N-1), 10**(N)-1)
    n_M = random.randint(10**(M-1), 10**(M)-1)

    return {"N": n_N, "M": n_M, "ans": {"value": n_N*n_M}}


def make_divisors(n):
    lower_divisors , upper_divisors = [], []
    i = 1
    while i*i <= n:
        if n % i == 0:
            lower_divisors.append(i)
            if i != n // i:
                upper_divisors.append(n//i)
        i += 1
    return lower_divisors + upper_divisors[::-1]


def generate_division(N, M, s, a):

    if a is False:

        p_divisors = []

        while len(p_divisors) == 0:
            n_N = random.randint(10**(N-1), 10**(N)-1)
            divisors = make_divisors(n_N)
            print(divisors)
            if s is False:
                del divisors[0]
                del divisors[-1]
            p_divisors = [i for i in divisors if 10**(M-1) <= i <= 10**(M)-1]

        n_M = random.choice(p_divisors)

    else:
        n_N = random.randint(10**(N-1), 10**(N)-1)
        divisors = make_divisors(n_N)
        if N == M:
            n_M = random.randint(10**(M-1), n_N)
        else:
            n_M = random.randint(10**(M-1), 10**(M)-1)
        while n_M in divisors:
            if N == M:
                n_M = random.randint(10**(M-1), n_N)
            else:
                n_M = random.randint(10**(M-1), 10**(M)-1)

    return {"N": n_N, "M": n_M, "ans": {"value": int(n_N/n_M), "sho": n_N//n_M, "amari":n_N%n_M}}



def Set_calc(c, x, y, problem, ope, ans=False, a=False):
    
    c.setLineWidth(1.5)

    #答えあり
    if ans is True:

        x_offset = 10   
        if ope == "÷" and a is True:
            c.drawString((x+x_offset)*mm, y*mm, f'{problem["N"]} {ope} {problem["M"]} = {problem["ans"]["sho"]}...{problem["ans"]["amari"]}')
        else:
            c.drawString((x+x_offset)*mm, y*mm, f'{problem["N"]} {ope} {problem["M"]} = {problem["ans"]["value"]}')


    # 答えなし
    else:

        if H is False:
            x_offset = 10   
            c.drawString((x+x_offset)*mm, y*mm, f'{problem["N"]} {ope} {problem["M"]} =')

 
# 1ページ分の計算式を生成する関数
# aはあまりあるかどうか
# 答えなしとありをセットで出力します。
def make_page(c, x_list, y_list, list_of_problems, ope, a=False):

    # 答えなし
    # フォントを指定することも可能です
    fontname = "Helvetica"
    c.setFont(fontname,30)
    # ヘッダー部分の文字列出力
    c.drawString(10,800, "  Day:    /          Score:    /          Time:            ")
    
    # Set Value
    n = 0
    for row in y_list:
        for col in x_list:
            n += 1
            Set_calc(c,col,row, list_of_problems[n-1], ope, ans=False, a=a)
    c.showPage()

    # 答えあり
    c.drawString(10,800, "  Day:    /          Score:    /          Time:            ")
    # Set Value
    n = 0
    for row in y_list:
        for col in x_list:
            n += 1
            Set_calc(c,col,row, list_of_problems[n-1], ope, ans=True, a=a)
    c.showPage()
           


def pdf_export(list_of_paper, x_list, y_list, ope, a=False):
    # pathに注意
    # 親ディレクトリにpdfというディレクトリを用意してください。
    c = canvas.Canvas(f"{os.path.dirname(os.path.dirname(os.path.dirname(__file__)))}/pdf/calc_train_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf",pagesize=A4)

    for l in list_of_paper:
        make_page(c, x_list, y_list, l, ope, a)

    c.save()

足し算

入力として
N, M, kが必要ですのでそれを取得できるようなTkinterを作成しました。
コードが長いので隠しておきます。

足し算ドリル作成のコード
addition.py
import random
import re
import tkinter as tk
from tkinter import ttk
import os, sys


from parts.parts import generate_addition, pdf_export


# 値を取得するTkinter
def get_value():

    root = tk.Tk()
    root.title("足し算")
    root.geometry("600x600")

    v1, v2, v3, v4 = None, None, None, None

    # ウィンドウを閉じた際にPythonを終了するように誘導するためにFalseを返す
    def close_window():
        root.quit()
        return False

    # ウィンドウを閉じるイベントの設定
    root.protocol("WM_DELETE_WINDOW", close_window)

    #桁数の選択肢 とりあえず5桁までにしていますが変更可能
    options = [1, 2, 3, 4, 5]

    # 一項目
    label1=tk.Label(root,text="一項目の桁数:")
    label1.pack(padx=10, pady=10)
    value1 = ttk.Combobox(root, values=options, state="readonly")
    value1.pack(padx=10)

    # 二項目
    label2=tk.Label(root,text="二項目の桁数:")
    label2.pack(padx=10, pady=10)
    value2 = ttk.Combobox(root, values=options, state="readonly")
    value2.pack(padx=10)

    # 繰り上がり
    value3 = tk.BooleanVar()
    rb1 = ttk.Radiobutton(root, value=True,variable=value3, text="繰り上がりあり")
    rb2 = ttk.Radiobutton(root, value=False,variable=value3, text="繰り上がりなし")
    rb1.pack(padx=10, pady=10)
    rb2.pack()

    # 枚数
    def invalidText():
        print('半角数字を入力してください。')

    # 1. 入力制限の条件を設けて検証する関数の名前を決める
    # 4. 入力制限の条件を設けて検証する関数を実装する
    def onValidate(S):
        # 入力された文字が半角数字の場合
        if re.match(re.compile('[0-9]+'), S):
            return True
        else:
            # 入力不正のブザーを鳴らす。
            root.bell()
            return False

    # 2. 1で決めた関数名を、register関数を用いて登録する
    # register : 入力制限を行うための関数の登録を行う。パラメータと関数を紐づけるために必要。
    vcmd = root.register(onValidate)

    # validate : 入力制限するオプションの値を設定。
    # validatecommand or vcmd : 入力制限用関数の設定。(3. entryのvalidatecommand option or vcmd optionへ、2の戻り値とパラメータを渡す)
    # invalidcommand : 入力制限により、入力不正が発生した場合に呼ばれる関数の設定。
    
    label4=tk.Label(root,text="プリントの枚数:")
    label4.pack(padx=10, pady=10)
    value4 = tk.Entry(root, width=10, validate="key", validatecommand=(vcmd, '%S'), invalidcommand=invalidText)
    value4.pack(padx=10, pady=10)

    
    # OKボタン押すと値を返す
    def ok_get(event):
        nonlocal v1, v2, v3, v4
        v1, v2, v3, v4 = value1.get(), value2.get(), value3.get(), value4.get()
        root.quit()


    #ボタン
    button1 = tk.Button(text='OK')
    button1.bind("<Button-1>", ok_get)
    button1.pack(padx=10, pady=20)
    
    # Enter でも OK
    root.bind('<Return>', ok_get)
    
    root.mainloop()

    try:
        return [v1, v2, v3, v4]
    except:
        return False



def get_list_problems(d_N, d_M, k, n):

    list_of_problems = []

    while len(list_of_problems) < n:
        list_of_problems.append(generate_addition(d_N, d_M, k))

    return list_of_problems


def main():
    n = 20 # 1枚の問題数
    # 計算式の基本座標
    # 2 * 10
    # A4 210 297
    # 位置のリスト
    x_list = list(range(0, 210, 105))  
    y_list = list(range(260, -10, -27))  


    if n != len(x_list) * len(y_list):
        return False

    lst = get_value()
    if lst is False:
        return None

    list_of_paper = []
    for i in range(int(lst[3])):
        list_of_paper.append(get_list_problems(int(lst[0]), int(lst[1]), lst[2], n))


    # ope is "+"
    pdf_export(list_of_paper, x_list, y_list, "+")


if __name__ == "__main__":
    main()

引き算

入力として
N, M, kが必要ですのでそれを取得できるようなTkinterを作成しました。
コードが長いので隠しておきます。

引き算ドリル作成のコード
subtraction.py
import random
import re
import tkinter as tk
from tkinter import ttk
import os, sys


from parts import generate_subtraction, pdf_export


# 値を取得するTkinter
def get_value():

    root = tk.Tk()
    root.title("引き算")
    root.geometry("600x600")

    v1, v2, v3, v4 = None, None, None, None

    # ウィンドウを閉じた際にPythonを終了するように誘導するためにFalseを返す
    def close_window():
        root.quit()
        return False

    # ウィンドウを閉じるイベントの設定
    root.protocol("WM_DELETE_WINDOW", close_window)

    #pull down
    options = [1, 2, 3, 4, 5]
    options2 = {"1": [1], "2": [1, 2], "3": [1, 2, 3], "4":[1, 2, 3, 4], "5":[1, 2, 3, 4, 5]}

    # コールバック関数
    def update_combo2(event):
        selected = value1.get()
        value2['values'] = options2[str(selected)]
        #combo2.set('')  # 初期値をリセット

    # 一項目
    label1=tk.Label(root,text="一項目の桁数:")
    label1.pack(padx=10, pady=10)
    value1 = ttk.Combobox(root, values=options, state="readonly")
    value1.pack(padx=10)
    value1.bind("<<ComboboxSelected>>", update_combo2)

    # 二項目
    label2=tk.Label(root,text="二項目の桁数:")
    label2.pack(padx=10, pady=10)
    value2 = ttk.Combobox(root, values=options2, state="readonly")
    value2.pack(padx=10)

    # 繰り下がり
    value3 = tk.BooleanVar()
    rb1 = ttk.Radiobutton(root, value=True,variable=value3, text="繰り下がりあり")
    rb2 = ttk.Radiobutton(root, value=False,variable=value3, text="繰り下がりなし")
    rb1.pack(padx=10, pady=10)
    rb2.pack()


    # 枚数

    def invalidText():
        print('半角数字を入力してください。')

    # 1. 入力制限の条件を設けて検証する関数の名前を決める
    # 4. 入力制限の条件を設けて検証する関数を実装する
    def onValidate(S):
        # 入力された文字が半角数字の場合
        if re.match(re.compile('[0-9]+'), S):
            return True
        else:
            # 入力不正のブザーを鳴らす。
            root.bell()
            return False

    # 2. 1で決めた関数名を、register関数を用いて登録する
    # register : 入力制限を行うための関数の登録を行う。パラメータと関数を紐づけるために必要。
    vcmd = root.register(onValidate)

    # validate : 入力制限するオプションの値を設定。
    # validatecommand or vcmd : 入力制限用関数の設定。(3. entryのvalidatecommand option or vcmd optionへ、2の戻り値とパラメータを渡す)
    # invalidcommand : 入力制限により、入力不正が発生した場合に呼ばれる関数の設定。
    
    label4=tk.Label(root,text="プリントの枚数:")
    label4.pack(padx=10, pady=10)
    value4 = tk.Entry(root, width=10, validate="key", validatecommand=(vcmd, '%S'), invalidcommand=invalidText)
    value4.pack(padx=10, pady=10)

    
    # OKボタン押すと値を返す
    def ok_get(event):
        nonlocal v1, v2, v3, v4
        v1, v2, v3, v4 = value1.get(), value2.get(), value3.get(), value4.get()
        root.quit()


    #ボタン
    button1 = tk.Button(text='OK')
    button1.bind("<Button-1>", ok_get)
    button1.pack(padx=10, pady=20)
    
    # Enter でも OK
    root.bind('<Return>', ok_get)
    
    root.mainloop()

    try:
        return [v1, v2, v3, v4]
    except:
        return False


def get_list_problems(d_N, d_M, k, n):

    list_of_problems = []

    while len(list_of_problems) < n:
        list_of_problems.append(generate_subtraction(d_N, d_M, k))

    return list_of_problems


def main():
    n = 20 # 1枚の問題数
    # 計算式の基本座標
    # 2 * 10
    # A4 210 297
    x_list = list(range(0, 210, 105))  
    y_list = list(range(260, -10, -27))  


    if n != len(x_list) * len(y_list):
        return False

    lst = get_value()
    if lst is False:
        return None

    list_of_paper = []
    for i in range(int(lst[3])):
        list_of_paper.append(get_list_problems(int(lst[0]), int(lst[1]), lst[2], n))


    # ope is "-"
    pdf_export(list_of_paper, x_list, y_list, "-")


if __name__ == "__main__":
    main()


コンボボックスの選択肢を動的に変更するのは
この記事を参考にしました
https://zenn.dev/nuinui/articles/7e7588fe49db40

掛け算

コードが長いので隠しておきます。

掛け算ドリル作成のコード
multiplication.py
import random
import re
import tkinter as tk
from tkinter import ttk
import os, sys


from parts import generate_multiplication, pdf_export


# 値を取得するTkinter
def get_value():

    root = tk.Tk()
    root.title("掛け算")
    root.geometry("600x600")

    v1, v2, v3 = None, None, None

    # ウィンドウを閉じた際にPythonを終了するように誘導するためにFalseを返す
    def close_window():
        root.quit()
        return False

    # ウィンドウを閉じるイベントの設定
    root.protocol("WM_DELETE_WINDOW", close_window)

    #pull down
    options = [1, 2, 3, 4, 5]
    options2 = [1, 2, 3, 4, 5]

    # 一項目
    label1=tk.Label(root,text="一項目の桁数:")
    label1.pack(padx=10, pady=10)
    value1 = ttk.Combobox(root, values=options, state="readonly")
    value1.pack(padx=10)

    # 二項目
    label2=tk.Label(root,text="二項目の桁数:")
    label2.pack(padx=10, pady=10)
    value2 = ttk.Combobox(root, values=options2, state="readonly")
    value2.pack(padx=10)


    # 枚数

    def invalidText():
        print('半角数字を入力してください。')

    # 1. 入力制限の条件を設けて検証する関数の名前を決める
    # 4. 入力制限の条件を設けて検証する関数を実装する
    def onValidate(S):
        # 入力された文字が半角数字の場合
        if re.match(re.compile('[0-9]+'), S):
            return True
        else:
            # 入力不正のブザーを鳴らす。
            root.bell()
            return False

    # 2. 1で決めた関数名を、register関数を用いて登録する
    # register : 入力制限を行うための関数の登録を行う。パラメータと関数を紐づけるために必要。
    vcmd = root.register(onValidate)

    # validate : 入力制限するオプションの値を設定。
    # validatecommand or vcmd : 入力制限用関数の設定。(3. entryのvalidatecommand option or vcmd optionへ、2の戻り値とパラメータを渡す)
    # invalidcommand : 入力制限により、入力不正が発生した場合に呼ばれる関数の設定。
    
    label3=tk.Label(root,text="プリントの枚数:")
    label3.pack(padx=10, pady=10)
    value3 = tk.Entry(root, width=10, validate="key", validatecommand=(vcmd, '%S'), invalidcommand=invalidText)
    value3.pack(padx=10, pady=10)

    
    # OKボタン押すと値を返す
    def ok_get(event):
        nonlocal v1, v2, v3
        v1, v2, v3 = value1.get(), value2.get(), value3.get()
        root.quit()


    #ボタン
    button1 = tk.Button(text='OK')
    button1.bind("<Button-1>", ok_get)
    button1.pack(padx=10, pady=20)
    
    # Enter でも OK
    root.bind('<Return>', ok_get)
    
    root.mainloop()

    try:
        return [v1, v2, v3]
    except:
        return False




def get_list_problems(d_N, d_M, n):

    list_of_problems = []

    while len(list_of_problems) < n:
        list_of_problems.append(generate_multiplication(d_N, d_M))

    return list_of_problems


def main():
    n = 20 # 1枚の問題数
    # 計算式の基本座標
    # 2 * 10
    # A4 210 297
    x_list = list(range(0, 210, 105))  
    y_list = list(range(260, -10, -27))  


    if n != len(x_list) * len(y_list):
        return False

    lst = get_value()
    if lst is False:
        return None

    list_of_paper = []
    for i in range(int(lst[2])):
        # 掛け算割り算ではkはなし
        list_of_paper.append(get_list_problems(int(lst[0]), int(lst[1]), n))


    # ope is "×"
    pdf_export(list_of_paper, x_list, y_list, "×")



if __name__ == "__main__":
    main()

割り算

コードが長いので隠しておきます。

割り算ドリル作成のコード
multiplication.py
import random
import re
import tkinter as tk
from tkinter import ttk
import os, sys


from parts import generate_division, pdf_export


def get_value():

    root = tk.Tk()
    root.title("割り算")
    root.geometry("600x600")

    v1, v2, v3, v4, v5 = None, None, None, None, None

    # ウィンドウを閉じた際にPythonを終了するように誘導するためにFalseを返す
    def close_window():
        root.quit()
        return False

    # ウィンドウを閉じるイベントの設定
    root.protocol("WM_DELETE_WINDOW", close_window)

    #pull down
    options = [3, 4, 5]
    options2 = {"3": [1, 2], "4":[1, 2, 3], "5":[1, 2, 3, 4]}

    # コールバック関数
    def update_combo2(event):
        selected = value1.get()
        value2['values'] = options2[str(selected)]
        #combo2.set('')  # 初期値をリセット

    # 一項目
    label1=tk.Label(root,text="一項目の桁数:")
    label1.pack(padx=10, pady=10)
    value1 = ttk.Combobox(root, values=options, state="readonly")
    value1.pack(padx=10)
    value1.bind("<<ComboboxSelected>>", update_combo2)

    # 二項目
    label2=tk.Label(root,text="二項目の桁数:")
    label2.pack(padx=10, pady=10)
    value2 = ttk.Combobox(root, values=options2, state="readonly")
    value2.pack(padx=10)

    # N ÷ N, N ÷ 1を含むかどうか
    value3 = tk.BooleanVar()
    rb1 = ttk.Radiobutton(root, value=True,variable=value3, text="n ÷ n や n ÷ 1 を含む")
    rb2 = ttk.Radiobutton(root, value=False,variable=value3, text="含まない")
    rb1.pack(padx=10, pady=10)
    rb2.pack()

    # 余りあるかどうか
    value4 = tk.BooleanVar()
    rb3 = ttk.Radiobutton(root, value=True,variable=value4, text="余りあり")
    rb4 = ttk.Radiobutton(root, value=False,variable=value4, text="余りなし")
    rb3.pack(padx=10, pady=10)
    rb4.pack()


    # 枚数

    def invalidText():
        print('半角数字を入力してください。')

    # 1. 入力制限の条件を設けて検証する関数の名前を決める
    # 4. 入力制限の条件を設けて検証する関数を実装する
    def onValidate(S):
        # 入力された文字が半角数字の場合
        if re.match(re.compile('[0-9]+'), S):
            return True
        else:
            # 入力不正のブザーを鳴らす。
            root.bell()
            return False

    # 2. 1で決めた関数名を、register関数を用いて登録する
    # register : 入力制限を行うための関数の登録を行う。パラメータと関数を紐づけるために必要。
    vcmd = root.register(onValidate)

    # validate : 入力制限するオプションの値を設定。
    # validatecommand or vcmd : 入力制限用関数の設定。(3. entryのvalidatecommand option or vcmd optionへ、2の戻り値とパラメータを渡す)
    # invalidcommand : 入力制限により、入力不正が発生した場合に呼ばれる関数の設定。
    
    label5=tk.Label(root,text="プリントの枚数:")
    label5.pack(padx=10, pady=10)
    value5 = tk.Entry(root, width=10, validate="key", validatecommand=(vcmd, '%S'), invalidcommand=invalidText)
    value5.pack(padx=10, pady=10)

    
    # OKボタン押すと値を返す
    def ok_get(event):
        nonlocal v1, v2, v3, v4, v5
        v1, v2, v3, v4, v5 = value1.get(), value2.get(), value3.get(), value4.get(),  value5.get()
        root.quit()


    #ボタン
    button1 = tk.Button(text='OK')
    button1.bind("<Button-1>", ok_get)
    button1.pack(padx=10, pady=20)
    
    # Enter でも OK
    root.bind('<Return>', ok_get)
    
    root.mainloop()

    try:
        return [v1, v2, v3, v4, v5]
    except:
        return False




def get_list_problems(d_N, d_M, s, a, n):

    list_of_problems = []

    while len(list_of_problems) < n:
        list_of_problems.append(generate_division(d_N, d_M, s, a))

    return list_of_problems


def main():
    n = 20 # 1枚の問題数
    # 計算式の基本座標
    # 2 * 10
    # A4 210 297
    x_list = list(range(0, 210, 105))  
    y_list = list(range(260, -10, -27))  


    if n != len(x_list) * len(y_list):
        return False

    lst = get_value()
    if lst is False:
        return None

    list_of_paper = []
    for i in range(int(lst[4])):
        list_of_paper.append(get_list_problems(int(lst[0]), int(lst[1]), lst[2], lst[3], n))


    # ope is "÷", a is ,,,
    pdf_export(list_of_paper, x_list, y_list, "÷", lst[3])





if __name__ == "__main__":
    main()


おわり

以上!!!
それぞれのpy fileを起動すればTkinterが起動されて値を入力すればPDFファイルがバシバシ出力されます!!!

こんな感じで入力したら、、、
スクリーンショット (356).png

こんなPDFが出てきました!!!
スクリーンショット (357).png

最高ですね!!!

3
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?