出直し!! ヘルプ

連載中

連載 終了

2023/12/31

Domino v14 Factory Tour レポートと HCLSoftware へのお願い

先日 Notes/Domino の最新バージョン v14 がリリースされました。それに合わせて、リリースイベント『Domino v14 Factory Tour』が 2023 年 12 月 13 日に ザ・リッツ・カールトン東京 で開催されました。


数年ぶりのリアルイベント。そして会場はリッツ・カールトン! お弁当付きで、〆は立食パーティ!! と Notes/Domino 再起動を感じさせる素晴らしいイベントでした。

長年ノーツ屋をやっている私としては、ノーツバブルだった 2000 年代前半までのイベントを彷彿させる内容でした。当時は、あまりにも来場者が多くてお弁当やパーティなどはできるような状況ではなかったので、今回のイベントの方が個人的には評価が高いですね(笑)

このイベントに携わったみなさま、お世話になりありがとうございました。次回も楽しみにしていますので、どうぞよろしくお願いいたします。弁当をせがんでいるつもりはありませんので誤解なきようお願いいたします...


ただ、このイベントに関連して気になった点があります。

このイベントの開催案内は以下のページで行われました。メディア協力が "ビジネス+IT" で、参加申し込みをするためにこの ビジネス+IT に申し込みを行う必要がありました。

2023年12月13日、Domino v14 Factory Tour を開催します


メディア系のサイトですので、登録に伴いメルマガが届くようになります。2週間くらいたって、ビジネス+IT から配信されて来たメールに次の記事が掲載されていました。
Factory Tour の約1週間前です。

太平洋セメントが「脱Notes」できた理由、難易度“超高い”5年間におよぶ大移行計画の詳細

この記事には『2024年のサポート終了に伴い、企業の脱Notesが進んでいる。』とあり、あたかも Notes この世から消えるような表現になっています。サポートが終わるのは古いバージョン 9 と 10 だけで、11 や 12 はもちろんサポートを継続することはみなさんご存じですよね。

気になったのは、最新バージョンの 14 のリリースイベントに協力しているメディアがそのイベントの直前に泥を塗るような記事を配信する。新着記事ならまだしも 2023/08/17 掲載の記事をぶつけてくるあたり、悪意すら感じます...


こんなデマに乗せられる方も問題ですが、私が気になったのは HCLSoftware のスタンスです。

私はただのノーツ屋、ただの一兵卒ですが、それでも ”なめられとんちゃうか !? ” と感じました。HCLSoftware のみなさまはどのように感じているのでしょうか?

不安をあおる形の営業活動がスタイルの企業、クライアントの意向に盲従し、過剰に忖度するメディアがあることは知っています。最近の世情を鑑みると、仕方がないことかもしれません。個別に対応してつぶすほどの価値は感じません。ただ、HCLSoftware には、このような悪意を感じる記事をかき消すぐらい、Notes/Domino の広告記事を連投してほしいと感じます。


そして Factory Tour のセッションの中で何度もふれられていた脱ノーツの行き詰まりに関するネタも気になりました。現時点で Notes/Domino のイベントに来る方は、Notes/Domino のことを十分に知っていて、移行が難しいことも理解している方が多いと思います。困っているのは、無理筋ともいえる移行を上層部から強要されていることのはずです。

だからこそ、『脱ノーツの行き詰まり』のような話は、内向きなイベントで語るのではなく、外向きに発信すべきと感じました。特にデジタルに疎いくせに物事を 0 か 1 で判断するアナログな上層部にも届くようなメディアで、どんどん発信してほしいと思います。


今回の Factory Tour では、カメラマンが絶えず写真を撮っていました。

てっきり『Notes/Domino v14 リリース! ノーツ復権の兆し!!』とか、『進化と深化! 知られざる Notes/Domino の真価!!』みたいな広告記事がイベントの写真とともにネットをにぎわせるのではないかと思っていました。ですが、私の知る限りレポート記事は以下の記事だけでした。

Domino v14 Factory Tour へのご来場をありがとうございました

あっさりしていますね...。

ノーツコンソーシアム大阪研でお世話になっているメンバーは、『弾幕うすいよ!何やってんの !! 』と言っていました。的確な表現ですね。まさにその通りです!


Notes/Domino は、機能、守備範囲、開発効率や費用をふくめ、十二分に競争力のある製品だと思います。最近はやりのコスパ / タイパ、ローコード / ノーコードにも対応できる戦闘力があると感じています。過小評価されている Notes/Domino を取り巻く現在の環境を HCLSoftware には打破してほしいですね。

そのためにも、もっともっと強力に情報発信して、アグレッシブで貪欲になってほしいです。

そうすれば、ネットで Notes/Domino を検索しても、サポート終了のデマが上位に出ることも減るでしょう。そして、私のような古株ノーツ屋がモチベをそがれる呪文『まだノーツやってんの??』の攻撃にさらされることも減るはずです。


2023 年最後の日にグダグダ愚痴ってしまいました。

HCLSoftware さんにとって失礼な発言もあったかもしれません。すべては、Notes / Domino を憂いてのことですので、どうぞご容赦ください。そして少しでも好転するようがんばっていただけるとありがたいです。私も微力ながら援護射撃させていただきます。


『来る 2024 年は前のめりな ”新生 HCLSoftware” に出逢える』ことを願い、これから二年参りに行ってきます。来年は Notes / Domino や HCLSoftware、当ブログにとっても、よりよい年になるといいですね。

そして、みなさま、よいお年をお迎えください!


引き続き活動を継続します。

2024 年もどうぞよろしくお願いいたします。

2023/12/30

Notes - Excel 連携:#30)印刷の設定(ヘッダーとフッター)

Notes から Excel を操作する方法について紹介する連載『Notes - Excel 連携』の 30 回目です。前回に続き、帳票の印刷で 2 ページ以上になった場合の設定に関してです。今回は、ページ番号を表示するときに使用するヘッダーとフッターについてです。


ヘッダーとフッターの設定

Excel では[ページ レイアウト]の[印刷タイトル]を開き、『ページ設定』の画面から[ヘッダー/フッター]タブを開きます。

コンボボックスから選択し設定するのですが、出来合いの設定ではなく、より詳細に設定したい場合には、[ヘッダーの編集]ボタンをクリックします。例えば帳票の右上に "( 1 / 5 )" と総ページ数と現在のページを表示する場合、次のように設定します。

