2024/11/06

Notes - Excel 連携:#46)テキストボックスの配置

前回紹介したように、グラフ上にテキストボックスを追加すると、座標 (0, 0) で作成したにも関わらず、左と上に少し隙間が空いてしまいます。

Set oText = voShape.Chart.Shapes.AddLabel(msoTextOrientationHorizontal, 0, 0, 50, 10)

今後、より詳細にオブジェクトの位置決めをするときに混乱しないよう挙動を確認します。


隙間のサイズ

まず、Excel のマクロの記録しながらテキストボックスに左上に移動してみました。すると、次のスクリプトが出力されました。

Sub Macro1()
   Selection.ShapeRange.IncrementLeft -4
   Selection.ShapeRange.IncrementTop -4
End Sub

IncrementLeft は、図形を指定したポイント数だけ水平方向に移動するメソッドです。マイナスの値を指定すると左、プラスの値で右に移動します。IncrementTop も同様の機能で、こちらは垂直方向に移動するメソッドです。Left や Top プロパティで位置を絶対的に設定するだけでなく、現在位置から相対的に移動できるんですね。

この結果より、左と上の隙間の幅は 4 ポイントであることがわかりました。


左上に配置するには

隙間の大きさがわかったので、左上ぎりぎりに配置する方法を確認します。

まず、テキストボックス作成時にマイナスの座標をセットしてみます。結果は、変わらず隙間が空いた状態となりました。

・・・   .Shapes.AddLabel(msoTextOrientationHorizontal, -4, -4, 50, 10)


次は、Left と Top プロパティに負の値を入れてみます。実行はしているようですが、結果の位置は変わりませんでした。


では先ほどの Increment* メソッドをテストします。座標 (0, 0) で作成したオブジェクトをそれぞれ -4 してみます。これで無事、左隅に移動することができました。

   'タイトルエリアの生成
   Dim oText As Variant
   Set oText = xCreateTextBox(oShape)
   oText.TextFrame2.TextRange.Text = "Test"

   Call oText.IncrementLeft(-4)
   Call oText.IncrementTop(-4)


座標系

上記のように、右上に寄せた状態で、オブジェクトの位置を確認すると -4 となっています。

   Call oText.IncrementLeft(-4)
   Call oText.IncrementTop(-4)

   Dim s As String
   s = "Left = " & CStr(oText.Left)
   s = s & Chr(10) & "Top = " & CStr(oText.Top)
   MsgBox s

グラフオブジェクトの幅は 500 ポイントでした。試しに座標 (0, 0) で作成し、幅を 492 ポイントに設定してみます。

   'タイトルエリアの生成
   Dim oText As Variant
   Set oText = xCreateTextBox(oShape)
   oText.TextFrame2.TextRange.Text = "Test"

   ' Call oText.IncrementLeft(-4)
   ' Call oText.IncrementTop(-4)


   oText.Width = 492

結果は以下のように、左右の隙間が同じになりました。


上記の結果より、Shape オブジェクトとその上に配置するテキストオブジェクトの関係は次の通りとなります。


まとめ

今回は、グラフオブジェクトの座標系に関して調査しました。Excel としてはオブジェクトのマージンとして 4 ポイント分確保し、そこを座標の起点としていることがわかりました。

Notes - Excel 連携:#42)PlotArea とサイズ』の「位置合わせの謎」のセクションで、PlotArea を左から 50 ポイント(約 66 ピクセル)にしても、54 ポイント(72 ポイント)になる原因が不明だと書きました。原因がわかりましたね。今回の検証で理解した 4 ポイントのマージン(約 5 ピクセル)ということだったのです。

上記は PlotArea の操作で、今回の検証はテキストボックスでしたが、位置の指定は同じです。Shape オブジェクト内の座標系はこれで統一されているようですね。

オブジェクトの配置やサイズの操作のポイントは 2 つです。

まず、4 ポイント分のマージンは利用できないわけではなく、 Increment* メソッドを使えば利用できます。ただし、Left や Top プロパティで、直接的にマイナスの値は指定できないようなので、注意が必要です。

もう一点は、今回の例でいうと Left と Top に -4 を指定しても、0 となったように、範囲外の値を指定してもエラーが発生しないことです。こちらは、 #42 でも触れましたが、プログラムを実行しても希望した通りの配置とならない場合、バグなのか、Excel が調整したのか、それがいつ(どの操作で)発生したのか判定しずらい点に注意が必要です。


前回 Notes - Excel 連携


2024/11/05

Notes - Excel 連携:#45)グラフタイトルとテキストボックスの違い

#41)グラフの調整』から開始した ”使える” グラフにするための調整作業です。今回はグラフ右上に配置するグラフタイトルエリアです。通常のタイトルのほかに補足情報を表示できるサブタイトル欄を用意します。

両タイトルとも右寄せで配置します。タイトルが長い場合、グラフエリアにはみ出して表示し、文字の折り返しはない前提とします。


グラフタイトルは特殊?

まずは Excel でグラフタイトルの設定を確認します。フォントの設定や右寄せなど一般的な操作は可能です。ただ、[グラフ タイトルの書式設定]-[タイトルのオプション]-[サイズとプロパティ]を表示すると、オブジェクトのサイズ設定がありません。

ところが、図形メニューから追加した ”テキスト ボックス” では、次の通り、サイズの設定が可能です。このようにグラフタイトルでは、設定できないプロパティが部分的に存在するようです。


グラフタイトルの削除

将来細かな操作が必要となった場合に備え、今回はグラフタイトルは使用せず、”テキスト ボックス” でタイトルとサブタイトルを表現することとします。

標準のグラフタイトルの削除は、HasTitle プロパティに False を設定するだけです。

Sub Initialize
         ・・・
   'タイトルのセット
   'oChart.ChartTitle.Text = "ユーザ数の推移"

   oChart.HasTitle = False
         ・・・


テキストボックスの配置

グラフ内にテキストボックスを配置する方法は、『#39)名前アイコン生成 ③』で紹介しました。Shapes オブジェクトの AddLabel メソッドを使用するんでしたね。

