HZK
@HZK (Ritoku Sakamae)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

tkinter:特定のcanvasに、自作の描画関数をインポートできますか?

解決したいこと

 tkinterのcanvasクラス上で、色々な図形を描画・操作しています。ベタ書きのコードだと長くなるので、自作の描画関数をインポートできればコードがスッキリするかと思い試してみましたが、ダメでした。
 tkinterにおける関数インポートについてネット上で調べるも、分かりませんでした。解決方法をお願い致します。

発生している問題・エラー

本当は特定のフレーム上にあるcanvasの上にインポートしたかったのですが、まずは、root上で試してみました。関数の外においた定数の扱いがダメなのか。mainloopの使用法がおかしいのか。インポート元の関数に、引数が必要なのか。そもそも、根本的に何か誤解しているのか(これが一番ありうる)、完全に迷子状態です。

該当するソースコード

インポート元の関数:同じディレクトリに置く。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#インポート元:imp_test.py

def rotate():
    global canvas
    global rad
    global WIDTH, HEIGHT, CENTER_X, CENTER_Y, ORBIT_A, SATELITE
    global TIME_INTERVAL, RAD_INTERVAL
    
    # 衛星の中心位置を計算
    x = CENTER_X + ORBIT_A * math.cos(rad)
    y = CENTER_Y + ORBIT_A * math.sin(rad)
    # 求めた位置を中心に衛星再配置
    canvas.coords("satelite",
        x - SATELITE, y - SATELITE,
        x + SATELITE, y + SATELITE)

    # 衛星を描画する角度を増加
    rad += RAD_INTERVAL
    if rad > 2*math.pi:
        rad -= 2 * math.pi

    # TIME_INTERVAL[ms]後に再度rotate実行
    after_id = canvas.after(TIME_INTERVAL, rotate)

def main():
    global canvas
    global rad
    global WIDTH, HEIGHT, CENTER_X, CENTER_Y, ORBIT_A, SATELITE
    global TIME_INTERVAL, RAD_INTERVAL
    
    canvas = tk.Canvas(root,
        width=WIDTH,
        height=HEIGHT,
        bg="white",
        highlightthickness=0)
    
    canvas.pack()

    canvas.create_oval(
        CENTER_X - PLANET, CENTER_Y - PLANET,
        CENTER_X + PLANET, CENTER_Y + PLANET,
        tag="planet", fill="green")

    canvas.create_oval(
        CENTER_X - ORBIT_A, CENTER_Y - ORBIT_A,
        CENTER_X + ORBIT_A, CENTER_Y + ORBIT_A,
        outline="red", width=5)

    # 衛星の中心位置を計算
    x = CENTER_X + ORBIT_A * math.cos(rad)
    y = CENTER_Y + ORBIT_A * math.sin(rad)

    # 衛星を表す円の描画
    canvas.create_oval(
        x - SATELITE, y - SATELITE,
        x + SATELITE, y + SATELITE,
        tag="satelite", fill="blue", width=0)

    rotate()

インポート先の関数

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import tkinter as tk
import math
import imp_test

WIDTH = 470
HEIGHT = 320

# 中央座標
CENTER_X = WIDTH // 2
CENTER_Y = HEIGHT // 2

# 軌道のサイズ
ORBIT_A = 140  

# 惑星と衛星の半径
PLANET = 100
SATELITE = 20

# 衛星を再配置する時間の間隔
TIME_INTERVAL = 10

# 衛星を再配置する角度の間隔
RAD_INTERVAL = math.pi / 100

# 衛星の回転角度
rad = 0
canvas = None

imp_test.rotate()
imp_test.main()

自分で試したこと

NameError: name 'CENTER_X' is not defined
となりますが、インポート元の関数にはglobal宣言をしています。
とりあえずglobal宣言を増やしましたが、こんなに必要なものでしょうか?

0

6Answer

グローバル変数は、モジュール毎に独立です。
importされた側からimportした側のグローバル変数にアクセスすることはできません。
importした側からは モジュール名.グローバル変数名 でアクセスできます。

