出直し!! ヘルプ

連載中

連載 終了

2023/11/30

チーム開発で大切なこと

ノーツは一般にビジネスで使用するアプリケーションです。そこでアプリケーションを開発される方はほとんどの場合 ”仕事” としてプログラミングされていると思います。

では、”仕事” でアプリケーションを開発する場合、どのようなことに気を付けるべきでしょうか?

ポイントは、趣味の開発とは違って、複数人で担当することがあるという点だと思います。たとえ現在一人で開発していたとしても、将来担当替えなどで誰かに引き継ぐかもしれません。この点を考慮に入れると、アプリ開発が一人で完結することは稀なはずです。

他のメンバーと効率よく作業できる ”チーム開発” が、”仕事” での開発に重要と考えており、その結果、生産性や開発物の品質の向上につながると考えています。

私が重視しているのは次の3点です。

  • わかりやすいこと
  • 効率が良いこと
  • メンテナンス性が高いこと

それぞれの項目について掘り下げてみましょう。


わかりやすいこと

簡単に言うと「プログラムが読みやすい」ということです。

スペースやタブを活用して整然と記述します。As や = などの位置がそろっていると気持ちいですよね。処理が複数のブロックに分かれる場合には、その間に改行を入れ間隔をあけるのも効果的です。

このように、プログラムのレイアウトを整え、見た目に美しいプログラムにするだけで、ずいぶんわかりやすくなります。汚い字で書かれた手書きメモは、読む前から気分がそがれるのと同じですね。

また、変数や関数名を適切に命名し、見ただけで用途がわかるようにします。必要に応じてコメントを記述すると、さらにわかりやすくなります。

チームの他のメンバーでもわかりやすいかを意識してコーディングするといいですね。これは技術ではなく ”思いやり” になりますね...


効率が良いこと

処理時間が短い、データの更新が少ない、メモリの使用量が少ない、プログラムが短いなど、”効率” にはさまざまな指標があります。ただ、こういった指標を追求したプログラムは得てして複雑になったり、読みにくくなります。例えば、変数を使いまわしたり、複雑なアルゴリズム利用したり、コメントを省略したりなどです。

ノーツのようなビジネスアプリケーションの場合は、これらの指標を追求するより、前項のわかりやすさを優先すべきかと思います。まずは仕様通りのプログラムをわかりやすく記述し、どうしてもレスポンスが悪いなど不都合がある部分だけをチューニングすればいいでしょう。

後任者がわかりやすいことがビジネス上の ”効率が良い” といえますよね。将来、すっかり忘れた自分自身が得するかもしれませんしね(笑)


メンテナンス性が高いこと

これは将来の仕様変更などプログラムの修正がしやすいということになります。

古い言い方ですが、ノーツは EUC(End User Computing)のツールなので、アプリケーション開発といっても、プログラマではない方が担当することが多々あります。そのため、アプリを動かすことだけに注力し、メンテナンス性まで意識できていない場合が多いといえます。ただ、最近の流れの早いビジネス環境に対応するためにはメンテナンスしやすい状態にしておくことが大切だと思います。


どうすればいいの?

これらを具体的に実現するためには、ソフトウェア工学という学問や、構造化プログラミング、オブジェクト指向などの技法や考え方を駆使する必要があります。私はその筋のプロではありませんので、そんなことは解説できません。

真似事かもしれませんが、私が実践していることは次のような項目です。参考までにご紹介します。主に LotusScript を意識していますが、@関数でも応用できることはあるかと思います。

  • コーディングスタイル(レイアウトのルール)を決定
  • コメントを適切に記述する
  • 命名規則を決定する
  • 変数を宣言する(暗黙の変数宣言を禁止する)
  • パブリック変数を(極力)使用しない
  • 無用なスコープは与えない
  • 共通の処理は関数化する
  • 関数のインターフェースを明確化する
  • 汎用的な関数はライブラリ化する
  • 一般的な処理は部品化する(@関数やダイアログボックス)


こういったことをルール化することを、コーディング規約やコーディングルール、標準化などと言います。ルールは制定するだけではだめで、実践することが大切です。チーム全員で実践することで、分担作業がしやすくなり、生産性が上がります。また、他人のプログラムが読みにくいという問題も改善できます。

ライブラリ化を進めると、ライブラリ開発とアプリ開発を分担したり、アプリ開発者はアプリの機能に集中できるという効果もあります。ノーツでは書かない場合も多いのですが、仕様書のドキュメント量を減らすこともできます。将来、担当替えがあったときは引継ぎ工数が削減できるはずです。


これらすべてを初めから完成させるのは大変です。ルールを作ることだけでなく、ルールを守ることにも手間(コスト)が発生します。実現することより、必要性をチームで認識する方が重要です。できることから順にはじめ、効果を感じられたら範囲を広げるなど、段階的に進めるぐらいで十分だと思います。


まとめ

転職などもあり、これまでに複数のチームで開発をしてきました。お客様の要望や上司の指示によりそれぞれの環境で、チームの生産性向上に取り組んできました。その経験を事例としてご紹介しました。

多岐にわたるため、今回は概要の説明だけとなりました。これだけでは、『だから??』となるかと思います。今後、より具体的に事例を紹介する機会を作る予定です。

2023/11/28

Notes - Excel 連携:#27)セルの背景色や文字色の設定

第 20 回からスタートした『帳票の作成』シリーズの 8 回目です。

帳票のヘッダ部分の装飾を題材に、前回紹介した罫線以外のセルの装飾についてまとめます。今回もセルを取り扱いますので、起点となるオブジェクトは Range となります。


背景色の設定

Range オブジェクトには Interior プロパティが存在します。このプロパティから取得できる Interior オブジェクトで背景色の設定を行うことができます。

Interior オブジェクト (Excel)

Microsoft Learn で仕様を確認するとプロパティしか定義されいません。主なプロパティは次の通りですが、背景色をセットするためのオブジェクトと理解して差しさわりないようですね。

プロパティ 説明
Color 背景色
Pattern 網掛けのパターン
(設定値については XlPattern 列挙 を参照)
PatternColor 網掛けの色


文字の色と装飾

セル内の文字の装飾は Range オブジェクトの Font プロパティから Font オブジェクト を取得できます。

この Font オブジェクトに色を設定する Color プロパティが存在します。他には、太字(Bold)や下線(Underline)などの装飾、第 21 回 で利用したフォント(Name)や文字サイズ(Size)も存在します。


文字揃え

セル内の文字揃えは Range オブジェクトの HorizontalAlignment プロパティ で行います。設定できる値は XlHAlign 列挙 に定義されています。主な値は次の通りです。

定数 説明
-4131 xlHAlignLeft 左揃え
-4108 xlHAlignCenter 中央揃え
-4152 xlHAlignRight 右揃え
1 xlHAlignGeneral データの種類に従って揃える

xlHAlignGeneral は Excel らしい設定ですね。これらの定数はスクリプトライブラリ lsXls に登録しておきましょう。


表のタイトル部分の設定

タイトル行背景をグレーに設定し、文字を白色に設定します。ヘッダ部分を設定する関数 xDrawHeader を新規で作成します。関数の引数は、Worksheet オブジェクトで、メインルーチン次のようになります。