今回は、タイトル、ダブタイトル、Y 軸ラベルでテキストボックスを使用します。そこで、オブジェクトを作成する汎用的な関数 xCreateTextBox を作成します。

引数はグラフを表す Shape オブジェクトです。テキストボックスの作成以外に共通の設定である垂直方向の中央揃えと文字色を設定しています。

Function xCreateTextBox(voShape As Variant) As Variant
   Dim oText As Variant

   Set oText = voShape.Chart.Shapes.AddLabel(msoTextOrientationHorizontal, 0, 0, 50, 10)
   With oText.TextFrame2
      '垂直方向に中央揃え
      .VerticalAnchor = msoAnchorMiddle
      '色の設定
      .TextRange.Font.Fill.ForeColor.RGB = RGB(32, 32, 32)
   End With

   Set xCreateTextBox = oText
End Function

なお、オブジェクトの配置や文字サイズなどはテキストボックス毎にバラバラになります。そのため、この関数の外側で設定する前提としています。ですので、オブジェクト作成時に初期設定しているオブジェクトの位置とサイズは適当な値で、意味はありません。


次回の予告

上記関数のテストを行います。次の通り、テキストボックスの作成して、文字をセットします。

Sub Initialize
          ・・・
   'プロット領域の設定
   oChart.PlotArea.InsideWidth = 350
   oChart.PlotArea.InsideLeft = 50
   oChart.PlotArea.InsideTop = 7
   oChart.PlotArea.InsideHeight = 170

   '関数テスト(テキストボックスの作成)
   Dim oText As Variant
   Set oText = xCreateTextBox(oShape)
   oText.TextFrame2.TextRange.Text = "Test"


   oXls.Visible = True
End Sub

※ 上記コードはテスト目的であり、以降の作業では不要です。


実行すると左と上に隙間が空きます。次回はこの挙動について整理したいと思います。


前回 Notes - Excel 連携


2024/10/31

ノーツ・しこく・フェスタ 2024:AIとNotes/Domino 禁断のコラボ ~ 生成 AI 実装事例集

2024年 10月 25日にうどん(香川)県 高松市で開催された『ノーツ・しこく・フェスタ 2024』のレポートです。イベント本体はもちろん有意義なものでしたが、前後のエクストラツアーも充実しており、全方位で魅力的なイベントでした(結果的に 4 日間もうどん県に滞在...)。

イベントの全体像については、以下のブログを参照ください。一緒に参加した仲間がすでに公開してくれています。

ノーツコンソーシアムブログ:ノーツ・しこく・フェスタ2024 開催

Domino Lab:NSF 2024 エクストラツアー


私はこのイベントで、ミニセッションを担当しました。『AIとNotes/Domino 禁断のコラボ ~ 生成 AI 実装事例集』というタイトルで ”一席” 設けさせていただきました。

生成 AI 実装事例ということで、OpenAI の gpt-4o の API と連動したノーツアプリを 4 本を一気に紹介させていただきました。

どんなアプリなのか、ざっくり紹介させていただきます。


1. AI じゃダメなんですか? - AI 文書仕分

文書作成者がタイトル、カテゴリ、要約を記入していると、その人の知識や経験、好みや文章力で統一感がなくなるという、文書管理のお悩みあるあるを AI で解決しようというサンプルです。

Notes/Domino に関する HCLSoftware Blog をコピペしてノーツ文書として保存し、勝手に事例にさせていただきました。もとの文書を AI に送信し、タイトル、カテゴリ、要約を生成させています。

ビューで比較すると効果は一目瞭然です。AI ってすごいですね。

カテゴリは、ドキュメントタイプ、テーマ/トピック、キーワード(タグ的なもの)の 3 種類生成しています。これらカテゴリを複数の人間が的確に行うのは不可能に近いと思います。

ビューのカテゴリ分けはノーツの特徴的な機能で大変便利です。それを活かすためにもカテゴリの適切な設定、平準化は重要ですよね。


2. AI が導く、次世代の学習体験 - AI Learning

eLearning のコンテンツって作成時間がかかる割に利用が一瞬だったり、さまざまなレベルの利用者を前提したコンテンツが難しかったりと、作成作業は非常に非効率ですよね。これを AI に助けてもらおうというサンプルです。

次のように学習のテーマと目標を入力し、学習をスタートするだけで、コンテンツは AI が生成してくれるというアプリケーションです。

生成されたコンテンツは、次の通り、体裁を整えたノーツ文書として提供されます。キーワードはリンクになっていて、クリックするその単語をより詳しく教えてくれます。

これを繰り返し、どんどん調査(学習)を繰り返すという仕組みです。生成されたコンテンツはノーツの返答文書として階層管理されます。返答文書のツリーをビューで確認すると学習の進捗がわかります。クリックしたキーワード別のビューでは用語集になり、全ユーザ分をキーワードでカテゴライズすると同じ悩み(学習課題)を持つ仲間を探したりできます。

当日のセッションでは解説しなかった技術的な補足を少しだけします。

コンテンツはすべて AI に生成させています。各セクションのタイトルや文章以外にリンクにすべきキーワードも AI 任せです。ノーツの文章として表現する(プログラムで利用する)ためにこれらが的確に判定できる必要があります。これを実現するために、gpt-4o で使用できる Structured Outputs という機能を利用しています。この機能は、AI の返答をあらかじめ指定したスキーマに従わせることができます。要は返答がこちらが指定する JSON フォーマットになるので、どの部分がタイトルで、キーワードは何かが後続のプログラムで簡単に判別できます。

ちなみに、コンテンツの生成は DXL を使用しています。フォントサイズや行間の設定はもちろん、リンクも生成しています。LotusScript で記述されたホットスポットのアクションで、その段落内の文章とともにクリックしたキーワードについて AI に問い合わせしており、連続性のあるコンテンツが生成されるようにしています。


3. 添付画像の説明を生成 - PicInsight

古いノーツで添付した文書はサムネイルが表示されません。また、XPages や LotusScript で添付した文書も同様です。こういった画像ファイルを AI に食べさせ画像に何が写っているか説明させようというものです。

