2024/10/13

DXL Step-by-Step:#41)インラインイメージの貼り付け

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 41 回です。しばらく表の話が続いたので少し流れを変えます。

今回はリッチテキストに直接貼り付ける画像についてまとめます。 ノーツクライアントでは、下図のようにプロパティで見るとソース欄に[インラインイメージ]と表示されるパターンですね。

DXL を使えば、リッチテキストに画像を貼った文書を作成することができます。また、文書から画像をダウンロードしてファイルに保存することも可能です。


インラインイメージの DXL

先ほどのリッチテキストを DXL で書き出すと次のようになります。

画像を表す png タグがあります。その配下には暗号化されたような文字列がテキストノードとして大量に出力されています。この構造は『#8)イメージリソース(設計要素)の DXL』で紹介した、イメージリソース内に保存されている画像データと同じですね。

画像データの1つ上は picture ノードとなっています。このノードは『#26)イメージリソースの DXL』で、リッチテキストにイメージリソースを配置する際に出てきました。

リッチテキストに配置したインラインイメージとイメージリソースの違いは、配下のノードが画像データか、リソースの参照 imageref かだけです。


サンプルプログラム

それでは、実際にインラインイメージを配置するプログラムを作成します。『#27)イメージリソースの表示』で作成したプログラムをベースに作業します。

エージェントをコピペして xSetDXL_ImageResource 関数を次の通り修正します。

Function xSetDXL_InlineImage(vddn As NotesDOMDocumentNode, vdenRT As NotesDOMElementNode, ByVal vsFileName As String)
   Dim denPar As NotesDOMElementNode
   Dim denPic As NotesDOMElementNode
   Dim den As NotesDOMElementNode
   Dim dtn As NotesDOMTextNode

   Dim nst As NotesStream
   Dim iType As Integer
   Dim iSizeX As Integer
   Dim iSizeY As Integer

   '画像ファイルの確認
   Set nst = xns.CreateStream()
   Call nst.Open(vsFileName)
   iType = xGetImageFileInfo(nst, iSizeX, iSizeY)

   If iType <> DXL_Image_Unknown Then

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

      '画像の作成
      Set den = vddn.CreateElementNode("picture")
      Call den.SetAttribute("width", CStr(iSizeX) & "px")
      Call den.SetAttribute("height", CStr(iSizeY) & "px")
      Set denPic = denPar.AppendChild(den)

      'インラインイメージの作成
      Set den = vddn.CreateElementNode(xGetImageNodeName(iType))
      Set den = denPic.AppendChild(den)

      '画像データセット
      Set dtn = vddn.CreateTextNode(StreamToBase64(nst))
      Call den.AppendChild(dtn)
   End If
End Function


画像ファイルの確認

xSetDXL_InlineImage 関数には、引数 vsFileName を追加しました。これはインラインイメージとして貼り付ける画像のファイル名です。指定したファイルは、関数の最初で画像の形式を確認しています。

画像ファイルを調査して、画像のフォーマットとサイズを返す関数 xGetImageFileInfo をコールしています。この関数は、『#13)イメージの形式とサイズの取得』で紹介しています。xGetImageFileInfo とサブ関数 xGetImageFileType をコピペします。


画像の作成

picture ノードの作成では、画像のサイズ width と height 属性を追加しています。イメージリソースを呼び出す場合はサイズ指定がなくても表示されたのですが、インラインイメージの場合は指定しないと画像が表示されませんでした。


インラインイメージの作成

インラインイメージの場合、画像の形式により作成するタグが変わります。画像ファイルに合致したタグ名を xGetImageNodeName 関数から取得し、ノードをを作成しています。

xGetImageNodeName 関数は次の通りです。

Function xGetImageNodeName(ByVal viImageType As Integer) As String
   Dim s As String

   If viImageType = DXL_Image_PNG Then s = "png"
   If viImageType = DXL_Image_GIF Then s = "gif"
   If viImageType = DXL_Image_JPEG Then s = "jpeg"

   xGetImageNodeName = s
End Function


画像データセット

画像データは Base64 でエンコードされた文字列を指定する必要があります。変換する関数 StreamToBase64 は『#10)イメージリソースの新規作成』で紹介しているので、コピペします。


呼び出し元の修正

xSetDXL_InlineImage 関数が完成したら、呼び出しもとを修正します。

Function xSetDXL(vdprs As NotesDOMParser)
         ・・・
   '段落定義
   Call xSetDXL_pardef(ddn, denRT)

   'インラインイメージの追加
   Call xSetDXL_InlineImage(ddn, denRT, "E:\411.PNG")
End Function

これでサンプルは完成です。実行するとこの記事の最初の画像のようにインラインイメージが貼り付けられたリッチテキストが作成されます。


まとめ

今回はインラインイメージをリッチテキストに追加する方法を紹介しました。LotusScript 標準の RichText クラスではできない機能ですので、これができるようになるとプログラム経由で作る文書の表現力が格段にアップしますね。


前回 DXL Step-by-Step


2024/10/11

DXL Step-by-Step:#40)表のスタイル - タブ形式

ノーツの表には特殊な設定があって、これを利用するとタブ形式やアコーディオン形式の UI が作成できます。表のプロパティでは[表の行]タブの設定になります。今回はこの設定を DXL で操作してみましょう。

