見出し画像

python(tkinter)で関数のグラフを書くプログラムを作ろう(基本編)

宿題によく出るグラフを書く問題…1次関数でも面倒なのに2次関数だともっと大変(平方完成が必要)、今回はそんな悩みを解決してくれるプログラムを作っていきます。$${\verb|tkinter|}$$モジュールでの作成なので、外部からのモジュールダウンロードなどもありません。その分機能性は多少劣ったり、プログラムが長くなったりしますが十分に使えるようにしているので最後のプログラムコードのコピペのみでもご利用ください。


先に忠告しておきますが、解説の中では専門用語や数学の話などが結構多く出てきます。無理にすべて理解しようとする必要は全くありません。また、今回は結構解説が長くなります。早く使ってみたいという方は先に一番下にある全体のプログラムコードをコピペで利用することをおすすめします。

使用するtkinterの関数

直線を書くには$${\verb|tkinter|}$$モジュールの$${\verb|create_line|}$$関数を使います。使うには次のようなコードを書きます。

変数 = canvas変数.create_line(開始座標のx,開始座標のy,終点座標のx,終点座標のy)

最初の「変数=」はなくてもいいですが、実行時の識別番号を保存するためにやっているので後々のプログラムでは使います。また、キーワード引数を使うことで色指定($${\verb|fill="Red"|}$$)などもできますが、ここでは省略します。

グラフの描き方

1次関数

1次関数は簡単です。グラフが画面外に出るところの座標同士をそれぞれ始点、終点として直線を書くだけです。

2次以上の関数

さて、問題は2次以上の関数、つまり曲線です。一応$${\verb|tkinter|}$$にも曲線を描く$${\verb|create_arc|}$$関数はあります。しかし、これは円ベースの描き方で円や楕円の弧の形になってしまい、放物線の形にはなってくれません。また、3次以上の場合はさらに複雑になってしまい、円ベースの描き方では足りません。そこで、直線を細かくして疑似的に曲線に見せることにします。例えば、正100角形はほぼ円のように見えますよね。それと同じように細かく直線を角度を変えて描けば曲線に見せることができます。これを任意の関数$${f(x)}$$について数式で表すと次のようになります。

$$
関数f(x)の  x  が\lim_{h \to 0}(x+h)に変化するとき、\\
yの変化量は\lim_{h \to 0} f(x+h)-f(x)
$$

つまりhの値が0.1,0.01,0.001…と0に小さくなっていけば曲線に近づいていくということです。これを利用し、x→x+hの時の直線、次はx+h→(x+h)+hの直線…というように連続で描いていきます。つまりゴリ押しで描きます。今回はh=0.1として書きましたが、もっと小さくすれば当然精度は上がります。しかし、h=0.1で十分曲線に見えるので特にこれ以上精度をを上げてもあまり意味はないと思われます。

まずはキャンバス作り

キャンバスを開く

さて、グラフを描こうにもまさかPythonシェルには描けないため、別ウィンドウが必要になります。今回は最初からpythonに組み込まれているtkinterを使います。まずは何も考えずに下のプログラムを入力してみてください。

from tkinter import *
tk = Tk()
tk.title("graph")
canvas = Canvas(tk,width=500,height=500)
canvas.pack()

1行目で$${\verb|tkinter|}$$をインポートします。$${\verb|from モジュール名 import *|}$$ とすると、関数を呼び出すときにいちいち$${\verb|モジュール名.関数名()|}$$とせずに$${\verb|関数名()|}$$だけで関数を呼び出すことができます。tkinterのような関数が多いモジュールに対しては便利です。2行目は変数にオブジェクトを割り当てているだけです。3行目でウィンドウのタイトル指定、4行目でキャンバスの設定をして変数にキャンバスデータを割り当てています。5行目でキャンバスを開きます。

マス目を付ける

さて、これを実行しても500×500のサイズの空白キャンバスが開くだけです。このまま関数を描画すると、どこが原点か、切片はどこか、そのような情報が全く分かりません。そこで、方眼紙のようにマス目を付けてみます。上で作ったプログラムに下のプログラムを追加してください。

###グラフ画面の初期化
def reset():
    #キャンバスにある線・図形をすべて消す
    canvas.delete('all')
    #キャンバスに方眼紙のように等間隔に線を引く
    for X in range(0,50):
        canvas.create_line(10+X*10,0,10+X*10,500,fill="lightgray")
    for Y in range(0,50):
        canvas.create_line(0,10+Y*10,500,10+Y*10,fill="lightgray")
    #先ほどの線のうち5マスおきに目印となる線を引く
    for X in range(-1,49,5):
        canvas.create_line(10+X*10,0,10+X*10,500,fill="gray")
    for Y in range(-1,49,5):
        canvas.create_line(0,10+Y*10,500,10+Y*10,fill="gray")
    #x軸,y軸を書く
    canvas.create_line(0,250,500,250)
    canvas.create_line(250,0,250,500)
    #画面更新
    tk.update()

#関数の実行
reset()

実行すると下のようなウィンドウが開くはずです。

