2025/02/19

ソートはソートにあらず!?

今回は、Notes/Domino と他のシステムとの間でデータ交換を行うプログラムで経験したトラブルのご紹介です。


システム間の連携仕様

別のシステム内にあるマスタデータをノーツフォームの選択肢として利用したいとの要望があり、マスタデータをテキストファイルでもらい、ノーツ DB 内に文書として保存することにしました。

日々の更新に対応するべく、毎日夜間にマスタデータ全件をファイルを出力してもらい、未明にノーツエージェントで DB に取り込むこととしました。データ件数が多いこともあり、エージェントの処理は、全件削除して全件取り込みするのではなく、差分更新としました。そこで、連携ファイルはソートして出力してほしいと依頼しました。

 

エージェントの仕様

まず、エージェントの処理用にマスタデータをソートしたビューを作成します。そのビューを利用して、次のようなマスタ更新するエージェントを作成しました。

  1. ファイルから最初のマスタデータを取得
  2. ビューから最初の文書を取得
  3. お互いのデータを比較し、以下の処理を行う
    1. 一致する場合、ファイルとビューの双方から次のデータを取得
    2. ノーツにない場合は、ノーツにマスタデータを登録し、ファイルの次のデータを取得
    3. ファイルにない場合は、マスタデータを削除し、ビューから次のデータを取得
  4. ファイルとビュー両方のデータが最後になるまで 3 に戻る

お互いソートされたデータ同士なので順に存在確認すれば、追加 か 削除、更新しないを効率よく判断できるという算段です。


発生した問題点

運用後、エージェントは一見正常に動作していました。ただ、よくよくチェックすると、ファイルに更新がなくても一定数のデータの追加と削除が発生しました。


その原因がソートだったのです。


連携ファイルは他のシステムでソートされ、ビューはノーツでソートされています。同じ ”ソート” でもデータの並び順が違っていたのです。


ノーツだけでも問題は起こる

このようにソートしてもデータの並び順が発生する現象はシステムが違うから発生するとは限りません。ノーツだけでも同様の症状を再現できます。

以前『LotusScript で値のソートを作成』の記事で紹介したソートする関数 xSort を利用したサンプルエージェントを作成します。関数 xSort は割愛(先のリンクを確認ください)。

Option Declare

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

   Dim s As String
   Dim vSrc As Variant
   Dim vSort As Variant
   Dim i As Integer

   s = "Test,Test1,Test_1,Test-1,Test 1,Test2,Test_2,Test-2,Test 2"
   vSrc = Split(s, ",")
   vSort = xSort(vSrc)

   '結果を1件ごとに文書に出力
   Set ndb = ns.CurrentDatabase
   For i = 0 To UBound(vSrc)
      Set nd = ndb.CreateDocument()
      nd.Form = "SortText"
      nd.Text = vSort(i)
      nd.SortNum = i

      Call nd.Save(True, False)
   Next
End Sub

実行するとソート順とその値が文書に保存されます。この文書を表示するビューを作成し、値でソートします。すると、LotusScript と同じソート順とはなりません。


続いて、フォームを使って @Sort をテストします。

Src と Sort の 2 つのフィールド(複数値)を持つフォームを作成します。Sort フィールドは計算結果フィールドに設定し、Src フィールドに入力された値をソートする計算式を設定します。

プリビューして先の LotusScript のテストと同じ値を入力し、計算させます。結果を確認すると LotusScript、ビューのソートのどちらとも一致しません。


まとめ

改めて、LotusScript、ビュー、@Sort の値を比較すると次のようになります。なかなかバラバラな結果となっていますね...

LotusScript ビュー @Sort
Test
Test 1
Test 2
Test_1
Test_2
Test-1
Test-2
Test1
Test2
Test
Test-1
Test-2
Test 1
Test 2
Test1
Test2
Test_1
Test_2
Test
Test 1
Test 2
Test1
Test2
Test-1
Test-2
Test_1
Test_2

Notes の場合にはデータがソートされているかだけでなく、どうやってソートしたのかも重要なようです。特に他のシステムと連携する場合には要注意ですね。

もちろん、このような症状は、数字だけや英字だけの場合には発生しません。今回の事例のように記号やスペースを含む場合に注意が必要です。


2025/02/18

@Do の使い方

先日『@If の使い方』についてまとめました。@If の構文は以下の通りでしたね。

@If(condition1; action1; condition2; action2; ... ; condition99; action99; else_action)

条件に一致した場合に実行される action には、ステートメントを一つしか書けません。ただ、実際にアプリを開発していると、複数のステートメントを書きたくなることがあります。


例えば、@If で紹介した例を拡張して考えます。今回のフィールド構成は次の通りです。

Type と Title フィールドが追加されており、Type の指定に応じて Category 値を変化させる仕様とします。

Type Category
"Title" Title フィールドを表示
未入力の場合は "(無題)" とする
"Category" 入力されたカテゴリまでを " - " でつなぐ
上位のカテゴリが未入力の場合は "(未設定)" と表示
すべて未入力の場合には、"(未設定)" と表示
上記以外 "(未定義のタイプ)" と表示

"Category" 選択時の仕様に変化はないので、その時作成した式を利用したいですよね。

xC1 := @If(Cat1 = ""; "(未設定)"; Cat1);
xC2 := @If(Cat2 = "" & Cat3 != ""; "(未設定)"; Cat2);

@Implode(@Trim(xC1:xC2:Cat3); " - ")


こんな時に効果があるのが @Do です。

@Do を使えば、複数のステートメントを 1 つにまとめることができます。これを利用すると Category フィールドの計算式は次のように記述できます。

@If(
   Type = "Title";
      @If(Title = ""; "(無題)"; Title);
   Type = "Category";
      @Do(
         xC1 := @If(Cat1 = ""; "(未設定)"; Cat1);
         xC2 := @If(Cat2 = ""; @If(Cat3 != ""; "(未設定)"; ""); Cat2);

         @Implode(@Trim(xC1:xC2:Cat3); " - ")

      );
      "(未定義のタイプ)"
)

@If から見ると Type = "Category" の時に実行する action は 1 ステートメントに見えるということですね。


実際アプリを見ていると、@Do を知らないのか、無理やり 1 ステートメントでまとめてある式を見ることがあります。

@If(
   Type = "Title";
      @If(Title = ""; "(無題)"; Title);
   Type = "Category";
      @Implode(@Trim(@If(Cat1 = ""; "(未設定)"; Cat1):@If(Cat2 = ""; @If(Cat3 != ""; "(未設定)"; ""); Cat2):Cat3); " - ");
      "(未定義のタイプ)"
)

この式でも仕様通り動作しますが、非常に分かりにくいですよね。まるで暗号化されたように見えます...。このような記述は、将来この式をメンテする人(自分を含む)を混乱させるだけですので、やめましょう。


2025/02/17

”コボラー” に気をつけろ!?

以前に『変数や関数の宣言とスコープ』をいう記事を投稿しました。

ビジネスでチーム開発を行うのであれば、

  • チーム開発を意識したプログラミングではスコープはできる限り絞った方が良い
  • そうすれば後任者がプログラムの解読時間が短くなる
  • 『動けばいいのではなく後任者にも配慮する』開発者のビジネスマナー

というまとめをさせていただきました。先日、この記事を思い出すプログラムに出くわしたので紹介します。

今回の記事では、COBOL 開発者に触れていますが、COBOL 開発者全体を区別する話ではありません。今回出くわしたプログラムの開発者が『COBOL 開発者だったのではないか?』という想定の下、そのコーディング癖について記載したものです。決して COBOL の言語仕様や開発者全体を揶揄するものではありません。予めご了承ください。


問題のプログラム

生々しいコードをそのまま出すわけにもいかないので、別ライブラリとして抽出してみました。

プログラムの全体は次の通りです。

Private xV01 As Variant
Private xV02 As Variant
Private xV03 As Variant
Private xV04 As Variant
Private xV05 As Variant
Private xV06 As Variant

Public Sub UpdateContorolItems(vndTarget As NotesDocument, vsType As String)
   Dim iYear As Integer
   Dim iMonth As Integer

   iYear = Year(Today())
   iMonth = Month(Today())
   If iMonth < 4 Then
      iYear = iYear - 1
   End If

   ReDim xV01(0)
   ReDim xV02(0)
   ReDim xV03(0)
   ReDim xV04(0)
   ReDim xV05(0)
   ReDim xV06(0)

   If vsType = "1" Then
      xV01(0) = Format(Today(),"yyyy/mm/dd")
      xV02(0) = iYear
      xV03(0) = iYear + 1
      xV04(0) = CStr(iYear + 1) & "/04/01"
      xV05(0) = CStr(iYear + 2) & "/03/31"
   ElseIf vsType = "2" Then
      xV01(0) = Format(Today(),"yyyy/mm/dd")
      xV02(0) = iYear
      xV03(0) = iYear
      xV04(0) = Format(Today(),"yyyy/mm/dd")
      xV05(0) = CStr(iYear + 1) & "/03/31"
   End If
   xV06(0) = vsType

   vndTarget.CrtYMD = xV01
   vndTarget.Year_1 = xV02
   vndTarget.Year_2 = xV03
   vndTarget.DateFrom = xV04
   vndTarget.DateTo = xV05
   vndTarget.AppType = xV06
End Sub

プログラムの機能はさておき、プログラムの構造について感じることはありませんか?


私が気になった点は次の 2 点です。

  1. 変数がライブラリで Private 宣言されていてスコープが広い
  2. フィールドに設定したい値を変数に入れて最後にまとめて文書にセットしている

この癖がコボラーっぽいと感じた理由です。というのも COBOL のプログラムは以下の 4 つに分けて記述します。

  • IDENTIFICATION DIVISION(見出し部)
  • ENVIRONMENT DIVISION(環境部)
  • DATA DIVISION(データ部)
  • PROCEDURE DIVISION(手続き部)

ざっくりいうと DATA DIVISION で入出力ファイルの定義などを行い、PROCEDURE DIVISION にプログラムを書くという感じです。ファイルの定義はデータベースのレコードといっても差し支えないと思いますが、これに相当するのが Private 宣言されている変数たちということです。

COBOL ではファイルに値をセットしてそれをまとめて処理するのが一般的です。UpdateContorolItems 関数のコードを見ると値をいったん Private 変数にセットしてから最後に文書にまとめてセットしています。この書き方が COBOL を連想させるのです。


問題は ”コボラー” ではない

とはいっても、サンプルプログラムがわかりにくい理由は COBOL 的記述の癖が原因だけではありません。ざっと挙げるなら次の通りです。

  • xV01 など変数の役割を想像できない無意味な変数名
  • 単一の値をセットするのになぜか配列化して、要素 0 に値をセット
  • 変数が Variant 型となっており、データ型を意識していない

そして、前述しましたが

  • 変数が Private 宣言されていて、無意味にスコープが広い

という点も問題です。

この開発者の改善点は、2つです。

まず、言語仕様にあった適切なコーディングをすることです。Private 宣言された変数はライブラリ内のどこからでもアクセスできます。値がいつセットされて、どのように使われているかライブラリ全体を理解しないと判断できません。UpdateContorolItems 関数内で宣言すればその影響範囲は関数内だけになります。値を共有しないのであれば関数内で宣言すべきです。また、Variant 型ですべて解決するのではなく、適切なデータ型を使うことも必要ですね(データ型は COBOL にもあります)。

2つ目はコーディングの姿勢です。ビジネスでプログラミングするのであれば、”動けばいい” ではありません。動くのは当たり前で、その先の保守性など将来のことを見据えたコーディングができていないことが問題です。ビジネスでは自分のためではなく、(自分を含む)誰かのためにプログラミングしていることを忘れないようにしましょう。

このような理由から今回の問題は ”コボラー” に起因するのではなく、開発者個人の意識の問題だということがわかりますね。


最後に

今回の記事は COBOL について記載しておりますが、筆者は約 40 年ほど前に、工業高校の授業で文法と実習を1年間実施した経験しかなく、実務経験はありません。当時を思い出しながら記述しましたが、間違いがあった場合はご容赦くださいませ。

また、プログラムの掲載にあたり、スペルミスやローマ字混在の変数名を修正、無用な引数を削除しました。さらに、記事と無関係なコードや他の関数を削除しています。その結果、私が混乱し頭を抱えた状態より、ずいぶん見通しが良いプログラムとなってしまいました。

私が訴えたかったことが、うまく伝わればいいのですが...


2025/02/13

@If の使い方

初中級の Notes/Domino 技術者向けを目指してスタートしたこのブログなのですが、ここ最近の記事は WebAPI や Excel 連携、DXL など、少々複雑なアプリ開発がメインテーマとなっていました。その反省もあり、今後は、@関数の使い方など基本的な機能についても触れていきたいと思います。

今回は、このブログですでに何度も登場している @If についてです。


構文

@If は条件分岐のための命令です。説明するまでもないですね...

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

@If(condition1; action1; condition2; action2; ... ; condition99; action99; else_action)