この画面からもわかるようにヘッダーの設定には、左 / 中央 / 右 の 3 種類があることがわかります。フッターも同様なので合計 6 箇所の設定があるということですね。


この設定を LotusScript から行うには、お察しの通り PageSetup オブジェクトのプロパティで設定します。プロパティは 6 種類それぞれ存在しています。

LeftHeader CenterHeader RightHeader
LeftFooter CenterFooter RightFooter


上記例の通り右上にページ番号を表示する場合には、次のように記述します。

   'ヘッダー
   voSheet.PageSetup.RightHeader = "( &P / &N )"

ヘッダー/フッターでは、"&P" がページ番号、"&N" が総ページ数に置き換えられ出力されます。プログラムで設定するには、Excel の設定画面とは違う値を指定します。Microsft Learn でこれらプロパティを調べても何を設定すれはよいかの記述がないようです。仕方がないので、私はマクロの記録で確認しています。


印刷の設定の全体

ここまで 3 回にわたって印刷の設定に関してまとめました。設定を行う関数 xPageSetup の全体は次のようになります。

Function xPageSetup(voSheet As Variant)
   'すべての列を 1 ページに印刷
   voSheet.PageSetup.Zoom = False
   voSheet.PageSetup.FitToPagesWide = 1
   voSheet.PageSetup.FitToPagesTall = False

   '印刷範囲の設定
   voSheet.PageSetup.PrintArea = CCToAA(2, 9)

   '列タイトル
   voSheet.PageSetup.PrintTitleRows = voSheet.Rows("3:5").Address

   'ヘッダー
   voSheet.PageSetup.RightHeader = "( &P / &N )"
End Function


実行して出力された Excel 帳票を印刷プレビューで表示すると次のようになります(コピペで行数を増やし 2 ページとなるよう加工)。

◇ 1 ページ目


◇ 2 ページ目


前回 Notes - Excel 連携 次回


2023/12/29

Notes - Excel 連携:#29)印刷の設定(タイトル行)

Notes から Excel を操作する方法について紹介する連載『Notes - Excel 連携』の 29 回目です。前回から帳票の印刷など出力に関してまとめていますが、今回は 2 ページ目以降の設定に関してです。


タイトル行の設定

まず、前回のコードで出力した帳票の明細行をコピペして、2 ページに入るまで行を追加します。印刷プリビューで見ると2ページ目は以下のようになります。これだと各項目が何の値なのかわからないですよね。

これを解決する機能が列タイトルの機能です。Excel では[ページ レイアウト]の[印刷タイトル]を開き、『ページ設定』の画面から タイトル行 を設定します。

上記のように設定した場合、2ページの目以降も 5 行目が出力されるようになります。


この設定を LotusScript で行うには、PageSetup オブジェクトの PrintTitleRows プロパティを使用します。

PageSetup.PrintTitleRows プロパティ (Excel)

このプロパティは String 型で、各ページの先頭で繰り返されるセルを含む行を表します。設定する文字列は、A1 スタイル表記(上記例では "$5:$5")となります。Range オブジェクトの Address プロパティを使用するとこの文字列が簡単に取得できます。

Range.Address プロパティ (Excel)


具体的なコードは、次の通りとなり、前回作成した xPageSetup 関数に追加します。

   '列タイトル
   voSheet.PageSetup.PrintTitleRows = voSheet.Rows(5).Address

まず、Row(5) で 5 行目を Range オブジェクトとして取得します。そして Address プロパティで列タイトルに設定する文字列を取得しています。


ちなみに 2 ページ目以降にも帳票タイトルを含めたい場合には次のように指定します。

   voSheet.PageSetup.PrintTitleRows = voSheet.Rows("3:5").Address


前回 Notes - Excel 連携 次回

2023/12/27

Notes - Excel 連携:#28)印刷の設定(拡大縮小 / 印刷範囲)

Notes から Excel を操作する方法について紹介する連載『Notes - Excel 連携』の 28 回目です。第 20 回から前回までは「帳票の作成」を行い、帳票としてのデザインは完成しました。今回から数回にわたって、帳票の印刷など出力に関してまとめます。


印刷の設定

前回できあがった帳票を印刷プレビューを表示すると、以下のように 1 ページに入りません。

Excel で印刷する際は「すべての列を 1 ページに印刷」の設定を行い紙のサイズに合わせて出力するのが一般的ですよね。このように Excel の印刷設定を VBA で操作するためには Worksheet オブジェクトの PageSetup プロパティ を使用します。

このプロパティから取得できるオブジェクト PageSetup の詳細は以下のリンクを確認ください。

PageSetup オブジェクト (Excel)


拡大・縮小

印刷の拡大・縮小の設定は 2 通りの方法があります。

1 つ目の方法は、単純に倍率を設定する Zoom プロパティの利用です。単位は%で 10 ~ 400 が設定できます。

PageSetup.Zoom プロパティ (Excel)

この設定でも用紙に合わせた出力は可能なのですが、列数が増えたり、項目幅を調整すると倍率も調整する必要があり非効率ですね。


2 つ目の方法が「すべての列を 1 ページに印刷」する設定のように用紙に合わせて、拡大・縮小する方法です。使用するプロパティは次の2つです。

PageSetup.FitToPagesWide プロパティ (Excel)

PageSetup.FitToPagesTall プロパティ (Excel)

それぞれ、シートのすべての列(または行)を何ページで出力するかを指定します。指定しない場合は False を設定することができます。

例えば、「すべての列を 1 ページに印刷」する場合は、次のようなコードとなります。すべての列を 1 ページに合わせる設定をし、縦は行数に合わせて印刷されるよう False を設定しています。

Function xPageSetup(voSheet As Variant)
   'すべての列を 1 ページに印刷
   voSheet.PageSetup.Zoom = False
   voSheet.PageSetup.FitToPagesWide = 1
   voSheet.PageSetup.FitToPagesTall = False
End Function

なお、この設定を使用する場合、Zoom プロパティに False を設定する必要があるので、注意してください。


印刷範囲の設定

今回作成した帳票の A 列は Excel での見た目を調整するためのマージンの役割でした。印刷する必要はありませんので、印刷範囲から除外します。

