【TkEasyGUI】Excelシートの並べ替えをつくったった（6/13追記）


ChatGPTがなあ！


# やりたかったこと
Excelをこねこねする処理があり、シート名部分一致で処理を実行するため、処理させたいシートの順番を前に持っていきたかった。  
……部分一致にさせるな案件と言えばそうなのだけども。
処理終わってからいちいち開いて直すより、GUI上で調整してから処理を開始すればよくね？　になり、初のGUI画面作成へ。

# ツール選定の経緯
初心者なので、pythonでGUI作るにはどんなフレームワークがいいのか分かりません。
天下のChatGPTに尋ねたところ、「Tkinter」や「PySimpleGUI」が軽くて良いよと教えてもらいました。

そしてPySimpleGUIを調べてみると、なるほど見やすい分かりやすいこれはいい！
https://qiita.com/dario_okazaki/items/656de21cab5c81cabe59


是非とも使いたかったのですが……なんと商用利用は有料（無償でもアカウント登録必須）になってしまったようで、類似のTkEasyGUIを使うことにしました。 
https://daisuke20240310.hatenablog.com/entry/tkeasygui
2024年4月うまれのピチピチちゃん。


# 成果物

GUIさぱらんなのでサンプルコピペしながら動作を把握した後にChatGPTに任せました。
8割くらいChatGPT製。
エラーが出たとこだけ修正しました。

PySimpleGUIで作ってくださいと言って出してくれたコードをTkEasyGUIで使うようにしました。
とはいえ完全な互換性はないようなので、複雑な処理を書きたいときは要注意。

```python
import TkEasyGUI as sg
# import PySimpleGUI as sg
import xlwings as xw
import pandas as pd

# Excelファイルを開く関数
def open_excel_file(file_path):
    try:
        read_book = pd.ExcelFile(file_path)
        sh_name = read_book.sheet_names     # sheet_names メソッドで Excel ブック内の各シートの名前をリストで取得
        app = xw.App(visible=False, add_book=False)
        wb = app.books.open(file_path)
        return wb, sh_name
    except Exception as e:
        sg.popup_error(f"Excelファイルを開く際にエラーが発生しました:{str(e)}")
        return None, []

# シートを並べ替える関数
def reorder_sheets(wb, sheet_names, from_index, to_index):
    try:
        
        # シートを移動する
        sheet_to_move = wb.sheets[from_index]
        new_name = wb.sheets[from_index].name
        # sheet_to_move.api.Copy(Before=sheet_to_move.api)
        if from_index > to_index:
            # 上に移動
            sheet_to_move.api.Copy(Before=wb.sheets[to_index].api)
            # sg.popup("moto"+str(wb.sheets[from_index+1].name))
            wb.sheets[from_index+1].delete()      
        else:
            # 下に移動
            sheet_to_move.api.Copy(After=wb.sheets[to_index].api)
            # sg.popup("moto"+str(wb.sheets[from_index].name))
            wb.sheets[from_index].delete()
        
        
        # sg.popup("moto"+str(wb.sheets[from_index].name)+"  ato"+str(wb.sheets[to_index].name))
        wb.sheets[to_index].name = new_name
        
        # ブックを保存して閉じる
        wb.save()
        wb.close()
        
        sg.popup("シートの並べ替えが完了しました。")
    except Exception as e:
        sg.popup_error(f"シートの並べ替え中にエラーが発生しました:{str(e)}")

# レイアウト
layout = [
    [sg.Text("Excelファイルを選択してください:")],
    [sg.Input(key="-FILE-", enable_events=True), sg.FileBrowse()],
    [sg.Text("シートの並べ替え:")],
    [sg.Listbox(values=[], size=(30, 6), key="-SHEETS-", enable_events=True)],
    [sg.Button("上に移動", key="-MOVE_UP-", disabled=True), sg.Button("下に移動", key="-MOVE_DOWN-", disabled=True)],
    [sg.Button("終了")]
]

# ウィンドウを作成
window = sg.Window("Excelシート並べ替え", layout)

# イベントループ
while True:
    event, values = window.read()
    if event == sg.WINDOW_CLOSED or event == "終了":
        break
    elif event == "-FILE-":
        file_path = values["-FILE-"]
        workbook, sheet_names = open_excel_file(file_path)
        window["-SHEETS-"].update(values=sheet_names)
        window["-MOVE_UP-"].update(disabled=True)
        window["-MOVE_DOWN-"].update(disabled=True)
    elif event == "-SHEETS-":
        selected_index = sheet_names.index(values["-SHEETS-"][0])
        if selected_index > 0:
            window["-MOVE_UP-"].update(disabled=False)
        else:
            window["-MOVE_UP-"].update(disabled=True)
        if selected_index < len(sheet_names) - 1:
            window["-MOVE_DOWN-"].update(disabled=False)
        else:
            window["-MOVE_DOWN-"].update(disabled=True)
    elif event == "-MOVE_UP-":
        selected_index = sheet_names.index(values["-SHEETS-"][0])
        reorder_sheets(workbook, sheet_names, selected_index, selected_index - 1)
        workbook, sheet_names = open_excel_file(file_path)
        window["-SHEETS-"].update(values=sheet_names)
    elif event == "-MOVE_DOWN-":
        selected_index = sheet_names.index(values["-SHEETS-"][0])
        reorder_sheets(workbook, sheet_names, selected_index, selected_index + 1)
        workbook, sheet_names = open_excel_file(file_path)
        window["-SHEETS-"].update(values=sheet_names)

window.close()
```

