伝説のツール「NotesPeek」をQtでリメイクする(その4・IDテーブル)

IDテーブル

IDテーブルは、Notesにおける文書のためのコンテナの役割を持っています。Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングでも書かれていますが、

文書IDテーブルとは、文書ID(NOTEID)を昇順で保持するコンテナで、std::setとほぼ同じ機能を提供します。

と、あります。もともとAPIはC用に作られているので、C++のコンテナをそのまま使うわけにはいきません。CのみであればこのIDテーブルを使えばいいわけです。またC++であればstd::setを使ってみても問題ありません。ただ、著者の津田氏も勧めているように、IDテーブルの仕組みの理解と、C++のような実装をする勉強のためにも、IDテーブルを取り込んだ、C++っぽいクラスを作ってみます。津田氏の場合、NoteIdCollectionクラスとそのイテレータクラスで構成されています。9割以上は氏のコーディングを参考にしています。ただ、あまりNoteクラスと密な関係にせず、基本的にはNOTEIDの出し入れというシンプルな構造に変えてみました。

まず、IDテーブルクラスの実装を紹介する前に、ここで扱うIDテーブル関連のAPIを紹介します。

IDテーブルは、IDCreateTable関数によって生成し、IDDestroyTable関数によって破棄します(例外もあります)。生成されたIDテーブルはDHANDLEにて管理します。

#include <idtable.h>

STATUS LNPUBLIC IDCreateTable (DWORD Alignment, DHANDLE far *rethTable);
STATUS LNPUBLIC IDDestroyTable(DHANDLE hTable);

ここで、Alignmentはsizeof(NOTEID)を渡すことになります。

IDテーブルにIDを追加するには、IDInsert関数を使います。

#include <idtable.h>

STATUS  LNPUBLIC IDInsert (DHANDLE hTable, DWORD id, BOOL far *retfInserted);

戻り値はいつものステータス値ですが、IDが挿入できたかどうかは3番目の引数、retfInsertedを見ます。これは、IDがすでにIDテーブル内に存在していればfalseを返します。必要なければnullptrを与えておきます。

テーブル内のIDは通常IDDeleteかIDDeleteAllを使って削除しますが、その実装は後日とします。また、Noteクラスとの連携に唯一getNoteメソッドを実装していますが、これも後日説明します。

それでは、僕流のIDTableクラスと、IDTable::iteratorクラスです。

// ntlx/idtable.h

#ifndef NTLX_IDTABLE_H
#define NTLX_IDTABLE_H

#include "ntlx_global.h"
#include "ntlx/status.h"

#if defined(NT)
#pragma pack(push, 1)
#endif

#include <nsfdata.h>

#if defined(NT)
#pragma pack(pop)
#endif

namespace ntlx
{

class Database;
class Note;

/**
 * @brief IDテーブルクラス
 */
class NTLXSHARED_EXPORT IDTable
    : public IStatus
{
public:

  /**
   * @brief イテレータインナークラス
   */
  class NTLXSHARED_EXPORT iterator
  {
  public:
    /**
     * @brief デフォルトコンストラクタ
     */
    iterator();

    /**
     * @brief NOTEIDを返す演算子
     * @return NOTEID
     */
    NOTEID operator*();

    /**
     * @brief 前置きインクリメント演算子
     * @return 自身への参照
     */
    iterator& operator++();

    /**
     * @brief 後置きインクリメント演算子
     * @return インクリメントする前のイテレータ
     */
    iterator operator++(int);

    /**
     * @brief NOTEIDに対応したNoteオブジェクトを取得する
     * @param db 取得元になるデータベースオブジェクト
     * @return Noteオブジェクト
     */
    Note getNote(Database& db) const;

  protected:
    /**
     * @brief コンストラクタ
     * @param pIdTable IDTableオブジェクトへのポインタ
     * @param noteId NOTEID
     * @param isLast 最後を示しているか
     */
    iterator(IDTable* pIdTable, NOTEID noteId, bool isLast = false);

    IDTable* idTable_;
    NOTEID id_;
    bool isLast_;

    /**
     * @brief 等値演算子
     * @param lhs 左辺値
     * @param rhs 右辺値
     * @return ブール値
     */
    friend NTLXSHARED_EXPORT bool operator==(
        const iterator& lhs
        , const iterator& rhs
        );

    /**
     * @brief 不等値演算子
     * @param lhs 左辺値
     * @param rhs 右辺値
     * @return ブール値
     */
    friend NTLXSHARED_EXPORT bool operator!=(
        const iterator& lhs
        , const iterator& rhs
        );

    friend class IDTable;
  };

  /**
   * @brief コンストラクタ
   */
  IDTable();

  /**
   * @brief デストラクタ
   */
  virtual ~IDTable();

  /**
   * @brief 最初のイテレータを返す
   * @return 最初のイテレータ
   */
  iterator begin();

  /**
   * @brief 最後のイテレータを返す
   * @return 最後のイテレータ
   */
  iterator end();

  /**
   * @brief NOTEIDを挿入する
   * @param id NOTEID
   * @return 重複していなければ真
   */
  bool insert(NOTEID id);

private:
  DHANDLE handle_;

  friend class iterator;
};

} // namespace ntlx

