2026/01/10

Domino 体力測定:#3)ビュー操作 ③ - フィールド数による影響

背景と目的

前回はビュー操作を行う 2 つのメソッドの基本的な特性の測定結果を紹介しました。そのテストでは、テストデータの文書は小さく軽量、検索用のビューもシンプルでした。より実運用に近い検証となるよう、追加でテストしたいと思います。

今回はフィールド数を変化させ、レスポンスにどのような影響があるのか測定します。


測定環境

まず、測定に使用する PC は前回と同じものを使用します。SSD 搭載の今どきの環境の想定です。

フィールド数とレスポンスの関係を調べることから、今回のテスト DB は、一定のダミーフィールドの個数を変化させた5 種類を用意します。ダミーフィールドには、読み込み用と同じ 100 Byte のテストを設定します。

DB 名 テスト文書の状態 文書サイズ
000 前回と同じ。188 Byte
050 000 の文書にダミーフィールドを 50 個追加(Text_00 ~ Text_31)5288 Byte
100 000 の文書にダミーフィールドを 100 個追加(Text_00 ~ Text_63)10388Byte
150 000 の文書にダミーフィールドを 150 個追加(Text_00 ~ Text_95)15488Byte
200 000 の文書にダミーフィールドを 200 個追加(Text_00 ~ Text_C7)20588Byte

参考までですが、ダミーフィールド作成する部分のコードは次の通りです。

      '取得用の値
      s = Right("0000000000" & s, 10)
      s = s & s & s & s & s & s & s & s & s & s
      Call nd.Replaceitemvalue("Text", s)


      'ダミーフィールド作成(viWait は作成する個数)
      For i = 0 To viWait - 1
         Call nd.Replaceitemvalue("Text_" & Right("00" & Hex(i), 2), s)
      Next

250 個作成しようとするとエラーとなりました。9.0.1 FP8 以降で対応した LargeSummary を有効化する必要があるので、今回は 200 個までで検証としています。


手順

測定手順は前回と全く同じです。ビューに対して 20 回の検索を行い、取得したコレクションからエントリと値を取得する操作を 50 回行います。測定回数も同じ 10 回とします。

この測定を DB 050 ~ 200 まで順に行います(DB 000 については前回の結果を流用)。


結果

測定結果は次の通りでした。

横軸を文書サイズに設定し、各測定項目と DB 毎の結果をグラフ化すると次の通りです。

D3、E5 のグラフは右肩上がりの直線となっており、フィールド数に比例しているといえます。それ以外のグラフは水平で、フィールド数の影響は受けないことがわかります。


考察

今回の測定結果より導くことができるビュー操作の特徴を整理します。


◇ ビュー検索はフィールド数に依存しない

ビューの検索を行う D1、E1 ともフィールド数増減の影響を受けず、ほぼ一定の処理時間となっています。また、コレクションからエントリを取得する処理(D2、E2)も同様です。さらに、NotesViewEntry から NotesDocument を取得する処理(E4)も一定です。

これらの結果より、コレクション、コレクション内のエントリ操作、NotesDocument オブジェクトの取得までは、文書単位で処理をしており、文書内のフィールド数には依存しないことがわかります。

ここまでの処理においては、1 文書は 1 文書で、その内部の状態は関係ないということですね。


◇ 値取得はフィールド数に依存

NotesDocument から値を取得する処理(D3、E5)では、フィールド数に比例して、処理時間が増加します。

今回の検証データは、読み込む Text フィールド作成後、ダミーフィールドを追加しました。内部的に作成順で記録されていると仮定すると、Text フィールドは文書のほぼ先頭に存在します。それでも、フィールド数に比例した結果になるということは、フィールドにアクセスしたタイミングで、すべてのフィールドの一覧を作成していると考えられます。

アプリの仕様変更を行うと無用となるフィールドが発生することがあります。実害がないので、ついつい残しがちなのですが、パフォーマンスという観点でいうと削除しておいた方がよさそうですね。

なお、今回の検証では、ダミーフィールドのサイズを固定したため、フィールド数 ≒ 文書サイズとなっています。今回の特性の要因が、文書サイズである可能性が残ります。


◇ 逆転の可能性

文書から値を取得することだけに限定すると、D1 ~ D3、E1 ~ E3 までで実現できます。この部分に絞って、結果を整理すると次のようになります。

GetAllDocumentsByKey(青)のグラフは右肩上がりで、GetAllEntriesByKey(オレンジ)は水平です。ダミーフィールドなしでは、ほぼダブルスコアですが、200 個ではずいぶん差が小さくなっています。さらにフィールドを増やしたテストを行えば逆転することがあるかもしれません。

なお、今回の検証では、ダミーフィールドのサイズを 100 バイトにしています。そのため 200 個以上フィールドを増やせませんでした(Summary フィールドの上限)。セットする値を小さくし、フィールド数を増やした場合や Domino 9.0.1 FP8 以降で対応した LargeSummary 環境での特性も気になりますね...


まとめ

今回の検証では、GetAllEntriesByKey で検索し ColumnValues で値を取得する限りにおいては、フィールド数に依存せず同じレスポンスになることがわかりました。逆に NotesDocument オブジェクトを介して値を取得するとフィールド数に比例した処理時間が必要とります。ただ、ここまでの検証では GetAllEntriesByKey の方が早くなるパターンは見つかっておらず、GetAllDocumentsByKey が優勢の状態に変わりはありません。

なお、今回の検証で不明瞭な点は、次の通りです。機会があれば検証したいと思います。

  • もっと大量のフィールドが存在する場合、処理時間が逆転することはあるか?
  • フィールドサイズの大小で変化はあるか?
  • LargeSummary 環境において特性に変化はあるか?


前回 Domino 体力測定


2025/12/31

Domino 体力測定:#2)ビュー操作 ② - 基本的な挙動

背景と目的

今回は GetAllDocumentsByKey と GetAllEntriesByKey の基本的な挙動を理解することを目的にビューを検索するメソッドとした検証を行います。

ビューを検索し結果のコレクションを作成、そこからエントリを取得して、値を取得する一連の処理のどこで処理時間がかかっているのか、特性を理解します。

今回は、最新の PC を使用して測定し、今どきの実環境に近い結果を目指して検証します。


測定環境

検証に使用する環境は次の通りです。参考までに CrystalMark Retro を使用して測定したベンチマーク結果を添付します。

CPU Ryzen 7 9700X ベンチマーク
メモリサイズ 128 GB
ストレージ SSD
OS Windows 11