Function xDrawHeader(voSheet As Variant)
   Dim oRange As Variant
   Dim s As String

   'ヘッダ行(背景と文字色)
   s = GetRangeString(xciHeaderRows, 2, xciHeaderRows, 9)
   Set oRange = voSheet.Range(s)
   oRange.Interior.Color = RGB(128, 128, 128)
   oRange.Font.Color = RGB(255, 255, 255)

End Function

続いて数値項目のタイトルを右寄せにします。数値項目は C 列 ~ I 列なので次のようになります。

   'ヘッダ行(揃え)
   s = GetRangeString(xciHeaderRows, 3, xciHeaderRows, 9)
   Set oRange = voSheet.Range(s)
   oRange.HorizontalAlignment = xlHAlignRight

ここまでを実行すると、以下のようにタイトルと数値の寄せがずれる現象が発生します。

これは、初期化処理で数値のセルで次のように指定したからです(第 22 回 参照)。最後のアンダースコアとスペースが右側に1文字分スペースを入れる設定になります。

   voSheet.Range("C:I").NumberFormatLocal = "#,##0_ " '数値

同様の設定は文字列のセルでも可能です。次の行を追加すると揃えることができます。

   oRange.NumberFormatLocal = "@_ "


出力日時の表示

2行目の出力日時の表示は次のように記述します。値としては現在時刻をセットしているだけで、Excel の書式と揃えで見た目を整えています。

   '出力日時
   Set oRange = voSheet.Cells(2, 2)
   oRange.HorizontalAlignment = xlHAlignRight
   oRange.NumberFormatLocal = |yyyy/m/d hh:mm "出力"|
   oRange.Value = Now


帳票タイトルの表示

同様に帳票のタイトルの設定を行います。タイトルは、文字を大きく、太字と下線を設定しています。タイトルも出力日時と同様に書式を使って

   '帳票タイトル
   Set oRange = voSheet.Cells(3, 2)
   oRange.HorizontalAlignment = xlHAlignCenter
   oRange.NumberFormatLocal = |"プリンタ出力費用一覧("yyyy"年"m"月)"|
   oRange.Font.Size = 16
   oRange.Font.Bold = True
   oRange.Font.Underline = True
   oRange.Value = oRange.Value

タイトルも出力日時と同様に書式を使って表示を整えています。書式は値をセットする前に設定しておかないと反映されないようです。一番下の『oRange.Value = oRange.Value』で自分自身の値を代入することで、書式を有効化しています。


なお、上記コードは帳票タイトルのセルに出力年月が日付値で入っている前提となっています。そのため、第 23 回 で作成した xPrintUsages 関数に以下のコードを追加しておく必要があります。

   '帳票タイトル用に年月を日付値でセット
   voSheet.Cells(3, 2).Value = DateNumber(viYear, viMonth, 1)


まとめ

これで、帳票の見た目部分は完成となります。実行すると次のようなシートが出力されます。

ノーツのデータから Excel で帳票を出力する方法を紹介しました。さまざまな Excel のオブジェクトを利用しましたが、そのほとんどが Range オブジェクトを起点としていました。

Worksheet オブジェクトの Cells や Range プロパティ、Rows や Columns プロパティなどを使用して Range オブジェクトの取得方法とその Range オブジェクトからどのような操作ができるのかを習得することが重要ですね。


前回 Notes - Excel 連携 次回

2023/11/26

Notes - Excel 連携:#26)罫線の設定 ②

前回は、罫線の設定に必要な情報収集とライブラリの準備を行いました。今回は、これらを利用して、作成中の帳票に罫線を設定します。


ライブラリの組み込みと重複排除

まずは前回作成したスクリプトライブラリ lsXls を作成中のエージェントに組み込みます。

ライブラリに移行した関数 xRCToA1 と x9ToA は不要なので削除します。すると、関数名が変わっているので文法エラーが発生します。ライブラリの関数名 RCToA1 にすべて修正します。

xCalcSum 関数内のエラーについては、前回新規作成した関数 GetRangeString に置き換えます。


oRange.Formula = "=Sum(" & GetRangeString(iMin, viCol, iMax, viCol) & ")"


外側の罫線作画

いよいよ罫線の作画を行います。

作画の処理は、専用の関数 xDrawLine に記述します。関数の引数は、Worksheet オブジェクトと明細行の行数とします。メインルーチンは次のようになります。

Sub Initialize
      ・・・
   '計算式のセット
   Call xCalcRow(oSheet, iDoc)
   Call xCalcCol(oSheet, iDoc)

   '罫線の設定
   Call xDrawLine(oSheet, iDoc)

   'Excel を UI に表示
   oXls.Visible = True
End Sub


まずは、帳票の外側に枠線を設定します。次のように記述すると上側を

  • スタイルは ”実線”
  • 幅は ”普通”
  • 色は ”グレー”

に設定します。

Function xDrawLine(voSheet As Variant, ByVal viDoc As Integer)
   Dim oRange As Variant
   Dim oBorder As Variant

   Dim s As String
   Dim lCol As Long
   Dim iTop As Integer
   Dim iBottom As Integer

   iTop = xciHeaderRows
   iBottom = xciHeaderRows + viDoc + 1

   '色の準備
   lCol = RGB(128, 128, 128)

   '表全体
   s = GetRangeString(iTop, 2, iBottom, 9)
   Set oRange = voSheet.Range(s)

   '外枠(上)の設定
   Set oBorder = oRange.Borders(xlEdgeTop)
   oBorder.Weight = xlMedium
   oBorder.LineStyle = xlContinuous
   oBorder.Color = lCol

End Function

帳票全体にあたるセルの範囲を oRange 変数に取得し、その上側の枠線を oBorder 変数にセット。そのオブジェクトのプロパティの罫線のスタイルと幅、色を設定しています。

これを上下左右の4辺に対して設定する必要があるのですが、幸い XlBordersIndex 列挙 において 7 ~ 10 の連続した値が割り当てられています。これを利用すると次のようにループ処理でシンプルに記述できます。

   '外枠の設定
   For i = xlEdgeLeft To xlEdgeRight
      Set oBorder = oRange.Borders(i)
      oBorder.Weight = xlMedium
      oBorder.LineStyle = xlContinuous
      oBorder.Color = lCol
   Next


内側の罫線の作画

内側の罫線は、まずベースとなる実線の最も細い線、色は同じくグレーで初期設定します。内側の罫線には、縦と横の設定がありますが、こちらも XlBordersIndex 列挙 で連続した値となっています。ですので、今回もループで対応します。

   '内側のベース罫線の設定
   For i = xlInsideVertical To xlInsideHorizontal
      Set oBorder = oRange.Borders(i)
      oBorder.Weight = xlHairline
      oBorder.LineStyle = xlContinuous
      oBorder.Color = lCol
   Next


仕上げとして、初期化した罫線とは違う部分を順に設定します。

例えば、ヘッダ行の下の罫線を2重線に設定するには、そのエリアの Range オブジェクトを取得して、罫線を設定します。範囲が違うだけで手順は同じですね。

   'ヘッダ行
   s = GetRangeString(iTop, 2, iTop, 9)
   Set oRange = voSheet.Range(s)
   Set oBorder = oRange.Borders(xlEdgeBottom)
   oBorder.LineStyle = xlDouble


