今日も見に来てくださってありがとうございます。石川さんです。久しぶりの更新です。
ちょうど一年くらい前にPythonでGUIという記事を書きました。いくつかサンプルをつくって勉強していくうちに色々とできるようなって、tkinterで使っているクラスの継承関係をいっちょ描いてみるか、ということでプログラミングしてみました。早く結論を知りたい方のために、まずはできあがりイメージの画像です。
できあがりイメージ

ソースコード
ソースコードは以下の通りです。
import tkinter as tk
import inspect
class Class:
'''クラスを描画するためのクラス'''
CLASS_COUNTER = 0
def __init__(self, canvas, name):
self.canvas = canvas
self.name = name
self.start_pos = None
self.connections = []
Class.CLASS_COUNTER += 1
x = 150 + 200 * (Class.CLASS_COUNTER % 5)
y = 150 + 50 * (Class.CLASS_COUNTER // 5)
text = canvas.create_text(x, y, text=name, tag=name)
rect = canvas.create_rectangle(canvas.bbox(name), tag=name, fill="lightyellow")
canvas.tag_lower(rect, text)
canvas.tag_bind(name, "<ButtonPress>", self.start)
canvas.tag_bind(name, "<Motion>", self.move)
canvas.tag_bind(name, "<ButtonRelease>", self.end)
def start(self, event):
'''クラスをクリックされたときのメソッド'''
self.start_pos = (self.canvas.canvasx(event.x),
self.canvas.canvasy(event.y))
def move(self, event):
'''クラスを動かすためのメソッド'''
if self.start_pos is None:
return
x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
dx, dy = x - self.start_pos[0], y - self.start_pos[1]
self.canvas.move(self.name, dx, dy)
self.start_pos = x, y
for connection in self.connections:
connection.move(self)
def end(self, event): # pylint: disable=unused-argument
'''クラスの選択が終わった時のメソッド'''
self.start_pos = None
def get_center(self):
'''中心点を戻す'''
bbox = self.canvas.bbox(self.name)
return (bbox[0] + bbox[2]) // 2, (bbox[1] + bbox[3]) // 2
@property
def x(self):
'''左上のx座標を戻す'''
bbox = self.canvas.bbox(self.name)
return bbox[0]
@property
def y(self):
'''左上のy座標を戻す'''
bbox = self.canvas.bbox(self.name)
return bbox[1]
@property
def height(self):
'''高さを戻す'''
bbox = self.canvas.bbox(self.name)
return abs(bbox[1] - bbox[3])
@property
def width(self):
'''幅を戻す'''
bbox = self.canvas.bbox(self.name)
return abs(bbox[0] - bbox[2])
def add_listener(self, connection):
''' コネクションのリスナーを登録します '''
self.connections.append(connection)
class Inheritance:
'''継承関係の線を表示するためのクラス'''
def __init__(self, canvas, parent, child):
self.canvas = canvas
self.parent = parent
self.child = child
parent.add_listener(self)
child.add_listener(self)
p_coords = self.get_intersection(parent)
c_coords = self.get_intersection(child)
self.id = self.canvas.create_line(*p_coords, *c_coords, arrow=tk.FIRST)
def get_intersection(self, box):
''' 矩形との接点を求める '''
x, y = box.get_center()
height, width = box.height // 2, box.width // 2
dx, dy = self.child.x - self.parent.x, self.child.y - self.parent.y
if box == self.child:
dx, dy = -dx, -dy
if dx == 0:
dx = 0.001
if abs(dy / dx) < (height / width): # 垂直側
x_pos = x + width if dx > 0 else x - width
y_pos = y + dy * width / abs(dx)
else: # 水平側
x_pos = x + dx * height / abs(dy)
y_pos = y + height if dy > 0 else y - height
return x_pos, y_pos
def move(self, box):
''' エンティティが移動したときの処理(エンティティから呼び出される)'''
coords = self.canvas.coords(self.id)
if box == self.parent:
coords[0:2] = self.get_intersection(box)
coords[2:4] = self.get_intersection(self.child)
elif box == self.child:
coords[0:2] = self.get_intersection(self.parent)
coords[2:4] = self.get_intersection(box)
self.canvas.coords(self.id, coords)
class Application:
'''アプリケーションクラス'''
def __init__(self, root):
self.root = root
root.title("tkinter classes")
root.geometry("1200x800")
self.start_pos = None
self.item = None
self.canvas = tk.Canvas(root, background="white")
self.canvas.pack(fill=tk.BOTH, expand=True)
self.canvas.bind("<Motion>", self.move)
self.classes = {}
self.init()
def init(self):
'''初期化'''
for name, obj in inspect.getmembers(tk):
if inspect.isclass(obj):
if name in ('getdouble', 'getint', '_setit'):
continue
if name not in self.classes.keys():
self.classes[name] = Class(self.canvas, name)
for parent in obj.__bases__:
p_name = parent.__name__
if p_name == 'object':
continue
if p_name not in self.classes.keys():
self.classes[p_name] = Class(self.canvas, p_name)
print(p_name, "<--", name)
Inheritance(self.canvas, self.classes[p_name], self.classes[name])
def move(self, event):
'''マウスが動いたときにタイトルに座標を表示します'''
title = "tkinter class[" + str(event.x) + "," + str(event.y) + "]"
self.root.title(title)
def main():
'''主処理'''
root = tk.Tk()
application = Application(root)
application.root.mainloop()
if __name__ == "__main__":
main()
詳細説明
最初のポイントは、モジュールを使って、tkinterモジュールの中身を検査しているところでしょうか(139行~)。メンバーを順番に取り出してクラスを抜き出します。実行してみてから小文字のクラスがあるのに気づいたのですが、これらはint、floatに別名を付けていたものと内部的なクラス(Internal class)とコメントに書いてあったので、出力されないようスキップしました。inspect
一度も出力していないクラスを出力して、その親クラスも同様に出力、その後承継を作成するようにしています。
その他の部分は、これまでに記事にした内容を参照すれば、できそうですね。
まとめ
tkinterのクラス図ができあがりました。本当は、レイアウトもある程度自動でやりたかったのですが、とりあえず、今回は、手で移動しました。もうちょっと、勉強してきます。