この結果をビューに表示すると、文書や画像を開くことなく、どのような画像か判別できます。より詳細に確認したい画像だけ開けばいいので、便利ですね。


4. AI はとびきりの愛なんだよ! - 忖度メール AI(アイ)

こちらは AI Learning と同様、文章生成系のアプリです。メールの宛先となる方の性格や趣味・趣向と要件を入力すると、メールの文面を考えてくれるアプリです。

メールの作成条件を入力してから、文面作成を依頼します。

すると、次のように素晴らしい文章が生成されます。新しいバイクが欲しいという欲求をダイレクトに伝えることなく、買ってもいいかな?買ったほうがいいかな?と思わる文章になっていますね。私には書けない文章です...

文面の作成は何度も実行できますので、気に入った文書が出るまで繰り返すことができます。実用においては、相手との関係性などを考慮して、言葉尻や事実関係などの調整が必要かもしれませんが、話の流れや構成はそのまま利用できると思います。


まとめ

これまで、AI との連携はつながるかどうかの検証など、技術的な側面からだけ見ていました。今回のセッションはサンプルアプリの紹介がテーマだったので、利用シーンなどを考えながらアプリ開発を行い、AI 利用の可能性を大きく感じました。

AI は膨大な知識を持っています。API を利用するとその知識や情報量を利用したアプリが作成できます。知らないことはもちろんですが、自分にない発想で回答してくれます。新たな気づきが得られることに感動しました。

AI への問い合わせは、標準化された方法で問い合わせします(連載『つないでみよう』参照)。パターンが決まっているということは、応用が簡単ということです。今回紹介した 4 アプリとボツとなった 2 本を合わせて、6 DBを作成しました。9月 19日 の DominoHub Osaka が終わって本格的に作成したので、期間は約 1 ヶ月間でした。Notes/Domino が備える基本機能の高さもありますが、開発効率が高いと感じました。スピード感をもって対応できますね。

AI × Notes/Domino。これまでとは違う新しい世界、そしてその可能性を感じました。今後の展開が楽しみです!


最後に、今回のセッションはノーツコンソーシアム 大阪研でともに活動している 林哲司さん と共同で行いました。前半は私の事例紹介で、後半は林さんの『Notesアプリと生成AIを融合 生成AI実装のポイントと課題』でした。

AI 連係の実装については林さんのブログ『Notes開発者のためのXPagesデザインレシピ』も参考にしてください。

AI 関連の記事はこちら


2024/10/23

Notes - Excel 連携:#44)軸のフォントサイズ設定と PlotArea の調整

#41)グラフの調整』から開始した ”使える” グラフにするための調整作業です。いよいよプロット領域を調整するコーディングを始めます。


軸のフォント調整の必要性

#42 で PlotArea の位置調整で無理な値を設定した場合、エラーもなく適当な値に設定されることを紹介しました。そして、PlotArea の Left や Width が指すのは、軸エリアを含んでいました。ということは、PlotArea の調整の前に軸エリアのサイズを把握しておく必要があります。

例えば軸エリアの幅は桁数だけでなく、フォントサイズにも依存します。そこで、Excel が自動で設定するフォントサイズではなく、明示的に指定することで、見た目の統一感と軸エリアのサイズを固定化を狙います。


軸のフォントサイズ設定

エージェントに以下のコードを追加して軸のフォントサイズを明示的に設定します。

Sub Initialize
         ・・・
   'Y 軸補助目盛線の設定
   oChart.Axes(xlValue).MinorUnit = 250
  
   '軸フォントサイズの指定
   oChart.Axes(xlValue).TickLabels.Font.Size = 10
   oChart.Axes(xlCategory).TickLabels.Font.Size = 10

  
   oXls.Visible = True
End Sub

軸のアクセスは #16#17 で紹介しました。Chart オブジェクトの Axes メソッド でアクセスするんでしたね。引数に xlValue を指定すると縦軸、xlCategory を指定すると横軸となります。

この Axes メソッド はヘルプでの説明が不明瞭です。戻り値には Object としか書いていません。Microsoft Learn の Excel 内を調査したところ軸に関する 2 つのオブジェクトが見つかりました。

Axes 指定したグラフ内のすべての Axis オブジェクトのコレクション
Axis グラフの 1 つの軸を表します

この結果より Chart オブジェクトの Axes メソッド で xlValue や xlCategory を指定することにより、Axis オブジェクトを返すと想定できます。Axis オブジェクトには、軸に関するさまざまなプロパティが存在し、TickLabels プロパティから取得する同名のオブジェクトの Font プロパティを利用して、フォントサイズがセットできます。

Axis.TickLabels プロパティ (Excel)

TickLabels.Font プロパティ (Excel)


PlotArea の調整

今回、作成しているグラフのサイズは 500 x 200 ポイントでした。

   'グラフの作成
   Set oShape = oSheet.Shapes.AddChart2(, 4, 200, 1, 500, 200)

このサイズに合わせて、グラフの配置を決定します。

横幅の設定は縦軸エリアを 50 ポイント、グラフエリアを 350 ポイント、残りをグラフタイトルや凡例エリアとします。

続いて高さについてです。軸ラベルの高さは文字サイズの 2 倍程度の幅が必要そうです。また、縦軸を見ると、一番上の縦軸ラベルが上にはみ出ています。文字がセンタリングされているので、軸のフォントサイズ 10 ポイントの半分です。これを考慮して、7 ポイント下に下げ、少し余裕を持たせることにします。以上の結果からプロット領域の高さは 70 ポイントとすればよさそうです。

これらの設定を行うプログラムは下記の通りです。いたってシンプルなコードなのですが、正常に動作させるために重要なポイントがあります。

一部は #42 で触れていますが、わかりますか? 

Sub Initialize
         ・・・
   '軸フォントサイズの指定
   oChart.Axes(xlValue).TickLabels.Font.Size = 10
   oChart.Axes(xlCategory).TickLabels.Font.Size = 10
  
   'プロット領域の設定
   oChart.PlotArea.InsideWidth = 350
   oChart.PlotArea.InsideLeft = 50
   oChart.PlotArea.InsideTop = 7
   oChart.PlotArea.InsideHeight = 170


   oXls.Visible = True
