2025/03/28

Notes - Excel 連携:#53)ディスプレイの拡大/縮小と画像サイズ

久しぶりに『Notes - Excel 連携』の連載の更新です。

この連載では、前回の画像のリサイズをはじめ、名前アイコンの作成やグラフの保存などで、画像として保存する方法を紹介してきました。私は日々の業務でこの機能を応用して、レポートのグラフ画像を作成しているのですが、 なんだか最近出力される画像が大きいことに気が付きました。

調査した結果、Excel から出力される画像のサイズは Windows のディスプレイの設定の拡大/縮小で指定した倍率に合わせてサイズが変化することがわかりました。

そういえば、最近は 150% で使うことが多いです。これは新しい PC の解像度が高く字が小さいためで、決して老眼のせいではありません(たぶん...)。それに、最近の Windows は拡大率を変更しても再起動などは不要ですから、アプリに応じて切り替えることも多くなっています。

このような背景から、設定次第で出力画像の解像度が変わるのは避けたいですよね。ということで、今回は、LotusScript で Windows の拡大率を取る方法についてまとめます。


今回の方針

初めに Google 先生に聞いてみたところ、いくつかヒントが出てきました。Windows API を使えば取得できるようです。

GetDeviceCaps 関数 (wingdi.h)

ただ、LotusScript で試してみたのですが、いつも同じ値が返り、残念ながら正確に判定できませんでした。

そこで、今回は、手持ちのテクニックで判定することにします。


判定方法

保存した画像ファイルが倍率に応じてサイズが変わる現象なので、出力した画像ファイルの Pixel を使って判定します。

前回の記事で、AddPicture メソッド を使って画像を呼び出し、サイズを取得する方法を紹介しました。ただ、この方法で呼び出した画像は、すでに倍率に応じて伸長されています。よって、Width や Height の値は、画像の Pixel 数の判定には使用できないことになります。

   '画像サイズ取得
   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


画像ファイルのリアルな Pixel 数の取得には、画像ファイルを直接参照するのが一番確実です。そこで、別の連載『DXL Step-by-Step』の『#13)イメージの形式とサイズの取得』で紹介した方法で横幅を取得することとします。


判定の流れは次の通りです。

  1. 横幅 750 Point の Chart オブジェクトを作成
  2. Chart オブジェクトを GIF ファイルとして保存(拡大率 100% なら 1000 Pixel)
  3. 画像をバイナリファイルとして開き、画像の幅(Pixel)を取得
  4. 画像の幅(Pixel)から拡大率を算出


サンプルプログラム

まずはエージェントのメインプログラムです。拡大率を取得する関数をコールして結果をメッセージで表示するだけです。

Option Declare

Use "lsXls"
Use "lsWindows"

Sub Initialize
   Dim iZoom As Integer

   iZoom = xGetDesktopZoom()
   MsgBox "デスクトップの拡大率 = " & CStr(iZoom) & " %", 64
End Sub

スクリプトライブラリは、これまで作成してきた lsXls を呼び出します。これは後述する関数内で定数を利用するためです。

また、lsWindows は画像ファイルを一時保存するため、Windows のテンポラリフォルダを取得する処理で使用しています。ライブラリの中身については、『Windows のテンポラリフォルダの取得』を参照ください。


◇ 拡大率の取得

先に記載した ”判定の流れ” にそった関数です(① ~ ④)。

画像のサイズを 750 Point と大きくしているのは Point - Pixel 変換時の誤差の影響を小さくすることが目的です。また、画像形式は、サイズが小さそうな上に、ファイルの最初のほうにサイズ情報がある GIF 形式を選択しました。

Function xGetDesktopZoom() As Integer
   Dim oXls As Variant
   Dim oSheet As Variant
   Dim oShape As Variant
   Dim sFN As String

   '画像ファイル名
   sFN = GetWinTmpPath()
   sFN = sFN & "Denaoshi" & Format(Now, "yyyymmddhhnnss") & ".GIF"

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

   '画像サイズ
   '倍率 100% の時、1 ピクセル = 0.75 ポイント

   Dim dX As Double '画像の幅と高さ(ポイント)
   dX = 750 '100% = 1000px

   '① Chart オブジェクトを作成
   Set oShape = xAddChart(oSheet, dX, 10) '高さはこだわらない

   '② GIF ファイルとして保存
   Call xSaveAsPicture(oShape, sFN)

   '③ 保存した画像の幅を取得
   Dim iX As Integer
   iX = xGetWidth_GIF(sFN)

   '④ デスクトップの拡大率を算出
   Dim dTmp As Double
   dTmp = dX / 0.75  '100% の時の画像サイズ
   xGetDesktopZoom = Int(iX/dTmp * 100 + .5)

   'Excel は保存せずに終了
   oXls.DisplayAlerts = False
   Call oXls.Workbooks(1).Close(False)

   '画像ファイルは削除
   Kill sFN
End Function

この関数では既存のサブ関数を利用しています。xAddChart と xSaveAsPicture 関数については、前回 掲載したコードをそのまま使用しています。


◇ GIF 画像の幅を取得

GIF ファイルの画像ファイルのフォーマットについては『#13)イメージの形式とサイズの取得』に記載しています。この記事で掲載した関数は、JPEG や PNG などの対応ていたので、GIF ファイルの画像幅だけを取得する関数に再編集しています。

