2025/04/18

DXL Step-by-Step:#54)ノード操作 ⑦ - ノードの検索

今回は DXL ツリーからノードを検索する方法についてまとめます。


ノードの検索

GetElementsByTagName メソッドを使用するとノードの検索が可能です。

GetElementsByTagName (NotesDOMElementNode - LotusScript®)

このメソッドの使い方はシンプルで、引数に検索したいノード名を指定するだけです。例えば、以下のプログラムでは、リッチテキストを表す richtext ノードを検索しています。

   Dim ddn As NotesDOMDocumentNode
   Dim denDoc As NotesDOMElementNode
   Dim dnl As NotesDOMNodeList
      ・・・
   '文書取得
   Set denDoc = ddn.DocumentElement

   'リッチテキスト検索
   Set dnl = denDoc.GetElementsByTagName("richtext")

戻り値は、名称が一致したすべてのノードを検索された順序で返します。複数のノードの集合となりますので、NotesDOMNodeList のオブジェクトとなります。GetItem メソッドを使用して、必要なノードを取得できます。

   '最初のリッチテキスト取得
   Set denRT = dnl.GetItem(1)

この動作をサンプルの DXL 使って図にすると次のようになります。

図からもわかる通り、GetElementsByTagName メソッドでは、子ノードだけでなく、配下のすべてのノードが検索対象となるという点がポイントとなります。


子ノードだけを検索したい

リッチテキストでは表の中に表を配置するなど、階層化して複雑なコンテンツを表現できます。例えば、リッチテキストにタブ表を作成して、その中に 2 つの表を配置します。

これを DXL 化すると外側の表(赤線の table ノード)の中に内側の表(紫線の table ノード)が作成されます。

このフィールドに対して GetElementsByTagName を利用すると、すべての表がヒットしてしまいます。要は、検索結果では表の階層関係がわからず、希望した位置の表が取得できているのか判別が難しいのです。このような背景から、子ノードだけを検索したいことがしばしばあります。この要件に対応するメソッドはありませんので、自作しなければなりません。

次の関数は、引数で指定した名称の最初の子ノードを取得することができます。

Function xGetFirstChildByName(vdenParent As NotesDOMElementNode, ByVal vsNodeName As String) As NotesDOMElementNode
   Dim dn As NotesDOMNode

   Set dn = vdenParent.FirstChild
   While Not(dn Is Nothing)
      If dn.IsNull = False Then
         If dn.NodeType = DOMNODETYPE_ELEMENT_NODE Then
            If dn.NodeName = vsNodeName Then
               Set xGetFirstChildByName = dn
               Exit Function
            End If
         End If
      End If

      Set dn = dn.NextSibling
      If dn.IsNull Then
         Set dn = Nothing
      End If
   Wend
End Function


まとめと次回の予告

今回は GetElementsByTagName メソッドでノードの検索方法を紹介しました。フィールドを表す item ノードなど階層化されないノードの場合は有効かつ便利に使用できます。しかし、事例に上げたように階層化されるノードでは、関係が不明瞭になり効果的に利用できません。その対策として、指定した名前の最初の子ノードを取得する関数を紹介しました。

最初のノードが取得できれば、次のノードを取得したくなります。最後のノードを取得したり、その一つ手前の取得したいというようなこともあるでしょう。DXL のノードを自由自在に操作するためには必要な機能と言えます。次回は、これらの便利関数を紹介します。


前回 DXL Step-by-Step


2025/04/16

Domino 14 マイグレーション: ”特に追加の設定は不要”ではなかった Windows アカウントの変更

2025 年 6 月 25 日をもって Domino 11 のサポートが終了します。

HCL Domino v11.0.x の EOM (End of Market) および EOS (サポート終了、End of Support) について

この手の話はフェイクニュースのようなフィッシング系の営業活動に利用されがちなので、あえて言います。Domino がなくなるのではなく、新しいバージョンがあるからそっちを使ってね!ということです。単に、古いバージョンのサポートが終了するだけで、Windows 10 のサポート終了と同じです。

新しいバージョンを導入すればさまざまな新機能が活用できます。私がサポートしている Domino サーバもいよいよバージョンアップすることになり、Domino 14.0 の検証をしています。

最近の Domino は互換性が非常に高く、基本的には検証時間がもったいないと感じるほど、フツーにそのまま動作します。そうはいっても、いくつかは気を付けるべき点があり、今回はその中から私の環境では比較的大きかった問題を紹介します。


Domino 14 のバージョンアップ情報

Domino 14 導入に関連して、バージョンアップの手順や注意点に関しては、HCL Ambassador の Kazumasa Hayashi さんのブログ、Domino Lab で詳しくまとめられています。私もこの記事を一読したうえで検証にとりかかりました。

Domino Server をV14 にバージョンアップ!

私が担当する環境では XPages を利用していなので、何も問題はないだろう、これまでのバージョンアップとは変わらないなと判断しました。


遭遇した問題

検証環境に Domino 14 をインストールしました。インストール作業自身は順調に進み、なんの問題もなく終了しました。ところが、通常アプリとしては正常に起動するのですが、サービス起動ができないのです。

Domino Console を起動して確認すると次のようなパニックエラーが発生していました。

[156C:0002-1570] HCL Domino (r) Server (64 Bit), Release 14.0FP3 HF33, February 05, 2025
[156C:0002-1570] (C) Copyright HCL Technologies. 1987, 2023

[156C:0002-1570] comp = 11, fnc = 81, probeid = 79, errcode = 5010, extsympt = 0065
69200000
Unexpected internal error returned to logger: 0x20692010

[156C:0002-1570] Thread=[156C:0002-1570]
[156C:0002-1570] Stack base=0x6487D080, Stack size = 12640 bytes
[156C:0002-1570] PANIC: Unexpected internal error returned to logger: 0x20692010


原因はフォルダのアクセス権限

HCL Ambassador 仲間やカスタマーサポートの協力で、早い段階から以下の記事に遭遇しました。

HCL Domino 14 (Windows 版) インストーラーウィザードで追加された Windows ユーザー名の入力画面について

そういえば、インストール時に見たことない画面が増えたなと感じてはいたのですが、この記事内に『デフォルトのまま NT AUTHORITY\LocalService を指定してインストールした場合は、特に追加の設定は不要です。』とあったので、黙殺していました...。

その後、以下の記事の 2 をみて、気が付きました。

HCL Domino 14 バージョンアップ時の注意事項


Domino 14  から サービス起動する際のアカウントが、 ”Local System” から ”Local Service” へ変更となりました。そして、Domino 14 のインストーラではその権限設定を自動で変更する仕様のようです。

ただ、自動で変更されるのはプログラムディレクトリとデータディレクトリのみとなっています。それ以外のフォルダを利用している場合、手動にて権限変更する必要があるのです。


外部フォルダの利用

検証環境のDomino ではさまざまな外部フォルダを利用していました。まず notes.ini では、次のような設定をしていました。

Technical Support ログ LogFile_Dir=D:\Lotus\Domino\TechnicalSupport
トランザクションログ TRANSLOG_Path=D:\Lotus\Domino\trans_log
全文索引 FTBasePath=D:\Lotus\Domino\Index_FT

また、アプリやメールなど DB の用途ごとにフォルダを切り、ディレクトリリンク機能で Domino サーバに認識させていました。

このような細かな設定は

  1. Disk の枯渇対応
  2. レスポンス改善を目的に一部を高速なストレージへ移行
  3. フォルダごとのサイズを把握しやすくする
ことが目的です。特に 1 と 2 は物理サーバで、リソースを柔軟に増減できず、SSD のような高速ストレージが希少だった時代の名残です。将来のリソース不足に備え、部分的に移行しやすくするために行った設定でした。


権限の設定と解決

これら外部フォルダに対して、順に Windows の権限を付与します。下図の通り、LOCAL SERVICE に対してフルコントロールを設定します。

すべてのフォルダに設定が完了したら、無事サービス起動するようになりました。PANIC のメッセージを出す前に少しでもヒントとなるような情報を出してくれてれば、もっと早く気が付けたんですけどね...。


おわりに

今回のトラブルは Domino サーバに対して追加で行った設定があだとなって発生しました。ただ、どの設定も正規の Domino の機能として提供されたものです。開発したアプリが外部のローカルフォルダをアクセスしているならまだしも、正規の機能の範囲は対応してほしいところですね。

特に、トランザクションログは、データディレクトリとは別の RAID カードのディスクに配置することを推奨する記述が Administrator ヘルプにあったと思います。要は、トラブルの種を推奨していたということなので、たくさんの Domino ユーザが該当すると思います。これからバージョンアップをご検討の方は、十分お気をつけください。


2025/04/15

DXL Step-by-Step:#53)ノード操作 ⑥ - 属性の操作

今回は NotesDOMElementNode が持つ属性の操作についてまとめます。


属性とは

下図は、#49)ノード操作 ② - ノード間の関係と取得方法 に掲載した DXL のノードを分類した図ですが、ピンクの下線が属性です。

属性は、NotesDOMElementNode の < > 内に定義され、属性名='値' で構成されます。値は、id='1' や def="1" のような数値でも、color='#ffe118' のような 16 進数でも、name='Body' のような文字列と同じ表現になります。つまり、属性の値の型には文字列しかないということですね。

また、font ノードのように複数の属性を持つこともあれば、richtext や run ノードのように属性を持たないこともあります。


属性の操作

属性を管理する NotesDOMAttributeNode というクラスが定義されています。しかし、属性は NotesDOMElementNode のメソッドから操作ができます。よって、NotesDOMAttributeNode クラスは普段使いでは使用しません。

NotesDOMElementNode のメソッド 機能
GetAttribute 属性の取得
SetAttribute 属性の設定
RemoveAttribute 属性の削除


属性の取得

属性の取得は、GetAttribute メソッドで属性名を引数に指定するだけです。戻り値はその属性の値が文字列で返されます。

attr$ = notesDOMElementNode .GetAttribute( attributeName )

なお、引数で指定した名前の属性が存在しない場合は null (空の文字列)が返されます。


属性の設定

属性の設定には、SetAttribute メソッドを使用します。引数は、属性の名称と値で、それぞれ文字列で指定します。

Call notesDOMElementNode .SetAttribute( attributeName , attributeValue )

属性がない場合には、新規で属性が追加され値がセットされます。すでに同名の属性が存在する場合には、値が上書きされます。

なお、値に null(空の文字列)を指定すると、属性が削除されるのではなく、値が空の属性が作成されます。

   Call denItem.SetAttribute("name", "")


属性の削除

属性を削除するには、RemoveAttribute メソッドを使用します。引数は削除したい属性名です。

Call notesDOMElementNode.RemoveAttribute( attributeName )

存在しない属性名を指定した場合、エラーは発生せず、事実上何も実行されません。削除前の存在チェックは不要ですので、使いやすいメソッドですね。


まとめ

今回は NotesDOMElementNode の属性操作に特化してまとめました。DXL のコーディングはなかなかクセがあって思い通りいかないと感じているのですが、属性操作に限って言えば、非常にシンプルで直感的なわかりやすい機能になっています。


DXL Step-by-Step 次回


2025/04/12

全銀仕様で使用できる文字の判定

前回は全角/半角の入力チェックが Nomad で動かず、対策した話をしました。文字コードがノーツクライアント(Windows)と違うことが原因だったのですが、同様の理由でもう一つ問題が発生した機能があります。それが全銀仕様で利用できる文字化チェックする機能です。


利用できる文字と 判定方法

全銀仕様で利用できる文字に関しては、ネットを調べるとすぐにわかります。何のご縁もないのですが但馬信用金庫さんの以下のサイトわかりやすかったので参考にさせていただきました。

全銀仕様データレコード使用可能文字 

半角カタカナが許可されているのですが、ヲ と ー(長音)は除外されています。そして、-(ハイフン)や括弧など一部の記号が使えます。

これまで、文字の判定は Asc 関数で文字コードを取得して判定していましたが、Nomad は、文字コードが違うためそのままでは利用できません。例えば、半角の ア は、Windows では 177、Nomad iOS では 15711665 となります。


サンプルプログラム

文字コードに応じた判定プログラムを作成することは可能かと思います。ただ、実行環境の文字コードを判定したり、利用可能文字の文字コードを調べるのは面倒です。

そこで、今回は使用できる文字が限られていることを利用します。使用可能文字のリストを作成し、チェックする文字がそこに含まれているのか確認する方法で対応します。

サンプルプログラムを以下に紹介します(スクリプトライブラリ内に記述する前提)。


◇ 定数宣言

まず、使用可能文字を定数として宣言します。

Option Declare

Private Const xsZengin_Ei = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Private Const xsZengin_Su = "1234567890"
Private Const xsZengin_Kana = "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン"
Private Const xsZengin_Special = "゙゚\「」()/.- "

◇ 全銀仕様の文字列か判定する関数

引数の文字列をチェックして、全銀仕様の文字で構成されている文字列なら True を返します。使用可能文字を変数 s に集めて、サブ関数 IsSubsetOfChars に渡してチェックしています。

Public Function IsZengin(ByVal vsTarget As String) As Boolean
   Dim s As String

   '許可された文字列
   s = xsZengin_Ei      '全銀半角英字
   s = s & xsZengin_Su     '全銀半角数字
   s = s & xsZengin_Kana      '全銀半角カナ
   s = s & xsZengin_Special      '全銀半角記号

   IsZengin = IsSubsetOfChars(vsTarget, s)
End Function

◇許可文字だけか判定

チェック対象の文字列 vsTarget が、許可された文字 vsAllowed のみで構成されているかどうか判定する関数です。

Public Function IsSubsetOfChars(ByVal vsTarget As String, ByVal vsAllowed As String) As Boolean
   Dim i As Integer
   Dim j As Integer
   Dim s As String
   Dim b As Boolean

   b = True

   '1 文字ずつ取得して許可されている文字か判定
   For i = 1 To Len(vsTarget)
      s = Mid(vsTarget, i, 1)
      j = InStr(vsAllowed, s)
      If j = 0 Then
         '見つからない(= 許可されていない文字)
         b = False
         Exit For
      End If
   Next

   IsSubsetOfChars = b
End Function

許可された文字列に含まれているかの判定は InStr 関数を利用して単純化しています。


まとめ

今回紹介したサンプルでは文字コードを使用した判定を行わず、実際の文字と比較しています。よって、プログラムが実行される OS など、環境に依存しないプログラムにできたと思います。


2025/04/11

入力チェック:全角/半角の判定と Nomad

Nomad は、Notes クライアントで使用していたアプリケーションをそのまま利用できることが特徴で、労せずモバイル対応できてしまう素晴らしいシステムです。

とはいっても、プラットフォームが違うわけですから、再現率は 100% ではありません。画面サイズや縦横比の違い、Excel  に帳票を出力するなど Windows の機能を利用するアプリなど一部に制約があります。

今回紹介する現象は、もっと細かな話になります。私の思慮が足りなかっただけといえばそれまでですが、全く想定していなかったことなので紹介いたします。


発生した現象

一部のアプリケーションで入力された文字の全角/半角チェックをする機能がありました。その判定ロジックは、Len で文字数を取得、LenBP でその文字列のバイト数を取得、文字数 = バイト数なら半角、文字数 x 2 = バイト数なら全角と判定していました。

判定のための関数は次の通りなのですが、これが Nomad で正常に動作しなかったのです。

◇ すべて半角文字かチェック

Public Function IsZenkaku(ByVal vsTarget As String) As Boolean
   Dim iLen As Integer
   Dim iLenBP As Integer

   iLen = Len(vsTarget)
   iLenBP = LenBP(vsTarget)
   If (iLen * 2) = iLenbp Then
      '文字数の 2 倍がバイト数(= すべて全角文字)
      IsZenkaku = True
   Else
      '文字数の 2 倍がバイト数ではない(= 半角文字が混入)
      IsZenkaku = False
   End If
End Function

◇ すべて全角文字かチェック

Public Function IsHankaku(ByVal vsTarget As String) As Boolean
   Dim iLen As Integer
   Dim iLenBP As Integer

   iLen = Len(vsTarget)
   iLenBP = LenBP(vsTarget)

   If iLen = iLenbp Then
      '文字数とバイト数が同じ(= すべての文字が半角)
      IsHankaku = True
   Else
      '文字数とバイト数が違う(= 全角文字が混入)
      IsHankaku = False
   End If
End Function


文字列の長さ

LotusScript で文字列の長さを取得する関数には Len、LenB、LenBP の 3 種類があります。デザイナーヘルプをまとめると次のような機能です。

