【Python/tkinter】マウスホイールで画像のリサイズを行う

マウスホイール操作で画像をリサイズするアプリの作成方法解説ページアイキャッチ

このページにはプロモーションが含まれています

今回は Python の tkinter を利用して「マウスホイールで画像のリサイズ」を行うアプリを開発していきたいと思います。

このアプリを開発することで、単に GUI に対する処理だけでなく、画像のリサイズに関する知識も身につけることができます!

MEMO

アプリ上に表示する画像の例はリズム727さんによる写真ACからの写真を使用させていただいています

開発するアプリ

まずは開発するアプリを簡単に紹介します。

アプリの画面

アプリの画面は下の図のようになります。

アプリ起動画面

メインウィンドウの中にキャンバスウィジェットが1つあり、このキャンバスウィジェット上に画像を描画しているだけです。

スポンサーリンク

アプリの操作

アプリが受け付ける操作はマウスホイール操作のみです。

ノートパソコンを利用されている&マウスが無い方であれば、トラックパッドで指2本のスワイプで同様の操作を行うことが可能です。

マウスホイールでスクロールアップすると(トラックパッドでスクロールアップすると)、表示されている画像が拡大されて表示されます。

逆にマウスホイールでスクロールダウンすると(トラックパッドでスクロールダウンすると)、表示されている画像が縮小されて表示されます。

マウスホイールで画像をリサイズする様子

MEMO

マウスホイールを上 or 下のどちらに動かせばスクロールアップ(画面が上方向にスクロール) or スクロール(画面が下方向にスクロール)のどちらが行われるかは環境や設定によって異なるようです

私の環境(MacOS)だと、トラックパッドやマウスホイールを上方向に動かすとスクロールダウンし、下方向に動かすとスクロールアップしますが、環境によっては逆の場合もあります

アプリの作り方

それでは先ほど紹介したアプリの作り方のポイントを解説していきたいと思います。

アプリの処理の全体的な流れ

まずはアプリの処理の全体的な流れについて簡単に解説しておきます。

最初に、アプリは起動時に下記の処理を行います。

  • 画像を読み込む
  • ウィジェットを作成・配置する
  • マウスホイール操作イベントを受け付ける
  • キャンバスサイズに合わせて画像をリサイズする
  • 画像をキャンバスに描画する

さらに、ユーザーからのマウスホイール操作によって下記の処理を行います。

  • マウスホイール操作の情報を取得する
  • 拡大率を計算する
  • 画像をリサイズする
  • 画像をキャンバスに描画する

ここからはこれらの処理の流れを実現するためのポイント部分のみを解説していきます。

スポンサーリンク

ウィジェットを作成・配置する

まずはウィジェットの作成・配置を行ってアプリの見た目を開発します。

といっても今回使用するウィジェットは、(メインウィンドウを除けば)キャンバスのみになります。

ウィジェとの配置

マウスホイール操作を受け付ける

次に必要なのはマウスホイール操作の受付です。

tkinter ではマウスホイール操作もイベントとして受け付けることが可能で、この操作に対してイベント処理を設定することが可能です。

マウスホイール操作イベントの受付

イベントに関しては下記ページで解説していますので、詳しく知りたい方は是非こちらのページも読んでみてください。

イベント処理解説ページのアイキャッチ Tkinterの使い方:イベント処理を行う

マウスホイール操作に対してイベント処理を設定するためには、"<MouseWheel>" をイベントシーケンスとして下記のように bind メソッドをすればOKです。

マウスホイール操作イベントの受付
# self.canvasはCanvasクラスのインスタンス
self.canvas.bind("<MouseWheel>", self.event_handler)

bind メソッドを実行したオブジェクトに対してイベント処理が設定されますので、キャンバス上でのマウスホイール操作が行われた際に event_handler 関数が実行されるようになります。

MEMO

OS が Linux の場合はイベントシーケンスを "<MouseWheel>" ではなく "<ButtonPress-4>""<ButtonPress-5>" に設定するとマウスホイールのイベントが受け付けられるようです

前者はスクロールアップ、後者はスクロールダウンのイベントに対するイベントシーケンスになります