同様の方法で他の罫線も設定します。

最終行となる合計行の上部を2重線に設定、縦罫線は白黒とカラーの間を極細、最終列の合計を2重線に設定します。

   '合計行
   s = GetRangeString(iBottom, 2, iBottom, 9)
   Set oRange = voSheet.Range(s)
   Set oBorder = oRange.Borders(xlEdgeTop)
   oBorder.LineStyle = xlDouble

   '縦罫線
   s = GetRangeString(iTop, 3, iBottom, 5)
   Set oRange = voSheet.Range(s)
   Set oBorder = oRange.Borders(xlEdgeLeft)
   oBorder.Weight = xlThin
   Set oBorder = oRange.Borders(xlEdgeRight)
   oBorder.Weight = xlThin

   '縦罫線(合計)
   s = GetRangeString(iTop, 9, iBottom, 9)
   Set oRange = voSheet.Range(s)
   Set oBorder = oRange.Borders(xlEdgeLeft)
   oBorder.LineStyle = xlDouble


実行結果

ここまでのプログラムを実行すると次のようになります。


前回 Notes - Excel 連携 次回

2023/11/25

Notes - Excel 連携:#25)罫線の設定 ①

第 20 回からスタートした『帳票の作成』シリーズの 6 回目です。今回は帳票を帳票らしく見せるために重要な要素である罫線についてまとめます。


設定方法

Excel で罫線を設定するには設定するセルを選択して『セルの書式設定』画面を開き、[罫線]のタブから設定します。

セルを選択して設定する操作を VBA で行うにはどうするでしょうか?

そうです。セルの範囲に対する操作ですから、Range オブジェクトを使うということになりますね。その Range オブジェクトには罫線を操作するためのプロパティ Borders があります。

Range.Borders プロパティ (Excel)

このプロパティは引数が1つあり、どの部分の罫線を取得するかを指定する必要があります。指定できる値は XlBordersIndex 列挙 として定義されていて、主な値は次の通りです。

定数 説明
7 xlEdgeLeft 範囲の左側の罫線
8 xlEdgeTop 範囲の上側の罫線
9 xlEdgeBottom 範囲の下側の罫線
10 xlEdgeRight 範囲の右側の罫線
11 xlInsideVertical 範囲の外側を除くすべての垂直罫線
12 xlInsideHorizontal 範囲の外側を除くすべての水平罫線


罫線のプロパティ

Borders プロパティの型は Borders オブジェクト となります。このオブジェクトに罫線のスタイルを設定するプロパティが存在します。今回使用するのは次の3つです。

プロパティ 説明
LineStyle 罫線のスタイル
Weight 罫線の幅
Color 罫線の色


LineStyle はその名の通り、実線や破線などの罫線の種類です。XlLineStyle 列挙 として定義されており、代表的なものは次の通りです。

定数 説明
1 xlContinuous 実線
-4119 xlDouble 2 本線
-4115 xlDash 破線
-4142 xlLineStyleNone なし


Weight は罫線の太さですが、次の4種類が XlBorderWeight 列挙 として定義されています。

定数 説明
1 xlHairline 細線 (最も細い罫線)
2 xlThin 極細
-4138 xlMedium 普通
4 xlThick 太線 (最も太い罫線)


Excel の UI とプロパティの関係

セルの書式設定の「線」の選択肢では、複数のプロパティを一度に設定するようになっています。例えば、罫線のスタイル(LineStyle)を xlContinuous に限定すると次のように4つすべての太さが表示されています。

同様に「プリセット」の外側では、選択範囲の外側(XlBordersIndex 列挙 の上下左右)を一括設定できます。このように、Excel の UI では、複数のプロパティをまとめて設定するようになっていて、操作性を向上させています。


罫線の色の指定

今回はテーマを使用せず、直接色を指定する方法で作成します。Excel の VBA には RGB という関数存在します。この関数の戻り値を Borders オブジェクトの Color プロパティにセットすると罫線の色が設定できます。

RGB 関数

この関数を LotusScript で作成すると次のようになります。

Public Function RGB(_
              ByVal vbyR As Byte, ByVal vbyG As Byte, ByVal vbyB As Byte) As Long
    RGB = vbyR + CLng(vbyG) * 256 + CLng(vbyB) * 256 ^ 2
End Function


スクリプトライブラリの更新

上記の RGB 関数や前回作成の行と列を A1 形式に変換する関数は、この先何度も使用することが想定されます。そこで、これら関数をスクリプトライブラリに追加します。