End Sub

正解はプロパティを設定する順番です。

デフォルトで作成されるグラフは、横幅がエリア全体を使っていますが、高さはグラフタイトルと凡例が配置され、プロット領域は狭くなっています。

今回のように、プロット領域を右にずらそうとすると、先に幅を縮める必要があります。逆に高さは広げることになるので、先に上に移動しておかないと必要な高さに変更できないということですね。


実行結果

ここまでの修正で作成されるグラフは次のようになります。

プロット領域の配置はこれで OK ですね。次回は、ラベルの調整を行います。


前回 Notes - Excel 連携


2024/10/22

Notes - Excel 連携:#43)作業前のコードの確認

前回は PlotArea のプロパティについて調査しましたので、今回からはその設定を行うコーディングを始めます。

今回以降の作業は『#18)グラフの目盛線』のプログラムをベースに行います。ただ、その記事から期間が空いていること、連載が長くなっていることから、いったん全体のコードを掲載します。

今回は、作業のスタート地点の確認だけの記事です。ご了承ください。


設計要素としては、エージェントとスクリプトライブラリとなります。


エージェント

内容は #18 と同じですが、#25 でスクリプトライブラリ名を lsXls に変更したので、その点だけ修正しています。

Option Declare
Use "lsXls"

Sub Initialize
   Dim oXls As Variant
   Dim oSheet As Variant
   Dim oShape As Variant
   Dim oChart As Variant

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

   'サンプルデータのセット
   Call xSetSampleData(oSheet)

   'グラフの作成
   Set oShape = oSheet.Shapes.AddChart2(, 4, 200, 1, 500, 200)
   Set oChart = oShape.Chart

   'データソースの指定
   Call oChart.SetSourceData(oSheet.Range("A:A, B:B"))

   'タイトルのセット
   oChart.ChartTitle.Text = "ユーザ数の推移"

   'Y 軸の設定
   oChart.Axes(xlValue).MinimumScale = 500
   oChart.Axes(xlValue).MaximumScale = 2000
   oChart.Axes(xlValue).MajorUnit = 500

   'X 軸の設定
   oChart.Axes(xlCategory).CategoryType = xlTimeScale
   oChart.Axes(xlCategory).MajorUnit = 7
   oChart.Axes(xlCategory).TickLabels.NumberFormatLocal = "m/d;@"

   '目盛線の追加
   Call oChart.SetElement(msoCategoryGridLinesMajor)
   Call oChart.SetElement(msoValueGridLinesMinorMajor)
   Call oChart.SetElement(msoCategoryGridLinesMinorMajor)

   'Y 軸補助目盛線の設定
   oChart.Axes(xlValue).MinorUnit = 250
  
   oXls.Visible = True
End Sub

Function xSetSampleData(voSheet As Variant)
   Dim i As Integer
   Dim vDT As Variant
   Dim iUsr As Integer
   Dim iVal As Integer
   Dim iMax As Integer

   '列フォーマットの設定
   voSheet.Columns(1).NumberFormatLocal = "yyyy/m/d"
   voSheet.Columns(2).NumberFormatLocal = "#,##0_ "
   voSheet.Columns(3).NumberFormatLocal = "#,##0_ "

   'ヘッダ行のフォーマット設定
   voSheet.Rows(1).NumberFormatLocal = "@"

   'カラム名称の設定(ヘッダ行)
   voSheet.Cells(1, 1).Value = "日付"
   voSheet.Cells(1, 2).Value = "ユーザ数"
   voSheet.Cells(1, 3).Value = "増減"

   'サンプルデータの設定
   Randomize
   iMax = 30
   vDT = Today - iMax
   iUsr = 1000

   For i = 2 To iMax + 1
      vDT = vDT + 1
      iVal = Int(Rnd()*100) - 30
      iUsr = iUsr + iVal
      voSheet.Cells(i, 1).Value = vDT
      voSheet.Cells(i, 2).Value = iUsr
      voSheet.Cells(i, 3).Value = iVal
   Next
End Function


スクリプトライブラリ

スクリプトライブラリ lsXls は #18 より後に掲載した内容も含め、現時点での最新の状態としています。少し長くなりますが、下記の通りです。

Option Declare

'XlAxisType 列挙 (Excel)
Public Const xlCategory = 1
Public Const xlValue = 2

'XlCategoryType 列挙 (Excel)
Public Const xlAutomaticScale = -4105
Public Const xlCategoryScale = 2
Public Const xlTimeScale = 3

'MsoChartElementType 列挙 (Excel)
Public Const msoCategoryGridLinesMajor = 334
Public Const msoValueGridLinesMinorMajor = 331
Public Const msoCategoryGridLinesMinorMajor = 335

'XlBordersIndex 列挙 (Excel)
Public Const xlEdgeLeft = 7 '範囲の左側の罫線
Public Const xlEdgeTop = 8 '範囲の上側の罫線
Public Const xlEdgeBottom = 9 '範囲の下側の罫線
Public Const xlEdgeRight = 10 '範囲の右側の罫線
Public Const xlInsideVertical = 11 '範囲の外側を除くすべての垂直罫線
Public Const xlInsideHorizontal = 12 '範囲の外側を除くすべての水平罫線

'XlBorderWeight 列挙 (Excel)
Public Const xlHairline = 1 '細線 (最も細い罫線)
Public Const xlMedium = -4138 '普通
Public Const xlThin = 2 '極細
Public Const xlThick = 4 '太線 (最も太い罫線)

'XlLineStyle 列挙 (Excel)
Public Const xlContinuous = 1 '実線
Public Const xlDouble = -4119 '2 本線
Public Const xlLineStyleNone = -4142 'なし
Public Const xlDash = -4115 '破線

'XlHAlign 列挙 (Excel)
Public Const xlHAlignLeft = -4131 '左揃え
Public Const xlHAlignCenter = -4108 '中央揃え
Public Const xlHAlignRight = -4152 '右揃え
Public Const xlHAlignGeneral = 1 'データの種類に従って揃える

'XlPaperSize 列挙 (Excel)
Public Const xlPaperA3 = 8
Public Const xlPaperA4 = 9
Public Const xlPaperB4 = 12
Public Const xlPaperB5 = 13

'XlPageOrientation 列挙 (Excel)
Public Const xlPortrait = 1 '縦モード
Public Const xlLandscape = 2 '横モード

'XlFixedFormatType 列挙
Public Const xlTypePDF = 0
Public Const xlTypeXPS = 1

'MsoAutoShapeType 列挙 (Office)
Public Const msoShapeOval = 9 '楕円

'MsoTextOrientation 列挙 (Office)
Public Const msoTextOrientationHorizontal = 1 '横方向

'MsoVerticalAnchor 列挙 (Office)
Public Const msoAnchorMiddle = 3 '垂直方向に中央揃え

'MsoParagraphAlignment 列挙 (Office)
Public Const msoAlignCenter = 2 '中央揃え

Public Function GetRangeString(_
         ByVal viRowFm As Integer, ByVal viColFm As Integer, _
         ByVal viRowTo As Integer, ByVal viColTo As Integer) As String
   Dim s As String

   s = RCToA1(viRowFm, viColFm)
   s = s & ":" & RCToA1(viRowTo, viColTo)

   GetRangeString = s
End Function

Public Function PointToCM(ByVal vdPoint As Single) As Single
   PointToCM = InchToCM(PointToInch(vdPoint))
End Function

Public Function PointToPixcel(ByVal vdPoint As Single) As Integer
   PointToPixcel = CInt(vdPoint / .75)
End Function

Public Function InchToPixcel(ByVal vdInch As Single) As Integer
   InchToPixcel = CInt(vdInch * 96)
End Function

Public Function CMToInch(ByVal vdCM As Double) As Single
   CMToInch = vdCM / 2.54
End Function

Public Function RGB(ByVal vbyR As Byte, ByVal vbyG As Byte, ByVal vbyB As Byte) As Long
   RGB = vbyR + CLng(vbyG) * 256 + CLng(vbyB) * 256 ^ 2
End Function

Public Function InchToPoint(ByVal vdInch As Single) As Single
   InchToPoint = vdInch * 72
End Function

Public Function PixcelToInch(ByVal viPixcel As Integer) As Single
   PixcelToInch = viPixcel / 96
End Function

Public Function PixcelToPoint(ByVal viPixcel As Integer) As Single
   PixcelToPoint = viPixcel * .75
End Function

Function x9ToA(ByVal viColNumber As Integer) As String
   Dim i As Integer
   Dim i1 As Integer
   Dim i2 As Integer
   Dim s As String

   i = viColNumber - 1
   i1 = (i Mod 26) + 1
   i2 = Int(i / 26)

   If i2 > 0 Then s = Chr(64 + i2)
   s = s & Chr(64 + i1)

   x9ToA = s
End Function

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

Public Function CMToPixcel(ByVal vdCM As Single) As Single
   CMToPixcel = InchToPixcel(CMToInch(vdCM))
End Function

Public Function RCToA1(ByVal viRow As Integer, ByVal viCol As Integer) As String
   RCToA1 = x9ToA(viCol) & CStr(viRow)
End Function

Public Function InchToCM(ByVal vdInch As Single) As Single
   InchToCM = vdInch * 2.54
End Function

Public Function PointToInch(ByVal vdPoint As Single) As Single
   PointToInch = vdPoint / 72
End Function

Public Function PixcelToCM(ByVal viPixcel As Integer) As Single
   PixcelToCM = InchToCM(PixcelToInch(viPixcel))
End Function

Public Function CMToPoint(ByVal vdCM As Single) As Single
   CMToPoint = InchToPoint(CMToInch(vdCM))
End Function


次回
前回 Notes - Excel 連携


2024/10/21

Notes - Excel 連携:#42)PlotArea とサイズ

前回から、”使える”グラフにするための操作をまとめています。今回は具体的なコーディングに入る前に、グラフの作画エリアの挙動を確認します。


作画エリアの操作

Excel のグラフを作画しているエリアは VBA で PlotArea オブジェクトとして管理されています。オブジェクトの取得は Chart オブジェクトの PlotArea プロパティから取得します。

Chart.PlotArea プロパティ (Excel)

PlotArea オブジェクト (Excel)


作画エリアのサイズ調整は PlotArea オブジェクトのプロパティから行います。上記リンク先を眺めるとサイズにかかわるプロパティが 2 セット存在します。

Height InsideHeight
Left InsideLeft
Top InsideTop
Width InsideWidth

それぞれの違いについて理解する必要がありそうです。ヘルプを確認すると、次の通り記載されていました。グラフ領域とグラフの端、プロット領域の違いが不明瞭ですね...

Left グラフ領域の左端までの距離
InsideLeft グラフの端からプロット領域の左端までの距離


Left と InsideLeft の違い

ヘルプだけではよくわからないので、各プロパティの値をメッセージボックスで確認します。あわせて、軸エリアの幅も表示させてみました(以下の画像は合成して作成)。

InsideLeft がおおよそ 24、軸エリアの幅が 23 となっています。この結果より、InsideLeft には軸エリアを含んだ値であることがわかりますので、Inside* プロパティが指し示すエリアは下図の赤枠の部分で、ヘルプで言う ”プロット領域” だと推察できます。

Left や Width が指すのは、軸エリアを含む緑枠のエリアのようです。ただ、今一つ不明瞭なので点線としています。

少なくともX軸とY軸の交点を固定するためには、Inside* プロパティを使えはできそうですね。


幅の操作

プロット領域の位置を調整するテストをしていて設定どおりにならないことが、何度もありました。これは位置の設定はグラフ領域を超えないからです。

例えば、事例に上げているグラフでは、グラフ領域はほぼエリアいっぱいに設定されています。この状態で Left プロパティに 100 をセットしても、実行後の Left の値は 18 となりました。デフォルトの状態で右側にあったマージンの分だけ、右側に移動したということだと思います。

