PySimpleGUIでexpand_x=Trueを指定したテーブルを並べていたら片方のテーブルが消えた

# 初めに

PySimpleGUIを使って，二つのファイルを左右に表示するような画面を作っていたら，ある時画面右側のテーブルが急にどこかに消えてしまうトラブルが発生した．
原因を探っていると，どうやら **2つのテーブルにexpand_x=True** を指定した状態でテーブルがスクロール表示されているタイミングで，新しくWindowを作って表示したりpopupを出したりすると，急に**左側に置いたテーブルが画面いっぱいに表示**されて，右側のテーブルが消える（＝表示幅0になって戻ってこない）ということが起こっていたらしい．

そこで，下記記載の対処法で今回は何とか繋いだのでメモ代わりに記事にしておく．

# 問題のコード

下記のコードの`open_file_view_window`関数に適当にファイルの幅がでかいやつを与えた状態で「もう1画面開く」を押すと，上記の問題が発生する．

``` Python
import PySimpleGUI as sg
from pathlib import Path
import numpy as np

def open_file_view_window(file_x, file_y):
    body_x = Path(file_x).read_text(encoding="utf-8").split("\n")
    body_y = Path(file_y).read_text(encoding="utf-8").split("\n")

    max_string_x = body_x[np.argmax(list(map(len, body_x)))]
    max_string_y = body_y[np.argmax(list(map(len, body_y)))]

    values_x = [
            [linenumber, body]
            for linenumber, body in enumerate(body_x, start=1)
        ]
    values_y = [
            [linenumber, body]
            for linenumber, body in enumerate(body_y, start=1)
        ]

    headings = ["L", "source"]
    justifications = list("ew")

    layout = [
            [
                    sg.Table(
                            values_x,
                            headings=headings,
                            text_color="black",
                            background_color="white",
                            key="-VIEWER-X-",
                            vertical_scroll_only=False,
                        ),
                    sg.Table(
                            values_y,
                            headings=headings,
                            text_color="black",
                            background_color="white",
                            key="-VIEWER-Y-",
                            vertical_scroll_only=False,
                        )
                ],
            [sg.Button("もう1画面開く", key="-OPEN-MORE-")]
        ]
    window = sg.Window("ファイルビューワ", layout, resizable=True, finalize=True)
    window.Maximize()
    # 列幅の計算のため一度画面を表示する
    window.read(timeout=0)

    str2pixel = lambda string: sg.T.string_width_in_pixels(None, string)
    lineno_width = str2pixel("0000")
    col_widths_xy = [
        [lineno_width, str2pixel(max_string_x)],
        [lineno_width, str2pixel(max_string_y)]
    ]

    table_keys = ["-VIEWER-X-", "-VIEWER-Y-"]
    table_xy = [window[key] for key in table_keys]
    for table in table_xy:
        table.expand(expand_x=True, expand_y=True)
        table_widget = table.Widget
        for column, anchor in zip(headings, justifications):
            table_widget.column(column, anchor=anchor)

    while True:
        # 画面サイズの変更に対応できるようwhileループ中で列幅を指定する
        for table, col_widths in zip(table_xy, col_widths_xy):
            table_widget = table.Widget
            for column, width in zip(headings, col_widths):
                table_widget.column(column, width=width)

        event, values = window.read(timeout=0.1)
        if event is None:
            break
        elif event == sg.WIN_CLOSE_ATTEMPTED_EVENT:
            break
        elif event == "-OPEN-MORE-":
            open_file_view_window(file_x, file_y)
    window.close()
```

# 対処法
結論としては，「上位ウィンドウのテーブルの列幅を縮める関数とそれをもとに戻す関数をもらって，画面表示前後で呼び出す」という方法をとった．
下記が改修後のコード．
（ややこしいが，`_get_backup_and_reexpand_func`関数の返り値はテーブルの幅を縮める関数で，縮めた幅をもとに戻すための関数を返り値として返す　というものになっている）