印刷範囲を設定するには、PrintArea プロパティ  を使用します。このプロパティには Range オブジェクトで範囲を指定する場合と同じ文字列を設定します。例えば、A 列を除外し B 列~ I 列を印刷範囲とする場合、"B:I" を設定します。

サンプルプログラムは次の通りです。

   '印刷範囲の設定
   voSheet.PageSetup.PrintArea = CCToAA(2, 9)

なお、上記処理では列の範囲を表す文字列を作成する関数をコールしています。この関数は次の通りで、スクリプトライブラリ lsXls に追加しておきます。

Public Function CCToAA(ByVal viColFm As Integer, ByVal viColTo As Integer) As String
   CCToAA = x9ToA(viColFm) & ":" & x9ToA(viColTo)
End Function


なお、今回作成した印刷設定を行う関数をメインプログラムに追加して実行します。

Sub Initialize
         ・・・
   '帳票ヘッダ
   Call xDrawHeader(oSheet)

   '印刷設定
   Call xPageSetup(oSheet)

   'Excel を UI に表示
   oXls.Visible = True
End Sub

出力された Excel シートを印刷プリビューすると、次のように用紙に合わせて印刷されるようになったことがわかります。


前回 Notes - Excel 連携 次回

2023/12/25

リッチテキスト:#15)添付ファイルのダウンロード

ノーツの特徴的な機能であるリッチテキストを LotusScript で操作する方法を紹介している連載『リッチテキストの基本操作』の 15 回目です。

先日、ノーツコンソーシアムで大変お世話になっている方から相談を受けました。『電帳法対策の一環でメールに届いた添付ファイルをひとつずつ指定フォルダに保存したい』とのことでサンプルプログラムをご要望でした。

ちょうどこの連載が既存文書内のリッチテキストの操作に移る時期であったことと日ごろのお礼も兼ねて、ブログネタにすることを条件にサンプル作成を引き受けました。ということで、今回はそのプログラムの紹介と添付ファイルの取得方法についてまとめます。


作成する機能

まずは、作成するプログラムの仕様です。次のように定義しました。

  • 受信ボックスでメールを選択し実行(複数可)
  • あらかじめ決められたフォルダ内に作成
  • メールごとにサブフォルダを作成
  • 1通のメールに複数の添付ファイルにも対応

また、処理を単純化するために次の前提条件を設定しました。

  • 選択する文書は受信メールのみ(送信メールやカレンダーエントリはない)
  • メール DB にエージェントを追加


なお、今回はメール DB にエージェントを追加しています。これはプログラムを単純化することが目的であり、標準機能であるメールのカスタマイズを推奨するわけではありません。

また、カスタマイズを行うとメーカサポートが受けられなくなる可能性があります。お使いの環境に適用される際には、リスクとベネフィットを十分にご検討の上、ご自身の責任で実施ください。


エージェントの作成と設定

新規でエージェントを作成し、エージェントのプロパティを設定します。今回は、選択した文書に対して実行するので、次の通りとなります。


このエージェントの設定と 次のメインルーチンで使用している UnprocessedDocuments については、別の記事『ビューで選択した文書の取得』で紹介していますので、必要に応じて参照にしてください。


メインルーチン

選択した文書を UnprocessedDocuments プロパティで取得して、1件ずつ処理しています。実際のダウンロードの処理は xSaveAttachment 関数内で行っています(後述)。

Option Declare

Sub Initialize
   Dim ns As New NotesSession
   Dim ndb As NotesDatabase
   Dim ndc As NotesDocumentCollection
   Dim nd As NotesDocument
   Dim sFol As String

   '保存フォルダ(最後は\つけてください)
   sFol = "E:\"

   '対象の文書を取得し、順に処理
   Set ndb = ns.CurrentDatabase
   Set ndc = ndb.UnprocessedDocuments
   Set nd = ndc.GetFirstDocument()
   While Not (nd Is Nothing)
      '添付ファイルのダウンロード
      Call xSaveAttachment(nd, sFol)
      '次の選択文書
      Set nd = ndc.GetNextDocument(nd)
   Wend
End Sub

メインルーチンでは、処理する文書のコントロールと保存するフォルダ名の設定だけを担当しています。


添付ファイルのダウンロード

1通のメール内の添付ファイルをフォルダ内にダウンロードする関数です。

はじめに xPrepareSubFolder 関数をコールして、メールごとのサブフォルダを準備しています(後述)。それ以降が、このエージェントのメインプログラムですね。前回『#14)既存リッチテキストフィールドの取得』で紹介した方法で、リッチテキストフィールドのオブジェクトを変数 nrti にセットしています。

添付ファイルは EmbeddedObjects プロパティ経由で取得できます。ただ、このプロパティは添付ファイルだけでなく、埋め込みオブジェクトなども返し、フィールド内のすべてのオブジェクトが配列として返される仕様となっています。なお、オブジェクトが一つもない場合は Empty を返します。

このような背景から EmbeddedObjects の値を Variant 型の変数 vTmp に代入し、Empty でなければ、配列として順に処理しています。配列の各要素は NotesEmbeddedObject となっており、Type プロパティで添付ファイルであるかを判定できます。

Function xSaveAttachment(vnd As NotesDocument, vsFol As String)
   Dim ni As NotesItem
   Dim nrti As NotesRichTextItem
   Dim nemb As NotesEmbeddedObject
   Dim sFol As String
   Dim sName As String
   Dim vTmp As Variant
   Dim i As Integer

   'メールごとのサブフォルダを準備
   sFol = xPrepareSubFolder(vnd, vsFol)

   '本文(Bodyフィールド)を取得
   Set ni = vnd.GetFirstItem("Body")
   If ni.Type = RICHTEXT Then
      'リッチテキストなら添付ファイルをチェック
      Set nrti = ni
      vTmp = nrti.EmbeddedObjects
      If Not (IsEmpty(vTmp)) Then
         For i = 0 To UBound(vTmp)
            '埋め込みオブジェクトを順に取得
            Set nemb = vTmp(i)
            If nemb.Type = EMBED_ATTACHMENT Then
               '添付ファイルの場合保存
               sName = nemb.Source
               '重複しないファイル名を取得
               sName = xGetName(sFol, sName, 0, 1)

               '添付ファイルを保存
               Call nemb.ExtractFile(sName)
            End If
         Next
      End If
   End If