condition1 が条件で、その条件を満たす場合 action1 が実行されます。条件を満たさない場合は次のステートメントである condition2 の条件が判定されます。これを繰り返し、どの条件も満たさない場合 else_action が実行されます。

condition と action は対に指定する必要があり、最後に else_action が必要となりますので、@If 内のステートメントは必ず 奇数 となります。condition と action の組み合わせは 99 個という制限があります。また、condition2 ~ action99 は省略可能となっており、次のように 3 つのステートメントが最小構成となります。

@If(condition; action; else_action)

LotusScript のように Else を省略できない点に注意が必要ですね。


戻り値

@If は ”関数” なので戻り値があります。

実行した action ステートメントの結果が戻り値として返されます。例えば、以下の式では、フィールド Align の値が "L" の場合 "Left"、 "R" の場合 "Right"、どちらでもない場合は "Unknown" を返します。

@If(Align = "L"; "Left"; Align = "R"; "Right"; "Unknown")

戻り値は変数に代入できます。以下のように記述すると @If の結果が xReturnValue 変数に代入されます。

xReturnValue := @If(Align = "L"; "Left"; Align = "R"; "Right"; "Unknown")


サンプル

例えば、カテゴリ1~3のフィールドがあり、入力に応じて表示用の ”カテゴリ” を作成するとします。入力されたカテゴリまでを " - " でつなぎます。もし上位のカテゴリが未入力の場合は "(未設定)" と表示することを条件とします。もし、すべて未入力の場合には、それがわかるよう "(未設定)" と表示します。

これを実現する式は次のようになります。


xC1 := @If(Cat1 = ""; "(未設定)"; Cat1);
xC2 := @If(Cat2 = "" & Cat3 != ""; "(未設定)"; Cat2);

@Implode(@Trim(xC1:xC2:Cat3); " - ")

カテゴリ1が未入力の場合は "(未設定)" に変換、カテゴリ2が未入力の場合はカテゴリ3が入力されている場合だけ "(未設定)" に変換しています。それぞれの結果は変数 xC1 と xC2 にいったん代入しています。

この結果とカテゴリ3を : 演算子でリスト値として連結、@Trim で空の要素を排除しています。この操作で下位のカテゴリが未入力だとリストから削除されるということですね。

最後に @Implode でリスト値を " - " で連結しています。


カテゴリ2の演算で & 演算子を使用して『カテゴリ2が未入力でカテゴリ3が入力されていたら』という条件を作成しています。この条件は @If 文をネストすることでも実現できます。

xC2 := @If(Cat2 = ""; @If(Cat3 != ""; "(未設定)"; ""); Cat2);

カテゴリ2が未入力の場合に実行するステートメントに @If 文が記述されていて、カテゴリ3が未入力でない場合は "(未設定)" 、入力されている場合は ""(この時点でカテゴリ2は未入力だから)となっています。


@If は整理して書こう

実際のアプリで @If を利用すると条件や実行する式が長くなりがちです。特に条件が多いときは顕著です。だらだらと記述すると condition と action の判別が難しくなり、式が解読しづらくなります。私は @If を記述する場合、よほど単純でない限り、改行とインデントを利用して、記述しています。

@If(
   Align = "L";
      "Left";
   Align = "R";
      "Right";
      "Unknown"
)

@If の後で改行し、condition をインデントして記述、action と else_action はもう一段インデントします。最後につける閉じる括弧は @If とインデントレベルを合わせます。こうすることで @If の範囲と条件、実行される式を明確に分離します。

カスケードされた @If の場合でも同じルールでインデントします。インデントを見ればカスケードレベルも含めて式の役割を明示できます(サンプルの式がシンプルすぎて体感しづらいですが...)。

xC2 := @If(
   Cat2 = "";
      @If(
         Cat3 != "";
            "(未設定)";
            ""
      );
      Cat2
);


2025/02/05

作ってみよう:#31)スマート名刺管理 - リンクでモバイルデバイスと連携

『スマート名刺管理』の最後のネタは、Nomad 用の機能改善です。

取得した名刺情報には、電話番号や住所が含まれます。 これらをリンク化して、電話を発信したり、地図アプリで開いたり、モバイルデバイスの機能と連携します。


作成するリンクと動作

今回、リンク化する項目は、以下の 6 項目です。

項目 動作
会社名 Google で検索
住所 Google Map で開く
電話番号 発信
携帯番号 発信
メール メール作成
URL ブラウザで開く

リンク化する作業手順は次の通りとなります。

  1. 編集用フィールドと表示用フィールドに分離
  2. 表示用フィールドに対して、動作に合わせたリンクを作成


編集用/表示用フィールドの分離

既存のフィールドをコピペしてフィールドを追加、名称を "_Dsp" とします。このフィールドは表示用として利用するので、作成時の計算結果に変更し、値に元のフィールド名を設定します。

続いて、非表示設定のタブを開いて、編集時のみ非表示に設定します(非表示式は元のフィールドのまま)。

元のフィールドは、読み込みモードでは表示されないよう、非表示設定を修正します。

これで、編集時は元のフィールド、読み込み時は今回作成したフィールドが表示されるようになります。


リンクの作成

続いて、表示用のフィールドをリンクに設定します。

フィールドを選択(反転)した状態で、メニューから [作成] - [ホットスポット] - [リンク] を選択します。リンクのプロパティの[@]ボタンをクリックして、リンクの式を入力します。

項目ごとのリンクの式は次の通りです(赤字はフィールド名)。

項目 動作
会社名 "https://www.google.com/search?q=" + @URLEncode("UTF-8"; CompanyName)
住所 "https://local.google.co.jp/maps?q=" + @URLEncode("UTF-8"; Address)
電話番号 "tel:" + Tel
携帯番号 "tel:" + Mobile
メール "mailto:" + eMail
URL URL

会社名と住所は2バイト文字列を含みます。モバイルデバイスの文字コードは UTF-8 なので @URLEncode で変換して URL の文字列に利用しています。

また、電話の発信は、"tel:" で行い、メールの作成は "mailto:" となります。


前回 作ってみよう


2025/02/03

作ってみよう:#30)スマート名刺管理 - インラインイメージを添付ファイルに変換 ②

前回作業を開始した『インラインイメージを添付ファイルに変換』する作業の続きです。当たり前ですが、この作業はもとから添付ファイルとして貼り付けてある場合は不要となります。

当初作成したメイン関数 ReadNameCard において、リッチテキスト内の画像データを取得しているのは、以下の JSON を作成している部分でした。