タブ形式の表では、表の行数分のタブが表示され、タブをクリックすることでその行を表示します。要はタブのクリックで表示する行を切り替える機能ということですね。ですので、プロパティでは『1 行のみ表示』を選択することになります。

そして、スタイルは『行の表示方法』で指定します。『タブボタンで行を選択』でタブ形式、『表題で行を選択』でアコーディオン形式となります。タブの表題は『タブラベルと表題』にしています。これは、行ごとに設定することになります。


表のスタイルの設定

DXL で表のスタイルを設定するには、table ノードの rowdisplay 属性を設定します。これまでこの属性を指定していなかったので、通常の表として表示されていたということですね。

属性 設定値 補足
rowdisplay (なし) 通常の表
'tabs' タブボタンで行を選択(タブ形式)
'captions' 表題で行を選択(アコーディオン形式)


タブの表題の設定

タブの表題は行ごとに指定することから tablerow ノードの属性として設定します。属性名は tablabel です。


サンプルプログラム

#32)表の作成と行・列の間隔 では、3 行 2 列の単純な表を作成するサンプルプログラムを紹介しました。今回はこれを改造します。

まず、表を作成する関数 xSetDXL_table を修正します。赤字が修正箇所です。

Function xSetDXL_table(_
            vddn As NotesDOMDocumentNode, _
            vdenRT As NotesDOMElementNode, _
            vvTabName As Variant, ByVal viCols As Integer _
            ) As NotesDOMElementNode
   Dim denTbl As NotesDOMElementNode
   Dim denRow As NotesDOMElementNode
   Dim den As NotesDOMElementNode
   Dim iCol As Integer
   Dim iRow As Integer

   '表の作成
   Set den = vddn.CreateElementNode("table")
   Set denTbl = vdenRT.AppendChild(den)
   'タブ形式の表
   Call denTbl.SetAttribute("rowdisplay", "tabs")

   '列定義の作成
   For iCol = 1 To viCols
      Set den = vddn.CreateElementNode("tablecolumn")
      Call denTbl.AppendChild(den)
   Next

   '行の作成
   For iRow = 0 To UBound(vvTabName)
      Set den = vddn.CreateElementNode("tablerow")
      Set denRow = denTbl.AppendChild(den)
      'タブ名称
      Call denRow.SetAttribute("tablabel", vvTabName(iRow))

      'セルの作成
      For iCol = 1 To viCols
         Set den = vddn.CreateElementNode("tablecell")
         Call denRow.AppendChild(den)
      Next
   Next

   Set xSetDXL_table = denTbl
End Function

まず、引数では行数を指定するのではなく、タブの表題を文字列型の配列で渡す仕様に変えています。これに伴い、行を作成するループの指定が配列の要素数分に変更されています。

そして、本題のタブの設定は、table ノードに rowdisplay 属性、tablerow ノードに対して tablabel 属性をセットしている 2 か所となります。


関数の修正に伴い、呼び出しもとを次のように修正します。

Function xSetDXL(vdprs As NotesDOMParser)
         ・・・
   '表前の段落追加
   Call xSetDXL_par(ddn, denRT)

   'タブ表の追加
   Dim asTabName(1)
   asTabName(0) = "Sample Tab 1"
   asTabName(1) = "タブ2"
   Set denTbl = xSetDXL_table(ddn, denRT, asTabName, 1)


   '間隔の設定
         ・・・
End Function

タブ名称を asTabName 配列にセットして、関数をコールしているだけです。


実行結果

修正したプログラムを実行すると、次のようにタブが 2 つの表が作成されます。

また、rowdisplay 属性を captions に変更すると次のようにアコーディオン形式となります。


前回 DXL Step-by-Step


2024/10/09

リスト配列

先日、LotusScript でリスト配列を使う機会がありました。初めての利用だったのでヘルプで調べながら開発を行いました。せっかく調べたので、機能を整理しておきます。


リスト配列とは?

LotusScript では、Boolean、Integer、String などさまざまなデータ型が存在します。そして、同じデータ型の値をまとめて管理する構造体があります。配列というとなじみがありますよね。List も配列の一種です。

通常の配列は a1(0) など変数の後ろにカッコでインデックス番号(添え字や要素番号とも言います)を付けると値にアクセスできます。一方、List の場合は、インデックス番号の代わりに文字列(以下リストタグと呼ぶ)を使用します。

例えば、ユーザとそのユーザの点数を管理する場合を考えてみましょう。それぞれ、次のようなコードとなります。

List の場合 配列の場合
aiScore("Ito") = 75
aiScore("Sato") = 90
asName(0) = "Ito"
asName(1) = "Sato"
aiScore(0) = 75
aiScore(1) = 90


宣言方法

リスト配列を宣言するには次のように記述します。

   Dim aiScore List as Integer

aiScore("Ito") と配列のように記述するのですが、宣言に ( ) は必要ありません。要素や領域の確保は不要ということですね。また、配列のように多次元にすることはできません。


値の追加方法

リスト配列に値を追加するのは簡単です。次のように要素にリストタグを指定するだけです。

    aiScore("Ito") = 75

リスト配列内に "Ito" という要素がなければ新規で追加され、値がセットされます。すでに存在した場合は、値が更新されます。配列のように ReDim 操作が不要なので便利ですね。


値のアクセス

リスト配列の値のアクセスも値との追加と同じように、リストタグを指定するだけです。例えば、以下のコードはリストタグ Ito の値を 10 加算しています。

    aiScore("Ito") = aiScore("Ito") + 10


ループ

平均値を出力するなど、リスト内のすべての要素にアクセスしたい場合があります。このような処理には Forall ループを使用します。

例えば、次のコードではリスト配列 aiScore の合計 iSum と 件数 iCnt を算出し、平均を表示しています。

   ForAll iScore In aiScore
      iSum = iSum + iScore
      iCnt = iCnt + 1
   End ForAll

   MsgBox "平均点 = " & Format(iSum / iCnt, "#0.0")

Forall で使用している iScore がリスト配列の各要素(値)が順に入る変数です。


リスト配列の各種操作

上記情報で、リスト配列の通常使用は可能だと思います。ここから先はリスト配列利用に関連する各種操作をまとめます。チェックなど込み入った操作をする場合などに参考にできます。


◇ リストタグの取得

Forall ループの処理では、各要素の値がループ変数にセットされました。その値のリストタグを取得する関数が ListTag 関数です。

次のコードではリスト配列 aiScore のすべてのリストタグを順に表示します。

   ForAll iScore In aiScore
      MsgBox ListTag(iScore)
   End ForAll


◇ 要素の存在確認

続いては指定したリストタグが、リスト内に存在するか判定する方法です。次のコードではリスト配列 aiScore に Tanaka の要素があるか判定します。

   If IsElement(aiScore("Tanaka")) Then
      MsgBox "リスト配列内に存在します!"
   Else
      MsgBox "リスト配列内に存在しません。"
   End If


◇ 要素の削除

Erase ステートメントを使用するとリスト内の要素を削除することができます。

   Erase aiScore("Hata")

もし、指定した要素がリスト配列内に存在しない場合、エラーが発生します。


◇ リスト配列の初期化

Erase ステートメントを使用するとリスト配列内のすべてのエントリを削除し初期化することができます。

   Erase aiScore

リストタグを指定せず変数名を指定するとすべての要素を削除するということですね。


◇ リスト変数の判定

DataType 関数を使用すると変数のデータタイプが取得できます。

   Dim aiScore List As Integer
   MsgBox DataType(aiScore)

リスト配列は 2048 となりますが、上記の結果は 2050 となります。これは 整数型のリスト配列のためで、リスト配列 2048 + 整数型 2 = 2050 となります。型により値が変わるので注意が必要です。

また、リスト配列かを判定するだけであれば IsList 関数が便利です。

   If IsList(aiScore) Then
      MsgBox "aiScore はリスト配列です。"
   Else
      MsgBox "aiScore はリスト配列ではありません。"
   End If


◇ 空の判定

リスト配列を宣言した直後や Erase で初期化した後は、中身が空っぽの状態となります。この判定の方法については明確な関数は発見できませんでした。Ubound や Lbound のような関数はないようです。また、残念ながら IsEmpty の戻り値は True となり判定できませんでした。

Forall ループでは、ループ内の処理が一切実行されないので、それで判定する方法が良いかもしれませんね。


まとめ

今回は、リスト配列の使い方についてまとめました。配列と同様に、同じデータ型の値をまとめて管理できる変数でした。要素のアクセスは リストタグ と呼ばれる文字列で指定する点がポイントですね。

この記事の最初に『ユーザとそのユーザの点数を管理する場合』の例を上げ、配列との比較をしました。どちらが優れているという話をしたかったわけではありません。配列とリスト配列、近しい機能ではありますが、それぞれ違いがあります。作成するプログラムの仕様に合わせて、適材適所、使い分けができるといいですね。


2024/10/06

意外と難しかった四捨五入の作成

前回 は Round という関数は四捨五入ではなかったという話をしました。アプリ開発では必要となるシーンがありますので、ないのなら作ってしまいましょう。

ということで、今回は四捨五入する処理の作成を行います。そのサンプルを紹介するだけのつもりだったのですが、想定外の障害に出くわし、思いのほか難航しまた。そこで、その問題点も含めてまとめておきます。


LotusScript の場合

まずは、簡単に解決できる LotusScript での処理です。Round という関数が存在するので RoundOff 関数としました。

引数は Round 関数と同じで、丸めたい値 vdNum と 丸める桁 viPlaces です。

Function RoundOff(Byval vdNum As Double, Byval viPlaces As Integer) As Double
   Dim dDigit As Double
   Dim d As Double

   '丸めたい桁を1の位にする係数を作成
   dDigit = 10 ^ viPlaces

   '丸めたい桁を1の位にする
   d = vdNum * dDigit

   '四捨五入(0.5 を加算して切り捨て)
   d = Int(d + 0.5)

   '桁を戻す
   RoundOff = d / dDigit
End Function

コメントを記述しているので解説は不要かと思います。ポイントは四捨五入の代わりに 0.5 を足して、少数を切り捨てている点です。


式言語で問題発生

続いては@関数の四捨五入を作成してみます。@Round に倣って、丸めたい値 xNumber と 丸める桁 xFactor を用意して演算させます。

式の構成は上記 LotusScript の関数と揃えています。ただ、丸める桁の値の持たせ方が違いますので、乗除記号が逆転している点に注意してください。

   xNumber := 0.25;
   xFactor := 0.1;

   REM {丸めたい桁を1の位にする};
   xTmp := xNumber / xFactor;

   REM {四捨五入(0.5 を加算して切り捨て)};
   xTmp := @Integer(xTmp + 0.5);

   REM {桁を戻す};
   xTmp * xFactor

式ができ上がって検証しているときに事件が発生しました。なぜか、0.350 の場合に切り捨てが発生してしまいました !?

xNumber の値 0.249 0.250 0.251 0.349 0.350 0.351
実行結果 0.2 0.3 0.3 0.3 0.3 0.4

式をいくら見直しても原因がわかりません。そこで、式の途中経過を順に確認しました(数値フィールドの計算結果として確認)。

検証した式 結果(0.250) 結果(0.350)
xNumber / xFactor 2.5 3.5
xNumber / xFactor + 0.5 3 4
@Integer(xNumber / xFactor + 0.5) 3 3

@Integer に原因があることが明白になりました。そこで、Google 先生に聞いてみたところ以下のリンクが見つかりました。

@Integer 関数で小数を扱った場合の動作について

リンク内の事例でも @Integer の結果が 1 少なくなる場合があることが示されています。今回もそれが原因なのでしょう。

回避策を探して Workaround を確認すると、@Round を使えと書いてあります。@Round の四捨五入が使えないから @Integer を頼ったのに...。頭が混乱してきました。


このページの一番最後にこの現象の原因解説のリンクがありました。

浮動小数点演算と丸め誤差について

細かな話は分かりませんが、少数の値は 2 進数で表すと有限桁数で表せない場合があり、それに起因する誤差によるものだそうです。


式言語の四捨五入

少数の値とその誤差が問題原因なのであれば、できる限り少数を使わなければ抑制できるかもしれません。今回の事例では、丸めたい値が少数なのは仕方がないとして、それ以外の少数利用を避けてみようと考えました。

具体的には次の部分、誤差が混入しうる値(要は小数値)を演算に使用している点に着目しました。

   xTmp := xNumber / xFactor;


対策は次の通りです。

丸める桁の値の持たせ方を LotusScript の Round と合わせて、次のように記述しました。これで、元の値以外は整数(もしくは整数部)となります。

   xNumber := 0.350;
   xPlaces := 1;

   REM {丸めたい桁を1の位にする係数を作成};
   xDigit := @Power(10; xPlaces);

   REM {丸めたい桁を1の位にする};
   xTmp := xNumber * xDigit;

   REM {四捨五入(0.5 を加算して切り捨て)};
   xTmp := @Integer(xTmp + 0.5);

   REM {桁を戻す};
   xTmp / xDigit

サポート情報にある Workaround では誤差を @Round でごまかす方法を取っていましたが、誤差が混入しにくくすることで回避できないか挑戦してみるという方法ですね。


結果は 0.250 であっても 0.350 であっても正しく四捨五入できました。念のため、さまざまな値や桁数を変えてテストしましたが、問題はなさそうでした。


まとめ

今回は、四捨五入の処理を@関数と LotusScript で作成してみました。@関数では想定外のトラブルに見舞われ、思いのほか時間がかかってしまいました。ただ、問題の原因がわかれば、症状が出にくいように対策すれば回避できる事例になればと思い、紹介しました。


原因となった『@Integer 関数で小数を扱った場合の動作について』なのですが、この現象がいつから発生しているのかは記載がありません。原因からするとはるか昔から延々と受け継がれているような気がします。そして、対応のステータスは Deferred となっています。要は対応する気がないのだと理解しました。

ノーツは 30 年以上前のプログラムがそのまま動作する互換性が高い製品です。これがノーツをビジネスで利用する上で、重要な利点のひとつだと思っています。

だからと言って、バグまで互換性維持する必要はないですよね...。せっかくの利点が悪しき文化にならないように祈ります。


2024/10/05

四捨五入にご用心!

今回は、@関数と LotusScript の丸め処理、なかでも四捨五入についてまとめます。Notes の世界で四捨五入するには Round という関数を使用すると理解していましたが、結果的には ”四捨五入” ではない部分がありました。順に確認しましょう。


@Round 関数

式言語で丸め処理のを行う関数に @Round があります。

@Round (式言語)

説明には『指定された数値を最も近い整数に四捨五入します。』とあります。そして、構文は以下の通りとなっています。

   @Round( number ; factor )

2 つ目の引数 factor が丸める桁を指定します。0.1 と指定すると少数 1 桁の値に丸めます。


LotusScript の Round

Round 関数は LotusScript にも存在ます。

Round 関数 (LotusScript 言語)

説明には『値を指定した小数で丸めます。』とあり、構文は次の通りです。

   Round ( numExpr , places )

@Round と同様に 2 つ目の引数が丸める桁の指定です。ただし、『目的の小数点以下桁数を表す数式』となっています。少数 1 桁の値に丸めたいときには 1 と指定することになります。


丸め処理の不思議

2 つの関数を紹介しましたが気になることはありませんでしたか?

そうなんです。@Round は ”四捨五入” となっていますが、Round は ”丸め” となっています。これが翻訳の問題なのか、機能が違うのか確認しましょう。

@関数と LotusScript で少数 1 桁の値に丸めるプログラムを作成します。数値 x の値を変えながら動作を観察します。すると次のようが結果が出ました。

x 0.249 0.250 0.251 0.349 0.350 0.351
@Round(x; 0.1) 0.2 0.3 0.3 0.3 0.3 0.4
Round(x, 1) 0.2 0.2 0.3 0.3 0.4 0.4

いかがですか?