修正したのは大きく二か所。
-SHEETS-のイベント処理とシートを並べ替える関数（def reorder_sheets）です。

一つ目のところは、文字列を渡さなきゃいけない引数に永遠に数値を渡そうとするGPTがいたので手で修正。
こういうエラーが出るから直してクレメンス、と伝えたつもりでも期待通りの回答（エラーが出ないコード）が返ってこなかったのでぜんぜん使いこなせてない。

二つ目のところは、大本の処理でExcelを操作する際、xlwingsを使っていたのでなんとかしてxlwingsを使う処理にさせた次第です。
xlwingsではシート移動できないようなので、コピーを挿入してリネームするという力業で実装しました。
ひな形は作ってもらいましたが、やはりというかエラー吐くコードが出力されてしまったので最終的に自力で修正しました。

xlwingsに拘らない人はopenpyxlを使えば下のように短くかけるのでオススメ。

```python
from openpyxl import load_workbook

# Excelファイルを開く関数
def open_excel_file(file_path):
    try:
        workbook = load_workbook(file_path)
        sheet_names = workbook.sheetnames
        return workbook, sheet_names
    except Exception as e:
        sg.popup_error(f"Excelファイルを開く際にエラーが発生しました:\n{str(e)}")
        return None, []

# シートを並べ替える関数
def reorder_sheets(workbook, sheet_names, from_index, to_index):
    try:
        sheet_name = sheet_names[from_index]
        workbook.move_sheet(sheet_name, offset=to_index - from_index)
        workbook.save(file_path)
        sg.popup("シートの並べ替えが完了しました。")
    except Exception as e:
        sg.popup_error(f"シートの並べ替え中にエラーが発生しました:\n{str(e)}")
```

# おしまい
PySimpleGUIではOutputのところにprintの結果を出力できるらしいですが、今のところTkEasyGUIではできなさそうです。
そこだけが地味にぐぬぬとなりましたが、あとは大した不満はありません。
強いて言うならテーマでダークにできると嬉しいかなくらい。
ほぼほぼ同一のコード使えて便利でした。

# 6/13追記
なんか勘違いしていたようなのですが、printでログ出力できますね！  
というメモ。  
MultilineじゃなくてOutputでもできる。

```python
import TkEasyGUI as eg
from datetime import datetime

logg =eg.Multiline(size=(70, 20), font=('Courier', 10))
layout = [
    [eg.Text("printテスト")],
    [logg],
    [eg.Submit(button_text='ログ入力',key="-input-")]
    ]
print = logg.print

# Create the window
window = eg.Window("Demo", layout, finalize=True)

while True:
    event, values = window.read()

    # とじる
    if event in [eg.WINDOW_CLOSED, "-close-"]:
        # wb.close()
        break

    if event == '-input-':
        now = datetime.now()
        ymd = now.strftime("%Y/%m/%d　%H:%M:%S")   
        print('現在の時刻は　{0}　です'.format(ymd))

```

ただ、下記サイトの例のように問答無用でprintがoutputに出力することは出来ない模様。
https://shinshin86.hateblo.jp/entry/2021/09/25/092419
  
問答無用でprintが出力される仕様はクセつよそうだけども。
GUI上とパーツ分けしている部品単体で動かしたときのどちらもいいかんじにprintだせたらなあと思って悩んどります。
のでもうすこし悩みます。