実行するとこのような白黒の方眼紙っぽいウィンドウが開くはず

このようにすれば原点の位置や座標、関数などの位置関係もわかりやすくなります。
また、もしx軸,y軸が見にくいと感じた場合は、プログラムの$${\verb|#x軸,y軸を書く|}$$の部分を下のように変更するとよいでしょう。

    #x軸,y軸を書く
    canvas.create_line(0,250,500,250,width=2)
    canvas.create_line(250,0,250,500,width=2)

原点Oを書くなどはここでは省略します。(あくまで機能性重視なので)

座標の変換

さて、tkinterと今回描いたグラフ上の座標には当然ながらズレがあります。それではいろいろと都合が悪いので、変換ができる関数を作っておきます。

まず、tkinterの今回のウィンドウでは左上が(0,0)で、右下が(500,500)です。それに対してグラフは、中央が(0,0)で右上が(25,25)、左下が(-25,-25)になります。このズレを変換したいのですが、いきなりプログラムにするのは大変なので、まずはウィンドウの座標を$${wx,wy}$$、グラフの座標を$${x,y}$$として文字式で表してみます。今回はグラフ→ウィンドウの変換が多いので、$${wx=}$$や$${wy=}$$の形で表します。

$$
wx=(x+25)×10,  wy=(y-25)×(-10)
$$

そして、$${wx,wy,x,y}$$を変数に変換して関数にします。最終的に関数は下のようになります。reset関数の下に書いておきましょう。

###座標変換
def co(con,xy):
    if xy == 1:
        return ((con+25)*10)
    elif xy == 2:
        return ((con-25)*-10)
    else:
        return None

この関数の引数はちょっと特殊で、conは普通に変換したい値を入れるだけですが、xyには変換する値がxの場合は1,yの場合は2を入れなければなりません(今考えたら1と-1にして掛け算させればifを使わなくてよかったかも)。最後のNoneを返す処理はエラー用処理です。これを利用するのは内部処理のみなので正直いらないのですが、念のため外部からの呼び出しも考えてつけておきます。

グラフを描く

さて、いよいよグラフを描いていきます。まずはそのための関数を定義し、関数の初期設定をしておきます。reset関数とco関数の下に書いておきましょう。

###描画部分
def draw(nlist):
    #初期設定
    n = len(nlist) - 1
    x1 = -25
    y1 = -25
    x2 = 25
    y2 = 25

このdraw関数は描くグラフの式の係数をリストで受け取り、その要素数で何次関数かを判断します。例えば、$${y=x^2-5x+6}$$を入力するときは引数に$${\verb|[1,-5,6]|}$$と入力するような感じです。$${y=x^3-8}$$などの場合は$${\verb|[1,0,0,-8]|}$$とします。このように係数を次数順に並べて入力します(その項がない時は係数は0)。変数nに最大次数をリストの要素数から特定し割り当てた後にx1,y1,x2,y2の初期値の設定をしておきます。x1,y1は直線の始点、x2,y2は直線の終点です。
ここからは描くプログラムを作ります。

0次関数(y=a)

まずは0次関数(普通の直線)です。$${y=3}$$や$${y=-2}$$など、$${x}$$が含まれない式なので、x軸を平行移動させたものと同じになります。draw関数に下のコードを追加してください。

    if n == 0:
        ### 0次関数
        y1 = nlist[0]
        y2 = nlist[0]
        canvas.create_line(co(x1,1),co(y1,2),co(x2,1),co(y2,2),fill="Red")

このプログラムは単純です。y1,y2はx軸の平行移動なので同じ値にし、x1,x2は初期値のままで線を引きます。ただし、グラフの背景と同じ色だと見づらいので赤色にしています。

1次関数(y=ax+b)

次は1次関数です。これも単純に2点を結べばいいだけです。単純に代入してもいいですが、描画量を少なくするためにyを基準にします(傾きが小さいグラフはあまり書かないため)。
$${y=ax+b}$$では$${x=\frac{y-b}{a}}$$なので、$${y=-25}$$と$${y=25}$$のときの2点を結ぶ直線にします。0次関数のプログラムの下にコードを追加してください。

    elif n == 1:
        ### 1次関数
        x1 = (-25 - nlist[1]) / nlist[0]
        x2 = (25 - nlist[1]) / nlist[0]
        canvas.create_line(co(x1,1),co(y1,2),co(x2,1),co(y2,2),fill="Red")

これは0次関数のプログラムとよく似ています。やっていることは2点の座標を傾きと切片から計算して結んでいるだけです。

n次関数(n=2,3,4,…)

ここからがちょっと大変なポイントです。「グラフの描き方」の項で説明しましたが、微分と同じような考え方でやります。下のプログラムを追加しましょう。

    else:
        ### 2次以上の関数
        for m in range(x1*10,x2*10):
            m = m / 10
            nlist_n = n
            y1 = 0
            y2 = 0
            for nl in nlist:
                y1 += nl * (m ** nlist_n)
                y2 += nl * ((m+0.1) ** nlist_n)
                nlist_n -= 1
            canvas.create_line(co(m,1),co(y1,2),co(m+0.1,1),co(y2,2),fill="Red")

mを0.1ずつ-25から25まで変化させ、それをx1に割り当てます。x2はmに0.1を加えたものにします。そして、x1,x2をそれぞれ式に代入してy1,y2を求め、直線を書きます。x1とx2の差は0.1なので、これでほぼ曲線のように見えます。

ここまで出来たら動作確認をしてみましょう。
起動しても先ほどのウィンドウが開くだけですが、起動したらシェルに下のようなコードを入力してみましょう。

draw([-1,2,3])

これを入力してEnterを押すと、グラフのウィンドウが下のように変化するはずです。

実行結果。放物線が現れる

draw([-1,2,3])と入れたので、$${y=-x^2+2x+3}$$のグラフが描かれました。さらに入力すると追加でグラフを増やすことができます。汚くなってきて消したいときは、$${\verb|reset()|}$$と入力すると書いたグラフが消えます。これでプログラムはほぼ完成です。最後に入力部を作っておきます。

入力部分とエラー処理

プログラムは完成でもいいのですが、draw([1,-3,1,-1])などの入力はちょっと面倒で、しかも変な入力をするとエラーメッセージを吐く…というのは個人使用なら問題はないですがほかの人に使わせることも考えるとあまりよろしくありません。最後に入力部だけ追加して完成です。これはグラフに関係ないのでプログラムの解説はしませんが、そこまで複雑なものではないので理解しやすいと思います。プログラムコードは下の全体コードと合わせて載せています。

全体のプログラムコード

全体のコードは次のようになります。

from tkinter import *
tk = Tk()
tk.title("graph")
canvas = Canvas(tk,width=500,height=500)
canvas.pack()

###グラフ画面の初期化
def reset():
    canvas.delete('all')
    for X in range(0,50):
        canvas.create_line(10+X*10,0,10+X*10,500,fill="lightgray")
    for Y in range(0,50):
        canvas.create_line(0,10+Y*10,500,10+Y*10,fill="lightgray")
    for X in range(-1,49,5):
        canvas.create_line(10+X*10,0,10+X*10,500,fill="gray")
    for Y in range(-1,49,5):
        canvas.create_line(0,10+Y*10,500,10+Y*10,fill="gray")
    canvas.create_line(0,250,500,250)
    canvas.create_line(250,0,250,500)
    tk.update()

###座標変換
def co(con,xy):
    if xy == 1:
        return ((con+25)*10)
    elif xy == 2:
        return ((con-25)*-10)
    else:
        return None

###描画部分
def draw(nlist):
    n = len(nlist) - 1
    x1 = -25
    y1 = -25
    x2 = 25
    y2 = 25
    if n == 0:
        ### 0次関数
        y1 = nlist[0]
        y2 = nlist[0]
        canvas.create_line(co(x1,1),co(y1,2),co(x2,1),co(y2,2),fill="Red")
    elif n == 1:
        ### 1次関数
        x1 = (-25 - nlist[1]) / nlist[0]
        x2 = (25 - nlist[1]) / nlist[0]
        canvas.create_line(co(x1,1),co(y1,2),co(x2,1),co(y2,2),fill="Red")
    else:
        ### 2次以上の関数
        for m in range(x1*10,x2*10):
            m = m / 10
            nlist_n = n
            y1 = 0
            y2 = 0
            for nl in nlist:
                y1 += nl * (m ** nlist_n)
                y2 += nl * ((m+0.1) ** nlist_n)
                nlist_n -= 1
            canvas.create_line(co(m,1),co(y1,2),co(m+0.1,1),co(y2,2),fill="Red")

###入力部分
def g():
    try:
        f = input("関数の係数をカンマ(,)区切りで入力してください>>>").split(',')
        for num in range(0,len(f)):
            f[num] = float(f[num])
        draw(f)
    except:
        ###エラー処理
        print("指定以外の値が入力されました。入力し直してください。")

###グラフの初期化
reset()

###説明文
print("""
このプログラムは関数のグラフを描くプログラムです。
g()と入力するとグラフを追加でき、reset()で描いたグラフを全て消せます。
g()では関数の係数を次数順のカンマ区切りで入力してください。
入力例1:1,-3,1,-1 出力:y = x^3 - 3x^2 + x - 1 のグラフ
入力例2:-2,1 出力:y = -2x + 1 のグラフ
入力例3:1,0,1 出力:y = x^2 + 1 のグラフ
*分数には対応していません。小数で入力してください。
""")

これでプログラムは完成です。だいぶ長くなってしまいましたがこれで動作はしてくれると思います。注意としては、キャンバスを閉じてしまうとdraw関数やreset関数がエラーを吐いてしまうことです。また、分数入力には対応していないので小数での入力にしてください。しかし普通に使用する分には問題ないと思うのでぜひ使ってみてください!

次回は発展編です。ぜひご覧ください!


この記事が気に入ったらサポートをしてみませんか?