#endif // IDTABLE_H
// idtable.cpp

#include "ntlx/idtable.h"
#include "ntlx/note.h"

#if defined(NT)
#pragma pack(push, 1)
#endif

#include <idtable.h>

#if defined(NT)
#pragma pack(pop)
#endif

namespace ntlx
{

IDTable::iterator::iterator()
  : idTable_(nullptr)
  , id_(0)
  , isLast_(false)
{
}

NOTEID IDTable::iterator::operator *()
{
  return id_;
}

IDTable::iterator& IDTable::iterator::operator++()
{
  isLast_ = !IDScan(idTable_->handle_, id_ == 0, &id_);
  return *this;
}

IDTable::iterator IDTable::iterator::operator++(int)
{
  iterator it = *this;
  operator++();
  return it;
}

Note IDTable::iterator::getNote(Database &db) const
{
  return Note(db, id_);
}

IDTable::iterator::iterator(IDTable *pIdTable, NOTEID noteId, bool isLast)
  : idTable_(pIdTable)
  , id_(noteId)
  , isLast_(isLast)
{
}

bool operator==(const IDTable::iterator& lhs, const IDTable::iterator& rhs)
{
  if (lhs.isLast_ && rhs.isLast_)
    return true;
  else
    return lhs.id_ == rhs.id_;
}

bool operator!=(const IDTable::iterator& lhs, const IDTable::iterator& rhs)
{
  return !operator==(lhs, rhs);
}

IDTable::IDTable()
  : IStatus()
  , handle_(NULLHANDLE)
{
  lastStatus_ = IDCreateTable(sizeof(NOTEID), &handle_);
}

IDTable::~IDTable()
{
  if (handle_)
    IDDestroyTable(handle_);
}

IDTable::iterator IDTable::begin()
{
  if (handle_ == NULLHANDLE)
    return end();
  return ++iterator(this, 0);
}

IDTable::iterator IDTable::end()
{
  return iterator(this, 0, true);
}

bool IDTable::insert(NOTEID id)
{
  Q_ASSERT(handle_);

  BOOL b = false;
  lastStatus_ = IDInsert(handle_, id, &b);
  return b;
}

} // namespace ntlx

Domino Web Server APIで自作Web APIは作ることはできるのか?プロローグ

Domino Web Server API、略してDSAPI。いわゆるHTTPタスクのアドインです。HTTPタスクに介入できるようになったのは、R5からだったと記憶しています。

昔からDSAPIで標準提供されているものに、Domino Offline Service(DOLS)がありますし、DSAPIではないですが、iNotesやXPagesなどもDSAPIを起点にしてHTTPタスクに統合されていったのではないかと勝手に想像しています。

Domino Data Service(DDS)は、DominoサーバをREST/WebAPI化する強力なサービスで、ググると8.5.3 SP1からの提供であるようですが、これ以前に、自前でRESTサービスをDominoに組み込めないか、よく思案したものです。

DDSはJSONでレスポンスを返すので、Node.jsやJavaScriptフロントエンドなどと相性もよく、標準のデータベースアクセスのほかにカレンダーに特化したものもあり、機能的にはそんなに遜色ない出来だと思っています。

でも、使い込んでくると欲が出てくるもので、「ああ!こんな機能があったらいいのに!!!」と思ったことは一度や二度ではなかったですね。

そんなわけで、これまた昔からの小さな夢を、実現してみようかと思い立ち、自作Web APIを作ってみることにしました。今回はそのプロローグ、Hello,Worldです。

OSとDominoはWindows、ライブラリはVS2013 32bitとQt5.6、Dominoは9.0.1です。VSモジュール(msvcr120.dll、msvcp120.dll)とQtCoreのDLLを、パスで解決できる場所に置きます。DSAPIを使ったアドインDLLは、他のアドインと違って序数1によるエキスポートは不要ですが、Cリンケージ(extern “C”)にしておく必要があります。