Function xGetWidth_GIF(ByVal vsFN As String) As Integer
   Dim ns As New NotesSession
   Dim nst As NotesStream
   Dim vTmp As Variant

   Set nst = ns.CreateStream()
   Call nst.Open(vsFN)

   'GIF 画像幅取得
   nst.Position = 0

   vTmp = nst.Read(6) '空読み
   vTmp = nst.Read(4)

   '画像幅
   xGetWidth_GIF = CInt(vTmp(1)) * 256 + CInt(vTmp(0))
End Function


まとめ

エージェントを実行すると次のようにディスプレイの拡大率が表示されます。この値を係数として利用すれば、どのような環境でも安定したサイズの画像ファイルが生成できます。

今回の手法では、一時的に Excel を起動して拡大率を取得しています。決して、効率が良いとは言えないのですが、これが利点になることもあります。

マルチディスプレイの場合、それぞれで拡大率を指定できます。先の Windows API を利用する手法の場合、どのディスプレイかを指定することになるのですが、ノーツが利用しているディスプレイがよくわからないんです。

ところが、今回の手法であれば、ノーツが利用しているディスプレイの設定を自動的に取得してくれるので、細かなことは意識しなくていいということになります。


前回 Notes - Excel 連携


2025/03/26

DXL Step-by-Step:#52)ノード操作 ⑤ - ノードを挿入する方法

前回は DXL でノードを追加する AppendChild メソッドを紹介しました。このメソッドは、最後の子ノードとして追加する機能しかありません。これでは、好きな位置にノードが配置できません。また、いくらヘルプを見ても InsertChild のような ”使える” メソッドはありません。

そこで、今回は利用できるメソッドだけを使用して、ノードを挿入する方法を紹介します。


ノードの挿入

前回紹介した事例では新規で作成したノードを AppendChild しましたが、このメソッドは DXL ツリー内に存在するノードを引数に指定することができます。この場合、元の位置から削除され、AppendChild を実行したノードの最後の子ノードとして追加されます。これの挙動を利用して ”ノードの挿入” を実現します。

下図の位置に新しいノードを追加する操作を例に説明します。

まず、前回と同様の操作で、新しいノードを一番後ろに追加します。

その後、挿入したい位置より後ろにあった 2 つのノードを順に AppendChild します。するとそれらは追加したノードの後ろに移動します。

少々回りくどいですが、これで ”ノードの挿入” が完了です。


サンプルプログラム

”ノードの挿入” を行うサンプルプログラムを紹介します。汎用的に利用できるように関数化してみました。

' 引数
' vdnInsert  挿入するノード
' vdnPos   挿入位置のノード
' vbAddBefore  True で前に追加

Function xInsertNode(vdnInsert As NotesDOMNode, _
      vdnPos As NotesDOMNode, _
      ByVal vbAddBefore As Boolean) As NotesDOMNode

   Dim iNotMove As Integer
   Dim iMove As Integer
   Dim i As Integer
   Dim j As Integer
   Dim dn As NotesDOMNode
   Dim dnParent As NotesDOMNode

   Set dnParent = vdnPos.ParentNode

   '挿入位置より前にあるノードを数える
   iNotMove = 0
   Set dn = vdnPos.PreviousSibling
   While Not (dn.IsNull)
      iNotMove = iNotMove + 1
      Set dn = dn.PreviousSibling
   Wend
   If vbAddBefore = False Then iNotMove = iNotMove + 1

   '挿入位置より後ろにあるノードを数える
   iMove = 0
   Set dn = vdnPos.NextSibling
   While Not (dn.IsNull)
      iMove = iMove + 1
      Set dn = dn.NextSibling
   Wend
   If vbAddBefore = True Then iMove = iMove + 1

   '挿入するノードを最後に追加
   Set xInsertNode = dnParent.AppendChild(vdnInsert)

   '挿入位置より後ろにあったノードを順に一番後ろに移動
   For i = 1 To iMove
      '移動すべき最初のノードを取得
      Set dn = dnParent.FirstChild
      For j = 1 To iNotMove
         Set dn = dn.NextSibling
      Next

      '一番後ろに移動
      Call dnParent.AppendChild(dn)
   Next
End Function


まとめ

DXL ではツリーと文書などノーツオブジェクトの構想は一致します。リッチテキストも同様なので、コンテンツを自由自在に操作しようとすると ”挿入” は不可欠です。ですが、DXL 関連のクラスには挿入がありません。今回のサンプルのように努力と根性で実現するしかありません。

InsertChild のようなメソッドが標準機能としてサポートしてほしいですね...。


前回 DXL Step-by-Step


2025/03/23

DXL Step-by-Step:#51)ノード操作 ④ - ノードの新規作成

今回から具体的なノード操作に入ります。まずは、基本操作となる新しくノードを追加する方法です。DXL では次のように少し回りくどい操作が必要となります。

  1. ノードの新規作成
  2. 作成したノードを追加


ノードの新規作成

DXL でノードを追加する機能は、NotesDOMDocumentNode クラスに集約されています。ヘルプで確認すると、このクラスだけ Create???Node というメソッドを持っています。

NotesDOMDocumentNode (LotusScript®)

全てのノードを作成できるようですが、『#48)ノード操作 ① - DXL 操作で必須のクラス』で説明した通り、通常使いで必要なノードは限られています。次のメソッドだけ覚えれば十分です。