おそらく下記でマウスホイールの設定を受け付けることができるようになると思います。

# self.canvasはCanvasクラスのインスタンス
self.canvas.bind("<ButtonPress-4>", self.event_handler)
self.canvas.bind("<ButtonPress-5>", self.event_handler)

マウスホイール操作の情報取得

このアプリのメイン機能は「マウスホイール操作により画像のリサイズを行う」です。

マウスホイール操作によりスクロールアップが行われた際には画像の拡大を行い、マウスホイール操作によりスクロールダウンが行われた際には画像の縮小を行います。

さらに、マウスホイール操作で何ステップ分マウスホイールが動かされたかに応じて拡大率・縮小率の大小を決定します。

この辺りの処理を実現するためには、マウスホイールによりスクロールアップとスクロールダウンのどちらが行われたかや、どれくらいマウスホイールが動かされたかの情報が必要ですので、まずはこれらの情報の取得方法について解説していきたいと思います。

マウスホイール操作の情報取得

マウスホイールによりスクロールアップとスクロールダウンのどちらが行われたかや、どれくらいマウスホイールが動かされたかの情報は、tkinter.Event クラスの delta 属性により取得することができます。

前述の bind メソッドで設定したイベントハンドラ(イベント発生時に実行される関数・メソッド)では、実行時に引数として tkinter.Event クラスのオブジェクトが渡されます。

イベントハンドラに渡される情報

ですので、 イベントハンドラ内で、このオブジェクトの delta 属性を参照することで、スクロールアップ or スクロールダウンのどちらが行われたかやどれくらいマウスホイールが動かされたかの情報を取得できます。

delta の値の意味

この delta には整数の値が格納されています。

この値はマウス操作によりスクロールアップ or スクロールダウンがされたか、どれくらいマウスホイールが動かされたかに基づいて設定されます。

  • プラスの値:スクロールアップされた
  • マイナスの値:スクロールダウンされた
  • 値の絶対値:マウスホイールが動かされた量

マウスホイールによりスクロールアップされた場合は delta に正の値が、スクロールダウンされた場合は delta に負の値が格納されます。

実際に delta の値はマウスホイールを動かした量に応じて設定されます。

MacOS の場合、マウスホイールを1ステップ分動かすと delta に ±1 の値が格納されます。マウスホイールを強く動かすと複数ステップ分マウスホイールが動かされたことになり、絶対値として大きな値が delta に格納されることになります。

MEMO

Windows の場合は delta には 120 の倍数が格納されるようです

私は Windows を使っていないのではっきりとは分からないですが、おそらくマウスホイールを動かしたステップ数分 x 120 の値が格納されます

スポンサーリンク

拡大率を算出する

続いて、ここまで説明してきた delta の値から実際にリサイズを行うときの拡大率を求めていきます。

今回紹介するスクリプトでは下記のように拡大率を決定しています。

拡大率の決定
# eventはEventクラスのインスタンス
self.ratio = self.ratio * (100 + event.delta) / 100

self.ratio には、前回キャンバスに描画するために画像をリサイズした時に用いた拡大率が格納されています。

この self.ratio をマウスホイール操作イベント発生時に上記処理により event.delta の値に基づいて新たな拡大率に更新しています。

この更新後の self.ratio を拡大率として画像をリサイズしてやれば、マウスホイール操作が行われるたびに、マウスホイール操作の情報に応じて画像のリサイズを行うことができます。

上記の計算では、event.delta がプラスの値の場合は、もともとの self.ratio よりも大きな値に、event.delta がマイナスの値の場合は、もともとの self.ratio よりも小さな値に更新されるようになっているため、スクロールアップをすれば画像がより大きく、スクロールダウンすれば画像がより小さくリサイズされるような拡大率(self.ratio)に更新することができます。

MEMO

上記の計算式は Mac で動作していることを想定して作成しています

前述の通り Windows の場合は delta の値が 120 の倍数になるようですので、下記のように計算すればもしかしたら上手く動くかもしれません

# eventはEventクラスのインスタンス self.ratio = self.ratio * (100 + (event.delta / 120)) / 100

画像処理を行う