DSAPIは、関数やシンボルなどを「FilterXXXX」と命名しているように、基本的にHTTP処理のフィルタリングを主目的としているようです。処理できる段階は、リクエストの受付時からレスポンスの返しまでの一般的なものから、認証処理時、名前リスト構築時など、認証機能を備えたDominoならではのタイミングもあります。今回は、認証済みのタイミング(kFilterAuthorized)を使ってみました。

Webブラウザなどから、http://domino.server/csapi/とたたくと、Hello~とプレーンテキストで返すだけの単純なサンプルは、以下の通りです。

main.cpp

#include <QByteArray>

#pragma pack(push, 1)
#include <global.h>
#include <dsapi.h>
#include <addin.h>
#pragma pack(pop)

extern "C"
__declspec(dllexport) uint FilterInit(FilterInitData* pInitData)
{
  pInitData->appFilterVersion = kInterfaceVersion;
  pInitData->eventFlags = kFilterAuthorized;
  strcpy(pInitData->filterDesc, "Chiburu Systems API");
  AddInLogMessageText("Chiburu Systems API Started.", NOERROR);
  return kFilterHandledEvent;
}

extern "C"
__declspec(dllexport) uint HttpFilterProc(
    FilterContext* ctx
    , uint eventType
    , void* /*ptr*/
    )
{
  uint errID;
  switch (eventType)
  {
  case kFilterAuthorized:
  {
    FilterRequest request;
    ctx->GetRequest(ctx, &request, &errID);
    QByteArray url(request.URL);
    if (url.startsWith("/csapi/") && request.method == kRequestGET)
    {
      QByteArray buffer(request.version);
      buffer += " 200 OK\r\n";
      buffer += "Content-Type: text/plain\r\n";
      buffer += "\r\n";
      buffer += "Hello, Chiburu Systems Web API!\r\n";
      ctx->WriteClient(ctx, buffer.data(), buffer.size(), 0, &errID);
      return kFilterHandledEvent;
    }
  }
    break;
  }
  return kFilterNotHandled;
}

訂正(2017/4/10)

誤:Domino Server API => 正:Domino Web Server API 誤:\n => 正:\r\n

IBM Notes C APIのMacにおけるDWORD問題

現在、このブログでQtによるNotesPeekリメイクプロジェクトを進めていますが、ここで今日、問題が発生しました。

Notesデータベースから文書を取得する際にNSFSearch関数を使用し、NOTEID型を取得しようとしました。

WindowsLinux(Ubuntu)では問題は出ませんでしたが、Mac環境において、正確にNOTEIDが取得できませんでした。

NOTEIDはDWORD型です。APIにおいてDWORDは、32bit幅の符号なし整数になります。この定義は、APIのglobal.hヘッダーファイルに含まれています。

Win32やUNIX(LINUX)においては問題が発生していません(Domino Serverを対象にしたWin64の場合は検証できていません)。

しかし、Macコンパイラにおいて、他のOSと違ってDWORDの幅が異なりました。

APIに掲載されているオプションもいろいろ試しましたが、今のところ、解決できた追加オプションは以下の通りです。

DEFINES += LONGIS64BIT

何かわかり次第、補足したいと思います。

伝説のツール「NotesPeek」をQtでリメイクする(その3・その2の補足)

ここでは、その2で紹介したコードについて、ファイル検索以外の点について補足します。

ntlxライブラリ

QString Status::toMessage()

StatusクラスにtoMessageメソッドを追加しました。以前は、

QString msg = Lmbcs(status).toQString();

としていましたが、

QString msg = status.toMessage();

と、Status単独でステータス値からメッセージを取得することができます。

IStatusクラス

メソッドとしては値を返り値にしたいが、関数が処理したステータスも重要な場合、いちいち引数にStatusのポインタを置くのも面倒です。そんな時、クラスにこのインターフェースクラスを多重継承します。メソッド内部で取得したステータスをメンバ変数lastStatus_に預けることで、メソッドから戻ってきてもステータス値を失うことなく処理を継続できます。

initEx関数、term関数

NotesInitExtended関数、NotesTerm関数のラッパー関数です。

PathSetクラス、NetPathクラス