Public Function ReadNameCard(vnd As NotesDocument) As Boolean
         ・・・
   '1. API リクエスト時に送信する JSON を作成
   Set jnavRequest = xMakeRequest(vnd)

   '2. API をコールし、結果の JSON を取得
   Set jnavResponce = xCallWebAPI(jnavRequest)

   '3. 結果の JSON 内から名刺情報部分だけの JSON を取得
   Set jnavNameCard = xGetNameCard(jnavResponce)

   '4. 名刺情報を文書に保存
   Call xSaveNameCard(jnavNameCard, vnd)
         ・・・
End Function

今回は、この関数から画像がインラインイメージだったのかを返すように修正し、その結果に応じて前回作成の関数をコールするか決定する仕様とします。


xMakeRequest 関数の修正

画像がインラインイメージの場合 True を返す引数 rbPhoto を追加します。実際の判定はサブ関数で行っていますので、その関数に引数をそのまま渡します。

Private Function xMakeRequest(vnd As NotesDocument, rbPhoto As Boolean) As NotesJSONNavigator
   Dim jnav As NotesJSONNavigator

   '送信する JSON(RequestBody)の準備
   Set jnav = xns.CreateJSONNavigator("")

   '1) model
   Call xMakeRequest_Model(jnav)

   '1) messages
   Call xMakeRequest_Message(jnav, vnd, rbPhoto)

   '1) response_format
   Call xMakeRequest_ResponseFormat(jnav)

   Set xMakeRequest = jnav
End Function


xMakeRequest_Message 関数の修正

この関数でも画像貼り付け形式の判定は行っていませんので、先の関数と同様の対応を行います。

Private Function xMakeRequest_Message(vjnav As NotesJSONNavigator, vnd As NotesDocument, rbPhoto As Boolean) As Boolean
         ・・・
   '3) image_url 名刺画像
   Set joCnt = jaCnt.AppendObject()
   Call joCnt.AppendElement("image_url", "type")

   Set jo = joCnt.Appendobject("image_url")
   s = "data:image/jpeg;base64,{" & xGetImage_Base64(vnd, "Body", rbPhoto) & "}"
   Call jo.AppendElement(s, "url")
End Function


xGetImage_Base64 関数の修正

この関数で添付ファイルかインラインイメージの判定を行っています。その結果に応じて戻り値、rbPhoto をセットするように調整します。

Private Function xGetImage_Base64(vnd As NotesDocument, ByVal vsFldName As String, rbPhoto As Boolean) As String
   Dim sTag As String
   rbPhoto = False


   '最初の添付ファイルを Base64 文字列で取得
   xGetImage_Base64 = xGetDXL_FirstAttachment64(vnd, "Body")

   If xGetImage_Base64 = "" Then
      '添付はない(= 最初のインラインイメージを取得)
      xGetImage_Base64 = xGetDXL_FirstInlineImage64(vnd, "Body", sTag)
      rbPhoto = True
   End If
End Function

また、インラインイメージの場合 xGetDXL_FirstInlineImage64 をコールしています。前回の記事で、この関数は画像形式を返す機能が必要となっていました。xGetImage_Base64 関数も、この新要件に合わせて、引数 sTag を追加します。


xGetDXL_FirstInlineImage64 関数の修正

やっと、前回から発生した新要件の回収です。関数の引数に画像形式を返す rsTag を追加します。

この関数は、リッチテキスト内のインラインイメージの画像データを Base64 の文字列を返す仕様でした。画像データの上位ノード名は、png、jpeg、gif となっており、この値はそのまま画像ファイルの拡張子として使用できます。以下のコードでは、sTag 変数にセットされているので、それを戻り値として返すように修正します。

Private Function xGetDXL_FirstInlineImage64(vnd As NotesDocument, ByVal vsFld As String, rsTag As String) As String
         ・・・
   '画像データの捜索
   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

            '戻り値セット
            xGetDXL_FirstInlineImage64 = sB64
            rsTag = sTag
            Exit ForAll
         End If
      End ForAll
   End If
End Function


ReadNameCard 関数の修正

いよいよメイン関数の修正です。

リクエストする JSON 作成時に名刺画像の貼り付け状態を取得する変数 bPhoto を新たに定義し、引数に追加します。この変数が True の場合、インラインイメージを添付ファイルに変換が必要となります。ReadNameCard 関数の最後で、前回作成の関数 xConvPhoto2Attach をコールさせています。

Public Function ReadNameCard(vnd As NotesDocument) As Boolean
   Dim jnavRequest As NotesJSONNavigator
   Dim jnavResponce As NotesJSONNavigator
   Dim jnavNameCard As NotesJSONNavigator
   Dim bPhoto As Boolean

   '1. API リクエスト時に送信する JSON を作成
   Set jnavRequest = xMakeRequest(vnd, bPhoto)
   'Call xSetRT(vnd, "JSON_Request", jnavRequest.Stringify)

   '2. API をコールし、結果の JSON を取得
   Set jnavResponce = xCallWebAPI(jnavRequest)
   Call xSetRT(vnd, "JSON_Responce", jnavResponce.Stringify)

   '3. 結果の JSON 内から名刺情報部分だけの JSON を取得
   Set jnavNameCard = xGetNameCard(jnavResponce)
   Call xSetRT(vnd, "JSON_NameCard", jnavNameCard.Stringify)

   '4. 名刺情報を文書に保存
   Call xSaveNameCard(jnavNameCard, vnd)

   '名刺管理フィールドセット
   vnd.ExchangeDate = Today
   vnd.Status = "3"
'3 = AI 問い合わせ完了

   '文書の保存
   Call vnd.Save(True, False)

   '5. 写真の場合、添付ファイルに変換
   If bPhoto Then
      'DXL でインラインイメージを添付ファイルに変換
      'DXL での文書保存、バックエンド文書再取得を含む

      Call xConvPhoto2Attach(vnd, "Body")
   End If
End Function

また、ノーツクライアント用と Nomad 用のエージェントで行っていた名刺管理フィールドのセットと文書の保存を ReadNameCard 関数移設します。

一連の処理で保存が複数回発生するのは構造上よくないと考えています。今回は、NotesDocument クラスの保存と DXL の保存を併用することから、複数回の保存が発生します。その状況をできる限りわかりやすくなるよう、保存処理を近くに配置してみました。


ReadNameCard エージェントの修正

最後に名刺読込処理エージェントの調整を行います。まずはノーツクライアント用である ReadNameCard エージェントです。

ReadNameCard 関数内に移設した処理をコメントアウト、もしくは、削除します(以下の赤字部分)。