End Function

添付のファイル名は Source プロパティから取得できます。保存する前にフォルダ内のファイルと重複チェックを行う関数 xGetName をコールしています(後述)。重複しないファイル名が関数から返されますので、その名前で保存しています。

添付ファイルの保存は、ExtractFile メソッドを利用します。


サブフォルダの準備

メールごとのサブフォルダ名を決定し、フォルダを作成、フォルダ名をフルパスで返す関数が xPrepareSubFolder です。

フォルダ名は、メールの受信時刻(DeliveredDate)を 14 桁の数字の文字列を作成、それに件名を付加して決定しています。メールの件名には、"/" などフォルダ名として使用できない文字が含まれる可能性がありますので、全角文字に変換して回避しています。

類似するメールを瞬間的に複数受信した場合や複数回実行した場合など、フォルダ名が重複する可能性があります。そこで、生成するフォルダ名も重複チェックを行う関数 xGetName を通しています。

Function xPrepareSubFolder(vnd As NotesDocument, vsFol As String) As String
   Dim sTmp As String
   Dim sName As String
   Dim sFol As String

   '配信日時を変換
   sName = Format(vnd.DeliveredDate(0), "yyyymmddhhnnss")

   'フォルダ名として成り立つよう件名を全角に変換
   sTmp = Left(StrConv(vnd.Subject(0), 4), 20)
   If sTmp <> "" Then
      '件名がある場合はフォルダ名に付加
      sName = sName & " " & sTmp
   End If

   '重複しないフォルダ名を取得
   sTmp = xGetName(vsFol, sName, 16, 1)

   'フォルダを作成戻り値にセット
   MkDir sTmp
   xPrepareSubFolder = sTmp & "\"
End Function


重複チェックと再帰呼び出し

ノーツの文書内には同名のファイル名を複数添付することができます。ですので、たとえメールごとにフォルダを作成したとしても、ファイル名の重複チェックが必要です。そこで今回作成した関数では、test.txt を複数回保存すると、test(2).txt、test(3).txt となるようにしています。

まず、関数の引数は次の 4 つです。

vsFol 処理対象となるフォルダ名
vsName 保存したいファイル名、または、作成したいフォルダ名
viType 重複チェックの対象
0:ファイル、16:フォルダ
viCount 重複チェックを行った回数

関数内では、まず重複チェックの回数に応じてチェックすべき名称を生成します。2 回目は "(2)" 、3 回目は "(3)" と最後に付加しています。その際、ファイル名の拡張子が変化しないよう調整しています。

ファイル名が決定すると Dir$ ステートメントを使用して存在チェックをしています。この命令は、引数と同じ名称のファイル(またはフォルダ)が存在するとその値を返します。よって、null を返した場合は重複しないと判定できるので、その名称を採用しています。

Function xGetName(vsFol As String, vsName As String, ByVal viType As Integer, ByVal viCount As Integer) As String
   Dim sPath As String
   Dim sName As String
   Dim vTmp As Variant

   If viCount > 1 Then
      '同名ありの場合
      '引数の名称に重複数(n)を付加

      vTmp = Split(vsName, ".")
      If UBound(vTmp) > 0 Then
         '拡張子あり
         '拡張子の直前に重複数を付加

         vTmp(UBound(vTmp)-1) = vTmp(UBound(vTmp)-1) & "(" & viCount & ")"
         sPath = vsFol & Join(vTmp, ".")
      Else
         '拡張子なし
         '最後に重複数を付加

         sPath = vsFol & vsName & "(" & viCount & ")"
      End If
   Else
      '同名なし(初回)
      '引数の名称をそのまま使用

      sPath = vsFol & vsName
   End If

   'viType = 0:ファイル、16:フォルダ
   sName = Dir$(sPath, viType)
   If sName = "" Then
      '存在しないので採用
      sName = sPath
   Else
      '同名があるのでカウントアップ
      sName = xGetName(vsFol, vsName, viType, viCount+1)
   End If

   xGetName = sName
End Function

重複する場合、自分自身の関数をコールしています(赤字部分)。コールする際、最後の引数 viCount を 1 加算して渡しています。この効果で、最初の重複では 2、その次も重複した場合は 3 ・・・ となるよう、重複した回数だけカウントアップしながら繰り返します。

重複しなくなったら、ネストしたコールが止まり、呼び出し元の関数に順次値を返し、最終的に最初に呼び出された xGetName 関数の戻り値となります。

文字で記述するとわかりにくいですね。図式化すると次のようになります。

このような構造をプログラミングの用語で”再帰呼び出し”と呼びます。

ただ、ネストした呼び出しを失敗すると、永久ループすることになるので注意が必要です。一般的には関数の呼び出し関係を管理するスタックがいっぱいになり、オーバフローのエラーが発生します。


前回 リッチテキストの基本操作 次回

2023/12/24

リッチテキスト:#14)既存リッチテキストフィールドの取得

ノーツの特徴的な機能であるリッチテキストを LotusScript で操作する方法を紹介している連載『リッチテキストの基本操作』の 14 回目です。これまでは以下の通り、文書もリッチテキストフィールドも新規作成していました。

   Set nd = ndb.CreateDocument()
   Set nrti = nd.CreateRichTextItem("Body")

今回からは既に存在するリッチテキストフィールドを操作する方法を紹介します。


ヘルプのサンプル

Domino Designer ヘルプで、NotesRichTextItem クラスの例 を確認すると、4つ目に既存のリッチテキストフィールドにアクセスするコードが紹介されています。そのまま転記すると、次のようなサンプルです。

   Dim doc As NotesDocument
   Dim rtitem As Variant
   '...set value of doc...
   Set rtitem = doc.GetFirstItem( "ProjectDescription" )
   If rtitem.Type = RICHTEXT Then
      Call rtitem.AddNewLine( 1 )
      Call rtitem.AppendText( "Book is 64 pages, full color.")
   End If
   Call doc.Save( False, True )

もちろん、このコードに間違いはなく正しく動作するのですが、リッチテキストが入る変数 rtitem が Variant で宣言されています。これだと、実際にコーディングする際にプロパティやメソッドの入力でタイプアヘッドが使用できず不便ですね。