今回は画像の拡大縮小(リサイズ)を行う必要がありますので、画像を取り扱う際には PIL の Image モジュールを利用しようと思います。

画像の読み込み

画像の読み込みには PIL の Image モジュールに用意された open 関数を利用します。

画像の読み込み
from PIL import Image

# IMAGE_PATHは読み込みたい画像ファイルへのパス
self.image = Image.open(IMAGE_PATH)

画像のリサイズ

画像のリサイズには resize メソッドを利用します。

画像のリサイズ
from PIL import Image

# self.imageはPIL Imageクラスのインスタンス
self.draw_image = self.image.resize(
    (resized_width, resized_height),
    Image.BILINEAR
)

resize メソッドの引数の詳細は下記の通りです。

  • 第1引数:リサイズ後の画像サイズ
  • 第2引数:リサイズ時に使用するフィルター

resize メソッドの第1引数では、リサイズ後の画像の幅と高さをタプル形式で指定します。

resizeメソッドの第1引数

リサイズ後の画像の幅や高さは拡大率を算出するで求めた self.ratio を用いて計算します。

PIL 画像オブジェクトでは、width メンバと height メンバから幅と高さを取得することができますので、それそれぞに拡大率 self.ratio を掛けてやれば、リサイズ後の幅と高さを計算することができます。

リサイズ後サイズの計算
# self.imageはPIL画像オブジェクト
resized_width = self.image.width * self.ratio
resized_height = self.image.height * self.ratio

resize メソッドの第2引数のフィルターとしては指定可能な値の一例は下記になります。

  • Image.NEAREST
  • Image.BILINEAR
  • Image.LANCZOS

使用するフィルターによってリサイズ後の画像の画質やリサイズ時の処理時間が変わります。フィルターの詳細は下記ページから確認することが可能です。

https://pillow.readthedocs.io/en/4.0.x/handbook/concepts.html#filters

tkinter 画像への変換

PIL で読み込んだ画像や画像処理した関数はそのままでは tkinter のキャンバスには描画できません。

そのため、キャンバスに描画する前に PIL の画像を tkinter 用の画像に変換を行います。

この辺りの変換については下記ページで解説していますので、詳しく知りたい方は是非こちらのページも読んでみてください。

画像オブジェクト変換方法の解説ページアイキャッチ 【Python】PIL ⇔ OpenCV2 ⇔ Tkinter 画像オブジェクトの相互変換

要は、下記の ImageTk モジュール PhotoImage クラスのコンストラクタを実行することで PIL の画像を tkinter 用の画像に変換することができます。

tkinter画像への変換
from PIL import Image, ImageTk

# self.imageはPIL Imageクラスのインスタンス
self.tk_image = ImageTk.PhotoImage(self.image)

変換後の画像をキャンバスへの画像描画メソッドである create_image の引数 image に指定してやれば、キャンバスに画像を描画することができます。

アプリのスクリプト

ではここまで解説してきたアプリのサンプルスクリプトや動作を紹介していきたいと思います。

スポンサーリンク

アプリのサンプルスクリプト

アプリのサンプルスクリプトは下記のようになります。

画像リサイズアプリ
import tkinter
from PIL import Image, ImageTk
import time
import math

# 読み込む画像へのファイルパス設定
IMAGE_PATH = "cat.jpg"

# キャンバスのサイズ設定
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 400