グラフの作成で利用したスクリプトライブラリに追加します(#16, #17, #18)。なお、今回より、スクリプトライブラリ名を lsXls に変更していますのでご注意ください。

現時点でのスクリプトライブラリのコードは次の通りとなります。

なお、スクリプトライブラリ化にあたり、一般的に利用しそうな関数は Public として定義しています。また、Range プロパティでセルの範囲を指定する文字列(ex. A3:H12)を作成する関数 GetRangeString を追加しています。

Option Declare

'XlAxisType 列挙 (Excel)
Public Const xlCategory = 1
Public Const xlValue = 2

'XlCategoryType 列挙 (Excel)
Public Const xlAutomaticScale = -4105
Public Const xlCategoryScale = 2
Public Const xlTimeScale = 3

'MsoChartElementType 列挙 (Excel)
Public Const msoCategoryGridLinesMajor = 334
Public Const msoValueGridLinesMinorMajor = 331
Public Const msoCategoryGridLinesMinorMajor = 335

'XlBordersIndex 列挙 (Excel)
Public Const xlEdgeLeft = 7 '範囲の左側の罫線
Public Const xlEdgeTop = 8 '範囲の上側の罫線
Public Const xlEdgeBottom = 9 '範囲の下側の罫線
Public Const xlEdgeRight = 10 '範囲の右側の罫線
Public Const xlInsideVertical = 11 '範囲の外側を除くすべての垂直罫線
Public Const xlInsideHorizontal = 12 '範囲の外側を除くすべての水平罫線

'XlBorderWeight 列挙 (Excel)
Public Const xlHairline = 1 '細線 (最も細い罫線)
Public Const xlMedium = -4138 '普通
Public Const xlThin = 2 '極細
Public Const xlThick = 4 '太線 (最も太い罫線)

'XlLineStyle 列挙 (Excel)
Public Const xlContinuous = 1 '実線
Public Const xlDouble = -4119 '2 本線
Public Const xlLineStyleNone = -4142 'なし
Public Const xlDash = -4115 '破線

Public Function GetRangeString(_
      ByVal viRowFm As Integer, ByVal viColFm As Integer, _
      ByVal viRowTo As Integer, ByVal viColTo As Integer) As String
   Dim s As String

   s = RCToA1(viRowFm, viColFm)
   s = s & ":" & RCToA1(viRowTo, viColTo)

   GetRangeString = s
End Function

Public Function RCToA1(ByVal viRow As Integer, ByVal viCol As Integer) As String
   RCToA1 = x9ToA(viCol) & CStr(viRow)
End Function

Public Function RGB(_
      ByVal vbyR As Byte, ByVal vbyG As Byte, ByVal vbyB As Byte) As Long
   RGB = vbyR + CLng(vbyG) * 256 + CLng(vbyB) * 256 ^ 2
End Function

Function x9ToA(ByVal viColNumber As Integer) As String
   Dim i As Integer
   Dim i1 As Integer
   Dim i2 As Integer
   Dim s As String

   i = viColNumber - 1
   i1 = (i Mod 26) + 1
   i2 = Int(i / 26)

   If i2 > 0 Then s = Chr(64 + i2)
   s = s & Chr(64 + i1)

   x9ToA = s
End Function


続きは次回

これで罫線の設定に必要な材料はそろいました。

ただ、少し長くなったので今回はここまででとします。次回は、このライブラリを使って実際に罫線を設定します。


前回 Notes - Excel 連携 次回

2023/11/22

Variant 型変数と EMPTY

Variant 型の変数は、数値や文字列などのスカラー値だけなく、Notes Object Class や Excel の Woorksheet などのオブジェクトも格納できます。また、これらを配列として格納することも可能です。とても便利な変数で頻繁に使いますよね。

ところで、この Variant 型の変数は、宣言時の初期値は EMPTY という特殊な値となっています。今回はこの EMPTY について整理します。


初期状態と演算

まずは、変数を宣言してすぐの状態をデバッガで確認します。すると次のように値に何も表示されません。この状態が EMPTY ということになります。

この EMPTY という状態は少し特殊で、数値演算を行うと 0(ゼロ)に変換され、文字列演算の場合は null の文字列("")に変換されます。

例えば、次のようなプログラムでは、宣言したての Variant 変数を文字列演算して、メッセージボックスで表示しています。

   Dim v As Variant
   MsgBox |v = "| & v & |"|

この結果は以下の通りで、null に変換されたことがわかります。


EMPTY の判定

続いて、EMPTY の状態を判定する方法です。

LotusScript には IsEmpty という関数が用意されています。例えば次のように記述すると『EMPTY です!』とメッセージが表示されます。

   Dim v As Variant
   If IsEmpty(v) Then
      MsgBox "EMPTY です!"
   End If


Variant 変数に EMPTY をセットする方法

では、Variant 変数にいったん値をセットした後、EMPTY に戻すにはどうすればいいでしょうか? 実はこの方法がわからず、ヘルプを調べたことがこの記事を書くきっかけになりました。

調査した結果、通常の方法では EMPTY をセットする方法はないようです。ヘルプによると『Variant 型が EMPTY 値をとるのは、初期化のとき、または値が EMPTY の別の Variant 型から代入されるときだけです。』とありました。

ということは、EMPTY に戻すためには、別途 Variant 型の変数を宣言するとその値が EMPTY となり、それを初期化したい変数に代入する手順になります。なんだかめんどくさいですね。


まとめ

今回は Variant 型変数の EMPTY という値についてまとめました。

似たような状態として、オブジェクト変数の Nothing があります。Nothing の 判定は『If oSheet Is Nothing Then MsgBox "Nothing です!"』と記述できます。また、代入するときは『Set oSheet = Nothing』と記述します。

この差が LotusScript のベースとなった VBA の仕様なのかは知りませんが、統一感がないですね...

2023/11/19

タブキーの設定 

今回は、ノーツコンソーシアムの地区別研究会『大阪研究会』で日ごろからお世話になっている林 哲司さんが先日公開したコンテンツ『20231108_Coolで使いやすいNotesアプリデザイン講座V2』に関してです。

このコンテンツは、ノーツコンソーシアム主催の『新任担当者向けワークショップ』の1コマとして実施されているので、出会った方もいらっしゃるかと思います。どうすればノーツアプリが ”カッコよく” ”使いやすく” なるのかが、事例ととともにわかりやすく記載されています。デザインはセンスではなく技術であること、技術なら習得できますし、この資料を見ればそのポイントが効率よく吸収できます。しかもノーツでに実現方法つきです。

ノーツ開発者であれば一度は参照すべきコンテンツです。そして、デザインに関する様々な気づきがちりばめられていますので、ノーツ以外の開発でも役に立つ知識がたくさんあります。是非ともご一読ください。

私が作成するアプリはノーツ臭プンプンなので、これを参考に少しでも改善したいと思います。


ところで、このコンテンツを読んでいて、自身で意識したことがない機能について、解説されていました。フォーム - フィールドの並び順 に記載されている”タブキー送り”です。

よい機会ですので、検証してまとめます。


デフォルトの動作

せっかくなので、資料に倣ってフォームを作ってみました。第二形態(蒲田くん) は、プロパティボックスをうまく利用すれば簡単に実現できます。ノーツと親和性の高い”形態”と言えますね。最上級ではありませんが、効率よく Cool なデザインになるので、私は ”蒲田くん” 推しです。

さて、話を戻します。まず、何も設定しないデフォルトの動作の確認です。フォームをプリビューして動作を確認します。

1行目左のフィールドにカーソルが表示され、Tab キーを押すとその右、その次は2行目の左と移りました。デフォルトの状態では、配置したフィールドの左から右、上から下の順となりました。


タブ順の設定方法

タブの順序はフィールドのプロパティの[フィールド情報]タブの下部で設定します。初期値の 0 は設定されていない状態を表します。また、タブ順とは別にデフォルトフォーカスの設定もできます。

タブの位置に数値を設定すると、フィールドの左下にその番号が表示されます。この番号順にカーソルが移動すると仕組みになっています。次のように設定するとカーソルの移動は上から下になります。

なお、この機能は、番号の小さい順に移動しますので、連番である必要はありません。ただ、以下のように設定漏れがあるとカーソルは、Feild1 → Feild2 → Feild7 → Feild3 → Field4 と移動します。フィールドを追加した際には、設定漏れがないよう注意しましょう。


2023/11/16

新機能 !? アプリで送信するメールの DJX 対応

DJX 導入環境では、DJX メールテンプレートを使用します。この環境で送信したメールはノーツ標準のフィールド以外に、別名が保存されるフィールドが作成されます。例えば、送信者 From に対しては AltFrom、宛先 SendTo に対しては AltSendTo などです。


この別名フィールドの仕様を検証していたところ、先日、新発見があったのでご報告します。

それは、

アプリで送信するメールでも、別名フィールドは自動セットされる

です!?


まずは、これまでの私の理解です。

アプリでメールを送信する場合、SendTo に宛先を設定して、メール送信の命令を実行します。この時、SendTo にしか値をセットしていませんので、送信先のユーザが DJX ユーザであっても、届いたメールは英字のユーザ名で表示されます。これは、別名フィールドに値がないからです。


ところが、先日、以下のような単純なエージェントを作成してメールを送信したところ、受信したユーザのメールではユーザ名が別名で表示されたんです。

Sub Initialize
   Dim ns As New NotesSession
   Dim ndb As NotesDatabase
   Dim nd As NotesDocument

   Set ndb = ns.CurrentDatabase
   Set nd = ndb.CreateDocument()
   nd.SendTo = "User01 Training/Domino"
   nd.Subject = "Test Mail"
   Call nd.Send(False)
End Sub


これまでの経験と明らかに違う症状だったので、テクニカルサポートに連絡し、この挙動が正しいのか?正しいならいつからか?を確認させていただきました。


その結果 9.0.1 の初期バージョンでは、別名フィールドはセットされなかったが、9.0.1 FP10 では別名フィールドがセットされたとのことでした。ただ、どの FP から仕様が変わったかはサポート内にも情報がなかったそうです。

アプリ開発者としては大事件だったのですが、扱いが小さく若干拍子抜けしました...


以前、DJX を導入したお客様に対しても DJX を導入する場合、アプリも DJX 対応が必要で、メール送信する機能を洗い出し、送信前に別名フィールドに漢字名をセットする対応を行ってきました。DJX 登場当初から、それを常識としていたのですが、最近は違うようです。

DJX ユーザにとっては今頃仕様変更?って印象なのですが、別名は Notes/Domino 標準の機能で、その機能強化のなのでしょう(DJX は日本だけ)。ですので、DJX メールテンプレートにある所属を表示する機能など、DJX 専用の部分には未対応のようです。

使い慣れた機能だからと言って、変化がないとは限らないのですね。たまには再検証するのも大事なようです...

2023/11/14

DXL Step-by-Step:#13)イメージの形式とサイズの取得

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 13 回です。

これまで数回にわたりイメージリソースを題材に画像データの取り扱いをまとめてきました。DXL で画像を操作していると、画像データのファイル形式を判定する必要が出てくることがあります。ファイルの拡張子で判定するのが簡単ですが、必ずしも正確にフォーマットを表しているとは限らないですよね。

そこで、ファイルの中身(バイナリーデータ)から情報を取得する方法についてまとめます。今回は DXL で画像を処理する上で必要となる情報のファイルフォーマットと画像サイズの取得だけに絞って記載いたします。

各フォーマットの画像ファイルをバイナリエディタで参照した画像を添付しています。画像内の黄色アンダーラインがフォーマットの判定、赤色が画像サイズを取得している箇所となります。


PNG

◇ フォーマットの判定

PNG は、ファイルの先頭 8 バイトが固定されています。特に 2 ~ 4 バイト目には Ascii コードの "PNG" という文字がセットされています。


◇ 画像サイズ

幅が 17 ~ 20 バイト目、高さが 21 ~ 24 バイト目となっています。これを 10 進数変換すれば取得できます。4 バイト分も確保されているので、ずいぶん大きな画像にも対応しているのですね。


GIF

◇ フォーマットの判定

GIF は、ファイルの先頭 3 バイトが Ascii コードの "GIF" という文字がセットされています。


◇ 画像サイズ

幅が 6 ~ 7 バイト目、高さが 8 ~ 9 バイト目となっています。GIF の画像サイズは、下位バイト、上位バイトの順となっていますので、サイズ(10進数)に変換時する際は注意が必要です。


JPEG

◇ フォーマットの判定

JPEG ファイルは、ファイルの中身をマーカと呼ばれる特定の値で管理する仕様です。JPEG ファイルのスタートを表すマーカは、FF D8 です。ファイルの最初の 2 バイトがこの値となっていれば JPEG と判定できます。


◇ 画像ファイル

JPEG ファイルの中身は、マーカーで区切られたセグメントに分割され管理されています。セグメントには、画像圧縮条件などのパラメータ、画像の大きさなどの基本情報、画像データの本体などそれぞれ役割が定義されています。

マーカーは 2 バイトで構成されており、1 バイト目が FF、2 バイト目でその役割が判定できます(水色)。セグメントのマーカーでは、その次の 2 バイトでセグメントのサイズを表しています(紫)。これを利用すると次のマーカーが簡単に取得できます。

今回必要となる画像サイズを持つマーカーは FF C0 となります。そのマーカーまでたどり着くには、次のような流れとなります。

  1. ファイル形式を判定した次の 2 バイトを取得します
  2. FF E0 なので目的のマーカーではない
  3. 次の 2 バイトからセグメントのサイズを取得して、次のマーカーまでファイルを空読みする
  4. 次のマーカーが目的のマーカー FF C0 であるか判定し、違う場合は 3 に戻る
  5. 画像サイズを取得する
画像サイズは、マーカーの開始位置 FF を 0 とすると 5 ~ 6 バイト目が高さ、7 ~ 8 バイト目が幅となっています。PNG や GIF とは高さと幅の並びが逆なので注意が必要です。

なお、実際にはこのマーカーは C0 ~ C3 と幅があるようです。


サンプルプログラム

上記の仕様に従い画像フォーマットの判定とサイズを取得する関数を作成してみました。

まずは、メインプログラムです。画像ファイルをストリームでオープンし、そのファイル形式と画像サイズを取得して、メッセージボックスで表示しています。

なお、画像形式は定数として定義しています。

Option Declare

Public Const DXL_Image_Unknown = 0
Public Const DXL_Image_JPEG = 1
Public Const DXL_Image_GIF = 2
Public Const DXL_Image_PNG = 3

Sub Initialize
   Dim ns As New NotesSession
   Dim nst As NotesStream
   Dim s As String
   Dim iType As Integer
   Dim iSizeX As Integer
   Dim iSizeY As Integer

   Set nst = ns.CreateStream()
   Call nst.Open("D:\TitleS.png")

   iType = xGetImageFileInfo(nst, iSizeX, iSizeY)
   If iType > 0 Then
      If iType = DXL_Image_PNG Then s = "PNG"
      If iType = DXL_Image_GIF Then s = "GIF"
      If iType = DXL_Image_JPEG Then s = "JPEG"

      s = "画像形式 = " & s
      s = s & Chr(10) & "幅 = " & CStr(iSizeX)
      s = s & ", 高さ = " & CStr(iSizeY)

      MsgBox s, 64
   Else
      MsgBox "画像形式を判定できませんでした。", 48
   End If
End Sub


関数のネスト順とは逆転するのですが、まず、画像形式を判定する関数から紹介します。

最初に整理した通り、画像形式の判定であればファイルの先頭から 4 バイトあれば判定できます。その部分だけをファイルから読み取り判定に使用しています。

Function xGetImageFileType(vnstImage As NotesStream) As Integer
   Dim vTmp As Variant
   Dim iT As Integer

   iT = DXL_Image_Unknown

   '先頭 4 バイト取得
   vnstImage.Position = 0
   vTmp = vnstImage.Read(4)

   If vTmp(0) = &h47 And vTmp(1) = &h49 Then
      'GIF
      iT = DXL_Image_GIF
   ElseIf vTmp(0) = &hFF And vTmp(1) = &HD8 Then
      'jpeg
      iT = DXL_Image_JPEG
   ElseIf vTmp(1) = &h50 And vTmp(2) = &h4E And vTmp(3) = &h47 Then
      'PNG
      iT = DXL_Image_PNG
   End If

   xGetImageFileType = iT
End Function


次の関数がメインルーチンからコールされている画像形式と画像サイズを取得している関数です。引数は、画像ファイルをオープンしたストリームとサイズ情報です。サイズ情報の riSizeX と riSizeY は値を返すための引数です。また、戻り値は画像の形式を返します。

まず、上記 xGetImageFileType をコールして画像形式を取得して、形式毎に画像サイズを取得しています。セグメントを捜索する処理のある JPEG が少し複雑になっていますが、上記画像フォーマットの説明の通りのコードとなっています。

Function xGetImageFileInfo(vnstImage As NotesStream, riSizeX As Integer, riSizeY As Integer) As Integer
   Dim vTmp As Variant
   Dim iT As Integer
   Dim iX As Integer
   Dim iY As Integer

   On Error GoTo ErrProc

   '画像形式取得
   iT = xGetImageFileType(vnstImage)
   If iT = 0 Then GoTo ExitProc

   '画像サイズ取得
   vnstImage.Position = 0
   If iT = DXL_Image_PNG Then
      vTmp = vnstImage.Read(16)'空読み

      '大きな画像はない前提に下2バイトのみ使用
      '幅

      vTmp = vnstImage.Read(4)
      iX = CInt(vTmp(2)) * 256 + CInt(vTmp(3))
      '高さ
      vTmp = vnstImage.Read(4)
      iY = CInt(vTmp(2)) * 256 + CInt(vTmp(3))
   ElseIf iT = DXL_Image_GIF Then
      vTmp = vnstImage.Read(6) '空読み
      vTmp = vnstImage.Read(4)
      '幅
      iX = CInt(vTmp(1)) * 256 + CInt(vTmp(0))
      '高さ
      iY = CInt(vTmp(3)) * 256 + CInt(vTmp(2))
   ElseIf iT = DXL_Image_JPEG Then
      vTmp = vnstImage.Read(2) 'フォーマットのマーカ空読み

SearchMarker:
      'マーカー確認
      vTmp = vnstImage.Read(2)
      If vTmp(0) = &HFF And vTmp(1) >= &HC0 And vTmp(1) <= &HC3 Then
         'マーカー発見
         vTmp = vnstImage.Read(8)
         iX = CInt(vTmp(5)) * 256 + CInt(vTmp(6))
         iY = CInt(vTmp(3)) * 256 + CInt(vTmp(4))
      Else
         'セグメントのサイズを取得し空読みして次のマーカを確認
         vTmp = vnstImage.Read(2)
         vnstImage.Position = vnstImage.Position + vTmp(0) * 256 + vTmp(1) - 2
         GoTo SearchMarker
      End If
   End If

ExitProc:
   riSizeX = iX
   riSizeY = iY
   xGetImageFileInfo = iT

   Exit Function
ErrProc:
   iT = DXL_Image_Unknown
   iX = 0
   iY = 0

   Resume ExitProc
End Function


どちらの関数もなのですが、ファイルを確実に先頭から読み込むためにストリームの Position を 0 (=先頭)に設定しています。このように、読み込み位置を自在に扱えるのでストリームは便利ですね。


まとめ

今回は、画像形式と画像サイズを取得する方法をまとめました。

ローコードとかノーコードとかが話題となっている昨今において、まさかバイナリーファイルをバイト単位で読み込むような処理を作ることになるとは思っていませんでした。今どきの開発者の方にはなじまないのかもしれないですね。

私は?というと、バイナリエディタでファイルを開くなんて久しぶりの経験でした。35 年ほど前、某ファ○コンのスーパー○リオのジャンプ力や走力を魔改造してた頃を思い出し、懐かしさを感じながら調査していました(笑)

前回 DXL Step-by-Step 次回

2023/11/12

DXL Step-by-Step:#12)イメージの形式と DXL の関係

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 12 回です。

第 8 回から DXL で画像データを扱う方法についてまとめてきました。これまでは、話を単純化するため、扱う画像を PNG に限定して記載してきました。ただ、実際のアプリ開発においては、別の形式の画像ファイルを使用することが多々あります。そこで、今回は一般的に使用される JPEG と GIF についてもまとめておきます。

なお、最近は使用頻度は下がっているとは言え BMP 形式を使用することもあるかと思います。ただ、ノーツ内で BMP は、内部的に一般的な BMP と NOTESBMP というノーツ専用の形式の2種類で扱われているようです。現時点では、仕様がつかめていない点もありますので、いったん割愛します。もう少し情報が集まれば別途まとめたいと思います。


DXL のタグと $MimeType

デザイナーから通常の手順でイメージリソースを登録し、それを DXL で表示させ確認しました。結果、各画像形式毎のタグと $MimeType は次の通りでした。

画像形式 タグ $MimeType
PNG <png> image/png
JPEG <jpeg> image/jpeg
GIF <gif> image/gif

この結果は、Notes 12.0.2 を使用して確認しました。

ところが、Notes 9.0.1 を使用した場合は、少し結果が違いました。PNG ファイルを DXL に変換してみたところ、画像データのタグが <jpeg> となっていました。

しかし、$MimeType の値は "image/png" で 12.0.1 と同じでした。

これがバグなのか仕様なのかは判断できませんが、バージョンによってタグが変わることがあるという点に注意が必要ですね。


画像データのアクセス方法

イメージリソースに対して、画像を入出力する場合、画像データが入っているタグが明確にならないと処理ができません。

例えば、DXL からイメージデータを抜き出す場合で考えると、上記の関係を利用して、次のように処理すると、画像形式にとらわれず柔軟に処理ができるかと思います。

  1. $MimeType フィールドを取得
  2. その値からタグ名を判断
  3. そのタグのエレメントを取得
  4. その配下のテキストノードから Base64 の画像データを取得

前回 DXL Step-by-Step 次回

2023/11/09

DXL Step-by-Step:#11)イメージリソースの DXL の注意点

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 11 回です。

DXL は知らないことばかりなので、整理しておきたい情報が多く連投状態となっております。他の連載などはおいおい進めますので、もう少々お待ちください。


さて、前回は、イメージリソース(設計要素)を 新規作成する方法を紹介しました。この処理で生成される DXL は次のようになります。

出来上がりを見るとシンプルでわかりやすいのですが、いくつか制約など注意点がありますので整理しておきます。


タグの順序

DXL をインポートするときタグの順序が正しくないと、保存(Import)時にエラーが発生します。今回の3つのタグを使用して実験を繰り返した結果、条件は次の通りでした。

  • <png> タグは先頭であること
  • フィールド <item> タグはその次(画像データの後)
  • フィールドは銃所が入れ替わってもかまわない


エラーが発生した場合、次のような画一的なメッセージが表示されます。

これでは、エラーの原因はわかりませんよね。

今回ぐらい単純な DXL ならトライ&エラーで原因調査することも可能ですが、複雑な場合はそうもいきません。せめてエラーの発生したタグぐらい表示してほしいですね。


フィールドの作成

今回は $FileSize と $MimeType の2つのフィールドを作成しました。この2つは作成しなくても、イメージリソースの作成は成功しました。また、フォームに呼び出すなど、利用も問題ないようでした。どうやらこれらフィールドは必須ではないようです。

ただ、$FileSize を作成しない場合、設計の一覧でファイルサイズが表示されなくなりました。実害はないようですが、表示された方が安心ですよね。

また、$MimeType フィールドは、ファイル形式を表している唯一のフィールドに見えたのであった方が良いと判断し、作成することとしました。


重複チェックはされない

そして最後に重要な点です。

DXL から設計要素を作成する場合、名称による重複チェックは行われません。例えば今回のエージェントを複数回実行すると次のように同名のイメージリソースが複数作成されます。まるで、設計要素の競合が発生したような状態ですね。

これは、保存(Import)の際、DesignImportOption プロパティに DXLIMPORTOPTION_CREATE を指定していたので、毎回新規作成したのだと思います。なかなか素直ですね。

実用時には、新規作成なのか更新なのかを判定して、DesignImportOption プロパティをコントロールする必要があるということですね。


まとめ

今回のような制限事項は、イメージリソースに限らずあるようです。エラーが発生して初めて気が付くことになりますので、トライ&エラーの連続となります。これが DXL 開発の難点ですね...

前回 DXL Step-by-Step 次回

2023/11/07

DXL Step-by-Step:#10)イメージリソースの新規作成

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 10 回です。

イメージリソース(設計要素)の読み込みについて、第 8 回第 9 回 で紹介しました。今回は、イメージリソースを新規作成する方法についてまとめます。

DXL を利用すれば、設計要素も新規作成できるようになります。これは、通常の NotesObjectClass だけではできないことなので DXL で Notes アプリの可能性が広がる部分となりますね。


Base64 のエンコード

DXL 内の画像データは、Base64 でエンコードされていました。今回は、イメージリソースに画像データを渡すことになるので、画像データ(バイナリファイル)を Base64 でエンコードされた文字列に変換する必要があります。

前回紹介したデコードと同じく OpenNTF の LotusScript Gold Collection プロジェクトに関数がありました。次のようなコードです。

%REM
   Function StreamToBase64
   Description: Convert a binary NotesStream to a string of Base64 data.
      The output can be used as part of DXL data for importing.
%END REM

Function StreamToBase64(streamIn As NotesStream) As String
   On Error GoTo theOldWay
   ' ReadEncoded function is not documented. In case it doesn't work have a backup.
   StreamToBase64 = Replace(streamIn.ReadEncoded(ENC_BASE64, 76), Chr$(13), "")
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)

   StreamToBase64 = Replace(mime.ContentAsText, Chr$(13), "")
