6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PySimpleGUIでシンプルなフォトフレームアプリを作る

Last updated at Posted at 2021-08-29

PySimpleGUIで画像ビュアー機能を実装するTipsの備忘録(シンプルなサンプルコード付)。
画像コンポーネントをウィンドウ全体に広げて配置する方法や、クリック位置の取得方法を日本語で見つけたい人向け。

できるもの

とてもシンプルなフォトフレーム的画像ビュアー。
カレントフォルダのimagesフォルダから画像を取得し、タイトルバーのないウィンドウに表示して3秒ごとに画像を切り替える。

2021-08-29 (3).png

画面の左半分をクリックすると前の画像を表示、右半分をクリックすると次の画像を表示する。
画面上部(ウィンドウ高さの1/10まで)をクリックすると終了ダイアログを表示する。
ESCキーで即時終了する。

ラズパイとかをタッチスクリーンにつないでフォトフレームにする時の参考になるかも。

シンプルなサンプルコード

viewer.py
import PySimpleGUI as sg
from PIL import Image
import io
from glob import glob

image_dir = "images"         # 画像フォルダのパス
interval = 1000 * 3          # 画像切り替えインターバル(3秒)
exts = ["jpg", "png", "bmp"] # 表示する画像の拡張子
sg.theme("Black")            # カラーテーマ(背景色を黒にするため)
# 画面を最大化する場合
from screeninfo import get_monitors
monitor = get_monitors()[0]
window_size = (monitor.width, monitor.height)
# 画面を最大化しない場合(下の行の#を削除すること)
#window_size = (800, 600)

# リストの画像を表示し、indexにループ補正した値を返す
def show_image(window, images, index):
    image_size = (window_size[0] - 20, window_size[1] - 20) # 画像が若干見切れる対策
    # indexがマイナスならリストの上限値、indexがリスト上限より上なら0を設定する
    index = len(images) - 1 if index < 0 else 0 if index >= len(images) else index
    path = images[index]
    # 画像を縮小表示
    image = Image.open(path)
    image.thumbnail(image_size)
    bio = io.BytesIO()
    image.save(bio, format="PNG")
    window["-IMAGE-"].update(data=bio.getvalue())
    return index

def main():
    # 画像リスト取得
    images = [x for ext in exts for x in glob(f"{image_dir}/*.{ext}")] # 内包表記2回で二次元配列を一次元に平坦化
    # ウィンドウ初期化
    layout = [
        [sg.Image(key="-IMAGE-", enable_events=True, expand_x=True, expand_y=True)],
    ]
    window = sg.Window("Image Viewer", layout, size=window_size, return_keyboard_events=True, finalize=True, no_titlebar=True)
    # メインループ
    index = show_image(window, images, 0) # ループ前に先頭の画像表示
    while True:
        event, values = window.read(timeout=interval)
        if event == "Exit" or event == sg.WIN_CLOSED or event.startswith("Escape"):
            break
        elif event == "__TIMEOUT__":
            index = show_image(window, images, index + 1)
        elif event == "-IMAGE-":
            # 画像コンポーネントクリックイベント
            widget = window["-IMAGE-"].Widget
            y = widget.winfo_pointery() - widget.winfo_rooty()
            if y < (window_size[1] // 10):
                if sg.popup_ok_cancel("終了しますか?") == "OK":
                    break
            x = widget.winfo_pointerx() - widget.winfo_rootx()
            if x < (window_size[0] // 2):
                index = show_image(window, images, index - 1)
            else:
                index = show_image(window, images, index + 1)
    window.close()
    
if __name__ == "__main__":
    main()

事前準備

Python 3.7以上に下記のパッケージをpipインストールすること

  • PySimpleGUI
    • GUIパッケージ。これがないと記事が始まらない
  • Pillow
    • 画像の縮小表示に必要
  • screeninfo
    • 画面解像度を楽に取得できる
    • 最大化しない場合やサイズ決め打ちで指定する場合はサンプルコードの# 画面を最大化する場合以下4行を削除して良い

実行時のフォルダにimagesサブフォルダを作成し、その中に任意の.jpg/.png/.bmp画像いずれかを1個以上入れること

備忘録

モニタの解像度を取得する

screeninfogetmonitorsからwidthheightを取得できる。

from screeninfo import get_monitors
monitor = get_monitors()[0]
window_size = (monitor.width, monitor.height)

ウィンドウをフォトフレーム向けに設定する

Windowreturn_keyboard_eventsでキーボードイベントを有効にし、初期表示で画像を表示するためにfinalizeを設定し、no_titlebarでタイトルバーを消す。

return_keyboard_events=TrueにするとESCキーを打った時にEscape:27の文字がevent変数に格納される。
finalize=Trueでないとwindow.read()する前にshow_imageする処理がエラーになる。

window = sg.Window("Image Viewer", layout, size=window_size, return_keyboard_events=True, finalize=True, no_titlebar=True)
# メインループ
index = show_image(window, images, 0) # ループ前に先頭の画像表示
while True:
    event, values = window.read(timeout=interval)
    if event == "Exit" or event == sg.WIN_CLOSED or event.startswith("Escape"):

画像を縮小表示する

原寸で表示するだけならPySimpleGUI.Imageにファイルパスを送るだけでいいけど、アスペクト比を保ったまま縮小表示するならPillowのImage.thumbnailを使うのが便利。
記事執筆時点ではImageはGIFとPNGしか表示できないから下記のコードでJPEGBMPPNG化する処理も兼ねている。
このコードではアニメーションGIFを考慮していない。

    image = Image.open(path)
    image.thumbnail(image_size)
    bio = io.BytesIO()
    image.save(bio, format="PNG")
    window["-IMAGE-"].update(data=bio.getvalue())

PySimpleGUIのコンポーネントを広げて表示する

Imageコンポーネントは左上に画像サイズで表示され、クリックイベントも発生しない。

expand_xexpand_yを指定することでコンポーネントのサイズをウィンドウ内で広げて配置することができる。
なお、Imageコンストラクタのexpand_xexpand_y引数は数か月前に追加されたので、古いバージョンだとエラーになる。

sg.Image(key="-IMAGE-", enable_events=True, expand_x=True, expand_y=True)

Imageコンポーネントのクリック座標を取得する

今回書きたかったことはこれ。
クリック座標を取得する方法をpysimplegui click positionとかpysimplegui get mouse locationとかでWeb検索しても全然見つからなかった。
でもWidgetで裏側のTkinterにアクセスできる。

クリックしたときにwinfo pointerxなどでマウスの位置を取得すればクリック座標が分かる。

widget = window["-IMAGE-"].Widget
x = widget.winfo_pointerx() - widget.winfo_rootx()
y = widget.winfo_pointery() - widget.winfo_rooty()

今回のオチ

記事を書く調べものして気づく。

あれ?
公式サンプルのPySimpleGUI/DemoPrograms/Demo_Img_Viewer.pyで大体似たようなことやってる…。

マウスホイールスクロールで切り替えたい人はそちらを参考にどうぞ。

参考資料

6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?