LibreOffice Base の使い方 フォームでのマクロの利用

プログラミング

概要

以前に説明したLibreOffice Baseの使い方に少し変更を加えて説明します。
リンク先に練習用のファイルとして前回説明した内容のファイルがダウンロードできます。

LibreOffice Base の使い方 基礎編
LibreOffice Base の基本的な使い方を説明します。この内容を理解するとデータベースの画面を作れるようになります。売上を売上番号(伝票)ごとに表示する。フォームを例にしています。

LibreOfficeでPythonのマクロを使う方法は、以下のリンクを参考にしてください。

LibreOfficeのPythonマクロを使う
LibreOfficeでPythonのマクロを使うための準備と注意点、使い方を説明します。

ここで作ったBaseのファイルとマクロは、こちらです。
Base練習用ファイル20241201.zip

以前のものは、マクロなどを使わずに基本的な機能のみでフォームを使う例を説明しました。

ですので、テーブルの内容をそのままフォームに表示という事になってます。

実際に使う場合には、データそのままに加えてそこから計算された値も表示したり、
(数量 x 単価 = 合計金額 などの計算)
入力された値に対してチェックをしたり、
(購入量 10個未満は認められないなどの場合)
数量がほかのテーブルと連動している場合、
(購入があると在庫が減るなどの場合)
などフォーム上でデータに対して処理が必要なことが多々あります。

そのような場合に、必要なこと基本を説明します。

今回は、サブフォームの単価と(購入した)数量から(商品ごとの)購入額と、
明細中の商品の合計金額を計算して表示する例を作ります。

テーブルの変更

まず、前回作ったBaseのファイルをコピーします。
無い方は、ダウンロードして使ってください。
これに変更を加えていくことにします。

売上明細_テーブル

売上明細_テーブルの内容は以下の通りです。

売上明細_id(主キー):BIGINT
商品_id(主キー):BIGINT
売上明細_数量:BINGINT

これに、”売上明細_購入額” を追加します。
これは、商品_テーブル.商品_単価 x 売上明細_テーブル.売上明細_数量 から計算された商品単位の購入額を入れるものです。
本来ですと表示の度に計算しているのでこのようなデータは必要ないのですが
サブフォームの仕様上データとして保存するほうが処理が簡単になるのでこの項目を追加します。

売上明細_id(主キー):BIGINT
商品_id(主キー):BIGINT
売上明細_数量:BINGINT
売上明細_購入額 : BIGINT

となります。

余談です。
リレーションを設定している場合、リレーションを設定してある項目(リレーションの画面で線のひかれているもの)を変更(名前の変更など)する場合は、一度リレーションを切断してから変更しないとデータベースが壊れる場合があるので気を付けてください。
今回は、当てはまらないのでそのままで大丈夫です。

フォームの変更

サブフォーム中に、”購入額”として商品ごとの購入額を表示する列と
明細中の購入した商品の”合計”を表示する欄を作ります。

購入額

購入額の列を追加します。

変更前は、以下の様になっています。

これを変更します。

サブフォーム上部のヘッダー部分を右クリックすると”列の挿入”というメニューが開きます。

“列の挿入”を選択して”テキストボックス”を選択してください。

そして追加された列”テキストボックス 1”の上で右クリックして”列”を選択してください。

そうすると以下のような”列”のプロパティが開きます。

そこで名前を“テキストボックス_購入額”とし、
タイトルを”購入額”とします。これは、ヘッダーに表示される文字です。

また、余談ですがフォーム編集画面からコントロールを右クリックで選択して”名前”を選択したときの名前と、
“コントロールのプロパティ”を開いてプロパティの画面から確認する名前が違う場合があります。
マクロで使うのは、コントロールのプロパティで確認できる名前です。
前者にも何か役割があるのだと思いますがは、なぜ二通りあるのか理由はわかりません。

合計

明細にある全商品の合計金額を表示する欄を作ります。

ラベルを追加して”合計”と表示させます。
通貨フィールドを追加して、“通貨フィールド_合計”と名前を変更します。
他の項目は、桁区切りとか右詰めにするとか小数点何桁にするとかありますのでお好みで設定してください。

“データ”タブで”データフィールド”を“売上明細_購入額”とします。
これは、”売上明細_テーブル”の”売上明細_購入額”のデータをこの列のデータとするという事です。

再計算ボタン

再計算用のボタンを追加します。

合計欄の下に再計算ボタンを追加しました。
このボタンを押すことでサブフォームに入力したデータを元に合計欄等を再計算します。

名前を“プッシュボタン_再計算”に、
タイトルを”再計算”にしてください。

