xlwings は、Python を使って Excel を直接操作したり、Excel から Python コードを呼び出したりできるライブラリです[BSD ライセンス ]。VBA マクロの代替として Python で Excel の自動化(スクリプト作成、マクロ、ユーザー定義関数(UDF))を実現でき、Excel と Python のデータをシームレスに連携させ、双方の得意な部分を組み合わせて強力な自動化・データ処理を可能にします。
今回の目的
PySide6 の GUI アプリから Excel を操作しようと、Excel とやり取りをする部分をスレッド化したところ、うまくいかずにハマってしまいました。
生成 AI から「PySide6 × xlwings を使う人が必ず通る“最大の罠 ”なんだよ」なんて言われていたのに、二度も同じ間違いをしてしまったので、備忘録にまとめました。
下記の OS 環境で動作確認をしています。
Windows 11 Pro
25H2
Excel
2024 MSO
Python
3.13.9
PySide6
6.10.1
xlwings
0.33.20
簡単なサンプル(GUI 無し)
まずは、xlwings を利用した、ごく簡単な GUI 無しのサンプルです。
import xlwings as xw
if __name__ == "__main__":
wb = xw.Book() # 新しいワークブックを開く
sheet = wb.sheets["Sheet1"] # シート・オブジェクトをインスタンス化
sheet["A1"].value = "Python から書き込みました。" # 値の書き込み
「簡単なサンプル」の実行例
GUI サンプル
GUI のプッシュボタンをクリックして、簡単なサンプル(GUI 無し)と同じ処理を実行するサンプルです。
このサンプルは Excel へ文字列を書き込むだけですが、複雑な処理を加えても GUI のイベントループに影響を与えないように、「別スレッド」で Excel へアクセスする処理をするようにしています。
import sys
import xlwings as xw
from PySide6.QtCore import (
QObject,
QThread,
Signal,
Slot,
)
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import (
QApplication,
QPushButton,
QVBoxLayout,
QWidget,
)
class ExcelWorker(QObject):
def __init__(self):
super().__init__()
self.sheet = None
@Slot()
def initWorker(self):
wb = xw.Book() # 新しいワークブックを開く
self.sheet = wb.sheets["Sheet1"] # シート・オブジェクトをインスタンス化
@Slot(str)
def excel_write(self, msg: str):
self.sheet["A1"].value = msg # 値の書き込み
class SampleXlwings(QWidget):
requestWorkerInit = Signal()
requestWrite = Signal(str)
def __init__(self):
super().__init__()
# xlwings用スレッド
self.thread = thread = QThread()
self.worker = worker = ExcelWorker()
worker.moveToThread(thread)
thread.started.connect(self.requestWorkerInit.emit)
self.requestWorkerInit.connect(worker.initWorker)
self.requestWrite.connect(worker.excel_write)
thread.start()
# GUI
layout = QVBoxLayout()
self.setLayout(layout)
but = QPushButton("新規 Excel へ文字列を書き込む")
but.clicked.connect(self.request_write)
layout.addWidget(but)
def closeEvent(self, event: QCloseEvent):
if self.thread is not None:
self.thread.quit()
self.thread.wait()
if self.worker is not None:
self.worker.deleteLater()
self.worker = None
event.accept()
@Slot()
def request_write(self):
msg = "Python から書き込みました。"
self.requestWrite.emit(msg)
def main():
app = QApplication(sys.argv)
win = SampleXlwings()
win.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
なお、PySide6 では @Slot のデコレータを省略しても動作します。
「GUI サンプル」の実行例
GUI サンプルを実行すると、Excel の新しいワークブックが開きます。
新規 Excel へ文字列を書き込む ボタンをクリックすると、「簡単なサンプル」と同じように、文字列が Excel シートに書き込まれます。
注意点
ExcelWorker クラスのコンストラクタ __init__ メソッドで、以下のように xlwings のインスタンスを定義すると動作しません。
class ExcelWorker(QObject):
def __init__(self):
super().__init__()
wb = xw.Book() # 新しいワークブックを開く
self.sheet = wb.sheets["Sheet1"] # シート・オブジェクトをインスタンス化
この場合、下記のようなエラーが出ます。
pywintypes.com_error: (-2147417842, 'アプリケーションは、別のスレッドにマーシャリング されたインターフェイスを呼び出しました。', None, None)
メインスレッドで ExcelWorker のインスタンスを定義するので、ExcelWorker.__init__ も当然、「メインスレッド」で実行されます。
「別スレッド」で Excel とやり取りをしたければ、「別スレッド」を thread.start() で開始した後に、その「別スレッド」内で xlwings のインスタンスを定義する必要があります。
そういうわけで、「別スレッド」が開始されたときに初期化用のメソッド ExcelWorker.initWorker を実行するようにしています。
self.thread = thread = QThread()
self.worker = worker = ExcelWorker()
worker.moveToThread(thread)
thread.started.connect(self.requestWorkerInit.emit) # 「別スレッド」が開始されればシグナルを発行
self.requestWorkerInit.connect(worker.initWorker)
self.requestWrite.connect(worker.excel_write)
thread.start() # 「別スレッド」開始
@Slot()
def initWorker(self):
wb = xw.Book() # 新しいワークブックを開く
self.sheet = wb.sheets["Sheet1"] # シート・オブジェクトをインスタンス化
参考サイト
xlwings Documentation
ASCII.jp:COM(Component Object Model)は古い技術だが、いまだに現役 あらためて解説する (1/2) [2023-04-23]
VIDEO
にほんブログ村
#オープンソース