2026-01-22

【備忘録】xlwings と QThread

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"]  # シート・オブジェクトをインスタンス化

参考サイト

  1. xlwings Documentation
  2. ASCII.jp:COM(Component Object Model)は古い技術だが、いまだに現役 あらためて解説する (1/2) [2023-04-23]

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

0 件のコメント: