出直し!! ヘルプ

連載中

連載 終了

2024/12/29

DXL Step-by-Step:#47)添付ファイルの取得 ②

DXL 経由で添付ファイルのダウンロードに関して、前回 は、プログラムの全体像を説明しました。今回はその続きです。添付ファイルの実体をを取得する部分を紹介します。


呼び出し元の確認

まずは、呼び出し元のプログラムを再確認します。

attachmentref ノードが denRef にセットされており、name 属性から文書内で一意となるファイル名を取得しています。このファイル名を引数に、そのファイルの実体を保存している $FILE フィールドを取得する関数が xGetDXL_FileItem でした。

   If Not (denRef Is Nothing) Then
      'ファイル名取得
      Dim sName As String
      Dim sDisp As String
      sName = denRef.GetAttribute("name")
      sDisp = denRef.GetAttribute("displayname")

      '$FILE フィールド取得
      Dim denFile As NotesDOMElementNode
      Set denFile = xGetDXL_FileItem(ddn, sName) 'Notes 内部の名称で検索


添付ファイルの DXL 確認

続いて、添付ファイルの DXL 構造の再確認です。

添付ファイルは $FILE というフィールド内に保存されいました。下図 の赤線のように $FILE も通常のフィールドと同列なので、Title や Body といったフィールド(item ノード)に並んで登場します。

item ノード内に添付ファイルがある場合、item > object > file と階層化さた file ノードの name 属性に、一意となるファイル名がセットされています。

よって、添付ファイルの実体を探す際には、item ノードだけでなく、file ノードの属性を含めて判定する必要があります。

これで作成すべきロジックが見えてきましたね。


ファイルの実体の取得

では、添付ファイルの実体を取得する関数 xGetDXL_FileItem を紹介します。

フィールドを取得する関数 xGetDXL_item をベースに作成しました。通常のフィールドと違うのは、フィールド名が $FILE 固定になっている点です(赤字)。フィールド名が $FILE であれば、file ノードを取得して、ファイル名を判定しています(紫字)。

Function xGetDXL_FileItem(vddn As NotesDOMDocumentNode, ByVal vsName As String) As NotesDOMElementNode
   '文書のノードの取得
   Dim denDoc As NotesDOMElementNode
   Set denDoc = vddn.DocumentElement

   'itemノードをチェック
   Dim denItem As NotesDOMElementNode
   Dim dnl As NotesDOMNodeList
   Dim i As Integer
   Dim sFld As String
   Dim sName As String
   Dim den As NotesDOMElementNode

   Set dnl = denDoc.GetElementsByTagName("item")
   For i = 1 To dnl.NumberOfEntries
      Set denItem = dnl.GetItem(i)
      sFld = denItem.GetAttribute("name")
      If LCase(sFld) = LCase("$FILE") Then
         '添付ファイルなのでチェックを継続
         'object ノード取得
         Set den = xGetDXL_FirstNodeByName(denItem, "object")
         If Not (den Is Nothing) Then
            'file ノード取得
            Set den = xGetDXL_FirstNodeByName(den, "file")
            If Not (den Is Nothing) Then
               '添付ファイルなので Notes 内部のファイル名取得
               sName = den.GetAttribute("name")
               If LCase(sName) = LCase(vsName) Then
                  'ファイル名一致
                  '戻り値セットしてループ脱出

                  Set xGetDXL_FileItem = den 'file ノード
                  Exit For
               End If
            End If
         End If
      End If
   Next
End Function

object や file ノードの取得は ”最初の子ノード” というような単純な取得ではなく、xGetDXL_FirstNodeByName 関数を利用して検索しています。また、ノードが見つからないときは、次のフィールドに進むようになっています。

$FILE フィールドは、添付ファイル以外にも利用されるようなので、それに備えています。


メインプログラム

これで必要な関数がそろいました。エージェントの本体、Initialize の処理を修正します。