End Function

この関数は、なかなか興味深いですね。

コード内のコメントにも記載がありますが、ReadEncoded というヘルプに記載のないメソッドを使用しています。確かに最新の 12.0.2 のデザイナーヘルプを見ても NotesStream クラスにそのようなメソッドは記載されていません。この OpenNTF のプロジェクト、きっと、Notes/Domino 開発の ”中の人” かその周辺の方の業物なのでしょうね...

そして、この関数には保険がかかっています。万一このメソッドでエラーが発生した場合に備えて、エラー処理の中(theOldWay)に Plan B が記述されています。こちらは、デコードと同様の方法で実現されており、NotesMIMEEntity クラスを活用して変換しています。


サンプルプログラム

上記関数を利用しながら、イメージリソースを新規作成します。次のようなプログラムを作成しました。

まず、メインルーチンです。DXL をコントロールするために NotesDOMParser のオブジェクトを作成し、これに DXL をセットする xSetDXL 関数と 設計要素としてを保存する xImportDXL 関数をコールしています。

Sub Initialize
   Dim dprs As NotesDOMParser
   Dim ndb As NotesDatabase

   Set xns = New NotesSession
   Set ndb = xns.CurrentDatabase

   Set dprs = xns.CreateDOMParser()
   Call xSetDXL(dprs, "d:\denaoshi.png", "denaoshi2.png")

   Call xImportDXL(dprs, DXLIMPORTOPTION_CREATE)