関数 機能
Len 文字列の文字数
LenB 文字列の長さを示すバイト数
LenBP 文字列の長さを示すバイト数(プラットフォーム固有の文字セット)

簡単なテストフォームを作成し、それぞれの関数の挙動をチェックします。

結果は次の通りでした。この結果すぐに気が付きました。全角や半角カナが 3 バイトとなっているので、Nomad iOS は Unicode で動作していると思われます。

文字 ノーツ(Windows) Nomad iOS
Len LenB LenBP Len LenB LenBP
(全角) 1 2 2 1 2 3
A(半角英字) 1 2 1 1 2 1
(半角カナ) 1 2 1 1 2 3

LenB 関数はどういった機能を提供してくれているのか今一つ理解できませんが、Len 系の関数だけで全角/半角の判定は難しそうです。


対策

◇ すべて半角文字かチェック

半角から全角文字の変更は必ずできます。全ての文字が全角であるかチェックするには、これを利用すれば簡単です。文字列を全角に変換して、元の文字列と一致するか確認するだけです。変化があれば半角文字が混ざっていたということですね。

Public Function IsZenkaku(ByVal vsTarget As String) As Boolean
   Dim sZen As string
   Dim v As Variant

   v = Evaluate(|@Wide("| & vsTarget & |")|)    '半角から全角に変換
   sZen = v(0)

   If sZen = vsTarget Then
      '全角に変換しても変化がないのですべて全角文字
      IsZenkaku = True
   Else
      '全角に変換すると変化したので半角文字が混入
      IsZenkaku = False
   End If
End Function

◇ すべて全角文字かチェック

全て半角であるかについては少し厄介です。漢字など半角に変換できない文字があるからです。まず、全角/半角変換の関係を整理します。

チェックする文字 半角に変換 全角に変換 備考
(全角) (全角) (全角) 半角にできない全角
(全角) A(半角) (全角) 半角にできる全角
(半角) (半角) (全角) 半角にしても変化なし

この関係を利用して関数を作成します。1 文字ずつ上表に当てはめて判定します。1 文字でも全角文字を発見したら、判定を終了し False 返しています。

Public Function IsHankaku(ByVal vsTarget As String) As Boolean
   Dim sSrc As String
   Dim sHan As String
   Dim sZen As String
   Dim v As Variant
   Dim i As Integer

   IsHankaku = True
   '1 文字ずつ確認
   For i = 1 To Len(vsTarget)
      sSrc = Mid(vsTarget, i, 1)
      '全角から半角に変換
      v = Evaluate(|@Narrow("| & sSrc & |")|)
      sHan = v(0)
      '半角から全角に変換
      v = Evaluate(|@Wide("| & sSrc & |")|)
      sZen = v(0)

      If sSrc = sHan And sSrc = sZen Then
         '全角(半角にできない全角文字)
         IsHankaku = False
         Exit For
      ElseIf sSrc <> sHan And sSrc = sZen Then
         '全角(半角に変換できる全角文字)
         IsHankaku = False
         Exit For
      Else
         '半角(全角でなく、半角に変換しても変化しない)
         '→ 処理を継続して次の文字をチェック
      End If
   Next
End Function


おわりに

今回は、全角/半角のチェックを例に Nomad 利用時に発生した課題を紹介しました。

私は長年 Notes クライアント用のアプリばかりを作っています。Web 化や XPages 化も詳しくありません。Windows アプリばかりだったせいもあり、文字コードには無頓着だったということですね。Nomad ≒ Notes クライアントではあるのですが、ところどころ落とし穴があるので注意が必要です。

ところで、紹介したコードでは、全角/半角の変換に Evaluate 関数を介た@関数を利用していますが、これには理由があります。LotusScript の関数 StrConv はノーツクライアントと Nomad 環境で結果が変わり、判定に使用できなかったからです。この点も Nomad 運用時には注意が必要です。

Notes は複数の言語をサポートしており、開発の自由度も高いです。歴史が長いこと幅が広いことで混乱することもありますが、探せば回避策が見つかるところはありがたいといえます。宝さがしみたいで面白いですね。


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 は特定の条件がある場合、とても便利に機能します。

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