class ImageZoom():
    '''画像拡大縮小アプリ'''

    def __init__(self, master):
        self.master = master
        
        # 画像の読み込み
        self.image = Image.open(IMAGE_PATH)

        # キャンバスのサイズ設定
        self.canvas_width, self.canvas_height = CANVAS_WIDTH, CANVAS_HEIGHT

        # 表示する画像の設定
        self.draw_image = self.image
        self.tk_image = None

        # ウィジェットの作成と配置
        self.create_widgets()

        # イベント設定
        self.set_events()

        # キャンバスサイズに合わせて画像の拡大率決定
        self.ratio = min(
            self.canvas_width / self.image.width,
            self.canvas_height / self.image.height
        )

        # 画像の表示
        self.resize(self.ratio)

        # 画像の描画
        self.draw()

    def create_widgets(self):
        '''ウィジェットを作成する'''

        # キャンバス作成
        self.canvas = tkinter.Canvas(
            self.master,
            width = self.canvas_width,
            height = self.canvas_height,
            highlightthickness=0
        )
        self.canvas.pack()

    def draw(self):
        '''キャンバスの中心に画像を描画する'''

        # tkinter画像オブジェクトに変換
        self.tk_image = ImageTk.PhotoImage(self.draw_image)

        # キャンバスに画像を描画
        self.canvas.create_image(
            self.canvas_width // 2, self.canvas_height // 2,
            anchor=tkinter.CENTER,
            image=self.tk_image
        )        

    def set_events(self):
        '''イベントの設定を行う'''

        # マウスホイールイベントを設定
        self.canvas.bind("<MouseWheel>", self.event_handler)

    def event_handler(self, event):
        '''マウスホイールイベント発生時の処理'''

        # マウスホイール(トラックパッド)の動きに合わせて拡大率設定
        self.ratio = self.ratio * (100 + event.delta) / 100

        # 拡大率の下限と上限を設定
        if self.ratio < 0.01:
            self.ratio = 0.01
        if self.ratio > 100:
            self.ratio = 100

        # リサイズ
        self.resize(self.ratio)

        # 画像の描画
        self.draw()

    def resize(self, ratio):
        '''画像のリサイズを行う'''

        start = time.time()

        # リサイズ後画像サイズを算出
        resized_width = self.image.width * self.ratio
        resized_height = self.image.height * self.ratio

        # リサイズ後画像サイズが0の場合は1に設定
        if resized_width < 1:
            resized_width = 1

        if resized_height < 1:
            resized_height = 1
        
        # 画像をリサイズ
        self.draw_image = self.image.resize(
            (int(resized_width), int(resized_height)),
            Image.BILINEAR
        )
        
        end = time.time()
        print("拡大率" + str(self.ratio) + "のリサイズ時間:" + str(end - start))

app = tkinter.Tk()
iz = ImageZoom(app)
app.mainloop()

アプリの設定

スクリプトの先頭付近でアプリの設定を行うことができるようにしています。

アプリの設定
# 読み込む画像へのファイルパス設定
IMAGE_PATH = "neko_before.jpg"

# キャンバスのサイズ設定
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 400

IMAGE_PATH に指定するファイルパスの設定により、アプリに表示する画像を変更することができます。

CANVAS_WIDTHCANVAS_HEIGHT ではキャンバスのサイズを設定することができます。大きすぎるとアプリが重くなるので注意してください。

アプリの動作確認

スクリプトを実行するとアプリが起動し、IMAGE_PATH に指定した画像が表示されます。画像上でマウスホイールを上下に動かしたり、トラックパッドに対して指2本でスワイプすることで画像がリサイズされます。

アプリ動作の様子

ただし、かなり処理が重くて画像の表示がゆっくりマウスホイールを動かすことをオススメします。

スポンサーリンク

アプリの動きを滑らかにする改善

先ほど紹介したスクリプトでは画像の表示がかなりカクつきます。特にスクロールアップして画像サイズが大きくなりすぎると、このカクつきが顕著に発生します。

ここからは、このカクつきの原因と対策について解説していきたいと思います。

リサイズ処理が重い

原因の一つ目はリサイズ処理が重い(時間がかかる)ことです。

原因

一般的にリサイズ処理は、リサイズ後画像のサイズが大きいほど重くなります。

マウスホイール操作を受け付けるたびにリサイズを行うようにしていますので、リサイズに処理に時間がかかると、マウスホイール操作が連続して行われた時に処理が追いつかなくなってしまい、アプリがカクつきます。

解決方法

ポイントは、キャンバスからはみ出る画像部分は表示されないが、リサイズ処理はキャンバスからはみ出る部分にも行われている点です。

キャンバスからはみ出る部分は表示されないのでリサイズしても意味がありません。その分無駄な処理が発生して時間がかかるだけです。

なので、キャンバスに表示される部分のみをリサイズしてやれば良いです。これにより、リサイズ後の画像サイズが基本的にキャンバスサイズ以下になります(誤差などあるので実際にはならない場合もある)。