◇ element ノードの作成

CreateElementNode メソッドで作成します。引数はノード名で、戻り値は 作成された NotesDOMElementNode オブジェクトとなります。

◇ text ノードの作成

CreateTextNode メソッドで作成します。引数はノードデータとなる文字列で、戻り値は作成された NotesDOMTextNode オブジェクトとなります。


ノードの作成方法はわかりましたが少々釈然としないですね。それは、どこに作成されるのか不明瞭だからです。その作業を行うのが次のステップです。


作成したノードの追加

ノードの追加は AppendChild メソッドで行います。このメソッドは、NotesDOMNode で定義されていて、このクラスを継承している NotesDOMElementNode でも利用できます。

例えば次のように記述します。

   ' 段落作成
   Set denNew = ddn.CreateElementNode("par")  ' ノードの新規作成
   Call denNew.SetAttribute("def", "1")
   Set denNew = denCur.AppendChild(denNew)  ' ノードを希望する位置に追加

ddn(NotesDOMDocumentNode のオブジェクト)のメソッドで section ノードを作成し、denNew に代入されます。AppendChild で作成したノードを追加しますが、メソッドをコールしたオブジェクト denCur の最後のノードとして追加されます。

DXL のイメージには次のような感じです。下図の場合 denCur オブジェクトは richtext ノードということになります。

ところで、SetAttribute メソッドで def という属性を追加しています。AppendChild を実行する前にセットしていますが、正しく DXL に反映されています。

以下のように記述しても、同じ結果を得られます。

   ' 段落作成
   Set denNew = ddn.CreateElementNode("par")  ' ノードの新規作成
   Set denNew = denCur.AppendChild(denNew)  ' ノードを希望する位置に追加
   Call denNew.SetAttribute("def", "1")

AppendChild メソッドの戻り値は追加されたノードなのですが、追加してから属性をセットしています。このようにノードは、属性をセットしてから追加しても、DXL ツリーに追加してからでもかまいません。


まとめ

Create???Node で作成されたノードは DXL のツリーのどこにも属さない 宙に浮いたような状態 で作成されます。このノードを DXL ツリーに追加するのが AppendChild ノードということになります。


前回 DXL Step-by-Step 次回


2025/03/22

@Word の活用

前回 紹介した @Word 関数の活用について考えます。

私が特に便利だと感じるのは、マスタデータの選択や検索をするシーンです。マスタデータは通常、名称だけではなく、コードや金額など付随する複数の情報を持ちます。アプリでマスタデータを選択する機能を作成する場合、名称と金額など、マスタ内の複数の項目を取得するのが一般的です。

@Word を使えば、このような機能を効率的に作成できます。


マスタデータ

まずは、単純な商品マスタをサンプルとして準備します。フォームは、商品コード、商品名、単価、カテゴリの 4 項目とします。

この文書を商品コードでソートしたビューを作成し、各項目を配置します。

ビュー名 商品マスタ01
ビュー別名vAppMst01


商品選択機能

続いて、アプリをイメージしたフォームに、商品選択機能を作成します。選択画面は @PickList で」実現し、選択した商品の 4 項目、すべての情報を取得する仕様とします。

この【商品選択】ボタンはどのような式を記述すればいいでしょうか?@関数を覚えたての方なら以下のように書くと思います。

xReturn := @PickList([Custom]:[Single]; @DbName; "vAppMst01"; "選択"; "商品を選択してください。"; 1) ;

@SetField("CD"; xReturn);
@SetField("Name"; @DbLookup("":"NoCache"; @DbName; "vAppMst01"; xReturn; "Name"));
@SetField("Category"; @DbLookup("":"NoCache"; @DbName; "vAppMst01"; xReturn; "Category"));
@SetField("Price"; @DbLookup("":"NoCache"; @DbName; "vAppMst01"; xReturn; "Price"))

まず、@PickList の戻り値で、商品コードを取得します。それ以外の項目は、@DbLookup で再検索し取得します。


@Word でアクセスを削減

上記の式でも正しく動作します。ですが、@PickList とそのあとの再検索で、同じビューに 4 回もアクセスしていて、非効率です。この機能は @Word を使えば効率的に記述できます。

まず、ビューを細工します。最後に列を追加して、次のような式を記述します。

xDelimiter := "///";
CD + xDelimiter + Name + xDelimiter + @Text(Price) + xDelimiter + Category

この式で、マスタのすべての項目が特定の区切り文字で繋げられ、1 列の中に表示されます。この列を取得すれば、必要な項目をまとめて取得できるという算段ですね。なお、この列は、選択画面には不要ですので非表示にしておきます。

※ 記事内の図や式では別のビュー『商品マスタ02 | vAppMst02』として記述しています。


続いて、【商品選択】ボタンの式を以下のように変更します。

xDelimiter := "///";

xReturn := @PickList([Custom]:[Single]; @DbName; "vAppMst02"; "選択"; "商品を選択してください。"; 6) ;

@SetField("CD"; @Word(xReturn; xDelimiter; 1));
@SetField("Name"; @Word(xReturn; xDelimiter; 2));
@SetField("Price"; @TextToNumber(@Word(xReturn; xDelimiter; 3)));
@SetField("Category"; @Word(xReturn; xDelimiter; 4))