Notesデータベースへのパスに使用するクラスです。PathSetは、パス、サーバ名、ポート名を一度に扱えるクラスです。NetPathはLmbcsクラスを継承して、Notes状のフルパスである「ネットパス」に対応しています。Databaseクラスを使って、データベースをオープンする場合、次のように書けます。

Database db1(PathSet("names.nsf", "Server1")); // PathSet使用
Database db2(PathSet("names.nsf", "Server2").toNetPath()); // NetPathに変換してもOK

PathSetからNetPathへはPathSet::toNetPathを使用しますが、NetPathからPathSetにする場合はNetPath::toPathSetを使うことができます。

DbInfoクラス

データベースのタイトルやカテゴリを取得する場合、Lmbcsから継承したDbInfoクラスを使用します。データベース情報はタイトル、カテゴリ、DBクラス、設計元DBクラスの4つの情報を有していますが、実体は128バイトしかないLMBCS文字列です。そこで、この文字列をDbInfo内に置き、メソッドによってタイトルなどの各情報を取得することができるようにしました。以前はDatabaseクラスに組み込まれていましたが、NSFSearch関数によって取得したサマリーバッファでも、データベース情報のパースが必要になることがわかったので、Databaseクラスから切り離し、DbInfoクラスとして独立させることにしました。

伝説のツール「NotesPeek」をQtでリメイクする(その2・ファイルの取得)

ではNotesPeekリメイク版、最初のミッションです。

最初の目標は、「ローカルのデータディレクトリから直下のファイルを取得」します。

Notes APIで、データベースの一覧を取得する関数は、NSFSearch関数です。

#include <nsfsearc.h>

STATUS LNPUBLIC NSFSearch (
    DBHANDLE hDB,
    FORMULAHANDLE hFormula,
    char far *ViewTitle,
    WORD SearchFlags,
    WORD NoteClassMask,
    TIMEDATE far *Since,
    NSFSEARCHPROC EnumRoutine,
    void far *EnumRoutineParameter,
    TIMEDATE far *retUntil
);

NSFSearchは、この引数のネーミング通りに受け止めれば、文書検索が目的の関数です。しかし、ファイルリストを取得するのにも利用します。ディレクトリに対してDBHANDLEを取得することができ、NoteClassMaskにFILE_xxxを適用すると、目的がファイル検索になるという、不思議な関数です。

このNSFSearch関数を利用するには、DBHANDLEを取得しておかなければなりません。通常、データベースへのネットパス(ポート名、サーバ名、ファイルパスを組み合わせたNotesの世界のフルパス)を元に、データベースハンドルを取得しますが、ディレクトリへのネットパスでNSFDbOpen関数を使うと、ディレクトリに対するデータベースハンドルを取得することができます。

#include <nsfdb.h>

STATUS LNPUBLIC NSFDbOpen (const char far *PathName, DBHANDLE far *rethDB);

もちろん、ディレクトリに対してデータベース操作はできません。データベースハンドルが、データベースのものか、ディレクトリのものか、判別する関数があります。

#include <nsfdb.h>

STATUS LNPUBLIC NSFDbModeGet (DBHANDLE hDB, USHORT far *retMode);

このNSFDbModeGet関数に、ハンドルとUSHORT変数へのポインタを渡すと、DB_LOADED(データベース)か、DB_DIRECTORY(ディレクトリ)かを返してくれます。

NSFSearch関数に話を戻しましょう。

2番目の引数FORMULAHANDLEは、コンパイル済み@関数の選択式へのハンドルですが、ファイル検索では使用しないので、NULLHANDLEを渡します。3番目の引数ViewNameは、前述の選択式内に@ViewTitleを使っている場合、ビューの名前を指定する必要があります。これもファイル検索では使わないので、nullptrを渡します。

4番目のSearchFlagsは、検索フラグを指定します。ここでSEARCH_FILETYPEというフラグを立てておくと、検索対象がDB中の文書から、ディレクトリ中のファイルに変わります。続く5番目のNoteClassMask(文書クラスのマスク)が、FILE_xxx(ファイル用マスク)の意味に変わります。

4番目のSearchFlagsではもう1つ、SEARCH_SUMMARYというフラグを立てておきます。これを立てておくと、サマリーバッファというデータにアクセスすることが可能になります。サマリーバッファについては、今回説明は割愛しますが、簡単に言うと「文書を開かなくても読み取り可能なデータ群」のことで、ファイル検索時には、データベースファイルに関する情報は、このサマリーバッファから取得することになります。