作成した xGetDXL_FirstAttachment 関数をコールして Body フィールド内の最初の添付ファイルを取得します。戻り値は、添付ファイル名です。

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

   '処理対象の文書を取得
   Set xns = New NotesSession
   Set ndb = xns.CurrentDatabase
   Set ndc = ndb.UnprocessedDocuments
   Set nd = ndc.GetFirstDocument()

   '画像データの取得
   Dim nst As NotesStream '添付ファイル
   Dim sName As String 'ファイル名

   If Not(nd Is Nothing) Then
      'フィールドの内の最初の添付を取得
      sName = xGetDXL_FirstAttachment(nd, "Body", nst)

      If sName <> "" Then
         '取得できたらファイルとして保存
         Call StreamToFile(nst, "c:\tmp\" & sName)
      End If
   End If
End Sub

添付ファイルが取得できたら(= sName に値がある)、保存処理を行います。nst にファイルの中身が NotesStream オブジェクトとして入っているので、StreamToFile 関数で保存します。

Sub StreamToFile(vnst As NotesStream, ByVal vsFP As String)
   Dim nstOut As NotesStream

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

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

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

この関数、どこかで見たことがありますね。

#9 で作成し、今回もとにしたエージェント #42 でも利用したダウンロードした画像をファイルとして保存する StreamToImageFile 関数です。今回は画像ではないので "Image" とった名前にしています。

関数の機能としては、画像に特化していません。初期段階で命名を失敗したということですね...


前回 DXL Step-by-Step 次回


2024/12/28

DXL Step-by-Step:#46)添付ファイルの取得 ①

#44#45 で文書内の添付ファイルと DXL の関係をまとめました。この情報を使って、DXL 経由で添付ファイルのダウンロードに挑戦します。


エージェントの準備と仕様

DXL において、添付ファイルは Base64 でエンコードされた文字列で表現されます。ファイルとして取得するにはデコードする必要があります。この流れは、インラインイメージから画像を取得した処理と同様です。そこで、今回は『#42)インラインイメージの取得』で作成したエージェントをコピペしてスタート地点とします。

また、処理を単純にするため、リッチテキスト内の最初の添付ファイルをダウンロードするものとします。インラインイメージの取得でも最初の画像をダウンロードしていたので、この点も同じですね。


添付ファイルの取得

まずは、最初の添付ファイルをダウンロードする関数です。

#42 の xGetDXL_FirstInlineImage に相当する関数で、取得した添付ファイルを NotesStream オブジェクトで返します。戻り値はファイル名となっています。

Function xGetDXL_FirstAttachment(vnd As NotesDocument, ByVal vsFld As String, rns As NotesStream) As String
   'DXL の準備
   Dim dprs As NotesDOMParser
   Set dprs = xGetDOMParser(vnd)

   'DOM ツリーのルートを取得
   Dim ddn As NotesDOMDocumentNode
   Set ddn = dprs.Document

   'リッチテキストの取得
   Dim denRT As NotesDOMElementNode
   Set denRT = xGetDXL_item(ddn, vsFld)

   '添付情報の取得
   Dim denRef As NotesDOMElementNode
   Set denRef = xGetDXL_FirstNodeByName(denRT, "attachmentref")

   If Not (denRef Is Nothing) Then
      'ファイル名取得
      Dim sName As String
      Dim sDisp As String
      sName = denRef.GetAttribute("name")
      sDisp = denRef.GetAttribute("displayname")

      '$FILE フィールド取得
      Dim denFile As NotesDOMElementNode
      Set denFile = xGetDXL_FileItem(ddn, sName) 'Notes 内部の名称で検索

      If Not (denFile Is Nothing) Then
         'ファイルの中身を取得
         Dim denData As NotesDOMElementNode
         Set denData = xGetDXL_FirstNodeByName(denFile, "filedata")

         If Not (denData Is Nothing) Then
            Dim dtn As NotesDOMTextNode
            Dim nst As NotesStream
            Dim sB64 As String

            'エンコードされたファイルを取得
            Set dtn = denData.FirstChild
            sB64 = dtn.NodeValue

            'デコードしてストリームとして取得
            Set nst = xns.CreateStream()
            Call Base64ToBinary(sB64, nst)

            '戻り値セット
            Set rns = nst
            xGetDXL_FirstAttachment = sDisp
         End If
      End If
   End If
End Function

少し長いコードなので、ポイントを順に解説します。


添付ファイルの取得(リッチテキスト内)

リッチテキスト内に存在する最初の添付ファイルを取得する部分です。以前作成した関数 xGetDXL_FirstNodeByName を使用して、リッチテキスト配下の最初の attachmentref ノードを取得します。

   '添付情報の取得
   Dim denRef As NotesDOMElementNode
   Set denRef = xGetDXL_FirstNodeByName(denRT, "attachmentref")


添付ファイル名の取得

attachmentref ノードが取得できたら、そのノードの属性から添付ファイル名を取得します。name 属性は文書内で一意のファイル名、displayname 属性が添付時のファイル名となります。

   If Not (denRef Is Nothing) Then
      'ファイル名取得
      Dim sName As String
      Dim sDisp As String
      sName = denRef.GetAttribute("name")
      sDisp = denRef.GetAttribute("displayname")

      '$FILE フィールド取得
      Dim denFile As NotesDOMElementNode
      Set denFile = xGetDXL_FileItem(ddn, sName) 'Notes 内部の名称で検索

xGetDXL_FileItem 関数は文書内の添付ファイル($FILE フィールド)を探し出し、ファイルの実体(file ノード)取得する関数です(詳細は後述)。文書内で一意な名前 name 属性の値を引数に検索しています。


ファイルの実体の取得

確認する限り file ノード配下には filedata しか存在しないようなのですが、念のため xGetDXL_FirstNodeByName 関数を使用して検索しています。

      If Not (denFile Is Nothing) Then
         'ファイルの中身を取得
         Dim denData As NotesDOMElementNode
         Set denData = xGetDXL_FirstNodeByName(denFile, "filedata")

filedata ノードの配下はテキストノードで Base64 でエンコードされたファイルの実体でした。これをデコードして NotesStream のオブジェクトに変換しています。

この部分はインラインイメージの処理と全く同じですね。

         If Not (denData Is Nothing) Then
            Dim dtn As NotesDOMTextNode
            Dim nst As NotesStream
            Dim sB64 As String

            'エンコードされたファイルを取得
            Set dtn = denData.FirstChild
            sB64 = dtn.NodeValue

            'デコードしてストリームとして取得
            Set nst = xns.CreateStream()
            Call Base64ToBinary(sB64, nst)

            '戻り値セット
            Set rns = nst
            xGetDXL_FirstAttachment = sDisp
         End If

最後にこの関数の戻り値として、添付時のファイル名である sDisp (displayname 属性)をセットしています。


次回の予告

今回は添付ファイルをダウンロードする処理の全体像を解説しました。次回は、後述するとしていた $FILE フィールドを取得する関数 xGetDXL_FileItem を中心に説明します。


前回 DXL Step-by-Step 次回


2024/12/27

DXL Step-by-Step:#45)添付ファイルの構造 ②

前回に続いて添付ファイルの DXL についてまとめます。

ノーツのリッチテキストには複数の添付ファイルが保存できます。しかも、同名のファイルを複数貼り付けることも可能です。このような文書が DXL でどのように表現されるのか確認しましょう。


DXL の全体像

文書全体の DXL の構造について確認します。段落を表す par ノード配下に 3 つの attachmentref ノードが存在します。これで 3 つの添付ファイルのアイコンを表しています。

添付したファイルは、同じファイル名だったのですが、name 属性には 002 など、一意になるよう名称が調整されています。

ファイルの実体を表す $FILE フィールドも 3 つ存在し、name 属性が対になるように設定されています。この機能により、アイコンとファイルの実体を関連付けています。

今回は、各ファイルの中身は同じとしたので filedata タグ内も全く同じで、データ構造的には冗長に見えます。ですが、この構造であればファイル名が同じで、中身が違う場合の対応も可能ですね。


ファイル名

3 つ目の添付ファイルに関する DXL のノードを抽出すると次の通りです。

<attachmentref name='Sample.003.txt' displayname='Sample.txt'>

<file name='Sample.003.txt' size='58' flags='storedindoc' encoding='none' hosttype='msdos' compression='none'>

添付ファイルのオリジナルファイル名は attachmentref ノードの displayname から取得するのがよさそうです。また、ファイルの実体側にはファイル名がないことも覚えておきましょう(間接的には作れそうですが...)。


まとめ

前回から 2 回にわたって添付ファイルの構造についてまとめました。次回はこの知識を使って、添付ファイルのダウンロードにチャレンジしたいと思います。


前回 DXL Step-by-Step 次回


2024/12/26

DXL Step-by-Step:#44)添付ファイルの構造 ①

今回は文書に添付したファイルについて整理します。調査で使用するのは、リッチテキストにテキストファイル(Sample.txt)だけが添付されている単純な文書を使用します。


リッチテキストフィールドと添付ファイル

この文書を DXL に変換して、リッチテキストフィールドの中を確認すると、次のようになっていました。

最初に気がつくのは、attachmentref ノードです。name 属性に添付ファイル名がセットされているので、添付ファイルであることは間違いないですね。

attachmentref ノードの配下には picture ノードがあります。添付したファイルはテキストファイルなので変ですね。ただ、ノード全体を見ると気がつくことがあります。

赤枠で囲った部分は、前回紹介した画像を表す DXL となっていますね。

画像に添えられたテキストである caption ノードにファイル名が指定されていて、その上に画像データらしきものが存在します。逆にこれ以外のノードは存在しませんので、attachmentref ノード内には添付ファイルのアイコンの情報しかないということになります。


添付ファイルの実体

DXL 全体を見ると添付ファイルの実体を発見できます。リッチテキストフィールドを表す item ノードを省略表示するとその下にもう一つ item ノードが存在します。

フィールド名は "$FILE"。どこかで見たことがありますね。

これは添付ファイルのある文書をプロパティで参照した際に表示される特殊なフィールドです。文書のプロパティと同じ構造なので、これはこれでわかりやすいですね。

添付ファイルの実体は、filedata ノード配下にあります。画像データと同様で、ファイルの実体を Base64 でエンコードした文字列として出力されます。この文字列をデコード、バイナリデータとしてファイル保存すれば、添付ファイルのダウンロードができます。


まとめ

今回は添付ファイルの基本的な構造をまとめました。

リッチテキスト内には添付ファイルの実体ではなく、アイコン情報が入っています。添付ファイルの ”参照” となるので、ノード名は attachmentref ということなんですね。

また、添付ファイルのアイコンはインラインイメージで構成されていました。ファイル名は画像のプロパティの見出しを活用して表現されています。リッチテキスト内の要素をうまく部品化して再利用しているのが面白いですね。


前回 DXL Step-by-Step 次回


2024/12/25

Notes - Excel 連携:#52)画像のリサイズ

Excel を使ってノーツの ”ニガテ” をカバーしようというこの連載。今回のネタは画像の操作です。LotusScript では、ファイル操作はできても画像ファイルを画像データとして取り扱うことはできません。Java を使えば実現できるようなのですが、ちょっと敷居が高いですよね。

そこで、Excel の出番です。LotusScript + Excel VBA で画像処理にチャレンジしましょう。今回ターゲットにするのは、画像のリサイズとフォーマット変換です。


基本的な仕組み

Excel 上に配置した画像はオブジェクトのサイズを変えるとその大きさにあわせて伸縮してくれます。今回はこの機能をリサイズに活用します。

画像ファイルを作成する方法は 『#36)帳票を画像で保存』で紹介しました。Chart オブジェクトの Export というメソッドをコールするだけででしたね。GIF や PNG など画像形式は保存するファイル名の拡張子で自動的に判別されます。

Chart.Export メソッド (Excel)

この 2 つの機能を組み合わせて、画像ファイルのリサイズとフォーマット変換を行います。大まかな手順は次の通りです。

  1. ワークシートに画像と同じサイズの Chart オブジェクトを作成
  2. Chart 上に画像ファイルを読み込む(Chart オブジェクトにピッタリ収まる)
  3. Chart オブジェクトを希望のサイズに変更(画像がリサイズされる)
  4. Export メソッドで画像ファイルとして出力(拡張子次第でフォーマットを変換)


サンプルプログラム

まず、新規でエージェントを作成し、この連載で作成してきたライブラリ lsXls を組み込みます。とはいっても、使用している関数は単位を変換する PixcelToPoint 関数だけです(『#35)Excel で使用する単位と変換』で紹介)。

エージェントのプログラムの全体は次の通りです。

Option Declare
Use "lsXls"

Sub Initialize
   Dim oXls As Variant
   Dim oSheet As Variant
   Dim oShape As Variant
   Dim oImage As Variant
   Dim sFN_In As String

   '入力画像ファイル
   sFN_In = "C:\tmp\Image_Input.png"

   'Excel の準備
   Set oXls = CreateObject("Excel.Application")
   Call oXls.Workbooks.Add
   Set oSheet = oXls.Workbooks(1).WorkSheets(1)

   '画像サイズ取得
   Dim dX As Double '画像の幅(ポイント)
   Dim dY As Double '画像の高さ(ポイント)
   Set oImage = oSheet.Shapes.AddPicture(sFN_In, msoFalse, msoTrue, 0, 0, -1, -1)
   dX = oImage.Width
   dY = oImage.Height
   MsgBox PointToPixcel(dX)

   'Chart オブジェクトを画像の大きさで作成
   Set oShape = xAddChart(oSheet, dX, dY)

   'Chart 上に画像ファイルを読み込み
   Set oImage = oShape.Chart.Shapes.AddPicture(sFN_In, msoFalse, msoTrue, 0, 0, -1, -1)

   '出力サイズ
   Dim iX_Out As Integer '画像の幅(ピクセル)
   Dim iY_Out As Integer '画像の高さ(ピクセル)
   iX_Out = 334
   iY_Out = 100

   'リサイズ
   dX = dX * (PixcelToPoint(iX_Out)/dX)
   dY = dY * (PixcelToPoint(iY_Out)/dY)
   oShape.Width = dX
   oShape.Height = dY

   '画像として保存
   Call xSaveAsPicture(oShape, "C:\tmp\Image_Output.gif")

   oXls.Visible = True
End Sub

Function xAddChart(voSheet As Variant, ByVal vdWidth As Double, ByVal vdHeight As Double) As Variant
   Dim oShape As Variant

   'Chart を作成(Shape オブジェクト)
   Set oShape = voSheet.Shapes.AddChart2( , , , , vdWidth, vdHeight)
   With oShape
      '背景は透明
      .Fill.Visible = False
      .Line.Visible = False

      'グラフのオブジェクトを削除
      On Error Resume Next
      .Chart.ChartTitle.Delete
      .Chart.Legend.Delete
      .Chart.Axes(xlCategory).Delete
      .Chart.Axes(xlValue).Delete
      .Chart.Axes(xlValue).MajorGridlines.Delete
   End With

   Set xAddChart = oShape
End Function

Function xSaveAsPicture(voIcon As Variant, ByVal vsFileName As String)
   voIcon.Chart.Export vsFileName
End Function

使い方は、

  1. リサイズしたい画像ファイルを sFN_In 変数に設定
  2. 出力サイズを iX_Out、iY_Out 変数にピクセルで指定
  3. xSaveAsPicture 関数の 2 つ目の引数で出力ファイル名を指定

で、実行すると指定した画像ファイルが作成されます。なお、今回はサンプルアプリですので、処理結果がわかるよう、Excel シートを画面に表示して終了しています。


ここから先は、処理のポイントとなる部分をかいつまんで解説します。


画像サイズの取得

リサイズに利用する Chart オブジェクトは、画像サイズにあわせて作成します。そのため、事前に画像サイズを取得する必要があります。

今回は、AddPicture メソッド でいったん画像を呼び出します。この時、最後の 2 つの引数に -1 をセットすると元の画像のサイズを維持して作成されます。できあがったオブジェクト(Shape)のサイズを取得すれば、画像のサイズが取得できるという算段です。

   '画像サイズ取得
   Dim dX As Double '画像の幅(ポイント)
   Dim dY As Double '画像の高さ(ポイント)
   Set oImage = oSheet.Shapes.AddPicture(sFN_In, msoFalse, msoTrue, 0, 0, -1, -1)
   dX = oImage.Width
   dY = oImage.Height


元の画像の準備

画像のサイズが判明したらそのサイズで Chart オブジェクトを作成します。画像の読み込みは、Chart オブジェクトの Shapes に対して AddPicture しているのがポイントですね。

   'Chart オブジェクトを画像の大きさで作成
   Set oShape = xAddChart(oSheet, dX, dY)

   'Chart 上に画像ファイルを読み込み
   Set oImage = oShape.Chart.Shapes.AddPicture(sFN_In, msoFalse, msoTrue, 0, 0, -1, -1)

Chart オブジェクトの作成は、サブ関数 xAddChart で実施しています。この関数では、オブジェクトを作成するだけでなく、今回は無用であるグラフオブジェクトを削除するなどの調整を行っています。


画像のリサイズ

以下が画像をリサイズしている部分です。わかりやすくなるよう画像サイズはピクセル(整数)で指定できるようにしています。ただ、元の画像サイズはポイントですから、PixcelToPoint 関数で変換し、単位をあわせた後、リサイズの比率を計算しています。

   '出力サイズ
   Dim iX_Out As Integer '画像の幅(ピクセル)
   Dim iY_Out As Integer '画像の高さ(ピクセル)
   iX_Out = 334
   iY_Out = 100

   'リサイズ
   dX = dX * (PixcelToPoint(iX_Out)/dX)
   dY = dY * (PixcelToPoint(iY_Out)/dY)
   oShape.Width = dX
   oShape.Height = dY

リサイズ後のサイズ dX, dY が決定したら Shape オブジェクトにセットしています。これで画像はサイズに合わせてリサイズされます。


画像の保存

リサイズした画像をファイルとして出力する関数は xSaveAsPicture 関数が担当しています(『#37)名前アイコン生成 ①』で紹介した関数をそのまま再利用)。

1 行しかない単純な関数で、Chart オブジェクトの Export メソッドをコールしているだけです。このメソッドは拡張子で画像の形式を指定できます。.gif を指定すると GIF、.png で PNG 形式となります。


前回 Notes - Excel 連携


2024/12/24

DXL Step-by-Step:#43)イメージのプロパティ

今回は画像のプロパティと DXL の関係についてまとめます。


イメージのサイズ

画像のプロパティでスケールを指定すると表示するサイズが決定できます。今回は幅を 200%、高さを 125% と少し横長に設定してみました。

この画像を DXL で出力すると次の通りとなります。

プロパティボックスでは、% で指定するのですが、DXL ではインチで表現されています。画像のサイズ(width、height)はピクセル値なので、ピクセルとインチの変換が必要となります。この 2 つの単位の関係は次の通りです。

1 インチ = 96 ピクセル

今回の画像の幅は、134 ピクセルの 200%、268 ピクセル ÷ 96 = 2.791666... インチとなり、DXL では scaledwidth='2.7917in' と表現されています。


見出し

画像のプロパティの見出しに文字を入力すると画像にテキストを添えることができます。

DXL を確認すると画像データを表すノードの次に caption というノードが追加されています。指定した文字列は caption ノード配下にテキストノードとして存在しています。

caption ノードには position という属性が存在し、テキストの表示位置を表します。

属性 設定値 補足
position 'below' イメージの下
'center' イメージ上の中央

なお、ノーツクライアントでは、画像をフォーカスした状態で文字のプロパティを変更すると見出しのフォントが指定できます。このフォント情報は DXL 未対応で、出力されません。ご注意ください。


折り返し

次は折り返し設定です。画像周辺の文字の配置設定ですね。

DXL では picture ノードの align 属性に出力されます。取りうる値は次の通りです。

属性 設定値 補足
align (なし) ラップなし、基線(デフォルト)
'left' ラップ、図形の左
'right' ラップ、図形の右
'around' ラップ、図形の周囲
'top' ラップなし、上
'middle' ラップなし、中央
'bottom' ラップなし、基線


代替テキスト

最後は代替テキストの設定です。この設定は picture ノードの alttext というプロパティに設定されます。



まとめ

今回は画像のプロパティと DXL についてまとめました。

事例ではインラインイメージを例に紹介しましたが、この設定は、イメージリソースを表示する画像でも同様の設定となります。

前回 DXL Step-by-Step 次回


2024/12/16

TypeName 関数

先日、LotusScript でプログラムを組んでいてヘルプを参照した関数 TypeName を紹介します。せっかく調べたので忘れる前にまとめておきます。

 

TypeName の機能

この関数は引数に指定した変数が持つ値の型を取得することができます。例えば、次のプログラムでは、文字列変数 s の値は "INTEGER" となります。すべて大文字で返される点に注意が必要です。

   Dim i As Integer
   Dim s As String

   s = TypeName(i)

単一の値を格納するスカラー型(Boolean、Byte、Integer、Long、String など)では、変数宣言時の型をそのまま返すだけです。


配列とリストの場合

スカラー型で配列やリストの場合は次のような結果になります。配列では後ろに "( )" が付加されます(括弧の中は半角スペースが 1 つ)。リストの場合は、後ろに "LIST" が付加されます。

変数宣言 TypeName(n) の戻り値
Dim n(5) As Long LONG( )
Dim n(5) As String STRING( )
Dim n List As Integer INTEGER LIST


クラスの場合

TypeName 関数は、Notes クラスのオブジェクトにも対応しています。動作はこれまでと同様で、クラス名大文字で出力されます。

変数宣言 TypeName(n) の戻り値
Dim n As NotesSession NOTESSESSION
Dim n As NotesDatabase NOTESDATABASE
Dim n(5) As NotesDocument NOTESDOCUMENT( )


この動作はユーザ定義クラスでも同様です。例えば以下のようなエージェントを実行した場合、”TESTCLASS” と出力されます。

Option Declare

Class TestClass
         ・・・(省略)・・・
End Class

Sub Initialize
   Dim n As TestClass

   MsgBox TypeName(n)
End Sub


Variant 型変数と TypeName

LotusScript には Variant という型があり、変数内にさまざまな値を代入できます。TypeName は Variant 変数内にどのような値が入っているか確認することができます。

次のプログラムは変数 v にさまざまな値をセットして、その型を出力しています。どれも VARIANT ではなく、セットされている値の型を出力します。

   Dim ns As New NotesSession
   Dim v As Variant

   v = "TestText"
   Print TypeName(v) ' STRING と出力

   v = 123
   Print TypeName(v) ' INTEGER と出力

   Set v = ns
   Print TypeName(v) ' NOTESSESSION と出力


TypeName の活用

私は、汎用的な関数を作成するときに TypeName をよく使用します。

例えばリッチテキストに引数で指定したコンテンツを出力する関数を考えます。出力する値は、文字列だけでなく、数値や日付値などが考えられます。また、DB リンクを追加するために NotesDatabase オブジェクトを指定する可能性もあります。

この要件を満たすため、引数は Variant 型となります。関数内では値に応じた処理が必要になり TypeName が活躍します。

サンプルの関数は次の通りです(構造を表す部分のみ抜粋、配列の考慮も省略)。出力する値 vvContent の型を判定し、適切な処理を行うイメージです。

Sub AppendContent(vnrti As NotesRichTextItem, vvContent As Variant)
   Dim s As String
   s = TypeName(vvContent)

   If s = "NOTESDATABASE" Then
      ' DB リンクを追加
   ElseIf s = "NOTESVIEW" Then
      ' View リンクを追加
   ElseIf s = "NOTESDOCUMENT" Then
      ' 文書リンクを追加
   Else
      If IsObject(vvContent) Then
         'その他のオブジェクト(名称を出力)
         s = "(" & s & ")"
      Else
         'リテラル値なので文字列に変換
         s = CStr(vvContent)
      End If
     
      's をリッチテキストに追加

   End If


Excel オブジェクト名は未対応

残念ながら OLE オブジェクトの場合 "OBJECT" という値を返します。例えば、次のように Excel と連携するプログラムがあったとします。oXls と oSheet は VBA 的には別のクラスのオブジェクトになりますが、TypeName の戻り値はどちらも "OBJECT" となります。

Excel のクラス名が取得できればすごく便利になるんですけどね...。

   Dim oXls As Variant
   Dim oSheet As Variant

   'Excel の準備
   Set oXls = CreateObject("Excel.Application")
   Call oXls.Workbooks.Add
   Set oSheet = oXls.Workbooks(1).WorkSheets(1)


2024/12/10

DXL Step-by-Step:#42)インラインイメージの取得

前回はインラインイメージ(リッチテキストに直接貼り付ける画像)を貼り付ける方法を紹介しました。今回はインラインイメージを取得する方法についてまとめます。


DXL の構造

まずはインラインイメージの DXL の確認です。

前回説明したように picture ノードがリッチテキストに貼り付けられた画像を表します。その配下に gif ノードなど画像データが直接存在すればインラインイメージです(画像がイメージリソースを参照している場合は imageref ノード)。

DXL の読み込みにおいては、注意点があります。

上記 DXL で picture ノードの最初の子ノードは border となっています。これは、イメージリソースに設定された枠の情報です。

border ノードは画像データより前に配置され、標準の設定(枠なし)の場合は出力されません。画像データを取得するには ”最初の子ノード” ではなく、子ノードの中から画像データを探す必要があるということになります。


エージェントの作成

#14 以降、文書の DXL について解説してきました。ただ、これまでずっと文書やリッチテキスト内のオブジェクトを新規作成する方法が中心でした。今回は既存文書を DXL でアクセスする方法となります。

そこで、今回はプログラム全体を確認しながら進めます。まずは、選択した文書に対して実行するエージェントを作成します。エージェントのメインプログラムは次の通りです。

Option Declare
Private xns As NotesSession

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

   '処理対象の文書を取得
   Set xns = New NotesSession
   Set ndb = xns.CurrentDatabase
   Set ndc = ndb.UnprocessedDocuments
   Set nd = ndc.GetFirstDocument()

   '画像データの取得
   Dim nst As NotesStream '画像データ
   Dim sTag As String '画像の拡張子

   If Not(nd Is Nothing) Then
      sTag = xGetDXL_FirstInlineImage(nd, "Body", nst)

      If sTag <> "" Then
         '取得できたらファイルとして保存
         Call StreamToImageFile(nst, "c:\tmp\test." & sTag)
      End If
   End If
End Sub

StreamToImageFile 関数は #9 でイメージリソースから画像をダウンロードした際に作成した関数をそのまま再利用しています。

xGetDXL_FirstInlineImage が画像データを取得する関数で、今回の主題のプログラムとなります。


画像データの取得

関数の全体は次の通りです。

引数は、画像が含まれる文書 vnd、インラインイメージが存在するフィールド名 vsFld、取得した画像を NotesStream オブジェクトで返す rnst です。戻り値は画像データがあったノード名で、png / gif / jpeg となり、メインプログラムでは保存するファイルの拡張子として使用しています。もし、インラインイメージが見つからない場合には null を返します。

Function xGetDXL_FirstInlineImage(vnd As NotesDocument, ByVal vsFld As String, rnst As NotesStream) As String
   'DXL の準備
   Dim dprs As NotesDOMParser
   Set dprs = xGetDOMParser(vnd)

   'DOM ツリーのルートを取得
   Dim ddn As NotesDOMDocumentNode
   Set ddn = dprs.Document

   'リッチテキストの取得
   Dim denRT As NotesDOMElementNode
   Set denRT = xGetDXL_item(ddn, vsFld)

   '画像の取得
   Dim denPct As NotesDOMElementNode
   Set denPct = xGetDXL_FirstNodeByName(denRT, "picture")

   '画像ファイル取得
   Dim denImg As NotesDOMElementNode
   Dim dnl As NotesDOMNodeList
   Dim dtn As NotesDOMTextNode
   Dim nst As NotesStream
   Dim sB64 As String

   '調査対象ノード
   Dim asTag(2) As String
   asTag(0) = "png"
   asTag(1) = "gif"
   asTag(2) = "jpeg"

   '画像データの捜索
   If Not(denPct Is Nothing) Then
      ForAll sTag In asTag
         Set denImg = xGetDXL_FirstNodeByName(denPct, sTag)
         If Not(denImg Is Nothing) Then
            'エンコードされた画像データ取得
            Set dtn = denImg.FirstChild
            sB64 = dtn.NodeValue

            'デコードしてストリームとして取得
            Set nst = xns.CreateStream()
            Call Base64ToBinary(sB64, nst)

            '戻り値セット
            Set rnst = nst
            xGetDXL_FirstInlineImage = sTag
            Exit ForAll
         End If
      End ForAll
   End If
End Function

DXL の準備では文書を DXL に変換しています。コールしている xGetDOMParser 関数は #3 で紹介したものをそのまま流用しています。

xGetDXL_item 関数でフィールドを表す item ノードを取得、xGetDXL_FirstNodeByName 関数で、item ノードの中から最初に出現する画像のノード picture を取得しています。それぞれの関数は後述します。

picture ノード配下から画像データ(png / gif / jpeg のどれか)を捜索します。見つかったら、画像データであるテキストノードを取得、Base64 でエンコードされているので、デコードして NotesStream オブジェクトに変換します。デコードには #9 で紹介した Base64ToBinary 関数を利用しています。

画像が取得できたら戻り値にセットして、関数を終了しています。


フィールドを取得する関数

引数で指定したフィールドを取得する関数 xGetDXL_item は次の通りです。すべての item ノードを取得して、name 属性の値で判定しています。

Option Declare
Private xns As NotesSession

Function xGetDXL_item(vddn As NotesDOMDocumentNode, ByVal vsFld As String) As NotesDOMElementNode
   '文書のノードの取得
   Dim denDoc As NotesDOMElementNode
   Set denDoc = vddn.DocumentElement

   'itemノードをチェック
   Dim den As NotesDOMElementNode
   Dim dnl As NotesDOMNodeList
   Dim i As Integer
   Dim sFld As String

   Set dnl = denDoc.GetElementsByTagName("item")
   For i = 1 To dnl.NumberOfEntries
      Set den = dnl.GetItem(i)

      'フィールド名をチェック
      sFld = den.GetAttribute("name")
      If LCase(sFld) = LCase(vsFld) Then
         '戻り値セットしてループ脱出
         Set xGetDXL_item = den
         Exit For
      End If
   Next
End Function


配下のノードを取得

最初に紹介した picture ノード配下の画像データを取得しているのが xGetDXL_FirstNodeByName 関数です。この関数は GetElementsByTagName で配下のノードを検索し、最初のノードを取得しています。

フィールドの取得とほぼ同じ構造ですが、name 属性の判定がない分シンプルです。

Function xGetDXL_FirstNodeByName(vdenParent As NotesDOMElementNode, ByVal vsName As String) As NotesDOMElementNode
   Dim dnl As NotesDOMNodeList

   On Error Resume Next

   '配下のノードを検索
   Set dnl = vdenParent.GetElementsByTagName(vsName)
   If dnl.NumberOfEntries > 0 Then
      '最初のノードを戻り値にセット
      Set xGetDXL_FirstNodeByName = dnl.GetItem(1)
   End If
End Function


まとめ

今回は、既存文書内に貼り付けられたインラインイメージを画像ファイルとして取得する方法についてまとめました。処理を簡素化するため、リッチテキスト内の最初の picture ノードがインラインイメージであることを前提としています。

リッチテキストにおいて、picture ノードはイメージリソースを貼り付けた時、添付ファイルのアイコン画像などさまざまな用途で出現します。GetElementsByTagName メソッドで単純に配下すべてを検索すると、想定外の picture ノードを取得することがあります。実際のリッチテキストを処理する場合には、注意が必要です。


前回 DXL Step-by-Step 次回


2024/12/07

つないでみよう:#番外)GPT4o Structured Outputs 活用事例 と dxlSuite

前回 #24)GPT4o Structured Outputs - 活用事例 ② で紹介したサンプルアプリ『AI が導く、次世代の学習体験 - AI Learning』で、AI が作成したコンテンツをどのようにノーツの文書に展開したかを紹介します。今回の内容は WebAPI の結果活用であり、"つながる" 部分ではないので "番外" としています。


フォームの構造

まずは、コンテンツフォームの設計です。

上部のグレーの部分は管理用の隠しフィールドで、ユーザ向けのフィールドはタイトル (Title) と本文 (Body) だけとなっています。

実際の文書は次のようになっています(画像は管理フィールドを表示して取得)。AI のレスポンスをリッチテキストに体裁よく表示させています。


本文の構造

通常の文字はフォントや文字サイズ、行間などを設定しつつ見栄えよく表示しています。キーワードとなるリンクは、ホットスポットのアクションで構成されています。

アクションは LotusScript で記述されており、クリックしたキーワードとそのキーワードが含まれる文章を引数にした関数が指定されています。

Sub Click(Source As Button)
   Call ClickKeyword("ピザ生地", "ピザの作り方は、まず材料を揃えることから始まります。基本の材料にはピザ生地、トマトソース、チーズ、そしてお好みのトッピングが挙げられます。")
End Sub

コールしている関数は、スクリプトライブラリに定義されています。

この関数は、引数を利用して過去のクリック履歴を確認、初回であれば AI に問い合わせ、履歴があればその時の結果の文書を開く機能になっています。

問い合わせする際には、クリックしたキーワードだけでなく、キーワードを含む文章を含めて送信しています。AI に送信しているリクエストだけを抜粋すると次のようになります。

『ピザ屋の運営』について学習しています。
学習のゴールは次の通りです。
ピザの作り方、飲食店でのコスト構造、経営上の課題を理解する
その中で『ピザの作り方は、まず材料を揃えることから始まります。基本の材料にはピザ生地、トマトソース、チーズ、そしてお好みのトッピングが挙げられます。』という文書があり、『ピザ生地』についてさらに詳しく教えてください。

これにより、AI が的外れな回答をしないようにしています。また、同じキーワードであっても段落が違えば別のコンテンツが生成されるようになります。


コンテンツのリッチテキスト化

このリクエストの返信は Structured Outputs の機能で、次のような JSON となります。

このレスポンスをリッチテキストに体裁よく表示したり、リンクを生成しているのが『DominoHub 2024 Osaka 大盛況 & DXL 拡張ライブラリリリース!』で紹介した dxlSuite for LotusScript です。

リンクを作成する部分を事例に dxlSuite の機能を紹介します。

リンクは、先ほど紹介した LotusScript のコードがセットされていること以外に、ホットスポットには境界線の指定がなく太字となっています。また、リンクが目立つように左右にスペースが入っています。

このリンクを作成する関数だけを抜粋すると次の通りです。

Function xDrawKeyword(voPar As DXL_Paragraph, ByVal vsKeyword As String, ByVal vsText As String)
   Dim oHS As DXL_HotSpot_Action

   'キーワードが段落の先頭でなければ、スペースを追加
   If voPar.GetText() <> "" Then
      Call voPar.AppendText_F(" ", xoFont_Body)
   End If

   'ホットスポットの作成(式は後で入れ替えるのでダミー)
   Set oHS = voPar.AppendHotSpot_Action(vsKeyword, "dummy")

   'ホットスポットのデザイン調整
   Call oHS.SetFont(xoFont_Keyword)
   oHS.ShowBorder = False

   'LotusScript のプログラムを準備
   Dim oCode As New DXL_Code(DXL.Code_LotusScript)
   Dim oLS As DXL_LotusScript

   Set oLS = New DXL_LotusScript()
   Call oLS.AppendScript(|Sub Click(Source As Button)|)
   Call oLS.AppendScript(|Call ClickKeyword("| & vsKeyword & |", | & xConvStr_LSString(vsText) & |)|)
   Call oLS.AppendScript(|End Sub|)
   Call oCode.ReplaceEvent("click", oLS)

   'ホットスポットのプログラムを準備した LotusScript に差し替え
   Call oHS.ReplaceCode(oCode)

   'キーワードの後ろにスペースを追加
   Call voPar.AppendText_F(" ", xoFont_Body)
End Function

引数の voPar が dxlSuite のオブジェクト DXL_Paragraph でリッチテキストの段落を表します。このオブジェクトにリンク手前の文章までがセットされた状態でこの関数に渡されるということになります。残りの引数は作成するリンクの情報です。

voPar 現在処理中のリッチテキストの段落を表すオブジェクト
vsKeyword 作成するリンクのテキスト(赤字
vsText リンクが含まれる文章(青字


リンクの追加

dxlSuite でリンクを作成する部分を順に解説します。

まず、AppendHotSpot_Action メソッドでリンクを作成します。戻り値はリンクであるホットスポットのアクションを表す DXL_HotSpot_Action のオブジェクトです。

   'ホットスポットの作成(式は後で入れ替えるのでダミー)
   Set oHS = voPar.AppendHotSpot_Action(vsKeyword, "dummy")

このメソッドは式言語のアクションを作成する機能がデフォルトです。今回は LotusScript のプログラムに後で入れ替えるため、適当な文字列 ”dummy” を式として指定しています。


リンクができあがったら、リンクの見た目を整えます。ホットスポットのオブジェクト oHS に対してフォントと境界線をなしにセットしています。

   'ホットスポットのデザイン調整
   Call oHS.SetFont(xoFont_Keyword)
   oHS.ShowBorder = False

なお、引数の xoFont_Keyword は、この関数外で事前に準備したリンク用のフォント情報で、メイリオ / 12 ポイント / 太字 / 青 となっています。

   With xoFont_Keyword
      .FontName = "メイリオ"
      .FontSize = 12
      .Bold = True
      .Color = DXL.Color_Blue
   End With


LotusScript のプログラムは DXL_LotusScript オブジェクトの AppendScript メソッドで 1 行ずつ追加して作成します。

   'LotusScript のプログラムを準備
   Dim oCode As New DXL_Code(DXL.Code_LotusScript)
   Dim oLS As DXL_LotusScript

   Set oLS = New DXL_LotusScript()
   Call oLS.AppendScript(|Sub Click(Source As Button)|)
   Call oLS.AppendScript(|Call ClickKeyword("| & vsKeyword & |", | & xConvStr_LSString(vsText) & |)|)
   Call oLS.AppendScript(|End Sub|)
   Call oCode.ReplaceEvent("click", oLS)

プログラムが完成したら DXL_Code オブジェクトの ReplaceEvent メソッドで Click イベント用のプログラムとしてセットします。


最後に ReplaceCode メソッドを使用して、ホットスポットのプログラムを作成したLotusScript に差し替えています。

   'ホットスポットのプログラムを準備した LotusScript に差し替え
   Call oHS.ReplaceCode(oCode)


まとめ

今回は GPT4o Structured Outputs 活用事例で利用した dxlSuite for LotusScript の機能について紹介させていただきました。

dxlSuite では、ホットスポットや段落のようにリッチテキスト内の構成要素、フォントやコード類などの管理単位でオブジェクトを用意しています。抜粋すると次の通りです。

dxlSuite クラス名 役割
DXL_RichTextItem リッチテキスト全体。
DXL_Table 表。タブやタイマー切替の形式、罫線や枠線、行や列の間隔など。
行列を指定してセルを取得。
DXL_Cell セル。罫線の幅や背景色やグラデーション、背景画像など。
カスケードの表に対応。
DXL_Paragraph 段落。文字や添付ファイル、ホットスポットなどを配置。
DXL_HotSpot_Action ホットスポットのアクション。
DXL_HotSpot_Button ホットスポットのボタン。
DXL_InlineImage インライン(見える状態)の画像。
DXL_LotusScript LotusScript のプログラム。
DXL_Formula 式言語のプログラム。
DXL_Style_Font 文字のフォントやサイズ、装飾。
DXL_Style_Hide 非表示設定や式。

各オブジェクトにはさまざまなプロパティやメソッドが用意されており、ノーツクライアントで操作できるほとんどの設定ができます。コーディング中にポップアップヘルプが表示されますので、簡単にコーディングできます。


今回はリンク(ホットスポットアクション)を例に dxlSuite の機能を紹介しました。

このライブラリはリッチテキストを縦横無尽、自由自在に操作することを目標に開発しました。今回の事例のように、標準のノーツクラスだけでは実現できない機能が作成できるようになります。ご興味のある方は以下のリンクを参照ください。


前回 連載:つないでみよう