1Like

逆上がりするのに苦労したり、自転車に乗るのに苦労した経験があると思いますが、クラスも一度理解できれば使えるようになりますよ。
そのあと、正しいオブジェクト指向設計ができるようになるには数年かかりますけれど。

仕組みを知るのが好きなようでしたら、私の記事が理解の助けになるといいのですが・・・

1Like

記事を拝見し、何とか以下のように理解しました。
コンストラクタは処理であり、初期化メソッドが同一ではないことや、「インスタンスメソッド」は第一引数にインスタンスを自動挿入するので、クラス定義のときには省略されたように見えるselfを記述する必要がある。
少しは理解が進んだ実感がありますが、この先クラスを完璧に理解し、それから、オブジェクト指向設計できるまでさらに数年、気が遠くなります。
ご指導ありがとうございました。

1Like

回答ありがとうございます。

助言をもとに試行錯誤を繰り返し、以下のコードに修正したらうまくいきました。

インポートする関数

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#インポート元:imp_test.py
#同一ディレクトリに置く

def rotate():
    global canvas
    global rad
    global WIDTH, HEIGHT, CENTER_X, CENTER_Y, ORBIT_A, SATELITE
    global TIME_INTERVAL, RAD_INTERVAL
    global math, root
    # 衛星の中心位置を計算
    x = CENTER_X + ORBIT_A * math.cos(rad)
    y = CENTER_Y + ORBIT_A * math.sin(rad)
    # 求めた位置を中心に衛星再配置
    canvas.coords("satelite",
        x - SATELITE, y - SATELITE,
        x + SATELITE, y + SATELITE)
    # 衛星を描画する角度を増加
    rad += RAD_INTERVAL
    if rad > 2*math.pi:
        rad -= 2 * math.pi
    # TIME_INTERVAL[ms]後に再度rotate実行
    after_id = canvas.after(TIME_INTERVAL, rotate)

def main():
    global canvas
    global rad
    global WIDTH, HEIGHT, CENTER_X, CENTER_Y, ORBIT_A, SATELITE
    global TIME_INTERVAL, RAD_INTERVAL
    global math, root, tk
    
    canvas = tk.Canvas(root,
        width=WIDTH,
        height=HEIGHT,
        bg="white",
        highlightthickness=0) 
    canvas.pack()
    canvas.create_oval(
        CENTER_X - PLANET, CENTER_Y - PLANET,
        CENTER_X + PLANET, CENTER_Y + PLANET,
        tag="planet", fill="green")

    canvas.create_oval(
        CENTER_X - ORBIT_A, CENTER_Y - ORBIT_A,
        CENTER_X + ORBIT_A, CENTER_Y + ORBIT_A,
        outline="red", width=5)

    # 衛星の中心位置を計算
    x = CENTER_X + ORBIT_A * math.cos(rad)
    y = CENTER_Y + ORBIT_A * math.sin(rad)
    # 衛星を表す円の描画
    canvas.create_oval(
        x - SATELITE, y - SATELITE,
        x + SATELITE, y + SATELITE,
        tag="satelite", fill="blue", width=0)
    rotate()

インポートされた関数

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#インポート先 修正 成功!

import tkinter as tk
import math
import imp_test as i

root = tk.Tk()

i.WIDTH = 470
i.HEIGHT = 320
i.math = math
i.tk = tk
i.root = root

# 中央座標
i.CENTER_X = i.WIDTH // 2
i.CENTER_Y = i.HEIGHT // 2
# 軌道のサイズ
i.ORBIT_A = 140  

# 惑星と衛星の半径
i.PLANET = 100
i.SATELITE = 20
# 衛星を再配置する時間の間隔
i.TIME_INTERVAL = 10
# 衛星を再配置する角度の間隔
i.RAD_INTERVAL = math.pi / 100

# 衛星の回転角度
i.rad = 0
i.canvas = None
i.main()
root.mainloop()