5番目のNoteClassMaskには、前述の通りFILE_xxxで定義されているファイル用のマスクを使って、取得したいファイルの種類を指定します。通常、FILE_DBANY(NSFファイル)、FILE_FTANY(NTFテンプレートファイル)を使いますが、他にもいくつか定義されています。また、上位8ビットがフラグビットになっていて、FILE_DIRS(ディレクトリ名も取得)、FILE_RECURSE(サブディレクトリも検索)などを組み合わせることもできます。ちなみに、下位8ビットはフラグビット状にはなっていません(0から順に取得できるファイルの種類が決められている)。

6番目のSinceは、この日時以降の文書を検索対象とするもので、ファイル検索には使用しないので、NULLを指定します。

7番目の関数ポインタは、検索対象が見つかるたびに呼び出されるコールバック関数です。形式は以下の通りです。

#include <nsfsearc.h>

typedef STATUS (LNCALLBACKPTR NSFSEARCHPROC)(
    void far *EnumRoutineParameter,
    SEARCH_MATCH far *SearchMatch,
    ITEM_TABLE far *SummaryBuffer);

最初の引数は汎用ポインタで、検索対象のデータを格納するためのコンテナ変数へのポインタを渡すのに使用します。2番目はSEARCH_MATCH構造体へのポインタです。文書IDや文書クラス、文書の権限などの情報が含まれます。また、選択式にマッチしているかどうかを判定するのに使うメンバSERetFlagsもあります。通常、ファイル検索では使用しません。最後がサマリーバッファへのポインタです。

いったん、NSFSearch関数に話を戻します。8番目のEnumRoutineParameterは、さきほどのコールバック関数の最初の引数に渡される汎用ポインタです。9番目の引数は、検索の終了日時が返されます。次にNSFSearch関数を呼び出す時に、Sinceにこの日時を指定することで、検索対象の日時を重複させず、前回以降に追加修正があった文書だけを対象にすることができます。もちろん、ファイル検索には使用しないので、NULLを指定します。

さて、最後にサマリーバッファについて、ファイル検索時に必要な、最低限のポイントに触れておきます。

サマリーバッファは、コールバック関数に渡されるSummaryBuffer(ITEM_TABLEへのポインタ)を使ってアクセスします。ポインタ構造としては、以下のようになっています(例はアイテムが3つの場合)。

ITEM_TABLE ITEM ITEM ITEM NAME VALUE NAME VALUE NAME VALUE
ITEM_TABLE 1 2 3 1 1 2 2 3 3

(Markdown、タイトルなくてもいいようにならないかな?)

ITEM_TABLE::Itemsに、ITEM構造体がいくつあるかが示されていて、ITEM_TABLEの直後のポインタから始まっています。ITEM構造体には、名前の長さと値の長さが格納されていて、前述の通り、ITEM構造体の個数分の直後から配置されています。名前はchar*として示されたバイト数分だけ取得すればOKです。値の方はひとひねりあり、値が示している最初の2バイトは、値の型(アイテム型)を表しています。単一文字列はTYPE_TEXT、数値はTYPE_NUMBER、日時はTYPE_TIMEといったWORD型のデータです。ですから、値の取得には最初の2バイトで型を判別し、3バイト目からITEM::ValueLength - sizeof(WORD)の長さで値を取得してくる必要があります。ポインタの移動についても処理が煩雑になるので、少々面倒な作業になります。

ただ、型が単一文字列(テキスト型)で、名前もわかっていれば、NSFGetSummaryValue関数が使えます。

#include <nsfnote.h>

BOOL LNPUBLIC NSFGetSummaryValue (const void far *SummaryBuffer, const char far *Name, char far *retValue, WORD ValueBufferLength);

NSFGetSummaryValueは、サマリーバッファから既知の名前のテキストデータを取得できます。1番目にサマリーバッファへのポインタ、2番目に名前(LMBCS)、3番目に文字列格納用のバッファ、4番目にそのバッファの長さを指定します。成功すれば0以外が返ります。

ファイル名を取得するには、DBDIR_PATH_ITEMかFIELD_TITLEを、データベースタイトルを含むDB情報を取得するにはDBDIR_INFO_ITEMを指定すればOKです。Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングには、データベースタイトルの取得にFIELD_TITLEを使うと書かれていましたが、当時の仕様から変更されたのか、FIELD_TITLEではファイル名しか取得できないので、注意しましょう。

NotesPeekリメイクアプリケーションの暫定名を、「nsfinder」としました。そのデモ画面(Mac版)です。