リサイズ後の画像サイズが小さくなるので、その分処理時間が短くなり、アプリのカクつきを抑えることができます。

これは、リサイズによりキャンバスからはみ出ると判断した部分はリサイズ処理前にクロップしてやり、残った部分のみをリサイズするようにすることで実現することができます。

クロップ後にリサイズする様子

リサイズの前にクロップ処理を行うようにした resize メソッドが下記になります。

クロップ後にリサイズを行う
def resize(self, ratio):
    '''画像のリサイズを行う'''

    start = time.time()

    # リサイズ後画像サイズを算出
    resized_width = self.image.width * self.ratio
    resized_height = self.image.height * self.ratio

    # リサイズ後画像サイズが0の場合は1に設定
    if resized_width < 1:
        resized_width = 1

    if resized_height < 1:
        resized_height = 1

    # クロップする領域を決定していく
    sx = 0
    sy = 0
    ex = resized_width - 1
    ey = resized_height - 1

    if resized_width > self.canvas_width:

        # はみ出る場合はリサイズ後にキャンバスからはみ出ないようにリサイズ前にクロップ
        sx = (resized_width - self.canvas_width) / 2
        ex = sx + self.canvas_width
        
    if resized_height > self.canvas_height:

        # はみ出る場合はリサイズ後にキャンバスからはみ出ないようにリサイズ前にクロップ
        sy = (resized_height - self.canvas_height) / 2            
        ey = sy + self.canvas_height

    # リサイズ後後にキャンバスからはみ出ない領域の矩形を設定
    crop_size = (
        math.floor(sx / self.ratio),
        math.floor(sy / self.ratio),
        math.ceil(ex / self.ratio),
        math.ceil(ey / self.ratio)
    )

    # クロップ
    crop_image = self.image.crop(crop_size)
    
    # キャンバスに描画する画像のサイズを設定
    draw_width = int(self.ratio * crop_image.width)
    draw_height = int(self.ratio * crop_image.height)

    # 画像をリサイズ
    self.draw_image = crop_image.resize(
        (draw_width, draw_height),
        Image.BILINEAR
    )
    
    end = time.time()
    print("拡大率" + str(self.ratio) + "のリサイズ時間:" + str(end - start))

リサイズ後画像がキャンバスサイズよりも極力大きくならないようにクロップを行うようにしたことで、アプリのカクつきを抑えることができます。

上記スクリプトではリサイズにかかる時間を表示するようにしていますので、クロップを行う例と行わない例を比較してみると時間の違いが分かり易いと思います。

mainloop に戻らない

おそらくクロップ処理を追加することでアプリのカクつきはかなり抑えられるのではないかと思います。

ただしマウスホイール操作を素早く連続で行った場合はまだカクつくと思います。

次はマウスホイール操作を連続で行った場合もカクつかないように改善していきたいと思います。

原因

下記ページで解説しているように、tkinter でのウィジェットへの設定反映やキャンバスでの図形・画像の描画は mainloop の中で行われます。

tkinterのmainloopの解説ページのアイキャッチ 【Python】tkinterのmainloopについて解説

要は create_image メソッドでキャンバスに画像を描画しても即座には画面には反映されず、mainloop に処理が戻ったときに画面に反映されるということです。

一方で、イベントハンドラ内での処理が終わらないと mainloop には処理が戻りません(アプリが暇にならないと mainloop に戻らない)。

先程紹介したスクリプトでは、イベントハンドラでリサイズを実行しているので、リサイズが終わらないと mainloop に処理が戻らず、画像の描画が行われないことになります。

さらにマウスホイール操作が行われるたびにイベントハンドラが実行されるようにしているため、素早くマウスホイール操作が行われると連続してリサイズが実行され、それらのリサイズが全て終了しないと画像の描画(の画面への反映)は行われません。

イメージとしては下の図のように画面更新が遅れます。

画面更新が遅れる様子

つまり、マウスホイール操作が素早く連続で行われるとリサイズ処理が連続で実行されてしまい、その間キャンバスの画像が更新されないのでアプリがカクついているやように見えるというわけです。

