適当のごった煮

Pythonと境界標とQGISを中心にいろいろと

TkinterのCanvas内で点を移動させる

スポンサードリンク

Python TkinterのCanvasは幅と高さで大きさを表し、Canvas内の点は座標として表されます。これを利用すれば、点を任意のタイミングで指定位置に動かすことができます。

Canvas内の座標は、bindした関数に渡されるeventオブジェクトを通じて取得できます。最初は、Canvas上のマウスポインタの位置を表示するスクリプトでテストします。

f:id:tekito-gottani:20180403192604j:plain

import tkinter as tk

def xy_print(event):
  line = 'x:' + str(event.x) + ' ' + 'y:' + str(event.y)
  ent.delete(0, 20)
  ent.insert(0, line)

root = tk.Tk()

ent = tk.Entry(root)
ent.pack()
cvs = tk.Canvas(root, height=200, width=200, bg='white')
cvs.pack()

cvs.bind('<Motion>', xy_print)

root.mainloop()

このスクリプトを応用してCanvas上でクリックした位置を順番に記録して座標を保存し、そのデータを再生スクリプトで読み込んで表示させれば点の移動を再現できます。

マウスクリックして移動する点を指定してファイルに保存
f:id:tekito-gottani:20180403193202j:plain

youtu.be

上記の例では、キーボードの右と左を押すことで座標を順番に進めたり戻したりしています。背景が何もないと、ただ点が移動しているだけですが、Canvasに背景画像を読み込めば点の移動にも意味を持たせることができます。

そこで、気象庁 台風位置表(2017年) のページで公開されているCSVから、2017年台風第5号の中心位置情報を抜き出して経路を図示します。

QGISを利用して地球地図日本のデータをワールドファイル付きで画像出力すると、画像左上位置(正確には左上ピクセルの中心)とピクセル当たりの長さが分かるので、計算でCanvasの座標系に合わせたデータを作成します。

Canvasはgif形式なら簡単に読み込むことができるので、画像ソフトで形式を変更して座標再生スクリプトに読み込ませます。具体的にはCanvasウィジットを作成するときにcreate_imageを使います。

    def create_widgets(self):
~中略~
        self.i = tk.PhotoImage(file='jp.gif')
        haba = self.i.width()
        takasa = self.i.height()
        self.cvs = tk.Canvas(self, width=haba, height=takasa)
        self.cvs.create_image(haba/2, takasa/2, image=self.i)
        self.cvs.grid(row=0, column=0, rowspan=5)
~後略~

これで準備が整ったので再生スクリプトに画像とデータを読み込ませて左右キーで台風位置を移動させると以下のようになります。

地図画像は、「国土地理院技「地球地図日本(行政界)」をもとにid:tekito-gottaniがQGISを用いて編集・加工」。
youtu.be

地図の図法が違うので気象庁 台風経路図(2017年)とはズレているように見えますが、温帯低気圧に変化した地点を比較するとだいたい同じなので、テストとしては合格点くらいではないでしょうか。

この他、地形図を読み込んで同時に移動する座標を複数にすれば、戦況図などを描くことも可能だと思いますが、使用するデータの準備が大変そうです。

記録スクリプト

import tkinter as tk

class App(tk.Frame):
    def __init__(self, master = None):
        tk.Frame.__init__(self, master)
        self.pack()
        self.create_widgets()

    def create_widgets(self):
        self.ent = tk.Entry(self)
        self.ent.grid(row=0, column=0)

        self.btn = tk.Button(self, text='保存', command=self.save_text)
        self.btn.grid(row=0, column=1)

        self.text = tk.Text(self, height=20, width=20)
        self.text.grid(row=1, column=1)

        self.cvs = tk.Canvas(self, height=300, width=300, bg='white')
        self.cvs.grid(row=1, column=0, rowspan=5)
        self.cvs.bind('<Motion>', self.pos)
        self.cvs.bind('<Button-1>', self.lclick)

        self.mae_x, self.mae_y = None, None

    def pos(self, event):
        line = 'x:' + str(event.x) + ' ' + 'y:' + str(event.y)
        self.ent.delete(0, 20)
        self.ent.insert(0, line)

    def lclick(self, event):
        self.cvs.create_oval(event.x-2, event.y-2, event.x+2, event.y+2, fill='RED')
        self.text.insert(tk.END, str(event.x) + ', ' + str(event.y) + '\n')
        if self.mae_x:
            self.cvs.create_line(self.mae_x, self.mae_y, event.x, event.y)
        self.mae_x, self.mae_y = event.x, event.y

    def save_text(self):
        all_text = self.text.get('1.0', tk.END)
        f = open('data.txt', 'w')
        f.write(all_text)
        f.close()
            
app = App()
app.mainloop()

再生スクリプト

import tkinter as tk

class App(tk.Frame):
    def __init__(self, master = None):
        tk.Frame.__init__(self, master)
        self.pack()
        self.create_widgets()

    def create_widgets(self):
        self.lbl_ima = tk.Label(self, text='現在位置')
        self.lbl_ima.grid(row=0, column=1)

        self.ent_ima = tk.Entry(self)
        self.ent_ima.grid(row=1, column=1)
        
        self.text = tk.Text(self, width=20, height=20)
        self.text.grid(row=2, column=1)

        self.cvs = tk.Canvas(self, height=300, width=300, bg='white')
        self.cvs.grid(row=0, column=0, rowspan=5)
        
        self.bind_all('<Key-Right>', self.susumu)
        self.bind_all('<Key-Left>', self.modoru)

        elem, self.x, self.y = [], [], []
        f = open('data.txt')
        i = 0
        for line in f:
            if line == '\n':
                break
            elem = line.strip().replace(' ', '').split(',')
            self.text.insert('end', '{:03d} : {}'.format(i, line))
            self.x.append(int(elem[0]))
            self.y.append(int(elem[1]))
            i += 1
        self.now = 0
        self.ent_ima.insert(0, '{:03d}'.format(self.now))
        self.cvs_num = self.cvs.create_oval(self.x[0]-5, self.y[0]-5, self.x[0]+5, self.y[0]+5, fill='RED')
        f.close()

    def susumu(self, event):
        n = self.now
        if n+1 < len(self.x):
            self.cvs.move(self.cvs_num, self.x[n+1]-self.x[n], self.y[n+1]-self.y[n])
            self.now += 1
            self.ent_ima.delete(0, 20)
            self.ent_ima.insert(0, '{:03d}'.format(self.now))

    def modoru(self, event):
        n = self.now
        if n > 0:
            self.cvs.move(self.cvs_num, self.x[n-1]-self.x[n], self.y[n-1]-self.y[n])
            self.now -= 1
            self.ent_ima.delete(0, 20)
            self.ent_ima.insert(0, '{:03d}'.format(self.now))

app = App()
app.mainloop()