Sub Initialize
         ・・・
   'AI で名刺を読み込み
   Call ReadNameCard(nd)

   '名刺管理フィールドセット
   'nd.ExchangeDate = Today
   'nd.Status = "3" '3 = AI 問い合わせ完了

   '文書の保存と画面の表示
   'Call nd.Save(True, False)

   Set nuid = nuiw.Editdocument(True, nd)
End Sub


ReadNameCard_OnServer エージェントの修正

続いて、Nomad 用のエージェントについても、同様の対応を行います。以下の赤字部分をコメントアウト、あるいは、削除します。

Sub Initialize
   Dim na As NotesAgent
   Dim nd As NotesDocument

   Set xns = New NotesSession
   Set xndb = xns.Currentdatabase
   Set na = xns.CurrentAgent

   '呼び出し元の文書を取得
   Set nd = xGetPramDoc(na)

   'AI で名刺を読み込み
   Call ReadNameCard(nd)

   '名刺管理管理フィールドセット
   'nd.ExchangeDate = Today
   'nd.Status = "3"

   '文書の保存(画面の表示は呼び出し元が担当)
   'Call nd.Save(True, False)

End Sub


動作検証

これで改造は完了です。

成功すると、カメラから貼り付けたインラインイメージの名刺画像は、[名刺読込]が完了すると添付ファイルに変換されます。

そして、この変換効果で[登録情報]タブの参照で、横スクロールは発生しなくなります。


前回 作ってみよう 次回


2025/02/02

作ってみよう:#29)スマート名刺管理 - インラインイメージを添付ファイルに変換 ①

リッチテキスト内の名刺画像が写真(= インラインイメージ)の場合、画像テータを添付ファイルに変換します。インラインイメージの操作は、通常の NotesRichText* クラスでは実現できません。そこで、今回も DXL を利用します。


インラインイメージを添付ファイルに変換

まずはこの処理のメイン関数 xConvPhoto2Attach を紹介します。

この関数はリッチテキスト内のインラインイメージを添付ファイルに変換する関数ですが、次の手順で処理を行います。

  1. リッチテキスト内からインラインイメージデータ(Base64)を文字列で取得
  2. リッチテキスト内のイメージデータを削除
  3. リッチテキストに添付ファイル(の参照)を作成
  4. 添付ファイルの実体を文書に追加
  5. 文書を保存(DXL 経由)

各手順はサブ関数により実現しています。

Private Function xConvPhoto2Attach(rndNameCard As NotesDocument, ByVal vsFld As String) As Boolean
   Dim sBase64 As String
   Dim sID As String
   Dim sTag As String
   Dim sName As String

   '名刺情報が保存された文書を取得(再取得)
   sID = rndNameCard.Noteid
   Set rndNameCard = Nothing '一旦文書開放
   Set rndNameCard = xndb.GetDocumentByID(sID)

   '① インラインイメージデータ(Base64)取得
   sBase64 = xGetDXL_FirstInlineImage64(rndNameCard, vsFld, sTag)
   sName = "NameCard." & sTag 'ファイル名

   'DXL の準備
   Dim dprs As NotesDOMParser
   Set dprs = xGetDOMParser(rndNameCard)

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

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

   'イメージデータのある段落を取得
   Dim denPar As NotesDOMElementNode
   Set denPar = xGetDXL_FirstNodeByName(denRT, "par")

   'インラインイメージを添付ファイルに変換
   '② 既存のコンテンツ(=インラインイメージ)を削除

   Call xConvP2A_DelImage(denPar)

   '③ 添付ファイル(=実体に対する参照)を追加
   Call xConvP2A_AddAttachRef(ddn, denPar, sName)

   '④ 添付ファイルの実体を追加
   Call xConvP2A_AddAttach(ddn, sName, sBase64)

   '⑤ 文書の保存(DXL 経由)
   If xSaveDXL_Update(dprs) Then
      '更新した文書を再取得
      sID = rndNameCard.Noteid
      Set rndNameCard = Nothing '一旦文書開放
      Set rndNameCard = xndb.GetDocumentByID(sID)
   End If
End Function

① のインラインイメージデータ(Base64)取得については既存の関数 xGetDXL_FirstInlineImage64 を利用しています。ただ、今回の処理では添付ファイルの拡張子を決定するために画像の形式が必要となることから、引数 sTag を追加しています(関数側の修正については次回)。


② 既存のコンテンツ(=インラインイメージ)を削除

添付ファイルとして保存する Base64 の文字列は ① で取得しました。よって、リッチテキスト内のインラインイメージは不要になります。そこで、リッチテキストから完全に削除します。 DXL で言うと、以下の赤枠の部分を削除することになります。

関数は次の通りです。引数には、インラインイメージが存在する段落 par ノードを指定します。処理は単純で、配下のノードがなくなるまで、順に消去しているだけです。

Private Function xConvP2A_DelImage(vdenPar As NotesDOMElementNode) As Boolean
   '配下ノード全削除で対応
   Dim dn As NotesDOMNode
   Set dn = vdenPar.FirstChild
   While Not(dn Is Nothing)
      If dn.IsNull Then
         Set dn = Nothing
      Else
         Call vdenPar.RemoveChild(dn)
         Set dn = vdenPar.FirstChild
      End If
   Wend
End Function


③ 添付ファイル(=実体に対する参照)を追加

削除した写真と同じ場所に添付ファイルを配置します。リッチテキスト内には、添付ファイルの実体ではなく、その参照(attachmentref ノード)を配置します。作成する DXL は下図のような構成となります。

この DXL を生成する関数は次の通りです。

Private Function xConvP2A_AddAttachRef(vddn As NotesDOMDocumentNode, vdenPar As NotesDOMElementNode, ByVal vsName As String) As Boolean
   Dim den As NotesDOMElementNode
   Dim dtn As NotesDOMTextNode

   'attachmentref ノード
   Dim denRef As NotesDOMElementNode
   Set den = vddn.CreateElementNode("attachmentref")
   Set denRef = vdenPar.Appendchild(den)

   Call denRef.SetAttribute("displayname", vsName)
   Call denRef.SetAttribute("name", vsName)

   'picture ノード
   Dim denPic As NotesDOMElementNode
   Set den = vddn.CreateElementNode("picture")
   Set denPic = denRef.Appendchild(den)

   Call denPic.SetAttribute("align", "baseline")
   Call denPic.SetAttribute("width", "76px")
   Call denPic.SetAttribute("height", "96px")

   'png ノード
   Dim denPNG As NotesDOMElementNode
   Set den = vddn.CreateElementNode("png")
   Set denPNG = denPic.AppendChild(den)

   'アイコン画像データ
   Set dtn = vddn.CreateTextNode(xcsIconImage)
   Call denPNG.AppendChild(dtn)

   'caption
   Dim denCap As NotesDOMElementNode
   Set den = vddn.CreateElementNode("caption")
   Set denCap = denPic.AppendChild(den)

   Call denCap.SetAttribute("position", "below")

   'caption 文字列
   Set dtn = vddn.CreateTextNode(vsName)
   Call denCap.AppendChild(dtn)