Server Domino 14.5 FP1
Client Notes 14.5 FP1


◇ テスト DB

前回紹介したテスト DB を Domino サーバ内に配置します。パフォーマンスに関係しそうな情報を整理すると次の通りです。

文書数 100,000 文書
検索ビュー 000000 ~ 099999 の上 3 桁で検索、1 検索当たり 1000 文書がヒット
文書サイズ 13 フィールド、188 バイト
値取得用フィールドは 100 バイト


手順

前回紹介した関数を利用して D1 ~ D3、E1 ~ E5 を順に測定します。関数の引数は次のように設定します。

viFm 0
ビューを ”000” ~ ”019” の合計 20 回検索
作成されるコレクションは毎回 1000 件
viTo 19
viLoop 50 コレクションの前から 50 件を取得

測定のメインルーチンは次のような感じとなります(抜粋)。このプログラムをエージェントに記述してノーツクライアントから実行して測定します。

   '測定用ビュー取得
   Set nv = ndb.GetView("Category3")
   nv.AutoUpdate = False
   Call nv.Refresh()    '索引を更新してから測定

   '測定パラメータ
   iFm = 0
   iTo = 19
   iLoop = 50

   '測定実行
   For iTest = 1 To 10     '測定回数
      'GetAllDocumentsByKey の測定
      dD1 = xTest_D1(nv, iFm, iTo, iLoop)
      dD2 = xTest_D2(nv, iFm, iTo, iLoop)
      dD3 = xTest_D3(nv, iFm, iTo, iLoop)
      'GetAllEntriesByKey の測定
      dE1 = xTest_E1(nv, iFm, iTo, iLoop)
      dE2 = xTest_E2(nv, iFm, iTo, iLoop)
      dE3 = xTest_E3(nv, iFm, iTo, iLoop)
      dE4 = xTest_E4(nv, iFm, iTo, iLoop)
      dE5 = xTest_E5(nv, iFm, iTo, iLoop)

      '測定結果の記録
               ・・・
   Next

測定結果はメッセージボックスで表示するだけでも構いませんが、結果の分析に備えて Excel シートに出力すると便利ですね。


結果

10 回の測定結果から平均値を算出し、棒グラフと積み上げ折れ線グラフで表示します。


ビューを検索して値を取得する検証 3 までの結果を比較すると GetAllDocumentsByKey の方が約 2 倍早い結果となりました。


考察

今回の結果より導くことができるビュー操作の特徴を整理します。


◇ GetAllDocumentsByKey vs GetAllEntriesByKey

結果に記載した通り、GetAllDocumentsByKey 方が 2 倍早い結果となりました。文書を取得する必要がある場合においては、さらに差が開きます。少なくとも今回のテスト環境においては、GetAllDocumentsByKey を使用すべきと言えます。

GetAllEntriesByKey は、ビューのソート順の通りに取得したい、ビューの列値を取得したいという特殊な要件があるときの特殊用途だということだと考えられます。


◇ GetAllDocumentsByKey の挙動

検索結果のコレクションを作成する処理(D1)が最大で、処理時間の 70% を占めていて、残りが値の取得(D3)となっています。コレクションからエントリ(文書)の取得(D2)はほぼ 0 でした。

この結果より、NotesDocumentCollection オブジェクトを作成した時点で内部的に NotesDocument オブジェクトが生成されていることが想定できます。以前の記事 の「検証レポートの要点」で紹介した『GetNextDocument はコレクション内のポインタ移動だけで圧倒的に軽量』の証左と言えますね。


◇ GetAllEntriesByKey  の挙動

GetAllEntriesByKey  では、検索結果のコレクションを作成する処理(E1)とエントリを取得する処理(E2)で処理時間が記録されています。値の取得では NotesViewEntry で保持している ColumnValues プロパティ、要は配列から値を取得するだけなので処理コストはほぼ 0 となるということになります。

NotesViewEntryCollection と NotesViewEntry のオブジェクト作成のコストが非常に高いことがわかります。この結果も 以前の記事 の「検証レポートの要点」と合致します。


◇ NotesViewEntry から文書のアクセス

値を取得する検証 D3 と E5 はほぼ同じ結果でした。NotesDocument から値を取得する処理においては、文書を NotesViewEntry から取得しても同等の結果が得られることがわかります。

ただ、NotesDocument を取得する処理(E4)でも処理時間が記録されています。この結果より、NotesViewEntry のオブジェクト内に NotesDocument は含まれておらず、Document プロパティアクセス時にオブジェクトを生成していることがわかります。

NotesViewEntry は、ビューからの値取得を目的にチューニングされていると解釈すべきだと考えられます。


まとめと次回の検証

今回の検証は、近年一般的な SSD 環境で検証しました(CPU は AMD なので一般的とはいいがたいのかもしれませんが...)。その結果、GetAllDocumentsByKey の方が約 2 倍早いという結果を得ました。

テストケースは、文書数が 10 万件と多いもの、文書内には必要最小限のデータしかなく軽量で、ビューは 2 列だけのシンプルなものでした。この結果だけで実運用で応用できる検証結果とはいいきれません。

そこで、今回の結果をひとつの基準として、今後は、テスト環境やテストケースを変化させることで、処理時間や特性がどのように変化するのか調査したいと思います。

次回は、文書サイズにより特性が変化するのか確認したいと思います。


前回 Domino 体力測定 前回


2025/12/30

Domino 体力測定:#1)ビュー操作 ① - 測定方法

先日の記事『訂正記事:GetAllEntriesByKey は本当に速いのか? - 再検証でわかった“逆転の真実”』 では、ビューを操作するメソッド GetAllDocumentsByKey と GetAllEntriesByKey をパフォーマンスの観点から比較しました。

その中で『GetAllDocumentsByKey が GetAllEntriesByKey より有利』と結論付けましたが、その要因としてストレージの I/O や CPU などの影響が考えられることに触れるにとどまり、具体的な数値までは示していませんでした。そのため「実際にどの程度の差があるのか?」という疑問を持たれた方もいるかと思います。

そこで、この新連載『Domino 体力測定』では、Notes/Domino の機能や性能、挙動などについて、スポーツテストのように測定し、記録していきます。検証は手元の PC を使用して行い、測定条件や環境についても可能な限り明記します。

ビジネスでの実運用環境、とくにクラウド環境や仮想環境では異なる結果となる可能性もありますが、基準となる情報があれば、別の環境であっても仮説を立てることができるかと思います。この記事が何らかの参考になれば幸いです。