解決策

これはとにかく mainloop に処理を戻すことを意識してプログラミングしてやれば解決できます。

より具体的に言うと、リサイズ処理後は(マウスホイール操作の)イベント受付を一定時間停止します。

event_handler メソッドを下記のように変更することで、リサイズ処理後のイベント受付を一定時間(10 ms)停止することができます。

イベント受付を中止する
def event_handler(self, event):
    '''マウスホイールイベント発生時の処理'''

    # マウスホイール(トラックパッド)の動きに合わせて拡大率設定
    self.ratio = self.ratio * (100 + event.delta) / 100

    # 拡大率の下限と上限を設定
    if self.ratio < 0.01:
        self.ratio = 0.01
    if self.ratio > 100:
        self.ratio = 100

    # リサイズ
    self.resize(self.ratio)

    # 画像の描画
    self.draw()

    # 一旦イベント受付終了
    self.unset_events()

    # 少しディレイを入れてイベント再受付
    self.master.after(10, self.set_events)

unset_events はイベント受付を終了するメソッドです。詳細は最後に載せている改善後のスクリプトを参照してください。

これにより、リサイズ処理の後は必ず mainloop に処理が戻るようになります。

画面更新が即座に行われる様子

これにより、連続してリサイズ処理が実行されて画像の描画が遅れることが無くなるので、アプリの動きとしてカクつきを防ぐことができます。

リサイズ中のマウスホイール操作が受け付けられなくなるので操作性が下がりそうですが、キャンバスのサイズがそこまで多くなければリサイズの時間も短いので、操作性にあまり影響は無いと思います。

アプリがカクつく時と比較すると、おそらくほとんどの方が上記でイベント受付を中止した時の方が使用感が良いと感じるのではないかと思います。

スポンサーリンク

改善後のスクリプト

改善後のスクリプト全体は下記のようになります。

改善後のスクリプト
import tkinter
from PIL import Image, ImageTk
import time
import math

# 読み込む画像へのファイルパス設定
IMAGE_PATH = "cat.jpg"

# キャンバスのサイズ設定
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 400