f:id:takahide-kondoh:20170402163419p:plain

NotesPeekと同じ、ツリー構造です。「Local」というローカルディレクトリを表すアイテムが初期配置されています。

f:id:takahide-kondoh:20170402163436p:plain

Localで右クリックを押すと、コンテキストメニューが表示されます。Refresh Childrenをクリックします。

f:id:takahide-kondoh:20170402163443p:plain

ローカルのデータディレクトリが検索され、該当するNSFファイルのファイル名とデータベース名が表示されます。

構築環境は、以前のIntroQtデモと同じです。nsfinder本体と、ntlxライブラリのコードはBitbucketから取得できます。

Bitbucket/ntlxライブラリ

クローンをダウンロードしたあと、タグv0.1.1をチェックアウトしてください。

Bitbucket/nsfinder本体

こちらはタグv0.9.0をチェックアウトしてください。

Mac以外にWindowsLinux(Ubuntu)でも動作確認しています。Qt Creatorなどの設定は、以前の記事をご覧ください。

伝説のツール「NotesPeek」をQtでリメイクする(その1)

NotesPeek。ご存じですか。

NotesPeekは、Notesのネットワークパス、データベース、文書、フィールド、リッチテキスト構造といった、Notesのあらゆる階層をインスペクトできるツールです。Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングに以下のように載っていました。

NotesPeekはIris社のエンジニアである、Mr. Ned Batchelder氏が開発したノーツデータベースの中身を覗くためのツールです。C API toolkitには含まれていませんが、ノーツデータベースの仕組みを知るのに大変役立ちます。

氏は、サイトを持っておられるようです。

nedbatchelder.com

NotesPeekの方は、IBMに以下の記事が見つかりました。

www-01.ibm.com

どうもNotesが6の時に1.53が出たきりのようですが、7、8でも問題なく使えると書かれています。おそらく、9でも問題なく使えると思います。

非常に優秀なNotesPeekですが、UIも古く、OSもWindowsのみです。

もちろん、問題ないです。調べるという目的に、UIやOSは問題になりません。

ただ、これほどリスペクトするツールなので、いつかリメイクしてみたい、カバーしてみたいという思いが、ずっとありました。

Qtを使って開発した経験を活かし、時間はかかるかもしれませんが、NotesPeekをWindowsMacUbuntuで実行できるリメイクアプリ、カバーアプリを作ってみたいと思います。

NotesとQtでWindows、Mac OS X、Ubuntuのデスクトップアプリ(その11 - 実行・完結編)

それでは、前回までのライブラリを使用して、一気にデスクトップアプリ「IntroQt」を作ります。

まず、メインソースコードです。

// main.cpp

#include "dialog.h"
#include <QApplication>

#include <lmbcs.h>

int main(int argc, char *argv[])
{
  ntlx::Status status = NotesInitExtended(argc, argv);
  if (status.failure())
    return -1;

  QApplication a(argc, argv);
  Dialog w;
  w.show();

  int result = a.exec();

  NotesTerm();

  return result;
}

NotesInitExtended関数は、Notes APIを初期化します。アドインライブラリやサーバータスクのような、NotesクライアントやDominoサーバから呼び出すことを前提とするものを除き、Notes APIを使用する場合は必ず初期化します。

プログラムを終える時は、NotesTermで終了処理をします。

次は、ダイアログクラスの定義(ヘッダーファイル)です。

// dialog.h

#ifndef DIALOG_H
#define DIALOG_H

#include <QDialog>

namespace Ui {
class Dialog;
}

class Dialog : public QDialog
{
  Q_OBJECT

public:
  explicit Dialog(QWidget *parent = 0);
  ~Dialog();

public slots:
  void pathChanged(const QString& path);
  void getTitle();

private:
  Ui::Dialog* ui_;
};

#endif // DIALOG_H

Dialogクラスは、Qt WidgetのQDialogを継承します。UI部品は、Qt Designerを使って作成します。Ui::Dialogクラスはその連携クラスになります。

Qtのシグナル/スロット機構は、とてもよくできた通知システムだと思います。今回は、パスの内容を変更するとpathChangedが、タイトル取得ボタンをクリックするとgetTitleがそれぞれシグナル/スロット機構を通して呼び出されるようにしています。

Qtのシグナル/スロット機構を詳しく知りたい方は、Webなどを検索してみてください。日本語書籍であれば、入門 Qt4 プログラミングがおすすめです。10年も前の本ですし、Qt4の時代のものですが、遜色なく読めると思います。あえて追記するなら、Qt5で新しいシンタックスが登場しているので、英語ですが、New Signal Slot Syntax in Qt 5をご覧ください。