NotesRichTextItem クラスの利用

既存文書の Body というリッチテキストフィールドを取得する部分だけを NotesRichTextItem クラスを使用して記述すると次のようになります。

Sub Initialize
   Dim nuiw As New NotesUIWorkspace
   Dim nuid As NotesUIDocument
   Dim nd As NotesDocument
   Dim nrti As NotesRichTextItem

   Set nuid = nuiw.CurrentDocument
   Set nd = nuid.Document

   Set nrti = nd.GetFirstItem("Body")
End Sub

GetFirstItem メソッドでフィールドを取得していますので、NotesRichTextItem クラスを使用していること以外はヘルプと同じですね。

ただ、ここで注意点があります。

GetFirstItem メソッド のヘルプによると、戻り値は NotesItem クラスのオブジェクトとなっています。そして、NotesRichTextItem クラス のヘルプでは、NotesItem クラスを継承していると記載されています。

ですので上記コードは、GetFirstItem メソッドで取得した Body フィールドのオブジェクトが、たまたまリッチテキストだったため、NotesRichTextItem と型が一致してエラーにならなかったということになります。

試しにテキストフィールドを指定した場合、次のようなエラーが発生します。

ちなみにデバッグモードで実行するとエラーの表示が次のように変わります。なぜメッセージが変わるのかはわかりませんが、このエラーの方が端的でわかりやすいですよね...


フィールドの型の判定

上記のようなエラーを回避するよう丁寧にプログラムを記述すると次のようになります。ヘルプに倣って、フィールドタイプを判定しリッチテキストの場合だけ、NotesRichTextItem の変数に代入しています。

Sub Initialize
   Dim nuiw As New NotesUIWorkspace
   Dim nuid As NotesUIDocument
   Dim nd As NotesDocument
   Dim ni As NotesItem
   Dim nrti As NotesRichTextItem


   Set nuid = nuiw.CurrentDocument
   Set nd = nuid.Document

   Set ni = nd.GetFirstItem("Body")
   If ni.Type = RICHTEXT Then
      Set nrti = ni
            ・・・
   End If
End Sub

このようにしておけば後続の処理の記述でもタイプアヘッドが使用でき効率的に開発できます。


前回 リッチテキストの基本操作 次回

2023/12/23

ビューの選択文書の処理で注意すること

前回、『ビューで選択した文書の取得』では、LotusScript でビューで選択した文書を順次処理する2つの方法を紹介しました。NotesUIView を使用した方法とエージェントで指定した対象を UnprocessedDocuments で取得する方法です。

どちらも同様の処理が実現できるのですが、細かな点で挙動が違うので、注意が必要です。


検証プログラム

まずは検証に使用するプログラムを準備します。『ビューで選択した文書の取得』と同様のコードなのですが、NotesDocumentCollection オブジェクトを取得した後、その件数を表示するだけに簡素化します。それぞれ、次のようなコードとし、ビューのアクションボタンから実行します。

◇ NotesUIView

Sub Click(Source As Button)
   Dim nuiw As New NotesUIWorkspace
   Dim nuiv As NotesUIView
   Dim ndc As NotesDocumentCollection

   Set nuiv = nuiw.CurrentView
   Set ndc = nuiv.Documents

   Msgbox Cstr(ndc.Count) & " 文書", 64, "UIView"
End Sub

◇ エージェント

Option Declare

Sub Initialize
   Dim ns As New NotesSession
   Dim ndb As NotesDatabase
   Dim ndc As NotesDocumentCollection

   Set ndb = ns.CurrentDatabase
   Set ndc = ndb.UnprocessedDocuments

   MsgBox CStr(ndc.Count) & " 文書", 64, "Agent"
End Sub

作成したエージェントは、シンプルアクションで実行する設定とします。


フォーカスの文書

ノーツのビューでは、左のチェックマークとは別に、1行だけフォーカスが当たった状態にできます。次のように、枠で囲われた行のことです。

この状態で上記2つのプログラムを実行すると結果に違いがあります。エージェントの場合だけ、選択した文書として取得できます。


ちなみに、左にチェックマークを入れた状態では、どちらの処理でもフォーカスがあったっている文書は無視されます。


選択状態の保持

続いて、左にチェックマークを付け文書を選択した状態で実行します。どちらも同様の結果となります。そして、実行後も文書は選択された状態を維持します。

ところが、下図のようにエージェントをアクションメニューから実行すると選択が解除されチェックマークが外れます。

なぜこのような挙動になるのかは不明ですが、エージェントをエージェントとして素直に利用した場合と他のプログラムから呼び出したときとの差なのだと思います。


選択の解除

上記の通り、選択状態が保持されることにより、あらぬバグを招く可能性があります。例えば、ワークフローアプリで[まとめて承認]ボタンを作ったとします。承認するとステータスが進み、次の承認者に編集権限が移ります。

この時、文書の選択が保持されていると、もう一度ボタンを押すと権限のない文書に対して再度承認処理を行うことになり、エラーにつながります。数回に分けて承認するような操作を行うときは特に注意が必要ですね。


強制的にビューの選択を解除するには NotesUIView クラスの DeselectAll メソッドを利用します。

   Call nuiv.DeselectAll()


2023/12/22

ビューで選択した文書の取得

メールのアーカイブや複数の文書をまとめて承認するなど、ビューで選択した文書を順次処理する操作は、ノーツでは一般的ですよね。この操作を自身のアプリケーションで実現できると使いやすく効率的な UI が構築できますね。

そこで、今回は LotusScript を使ってビューで選択した文書を順に取得する方法はついてまとめます。


NotesUIView クラス

まずは、NotesUI クラスをを使用した方法です。

NotesUIVeiw クラスは、現在ノーツクライアントで操作中のビューに対してアクセスするクラスです。そして、Documents プロパティで選択した文書を NotesDocumentCollection オブジェクトとして取得できます。

例えば、ビューのアクションボタンに次のようなコードをセットすると、ビューで選択した文書の Title フィールドの値を順にメッセージボックスで表示します。

