前回からの続き。
半日ほど調べて、LibreOffice Calc では、印刷用のページテンプレート(ヘッダーやフッターなど)を UNO API からは操作できないことがわかりました。ドラッグ&ドロップでコピーできないのも変だけどマクロからも操作ができませんね。
仕方がないので、あらかじめ印刷用のページテンプレートを設定したファイル(*.ots)を作っておいて、それを利用することにします。別に普通の Calc ファイル(*.ods)を使っても作れます。
ページスタイルの作成
あれこれ探すとページスタイルが作れるようになりました。
「スタイルペインを開く」→「ページスタイルアイコンをクリック」という手順でページスタイル(いわゆるシートに紐づけるスタイル)を作成&設定できます。デフォルトで「標準」と「レポート」というスタイルが作っていあるので、ここでは「帳票テンプレート」というスタイルを作成しました。

ヘッダー、フッターの仕方は Excel の設定と似たような感じです。

確か Excel では印刷プレビューから飛べたはずなのですが、LibreOffice Calc では飛べません。まあ、あまり印刷をしないのかもしれません。ただし、文書として PDF に変換することが多いでしょうから、こういう印刷スタイルも整備してほしいかなと。

このファイルを「report-template.ots」あるいは「report-template.ods」のように保存しておきます。テンプレートファイルの *.ots にしてしまうと、
C:\Users\Tomoaki\AppData\Roaming\LibreOffice\4\user\template\report-template.ots
こんな感じの LibreOffice のテンプレート用のフォルダーに保存されてしまうので結構面倒です。テンプレート文書とはいえ Python マクロから参照するだけなので、プログラムと同じ場所に置いたほうが実験がしやすいと思います。
テンプレート文書を開いて、別名で閉じる
いろいろ試したのですが、以下の手順が一番安定しています。
- テンプレート文書を開く
- データ等を書き込む
- 別名でファイルを保存して閉じる
途中の動作を確認しようかと思って、テンプレート文書のほうの Calc を開いたままにしようかと思ったのですが、Calc が落ちます。どうやら、UI で何か受け付けようとしてうまくいっていないようです。マクロを動かしている Calc を Hidden にしておくと安定します。
import uno
from pathlib import Path
# サーバーの起動
# & "C:\Program Files\LibreOffice\program\soffice" --accept="socket,host=localhost,port=2002;urp;" --norestore --nologo
# PowerShell での起動
# & "C:\Program Files\LibreOffice\program\python" .\calcReport002.py
"""
テンプレートから新規ドキュメントを作成して Calc レポートを生成するサンプル。
手順:
1. 「帳票テンプレート」(report-template.ots) をテンプレートとして新規ドキュメントを開く
2. サンプルデータの行数に合わせて明細行の書式を複製
3. データを書き込む
4. 別名で保存
"""
# 設定: テンプレートと出力先
TEMPLATE_NAME = "report-template.ots"
OUTPUT_NAME = "report-output.ods"
TEMPLATE_SHEET_NAME = "Template" # テンプレート内の帳票シート名
REPORT_SHEET_NAME = "Report" # 必要ならこの名前のシートを使用(存在しなければ先頭シートを使う)
def to_file_url(path: Path) -> str:
return uno.systemPathToFileUrl(str(path.resolve()))
def connect_desktop():
local_ctx = uno.getComponentContext()
resolver = local_ctx.ServiceManager.createInstanceWithContext(
"com.sun.star.bridge.UnoUrlResolver", local_ctx
)
ctx = resolver.resolve(
"uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext"
)
smgr = ctx.ServiceManager
desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
doc = desktop.getCurrentComponent()
if doc is None:
raise RuntimeError("アクティブな Calc ドキュメントが見つかりません")
return desktop, doc
def load_template(desktop, template_path: Path):
url = to_file_url(template_path)
props = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
props[0].Name = "Hidden"
props[0].Value = True
return desktop.loadComponentFromURL(url, "_blank", 0, props)
def ensure_rows_with_format(sheet, template_row_idx: int, start_row: int, rows_needed: int, last_col: int):
template_range = sheet.getCellRangeByPosition(0, template_row_idx, last_col, template_row_idx)
source_addr = template_range.getRangeAddress()
template_height = sheet.getRows().getByIndex(template_row_idx).Height
for i in range(rows_needed):
target_row = start_row + i
if target_row == template_row_idx:
continue
dest_addr = uno.createUnoStruct("com.sun.star.table.CellAddress")
dest_addr.Sheet = source_addr.Sheet
dest_addr.Column = 0
dest_addr.Row = target_row
sheet.copyRange(dest_addr, source_addr)
sheet.getRows().getByIndex(target_row).Height = template_height
def write_data(sheet, data, start_row: int = 1, start_col: int = 0):
for r, row in enumerate(data):
for c, value in enumerate(row):
cell = sheet.getCellByPosition(start_col + c, start_row + r)
if isinstance(value, (int, float)):
cell.setValue(value)
else:
cell.setString(str(value))
def main():
base_dir = Path(__file__).resolve().parent
template_path = base_dir / TEMPLATE_NAME
output_path = base_dir / OUTPUT_NAME
sample_data = [
["001", "Alice", "Sales", 1200.5],
["002", "Bob", "Marketing", 980.0],
["003", "Carol", "Engineering", 1540.75],
["004", "Dave", "Support", 760.25],
]
if not template_path.exists():
raise FileNotFoundError(f"テンプレートが見つかりません: {template_path}")
desktop, _ = connect_desktop()
doc = load_template(desktop, template_path)
# 帳票シートを取得(指定名がなければ先頭シート)
sheets = doc.Sheets
if sheets.hasByName(REPORT_SHEET_NAME):
report_sheet = sheets.getByName(REPORT_SHEET_NAME)
elif sheets.hasByName(TEMPLATE_SHEET_NAME):
report_sheet = sheets.getByName(TEMPLATE_SHEET_NAME)
else:
report_sheet = sheets.getByIndex(0)
# データ件数に合わせて行を用意(ヘッダー1行 + データ行)
last_col = len(sample_data[0]) - 1 if sample_data else 0
ensure_rows_with_format(
sheet=report_sheet,
template_row_idx=1,
start_row=1,
rows_needed=len(sample_data),
last_col=last_col,
)
# データを書き込み(1行目はヘッダー想定)
write_data(report_sheet, sample_data, start_row=1, start_col=0)
# シート名を変更
report_sheet.Name = "給与レポート"
# 別名で保存
store_props = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
store_props[0].Name = "FilterName"
store_props[0].Value = "calc8"
doc.storeToURL(to_file_url(output_path), store_props)
print(f"レポートを保存しました: {output_path}")
# 後処理
try:
doc.dispose()
except Exception:
pass
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"error: {e}")
後で、もう少し業務用の Python コードを整理しますが、肝は以下の部分です。
def load_template(desktop, template_path: Path):
url = to_file_url(template_path)
props = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
props[0].Name = "Hidden"
props[0].Value = True
return desktop.loadComponentFromURL(url, "_blank", 0, props)
ここで Hidden の値を False にすると Calc が表示されるようになりますが、頻繁に Calc が落ちます。UNO API で操作しているときは、UI を止めたほうがよさそうです。
帳票作成
作成できた report-output.ods がこれです。

印刷プレビュー

ヘッダー、フッターの操作はできないので、あらかじめテンプレート文書に入れておきます。マクロから操作できないのは痛いのですが、社名などは固定になるし印刷したときの日時はヘッダー/フッターの機能として用意されているので、あまり不便にはならないでしょう。
複数ページになったときの設定とかは Excel と同じなので、これもいけると思います。
次は、裏にデータシートを設定して関数とかでセル参照をしているときの動作ですね。請求書とかを作るときに便利な方法です。