次はDialogクラスの実装(ソースファイル)です。

// dialog.cpp

#include "dialog.h"
#include "ui_dialog.h"

#include <QMessageBox>
#include <database.h>
#include <lmbcs.h>

Dialog::Dialog(QWidget *parent)
  : QDialog(parent)
  , ui_(new Ui::Dialog)
{
  ui_->setupUi(this);

  connect(ui_->buttonBox->button(QDialogButtonBox::Close)
          , &QPushButton::clicked
          , this, &Dialog::close
          );

  connect(ui_->pathLineEdit, &QLineEdit::textChanged
          , this, &Dialog::pathChanged
          );

  connect(ui_->getTitleButton, &QPushButton::clicked
          , this, &Dialog::getTitle
          );
}

Dialog::~Dialog()
{
  delete ui_;
}

void Dialog::pathChanged(const QString &path)
{
  ui_->getTitleButton->setEnabled(!path.isEmpty());
}

void Dialog::getTitle()
{
  ntlx::Status status;
  ntlx::Database db(ui_->pathLineEdit->text()
                    , ui_->serverLineEdit->text()
                    , QString()
                    , &status);
  if (status.failure())
    QMessageBox::critical(this
                          , tr("Error")
                          , ntlx::Lmbcs(status).toQString()
                          );

  QString title = db.getTitle();
  ui_->titleLineEdit->setText(title);
}

pathChangedスロットメソッドは、パス入力欄のテキスト変更を検知するとスロットされます。受け取った変更済みテキストが空かどうかをチェックして、タイトル取得ボタンを有効にするかどうかを設定します。Notesデータベースはサーバ名は省略できますが、パスは省略できないので、このスロットは空のテキストを処理しないための仕組みです。

タイトル取得ボタンをクリックすると、getTitleスロットメソッドが呼び出されます。ntlxライブラリを活用して、データベースのタイトルを取得し、もう一つの入力欄に表示します。

続いて、Ui::DialogクラスのQt Designer定義です。

<!-- dialog.ui -->

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Dialog</class>
 <widget class="QDialog" name="Dialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>320</width>
    <height>199</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Dialog</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <layout class="QFormLayout" name="formLayout_3">
     <item row="0" column="0">
      <widget class="QLabel" name="serverLabel">
       <property name="text">
        <string>Server</string>
       </property>
      </widget>
     </item>
     <item row="0" column="1">
      <widget class="QLineEdit" name="serverLineEdit"/>
     </item>
     <item row="1" column="0">
      <widget class="QLabel" name="pathLabel">
       <property name="text">
        <string>Path</string>
       </property>
      </widget>
     </item>
     <item row="1" column="1">
      <widget class="QLineEdit" name="pathLineEdit"/>
     </item>
    </layout>
   </item>
   <item>
    <widget class="QPushButton" name="getTitleButton">
     <property name="enabled">
      <bool>false</bool>
     </property>
     <property name="text">
      <string>Get Title</string>
     </property>
    </widget>
   </item>
   <item>
    <layout class="QFormLayout" name="formLayout_2">
     <item row="0" column="0">
      <widget class="QLabel" name="titleLabel">
       <property name="text">
        <string>Title</string>
       </property>
      </widget>
     </item>
     <item row="0" column="1">
      <widget class="QLineEdit" name="titleLineEdit"/>
     </item>
    </layout>
   </item>
   <item>
    <spacer name="verticalSpacer">
     <property name="orientation">
      <enum>Qt::Vertical</enum>
     </property>
     <property name="sizeHint" stdset="0">
      <size>
       <width>20</width>
       <height>4</height>
      </size>
     </property>
    </spacer>
   </item>
   <item>
    <widget class="QDialogButtonBox" name="buttonBox">
     <property name="standardButtons">
      <set>QDialogButtonBox::Close</set>
     </property>
    </widget>
   </item>
  </layout>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources/>
 <connections/>
</ui>

さすがにこれはソースコードを見ただけではわかりにくいでしょう。後半にイメージを載せているので、参考にしてみてください。

最後にプロジェクトファイルです。

# introqt.pro

#-------------------------------------------------
#
# Project created by QtCreator 2017-03-11T16:33:52
#
#-------------------------------------------------

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = introqt
TEMPLATE = app

# The following define makes your compiler emit warnings if you use
# any feature of Qt which as been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += main.cpp\
        dialog.cpp