この時活躍するのが @Word です。区切り文字で分割し、部分文字列を取得する機能がぴったりハマります。部分文字列の 1 つ目は商品コード、2 つ目は商品名、... というように順に読み込んで、フィールドに当てはめるだけです。

ポイントは、各項目に含まれない文字列を区切り文字に設定し、ビューとボタンで同じものを設定することです。区切り文字は、複数文字を指定できるので、安心ですね。

この方法であればビューの接続は 1 度だけになります。


複数値の場合は特に効果的

ノーツでは、複数値の区切り文字を改行にして、疑似的な表のように見せる技をよく使います。


この場合、【商品選択】ボタンの @PickList は [Single] キーワードを外して、複数文書選択できるようにします。

xReturn := @PickList([Custom]:[Single]; @DbName; "vAppMst02"; "選択"; "商品を選択してください。"; 6) ;

@SetField("CD"; @Word(xReturn; xDelimiter; 1));
@SetField("Name"; @Word(xReturn; xDelimiter; 2));
@SetField("Price"; @TextToNumber(@Word(xReturn; xDelimiter; 3)));
@SetField("Category"; @Word(xReturn; xDelimiter; 4))

この対応だけで、複数の商品を選択する機能が実現できます。

@PickList で複数文書を選択すると、結果はリスト値で返されます。そして、@Word は、リスト値に対応していますので、そのままのコードで都合よく処理してくれます。


まとめ

ノーツのビューは、文書数が多い、更新頻度が高いなどの理由により、レスポンスが遅くなることがあります。今回の事例のように、ビューに接続する回数を減らせば、比較的に良いレスポンスを維持することができます。

また、@Word を組み合わせれば効率的に必要なデータにアクセスでき、プログラムをシンプルに保つことができます。今回は、@PickList を例にしましたが、@DbLookup でも利用できる技です。いろいろな場面で活用できますので、是非とも覚えてください。


2025/03/19

@Word の使い方

今回は @Word 関数を紹介します。文字列から部分文字列を取得する関数で、便利に使えるケースがある関数です。ぜひ覚えておきましょう。

ヘルプでは @Word は文字列から指定された ”単語” を返すと説明されています。ここでいう ”単語” とは文字列を指定した区切り文字で分割した一つを指すので、”単語” より ”部分文字列” のほうが誤解がないと思います。


構文

ヘルプによると @Word の文法は次の通りです。

@Word( string ; separator ; number )

引数の役割は次の通りで、どの引数も省略することはできません。

1 string 文字列
文字列リスト
検索され取得元となる文字列
2 separator 文字列 区切り文字
3 number 数値 取り出す位置
正数は前から、負数は後ろから

LotusScript では、StrToken 関数が同等の機能を提供します。


戻り値

@Word の戻り値は次の通りです。

文字列
文字列リスト
string 内の number で指定した位置にある部分文字列を返します


サンプル

単純なフォームを作成してテストします。各引数を入力できるようにし、その値を使用して @Word を実行します。

プリビューして、ソース文字列に "Returns the specified word from a text string."、区切り文字に " " (スペース)を入力します。

取り出し位置に 3 を指定すると "specified"、-2 を指定すると "text" が返されます。なるほど、Word という関数名になった理由がよくわかりますね。日本語で ”単語” の抽出には利用できないですが...

ちなみに、存在しない数値、例えば上記例で 10 を指定すると、空の文字列が返され、エラーとはなりません。


また、この関数は構文や戻り値に記載した通り、リスト値に対応しています。次の例では氏名から 姓 だけを取得しています。

次の事例では、氏名と所属を ”/” 区切りで表現していたとします。最後が事業所で、後ろから二つ目が部門(部)だったとします。以下のように記述すれは簡単に部門(部)が取得できます。


まとめ

上記の例において、一般に所属は役職や部門によって階層が異なります。このようなデータでは、負数を指定して後ろから取得する機能が有効です。

このように @Word は特定の条件がある場合、とても便利に機能します。

活用できるケースは多岐にわたるので、探してみましょう。一発で取得できる事例に出会うとコードも気分もスッキリしますよ。


2025/03/14

3 年目突入 !!

2023 年に始めたブログ『出直し!! ドミノ塾 』は、本日 3 月 14 日で 2 周年を迎え、3 年目に突入しました!


人気の記事

2 年分をざっと振り返ると、投稿した記事は 約 330 本、約 9 割の記事が開発系でした。中でも @関数や LotusScript など、90 年代後半のノーツバブルのころからある機能が多数を占めています。プロフィールに書いていますが『オールドタイプ』全開ですね(笑)

記事もそれなりにたまってきたので、直近 1 年分のアクセスログを調査してみました。アクセス頻度の上位 20 の記事は下表の通りでした。

結果から見て取れるのは、

  1. @関数の使い方
  2. リッチテキストの操作
  3. Excel 連携

が主で、それに続いて LotusScript といったところでしょうか...。やはり、すそ野が広い分、基礎的なコンテンツにはアクセスが集まるのですね。

逆に DXL(DXL Step-by-Step) や WebAPI連携(つないでみよう)の連載は、マイノリティ向けの記事と思っているのですが、それでもランクインするということは、一部の方には気に入っていただけているのだと思います。

今後も硬軟織り交ぜながらバランスよくネタを選んでいこうと感じました。

◇ Top 20