どちらも四捨五入ではありませんでした。しかも、ちょうど 5 の時の処理は @関数は”奇数”になるように調整、LotusScript は”偶数”となるように調整していました。

なぜ、このような違いになっているのか不思議ですね...


まとめ

今回の検証で、Round は、@関数、LotusScript とも四捨五入ではないことがわかりました。これら関数を利用する際には特性を理解した上で使用しましょう。正しい結果が得られない可能性がありますので...。

ちなみに、ノーツの四捨五入について調査していると以下のリンクに出くわしました。

LotusScript での四捨五入について

本家のサポート情報で、LotusScript で四捨五入を行う関数は用意されていないので Evaluate で @Round を実行する方法が紹介されています。今回の検証結果によるとうまくいかないはずなのですが...。

注意しましょう!


2024/10/03

@Text と Format

数値や日付データを文字列に変換する方法についてまとめます。特に書式を指定して変換する関数として、@関数では @Text、LotusScript では Format があります。これら関数は、使用頻度も高く便利なのですが、多機能な分、引数の指定が複雑です。ついついヘルプを見てしまう関数ですね。そこでよく使う変換をまとめておきます。


構文の確認

まずはそれぞれの関数の構文を確認します。

@関数の @Text は次の通りです。

   @Text( value ; format-string )

LotusScript の Format 関数は次の通りです。

   Format[$] ( expr [ , fmt ] )

引数の順は同じですね。どちらも 2 つ目の引数で変換する書式を文字列で指定する仕様となっています。今回はこの引数の指定方法についてまとめることになります。


数値の場合

数値を文字列に変換する際に行う処理としては、小数点以下の桁数をそろえる、3 桁ごとにカンマで区切る(カンマ編集)、一定の桁数に満たない場合 0 で埋める(ゼロ埋め)があります。それぞれ下記の通りです。

1234.567 という値の変換についてまとめました。

変換結果 @Text の書式 Format の書式 補足
1235 F0 0 整数のみ
1234.6 F1 0.0 少数 1 桁
1234.57 F2 0.00 少数 2 桁
1,234.6 F,1 #,##0.0 カンマ編集
01235 (なし) 00000 ゼロ埋め

@Text ではゼロ埋めする指定がないようなので、式で記述する必要がありそうです。

表示用の計算結果フィールドの式の例です。SourceNumber フィールドの値をゼロ埋めしています。xNumber に変換する値、xDigit に桁数を代入します。

   xNumber := SourceNumber;
   xDigit := 5;

   REM {エラー処理};
   @If(xNumber = ""; @Return(""); @Success);

   REM {指定した桁数でゼロ埋め};
   @Right(@Repeat("0"; xDigit) + @Text(xNumber; "F0"); xDigit)

なお、この式は桁あふれの場合、注意が必要です。例えば 1234567 の場合、"34567" となります。LotusScript の Format では "1234567" となります。


日付/時刻の場合

日付/時刻データでは、@関数と LotusScript で対比できるほど近しい指定が少ないので別々に記載します。

LotusScript の場合

Format 関数の引数の指定は単純です。年 月 日 時 分 秒 のそれぞれの英単語の頭文字を使って指定します。ただし、月 と 分 は同じ m となるため、分 では n を使用します。

また、1 文字ではゼロ埋めなし、2 文字でゼロ埋めとなります。これもわかりやすいですね。

例えば、2024/1/2 3:04:05 の場合、変換結果と引数の文字列は次のようになります。

変換結果 Format の書式 補足
1/2 m/d 月日(ゼロ埋めなし)
2024/01 yyyy/mm 年月(ゼロ埋め)
2024/01/02 yyyy/mm/dd 年月日(ゼロ埋め)
3:04 h:nn 時分(時はゼロ埋めなし)
3:04:05 h:nn:ss 時分秒(時はゼロ埋めなし)
2024/01/02 03:04:05 yyyy/mm/dd hh:nn:ss 年月日時分秒(ゼロ埋め)


@関数の場合

@Text 関数で日付/時刻値に対して指定できる記号は次のようなものがあります(抜粋)。

書式記号 補足
D0 年月日
D1 月日、現在の年でなければ年
D2 月日
D3 年月
T0 時分秒
T1 時分
S0 日付のみ
S1 時刻のみ
S2 日付と時刻
S3 日付と時刻(今日、昨日に限り文字で表示)

これらを組み合わせて指定すると希望する書式に変換できます。

変換結果 @Text の書式 補足
2024/01 D3S0 年月
2024/01/02 D0S0 年月日
03:04 T1S1 時分
2024/01/02 03:04:05 D0T0S2 年月日時分秒

@Text による変換の場合、必ずゼロ埋めになるようです。


@Text 仕様値の注意点

@Text を使用して日付を変換した場合、そのフォーマットは OS の日付の設定に依存します。例えば、日付(短い形式)を次の通り設定すると日付の区切り文字は ”-” となります。

次のように 月/日/年 の順にするとその順になってしまいます。

OS の設定に応じて結果も変わるところがポイントです。特に、グローバルな会社では注意が必要となりますね。


日付/時刻の変換