ビュー操作の測定

最初の検証は、この連載のきっかけにもなった「ビューの検索や結果の取得のパフォーマンス」です。実験に使用するメソッドは、もちろん GetAllDocumentsByKey が GetAllEntriesByKey です。今回は測定準備として、テスト用 DB とテストデータ(文書)、測定項目と測定プログラム(関数)を紹介します。


テスト項目

まずは測定項目です。以前の記事とほぼ同じですが改めて掲載します。


検証項目 D)GetAllDocumentsByKey E)GetAllEntriesByKey
1 コレクション作成 GetAllDocumentsByKey で検索し、コレクションを取得GetAllEntriesByKey で検索し、コレクションを取得
2 エントリ取得 GetFirstDocument、GetNextDocument で文書を取得GetFirstEntry、GetNextEntry でエントリを取得
3 値の取得 文書から値を取得ColumnValues で値を取得
4 文書の取得 Document プロパティで文書を取得
5 文書から値の取得 文書から値を取得

測定項目は、検索方法のイニシャル(D または E)と検証番号の組み合わせて表現します。例えば、D1 は ”GetAllDocumentsByKey のコレクション作成” となります。

ビューを検索して値を取得する時間の比較であれば、D1 ~ D3 と E1 ~ E3 を比較します。ビューの列にないフィールド値を取得したりフィールド値を更新する場合には、文書が必要となります。このような操作では、D1 ~ D3 と E1, E2, E4, E5 を比較することになります。


テスト DB

検証に使用する DB にはノーツでは少し多めの 10 万文書を用意します。0 ~ 99999 の番号に対して、次のような文書を作成します。

各桁のフィールドやカテゴリの項目は、将来の測定で柔軟にテストできるようにあらかじめ用意しておきます。また、テキストフィールドには値取得用に 100 バイトの文字列を設定します。

Private Function xViewSearch_CreateTestData2(vndb As NotesDatabase)
   Dim l As Long
   Dim s As String
   Dim i As Integer
   Dim nd As NotesDocument

   For l = 0 To 99999    '10万件
      s = Format(l, "000000")

      Set nd = vndb.Createdocument()
      nd.Form = "fTest"

      '検索用の値
      For i = 1 To Len(s)
         Call nd.Replaceitemvalue("Digit_" & CStr(i), Mid(s, Len(s) - i + 1, 1))
         Call nd.Replaceitemvalue("Category_" & CStr(i), Left(s, i))
      Next

      '取得用の値
      s = Right("0000000000" & s, 10)    '10バイト
      s = s & s & s & s & s & s & s & s & s & s     '10 x 10 = 100バイト
      Call nd.Replaceitemvalue("Text", s)

      Call nd.Save(True, True)
   Next
End Function

検索用のビューには、2 列を作成します。1 列目は検索列で Category_3 をソートして配置、2 列目は値取得用の列で Text フィールドを配置します。

例えば、このビューで "012" で検索すると 1000 文書がヒットすることになります。


測定方法

実行時間の測定には Timer 関数を使用します(使い方については過去記事『実行時間の計測(Timer 関数)』を参照)。ただ、この関数は精度が 1/100 秒となっており、処理時間を測定するには粗すぎます。そこで、ループを使って繰り返し実行させて処理時間を稼ぎ、できる限り正確に測定することとします。

各検証項目(D1 ~ D3、E1 ~ E5)毎に測定関数を作成します。各関数のインターフェースは同じとします。

・ 引数

1 vnv NotesView 測定に使用するビュー
・Refresh メソッドで索引更新済み
・AutoUpdate は False
2viFm Integer 検索カテゴリ(開始値)
3viTo  Integer 検索カテゴリ(終了値)
4viLoop Integer コレクションから取得するエントリ数

引数の 2 ~ 4 はループ回数を調整するために使用します。

・ 戻り値

Double 実行時間(秒)


◇ 検証 D1

GetAllDocumentsByKey でビューを検索し、コレクションを取得する時間を測定します。viFm ~ viTo までを順に検索させ、毎回違うコレクションが返ってくるようにしています。