class ImageZoom():
    '''画像拡大縮小アプリ'''

    def __init__(self, master):
        self.master = master
        
        # 画像の読み込み
        self.image = Image.open(IMAGE_PATH)

        # キャンバスのサイズ設定
        self.canvas_width, self.canvas_height = CANVAS_WIDTH, CANVAS_HEIGHT

        # 表示する画像の設定
        self.draw_image = None
        self.tk_image = None

        # ウィジェットの作成と配置
        self.create_widgets()

        # イベント設定
        self.set_events()

        # キャンバスサイズに合わせて画像の拡大率決定
        self.ratio = min(
            self.canvas_width / self.image.width,
            self.canvas_height / self.image.height
        )

        # 画像の表示
        self.resize(self.ratio)

        # 画像の描画
        self.draw()

    def create_widgets(self):
        '''ウィジェットを作成する'''

        # キャンバス作成
        self.canvas = tkinter.Canvas(
            self.master,
            width = self.canvas_width,
            height = self.canvas_height,
            highlightthickness=0
        )
        self.canvas.pack()

    def draw(self):
        '''キャンバスの中心に画像を描画する'''

        # tkinter画像オブジェクトに変換
        self.tk_image = ImageTk.PhotoImage(self.draw_image)

        # キャンバスに画像を描画
        self.canvas.create_image(
            self.canvas_width // 2, self.canvas_height // 2,
            anchor=tkinter.CENTER,
            image=self.tk_image
        )        

    def set_events(self):
        '''イベントの設定を行う'''

        # マウスホイールイベントを設定
        self.canvas.bind("<MouseWheel>", self.event_handler)

        # Linux の場合は下記で設定できる?
        # self.canvas.bind("<ButtonPress-4>", self.event_handler)
        # self.canvas.bind("<ButtonPress-5>", self.event_handler)

        
    def unset_events(self):
        '''イベント設定の取り消し'''

        # マウスホイールイベント取り消し
        self.canvas.unbind("<MouseWheel>")

        # Linux の場合は下記で取り消しできる?
        # self.canvas.bind("<ButtonPress-4>"
        # self.canvas.bind("<ButtonPress-5>")

    def event_handler(self, event):
        '''マウスホイールイベント発生時の処理'''

        # マウスホイール(トラックパッド)の動きに合わせて拡大率設定
        self.ratio = self.ratio * (100 + event.delta) / 100

        # 拡大率の下限と上限を設定
        if self.ratio < 0.01:
            self.ratio = 0.01
        if self.ratio > 100:
            self.ratio = 100

        # リサイズ
        self.resize(self.ratio)

        # 画像の描画
        self.draw()

        # 一旦イベント受付終了
        self.unset_events()

        # 少しディレイを入れてイベント再受付
        self.master.after(10, self.set_events)

    def resize(self, ratio):
        '''画像のリサイズを行う'''

        start = time.time()

        # リサイズ後画像サイズを算出
        resized_width = self.image.width * self.ratio
        resized_height = self.image.height * self.ratio

        # リサイズ後画像サイズが0の場合は1に設定
        if resized_width < 1:
            resized_width = 1

        if resized_height < 1:
            resized_height = 1

        # クロップする領域を決定していく
        sx = 0
        sy = 0
        ex = resized_width - 1
        ey = resized_height - 1

        if resized_width > self.canvas_width:

            # はみ出る場合はリサイズ後にキャンバスからはみ出ないようにリサイズ前にクロップ
            sx = (resized_width - self.canvas_width) / 2
            ex = sx + self.canvas_width
            
        if resized_height > self.canvas_height:

            # はみ出る場合はリサイズ後にキャンバスからはみ出ないようにリサイズ前にクロップ
            sy = (resized_height - self.canvas_height) / 2            
            ey = sy + self.canvas_height

        # リサイズ後後にキャンバスからはみ出ない領域の矩形を設定
        crop_size = (
            math.floor(sx / self.ratio),
            math.floor(sy / self.ratio),
            math.ceil(ex / self.ratio),
            math.ceil(ey / self.ratio)
        )

        # クロップ
        crop_image = self.image.crop(crop_size)
        
        # キャンバスに描画する画像のサイズを設定
        draw_width = int(self.ratio * crop_image.width)
        draw_height = int(self.ratio * crop_image.height)

        # 画像をリサイズ
        self.draw_image = crop_image.resize(
            (draw_width, draw_height),
            Image.BILINEAR
        )
        
        end = time.time()
        print("拡大率" + str(self.ratio) + "のリサイズ時間:" + str(end - start))

def main():
    app = tkinter.Tk()
    iz = ImageZoom(app)
    app.mainloop()

main()

動作させると、改善前のスクリプトに比べてサクサク画像のリサイズが行われるようになったことが確認できると思います。

マウスホイールで画像をリサイズする様子

まとめ

このページでは、Python の tkinter で、マウスホイール操作(トラックパッド操作)により画像をリサイズする方法について解説しました。

tkinter ではさまざまなイベントを受け付けることが可能であり、このマウスホイール操作イベントはその一例です。

ユーザーの操作を受け付けるためにはイベント処理の知識は必須ですので、是非理解しておきましょう!

イベント処理解説ページのアイキャッチ Tkinterの使い方:イベント処理を行う

また今回は画像処理やアプリの処理を軽くする方法についても解説しました!

PIL 等の高度な画像処理が行えるライブラリで作成したオブジェクトは、そのままでは tkinter で表示できないので注意しましょう。下記ページで解説しているように変換が必要です。

画像オブジェクト変換方法の解説ページアイキャッチ 【Python】PIL ⇔ OpenCV2 ⇔ Tkinter 画像オブジェクトの相互変換

さらに、スクリプトを改善する時に出てきた mainloop もイベントを扱う上で非常に重要な関数になります。

しっかり理解しておくと、よりユーザーが操作しやすいアプリを開発しやすくなります。

mainloop については下記ページで解説していますので、是非こちらのページも読んでみていただければと思います!

tkinterのmainloopの解説ページのアイキャッチ 【Python】tkinterのmainloopについて解説

同じカテゴリのページ一覧を表示