私はというとクライアント環境に依存して動作が変わることを避けたいので、@関数を使って日付値を文字列にする場合以下の式を使用しています。SourceDatetime が変換したい日付/時刻値が入ったフィールドです。

   xSrc := SourceDatetime;

   @If(xSrc = ""; @Return(""); @Success);

   REM {桁ごとに変換(不要な桁は削除)};
   xY := @Text(@Year(xSrc));
   xM := @Text(@Month(xSrc));
   xD := @Text(@Day(xSrc));
   xH := @Text(@Hour(xSrc));
   xN := @Right("0" + @Text(@Minute(xSrc)); 2);
   xS := @Right("0" + @Text(@Second(xSrc)); 2);


   REM {日付文字列を生成};
   xDate := @Implode(@Trim(xY:xM:xD); "/");
   xTime := @Implode(@Trim(xH:xN:xS); ":");

   @Implode(@Trim(xDate:xTime); " ");

赤字の部分がポイントです。年月日時分秒の各桁ごとに文字列で変換しています。0 埋めしたい場合は、分 や 秒のように @Right を使用しています。

もし、秒 が不要であれば xS の行を消すだけとなります。時刻が不要であれば、xH, xN, xS の行を消します。この操作だけで、後述の @Trim と @Implode がうまく調整してくれて、希望の文字列が出力されます。


2024/09/29

Notes - Excel 連携:#40)訂正 - 名前アイコン生成

前回まで、3 回にわたって名前アイコン作成について紹介しました。その中で一部プログラムに訂正が発生したのでアップデートさせていただきます。訂正箇所は『#38)名前アイコン生成 ②』の以下の部分です。

--- 以下、訂正箇所

   dPoint = PixcelToPoint(viPixcel-1)

ちなみに端数処理の関係かはわかりませんが 1 ピクセル大きく出力されたので、1 引いています。

--- ここまで


問題点

まず発生する症状です。

作成するアイコンのサイズが奇数の場合、幅が 1 Pixcel 小さくなる症状が発生しました。

試しに 10 Pixcel から順にサイズを大きくしながらアイコンを作成したところ、以下のようになりました。


原因

記載時点でうすうす気づいていた通り、端数処理の問題ですね。内部処理で端数が出ない場合、1 を引くとサイズが小さくなるということなのでしょう。

ただ、高さについては指定通りのサイズになっています。幅と高さで端数処理が違うということになります。Excel は難しいですね。


修正内容

サイズそのままだと、当初経験した通りサイズが大きくなることがあり、今回 1 を引くと引きすぎということがわかりました。そこで、次のように修正しました。

   dPoint = PixcelToPoint(viPixcel) - PixcelToPoint(1) / 2

Pixcel は整数なので、1 ピクセルをインチに変換してから、半分にして引いています。

この結果、指定したサイズで出力されるようになりました。もちろん、大きなサイズであっても正しく出力されています。

 


まとめ

今回は、過去の記事の訂正でした。検証の甘さで正しくない情報を公開してしまいました。記載時点で気になっていたのであれば、その時検証しなかったのか後悔しています。

実際の業務においても、業務の込み具合や費用対効果、時間の都合などで、黙殺することがあります。ただ、トラブルが発生しないことを黙殺するのはいいですが、問題が発生するのはよくありません。このあたりの嗅覚が腕の見せ所ですよね。もっと磨かねばと感じました...。


前回 DXL Step-by-Step


2024/09/27

DXL Step-by-Step:#39)セルの設定 - サンプルコード

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 39 回です。直近 2 回、『#37)セルの設定 - 背景色』と『#38)セルの設定 - 表の罫線』の設定を行うプログラムを作成します。


サンプルの内容

この時点で最新のサンプルコードである『#36)罫線の設定 - 境界線のスタイル』のプログラムをもとに作業します。

実現する機能は、#37 で行った背景のグラデーション設定と #38 の罫線の幅の設定です。


セルの取得

背景色や罫線の幅の設定はセルのノード tablecell の属性設定となります。そこで、表 table ノードからセルを取得する関数を作成します。引数は table ノードと取得する行と列となります。

Function xGetDXL_cell(vdenTbl As NotesDOMElementNode, ByVal viRow As Integer, ByVal viCol As Integer) As NotesDOMElementNode
   'セルのマージは考慮しない
   Dim denRow As NotesDOMElementNode

   '行を取得
   Set denRow = GetChildByName_Nth(vdenTbl, "tablerow", viRow)

   'セルを取得
   If Not(denRow Is Nothing) Then
      Set xGetDXL_cell = GetChildByName_Nth(denRow, "tablecell", viCol)
   End If
End Function

ライブラリ lsDXL 内に事前に作成した GetChildByName_Nth 関数を利用しています。この関数は、指定した名前の子ノードの中から n 番目のノードを返す関数でした。

まず、この関数で指定された行の tablerow ノードを取得しています。そして同様に tablerow ノード配下の tablecell ノードを取得して戻り値としています。

もし、引数の行や列が大きい場合などは Nothing を返します。


なお、この関数はセルのマージを考慮していません。あらかじめご了承ください。


背景色と罫線の幅の設定

上記関数を使用してセルが取得できたら、背景色と罫線の幅の設定を行います。DXL を作成する関数 xSetDXL を変更します。

今回作成した関数を使用して、真ん中のセルのノードを取得。背景色を設定したあと、四方の罫線の幅を 3 ピクセルに設定しています。