Private Function xTest_D1(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer

   'オブジェクト格納用配列
   Dim ndc() As NotesDocumentCollection
   ReDim ndc(viTo-viFm)

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo '範囲を順に検索
      '検証 ①:コレクションの取得
      Set ndc(iCD-viFm) = vnv.GetAllDocumentsByKey(Right("00" & CStr(iCD), 3), True)
   Next

   '計測時間
   xTest_D1 = Timer() - sgST
End Function


◇ 検証 D2

NotesDocumentCollection からエントリを取得する時間を測定します。

計測準備で、検索結果のコレクションを格納用配列 ndc() にあらかじめ取得しておきます。測定開始着、この格納用配列からコレクションにアクセスすることで、エントリ取得時間だけを測定するようにしています。

Private Function xTest_D2(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer

   'オブジェクト格納用配列
   Dim ndc() As NotesDocumentCollection
   Dim nd() As NotesDocument
   ReDim ndc(viTo-viFm)
   ReDim nd(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set ndc(iCD-viFm) = vnv.GetAllDocumentsByKey(Right("00" & CStr(iCD), 3), True)
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      '検証 ②:エントリの取得
      Set nd(iCD-viFm, 1) = ndc(iCD).GetFirstDocument()
      For iLoop = 2 To viLoop
         Set nd(iCD, iLoop) = ndc(iCD).GetNextDocument(nd(iCD, iLoop-1))
      Next
   Next

   '計測時間
   xTest_D2 = Timer() - sgST
End Function


◇ 検証 D3

NotesDocument から値の取得時間を測定します。

コレクションごとに viLoop 回取得させます。よって、格納用配列 nd は 2 次元で、測定時のループは 2 重となっています。

Private Function xTest_D3(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer
   Dim s As String

   'オブジェクト格納用配列
   Dim ndc() As NotesDocumentCollection
   Dim nd() As NotesDocument
   ReDim ndc(viTo-viFm)
   ReDim nd(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set ndc(iCD-viFm) = vnv.GetAllDocumentsByKey(Right("00" & CStr(iCD), 3), True)

      '検証 ②:エントリの取得
      Set nd(iCD-viFm, 1) = ndc(iCD).GetFirstDocument()
      For iLoop = 2 To viLoop
         Set nd(iCD, iLoop) = ndc(iCD).GetNextDocument(nd(iCD, iLoop-1))
      Next
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      For iLoop = 1 To viLoop
         '検証 ③:値の取得
         s = nd(iCD, iLoop).Text(0)
      Next
   Next

   '計測時間
   xTest_D3 = Timer() - sgST
End Function


◇ 検証 E1

GetAllEntriesByKey でビューを検索し、コレクションを取得する時間を測定します。D1 とはメソッドが違うだけで、構造は同様になります。

Private Function xTest_E1(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer

   'オブジェクト格納用配列
   Dim nvec() As NotesViewEntryCollection
   ReDim nvec(viTo-viFm)

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set nvec(iCD-viFm) = vnv.GetAllEntriesByKey(Right("00" & CStr(iCD), 3), True)
   Next

   '計測時間
   xTest_E1 = Timer() - sgST
End Function


◇ 検証 E2

NotesViewEntryCollection から NotesViewEntry オブジェクトを取得する時間を測定します。

Private Function xTest_E2(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer

   'オブジェクト格納用配列
   Dim nvec() As NotesViewEntryCollection
   Dim nve() As NotesViewEntry
   ReDim nvec(viTo-viFm)
   ReDim nve(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set nvec(iCD-viFm) = vnv.GetAllEntriesByKey(Right("00" & CStr(iCD), 3), True)
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      '検証 ②:エントリの取得
      Set nve(iCD, 1) = nvec(iCD).GetFirstEntry()
      For iLoop = 2 To viLoop
         Set nve(iCD, iLoop) = nvec(iCD).GetNextEntry(nve(iCD, iLoop-1))
      Next
   Next

   '計測時間
   xTest_E2 = Timer() - sgST
End Function


◇ 検証 E3

ColumnValues プロパティを使って、ビューから値を取得する時間の計測です。

Private Function xTest_E3(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer
   Dim s As String

   'オブジェクト格納用配列
   Dim nvec() As NotesViewEntryCollection
   Dim nve() As NotesViewEntry
   ReDim nvec(viTo-viFm)
   ReDim nve(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set nvec(iCD-viFm) = vnv.GetAllEntriesByKey(Right("00" & CStr(iCD), 3), True)

      '検証 ②:エントリの取得
      Set nve(iCD, 1) = nvec(iCD).GetFirstEntry()
      For iLoop = 2 To viLoop
         Set nve(iCD, iLoop) = nvec(iCD).GetNextEntry(nve(iCD, iLoop-1))
      Next
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      For iLoop = 1 To viLoop
         '検証 ③:値の取得
         s = nve(iCD, iLoop).ColumnValues(1)
      Next
   Next

   '計測時間
   xTest_E3 = Timer() - sgST
End Function


◇ 検証 E4

NotesViewEntry の Document プロパティで文書を取得する時間を測定します。

Private Function xTest_E4(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer
   Dim s As String

   'オブジェクト格納用配列
   Dim nvec() As NotesViewEntryCollection
   Dim nve() As NotesViewEntry
   Dim nd() As NotesDocument
   ReDim nvec(viTo-viFm)
   ReDim nve(viTo-viFm, viLoop)
   ReDim nd(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set nvec(iCD-viFm) = vnv.GetAllEntriesByKey(Right("00" & CStr(iCD), 3), True)

      '検証 ②:エントリの取得
      Set nve(iCD, 1) = nvec(iCD).GetFirstEntry()
      For iLoop = 2 To viLoop
         Set nve(iCD, iLoop) = nvec(iCD).GetNextEntry(nve(iCD, iLoop-1))
      Next
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      For iLoop = 1 To viLoop
         '検証 ④:文書の取得
         Set nd(iCD, iLoop) = nve(iCD, iLoop).Document
      Next
   Next

   '計測時間
   xTest_E4 = Timer() - sgST
End Function


◇ 検証 E5

NotesViewEntry から取得した NotesDocument 経由でフィールド値を取得する時間を計測します。D3 と比較すれば NotesDocument の取得方法で差があるのかが確認できます。 

Private Function xTest_E5(vnv As NotesView, ByVal viFm As Integer, ByVal viTo As Integer, ByVal viLoop As Integer) As Double
   Dim sgST As Single
   Dim iCD As Integer
   Dim iLoop As Integer
   Dim s As String

   'オブジェクト格納用配列
   Dim nvec() As NotesViewEntryCollection
   Dim nve() As NotesViewEntry
   Dim nd() As NotesDocument
   ReDim nvec(viTo-viFm)
   ReDim nve(viTo-viFm, viLoop)
   ReDim nd(viTo-viFm, viLoop)

   '計測準備:測定されたくないオブジェクト取得
   For iCD = viFm To viTo    '範囲を順に検索
      '検証 ①:コレクションの取得
      Set nvec(iCD-viFm) = vnv.GetAllEntriesByKey(Right("00" & CStr(iCD), 3), True)

      '検証 ②:エントリの取得
      Set nve(iCD, 1) = nvec(iCD).GetFirstEntry()
      For iLoop = 2 To viLoop
         Set nve(iCD, iLoop) = nvec(iCD).GetNextEntry(nve(iCD, iLoop-1))
      Next

      '検証 ④:文書の取得
      For iLoop = 1 To viLoop
         Set nd(iCD, iLoop) = nve(iCD, iLoop).Document
      Next
   Next

   '測定開始
   sgST = Timer()

   For iCD = viFm To viTo    '範囲を順に処理
      For iLoop = 1 To viLoop
         '検証 ⑤:値の取得
         s = nd(iCD, iLoop).Text(0)
      Next
   Next

   '計測時間
   xTest_E5 = Timer() - sgST
End Function


次回の予定

これで測定材料が整いました。これらを使ってビューの検索と値取得の性能調査を順次行います。次回は、GetAllDocumentsByKey と GetAllEntriesByKey 基本的な挙動を調査します。


Domino 体力測定 次回


2025/12/13

訂正記事:GetAllEntriesByKey は本当に速いのか? - 再検証でわかった“逆転の真実”

2025 年 8 月 24 日に投稿した記事『LotusScript でビューを検索するメソッドの違い』で「GetAllDocumentsByKey より GetAllEntriesByKey のほうがパフォーマンス面で有利」と説明しました(以降、”前回の記事”といいます)。

ところが後日、実際に検証を行うとまったく逆の結果が出ました。まさかと思いつつ、改めて HCL テクニカルサポートに問い合わせたところ、私の検証と同じ結論(前回の結果の撤回)が返ってきました。今回の記事では、前回の記事の訂正とともに、両メソッドの特性について最新の知見をまとめておきます。


前回記事の要旨

前回の記事では、両メソッドの特徴を次のようにまとめました

GetAllDocumentsByKey 文書そのものを一つ一つコレクションすることから比較的に遅い
GetAllEntriesByKey ビュー索引を活用する仕様
索引は軽量であり、メモリ消費も少なく高速に動作

2025 年 8 月の Domino Lounge Osaka でこの話をしたところ、同じ理解をされている方が多数いたのでこれが ”定説” だったことがうかがえます。


検証したら...真逆!?

どれぐらいパフォーマンスに差があるのか気になり、簡易的に検証したところ、GetAllEntriesByKey の方が ”数倍遅い!?” という結果がでました。

測定した項目は次の 5 項目です。GetAllEntriesByKey では高速なはずの ColumnValues による値の取得だけでなく、文書オブジェクトを取得して、値を取得するテストも行いました。

シナリオ GetAllDocumentsByKey GetAllEntriesByKey
コレクション作成 GetAllDocumentsByKey で検索し、コレクションを取得GetAllEntriesByKey で検索し、コレクションを取得
エントリ取得 GetFirstDocument、GetNextDocument で文書を取得GetFirstEntry、GetNextEntry でエントリを取得
値の取得 文書から値を取得ColumnValues で値を取得
文書の取得 Document プロパティで文書を取得
文書から値の取得 文書から値を取得

結果は次の通りで、GetAllDocumentsByKey の方がずいぶん早かったのです。

はじめは、私のテストケース設定が悪く特徴を捕まえ損ねたと考えましが、より明確にメソッドの特性を理解するチャンスとばかりに、検証に使用した NSF(データと検証のコード)をサポートに送付して質問しました。

冒頭に記載した通り、返答は驚きの結果でした。”定説” とは真逆で『GetAllDocumentsByKey の方が高速』とありました。かなり丁寧な検証を実施いただいたようで、複数のテストケースに対して目的、検証手順、結果、考察をまとめた詳細なレポートと検証に利用した NSF が添付されていました。


検証レポートの要点

レポートでは、両メソッドの挙動が処理フェーズごとに整理されていました。

処理フェーズ GetAllDocumentsByKey GetAllEntriesByKey
コレクション作成 NotesDocumentCollection は内部構造がシンプルで軽量 NotesViewEntryCollection は内部構造が複雑で初期作成コストが高い
エントリ取得 GetNextDocument はコレクション内のポインタ移動だけで圧倒的に軽量 GetNextEntry は移動のたびに複雑なオブジェクトを生成・破棄が発生
値の取得 NotesDocument は読み込むフィールド数に比例してコスト増 NotesViewEntry は列数が増えると内部的な配列生成など処理コストが増加

表を見てわかるように、すべてのフェーズで GetAllDocumentsByKey  が優勢となっています。


”定説” 逆転の背景

HCL サポートの見解では、主に以下の2点の要因が指摘されていました。

I/O コストの低下 かつての HDD 環境では、文書データへのアクセスのようなランダムアクセスは大きな遅延要因であったが、現在の SSD 環境になり劇的に短縮。
ボトルネックの変化I/O コスト低下により処理時間の主たる要因がディスクアクセスからオブジェクト操作などのプログラムの内部処理へ移行
(ストレージの I/O → CPUやメモリ)

文書を開かずビュー索引を使用したほうが早いという ”定説” は、ストレージが HDD だった過去の環境から導き出されたものだったということです。

現在の環境においては、GetAllEntriesByKey の複雑な内部処理が圧倒的に重く、フィールド数が多いなど文書操作が重くなる状況でも逆転することはないとのことでした。


今どきの”定説”

HCL サポートからのレポートは私の簡易的な検証とも一致する結果でした。

現在の一般的な環境では『GetAllDocumentsByKey のほうがパフォーマンス面で有利』という結論です。

前回の記事の内容は、この知見を踏まえて、完全に撤回し訂正いたします。

ちなみに、GetAllEntriesByKey を使う利点は、次のような用途に限られます。

  • ビューのソート順通りに取得したい
  • ビューの列の値にアクセスしたい


検証は大事

今回のような訂正記事を書くハメになったのは、「メーカサポートの回答だから」と無条件に信じてしまったことが原因です。これは ”サポートを信頼できない” と言っているのではありません。自分で検証すると理解が深まり、結果的に応用の幅も広がるということを体験できてよかったと感じました。検証は技術者としてとても重要だと再確認できました。

今回の記事において、パフォーマンスを左右する要素としては

  • ストレージの I/O
  • CPUやメモリ
  • 文書のサイズやフィールド数
  • ビューの列数

があることを知りました。これらがどのように影響するのか、特性を知っておくとより理解が深まります。調査が難しいものありますが、可能な限り検証してまとめたいと思います。


お詫び

ノーツコンソーシアム主催の次世代エース養成ワークショップ(2025 年 7 月、10 月)では、訂正前の内容を紹介しておりました。当時の私にとっては ”新しい知見” だったので、少し得意げに紹介してしまったことを反省しております。

ここでお詫びしても受講者のみなさま全員に届くとは限りませんが、この場を借りてお詫びいたします。今後はより慎重に検証したた上で、研修テキストの作成をしてまいります。


2025/12/07

QR コードの作画:#9)DXL でリッチテキストに表示

LotusScript で QR コードを作画するこの連載は今回が最終回となります。

DXL がビットマップに対応していないことから、暫定策として GIF ファイルに変換する対応をしました。今回は出来上がった GIF ファイルを文書に貼り付ける部分を作成します。


エージェントの作成と関数の追加

前回作成した CreateQRcode_GIF エージェントをコピペして DrawQRcode を作成します。このエージェントに関数を追加して作業を進めます。

リッチテキストフィールドに QR コードをインラインイメージ(見える状態)で貼り付ける処理には DXL を活用しなければなりません。この連載の本題ではないので詳細は割愛しますが、DXL にご興味がある方は以下の連載をご確認ください(一部関数は連載より流用)。


◇ 文書を DXL に変換

既存文書を DXL で操作できるよう変換処理を行う関数です。

Function xGetDOMParser(vnd As NotesDocument) As NotesDOMParser
   'Dominoデータ を DXL に変換する準備
   Dim dexp As NotesDXLExporter
   Set dexp = xns.CreateDXLExporter()
   Call dexp.SetInput(vnd)

   'パーサーに変換する DXL をセット
   Dim dprs As NotesDOMParser
   Set dprs = xns.CreateDOMParser()
   Call dprs.SetInput(dexp)

   'DXL 変換を実行
   Call dexp.Process()

   Set xGetDOMParser = dprs
End Function

この関数は『DXL Step-by-Step:#3)文書を DXL で取得』で紹介しています。


◇ QR コードの表示

リッチテキスト内に QR コードをインラインイメージで貼り付ける関数です。

引数 vsFld で指定された名前のリッチテキストフィールドにセットするのですが、フィールドをいったん削除しています。フィールド内の既存コンテンツはクリアされますので注意してください。

Function xSetDXL_GIF(vdprs As NotesDOMParser, ByVal vsFld As String, ByVal vsFP_GIF As String, viX As Integer, viY As Integer)
   Dim ddn As NotesDOMDocumentNode
   Dim den As NotesDOMElementNode
   Dim denDoc As NotesDOMElementNode
   Dim denItem As NotesDOMElementNode
   Dim denRT As NotesDOMElementNode
   Dim denPar As NotesDOMElementNode
   Dim denPic As NotesDOMElementNode
   Dim denGIF As NotesDOMElementNode
   Dim dtn As NotesDOMTextNode
   Dim nst As NotesStream

   Set ddn = vdprs.Document

   'document ノード取得
   Set denDoc = ddn.DocumentElement

   '既存フィールドを削除
   Call xRemoveItemByName(denDoc, vsFld)

   'リッチテキストフィールド作成
   Set den = ddn.CreateElementNode("item")
   Call den.SetAttribute("name", vsFld)
   Set denItem = denDoc.AppendChild(den)

   'リッチテキスト作成
   Set den = ddn.CreateElementNode("richtext")
   Set denRT = denItem.AppendChild(den)

   '段落定義 id='1'
   Set den = ddn.CreateElementNode("pardef")
   Call den.SetAttribute("id", "1")
   Call denRT.AppendChild(den)

   '段落の作成
   Set den = ddn.CreateElementNode("par")
   Call den.SetAttribute("def", "1")
   Set denPar = denRT.AppendChild(den)

   'イメージリソースの追加
   Set den = ddn.CreateElementNode("picture")
   Call den.SetAttribute("align", "baseline")
   Call den.SetAttribute("width", CStr(viX) & "px")
   Call den.SetAttribute("height", CStr(viY) & "px")
   Set denPic = denPar.AppendChild(den)

   '画像の作成
   Set den = ddn.CreateElementNode("gif")
   Set denGIF = denPic.AppendChild(den)

   'GIF をストリームで開く
   Set nst = xns.CreateStream()
   Call nst.Open(vsFP_GIF, "binary")

   '画像の中身
   Set dtn = ddn.CreateTextNode(StreamToBase64(nst))
   Call denGIF.AppendChild(dtn)

   Call nst.Close()
End Function

この関数については合致する記事はありませんが、インラインイメージの貼り付けについては『DXL Step-by-Step:#41)インラインイメージの貼り付け』で触れています。

また、今回は画像サイズは QR コードのサイズであり、事前にわかっています。そこで、画像ファイルから取得するのではなく、関数の引数で受け取り処理を簡略化しています。


◇ 指定したフィールドの削除

xSetDXL_GIF からコールされているサブ関数で、DXL でフィールドを削除する関数です。

Function xRemoveItemByName(vden As NotesDOMElementNode, ByVal vsName As String)
   Dim dn As NotesDOMNode
   Dim den As NotesDOMElementNode
   Dim sName As String
   Dim s As String

   sName = LCase(vsName)

   Set dn = vden.FirstChild
   Do Until dn.Isnull
      If dn.NodeType = DOMNODETYPE_ELEMENT_NODE Then
         If dn.NodeName = "item" Then
            Set den = dn
            s = LCase(den.GetAttribute("name"))

            If sName = s Then
               Call vden.RemoveChild(den)
               Exit Function
            End If
         End If
      End If
      Set dn = dn.NextSibling
   Loop
End Function

この関数の参考記事は現時点でありません。フィールドである item ノードから名前が一致するものを探し出し削除しています。


◇ 画像のエンコード

こちらも xSetDXL_GIF からコールされているサブ関数です。DXL 内の画像は Base64 でエンコードしておく必要があります。そのエンコード処理を行う関数です。

'OpenNTF LotusScript Gold Collection より拝借(StreamToBase64)
Function StreamToBase64(streamIn As NotesStream) As String
   Dim s As String

   On Error GoTo theOldWay
   ' ReadEncoded function is not documented. In case it doesn't work have a backup.
   s = streamIn.ReadEncoded(ENC_BASE64, 76)
   s = Replace(s, Chr$(13), "")
   s = Replace(s, Chr$(10), "")
   StreamToBase64 = s
   Exit Function

theOldWay:
   Dim session As New NotesSession
   Dim db As NotesDatabase
   Dim doc As NotesDocument
   Dim mime As NotesMIMEEntity

   Set db = session.CurrentDatabase
   Set doc = db.CreateDocument
   Set mime = doc.CreateMIMEEntity("Body")
   streamIn.Position = 0
   Call mime.SetContentFromBytes(streamIn, "image/gif", ENC_NONE)
   mime.EncodeContent(ENC_BASE64)
   s = mime.ContentAsText
   s = Replace(s, Chr$(13), "")
   s = Replace(s, Chr$(10), "")
   StreamToBase64 = s
End Function

コメントに記載した通り、 OpenNTF の LotusScript Gold Collection プロジェクトより拝借した関数です。『DXL Step-by-Step:#10)イメージリソースの新規作成』で紹介しています。


◇ 文書の保存

最後の関数は文書を保存する関数です。

Function DXL_Import(vdprs As NotesDOMParser, ByVal viOption As Integer, ByVal vbIsDesign As Boolean) As Boolean
   Dim nst As NotesStream
   Dim ndb As NotesDatabase
   Dim dimp As NotesDXLImporter

   On Error GoTo Err_Proc

   'DXL の抽出準備
   Set nst = xns.CreateStream()
   Call vdprs.SetOutput(nst)
   Call vdprs.Serialize()

   '保存(インポート)
   Set ndb = xns.CurrentDatabase
   Set dimp = xns.CreateDXLImporter()
   If vbIsDesign = True Then
      '設計の保存
      dimp.DesignImportOption = viOption
   Else
      '文書の保存
      dimp.DocumentImportOption = viOption
   End If

   'DXL の保存
   Call dimp.Import(nst.ReadText(), ndb)
   DXL_Import = True

Exit_Proc:
   Exit Function

Err_Proc:
   MsgBox Error$, 16, "DXL_Import"
   DXL_Import = False

   Resume Exit_Proc
End Function

この関数は『DXL Step-by-Step:#23)サンプルコード(段落と文字の装飾①)』で紹介しています。


メインルーチンの修正

エージェントの Initialize を修正し、今回作成した QR コード表示機能を追加します。

Sub Initialize
                  ・・・
   Dim iX As Integer    '画像の幅
   Dim iY As Integer    '画像の高さ
   Dim dprs As NotesDOMParser
                  ・・・
      '② GIF ファイルの作成
      Call DrawQR_GIF(abQR, "c:\tmp\QR.gif")

      '③ リッチテキストにインラインで貼り付け
      Set dprs = xGetDOMParser(nd)
      iX = UBound(abQR, 1) + 1    '画像の幅
      iY = UBound(abQR, 2) + 1    '画像の高さ
      Call xSetDXL_GIF(dprs, "QRcode", "c:\tmp\QR.gif", iX, iY)
      Call DXL_Import(dprs, 5, False)    '文書を更新
   End If
End Sub


ビューの修正と動作検証

エージェントが完成したら、アクションボタンをビューに追加します。

ビューを保存後、動作検証します。正常に実行されると冒頭の画像のように QR コードが表示されます。


まとめ

今回の連載は、LotusScript だけを使って QR コードをビットマップ画像として描画する方法について解説しました。あわせて、ビットマップ画像のフォーマット仕様についても整理し、仕組みを理解しながら作り上げる流れをご紹介しました。

本来であれば、できる限り幅広い環境で動作させるために LotusScript だけで完結させることを目指していましたが、現状の DXL ではビットマップに対応していないため、最終的な GIF 変換には Windows の機能に頼らざるを得ない部分がありました。その結果、完全に LotusScript だけで完結する構成にはできなかった点は少し心残りです。

機会があれば GIF フォーマットのファイルを LotusScript で生成することにも挑戦してみたいと思います。仕様が理解できたらという制約はありますが...


前回 QR コードの作画


2025/12/06

QR コードの作画:#8)BMP → GIF 変換

前回は、DXL はビットマップファイルに対応していないと(個人的に)衝撃的な事実を知ったことを紹介しました。せっかく作ったライブラリを何とか使いたいので、GIF ファイルに変換して利用したいと思います。

こういった汎用的なコンバート処理は、AI に聞きながら開発すると効率的ですよね。そこで、今回は ChatGPT 先生に相談しながら作業することにします。


BMP → GIF 変換

WIA(Windows Image Acquisition)を使うなどいくつかの方法を提案してくれましたが、64 ビット版の Notes でも動作させたいので、今回は PowerShell を利用して実現することにします。次のコマンドを実行すればよいそうです。

Add-Type -AssemblyName System.Drawing
$bmp = [System.Drawing.Bitmap]::FromFile('C:\tmp\BMP.bmp')
$bmp.Save('C:\tmp\BMP.gif', [System.Drawing.Imaging.ImageFormat]::Gif)

各行は、

  1. .NET の System.Drawing 名前空間を PowerShell セッションにロードします。
    (System.Drawing は Bitmap や Image、ImageFormat の定義を含むライブラリ)
  2. 指定した BMP ファイルを読み込み、System.Drawing.Bitmap オブジェクト(ビットマップ画像を表す .NET オブジェクト)を作成して $bmp に代入
  3. $bmp に保持されている画像を、指定したパスに GIF 形式で保存

となっているそうです。


LotusScript で関数化

教えてもらった PowerShell のコマンドを実行する LotusScript の関数の作成も依頼してみます。頼んでないのに複数ファイルを一括変換してみたり、Domino Designer に貼り付けると文法エラーになったりと、少々回り道をしましたが、何とか完成しました。

完成に向けていくつか要望を出してみたのですが、それぞれ叶えてくれました。

PowerShell 実行ウィンドウの非表示 非表示で WSH を起動し、PowerShell を実行させる
全角対応UTF-8 で PowerShell スクリプトを保存
これによりパスやフォルダ名に日本語(全角文字)が含まれても安全に動作

出来上がった関数は以下の通りです。ChatGPT が出力した関数を不要な部分を削除し、命名規則を整えたバージョンです。

Private Function xConvertBMPtoGIF_PS(ByVal vsFP_BMP As String, ByVal vsFP_GIF As String) As Boolean
   Dim oFSO As Variant
   Dim oShell As Variant
   Dim sPS_FP As String
   Dim sPS As String
   Dim sCMD As String
   Dim sCRLF As String

   ' 初期化
   On Error GoTo ErrorHandler

   xConvertBMPtoGIF_PS = False
   sCRLF = Chr(13) & Chr(10)

   ' 変換元ファイルの存在チェック
   Set oFSO = CreateObject("Scripting.FileSystemObject")
   If Not oFSO.FileExists(vsFP_BMP) Then Exit Function

   ' PowerShell スクリプトの作成
   sPS_FP = Environ$("TEMP") & "\convert_bmp_to_gif.ps1"

   ' PowerShellスクリプト内容(全角パス対応)
   sPS = "Add-Type -AssemblyName System.Drawing"
   sPS = sPS & sCRLF & "$src = '" & Replace(vsFP_BMP, "'", "''") & "'"
   sPS = sPS & sCRLF & "$dst = '" & Replace(vsFP_GIF, "'", "''") & "'"
   sPS = sPS & sCRLF & "$bmp = [System.Drawing.Bitmap]::FromFile($src)"
   sPS = sPS & sCRLF & "$bmp.Save($dst, [System.Drawing.Imaging.ImageFormat]::Gif)"
   sPS = sPS & sCRLF & "$bmp.Dispose()"

   ' UTF-8で.ps1を書き出し(全角対応のため)
   Call xWriteUtf8File(sPS_FP, sPS)

   ' PowerShell実行
   Set oShell = CreateObject("WScript.Shell")
   sCMD = |powershell -NoProfile -ExecutionPolicy Bypass -File "| & sPS_FP & |"|
   oShell.Run sCMD, 0, True       ' 0=ウィンドウ非表示, True=完了待ち

   ' 結果確認
   If oFSO.FileExists(vsFP_GIF) Then
      xConvertBMPtoGIF_PS = True
   End If

ExitFunc:
   ' 一時ファイル削除
   On Error Resume Next
   oFSO.DeleteFile sPS_FP

   Exit Function

ErrorHandler:
   xConvertBMPtoGIF_PS = False
   Resume ExitFunc
End Function

' UTF-8 (BOMなし) でテキストファイルを書き出し
Private Sub xWriteUtf8File(ByVal vsFP As String, ByVal vsText As String)
   Dim oStream As Variant
   Set oStream = CreateObject("ADODB.Stream")
   With oStream
      .Charset = "UTF-8"
      .Open
      .WriteText vsText
      .SaveToFile vsFP, 2    ' 上書き
      .Close
   End With
End Sub


ライブラリへの組み込み

上記関数を前回までに作成した lsDrawQRcode ライブラリに追加します。

続いて、GIF ファイルを作成するメインルーチンも作成します。ビットマップ作成と上記の関数を実行するだけの単純な構造です。

%REM
QR コードの論理データ(Boolean 型の 2 次元配列)から GIF ファイルを作成します。

◆ 引数
   vabQR Boolean QR コードの論理データ(Boolean 型の 2 次元配列)
   vsFP_GIF String フルパスで指定されたファイル名

◆ 戻り値 Booelan
   ファイルが作成出来れば True
%END REM

Public Function DrawQR_GIF(vabQR As Variant, ByVal vsFP_GIF As String) As Boolean
   Dim sFP_BMP As String

   sFP_BMP = vsFP_GIF & ".bmp"
   If DrawQR_BMP(vabQR, sFP_BMP) Then
      Call xConvertBMPtoGIF_PS(sFP_BMP, vsFP_GIF)

      ' 一時ファイル削除
      On Error Resume Next
      'Kill sFP_BMP
   End If
End Function

なお、一時ファイルであるビットマップは最後に削除するコードを書いています。検証のためコメントアウトしていますが、確認後はコメント解除してください。


動作検証

関数が完成したので動作検証を行います。

前回作成したエージェントをコピペして CreateQRcode_GIF を作成します。画像を作成する部分を今回作成した関数に差し替えます。

         ・・・
      '② GIF ファイルの作成
      Call DrawQR_GIF(abQR, "c:\tmp\QR.gif")
         ・・・

このエージェントを実行するアクションボタンをビューに追加します。

正常に動作すると出力フォルダに GIF ファイルが作成されます。


次回の予定

これで GIF 形式の QR コードが出力できるようになりました。次回の最終回では DXL を使ってこの画像をリッチテキストにインラインで貼り付けるコードを紹介します。


前回 QR コードの作画 次回


2025/12/04

QR コードの作画:#7)BMP は作成できたけど...

前回でスクリプトライブラリは完成しましたので、正しくビットマップファイルが作成できるか動作検証をしましょう。


フォームの作成

新規でフォームを作成します。名称にこだわりはないのですが、ここでは QR_Pict とします。フィールドは次の 2 個作成します

項目 フィールド名 種類 補足
変換する文字列 StrSrc テキスト 編集可能
QR コード QRcode リッチテキスト 編集可能

入力した文字列を QR コード化にしてリッチテキストに表示することをイメージした単純なフォームとなります。


エージェントの作成

続いてビューで選択した文書に QR コードを添付するエージェントを作成します。とりあえず今回は、QR コードに変換しビットマップファイルとして保存するところまでを作成します。

LotusScript のエージェント CreateQRcode_BMP を新規作成し、プロパティで実行対象を”なし”に設定します。

エージェントのコード全体は次の通りです。

まず、前回までに作成したライブラリ lsDrawQRcode を組み込みます。

選択した文書から変換する文字列を取得して QR コードにエンコードします(①)。ここまでは、連載『ノーツで QR コード』で作成した lsQRCode ライブラリを利用した部分ですね。

今回作成した関数は ② で使用しています。動作検証なので C:\tmp\ フォルダに QR.bmp という名前で固定して出力させています(実行前にはフォルダを用意ください)。

Option Declare
Use "lsDrawQRcode"

Private xns As NotesSession

Sub Initialize
   Dim ndb As NotesDatabase
   Dim ndc As NotesDocumentCollection
   Dim nd As NotesDocument
   Dim sSrcText As String

   Dim sEnc As String             'エンコード文字列
   Dim abQR() As Boolean    'QR コードの論理データ(Boolean 型の 2 次元配列)

   Set xns = New NotesSession
   Set ndb = xns.CurrentDatabase
   Set ndc = ndb.UnprocessedDocuments

   If ndc.Count <> 1 Then
      MsgBox "QR コードを作成する文書を1文書だけ選択してください。", 16, ndb.Title
   Else
      Set nd = ndc.GetNthDocument(1)
      sSrcText = nd.StrSrc(0)    'QRコード化したい文字列

      '① QR コードのエンコード
      sEnc = EncodeBarcode(sSrcText, 3)    '3 = 誤り訂正レベル
      Call bc_2Dms_New(sEnc, abQR)         'QR コードの論理データに変換

      '② ビットマップファイルの作成
      Call DrawQR_BMP(abQR, "c:\tmp\QR.bmp")
   End If
End Sub


ビューの作成

先に作成したフォームの文書を表示するためのビューを作成します。フォーム同様特にこだわりはないので適当に作成します。

ただ、このビューは QR コードを作成を実行する役割があります。[QRコード作成]アクションボタンで作成したエージェントを実行するようにします。


動作検証

テストできる環境が整ったので動作検証を行います。適当な文字列を入力したテストデータを作成し、[QRコード作成]ボタンをクリックします。

正常に動作すると出力フォルダにビットマップファイルが作成されます。


想定外の大どんでん返し...

最難関と考えていた画像生成ができたので、あとは QR コードをフォームに貼り付けるだけです。

リッチテキストフィールドに画像をインライン(見える状態)で貼り付けるには、通常の NotesRichText??? クラスでは実現できません。LotusScript で DXL を使用する必要があります。

他の連載『DXL ことはじめ』『DXL Step-by-Step』でまとめている通り、DXL には心得があります。それを使って実現しようとしたのですが、なんと!DXL は BMP 未対応だったんです !? 

GIF や JPEG などの画像形式に対応しているので、てっきり対応しているものと思っていました...


次回の予定

仕方がないので、次に仕様が簡単そうな GIF 形式を調べてみたのですが、画像圧縮する機能あり、ビットマップよりずいぶん複雑です。現時点では、残念ながら理解に至っていません。

そこで今回は、ビットマップを GIF に変換することで暫定対応としたいと思います。詳しくは次回紹介します。


前回 QR コードの作画