PythonでCanvasをリサイズできるようにしてみた

Pythonのtkinterのcanvasを、ウインドウに合わせてリサイズできるようにしてみました。

目次

  1. Canvasのリサイズ
  2. 試してみた
  3. ウィンドウ座標系とキャンバス座標系

Canvasのリサイズ

Windowsのペイントアプリをイメージしていただくと分かり易いと思います。ウィンドウの大きさにの変更にスクロールバーの大きさが追随して、画像よりウィンドウが小さくなるとスクロールバーによる画像のスクロールが可能になりますよね。あの機能が欲しいのです。

方法としては、

  1. canvasとscroll barを含むフレームを作る。
  2. canvasの子としてさらにフレームを作る。
  3. canvasの子のフレームがリサイズされたら、親のcanvasをリサイズする。

という流れで行います。

今回は、canvasとscroll barを含むフレームを、1つのクラスにします。

import tkinter as tk
import tkinter.ttk as ttk
from PIL import Image, ImageTk

class ScrollableFrame(tk.Frame):
    def __init__(self, parent, minimal_canvas_size):
        tk.Frame.__init__(self, parent)

        self.minimal_canvas_size = minimal_canvas_size

        # 縦スクロールバー
        vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
        vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=False)

        # 横スクロールバー
        hscrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
        hscrollbar.pack(fill=tk.X, side=tk.BOTTOM, expand=False)

        # Canvas
        self.canvas = tk.Canvas(self, bd=0, highlightthickness=0,
            yscrollcommand=vscrollbar.set, xscrollcommand=hscrollbar.set)
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        # スクロールバーをCanvasに関連付け
        vscrollbar.config(command=self.canvas.yview)
        hscrollbar.config(command=self.canvas.xview)

        # Canvasの位置の初期化
        self.canvas.xview_moveto(0)
        self.canvas.yview_moveto(0)

        # スクロール範囲の設定
        self.canvas.config(scrollregion=(0, 0, self.minimal_canvas_size[0], self.minimal_canvas_size[1]))

        # Canvas内にフレーム作成
        self.interior = tk.Frame(self.canvas)
        self.canvas.create_window(0, 0, window=self.interior, anchor='nw')

        # Canvasの大きさを変える関数
        def _configure_interior(event):
            size = (max(self.interior.winfo_reqwidth(), self.minimal_canvas_size[0]),
                max(self.interior.winfo_reqheight(), self.minimal_canvas_size[1]))
            self.canvas.config(scrollregion=(0, 0, size[0], size[1]))
            if self.interior.winfo_reqwidth() != self.canvas.winfo_width():
                self.canvas.config(width = self.interior.winfo_reqwidth())
            if self.interior.winfo_reqheight() != self.canvas.winfo_height():
                self.canvas.config(height = self.interior.winfo_reqheight())

        # 内部フレームの大きさが変わったらCanvasの大きさを変える関数を呼び出す
        self.interior.bind('<Configure>', _configure_interior)

縦スクロールバーをフレームの右側に、横スクロールバーをフレームの底に配置して、Canvasを左側に配置し上下左右に伸ばせるようにします。

スクロールバーとCanvasの連携を設定して、Canvasのスクロール範囲を設定します。

interiorという名前のフレームをcanvasの子に作って、create_windowというメソッドでinteriorフレームをcanvasに配置します。

configure_interiorという関数は、interiorのサイズを取得して、canvasのスクロール範囲をinteriorのサイズに合わせます。さらにcanvasのサイズをinteriorのサイズに合わせます。

最後に、Configureというイベントをinteriorにバインドします。サイズが変更されると、このイベントが呼び出されます。Configureイベントが発行されると、先のconfigure_interior関数が実行されます。

canvasの子の要素としてframeを作るというのがちょっとトリッキーですが、要は変更後のフレームウィジェットのサイズにcanvasを合わせるということです。

試してみた

このクラスを使って、Canvasと、それをクリックしたときのポインタの座標を表示するアプリを作ってみます。

import tkinter as tk
import tkinter.ttk as ttk
from PIL import Image, ImageTk

# ここにクラスのコードを書き込む

