Windows7 のスライドショー(ガジェット)をWindows10で再現したメモ


# 目指したもの

かつてのWindows7にはガジェットと呼ばれる機能があり、その中に[スライドショー](https://dynabook.com/assistpc/faq/pcdata/010836.htm)と呼ばれるものがありました。
これに近いものを作ります。
以下は作るにあたって最低限満たしたい機能です。

 - フォルダ内にある画像をランダムに映す
 - 画像が最前面に表示される
 - ウィンドウの枠がない
 - 秒数や大きさを自由に設定できる


# なぜ作ろうと思ったか

Win7時代、気持ちがすさんだときや落ち着きたいときなど、スライドショーに映る猫ちゃんの写真を眺めておりました...
Win10になってガジェット機能がなくなったときは少し落ち込みましたが、無くなったことで大きな支障が出たわけでもなく、その存在をすっかり忘れていました。

ある日とても気持ちが荒れた際「ああ、気持ちが落ち着くようなものがあれば...。」
そんな時ふとこのガジェットを思い出しました。
「最前面に猫ちゃの写真を置きたい！！癒されたい！！！！」
そして今に至ります。


# 環境
VScode
Python3.7

# つくりました
![スライドショー00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551842/3f3f43f9-b308-7ccb-47c5-bbc623db7c54.png)![スライドショー01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551842/d8b086ce-86f1-e5f4-a314-8a9dee410cbf.png)

それっぽい形でうごきました…！



# かいせつ（本文末尾のコードより）
### フォルダ内にある画像をランダムに映す
```Python
def random_pic():
    # ~~~ 省略 ~~~
    while True:
        img_list = glob.glob(pict_dir + '\\*')
        pict = random.choice(img_list)
        if pict.lower().endswith(img_types):
            return pict
```

`pict_dir` に格納されているパス（上記のコードではどこにあるか分かりやすくするため関数外で設定しています）のうち
`img_types` 内の拡張子と合致するものをランダムで選びます。


### 画像が最前面に表示される & ウィンドウの枠がない
```Python
class Slideshow_window:
    def __init__(self):
        # ~~~ 省略 ~~~
        self.window = sg.Window('', self.layout, finalize=True, no_titlebar=True, location=(self.winsize.width-360, 0), 
                    keep_on_top=True, grab_anywhere=True, element_padding=((0,0),(0,0)), margins=(0,0), alpha_channel=1)
```

 - `keep_on_top=True`
    常に最前面に表示されます。

 - `no_titlebar=True` , `margins=(0,0)`
    この２つの設定によりウィンドウ枠の無い画像だけの窓を設定することができました。

 - `location=(self.winsize.width-360, 0)`
    作業の邪魔にならないよう、画面の右上に窓が表示されるように設定しています。

 - `grab_anywhere=True`
    邪魔になったときは場所をずらせるようにしています。

 - `alpha_channel=1`
    この値を0～1の間に設定することで画像窓を透過することができます。
    場合によっては0.5などにすると後ろが透けて見えます。

[こちらのサイト](https://qiita.com/melka-blue/items/33c89a62c2392bbbd450)には大変お世話になりました。


### 秒数や大きさを自由に設定できること
```Python
class ConfigDisplay:
    # ~~~ 省略 ~~~
```

２つめのクラスで設定画面を出せるようにしました。
Set ボタンを押すことで各種設定が適応されるようになっています。
うっかり空欄にしたときにエラーを起こさないよう全てのイベントをifで取得するようにしていますが、美しくないので上手に書けるようになりたいです。


# 改善したいところ
1. 設定画面のフォルダ名
   短いパスであれば表示されますが、階層の深いところに置いている場合だとパスが長くなり途中までしか表示されません。
   自分がどのフォルダを選んだか一目でわかるようにしたいですね。
![スライドショー02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551842/8841fde9-cbb8-4d33-e730-328e07550f77.png)![スライドショー03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551842/4869a023-9b0c-54ff-c5f6-07fd2476d121.png)

2. 画像の無いフォルダを選んだときの処理
   自分が使う用なのでエラー処理をしていませんが、画像ファイルがないフォルダを選択した場合に「画像がありません」といった表示をできるようにしたいです。
3. 画像を表示するフォルダ設定を保存しておく
   設定画面で違うフォルダを選択した際、次回起動するときにもそのフォルダを選択するようにしたいです。
   プログラムの起動/終了をまたぐ場合はテキストファイルにパスを保存するなどの処理が必要なのでしょうか？
   ちょっと見当ついていません。
4. サイズ変更後の窓位置
    現在、画像表示の位置を、ディスプレイ右上に設定していますが、画像サイズ初期値に依存するため、設定からサイズを変えると…
    ![スライドショー04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551842/b3c86b45-7cee-4955-7413-f4633d4896c1.png)

    こうなります。
    サイズに関わらず右上を基点にとれるようにしたいですね。

# さいごに
自分の欲しいものを満たす機能は作れたと思います。
今後も改良を重ねたいです。
あとは美しいコーディングを学びたいですね。

#### 小言
このガジェット機能、当時需要あったのかな...

# コード

<details><summary>スライドショー.py</summary><div>

```Python
#!python3.7

import glob
import io
import random
import time

import PySimpleGUI as sg
import pyautogui
from PIL import Image, ImageTk

pict_dir = "スライドショーで表示したいフォルダ"
img_types = (".jpg",".jpeg",".jfif")

# img_typesの拡張子に一致する画像をランダムに1枚取る関数
def random_pic():
    global pict_dir
    global img_types
    while True:
        img_list = glob.glob(pict_dir + '\\*')
        pict = random.choice(img_list)
        if pict.lower().endswith(img_types):
            return pict


# イメージデータを開いて情報を取る関数
"""
参考にしたサイト
https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Img_Viewer.py
"""
def get_img_data(f, maxsize, first=False):
    im = Image.open(f)
    im.thumbnail(maxsize)
    if first:                     # tkinter is inactive the first time
        bio = io.BytesIO()
        im.save(bio, format="PNG")
        del im
        return bio.getvalue()
    return ImageTk.PhotoImage(im)


# ウィンドウの大きさを取る
def get_window_size():
    window_size = pyautogui.size()
    return window_size


# メインのウィンドウ
"""
参考にしたサイト
https://qiita.com/melka-blue/items/33c89a62c2392bbbd450
"""
class Slideshow_window:
    def __init__(self):
        self.tout = 90000                       # 画像のランダム間隔（ミリ秒）
        self.maxw = 360                         # 表示画像の最大幅
        self.maxh = 240                         # 表示画像の最大高さ
        self.winsize = get_window_size()        # 画像表示位置調整のためのウィンドウサイズ
        self.pfile = random_pic()
        # sg.theme('好みでThemeを変える')

        # ------ ウィンドウレイアウト ------ 
        self.image_col = [[sg.Image(data=get_img_data(self.pfile, (self.maxw, self.maxh), first=True), key='-IMAGE-')]]
        self.layout = [[sg.Column(self.image_col, element_justification='c')]]
        self.window = sg.Window('', self.layout, finalize=True, no_titlebar=True, location=(self.winsize.width-360, 0), 
                    keep_on_top=True, grab_anywhere=True, element_padding=((0,0),(0,0)), margins=(0,0), alpha_channel=1)

    def next_picture(self, previousfile='NONE'):
        self.pfile = random_pic()
        # ランダムで前に表示していた画像と同じものが選択される場合を避けるための処理
        while self.pfile == previousfile:
            self.pfile = random_pic()
        self.window['-IMAGE-'].Update(data=get_img_data(self.pfile, (self.maxw, self.maxh)))

    def open_config(self):
        disp2 = ConfigDisplay()
        disp2.main()
        del disp2
        self.next_picture(previousfile=self.pfile)

    def main(self):
        # キーバインドを設定する → エスケープ：終了、s：設定、n：次の写真を表示
        self.window.bind('<Escape>', 'esc') 
        self.window.bind('
', 'setting') 
        self.window.bind('<n>', 'next')

        while True:
            # timeout(ミリ秒)を設定して時間経過で timeout_key='-TIMEOUT-' のイベントを発生させる
            event, _ = self.window.read(timeout=self.tout, timeout_key='-TIMEOUT-')
            if event in (None, 'esc'):
                break
            elif event == 'setting':
                self.open_config()
            elif event == 'next' or event == '-TIMEOUT-':
                self.next_picture(previousfile=self.pfile)
        self.window.close()


#設定画面のウィンドウ
class ConfigDisplay:
    def __init__(self):
        sg.theme('GreenMono')
        self.layout2 = [
                    [sg.Text('表示先',size=(5,1)), sg.Input(default_text=pict_dir, key="-PICDIR-",size=(40, 1)), sg.FolderBrowse(size=(5,1),initial_folder=pict_dir[:pict_dir.rfind("\\")])],
                    [sg.Text("秒数",size=(5,1)),  sg.InputText(default_text=int(disp1.tout/1000) ,key="-INTIME-",size=(5,1))],
                    [sg.Text("最大幅",size=(5,1)),  sg.InputText(default_text=disp1.maxw ,key="-SIZEW-",size=(5,1)), 
                     sg.Text("最大高",size=(5,1)),  sg.InputText(default_text=disp1.maxh, key="-SIZEH-",size=(5,1))],
                    [sg.Button("Set",key="-SETOK-",size=(10,1))]]
        self.window = sg.Window("ConfigDisplay", self.layout2, no_titlebar=True, keep_on_top=True, location=(disp1.winsize.width-450, 100))

    def main(self):
        global pict_dir
        while True:
            event, value = self.window.read()
            # Set ボタンが押されたときの処理
            if event == "-SETOK-":
                if value['-PICDIR-'] != '':
                    pict_dir = value['-PICDIR-']
                if value['-INTIME-'] != '':
                    # 入力窓では秒数で入力するためミリ秒に戻す
                    disp1.tout = int(value['-INTIME-']) * 1000
                if value['-SIZEW-'] != '':
                    disp1.maxw = int(value['-SIZEW-'])
                if value['-SIZEH-'] != '':
                    disp1.maxh = int(value['-SIZEH-'])
                break
        self.window.close()


if __name__ == "__main__":
    disp1 = Slideshow_window()
    disp1.main()

```

</div></details>