Sub Click(Source As Button)
   Dim nuiw As New NotesUIWorkspace
   Dim nuiv As NotesUIView
   Dim ndc As NotesDocumentCollection
   Dim nd As NotesDocument

   Set nuiv = nuiw.CurrentView
   Set ndc = nuiv.Documents


   Set nd = ndc.GetFirstDocument()
   While Not (nd Is Nothing)
      Msgbox nd.Title(0), 64
      Set nd = ndc.GetNextDocument(nd)
   Wend
End Sub

NotesUIWorkspace オブジェクトから NotesUIView を取得、そこから選択文書を取得しています。直接的でわかりやすいコードですね。


エージェントを使用した方法

続いて、同様の操作をエージェントで実現する方法です。

選択文書を NotesDocumentCollection で取得する部分が少し違い、NotesDatabase クラスの UnprocessedDocuments プロパティを使用しています。

Option Declare

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

   Set ndb = ns.CurrentDatabase
   Set ndc = ndb.UnprocessedDocuments

   Set nd = ndc.GetFirstDocument()
   While Not (nd Is Nothing)
      Msgbox nd.Title(0), 64
      Set nd = ndc.GetNextDocument(nd)
   Wend
End Sub

この UnprocessedDocuments プロパティは、実行中のエージェントが「未処理」と判断する文書コレクションを返します。対象となる文書は、エージェントの設定に応じて変化します。

今回のように、ビューで選択した文書を対象とする場合、エージェントのプロパティで次のように設定します。

この”対象”の設定と UnprocessedDocuments プロパティの利用の両方がそろって、ビューの選択文書が取得できます。コードだけで処理がすべて表現されていないので注意が必要ですね。


2023/12/20

Outlook のメールを体裁よくノーツに貼り付け

ノーツを利用しつつもメールは Outlook を使用している環境はたくさんありますよね。私がお世話になっているお客様でもメールが Outlook に移行しました。とはいっても、現時点ではノーツアプリは残っており、作業報告や問い合わせの対応履歴などはノーツで書いています。

メールがノーツで届いている環境であれば、メールをリッチテキストに転載することは簡単でした。ところが、Outlook からメールをコピペすると改行が吹っ飛んでしまう現象にみまわれ、レポートにならなくなってしまいました。原因は、Outlook 内の改行とノーツリッチテキスト内の改行が一致しないためだと思われます。

そこで、ノーツ - ノーツほどではないにしろ、Outlook のメールをそれなりの体裁を維持しつつリッチテキストに貼り付ける方法を教えてもらいました。私自身の備忘録を兼ねて整理しておきます。

手順は次の通りです。

  1. Outlook でメールを開き、[転送] ボタンを押す
  2. 閲覧ウィンドウの場合は [ポップアウト] ボタンを押す
  3. 『検索と置換』ウィンドウを開く(Ctrl+H)
  4. [あいまい検索] のチェックを外す( [特殊文字] のボタンがアクティブになる)
  5. [検索する文字列] に「任意指定の行区切り」を指定
  6. [置換後の文字列] に「段落記号」を指定
  7. [すべて置換] をクリック(本文内の”↓”が改行マークに代わる)
  8. 本文全体を選択して、コピー(CTRL+C)
  9. ノーツのリッチテキストに貼り付け
  10. ホップアウトウィンドウを閉じる(下書き保存しない) 


手順で操作する Outlook の『検索と置換』ウィンドウと操作する箇所は次の通りです。

また、特殊文字の選択肢はたくさんありますが、今回の操作で使用するのは次の2つです。


ノーツで貼り付けたときのように返答スレッドがセクション形式とはなりません。しかし、メール内の画像にも対応しています。少し手順が多いですが、これぐらいならギリギリ耐えられますね...


2023/12/18

つないでみよう:#7)お天気アイコンの表示

Web 系アプリ開発のど素人が、チャレンジする WebAPI 連携日記の第 7 回です。

気象庁の天気予報取得シリーズの最終回です。前回まででお天気アイコンの準備ができたので、いよいよ天気マークを表示します。完成すると、次のように天気予報が表示できます。


天気コードの取得

第 4 回 で気象庁の Web サイトから天気予報取得のサンプルコードを紹介しました。ただ、シンプルなコードにするため、天気の名称だけを取得していました。まずは、天気コードを取得するように修正します。

修正する個所は次の通りです。修正箇所のみを抽出して赤字で表示します。

Sub Initialize
            ・・・
   Dim vCD As Variant
            ・・・
   '大阪の天気予報を取得
   sAreaCD = "270000"
   Call xGetForecast(sAreaCD, vCD, vWeather)
End Sub

Function xGetForecast(ByVal vsAreaCD As String, rvCD As Variant, rvWeather As Variant)
            ・・・
   '明後日までの天気予報
   Set je3 = jnav.GetFirstElement()
   Call xGetForecast_3Days(je3, rvCD, rvWeather)
End Function

"weatherCodes" というオブジェクト内に天気コードがありますので、それを取得して新設した変数にセットします。

Function xGetForecast_3Days(vje3Days As NotesJSONElement, rvCD As Variant, rvWeather As Variant)
            ・・・
      If jeTmp.Type = Jsonelem_type_array Then
         '配列だが1つしか要素がない
         Set jaTmp = jeTmp.Value
         Set jeTmp = jaTmp.GetFirstElement()
         Set jobjTmp = jeTmp.Value

         '天気 CD
         Set jeTmp = jobjTmp.GetElementByName("weatherCodes")
         rvCD = xGetArray_String(jeTmp)


         '天気
         Set jeTmp = jobjTmp.GetElementByName("weathers")
         rvWeather = xGetArray_String(jeTmp)
      End If
   End If
End Function


天気予報の表示

続いて天気予報を表示する機能をエージェントに追加します。

まず、表示する関数は次の通りです。新規文書を作成し、天気コードと天気名称をセットして、文書を表示するだけのシンプルなプログラムです。

Function xShowForecast_3Dyas(vvCD As Variant, vvWeather As Variant)
   Dim ndb As NotesDatabase
   Dim nd As NotesDocument
   Dim nuiw As New NotesUIWorkspace
   Dim i As Integer

   Set ndb = xns.CurrentDatabase
   Set nd = ndb.CreateDocument()

   nd.Form = "fForecast"
   For i = 0 To UBound(vvCD)
      Call nd.ReplaceItemValue("Days3_WeatherCode_" & CStr(i+1), vvCD(i))
      Call nd.ReplaceItemValue("Days3_WeatherText_" & CStr(i+1), vvWeather(i))
   Next

   Call nd.ComputeWithForm(False, False)
   Call nuiw.EditDocument(False, nd)