``` Python
import PySimpleGUI as sg
from pathlib import Path
import numpy as np

def _get_backup_and_reexpand_func(table_objects, other_func=None):

    def _take_widths_backup():
        # 列幅のバックアップを取りながら縮める関数
        width_backups = []
        for table in table_objects:
            table_widget = table.Widget
            col_widths = []
            for column in table_widget["displaycolumns"]:
                col_widths.append(table_widget.column(column)["width"])
                table_widget.column(column, width=1)
            width_backups.append(col_widths)
        if other_func is not None:
            other_reexpand_func = other_func()

        # 列幅をもとに戻すための関数
        def _expand_columns():
            if other_func:
                other_reexpand_func()
            for table, col_widths in zip(table_objects, width_backups):
                table_widget = table.Widget
                for column, width in zip(table_widget["displaycolumns"], col_widths):
                    table_widget.column(column, width=width)

        return _expand_columns

    return _take_widths_backup


def open_file_view_window(file_x, file_y, backup_width_and_reexpand_func=None):
    body_x = Path(file_x).read_text(encoding="utf-8").split("\n")
    body_y = Path(file_y).read_text(encoding="utf-8").split("\n")

    max_string_x = body_x[np.argmax(list(map(len, body_x)))]
    max_string_y = body_y[np.argmax(list(map(len, body_y)))]

    values_x = [
            [linenumber, body]
            for linenumber, body in enumerate(body_x, start=1)
        ]
    values_y = [
            [linenumber, body]
            for linenumber, body in enumerate(body_y, start=1)
        ]

    headings = ["L", "source"]
    justifications = list("ew")

    layout = [
            [
                    sg.Table(
                            values_x,
                            headings=headings,
                            text_color="black",
                            background_color="white",
                            key="-VIEWER-X-",
                            vertical_scroll_only=False,
                        ),
                    sg.Table(
                            values_y,
                            headings=headings,
                            text_color="black",
                            background_color="white",
                            key="-VIEWER-Y-",
                            vertical_scroll_only=False,
                        )
                ],
            [sg.Button("もう1画面開く", key="-OPEN-MORE-")]
        ]

    # 列幅のバックアップを取る
    if backup_width_and_reexpand_func:
        reexpand_func = backup_width_and_reexpand_func()
    window = sg.Window("差分比較", layout, resizable=True, finalize=True)
    window.Maximize()
    window.read(timeout=0)
    # 縮めていた列幅をもとに戻す
    if backup_width_and_reexpand_func:
        reexpand_func()

    str2pixel = lambda string: sg.T.string_width_in_pixels(None, string)
    lineno_width = str2pixel("0000")
    col_widths_xy = [
        [lineno_width, str2pixel(max_string_x)],
        [lineno_width, str2pixel(max_string_y)]
    ]

    table_keys = ["-VIEWER-X-", "-VIEWER-Y-"]
    table_xy = [window[key] for key in table_keys]
    for table in table_xy:
        table.expand(expand_x=True, expand_y=True)
        table_widget = table.Widget
        for column, anchor in zip(headings, justifications):
            table_widget.column(column, anchor=anchor)

    while True:
        # 画面サイズの変更に対応できるようwhileループ中で列幅を指定する
        for table, col_widths in zip(table_xy, col_widths_xy):
            table_widget = table.Widget
            for column, width in zip(headings, col_widths):
                table_widget.column(column, width=width)

        event, values = window.read(timeout=0.1)
        if event is None:
            break
        elif event == sg.WIN_CLOSE_ATTEMPTED_EVENT:
            break
        elif event == "-OPEN-MORE-":
            open_file_view_window(
                    file_x, file_y,
                    _get_backup_and_reexpand_func(table_xy, backup_width_and_reexpand_func)
                )
    window.close()

if __name__ == "__main__":
    open_file_view_window("file_diff_viewer_bk.py", "file_diff_viewer.py")
```

上記改修によって，列幅が勝手に変わることなく，新しい画面を開いていけるようになった．
注意点として，この方法で影響がないのは枝分かれしないよう新しい画面をどんどん開いていった場合のみで，例えば
ウィンドウAからウィンドウBを開いた状態で、さらにウィンドウAからウィンドウCを開く　というような場合は，ウィンドウCを開いたタイミングで，ウィンドウBの画面がおかしくなることがあるので注意してほしい．
（というかこういった場合本来どう対処すべきなのか知ってる人がいたら教えてほしい．）


以上．