End Function

attachmentref ノードの役割として、リッチテキストに表示する添付ファイルアイコンの管理があります。今回は少し見栄えが良くなるよう、少し大きめの 76 x 96 ピクセルの PNG 画像をアイコンとします。アイコンの画像は、あらかじめ Base64 で変換し文字列定数 xcsIconImage に定義しています(後述)。

添付ファイルに表示されるファイル名は、画像の見出しと同じ機能を使用しています。上記関数では caption ノードを作成し、ファイル名をテキストノードにセット、見出しを ”イメージの下” に設定し、通常の添付ファイルと同等に表現しています。


④ 添付ファイルの実体を追加

添付ファイルは文書内で $FILE という特殊なフィールド内に格納されています。フィールドですので、ドキュメントノードに追加することとなります。作成する DXL は下図のようになります。

この処理を担当しているのが xConvP2A_AddAttach 関数です。

Private Function xConvP2A_AddAttach(vddn As NotesDOMDocumentNode, ByVal vsName As String, ByVal vsBase64 As String) As Boolean
   Dim den As NotesDOMElementNode
   Dim dtn As NotesDOMTextNode

   '$FILE フィールドとして追加(=ドキュメントノードに追加)
   Dim denDoc As NotesDOMElementNode
   Set denDoc = vddn.DocumentElement

   'item ノード
   Dim denItem As NotesDOMElementNode
   Set den = vddn.CreateElementNode("item")
   Set denItem = denDoc.AppendChild(den)

   Call denItem.SetAttribute("name", "$FILE")
   Call denItem.SetAttribute("seal", "true")
   Call denItem.SetAttribute("sign", "true")
   Call denItem.SetAttribute("sealed", "false")
   Call denItem.SetAttribute("summary", "true")
   Call denItem.SetAttribute("placeholder", "false")

   'object ノード
   Dim denObj As NotesDOMElementNode
   Set den = vddn.CreateElementNode("object")
   Set denObj = denItem.AppendChild(den)

   'file ノード
   Dim denFile As NotesDOMElementNode
   Set den = vddn.CreateElementNode("file")
   Set denFile = denObj.AppendChild(den)

   Call denFile.SetAttribute("name", vsName)
   Call denFile.SetAttribute("flags", "storedindoc")
   Call denFile.SetAttribute("encoding", "none")
   Call denFile.SetAttribute("hosttype", "msdos")
   Call denFile.SetAttribute("compression", "none")

   'created ノード
   Dim denDT As NotesDOMElementNode
   Set den = vddn.CreateElementNode("created")
   Set denDT = denFile.AppendChild(den)
   Set den = vddn.CreateElementNode("datetime")
   Set denDT = denDT.AppendChild(den)
   Call denDT.SetAttribute("dst", "false")
   Set dtn = vddn.CreateTextNode(xDateTime2ISO(Now))
   Call denDT.AppendChild(dtn)

   'modified ノード
   Set den = vddn.CreateElementNode("modified")
   Set denDT = denFile.AppendChild(den)
   Set den = vddn.CreateElementNode("datetime")
   Set denDT = denDT.AppendChild(den)
   Call denDT.SetAttribute("dst", "false")
   Set dtn = vddn.CreateTextNode(xDateTime2ISO(Now))
   Call denDT.AppendChild(dtn)

   'filedata ノード
   Dim denData As NotesDOMElementNode
   Set den = vddn.CreateElementNode("filedata")
   Set denData = denFile.AppendChild(den)

   'ファイルの実体
   Set dtn = vddn.CreateTextNode(vsBase64)
   Call denData.AppendChild(dtn)
End Function


⑤ 文書の保存(DXL 経由)

手順の最後は DXL 経由で文書を保存する関数です。今回はすでに存在する文書の更新しか行いませんので、専用の関数 xSaveDXL_Update を作成しました。

Private Function xSaveDXL_Update(vdprs As NotesDOMParser) As Boolean
   Dim dimp As NotesDXLImporter
   Dim nsOut As NotesStream
   Dim nd As NotesDocument
   Dim nrti As NotesRichTextItem

   Set nsOut = xns.CreateStream()
   Call vdprs.SetOutput(nsOut)
   Call vdprs.Serialize()

   On Error GoTo ErrProc

   'Import時リッチテキストを引数にするのが一番安定
   Set nd = xndb.CreateDocument
   Set nrti = nd.CreateRichTextItem("Body")
   Call nrti.AppendText(nsOut.ReadText)

   Set dimp = xns.CreateDXLImporter()
   dimp.DocumentImportOption = 5 '上書き保存
   Call dimp.Import(nrti, xndb)

   xSaveDXL_Update = True

ExitProc:
   Exit Function

ErrProc:
   xSaveDXL_Update = False
   MsgBox Error$
   Resume ExitProc
End Function

DocumentImportOption プロパティに 5 を指定している点がポイントですね。


関連関数

上記処理 ④ で添付ファイルの作成日(created ノード)と更新日(modified ノード)に現在時刻を指定している処理があります。DXL で日時を指定するためには、特別なフォーマット(ISO 8601)に変換する必要があります。このフォーマットについては過去の記事『DXL や JSON の日付値の変換』を参考にしてください。

今回は日時を文字列に変換する一方通行であること、例外処理は不要であることから、かなりの部分を固定化した単純な関数を作成しました。

Private Function xDateTime2ISO(vvDT As Variant) As String
   xDateTime2ISO = Format(vvDT, "yyyymmdd") & "T" & Format(vvDT, "hhnnss") & ",00+09"
End Function


添付ファイルのアイコン画像

③ の処理でアイコン画像にセットしている文字列を定義します。以下の画像ファイルを Base64 に変換したもので、あまりに長いので適当な位置で改行しています。

以下の宣言をスクリプトライブラリ lsReadNameCard の (Declarations) に追加します。