class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
        self.master.title('scrollbar trial')
        self.pack(fill=tk.BOTH, expand=True)
        self.create_widgets()

    def create_widgets(self):
        self.canvas_frame = ScrollableFrame(self, minimal_canvas_size)
        self.canvas_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        self.control_frame = tk.Frame(self)
        self.control_frame.pack(side=tk.TOP, fill=tk.Y, expand=False)

        self.label_title1 = ttk.Label(self.control_frame, text='Window coordinate')
        self.label_title1.pack()
        self.point_x = tk.StringVar()
        self.point_y = tk.StringVar()
        self.label_x = ttk.Label(self.control_frame, textvariable=self.point_x)
        self.label_x.pack()
        self.label_y = ttk.Label(self.control_frame, textvariable=self.point_y)
        self.label_y.pack()
        self.label_title2 = ttk.Label(self.control_frame, text='Canvas coordinate')
        self.label_title2.pack()
        self.point_xc = tk.StringVar()
        self.point_yc = tk.StringVar()
        self.label_xc = ttk.Label(self.control_frame, textvariable=self.point_xc)
        self.label_xc.pack()
        self.label_yc = ttk.Label(self.control_frame, textvariable=self.point_yc)
        self.label_yc.pack()
        self.canvas_frame.canvas.bind('<ButtonPress-1>', self.pickup_point)

        # canvasに画像をセットする
        im = ImageTk.PhotoImage(image=read_image)
        self.canvas_frame.canvas.config(width=read_image.width, height=read_image.height)
        self.canvas_frame.canvas.photo = im
        self.canvas_frame.canvas.create_image(0, 0, anchor='nw', image=im)

    # ポインタの座標を取得する
    def pickup_point(self, event):
        self.point_x.set('x : ' + str(event.x))
        self.point_y.set('y : ' + str(event.y))
        self.point_xc.set('x : ' + str(self.canvas_frame.canvas.canvasx(event.x)))
        self.point_yc.set('y : ' + str(self.canvas_frame.canvas.canvasy(event.y)))
        print(event.x, event.y, self.canvas_frame.canvas.canvasx(event.x), self.canvas_frame.canvas.canvasy(event.y))

read_image = Image.open('test_figure.png')
canvas_width, canvas_height = read_image.size
minimal_canvas_size = read_image.size

# アプリケーション起動
root = tk.Tk()
app = Application(master=root)
app.mainloop()

読み込む画像は、 プロ生ちゃん の画像です。画像サイズは256x256です。

前述のクラスで作るフレームと、普通のフレームを並べて、普通のフレームの方にラベルを使ってポインタの座標を表示します。

動かしてみましょう。

起動すると、このように表示されます。

初期

ウィンドウを大きくするとこうなります。スクロールバーがウィンドウの大きさについて行ってますね。

拡大

ウィンドウを小さくするとこうなります。スクロールバーがアクティブになります。

縮小

ウィンドウ座標系とキャンバス座標系

Canvasウィジェットには、ウィンドウ座標系(Window Coordinate)とキャンバス座標系(Canvas Coordinate)があります。ウィンドウ座標系はウィンドウの左上を基準にした座標系で、キャンバス座標系はキャンバスの左上を基準にした座標系です。

具体的に見てみましょう。

上記で作ったアプリには、ウィンドウ座標系とキャンバス座標系の両方を表示するようにしてあります。

まずcanvas全体が表示されている場合です。

等倍

ウィンドウ座標系とキャンバス座標系が、同じ値になっています。こういう場合はどちらの値を使っても大丈夫でしょう。

では、スクロール表示で画像の右下だけが表示される状態ではどうでしょうか。

スクロール

ウィンドウ座標系のYの値は96になっています。それに対して、キャンバス座標系のYの値は238になっています。

つまり、Canvasがスクロールしても、キャンバス座標系を取得すればCanvas基準での座標を取得できるわけです。

では、ウィンドウ座標系からキャンバス座標系を取得する方法です。

ret_x = tkinter.Canvas.canvasx(x, [gridspacing])
ret_y = tkinter.Canvas.canvasy(y, [gridspacing])
変数 内容
x int ウィンドウ座標系でのX座標の値。
y int ウィンドウ座標系でのY座標の値。
gridspacing   省略可。既定値はNone。
ret_x float キャンバス座標系でのX座標。
ret_y float キャンバス座標系でのY座標。

前述のコードでは、eventのxとyを引数に渡しています。

広告

PythonでGUIカテゴリの投稿