1 @DbLookup の使い方
2 Notes - Excel 連携:#35)Excel で使用する単位と変換
3 Notes - Excel 連携:#4)ノーツ文書をワークシートに出力
4 リッチテキスト:#16)テキストの抽出
5 リッチテキスト:#15)添付ファイルのダウンロード
6 @Prompt の使い方
7 LotusScript(ラベルのクリック)
8 リッチテキスト:#1)フィールドの作成
9 NSF メンテナンス 三種の神器
10 DXL Step-by-Step(連載の目次ページ)
11 Notes - Excel 連携:#6)マクロの記録の活用 と 文法の違い
12 リッチテキストの基本操作(連載の目次ページ)
13 つないでみよう(連載の目次ページ)
14 @関数編(出直しヘルプ「@関数編」の目次ページ)
15 @DbColmun の使い方
16 リッチテキスト:#14)既存リッチテキストフィールドの取得
17 つないでみよう:#14)GPT4o で画像認識 - 準備作業
18 リッチテキスト:#6)文字色の設定
19 リッチテキスト:#3)添付ファイルの作成
20 Notes - Excel 連携(連載の目次ページ)


まとめ

この 2 年間を振り返ってみると、ずっと Notes/Domino を検証しながら、キーボードをたたいて記事を書いていたような気がします...。連載 100 回記念 で記載しましたが、ノーツ担当 30 周年記念に加えて、私の社会人人生をまるっとノーツにお世話になったお礼を兼ねて始めたこのブログですが、それなりに恩返しはできたかなと思っております。

こんな風に書くと終了するのか?と思われるかもしれませんが、まだもうしばらく続けるつもりです。ただ、ネタが尽きてきたので投稿頻度は減ってくると思います...。

あらためまして、3 年目もどうぞよろしくお願いいたします!


2025/03/11

DXL Step-by-Step:#50)ノード操作 ③ - NotesDOMNode の使い方

DXL を自由自在に操作するためには NotesDOMNode の役割と使い方を理解することが重要です。


NotesDOMNode クラスとは

まずは、デザイナーヘルプで NotesDOMNode を確認します。

クラスの説明には、

文書ツリーの単体のノードを表します。

とだけ記載されています。

また、『作成方法とアクセス方法』には、以下の通り記載されています。

NotesDOMNode クラスは抽象クラスであるため、NotesDOMNode オブジェクトを作成できません。代わりに、NotesDOMDocumentNode クラスの適切な Create メソッドを使用して、特定の派生ノードクラスのオブジェクトを作成します。
NotesDOMNode クラスとその派生クラスのオブジェクトは、作成元の NotesDOMParser オブジェクトとの関連性を保持します。このメソッドは該当する 2 つのノードが同じ DOM パーサーから派生した場合のみ機能します。

これだけでは、さっぱりわかりませんね...。ポイントは、以下の部分です。

派生クラスというセクションがあり、NotesDOMElementNode などのクラスがリストアップされており、最後に「NotesDOMNode クラスから継承」とあります。

簡単に説明すると、NotesDOMElementNode などのクラスは、

  • NotesDOMNode を基に定義されたクラスであること
  • NotesDOMNode は派生クラスの値を持てること

を表します。


言葉だけではわかりにくいので具体的なコードとともに確認しましょう。

NotesDOMElementNode には、配下のノードを検索するメソッド GetElementsByTagName が存在します。この命令を使用して、"table" ノードを検索し、順に取得する処理を例にします。

   '表を検索し順に処理
   Dim dnl As NotesDOMNodeList
   Dim dn As NotesDOMNode
   Dim i As Integer

   Set dnl = denForm.GetElementsByTagName("table")
   For i = 1 To dnl.NumberOfEntries
      Set dn = dnl.GetItem(i)

      '”表”に対する処理を記述
         ・・・
   Next

GetElementsByTagName メソッドの戻り値は NotesDOMNodeList です。このクラスは、検索結果を保持するためのクラスで、複数の NotesDOMNode をコレクションのように保持します。上記サンプルでは、GetItem メソッドで、検索結果から 1 つずつ取得して順に処理させていますが、戻り値の型は NotesDOMNode となっています。実体は ”表” を表すノードですから、NotesDOMElementNode となります。

このように NotesDOMNode クラスは、派生クラスに記載されているオブジェクトを保持できる汎用的なクラスということになります。


NotesDOMNode クラスの使い方

続いては、上記で取得した NotesDOMNode の使い方です。

前述の通り、中身は NotesDOMElementNode クラスのオブジェクトです。ですので、下記の通り NotesDOMElementNode の変数 den に代入できます。代入後の den を利用すれば NotesDOMElementNode クラスのプロパティやメソッドにアクセスできます。

   '表を検索し順に処理
   Dim dnl As NotesDOMNodeList
   Dim den As NotesDOMElementNode
   Dim dn As NotesDOMNode
   Dim i As Integer

   Set dnl = denForm.GetElementsByTagName("table")
   For i = 1 To dnl.NumberOfEntries
      Set dn = dnl.GetItem(i)

      '”表”に対する処理を記述
      Set den = dn
      'den を使えば NotesDOMElementNode の機能が使える
          ・・・
   Next

NotesDOMNode の実態は、NotesDOMElementNode だけでなく、派生クラスとして記述されているさまざまなオブジェクトを取りうることになります。上記例では、"table" ノードを検索したので、実態は NotesDOMElementNode だとわかります。ですが、前回 紹介した FirstChildNextSibling で取得したノードの場合はどうでしょう。DXL 内で固定の文字列を表す NotesDOMTextNode の可能性もあります。