このように範囲外の数値を設定した場合、エラーも発生させず、適当な値に設定されます。希望通り配置できないことがあれば、各プロパティの値を表示させ確認しないと、バグなのか計算間違いなのか判定しづらくなります。

私は、通常 Pixel で考えるのですが、これらプロパティの単位はすべて Point ととなります。これも、状況を複雑にさせる要因ですね。Excel の単位については、以前まとめたので以下のリンクをご確認ください。

#35)Excel で使用する単位と変換


位置合わせの謎

次の実験は、Inside* プロパティで位置をセットしたグラフを画像にした場合、希望通りの位置に配置されるかを確認します。

テストとしては、InsideWidth = 400 にセットしてから、InsideLeft = 50 にセットします。グラフ領域のサイズが 500 なので、中央に配置する算段です。

その結果は次の通りでした。

いい感じなのですが、微妙に右にずれているような気がします。そこで、このグラフをペイントにコピペして、サイズをチェックします。

すると次の通りでした。

やはり右にずれていますね。プロット領域の 535 ピクセル は 401.25 ポイントなので、おおむね指定したサイズになっています。左の 72 ピクセルは 54 ポイントとなり指定通りとなっていません。

試しに、設定後のプロパティを表示したところ、50 ポイントとなっています。

前述のテストで不明瞭だった Left の値は約 27 となっており、軸エリアの幅を足すと InsideLeft と合致します。よって、Left に起因する問題ではなさそうです。


現時点では残念ながら原因は不明でした。少し右にずれるという点を認識して先に進めます。原因が判明したら別途レポートさせていただきます。


まとめ

今回は、プロット領域 PlotArea について調査しました。一部不明瞭な点がありましたが、軸をそろえることは今回の調査の範囲で実現できそうです。

次回からは、この情報を活用しつつ、実際にコーディングしていきます。


次回
前回 Notes - Excel 連携


2024/10/18

Notes - Excel 連携:#41)グラフの調整

この連載の #11 ~ #19 で Notes から Excel のグラフを作成する方法を紹介しました。そして、前回まで 3 回にわたり紹介した名前アイコンの生成では、画像ファイルとして保存できるようになりました。これらを組み合わせると、LotusScript だけでノーツのデータをグラフ化できます(裏で Excel は使用しますが...)。

そして、別の連載 DXL Step-by-Step の『#41)インラインイメージの貼り付け』を利用すれば、作成したグラフを見える状態(インラインイメージ)で貼りつけることができます。ここまでできるようになると、別の次元のノーツ文書が作成できるようになりますね。


グラフの調整が必要となるシーン

ただ、これまでに紹介したグラフの作成方法は、Excel が自動で行ってくれる機能を多分に利用しています。これでもそれなりに美しいグラフは作成できるのですが、業務で実用しようとすると問題点が出てきます。

例えば、複数のグラフを作成して比較する場合です。#18 のプログラムは 30 日分のユーザ数の推移をグラフにするサンプルでした(下図の上のグラフ)。これを修正して Y 軸の桁数が違うグラフを作成して並べてみます(下図の下のグラフ)。すると、X 軸と Y 軸の交点の位置がずれてしまいます。これでは比較しづらく、レポートして及第点はもらえないですよね。

今回からしばらくは、この問題の解決を題材とします。具体的には、グラフの軸ラベル、タイトル、凡例などを制御して、”使える”グラフにするための操作をまとめます。


作成するグラフ

今回チャレンジするグラフのイメージは次の通りです。

まずは、メインの目的であるグラフエリアのサイズを固定します。

左には軸ラベルを表示するエリアを含め一定の幅を確保します。タイトルと凡例はグラフ右に集め、表示するエリアとして、こちらも決められた幅を確保します。これらは位置はグラフエリアを最大化して値の変動を確認しやすくするのが狙いです。


次回
前回 Notes - Excel 連携


2024/10/13

DXL Step-by-Step:#41)インラインイメージの貼り付け

DXL 活用の調査・検証で、実現できたことや発見したことご紹介する『DXL Step-by-Step』シリーズの第 41 回です。しばらく表の話が続いたので少し流れを変えます。

今回はリッチテキストに直接貼り付ける画像についてまとめます。 ノーツクライアントでは、下図のようにプロパティで見るとソース欄に[インラインイメージ]と表示されるパターンですね。

DXL を使えば、リッチテキストに画像を貼った文書を作成することができます。また、文書から画像をダウンロードしてファイルに保存することも可能です。


インラインイメージの DXL

先ほどのリッチテキストを DXL で書き出すと次のようになります。

画像を表す png タグがあります。その配下には暗号化されたような文字列がテキストノードとして大量に出力されています。この構造は『#8)イメージリソース(設計要素)の DXL』で紹介した、イメージリソース内に保存されている画像データと同じですね。

画像データの1つ上は picture ノードとなっています。このノードは『#26)イメージリソースの DXL』で、リッチテキストにイメージリソースを配置する際に出てきました。

リッチテキストに配置したインラインイメージとイメージリソースの違いは、配下のノードが画像データか、リソースの参照 imageref かだけです。


サンプルプログラム

それでは、実際にインラインイメージを配置するプログラムを作成します。『#27)イメージリソースの表示』で作成したプログラムをベースに作業します。

エージェントをコピペして xSetDXL_ImageResource 関数を次の通り修正します。

Function xSetDXL_InlineImage(vddn As NotesDOMDocumentNode, vdenRT As NotesDOMElementNode, ByVal vsFileName As String)
   Dim denPar As NotesDOMElementNode
   Dim denPic As NotesDOMElementNode
   Dim den As NotesDOMElementNode
   Dim dtn As NotesDOMTextNode

   Dim nst As NotesStream
   Dim iType As Integer
   Dim iSizeX As Integer
   Dim iSizeY As Integer

   '画像ファイルの確認
   Set nst = xns.CreateStream()
   Call nst.Open(vsFileName)
   iType = xGetImageFileInfo(nst, iSizeX, iSizeY)

   If iType <> DXL_Image_Unknown Then

      '段落の作成
      Set den = vddn.CreateElementNode("par")
      Call den.SetAttribute("def", "1")
      Set denPar = vdenRT.AppendChild(den)

      '画像の作成
      Set den = vddn.CreateElementNode("picture")
      Call den.SetAttribute("width", CStr(iSizeX) & "px")
      Call den.SetAttribute("height", CStr(iSizeY) & "px")
      Set denPic = denPar.AppendChild(den)

      'インラインイメージの作成
      Set den = vddn.CreateElementNode(xGetImageNodeName(iType))
      Set den = denPic.AppendChild(den)

      '画像データセット
      Set dtn = vddn.CreateTextNode(StreamToBase64(nst))
      Call den.AppendChild(dtn)
   End If
End Function


画像ファイルの確認

xSetDXL_InlineImage 関数には、引数 vsFileName を追加しました。これはインラインイメージとして貼り付ける画像のファイル名です。指定したファイルは、関数の最初で画像の形式を確認しています。

画像ファイルを調査して、画像のフォーマットとサイズを返す関数 xGetImageFileInfo をコールしています。この関数は、『#13)イメージの形式とサイズの取得』で紹介しています。xGetImageFileInfo とサブ関数 xGetImageFileType をコピペします。


画像の作成

picture ノードの作成では、画像のサイズ width と height 属性を追加しています。イメージリソースを呼び出す場合はサイズ指定がなくても表示されたのですが、インラインイメージの場合は指定しないと画像が表示されませんでした。


インラインイメージの作成

インラインイメージの場合、画像の形式により作成するタグが変わります。画像ファイルに合致したタグ名を xGetImageNodeName 関数から取得し、ノードをを作成しています。

xGetImageNodeName 関数は次の通りです。

Function xGetImageNodeName(ByVal viImageType As Integer) As String
   Dim s As String

   If viImageType = DXL_Image_PNG Then s = "png"
   If viImageType = DXL_Image_GIF Then s = "gif"
   If viImageType = DXL_Image_JPEG Then s = "jpeg"

   xGetImageNodeName = s
End Function


画像データセット

画像データは Base64 でエンコードされた文字列を指定する必要があります。変換する関数 StreamToBase64 は『#10)イメージリソースの新規作成』で紹介しているので、コピペします。


呼び出し元の修正

xSetDXL_InlineImage 関数が完成したら、呼び出しもとを修正します。

Function xSetDXL(vdprs As NotesDOMParser)
         ・・・
   '段落定義
   Call xSetDXL_pardef(ddn, denRT)

   'インラインイメージの追加
   Call xSetDXL_InlineImage(ddn, denRT, "E:\411.PNG")
End Function

これでサンプルは完成です。実行するとこの記事の最初の画像のようにインラインイメージが貼り付けられたリッチテキストが作成されます。


まとめ

今回はインラインイメージをリッチテキストに追加する方法を紹介しました。LotusScript 標準の RichText クラスではできない機能ですので、これができるようになるとプログラム経由で作る文書の表現力が格段にアップしますね。


前回 DXL Step-by-Step


2024/10/11

DXL Step-by-Step:#40)表のスタイル - タブ形式

ノーツの表には特殊な設定があって、これを利用するとタブ形式やアコーディオン形式の UI が作成できます。表のプロパティでは[表の行]タブの設定になります。今回はこの設定を DXL で操作してみましょう。

タブ形式の表では、表の行数分のタブが表示され、タブをクリックすることでその行を表示します。要はタブのクリックで表示する行を切り替える機能ということですね。ですので、プロパティでは『1 行のみ表示』を選択することになります。

そして、スタイルは『行の表示方法』で指定します。『タブボタンで行を選択』でタブ形式、『表題で行を選択』でアコーディオン形式となります。タブの表題は『タブラベルと表題』にしています。これは、行ごとに設定することになります。


表のスタイルの設定

DXL で表のスタイルを設定するには、table ノードの rowdisplay 属性を設定します。これまでこの属性を指定していなかったので、通常の表として表示されていたということですね。

属性 設定値 補足
rowdisplay (なし) 通常の表
'tabs' タブボタンで行を選択(タブ形式)
'captions' 表題で行を選択(アコーディオン形式)


タブの表題の設定

タブの表題は行ごとに指定することから tablerow ノードの属性として設定します。属性名は tablabel です。


サンプルプログラム

#32)表の作成と行・列の間隔 では、3 行 2 列の単純な表を作成するサンプルプログラムを紹介しました。今回はこれを改造します。

まず、表を作成する関数 xSetDXL_table を修正します。赤字が修正箇所です。

Function xSetDXL_table(_
            vddn As NotesDOMDocumentNode, _
            vdenRT As NotesDOMElementNode, _
            vvTabName As Variant, ByVal viCols As Integer _
            ) As NotesDOMElementNode
   Dim denTbl As NotesDOMElementNode
   Dim denRow As NotesDOMElementNode
   Dim den As NotesDOMElementNode
   Dim iCol As Integer
   Dim iRow As Integer

   '表の作成
   Set den = vddn.CreateElementNode("table")
   Set denTbl = vdenRT.AppendChild(den)
   'タブ形式の表
   Call denTbl.SetAttribute("rowdisplay", "tabs")

   '列定義の作成
   For iCol = 1 To viCols
      Set den = vddn.CreateElementNode("tablecolumn")
      Call denTbl.AppendChild(den)
   Next

   '行の作成
   For iRow = 0 To UBound(vvTabName)
      Set den = vddn.CreateElementNode("tablerow")
      Set denRow = denTbl.AppendChild(den)
      'タブ名称
      Call denRow.SetAttribute("tablabel", vvTabName(iRow))

      'セルの作成
      For iCol = 1 To viCols
         Set den = vddn.CreateElementNode("tablecell")
         Call denRow.AppendChild(den)
      Next
   Next

   Set xSetDXL_table = denTbl
End Function

まず、引数では行数を指定するのではなく、タブの表題を文字列型の配列で渡す仕様に変えています。これに伴い、行を作成するループの指定が配列の要素数分に変更されています。

そして、本題のタブの設定は、table ノードに rowdisplay 属性、tablerow ノードに対して tablabel 属性をセットしている 2 か所となります。