Function xSetDXL(vdprs As NotesDOMParser)
         ・・・
   Call xSetDXL_colwidth(denTbl, 3, "30%")

   '境界線のスタイル
   'Call denTbl.SetAttribute("cellborderstyle", "ridge")
   Call denTbl.SetAttribute("cellbordercolor", "green")

   'セルの取得
   Dim denCell As NotesDOMElementNode
   Set denCell = xGetDXL_cell(denTbl, 2, 2)


   '背景色の設定
   Call denCell.SetAttribute("bgcolor", "#e0e0ff")
   Call denCell.SetAttribute("altbgcolor", "#ffffd0")
   Call denCell.SetAttribute("colorstyle", "hgradient")


   '罫線の設定
   Call denCell.SetAttribute("borderwidth", "3px")

   '表後の段落追加
   Call xSetDXL_par(ddn, denRT)
End Function

ただ、これで完成ではありません。この状態で実行すると次のようになります。

#38 で紹介したように隣接するセルの幅も設定する必要があります。今回は、正しく表示されていない右と下のセルについて設定します。

         ・・・
   '罫線の設定
   Call denCell.SetAttribute("borderwidth", "3px")

   '右と下のセルの罫線の調整
   Set denCell = xGetDXL_cell(denTbl, 2, 3)
   Call denCell.SetAttribute("borderwidth", "1px 1px 1px 3px")
   Set denCell = xGetDXL_cell(denTbl, 3, 2)
   Call denCell.SetAttribute("borderwidth", "3px 1px 1px 1px")


   '表後の段落追加
   Call xSetDXL_par(ddn, denRT)
End Function

以上で完成です。


まとめ

今回はセルの操作についてサンプルコードを紹介しました。

罫線の設定においては、隣接するセルの設定を行うことがポイントとなります。今回のサンプルでは最低限必要な、右と下のセルだけ設定しましたが、理想を言えば上や左のセルも設定するほうが正しいと言えます。

設定位置が違うので当たり前なのですが、上下左右で幅を指定する文字列はそれぞれ違います。また、1 行目などセルの位置によっては隣接するセルがない場合があります。

これらすべてを考慮して確実に操作するコードを抜けもれなく確実に記述するのは大変です。その上、コードがだらだら長くなりますので見づらくなります。

実運用のコーディングでは関数化するなど工夫が必要ですね。


前回 DXL Step-by-Step 次回


2024/09/20

DominoHub 2024 Osaka 大盛況 & DXL 拡張ライブラリリリース!

2024 年 9 月 19 日(木)、南森町の『プレミアホテル-CABIN PRESIDENT-大阪』にて、DominoHub 2024 Osaka が開催されました。

6 月の東京開催に続き、大阪は今年 2 回目の開催。主催側の立場として手前みそではありますが、大盛況のうちに幕を閉じつことができました。大阪でオンサイトでの Notes/Domino のイベントは 5 年ぶりだったので、情報を求めている方がたくさんいらっしゃったということですね。

ご来場いただきましたみなさま、ご参加誠にありがとうございました。

最先端の Notes/Domino の技術、他のシステムや AI などの連携、パネルディスカッションなど多方面の発表や議論があり、新しいアイデアを共有する場となったと思っております。


私はというと、この DominoHub 2024 Osaka で 2 年かけて作成してきた DXL 拡張ライブラリ『dxlSuite for LotusScript』のリリース発表をさせていただきました。展示ブースでは、実際に触って、動かしてみる環境をご用意し、多くの方に説明させていただきました。

特に、熱心に質問をしてくださった方もおり、技術的な興味や実際の業務での活用方法に関して具体的なディスカッションができたことが印象的でした。このライブラリが、今後の Notes/Domino のアプリ開発における有効なツールとなればいいなと期待しています。


最後に『dxlSuite for LotusScript』のご案内です。『MISC Market』内で販売させていただいており、ライブラリ本体は『dxlSuite for LotusScript 1.0』に掲載しております。

また、このライブラリを使用したサンプルアプリとして『dxlナンプレ』『dxlTenki - Notes ポータルに天気予報』をあわせて公開しました。これらサンプルに含まれる dxlSuite for LotusScript は出荷前バージョンから必要な機能だけを抽出したサンプル版となっております。どちらも設計を公開ですので、ライブラリがどのようなものなのかご確認いただけます。


2024/09/15

Notes - Excel 連携:#39)名前アイコン生成 ③

名前を表示したアイコン作成の 3 回目です。今回は残りの名前(テキスト)を表示する関数 xAddText についてまとめます。


名前(テキスト)の作成

先に作成した関数を紹介します。細かな調整や新たなオブジェクト(プロパティ)が登場していますので順に解説します。

Function xAddText(voIcon As Variant, ByVal vdPoint As Double, ByVal vsName As String)
   Dim oText As Variant
   Dim dFont As Double
   Dim dMarginTop As Double
   Dim dMarginLR As Double
   Dim dSpace As Double

   '設定材料算出
   dFont = vdPoint / 3   '2行表示を想定し高さの 1/3
   dSpace = 0.6   '行間
   dMarginTop = dFont * (1 - dSpace)   '行間を詰めた分だけ上にずれる
   dMarginLR = vdPoint / 8   '少し詰めないと 3 文字入る

   'ラベル作成(Shape オブジェクト)
   Set oText = voIcon.Chart.Shapes.AddLabel(msoTextOrientationHorizontal, 0, 0, vdPoint, vdPoint)
   With oText.TextFrame2
      'ラベルの設定
      .VerticalAnchor = msoAnchorMiddle   '垂直に中央揃え
      'マージン
      .MarginLeft = dMarginLR
      .MarginRight = dMarginLR
      .MarginTop = dMarginTop
      .MarginBottom = 0
      'テキストの設定
      .TextRange.Font.Size = dFont   'フォントサイズ
      .TextRange.ParagraphFormat.Alignment = msoAlignCenter   '水平に中央揃え
      .TextRange.ParagraphFormat.SpaceWithin = dSpace   '行間
      'テキスト
      .TextRange.Characters.Text = vsName
      .TextRange.Characters.Font.Fill.ForeColor.RGB = RGB(255, 255, 255)
   End With