取得したノードの種類を調べるには、NodeType というプロパティを使用します。戻り値は数値で、多数のノードが定義されています。ただ、一般的な DXL のコーディングでは 1 と 3 を覚えておけば十分でしょう。

定数 クラス名
1 DOMNODETYPE_ELEMENT_NODE  NotesDOMElementNode
3 DOMNODETYPE_TEXT_NODE  NotesDOMTextNode
9 DOMNODETYPE_DOCUMENT_NODE  NotesDOMDocumentNode

9 の NotesDOMDocumentNode については 前々回 で必須クラスとして記載したので掲載していますが、NotesDOMDocumentNode であるかを判断することはないと思います。


NodeType は、具体的には次のように利用します。

FirstChild プロパティで取得したノードを NotesDOMNode の変数でいったん受け取ります。その NodeType を確認したうえで、オブジェクトの型に合致した変数に代入します。

   Dim dn As NotesDOMNode
   Dim dnChild As NotesDOMNode
   Dim den As NotesDOMElementNode
   Dim dtn As NotesDOMTextNode
          ・・・
   Set dnChild = dn.FirstChild
   If dnChild.NodeType = DOMNODETYPE_TEXT_NODE Then
      'テキストノード
      Set dtn = dnChild
          ・・・
   ElseIf dnChild.NodeType = DOMNODETYPE_ELEMENT_NODE Then
      'エレメントノード
      Set den = dnChild
          ・・・
   End If


まとめ

今回は NotesDOMNode クラスの使い方についてまとめました。少々ややこしいのですが、ここをしっかり押さえておくと DXL のプログラミングが格段に速くなると思います。

ところで、この記事をここまで読んで、あれっ?? と思うことはなかったですか?

NotesDOMNode の派生クラスの一覧(最初の画像内)に NotesDOMTextNode が含まれていません。ですが、上記サンプルプログラムでは、代入していました。これは、ヘルプやサンプルコードの間違いではありません。

      'テキストノード
      Set dtn = dnChild
          ・・・

NotesDOMTextNode のヘルプを見ると継承元が NotesDOMCharacterDataNode となっています。そしてそのクラスの継承元が NotesDOMNode となっています。カスケードした関係だから、記載がなかったということですね。

 NotesDOMNode
 ┗ NotesDOMCharacterDataNode
   ┗ NotesDOMTextNode 

 

前回 DXL Step-by-Step 次回


2025/03/09

Fix の適用ってお得 !? ドメイン検索の索引作成時間が改善!

今回も昨年末の Domino サーバのメールルーティングトラブル に関連する件です。トラブルの緊急対応だったので、半ば強制的に最新の Fix を適用しました。その作業をぼやくのではなく、”うれしい誤算” があったお話です。


棚ボタな現象

社内ではドメイン検索を運用しています。運用スケジュールとして、毎週土曜日に全対象 DB の索引を再作成する作業を行っています。対象 DB の数や文書数が多く、かなりのサイズを索引するため、実行時間が非常に長くなります。この処理時間が、最新 Fix 適用でずいぶん短くなったのです!

バージョン 処理時間
11.0.1 FP2 約 15 時間
11.0.1 FP9 約 8 時間

最新 Fix 適用前後で対象の DB の変更は行っていませんので、Fix 適用の効果といえると思います(もちろんサーバ機のリソースなども変更していません)。

少し改善するだけならまだ理解できるのですが、ほぼ半減となるとさすがに障害を疑いますよね。カスタマーサポートに問い合わせしたところ、最新 Fix にはさまざまなアップデートがあり、それに起因して短縮したとの返答でした。

運用上、問題が発生していないこともあり、この返答に納得しました。まさに、うれしい誤算、棚からぼた餅、瓢箪から駒ですね !!


修正リスト

サポートからいただいた修正リストをまとめると次の通りです。項目数が多く驚きましたが、日々改善されているのはありがたいですね。