'変換後のアイコン画像(PNG 形式 & Base64 変換済み)
Private Const xcsIconImage = _
"iVBORw0KGgoAAAANSUhEUgAAAEwAAABgCAYAAAC3+ZRmAAAAAXNSR0IArs4c6QAAAARnQU1BAACx" & _
"jwv8YQUAAAAJcEhZcwAADsEAAA7BAbiRa+0AAAXySURBVHhe7ZqxbuNGFEXfBgkgsglAGW7EdBJs" & _
"F1IVgJVT5TeSD0i9CwRIkRQBAmzqfEDyG6kCAwGEdFZhG1JJNl57SpJFEOfe4ciWtJKsoUYAJc0B" & _
"ZJFvyVnq8r7heyTF4/F4PB7PCt6Y74+4urp6Mot7w8XFxRcnJyepWd0JawVLksSsNZ/hcKi/z87O" & _
"/j09Pf1Mr+yAT8z3QXB+fi53d3ef3t/f/2dCzjkowYgR7Q1EezAhpxycYMSI1oZo/5iQMw5SMGJE" & _
"+xKi3ZqQEw5WMGJEO/vw4cNbE9qagxaMULTb29tf4bQfTGgrDl4wYpz2M0T7zYRqcxSCESPadw8P" & _
"D1+bUC2ORjBC0W5ubv6E074yIWuOSjBinPYXLgTfmJAVRycYMReCP5Ce35rQxhylYMSk5+9Izx9N" & _
"aCOOVjBi0vMniPbehF7lqAUjRrR3mNO+N6G1HL1gxMxpv0C0dya0Ei+YwYj2Hun5twkt5eBuILrg" & _
"8vJypS4HI5grKPw6wXxKWuIFs8QLZokXzBIvmCVeMEu8YJZ4wSzxglniBbPEC2aJF8wSL5glXjBL" & _
"vGCWeMEs8YJZ4gWzxEowhc9wONa3cfnJxiNECh3PRmO9fOhYOyzCJ0l6MkgSSaUDFeu95T0aQ+Yi" & _
"M2v7Q+2UDPAJw0CKotTrOT4Ugc5TxnlknBWIjXS8yBjHdojlCk69Tp9jy7ZrIrUFoxx5XkgQtKoA" & _
"FOvHgXZeVsKHhdKpKmkqyaAjPToy5bZK+p1AwqiHeCxBp79yuyZiLRh/BuexazghFqRUFOt4GOJP" & _
"QN+9UEDVMIQDEWcql1G7Ci6w6XZNoPYcxmeWnV4fa/MiHTq1U3ITaLg8R3rBLXRmSz1+5EKy6XZN" & _
"YKeC0Y0Sx5jcMxkzhWNeIHQUORg8T/prt2sY/lWBBXiV9q8KOMQLZkktwfa1SneBtWD7XKW7wFqw" & _
"fa7SXbD1HLZPVboL/KRvydaC7VOV7oJ6gu1ple4CX+kv4Ct9x3jBLPGCWVJLMHZGI+T6LtojXmmb" & _
"/ATKWjD+jDLjrelICjVf0S/2mE3uOesem/VVEm2jyAT9YreL74l0+viWoOox02u9zSAOZSLduXUV" & _
"9SW9Zp+ZC1ax3wDLge5DVTrRcW5XoN16hMPKVoieNZVeFErU68LVqO0eR8JzhH+RFkoZlSq93Eeb" & _
"VgQdmUAE9rnT8RXG51j6tObqeawRdJoe2+JvdH6VzCEYzIXaNKgKVpM6iz3m4jpWcXB9fYBpq4fc" & _
"S6sfYvpQxrmdZskTKBKWObbrSquXSJhjPyxLPNBOh3bSkWxufA3GSrrtubFmj80Wa8HUI85b0NKl" & _
"qWLfyMp+A+jMIc42z6DABWS2D51l2RMoUj3Rq+LTv62Z7cYqnxufrBqrLlaC6fMMgYaw/vTApg9y" & _
"X0OpQgZRqR0QwhW7gCnN8flB7pmoW6wEoyNiOCJJBvqgIqRGkfOZ9+YwgfkAmPDET/tQFyg4rPof" & _
"doeVYPydQYsHVVkcLaUUJT1vDnKmx1xcj6JAsCgTODMsqzTWHafpQ+nYbW4+xhhMtTDW8Hqzkscc" & _
"my2+l1zA+VXy2PGCWeIFs8SJYKzy1RgT7RpYksy+vTgevUzM3H864fPyMWQpjpKF+zStr3QiGCtn" & _
"lhivwavi9M0f6Qwkm/AqVUgH+6eqetrEij0WxM1rVE3DSjD2q1Mn8ZyP6AS4pOpjR1U1b55PZnTQ" & _
"GmfEKDMUpGGrQiFLCJShqVfjMQRkxF117hIrwWbrLhTu+KFQKtDVlGZZv7gpXe2yCIUxxmyou4iV" & _
"YM+VOQRbLGLJsn5xU3gCeEeh6VgJRi9NG+4S7UzUfnEX2aZfTDF59aJcO7PIeLunmVhP+hQpe1TS" & _
"4o0pircEbRbTL64ihbgRJ3ekNJ0ZqQwXjh46JUR1JjfTbdaCsWdLy1jaLf6qeYct6xdnoQTT0kKy" & _
"a+l0OVdh8od4bbiL43EeVBHf/6/2TxFmf8h9tuk1XeF7yQV4Ynwv6RAvmCVrU9IsHh3rUnIlT09P" & _
"n5tFj8fj8XiOEZH/AW6rKKt4M8mjAAAAAElFTkSuQmCC"


次回の予告

ここまで対応するとスクリプトライブラリのエラーは次の 1 ヶ所となっているはずです。

今回の最初に記載した通り、作成済みの関数 xGetDXL_FirstInlineImage64 の引数 sTag が増えているからです。次回はこの関数の修正から作業を開始します。

Private Function xConvPhoto2Attach(rndNameCard As NotesDocument, ByVal vsFld As String) As Boolean
   Dim sBase64 As String
   Dim sID As String
   Dim sTag As String
   Dim sName As String

   '名刺情報が保存された文書を取得(再取得)
   sID = rndNameCard.Noteid
   Set rndNameCard = Nothing '一旦文書開放
   Set rndNameCard = xndb.GetDocumentByID(sID)

   'Base64 イメージデータ取得
   sBase64 = xGetDXL_FirstInlineImage64(rndNameCard, vsFld, sTag)
   sName = "NameCard." & sTag 'ファイル名

   'DXL の準備


前回 作ってみよう 次回


2025/02/01

作ってみよう:#28)スマート名刺管理 - モノ言う読者の追加要望

『スマート名刺管理』の作成は、前回で終了の予定だったのですが、少しだけ追記します。