HEADERS  += dialog.h

FORMS    += dialog.ui

DISTFILES += \
    .gitignore

win32 {
    DEFINES += W32 NT
}
else:macx {
    DEFINES += MAC
}
else:unix {
    DEFINES += UNIX LINUX W32
    QMAKE_CXXFLAGS += -std=c++0x
}

LIBS += -lnotes -lntlx

INCLUDEPATH += $$PWD/../ntlx
DEPENDPATH += $$PWD/../ntlx

インクルードパスとライブラリに、ntlxを追加しています。「$$PWD」は「現在のソースコードの場所」を意味します。本来ならインクルードパスはビルド時の設定に逃がすべきですが、同一プロジェクトフォルダ上でコーディングしていれば、このような書き方も有効です。

コード類は以上です。

次は、各プラットフォームごとのビルド設定、実行時設定のトピックです。

# Windows

## ビルド時の追加の引数

"INCLUDEPATH+=Z:/Users/Shared/notesapi/include" "LIBS+=-LZ:/Users/Shared/notesapi/lib/mswin32 -L$$PWD/../build-ntlx-Desktop_Qt_5_6_2_MSVC2013_32bit-Debug/debug"

## 実行時の環境変数

PATH=%PATH%;C:\Program Files (x86)\IBM\Notes

Windowsでは、Unix系と違い、パスの区切りがバックスラッシュ(日本語での表記は円記号)になります。qmakeに渡すパスは、Windowsであってもスラッシュを使います。

Notes APIへのパスに加え、ntlxへのパスも追加します。ntlxのインポートライブラリを指すように、相対パスで指定しています。

# Mac

## ビルド時の追加の引数

"INCLUDEPATH+=/Users/Shared/notesapi/include" "LIBS+=-L'/Applications/IBM Notes.app/Contents/MacOS' -L\$\$PWD/../build-ntlx-Desktop_Qt_5_6_2_clang_64bit-Debug"

MacLinuxのようなUnix系の場合、「$」記号は意味を持ってしまうため、バックスラッシュでエスケープする必要があるため、「\$\$PWD」のような書き方になります。

# Linux(Ubuntu)

## ビルド時の追加の引数

"INCLUDEPATH+=/opt/ibm/notesapi/include" "LIBS+=-L/opt/ibm/notes -Wl,-rpath,/opt/ibm/notes -L\$\$PWD/../build-ntlx-Desktop_Qt_5_5_1_GCC_32bit-Debug"

## 実行時の環境変数

Notes_ExecDirectory=/opt/ibm/notes

Linux(Ubuntu)では、実行時に環境変数「Notes_ExecDirectory」を使って、Notesプログラムディレクトリを見つけます。ビルド時には必要ないですが、実行時には指定するようにします。

ここまでで問題がなければ、各OSでコンパイルしてデバッグ実行してみましょう。

Windows

f:id:takahide-kondoh:20170324144850p:plain

Windowsのアプリケーション例です。パスワードが必要な場面になると、次のようなダイアログを表示します。

f:id:takahide-kondoh:20170324144837p:plain

コンソールアプリケーションであれば、標準入力から入力することになります。

MacOSX

f:id:takahide-kondoh:20170324144728p:plain

Macのアプリケーション例です。私の環境では、Macではパスワードが必要な場面で標準入力から入力するようです。ダイアログが表示されてしかるべきなのですが、方法がわかりません。Qt Creatorデバッグ実行が、標準入力を握ってしまい、どうしてもパスワードを入力できませんでした。結局、Mac版Notesクライアント独特の機能「キーチェーンにパスワードを保存する」で、一種のパスワードレス状態にすることで、デバッグ実行することができました。

Linux(Ubuntu)

f:id:takahide-kondoh:20170324144811p:plain

Ubuntuのアプリケーション例です。パスワードが必要な場面になると、次のようなダイアログを表示します。

f:id:takahide-kondoh:20170324144759p:plain

未確認ですが、コンソールアプリケーションであれば、標準入力から入力することになると思います。

最後は、ほぼほぼ駆け足ですが、Qtデスクトップアプリケーションとしてはとても一般的な話であるのでご容赦ください。

Notes/DominoとQtフレームワーク、どちらもマルチプラットフォームに対応しています。一度これを、一つのソースコードで、それぞれのOS用にコンパイルして、実行してみたいという小さな夢が実現できました。長編をおつきあいくださり、ありがとうございました。