バージョン SPR# 修正
11.0.1 FP3 JBUDBWYRRN 添付ファイルの全文索引作成時にファイルタイプの判定を改善しました。 
YCHABULJCB Domino 10.x.x で作成された索引ファイルのサイズが Domino 9.0.1 で作成されるものよりも大きくなる問題を修正しました。このリグレッションは 10.0 で確認されました。
11.0.1 FP4 JBUDBNFH6R データベースの全文索引作成時のオプションで添付ファイルの索引に簡易検索を選択している場合、添付ファイルのファイル名を検索することができなくなる問題を修正しました。このリグレッションは 11.0.1 で確認されました。
JBUDC36HZX 大量の添付ファイルを含むデータベースに対して全文索引の作成や更新を行うとパフォーマンスが悪化する問題を修正しました。
JBUDC57T3N 全文索引の添付ファイルをフィルタリングする際に、bmp ファイルが無視されない問題を修正しました。
NNAIBZV2VR 全文索引が作成されているデータベースで、新規文書の全文索引作成が停止することがあるという問題を修正しました。
RRENBBRJN4 索引の更新が先に実施されることで検索が顕著に遅延する、全文索引の自動更新の機能について修正しました。このリグレッションは 10.0.1 で確認されました。
11.0.1 FP5 WSZO7JU6U6 文書の全文索引作成時に、ログにエラー 3351 (Open error) が表示される問題を修正しました。
11.0.1 FP7 MOBNCE5N54 全文索引でエラー番号 3351 が発生すると、全文索引の再構築が必要になる問題を修正しました。
MOBNCGWPWM GTR のエラー番号 223 が発生すると、稀に全文索引の再構築が破損する問題を修正しました。
MOBNCJETFX 全文索引処理で暗号化した MIME 文書の索引を作成する際に「Handle out of range」エラーが発生し Domino サーバーがクラッシュしてしまう問題を修正しました。
NNAICLEE5H ドメイン検索で、Brute Forceを使用して索引作成された添付ファイルを見つけることができない問題を修正しました。このリグレッションは 12.0.2 で確認されました。
11.0.1 FP8 DCONBYWHG9 サイズの大きな動画ファイルに対して全文索引を作成する際に、動画ファイルが「MediaDataBox」から始まるファイル名で Domino の一時ファイルフォルダ内に残ってしまう問題を修正しました。
JBUDC36HZX 大量のファイルが添付されているデータベースに対する全文索引作成時のパフォーマンスに関する問題を修正しました。
JBUDC57T3N 全文索引の添付フィルタリング時に bmp ファイルが無視される問題を修正しました。
WSZO7JU6U6 文書の全文索引作成時に、ログにエラー 3351 (Open error) が表示される問題を修正しました。
11.0.1 FP9 MOBNCRUJSJ 全文索引作成時に、クラッシュの原因となる文書が隔離されないことがある問題を修正しました。

ところで、”リグレッション” って単語がたびたび出てきます。少々、気にはなったのですが、今回は ”うれしい誤算” の話なので黙殺したいと思います...。


2025/03/07

Domino インストールエラーの対処

年の瀬に Domino サーバのメールルーティングトラブル が発生しました。すべての Domino サーバで発生するトラブルで、影響は甚大です。Notes/Domino のトラブルでこれほど大きいのは珍しいですね。

発災からほぼ 3 ヶ月経ちましたので、対応も進み状況は落ち着いたのではないでしょうか?

私が管理するサーバも影響を受けたのですが、メーカーによる迅速なパッチ提供があり、それを適用することで回避できました。対応としては、最新の FixPack の適用とこのトラブルの Interim Fix を適用を行いました。

ただ、その作業でインストールトラブルに見舞われました。他の HotFix 適用など将来も起こりうる現象なので、後学のためにまとめておきます。


なお、本件の解決は、HCL Software の Customer Support に協力をいただきました。超ド級トラブルの最中だったので、サポート担当に皆さんは、私ら以上にパニック状態だったと思います。それでも、迅速かついつも通りの丁寧な対応をくださいました。解決できてありがたいのはもちろんですが、それ以上に感心いたしました。IT 担当者として見習わなくては...。


トラブル①:インストール中のエラー

まずは、FixPack インストール途中で発生したエラーです。

インストールを開始してしばらくたってから「RenameTempFiles: ERROR: Can't rename・・・」のエラーが発生し、インストールに失敗するというものです。

このエラーについては、以下の技術情報がありました。この記事によると Windows Management Instrumentation サービスが実行していることが原因だそうです。

Domino サーバーの Fix Pack が "RenameTempFiles: ERROR: Can't rename old <file name>: access problem" のエラーでインストールできない


トラブル②:インストーラが起動しない

インストールのトラブルはもう一つあり、FixPack のインストーラを実行したら  "Notes/Domino or a Notes/Domino related process is still running." とメッセージが表示され、インストーラが起動しないというものでした。

この問題もすでに技術情報がありました。

Fix Pack または Hotfix が "Notes/Domino or a Notes/Domino related process is still running." エラーでインストール/アンインストールできない

エラー発生の原因として、以下の 6 点が挙げられていますが、3 に起因して発生しています。またしても Windows Management Instrumentation サービスが原因だったのです。

  1. Domino サーバー関連のプロセス(nsd.exe も含めて主に "n" で始まるプロセス、scontroller.exe、jconsole.exe)およびサービスが終了していない。
  2. バックアップソフト、もしくはウィルススキャンソフトが Domino サーバーのファイルにアクセスしている。
  3. Windows Management Instrumentation サービス (WmiPrvSE.exe) が稼動している。
  4. Fix Pack もしくは Hotfix のインストーラーを管理者権限で実行していない。
  5. Fix Pack もしくは Hotfix のインストーラーが OS のプロパティにて「読み取り専用」にチェックされている。
  6. Windows 側(OS 側)で、インストール先のフォルダに書き込み禁止権限が設定されている。


インストール手順

先の技術情報では『Windows Management Instrumentation サービスを停止する』とだけしか書かれていませんが、停止するだけではエラーが継続する現象がありました。これについては OS を再起動することで回避できました。

この経験を踏まえ、FixPack や Interim Fix の適用手順を改めて整理します。


1.Dominoサーバーサービスを無効に設定

サービスの一覧で Notes/Domino に関するすべてのサービスを無効化して、自動的に起動しないように設定します。

2.システムを OS から再起動 

Windows OS を再起動します。1 の設定により Notes/Domino 関連のサービスは起動しない。

3.画面上で起動している、すべてのアプリケーションを停止

4.Windows Management Instrumentation サービス の停止