といいますのも、このブログでたびたび登場するとあるヘビーユーザ(モノ言う読者?)からご意見をいただきました。『iPhone SE では、画面が狭く全項目が表示されない』『一切スクロールしないので参照できない項目がある』『縦スクロールだけ利用できるようにしてほしい』というご要望です。

このご要望に対応すべく、『スマート名刺管理』を改造します。


縦スクロールの対応

現時点で、名刺情報のフォームがスクロールしないのは、前回作成したフレームセット mfsNameCard でスクロールを止めたことが原因です。

まずは、この設定を ”自動” に変更し、スクロールを必要に応じて動作するようにします。

スクロールを許可するなら、画面構成に余裕が出ます。そこで、名刺情報フォームにもタイトルを追加します。mfsMain フレームセットに合わせて、上下 2 フレームにし、上のフレームにタイトルページを追加します。

フレーム 名前 内容 補足
mfrmTitle pTitle ページ このフレームを追加
mfsNameCard (指定なし) 元からあったフレーム

ただ、これだと左右にもスクロールしてしまいます。これを止めるために、フォームを細工します。

手順は次の通りです。

  1. 1 行 1 列の表を作成
  2. 幅をウィンドウに合わせる設定にする
  3. 罫線を 0 に設定
  4. 元々あったタブ表をこの表の中に移動

なお、今回作成した外側の表のプロパティで、行/列の間隔を設定しないことがポイントです。

なぜ、この作業で横スクロールが抑制できるのかは不明瞭です。そのため、将来のバージョンで挙動が変わる可能性はあるかもしれません。その点、ご了承ください。


動作確認

上記対応が終われば、必要な場合のみ、縦にスクロールするようになります。


ただし、これで完成ではありません。

例えば、名刺画像をカメラから入力すると横スクロールが発生します。これは、カメラからの画像幅が大きく、[名刺画像]のタブで横スクロールが必要となり、これにつられて[登録情報]タブでもスクロールするためです。

この問題の回避策として、インラインで貼り付けられた画像を添付ファイルに自動変換する機能を付けます。添付ファイルであれば、[名刺画像]のタブでスクロールが発生しなくなるという算段ですね。

この処理については、次回詳しく紹介します。


前回 作ってみよう 次回


2025/01/14

作ってみよう:#27)スマート名刺管理 - Nomad 用フレームセットの作成

前回までで Nomad 用の部品が完成したので、これらを組み合わせてアプリとして仕上げます。


メインフレームセットの作成

まず、Nomad 用アプリのメイン画面となるフレームセットを作成します。上下 2 分割とし、上にヘッダーとしてタイトルを表示します。


◇ フレームセットの設定

名前 mfsMain
タイトル @DbTitle


◇ フレームの設定

フレーム 名前 内容 補足
mfrmTitle pTitle ページ
mfrmForm m02.会社名別 フォーム

作成した各フレームのスクロールは ”オフ” にしておきます。


フォーム用のフレームセット作成

Nomad でフォームを開くと上下だけでなく、左右にもスクロールすることがあります。少々使いづらいので、フレームセットを使ってスクロールを止める設定を行います。

1 フレームだけのフレームセットを新規で作成します。


◇ フレームセットの設定

名前 mfsNameCard
タイトル @DbTitle


◇ フレームの設定

フレーム 名前 内容 補足

mfrmForm (設定不要) スクロールは ”オフ” 


Nomad 用フォームの修正

フォームを開くと、先ほど作成したフレームセット内に表示されるように設定します。

m01.名刺管理 フォームのプロパティ、[起動]タブの自動フレームを以下のように設定して保存してください。


UI 切り替えフレームセットの作成

今回 Nomad 用のフレームセット mfsMain を追加しました。ノーツクライアント用のフレームセットも存在します。ということは、アプリを開いたクライアントに応じて、開くフレームセットを切り替える必要があります。

この連載で前回のアプリ「お小遣い帳」を作成したときには、初期表示するページが開くタイミングで LotusScript を使って切り替える方法を紹介しました(#11)お小遣い帳 - Nomad とノーツクライアントの併用)。この記事に対して Miyo HCL Ambassador から『フレームセットの計算式でもできる』とステキな助言をいただきました。そこで、今回はこの方法にチャレンジします(Miyo-san の意図通りの方法かは確認はできていませんが...)。


新規で 1 フレームだけのフレームセットを作成し、フレームの内容を式で指定します。


◇ フレームセットの設定

名前 fsUISwitcher
タイトル @DbTitle


◇ フレームの設定

名前 frmSwitcher
内容 - 種類 名前付き設計要素
フレームセット
値(計算式) @If(@Contains(@Platform([Specific]); "iOS":"Android"); "mfsMain"; "fsMain")


起動画面の設定

最後にデータベースのプロパティを開き、起動画面を fsUISwitcher フレームセットに変更します。

これで、作業は完了です。


動作検証

Nomad から開いて、動作検証をします。以下の動画は、このシリーズの最初に添付したものですが、こんな感じで動作していたら成功です。

添付ファイルだけでなく、名刺の写真を撮影して画像で貼り付けたり、カメラロールから添付したり、さまざまな方法で試してみましょう。


トラブルの対処

最後に、今回のアプリの Nomad 実行時に起こりそうなトラブルについてまとめます。


◇ エージェントが実行されない

[名刺読込]ボタンを押してもエージェントが実行されないことがあります。これは、エージェントのプロパティのセキュリティレベルの設定で解決します。下図の通り 2 または 3 に設定しましょう。


◇ RunOnServer 実行時のエラー

[名刺読込]ボタンでは別のエラーが出ることがあります。

このエラーはエージェントの署名者(設計要素の最終更新者)がサーバでエージェントの実行権限がない場合に発生します。

対応には、サーバ文書のセキュリティ設定が必要です。サーバ管理者に連絡して権限を変更してもらうか、権限があるユーザ ID でエージェントを署名しましょう。


おわりに

『スマート名刺管理』いかがでしたか?

Nomad モバイルとノーツクライアントとのハイブリッドアプリを WebAPI 連係(GPT4o API)、DXL などノーツアプリ開発のさまざまな機能を組み合わせて実現しました。守備範囲が広く、自由度が高く、懐が深いところが Notes/Domino の魅力ですよね。

紹介したアプリは名刺管理としては基本機能しかありません。自社の要件に応じて、カスタマイズして、活用いただけたらと思います。

なお、今回の記事では、作成者フィールドを作成していません。よって、一般利用者の ACL は編集者以上となります。他のユーザの名刺情報の取り扱いについて制限したい場合には、作成者フィールドや読者フィールドを活用ください。

前回 作ってみよう 次回