End Sub


次はイメージリソースの DXL を生成するための xSetDXL 関数です。作成する DXL は最低限のタグだけとしています。まず、画像ファイルを NotesStream で開き、それを Base64 の文字列に変換して、<png> タグを作成しています。

Function xSetDXL( _
         vdprs As NotesDOMParser, _
         ByVal vsFilePath As String, _
         ByVal vsResourceName As String)

   Dim ddn As NotesDOMDocumentNode
   Dim denNew As NotesDOMElementNode
   Dim denDoc As NotesDOMElementNode
   Dim denItem As NotesDOMElementNode
   Dim dtnNew As NotesDOMTextNode

   Dim nst As NotesStream

   '画像ファイルを開く
   Set nst = xns.CreateStream()
   Call nst.Open(vsFilePath)


   'イメージリソースのエレメント作成
   Set ddn = vdprs.Document
   Set denNew = ddn.CreateElementNode("imageresource")
   Call denNew.SetAttribute("name", vsResourceName)
   Set denDoc = ddn.AppendChild(denNew)

   '画像イメージの追加
   '--------------------

   Set denNew = ddn.CreateElementNode("png")
   Set denItem = denDoc.AppendChild(denNew)
   '画像データセット
   Set dtnNew = ddn.CreateTextNode(StreamToBase64(nst))
   Call denItem.AppendChild(dtnNew)
   Set denNew = ddn.CreateElementNode("text")

   'フィールドの追加
   '--------------------
   '$FileSize

   Call xAppendField(ddn, "$FileSize", nst.Bytes)

   '$MimeType
   Call xAppendField(ddn, "$MimeType", "image/png")