ボタンにマクロを登録するのですが
他のやり方としてサブフォームのデータが書き換わったときのイベントにマクロを登録する
と言う方法も試したのですが使った感触があまり良くなかったのでボタンに登録する方法にしました。
やってみたところデータが書き換わるのは、サブフォームでセルのデータが書き換わった時ではなく行が書き換わったとき、つまりカーソルが入力した他の行に移動したときにイベントが発生するからです。
この辺りは、伝わりづらいかとおもいますのでご自分でいろいろと試してみてください。

好みで何か他のイベントに登録する方法もあるかもしれません。

完成したフォーム

ここまでの作業で、コントロールの配置が下のフォームにと同様ならOKです。

マクロ

base_macros.py に、マクロを書きます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# coding: utf-8
from __future__ import unicode_literals
import uno
from scriptforge import CreateScriptService

def start(*args):
    #recalc_subform(e)
    pass

def msgbox(str):
    bas = CreateScriptService("Basic")
    bas.MsgBox(str)
    bas.Dispose()

def recalc_subform(e=None):
    
    total_expenses = 0 # 総費用

    oDoc = XSCRIPTCONTEXT.getDocument()
    oForm = oDoc.DrawPage.Forms.getByName("フォーム")  # メインフォームの名前を指定

    # サブフォームを取得
    oSubForm = oForm.getByName("サブフォーム")  # サブフォームの名前を指定
    
    # データベースと接続
        # データベース接続を取得
    connection = oForm.ActiveConnection

    # SQLステートメントを作成
    statement = connection.createStatement()
    
    # 行セットのインターフェースを取得
    rowset = oSubForm.queryInterface(uno.getTypeByName("com.sun.star.sdbc.XRowSet"))

    # 行の更新を開始
    rowset.first()  # 最初の行に移動
    while True:
        try:
            # 各列の値を設定
            columns = rowset.getColumns()
            if columns:
                # 商品_id を取得
                product_id = columns.getByIndex(1).Value
                
                # 商品テーブルからデータを取得するSQLクエリ
                sql = f"SELECT 商品_単価 FROM 商品_テーブル WHERE 商品_id = {product_id}"
                unit_price_set = statement.executeQuery(sql)
                while unit_price_set.next():
                    unit_price = unit_price_set.getInt(1)

                # 2番目の列の値を取得
                quantity = columns.getByIndex(2).Value

                # 3番目の列に購入額を設定
                total_cost = unit_price * quantity
                total_expenses += total_cost
                columns.getByIndex(3).updateInt(total_cost)
                
                
                rowset.updateRow()  # 行を更新して反映させる

            # 次の行に移動
            if not rowset.next():
                break
        except Exception as e:
            print("エラー:", e)
            break

    # 更新後にリロードを行う
    oSubForm.reload()

    # サブフォームの合計値を設定(通貨フィールドなどがある場合)
    try:
        oSum = oForm.getByName("通貨フィールド_合計")
        oSum.Value = total_expenses
        
    except Exception as e:
        print("通貨フィールドの設定エラー:", e)
    

15行目からの
def recalc_subform(e=None)
からが解説する関数(マクロ)です。

(e=None)の部分は、ボタンのイベントからから情報を受け取るためにeが無いとエラーになります。
デフォールト値としてNoneを設定しておきます。

19行目

oDoc = XSCRIPTCONTEXT.getDocument()

oDocに現在開いているドキュメントつまりBaseのドキュメントを取得します。

20行目

oForm = oDoc.DrawPage.Forms.getByName("フォーム")

フォーム名からフォームを取得します。

23行目

oSubForm = oForm.getByName("サブフォーム")

サブフォーム名からサブフォームを取得します。

27行目

connection = oForm.ActiveConnection

フォームに接続しているデータソースと接続します。

30行目

statement = connection.createStatement()

sqlクエリを実行するためのオブジェクトを取得します。

33行目

rowset = oSubForm.queryInterface(uno.getTypeByName("com.sun.star.sdbc.XRowSet"))

サブフォームからサブフォーム中のデータセット(使用するデータのまとまり)を操作するために”XRowSetインターフェース”を実装したオブジェクトを取得します。

36行目

rowset.first()

サブフォーム中のデータセットの最初の行へ移動します。

37~67行目
データセットからデータを一行づつ取得して
そこから列ごとにデータを取得、つまり”商品_id”(1列目)と”商品_単価”(2列目)を取得して
計算して”商品明細_購入額”(3列目)としてへ計算した値を更新しています。
これを行がなくなるまで繰り返します。
その間に、並行して合計金額を加算しながら計算しています。

40行目

columns = rowset.getColumns()

1行分のデータを取得。

41行目

if columns:

データがあれば処理を続ける。

43行目

product_id = columns.getByIndex(1).Value

“商品_id”を取得。

46~49行目