関数の修正に伴い、呼び出しもとを次のように修正します。

Function xSetDXL(vdprs As NotesDOMParser)
         ・・・
   '表前の段落追加
   Call xSetDXL_par(ddn, denRT)

   'タブ表の追加
   Dim asTabName(1)
   asTabName(0) = "Sample Tab 1"
   asTabName(1) = "タブ2"
   Set denTbl = xSetDXL_table(ddn, denRT, asTabName, 1)


   '間隔の設定
         ・・・
End Function

タブ名称を asTabName 配列にセットして、関数をコールしているだけです。


実行結果

修正したプログラムを実行すると、次のようにタブが 2 つの表が作成されます。

また、rowdisplay 属性を captions に変更すると次のようにアコーディオン形式となります。


前回 DXL Step-by-Step


2024/10/09

リスト配列

先日、LotusScript でリスト配列を使う機会がありました。初めての利用だったのでヘルプで調べながら開発を行いました。せっかく調べたので、機能を整理しておきます。


リスト配列とは?

LotusScript では、Boolean、Integer、String などさまざまなデータ型が存在します。そして、同じデータ型の値をまとめて管理する構造体があります。配列というとなじみがありますよね。List も配列の一種です。

通常の配列は a1(0) など変数の後ろにカッコでインデックス番号(添え字や要素番号とも言います)を付けると値にアクセスできます。一方、List の場合は、インデックス番号の代わりに文字列(以下リストタグと呼ぶ)を使用します。

例えば、ユーザとそのユーザの点数を管理する場合を考えてみましょう。それぞれ、次のようなコードとなります。

List の場合 配列の場合
aiScore("Ito") = 75
aiScore("Sato") = 90
asName(0) = "Ito"
asName(1) = "Sato"
aiScore(0) = 75
aiScore(1) = 90


宣言方法

リスト配列を宣言するには次のように記述します。

   Dim aiScore List as Integer

aiScore("Ito") と配列のように記述するのですが、宣言に ( ) は必要ありません。要素や領域の確保は不要ということですね。また、配列のように多次元にすることはできません。


値の追加方法

リスト配列に値を追加するのは簡単です。次のように要素にリストタグを指定するだけです。

    aiScore("Ito") = 75

リスト配列内に "Ito" という要素がなければ新規で追加され、値がセットされます。すでに存在した場合は、値が更新されます。配列のように ReDim 操作が不要なので便利ですね。


値のアクセス

リスト配列の値のアクセスも値との追加と同じように、リストタグを指定するだけです。例えば、以下のコードはリストタグ Ito の値を 10 加算しています。

    aiScore("Ito") = aiScore("Ito") + 10


ループ

平均値を出力するなど、リスト内のすべての要素にアクセスしたい場合があります。このような処理には Forall ループを使用します。

例えば、次のコードではリスト配列 aiScore の合計 iSum と 件数 iCnt を算出し、平均を表示しています。

   ForAll iScore In aiScore
      iSum = iSum + iScore
      iCnt = iCnt + 1
   End ForAll

   MsgBox "平均点 = " & Format(iSum / iCnt, "#0.0")

Forall で使用している iScore がリスト配列の各要素(値)が順に入る変数です。


リスト配列の各種操作

上記情報で、リスト配列の通常使用は可能だと思います。ここから先はリスト配列利用に関連する各種操作をまとめます。チェックなど込み入った操作をする場合などに参考にできます。


◇ リストタグの取得

Forall ループの処理では、各要素の値がループ変数にセットされました。その値のリストタグを取得する関数が ListTag 関数です。

次のコードではリスト配列 aiScore のすべてのリストタグを順に表示します。

   ForAll iScore In aiScore
      MsgBox ListTag(iScore)
   End ForAll


◇ 要素の存在確認

続いては指定したリストタグが、リスト内に存在するか判定する方法です。次のコードではリスト配列 aiScore に Tanaka の要素があるか判定します。

   If IsElement(aiScore("Tanaka")) Then
      MsgBox "リスト配列内に存在します!"
   Else
      MsgBox "リスト配列内に存在しません。"
   End If


◇ 要素の削除

Erase ステートメントを使用するとリスト内の要素を削除することができます。

   Erase aiScore("Hata")

もし、指定した要素がリスト配列内に存在しない場合、エラーが発生します。


◇ リスト配列の初期化

Erase ステートメントを使用するとリスト配列内のすべてのエントリを削除し初期化することができます。

   Erase aiScore

リストタグを指定せず変数名を指定するとすべての要素を削除するということですね。


◇ リスト変数の判定

DataType 関数を使用すると変数のデータタイプが取得できます。

   Dim aiScore List As Integer
   MsgBox DataType(aiScore)

リスト配列は 2048 となりますが、上記の結果は 2050 となります。これは 整数型のリスト配列のためで、リスト配列 2048 + 整数型 2 = 2050 となります。型により値が変わるので注意が必要です。

また、リスト配列かを判定するだけであれば IsList 関数が便利です。

   If IsList(aiScore) Then
      MsgBox "aiScore はリスト配列です。"
   Else
      MsgBox "aiScore はリスト配列ではありません。"
   End If


◇ 空の判定

リスト配列を宣言した直後や Erase で初期化した後は、中身が空っぽの状態となります。この判定の方法については明確な関数は発見できませんでした。Ubound や Lbound のような関数はないようです。また、残念ながら IsEmpty の戻り値は True となり判定できませんでした。

Forall ループでは、ループ内の処理が一切実行されないので、それで判定する方法が良いかもしれませんね。


まとめ

今回は、リスト配列の使い方についてまとめました。配列と同様に、同じデータ型の値をまとめて管理できる変数でした。要素のアクセスは リストタグ と呼ばれる文字列で指定する点がポイントですね。

この記事の最初に『ユーザとそのユーザの点数を管理する場合』の例を上げ、配列との比較をしました。どちらが優れているという話をしたかったわけではありません。配列とリスト配列、近しい機能ではありますが、それぞれ違いがあります。作成するプログラムの仕様に合わせて、適材適所、使い分けができるといいですね。