End Function

その後必要なフィールドである $FileSize と $MimeType を作成しています。ファイルサイズは、NotesStream クラスの Bytes プロパティで取得しています。これは便利ですね。


フィールドを作成する関数は次の通りです。第 4 回 で紹介したフィールドを作成する関数 xAppendField_Text を少し改造し、テキストに限らず数値でも利用できるようにしました。具体的には、セットする値 vvValue を Variant 型に変更し、関数内で型をチェックして、型に合わせたタグを生成し、値をセットしています。

Function xAppendField( _
         vddn As NotesDOMDocumentNode, _
         ByVal vsFldName As String, _
         ByVal vvValue As Variant) As Boolean

   Dim sType As String

   '型判定
   If IsArray(vvValue) Then
      '配列は未対応
   Else
      If TypeName(vvValue) = "STRING" Then sType = "text"
      If TypeName(vvValue) = "INTEGER" Then sType = "number"
      If TypeName(vvValue) = "LONG" Then sType = "number"
      If TypeName(vvValue) = "SINGLE" Then sType = "number"
      If TypeName(vvValue) = "DOUBLE" Then sType = "number"

   End If
   If sType = "" Then Exit Function '未対応のため中断

   'document ノード取得
   Dim denCur As NotesDOMElementNode
   Set denCur = vddn.DocumentElement

   'item ノード作成とフィールド名の指定
   Dim denNew As NotesDOMElementNode
   Set denNew = vddn.CreateElementNode("item")
   Call denNew.SetAttribute("name", vsFldName)
   Set denCur = denCur.AppendChild(denNew)

   '値のノードと値のセット
   Dim dtnNew As NotesDOMTextNode
   Set denNew = vddn.CreateElementNode(sType)
   Set denCur = denCur.AppendChild(denNew)
   Set dtnNew = vddn.CreateTextNode(CStr(vvValue))
   Call denCur.AppendChild(dtnNew)
End Function

日付や配列にはまだ対応できていませんので、実用化するにはまだ改善が必要ですね。


最後は DXL を設計要素として保存する関数です。第 4 回 で文書を保存する関数として紹介した xImportDXL とほぼ同様なのですが、今回は設計要素なので DesignImportOption プロパティを使用しています。