sql = f"SELECT 商品_単価 FROM 商品_テーブル WHERE 商品_id = {product_id}"
unit_price_set = statement.executeQuery(sql)
while unit_price_set.next():
    unit_price = unit_price_set.getInt(1)

46~47行目
“商品_テーブル”から”商品_id”が43行目で取得した”product_id”のものの”商品_単価”を取得。
unit_price_set にsqlの条件に合うデータが複数取得される。
実際は、”商品_id”は重複しないので1件のみ。

48~49行目
複数行ある体でwhileで行ごとにデータを取得。
実際には、1行しかありえないのでデータは1行分しかない。
sql文で取得したデータは、”商品_単価”のみなので1列しかデータはない。
結果、”unit_price”には”商品_単価”の値が入る。

52行目

quantity = columns.getByIndex(2).Value

サブフォームの2行目から”商品_単価”を取得する。

55~57行目

total_cost = unit_price * quantity
total_expenses += total_cost
columns.getByIndex(3).updateInt(total_cost)

55行目
商品単位の購入額を計算している。
56行目
明細の総合計を加算している。
57行目
サブフォームの購入額(3列目)のデータを更新している。

63~64行目

if not rowset.next():
   break

次の行があれば処理を続ける。
無ければ(while)ループを抜ける。

70行目

oSubForm.reload()

サブフォームのデータをデータベースから再度読み込む。
このことによって、画面が更新されます。

73~79行目

    try:
        oSum = oForm.getByName("通貨フィールド_合計")
        oSum.Value = total_expenses
        
    except Exception as e:
        print("通貨フィールドの設定エラー:", e)

合計欄に明細の商品の合計金額を書き込みます。

74行目
名前で通貨フィールドを取得します。

もし、うまく書き込みが出来なければ”通貨フィールド_合計”が”サブフォーム”の下に作られている場合があるので確認してください。
“oForm”から名前で取得しているので”oSubForm”に所属している場合は、取得できないわけです。

75行目
total_expensesでループ中で取得した合計金額が入っています。

マクロの登録

ボタンへの登録

フォームの編集画面で
“再計算”ボタンのコントロールのプロパティを開きます。

“イベント”タブの”実行時”に登録するので右端の”…”ボタンをクリックします。

“アクションの割り当”で”実行時”を選択したまま、”マクロ”ボタンをクリックします。

今回は、base_macros.pyに書かれた”recal_subform”という関数を登録します。
以上で、ボタンをクリックしたときに”recalc_subform”という関数(マクロ)が実行されます。

サブフォームへの登録

ボタンに登録しただけですと、起動したときに明細の総合計を表示する”合計”の欄が空白になっています。
ですので起動したときにすぐ計算して書き込むようにします。

“フォームナビゲータ―”を開いて”サブフォーム”の属性を開いてください。

ここでイベントの”読み込む時”に”recalc_subform”を登録します。
これで起動時に”合計”が表示されるようになります。

まとめ

ドキュメントからのコントロールの取得とデータベースへのインターフェースの取得方法を説明しました。

XSCRIPTCONTEXTからドキュメントを取得します。
ドキュメントのフォームからデータベースとの接続を取得して、
さらにそこからデータベースへのインターフェースを取得しました。

そして、コントロールやサブフォーム中へのコントロールへの書き込み方法を説明しました。

なお、考え方を説明するためのものなのでエラーハンドリングやバリデート(データの検証)などは省略しています。

ここまでできればBaseの大概の操作ができるのでは、無いでしょうか。

普段の生活や仕事にぜひ使ってみてください。

追記

別窓でマスタを表示とか試してみたのですけど、
どうやらできなくは無いかもしれないけどかなりめんどくさいです。

実力不足のせいで簡単な方法を理解していないだけってこともありえます。

今わかっていることは、マスタ画面などの別画面を開こうとすると現在開いているフォームがデータをロックしているらしくデータが使えない。画面は、開く。

全く別の画面から開くことはできる。つまり、同じアプリを2つ起動するわけです。
で、そっちの方でマスタ画面を開く。
これは、開いてデータを見ることはできるけど保存できない。
つまり、更新できないのでマスタ画面を開く意味がない。

なんてことがあるので、回避方法はあるのかもしれないですが
素直に使用中のフォームを一旦閉じて、マスタ用のフォームを開き直しデータ変更する。
その後、もとのフォームを開き直す。
というような運用のほうが確実だし、手間もかからないようです。

そもそもそういうことを想定した作りになっているのだと思われます。

同一のフォーム中にマスタ用のサブフォームを用意して、表示非表示を切り替えたりとか
ダイアログを使うとか色々考えられますがやるたびに色々問題が出てきて時間ばかりかかるので
ここまでとします。

なにかご存知の方、コメント欄にでも書き込んでください。

Commnts