End Function

天気コードは Days3_WeatherCode_n 、天気の名称は  Days3_WeatherText_n フィールドにセットします。n = 1 が今日、n = 2 が明日、n = 3 が明後日となります。


関数ができたらメインルーチンから関数をコールします。これでエージェントは完成です。

Sub Initialize
            ・・・
   '大阪の天気予報を取得
   sAreaCD = "270000"
   Call xGetForecast(sAreaCD, vCD, vWeather)

   '天気予報を表示
   Call xShowForecast_3Dyas(vCD, vWeather)
End Sub


フォームの作成

続いて、天気予報を表示するフォームを作成します。3列の表を作成し、上記エージェントで作成されるフィールドを各列に配置し、3日分の天気を表示します。値はエージェントでセットするので作成時の計算結果で、式には "" をセットします。

エージェントでセットしていない Days3_WeatherIcon_n がイメージリソース名を保持するフィールドです。計算結果フィールドとして作成し、以下の式をセットします。この式は自身のフィールド名を利用して記述しているので n = 1 ~ 3 のすべてのフィールドで同じ式となります。

xFld := "Days3_WeatherCode_" + @RightBack(@ThisName; "_");
xWCD := @GetField(xFld);
xSVG := @DbLookup("":""; "":""; "vWeather"; xWCD; "WeatherFileName");
@If(@IsError(xSVG);
   "";
   @Left(xSVG; ".") + ".png"
)

イメージリソースのファイル名は、@DbLookup を使用して、天気コードマスタから取得しています。今回は、昼用のアイコンだけを対象としています。


最後に天気アイコンの表示です。表の背景を@式で設定する機能を利用して作成しました。表の内側にカスケードの表を作成し幅や高さを設定し、罫線を 0 にします。

イメージのソースを計算結果に設定し、式にリソース名が入っているフィールド Days3_WeatherIcon_1 を記述します。この設定はフィールド内ではないので、列ごとにそれぞれのフィールド名をしています。


以上で完成です。エージェントを実行すると、その時点の天気を Web API から取得して、フォームに表示します。これを応用すればノーツのポータル画面にも天気予報が表示できますね。


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

2023/12/16

つないでみよう:#6)お天気アイコンの取得

Web 系アプリ開発のど素人が、チャレンジする WebAPI 連携日記の第 6 回です。

前回に続いて気象庁の天気予報でお天気マークを表示する作業をすすめます。今回はお天気マークの画像を取得します。


アイコンファイル

天気予報 を開きアイコン画像の URL を確認します。例えば、晴れのアイコンの場合は次のようになっており、ファイル名がわかります。

https://www.jma.go.jp/bosai/forecast/img/100.svg

このファイル名は、前回 JSON より取得した天気コードマスタ内にありましたね。これら情報を利用して画像ファイルをダウンロードするプログラムを作成しましょう。


ところで、ファイル名には2種類あり、昼と夜のアイコンとなっています。例えば、411 の「雪後晴」の場合は、411.svg と 811.svg となっており、アイコンは次の通りとなっています。


アイコンファイルのダウンロード

ダウンロードしたアイコンファイルは、天気コードマスタ内に添付することとし、次のように添付用のリッチテキストを用意します。


HTTP でファイルをダウンロードするので、以前作成した関数を使用します。ファイルサイズが万一 64 KB を越えても動作するよう、LotusScript の NotesHTTPRequest クラスを使用しない方法を使います。詳しくは以下を参照してください。

HTTP でファイルをダウンロード(いにしえの呪文)


また、ダウンロードしたファイルを一時的に保存するため、Windows のテンポラリフォルダを利用します。フォルダ名の取得には、先日公開した関数を使用します。

Windows のテンポラリフォルダの取得


サンプルプログラム

天気コードマスタのアイコンを取得するためのエージェントを新規作成します。

まずはメインルーチンです。すべて天気コードマスタを取得するためのビュー  vWeather に接続し、1件ずつ順に取得して処理しています。1マスタにつき2つのアイコンを取得するので、サブ関数 xSetIconFile で処理させています。

Option Declare
Use "lsWindows"
Use "lsHttpDownload"

Sub Initialize
   Dim ns As New NotesSession
   Dim nuiw As New NotesUIWorkspace
   Dim nd As NotesDocument
   Dim ndb As NotesDatabase
   Dim nv As NotesView
   Dim i As Integer

   Set ndb = ns.CurrentDatabase
   Set nv = ndb.GetView("vWeather") '天気コードマスタ取得ビュー
   nv.AutoUpdate = False

   Set nd = nv.GetFirstDocument()
   While Not (nd Is Nothing)
      '昼のアイコン
      Call xSetIconFile(nd, "WeatherIcon_SVG", nd.WeatherFileName(0))
      '夜のアイコン
      Call xSetIconFile(nd, "WeatherIcon2_SVG", nd.WeatherFileName2(0))

      Call nd.Save(True, False)
      i = i + 1

      Set nd = nv.GetNextDocument(nd)
   Wend
   MsgBox CStr(i) & " マスタのファイルを取得しました。"

   'ビュー更新
   Call nuiw.ViewRefresh()
End Sub


アイコンファイルをマスタ文書に添付する処理は次の通りです。Use したライブラリ内の関数を使用して、ダウンロードしたファイルをテンポラリフォルダに保存しています。添付が完了したらファイルは削除しています。

Function xSetIconFile(vnd As NotesDocument, ByVal vsFld As String, ByVal vsFN As String)
   Dim sURL As String
   Dim sFP As String
   Dim nrti As NotesRichTextItem

   'アイコンのURL生成
   sURL = "https://www.jma.go.jp/bosai/forecast/img/" & vsFN

   'ファイルダウンロード
   sFP = GetWinTmpPath() & vsFN
   Call HttpDownload(sURL, sFP)

   'フィールドに添付
   Call vnd.RemoveItem(vsFld)
   Set nrti = vnd.CreateRichTextItem(vsFld)
   Call nrti.EmbedObject(EMBED_ATTACHMENT, "", sFP)

   'ダウンロードしたファイルは削除
   Kill sFP