感想・意見

結局すべてのグローバル変数を修正することになりました。
モジュールをimportすると確かにコードはすっきりしましたが、変数の再定義が必要となりその分の行数が増えるので、インポートした有り難みが薄くなりました。むしろ、tkinterではベタがきの方が、理解が直線的となり理解しやすいかなと思ったりしました。
上級者がtkinterを使用する場合、自作関数importの位置付けを、どのように考えているか疑問が生じました。

0Like

必要な情報を引数で渡すのが一般的な実装です。
保持し続ける情報があるならクラス定義してインスタンス変数に保持します。
必要なモジュールのimportはそれぞれのファイルに書きます。

クラス実装例:

imp_test.py
import math


class Planet:

    def __init__(self, canvas, x, y, size):
        canvas.create_oval(x - size, y - size,
                           x + size, y + size,
                           tag="planet", fill="green")


class Satelite:

    def __init__(self, canvas, x, y, distance, size, speed):
        self.canvas = canvas
        self.x = x
        self.y = y
        self.distance = distance
        self.size = size
        self.speed = speed
        self.angle = 0
        # 軌道を表す円の描画
        canvas.create_oval(x - distance, y - distance,
                           x + distance, y + distance,
                           outline="red", width=5)
        # 衛星を表す円の描画
        x += distance * math.cos(self.angle)
        y += distance * math.sin(self.angle)
        canvas.create_oval(x - size, y - size,
                           x + size, y + size,
                           tag="satelite", fill="blue", width=0)

    def rotate(self, interval):
        # 衛星の角度を増加
        self.angle = (self.angle + self.speed) % (2 * math.pi)
        # 衛星の中心位置を計算
        x = self.x + self.distance * math.cos(self.angle)
        y = self.y + self.distance * math.sin(self.angle)
        # 求めた位置を中心に衛星描画
        self.canvas.coords("satelite",
                           x - self.size, y - self.size,
                           x + self.size, y + self.size)
        # 次の移動
        self.canvas.after(interval, self.rotate, interval)
main.py
#!/usr/bin/env python

import tkinter as tk
import imp_test as sample
import math

WIDTH = 470
HEIGHT = 320

# 中央座標
CENTER_X = WIDTH // 2
CENTER_Y = HEIGHT // 2

# 惑星の半径
PLANET_SIZE = 100

# 衛星の軌道の距離
SATELITE_DISTANCE = 140
# 衛星の半径
SATELITE_SIZE = 20
# 衛星の移動間隔
SATELITE_INTERVAL = 10
# 衛星の角速度
SATELITE_SPEED = math.pi / 100

def main():
    root = tk.Tk()
    canvas = tk.Canvas(root,
                       width=WIDTH,
                       height=HEIGHT,
                       bg="white",
                       highlightthickness=0)
    canvas.pack()
    planet = sample.Planet(canvas, CENTER_X, CENTER_Y, PLANET_SIZE)
    satelite = sample.Satelite(canvas, CENTER_X, CENTER_Y, SATELITE_DISTANCE,
                               SATELITE_SIZE, SATELITE_SPEED)
    satelite.rotate(SATELITE_INTERVAL)
    root.mainloop()

if __name__ == "__main__":
    main()
0Like

ここまで手間をかけて頂き恐れ入ります。クラスを使用すると、グローバル変数の羅列が減ってこれだけスッキリするのかと驚いています。これがオブジェクト指向ということなのでしょうか。
ただ、現在の自分の実力では、クラスは本当に難しいです。
特に、クラス内の関数の引数はどこまでの指定が必要なのか、init.をどこまでつけるのかが悩みどころです。例えば、模範回答を走らせてみたところ、NameError: name 'angle' is not defined となったので、自分なりに以下のように修正したら、うまくいきましたが正しいのか自信ないです。
class Sateliteのコンストラクタの引数にangleと、中身にself.angle = angleを追加し、main.pyにおける呼び出しで0を指定。

0Like

Your answer might help someone💌