PySimpleGUIでforで使えるプログレスバーを作る

要素をforで回すときにプログレスバーがほしいと思ったときに作ったもの。
基本的に最後のコードを読んだ方が早い気がする。

## PySimpleGUIについて
GUIをいい感じに作れるようになるライブラリ、これについては書いてくれている人がいくらでもいそうなので割愛。

## 2つのプログレスバースタイル
プログレスバーは処理の全体が分かっている場合のプログレスバーと、
処理がどれだけ残っているか分からない場合のマーキースタイルがある。
どちらが適しているかを使う側が意識しなくていいように、Sizedで判定させて自動で切り替えるようにした。

プログレスバーはsg.ProgressBarとして用意されているが、MAXが不明なマーキースタイルについては標準で用意されているものが見当たらなかったのでsg.Textをみっちり並べて色を変えることでそれっぽい動きをさせるようにした。

## 処理の流れ
Progressのインスタンスを作り、iterメソッドを使う。
またはProgressの第一引数にiterableを与えてProgress自体をforで回す。
iterは引数1つの使い方のみ。
本家iterで第2引数を使うような使い方をしたいなら、本家でイテレータを作って渡せばいい。

単純にプログレスバーを表示しながら要素を返すだけなのでどこにでも組み込めるはず。

```python:使い方
# 使い切り
for item in Progress(range(1000)):
    ...

# 使いまわし
progress = Progress(title="Progress Bar Title")
for item in progress.iter(range(1000)):
    ...
for item in progress.iter(range(150)):
    ...
```

## iter関数の第2引数を使ったループ
iter関数は第2引数を使う場合は第1引数はCallableで、第2引数が終了条件になる。
これで返るイテレータはnextで第一引数に与えたCallableの実行結果を返す。
実行するたびに結果の変わる関数などを与えて、それを繰り返させるという用途で使うためのもの？

window.read(timeout=0)で読み込んですぐイベントを返させる無名関数を渡して、
終了条件に(None, None)なタプルを渡すことでウィンドウが閉じたら自動で終わるようにできる。
whileになんか抵抗があるのでforで書けないかと考えた結果の産物。
これをzipで回したいiterableと一緒に回すことで対象と同期したプログレスバーになる。

```python
# よくあるwhileを使うやつ
while True:
    event, values = window.read(timeout=0)
    if event is None:
        break

# iterの第2引数を使ってforで回す方法、これにzipを組み合わせる
for event, values in iter(lambda: window.read(timeout=0), (None, None)):
    ...

# これでもいい気はする
for item in iterable:
    event, values = window.read(timeout=0)
    if event is None:
        break
```

## マーキースタイルの実装
標準にマーキースタイルのプログレスバーが見つからなかったので自作。
見た目に関しては1メモリあたりの大きさと、メモリを何分割して用意するかを、
初期化時のmarquee_block_sizeとmarquee_lengthまたはクラス変数で定義しておく。

あとはこれをどの部分が色が変わるかを順番に切り替えられるようにitertools.cycleで回させ、
keysを回してすべてのブロックを変色させる。
このとき色は要素と一緒に回っているcurrentとkeyを比較することで取得できる関数を作って入れてある。

```python:マーキースタイルの実装
    def _get_marquee(self, iterable: Iterable[T]):
        def bc(flag: bool):
            return "limegreen" if flag else "silver"
        keys = self._marquee_keys
        window = sg.Window(
            title=self.title,
            keep_on_top=True,
            layout=[
                [
                    sg.Text(s=self._marquee_block_size,
                            p=(0, 0), k=key, background_color=bc(not i))
                    for i, key in enumerate(keys)
                ]
            ]
        )
        for item, current in zip(self._iter_with_window(iterable, window), itertools.cycle(keys)):
            for key in keys:
                window[key].update(background_color=bc(key == current))
            yield item
```

## コード全文
```python
import itertools
from collections.abc import Iterable, Iterator, Sized
from typing import TypeVar

import PySimpleGUI as sg

T = TypeVar("T")


class Progress(Iterable[T]):
    title = "progress bar"
    _marquee_block_size = (7, 1)
    _marquee_keys = tuple(map(str, range(1, 7)))

    def __init__(self, iterable: Iterable[T] = None, title: str = None, marquee_block_size: tuple[int | None, int | None] = None, marquee_length: int = None) -> None:
        if title is not None:
            self.title = title
        if marquee_block_size is not None:
            self._marquee_block_size = marquee_block_size
        if marquee_length is not None:
            self._marquee_keys = tuple(map(str, range(1, marquee_length + 1)))
        self._iterable = iterable

    def __iter__(self):
        if isinstance(self._iterable, Iterable):
            return self.iter(self._iterable)
        else:
            raise ValueError("self._iterable is not iterable")

    def iter(self, iterable: Iterable[T]) -> Iterator[T]:
        if not isinstance(iterable, Iterable):
            raise TypeError(f"'{type(iterable).__name__}' object is not iterable")
        if isinstance(iterable, Sized):
            return self._get_progress(iterable, len(iterable))
        else:
            return self._get_marquee(iterable)

    def _iter_with_window(self, iterable: Iterable[T], window: sg.Window):
        for item, (event, values) in zip(iterable, iter(lambda: window.read(timeout=0), (None, None))):
            yield item
        window.close()

    def _get_progress(self, iterable: Iterable[T], max_value: int):
        progress = sg.ProgressBar(max_value)
        window = sg.Window(
            title=self.title,
            keep_on_top=True,
            layout=[[progress]]
        )
        for i, item in enumerate(self._iter_with_window(iterable, window), 1):
            progress.update(i)
            yield item

    def _get_marquee(self, iterable: Iterable[T]):
        def bc(flag: bool):
            return "limegreen" if flag else "silver"
        keys = self._marquee_keys
        window = sg.Window(
            title=self.title,
            keep_on_top=True,
            layout=[[
                    sg.Text(s=self._marquee_block_size,
                            p=(0, 0), k=key, background_color=bc(not i))
                    for i, key in enumerate(keys)
            ]]
        )
        for item, current in zip(self._iter_with_window(iterable, window), itertools.cycle(keys)):
            for key in keys:
                window[key].update(background_color=bc(key == current))
            yield item
```