Function xImportDXL(vdprs As NotesDOMParser, ByVal viOption As Integer) As Boolean
   'DXL の抽出準備
   Dim nst As NotesStream
   Set nst = xns.CreateStream()
   Call vdprs.SetOutput(nst)
   Call vdprs.Serialize()

   '保存(インポート)
   Dim ndb As NotesDatabase
   Dim dimp As NotesDXLImporter
   Set ndb = xns.CurrentDatabase
   Set dimp = xns.CreateDXLImporter()
   dimp.DesignImportOption = viOption
   Call dimp.Import(nst.ReadText(), ndb)
End Function

今回は設計要素の新規作成ですのでこの viOption には、DXLIMPORTOPTION_CREATE が指定されています(この関数をコールしているメインルーチンを参照)。


実行結果

このプログラムを実行すると次のように設計要素が作成されます。


前回 DXL Step-by-Step 次回

2023/11/05

DXL Step-by-Step:#9)DXL 内の画像のダウンロード

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 9 回です。

今回はイメージリソース(設計要素) から 画像を取り出す方法についてまとめます。


画像ファイルの状態

まず、前回確認したイメージリソースの DXL で画像データは次のように文字列で表されていました。

画像データであるバイナリーデータが Base64 でエンコードした状態となっています。メールの添付ファイル(MIME)と同様ですね。


Base64 のデコード

エンコードされた画像ファイル(=文字列)を Base64 でデコードすれば、画像データが取得できます。では、どうやってデコードするのでしょうか?

私が DXL に出会うきっかけとなった OpenNTF の LotusScript Gold Collection プロジェクトで公開されている DB 内で関数化されていました。今回はこれを拝借します。

私が作ったコードではないので、関数全体をコメント付きでそのまま転載します。

%REM
   Sub Base64ToBinary
   Description: Given a string of base64-encoded data, write into a binary stream we are passed.
      This is done rather than creating the stream here and returning it, so that you can
   stream directly into a file if you choose.
%END REM

Sub Base64ToBinary(strBase64$, streamOut As NotesStream)
   ' Given a string of base64 encoded data, this routine decodes and writes the original binary data into a NotesStream
   Dim doc As NotesDocument
   Dim mime As NotesMIMEEntity
   Dim streamIn As NotesStream
   Dim db As NotesDatabase
   Dim session As New NotesSession

   Set db = session.CurrentDatabase
   Set doc = db.CreateDocument
   Set mime = doc.CreateMIMEEntity("Body") ' the mime classes already know how to do this conversion,
   Set streamIn = session.CreateStream
   Call streamIn.WriteText(strBase64)
   streamIn.Position = 0
   Call mime.SetContentFromText(streamIn, "binary", ENC_BASE64)
   Call mime.GetContentAsBytes(streamOut, True) ' decode as you stream out the data.
End Sub

デコードは NotesMIMEEntity クラスを活用して実行しているようです。このクラスは、今回初めて利用したので詳細はわかりかねるのですが、SetContentFromText メソッドで、エンコード文字列をデコードしつつ NotesMIMEEntity のオブジェクトに取り込み、GetContentAsBytes メソッドでバイナリデータとして抽出しているようです。


サンプルプログラム

それでは、上記関数も活用しつつ、イメージリソースからファイルを抽出するプログラムを作成します。

まず、メインルーチンです。前回のコードの DXL を保存する部分を画像ファイルをダウンロードする新しい関数を指定しています。

Option Declare
Private xns As NotesSession

Sub Initialize
   Dim ndb As NotesDatabase
   Dim ndAgent As NotesDocument

   Set xns = New NotesSession
   Set ndb = xns.CurrentDatabase

   Dim nnc As NotesNoteCollection
   Set nnc = ndb.CreateNoteCollection(False)
   nnc.SelectImageResources = True
   nnc.SelectionFormula = xGetFormula("TitleS.PNG")
   Call nnc.BuildCollection()

   Dim dprs As NotesDOMParser
   If nnc.Count > 0 Then
      Set ndAgent = ndb.GetDocumentByID(nnc.GetFirstNoteID)
      Set dprs = xGetDOMParser(ndAgent)

      Call xDownloadImage(dprs, "d:\denaoshi.png")
   End If
End Sub


続いて、画像ファイルをダウンロードするメインルーチンです。まず、png エレメントノードを取得して、Base64 でエンコードされた画像データを sB64 変数に代入しています(赤字)。png ノードの直下にエンコード文字列があります。この場合、最初の子ノードがテキストノードとなるため、このようはコードになっています。

Function xDownloadImage(vdprs As NotesDOMParser, ByVal vsFP As String) As Boolean
   'DOM ツリーのルートを取得
   Dim ddn As NotesDOMDocumentNode
   Set ddn = vdprs.Document

   'ImageResource エレメントの取得
   Dim denImage As NotesDOMElementNode
   Set denImage = ddn.DocumentElement

   '画像ファイルダウンロード
   Dim denPNG As NotesDOMElementNode
   Dim dnl As NotesDOMNodeList
   Dim dtn As NotesDOMTextNode
   Dim nst As NotesStream
   Dim sB64 As String

   Set dnl = denImage.GetElementsByTagName("png")
   If dnl.NumberOfEntries > 0 Then
      Set denPNG = dnl.GetItem(1)
      Set dtn = denPNG.FirstChild


      'エンコードされた画像データ取得
      sB64 = dtn.NodeValue

      'ファイルとして保存
      Set nst = xns.CreateStream()
      Call Base64ToBinary(sB64, nst)
      Call StreamToImageFile(nst, vsFP)

   End If
End Function

取得したエンコード文字列をファイルに変換しているのは青字の部分です。事前に紹介した関数を使用して、エンコード文字列をバイナリデータに変換し、NotesStream にセットしています。それを ファイルに出力する関数 StreamToImageFile に渡しています。


StreamToImageFile 関数は次の通りです。ファイルを NotesStream で開いて、イメージデータの NotesStream から ファイルの NotesStream に渡しています。

Sub StreamToImageFile(vnstImage As NotesStream, ByVal vsFP As String)
   Dim nstOut As NotesStream

   On Error Resume Next
   Kill vsFP '存在してたら削除

   Set nstOut = xns.CreateStream()
   Call nstOut.Open(vsFP)

   vnstImage.Position = 0
   Do Until vnstImage.Position >= vnstImage.Bytes
      Call nstOut.Write(vnstImage.Read(16000))
   Loop
End Sub

NotesStream クラスの使い方ってなんとなく抽象的で、つかみどころがなく苦手です。

ファイルみたいなもので、Position を使って、ファイル内を行ったり来たりできるということなのでしょうね。このコードを書いていてそんな気がしてきました。


実行結果とまとめ

このプログラムを実行すると次のように画像ファイルが出力されます。



ノーツでノーツアプリを作っているだけでは、ノーツ以外の知識以外が身に付きにくいですよね。今回初めて Base64 について調べることになりました。DXL は、LotusScript でノーツアプリを作っているだけではあるのですが、少しだけ外の世界を見るきっかけとなりました...


前回 DXL Step-by-Step 次回