サービスの一覧からサービスを停止します。その際、関連するサービスの停止を確認するメッセージが表示されますので、停止させます。停止するサービスは、1 つの場合と 2 つの場合がありました。

5.一時ファイルをすべて削除

コマンドプロンプトから set コマンドを実行し、TMP および TEMP 環境変数の示すディレクトリを確認、その一時ディレクトリを空にします。

6. その後 FP や IF をインストール

インストーラは、管理者権限で実行します。


まとめ

今回は、FixPack のインストール時に発生した Windows Management Instrumentation サービスによるエラーについて記述しました。

今回のトラブルは緊急対応だったため、準備もなく対応に当たりました。FixPack のインストールだけだったので、作業としては安易に考えており、業務時間中にサーバを停止してのぶっつけ本番作業としました。インストール途中でエラーが発生した時には、原因調査と並行して、ユーザを待たせないよう Domino サーバを起動することとなり、「中途半端な状態で、正常に起動するのか?」と 肝を冷やしました...。

このようなことにならないよう日頃から、手順を明確にしておくべきと再認識した次第です。サーバ管理者として、まだまだ半人前ってことですね...。

ちなみに、インストールエラーでキャンセルした後に起動した Domino サーバは、作業前の FixPack で無事稼働しました。インストーラってよくできているんですね。


2025/03/05

@Left, @Right, @LeftBack, @RightBack の違い

今回は文字列操作の中でもっとも使うといっても過言ではない @Left、@Right をまとめます。また、混乱してついついヘルプを見てしまう @LeftBack、@RightBack も併記して整理したいと思います。


基本的な構文

どの関数も同じ構文なので @Left を例に確認します。

@Left(stringToSearch; subString)

@Left(stringToSearch; numberOfChars)

これら関数の構文には 2 種類あり、2 つ目の引数が数値の場合と文字列の場合があります。引数の役割は次の通りで、どちらも省略することはできません。

1 stringToSearch 文字列
文字列リスト
検索され取得元となる文字列
2 subString 文字列 検索文字列
numberOfChars 数値 文字数

戻り値は、取得元となる文字列から抽出した部分文字列となります。


引数が文字列の場合

まずは、第 2 引数が検索文字列を指定するパターンです。

検索される文字列は "ABCXYZABC" で固定し、@関数と検索文字列 Chr を変化させながら検証します(例は @Left の場合)。

@Left("ABCXYZABC"; Chr)

検索文字列は、ヒットする文字以外に、イレギュラーパターンとして null と含まれない文字の場合を試しました。

Chr @Left @Right @LeftBack @RightBack
(null) (null) (null) (null) (null)
A (null) BCXYZABCABCXYZ BC
C AB XYZABC ABCXYZAB (null)
X
ABC YZABC ABC YZABC
0 (null) (null) (null) (null)

まずは、ヒットする C の場合です。@Left では左から検索し、ヒットした文字の左側である AB を返しています。@LeftBack では右側から検索するので、いきなりヒットすることになり、その左側を返しています。この構文では検索する方向が重要だといえますね。

イレギュラーパターンの場合は、すべて null となるので、わかりやすい結果となっています。

この動作を表にまとめると以下の通りとなります。


検索方法 取得する文字列
@Left 左 → 右 左側
@Right 右 → 左 右側
@LeftBack 右 → 左 左側
@RightBack 左 → 右 右側

@Left では、左から検索し、ヒットしたらその左側を返します。@Right の場合は右から検索し、右側を取得します。Left なら左、Right なら右でそろっているのでわかりやすいですね。

Back がつくと検索方向が逆になるだけで、取得する文字列の方向は変わりません。


引数が数値の場合

続いては、第 2 引数が文字数を指定するパターンです。文字列 "ABC" に対して、@関数と文字数 n を変えながら結果を確認します(例は @Left の場合)。

@Left("ABC"; n)

テストですので、イレギュラーな値についても検証しています。

n @Left @Right @LeftBack @RightBack
-1 ABC ABC (null) (null)
0 (null) (null)ABC ABC
1 A C AB BC
2
AB BC A C
3 ABC ABC (null) (null)
4 ABC ABC (null) (null)

まずは、妥当な文字数といえる n = 0 ~ 3 を例に挙動を確認します。

Back のついていない関数では指定した文字数を取得しています。@Left なら左、@Right なら右から、指定した文字数を取得するので、わかりやすい結果です。

Back がついている関数では、元の文字数 - n の文字数となっています。例えば、@LeftBack の場合、逆方向(Left の場合は右)から n 文字分を省いた文字列が返されます。

この動作を表にまとめると以下の通りとなります。


取得する文字列 取得方法
@Left 左側 左 から 文字数分を 取得
@Right 右側 右 から 取得
@LeftBack 左側 右 から 省く
@RightBack 右側 左 から 省く

関数名に Back がつく場合、逆方向から指定した文字数を ”省く” 動作となります。引数に指定した数値は "省く文字数" となり "取得する文字数ではない" ことが混乱のもとかもしれませんね。

n = -1 の場合は n = 0 と同じになるかと想定したのですが、予想外の結果でした。このような値をわざと指定する人はいないと思いますが、注意が必要かもしれません。


まとめ

今回は @Left、@Right、@LeftBack、@RightBack 関数の違いについてまとめました。引数のデータ型と機能の違いを覚えておきましょう。