End Function


実行結果

実行すると次のように各マスタ文書にアイコンファイルが添付されます。


アイコンをイメージリソースに登録

天気コードマスタは天気予報の種類を詳細に分類していますが、アイコン複数の天気コードで共通となっています。例えば、昼用にファイル名でカテゴライズしたビューを作成してみると複数の天気コードマスタに同じファイル名が割り当てられていることがわかります。

これで使用されているアイコンの種類がわかりますので、それぞれをイメージリソースに登録します。ノーツは現時点で SVG ファイルに対応していないので、インターネット上の変換サイトなどを利用して、PNG 形式に変換してからリソースに保存します。

本当であればこの処理も自動で行いたかったのですが、うまく変換できなかったので現時点では手作業としています。よい方法が見つかれば別途記事にしたいと思います。


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

2023/12/14

つないでみよう:#5)天気コードマスタの作成

Web 系アプリ開発のど素人が、チャレンジする WebAPI 連携日記の第 5 回です。

前回は気象庁の Web サイトから天気予報を取得するサンプルを紹介しました。ただ、テキストで表示するだけだと面白くないですよね。そこでお天気マークの表示にチャレンジしたいと思います。

利用するのは、天気コードです。


天気コードの定義

上記 JSON では 天気コードと天気のテキストがセットで提供されています。ですが、この天気コードは気象庁の Web サイトではマスタ化されているようです。

マスタデータをまるっと取得する URL は発見できなかったのですが、以下の Web ページに紹介されている手順で、JSON を取得できるようです。

気象庁JSON ファイルにある weatherCode 一覧

手順としては次の通りです。

  1. 天気予報の Web ページ を Chrome で開く
  2. デバッグツールを開く(F12)
  3. コンソールを開き ”Forecast.Const.TELOPS” と入力して、Enter を押す
  4. 出力された結果を右クリックして [object をコピー] を選択

これをメモ帳などに貼り付けると天気コードが JSON 形式で確認できます。この情報から天気コードと名称(日英)、アイコンの画像ファイル名(.svg)が確認できます。

{
   "100": [
      "100.svg",
      "500.svg",
      "100",
      "晴",
      "CLEAR"
   ],
   "101": [
      "101.svg",
      "501.svg",
      "100",
      "晴時々曇",
    ・・・


天気コードマスタの作成

では、この JSON から nsf 内に天気マスタを作成しましょう。

まずは、JSON を入力する画面を作成します。手順をすぐに忘れちゃうので、取得手順を含めて画面を作成してみました。

実行すると以下のように手順や URL リンク、Console に入力するコマンドが表示されます。これを利用して JSON を取得して、入力欄に貼りつけるという算段です。

作成処理が完了すると天気コードマスタが作成されます。

参考までですが、マスタフォームの設計は次のような感じです。


サンプルコード

実際にマスタを一括登録するためのエージェントを作成します。

まずはメインルーチンです。ここでは、初期設定とダイアログボックスを表示して、入力した JSON を取得、天気マスタを作成する関数 xCreateAllWeather をコールしています。

Option Declare
Private xns As NotesSession
Private xasFld(5) As String

Sub Initialize
   Dim nuiw As New NotesUIWorkspace
   Dim nuid As NotesUIDocument
   Dim nd As NotesDocument
   Dim ndb As NotesDatabase
   Dim nv As NotesView

   Dim sJSON As String
   Dim iCount As Integer

   'フィールド名の定義
   xasFld(0) = "WeatherCode"
   xasFld(1) = "WeatherFileName"
   xasFld(2) = "WeatherFileName2"
   xasFld(3) = "WeatherGroup"
   xasFld(4) = "WeatherName"
   xasFld(5) = "WeatherNameEN"

   Set xns = New NotesSession
   Set ndb = xns.CurrentDatabase
   Set nuid = nuiw.CurrentDocument
   Set nd = ndb.CreateDocument()

   'JSON の入力
   If nuiw.Dialogbox("dlgInputWeatherMst", True, True, False, False, False, False, "天気マスタ登録", nd, True) Then
      'JSON の取得
      sJSON = nd.JSON(0)

      'JSON を解析してマスタを作成
      iCount = xCreateAllWeather(ndb, sJSON)

      'ビュー更新
      Call nuiw.ViewRefresh()

      MsgBox CStr(iCount) & " 件の天気マスタを登録しました。"
   End If
End Sub


天気マスタを作成する関数 xCreateAllWeather では、JSON から天気コード1件分のオブジェクトを取得して、マスタ文書を作成する関数 xCreateWeather をコールしています。

Function xCreateAllWeather(vndb As NotesDatabase, ByVal vsJSON As String) As Integer
   Dim jnav As NotesJSONNavigator
   Dim jelm As NotesJSONElement
   Dim i As Integer

   '天気コードを1件ごとに取得
   Set jnav = xns.CreateJSONNavigator(vsJSON)
   Set jelm = jnav.GetFirstElement()
   While Not (jelm Is Nothing)
      If xCreateWeather(vndb, jelm) Then i = i + 1
      Set jelm = jnav.GetNextElement()
   Wend

   xCreateAllWeather = i
End Function


xCreateWeather 関数では、実際にマスタ文書を作成しています。今回の JSON の構造では、天気名称などの詳細な情報は配列として保存されていました。そこで、ループを利用してセットする単純な構造となっています。

Function xCreateWeather(vndb As NotesDatabase, vjelm As NotesJSONElement) As Boolean
   Dim jelm As NotesJSONElement
   Dim jarr As NotesJSONArray
   Dim jobj As NotesJSONObject
   Dim nd As NotesDocument
   Dim i As Integer

   xCreateWeather = False
   If vjelm.Type <> Jsonelem_type_array Then Exit Function

   'マスタ文書作成
   Set nd = vndb.CreateDocument()
   nd.Form = "fWeather"

   'コードをフィールドにセット
   Call nd.ReplaceItemValue(xasFld(0), vjelm.Name)

   '配列内の各要素をフィールドにセット
   Set jarr = vjelm.Value
   For i = 1 To jarr.Size
      Set jelm = jarr.GetNthElement(i)
      Call nd.ReplaceItemValue(xasFld(i), jelm.Value)
   Next

   xCreateWeather = nd.Save(True, False)
End Function


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