End Function


フォントサイズ

今回のアイコンでは、最大 2 行の表示を想定しています。アイコンは円でその中に入れるためには高さの 1/3 をフォントサイズとしました。引数で渡されるアイコンのサイズの単位は Point なので 3 で割るだけでフォントサイズとして使用できます。


テキストの配置

表示するテキストは縦横 2 文字ずつを想定しています。標準の設定では行間が広いので行間を詰めます。次の図は、左が標準の設定で、右が今回採用した 0.6 倍 の設定です。

以下は今回採用する設定(右側の設定)


マージンの設定

行間を詰めた場合、文字はその分上にずれます。これは垂直方向で中央ぞろえした場合でも同じです(要は中央揃えにならない)。そこで、行間のを詰めた分、上のマージンで補います。左がマージン 0、右が行間を詰めた 40%(1-0.6)をフォントサイズに合わせてマージンにセットした状態です。

また、左右のマージンが 0 だと 3 文字入ってしまうため、左右にマージンを設定します。

計算上は 1/3 をマージンにすればいいのですが、幅が広い文字がある可能性を想定し 1/4 とします。文字は水平方向で中央揃えとするので、左右同じ幅、1/8 を左右それぞれのマージンとします。


TextFrame2 オブジェクト

AddLabel メソッドでの戻り値は、これまでの AddChart2、AddShape メソッドと同じく、Shape オブジェクトです。実際のラベルのオブジェクトにアクセスするためには TextFrame2 プロパティ を使用します。

TextFrame2 オブジェクトのプロパティにマージンの設定と垂直揃えを設定する VerticalAnchor プロパティ が存在します。

セットできる値は MsoVerticalAnchor 列挙 に定義されています。思いのほか細かな設定がありますが、今回は 3 の msoAnchorMiddle を使用して、垂直方向に中央揃えさせます。


TextRange2 オブジェクト

フォントサイズや水平揃え、表示する文字の設定など上記以外の設定は、TextRange2 オブジェクトから行います。オブジェクトは、TextFrame2 オブジェクトのプロパティから取得します。なぜか TextRange プロパティ から TextRange2 オブジェクト が取得できます。ややこしいですね...

水平揃えは、ParagraphFormat プロパティ 経由で Alignment プロパティでセットするのですが、Microsoft Learn のリンクが途切れています。調べる限り ParagraphFormat プロパティは ParagraphFormat2 オブジェクト のようです。プロパティを確認すると  Alignment プロパティ があり、設定値は MsoParagraphAlignment 列挙 に定義されています。


Characters プロパティ

TextRange2 オブジェクト の Characters プロパティ の値は TextRange2 です。今回のコードだけでは判別しかねるのですが、これはテキスト内の一部分に対して文字色を変えるなど部分文字列に対応していからこのような構造になっていると想定します。

Excel のマクロの記録でベースとなる VBA を取得したのですが、その際に Characters プロパティを使用したコードが出力されました。今回は、その通り LotusScript に変換したのですが、構造を理解していると以下のように記述することも可能です。

         ・・・
      .TextRange.ParagraphFormat.SpaceWithin = dSpace '行間

      'テキスト
      .TextRange.Text = vsName
      .TextRange.Font.Fill.ForeColor.RGB = RGB(255, 255, 255)

      '.TextRange.Characters.Text = vsName
      '.TextRange.Characters.Font.Fill.ForeColor.RGB = RGB(255, 255, 255)

   End With
End Function


ライブラリの更新

ここまでで、プロパティに設定する定数がいくつかありましたので、ライブラリに追加します。lsXls ライブラリを開き、(Declarations) に以下を追加します。

'MsoVerticalAnchor 列挙 (Office)
Public Const msoAnchorMiddle = 3   '垂直方向に中央揃え

'MsoParagraphAlignment 列挙 (Office)
Public Const msoAlignCenter = 2   '中央揃え

なお、MsoParagraphAlignment 列挙 のページでは、中央寄せは 1 の msoAlignCenter と記載されています。そのまま実行すると左寄せになりました。実際には、2 を設定すると中央寄せとなりました。これが、Microsoft Learn の間違いなのかは判別できませんが、上記コードでは 2 で設定します。


まとめ

プログラムが完成したら、エージェントを実行してみましょう。指定したフォルダにアイコンファイルが作成されます。

アイコンのサイズは引数化しているので、お好みのサイズで作成してください。現時点では色合いは関数内で埋め込んでいるため固定ですが、引数に追加すれば好みの色で作成できますね。

今回紹介したプログラムでは、オートシェイプを利用しました。オートシェイプには多種多様な部品があります。今回の技を応用すればアイコンだけでなく、バナーなどさまざまな画像が作成できますね。

最後に、今回登場したオブジェクトの関係を整理しておきます。


前回 Notes - Excel 連携