APIから見るEvernoteのノートとNotes

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

これは、Evernoteのノート、NoteのAPIから見た構造です。ぼーっと眺めてみます。

とてもNotesっぽいですね。例えばこの構造を、Notesでなじみのある用語と比べてみると・・・

guid = unid
title = subject/title field
content = body rich text field
created = created
updated = last modified
notebookGuid = replica id
resources = $FILE(添付ファイル)
tagNames (like) categories field

・・・といった感じでしょうか。

何ってわけではないんですが、Notesの文書にとても似てます。

相互に交換できるといいですよねぇ。

いえいえ、何ってわけではないんです。本当に。

伝説のツール「NotesPeek」をQtでリメイクする(その12・サーバリストの取得)

今回のNSFinderでは、NotesPeekと同様に、ツリービューのルートに「Local」「Servers」という2つのアイテムを配置して、LocalにはNotesクライアントのデータディレクトリを、Serversには利用可能なDominoサーバのリストを取得してアイテム化します。

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

サーバリストとは、NotesクライアントおよびAPIから見て、現在利用可能なDominoサーバのことで、サーバリストを取得するには、NSGetServerList関数を使用します。

// #include <ns.h>

STATUS LNPUBLIC NSGetServerList (char far *pPortName, DHANDLE far *retServerTextList);

pPortNameは、ポート名をLMBCS形式で指定します。デフォルトでよければ0を渡します。retServerTextListは、サーバ名のテキストリストに対応したハンドルを取得するためのポインタになります。

返ってくるハンドルをロックすると、次の順番で格納されたメモリブロックを取得できます。

  • サーバの数count(サイズ:WORD)

  • サーバ名の長さ×サーバ数(サイズ:WORD×count)

  • サーバ名×サーバ数

これと似た構造に、「その11」で説明したテキストリストがあります。

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

伝説のツール「NotesPeek」をQtでリメイクする(その10・リスト型/範囲型) - Chiburu Systemsのブログ

先頭のWORD型がLIST構造体である以外は同じ構造です。また、LIST構造体はUSHORT型メンバーを1つしか持っておらず、そのUSHORT型は、32bitアプリではWORD型と同じなので、バイト数的にも同じになります。

それでは、私がこの関数を使って、NTLXライブラリにインプリメントした例をご紹介しておきます。まずはヘッダ部です。

// ntlx/status.h

/**
 * @brief サーバリストを取得する
 * @param list 格納先となるQStringのリストオブジェクトへの参照
 * @param port 検索するポート名
 * @return 処理ステータス
 */
NTLXSHARED_EXPORT Status getServers(
    QList<QString>& list
    , const QString& port = ""
    );

ポート名はQString型、受け取るサーバリストはQListで取得するようにします。

// status.cpp

Status getServers(QList<QString>& list, const QString& port)
{
  HANDLE handle = NULLHANDLE;
  Status result = NSGetServerList(
        port.isEmpty() ? 0 : Lmbcs::fromQString(port).data()
        , &handle
        );
  if (result.failure())
    return result;
  Q_ASSERT(handle);

  WORD* pLen = (WORD*)OSLockObject(handle);
  WORD count = *pLen++;
  char* pValue = (char*)(pLen + count);
  for (WORD i = 0; i < count; ++i)
  {
    Lmbcs value(pValue, *pLen);
    list.append(value.toQString());
    pValue += *pLen++;
  }

  OSUnlockObject(handle);
  OSMemFree(handle);
  return result;
}

構造自体はテキストリストの時と大差はありません。ポイントは、受け取ったテキストリストのハンドルは、受け取った側の責任で解放する必要がありますので、OSMemFreeを使います。

サーバリストは、あくまで現在利用可能なものなので、この関数で取得できたDominoサーバしかないというわけではありません。実際にはドミノディレクトリなどの登録情報を正として、この関数は補助や疎通用として利用する方がよいでしょう。詳しい疎通情報には、NSPingServerという関数を使うことができます。この関数では通りやすさはを0〜100で返してきます。100がさくさく通っており、0が通らない状態です。

次回はライブラリ側の最後のトピックとして、「名前(DomainName)」を説明したいと思います。

伝説のツール「NotesPeek」をQtでリメイクする(その11・範囲型の読み込み)

前回は、Notes APIにおける複数値、LIST型とRANGE型について説明しました。テキストの複数値はLIST型を、数値と日時はRANGE型を使って複数値を表現しています。RANGE型は2種類のリストを持っており、1つは単数値のリスト、もう1つは「範囲」というペア値のリストを同時に持ちます。そしてこのペア値は、数値も日時もLowerとUpperという値を持っています。

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

数値の複数値も、日時の複数値も、値が数値か日時かの違いのみで、それ以外の構造はまったく同じです。ということは、C++テンプレートで実装するにはうってつけでしょう。

それでは、どのように実装してみたのか、ソースコードを見ながら説明していきます。

今回、複数値も含めてNSFSearch関数で取得したサマリーバッファを格納するクラスとして、ntlx::SummaryMapクラスを定義しました。

// ntlx/summarymap.h抜粋

namespace ntlx
{

/**
 * @brief 名前付きサマリーバッファ用データクラス
 */
class NTLXSHARED_EXPORT SummaryMap
{
public:
  /**
   * @brief デフォルトコンストラクタ
   */
  SummaryMap();

  /**
   * @brief コピーコンストラクタ
   * @param other コピー元
   */
  SummaryMap(const SummaryMap& other);

  /**
   * @brief 代入演算子
   * @param other 代入元
   * @return 自身への参照
   */
  SummaryMap& operator=(const SummaryMap& other);

  /**
   * @brief 検索マッチ情報
   * @return SEARCH_MATCH型データへの参照
   */
  const SEARCH_MATCH& match() const { return match_; }

  QMap<QString, QVariant>& map() { return map_; }

  /**
   * @brief load NSFSearch関数のコールバック情報を取り込む
   * @param pMatch 検索マッチ情報
   * @param pItemTable サマリーバッファへのポインタ
   */
  void load(const SEARCH_MATCH* const pMatch, ITEM_TABLE* pItemTable);

  /**
   * @brief 型タイプに合わせてデータを取得する
   * @param type 型タイプ
   * @param ptr データへのポインタ
   * @param len データ長
   * @return データのQVariantオブジェクト
   */
  static QVariant getValue(WORD type, char* ptr, USHORT len);

private:
  SEARCH_MATCH match_;
  QMap<QString, QVariant> map_;
};

/**
 * @brief サマリーバッファのリストクラス
 */
typedef QList<SummaryMap> SummaryList;

} // namespace ntlx

ただし、今回はgetValueメソッドから数値リスト、日時リストを処理するためのテンプレート関数にフォーカスします。getValueメソッドの中で、数値リストと日時リストを呼び出す場所を抜粋します。

// summarymap.cpp抜粋

  case TYPE_NUMBER:
    return *(NUMBER*)ptr;

  case TYPE_NUMBER_RANGE:
    return getRangeValue<Number, NUMBER, NUMBER_PAIR>(ptr, "number");

  case TYPE_TIME:
  {
    TimeDate td;
    td.value_ = *(TIMEDATE*)ptr;
    return td.toQDateTime();
  }
    break;

  case TYPE_TIME_RANGE:
    return getRangeValue<TimeDate, TIMEDATE, TIMEDATE_PAIR>(ptr, "timedate");

ptrはデータの開始ポインタを表しています。サマリーバッファにおける値の開始位置は、WORD型のデータ種別を表すので、実際の開始位置はsizeof(WORD)分だけずれます。

NUMBER型(数値)の場合は、ポインタの開始位置をNUMBER型として取得すればOKです。

TIMEDATE型(日時)の場合は、TIMEDATE構造体のポインタに置き換えればいいのですが、以前に定義したTimeDateクラスを応用して、QDateTimeオブジェクトに変換しています。

それでは、本題のRANGE(範囲)型のデータを取得してみましょう。

数値と日時範囲の取得には、関数テンプレートgetRangeValueを使うことにします。この関数は、クラス、単一データリストの型、ペアデータ(範囲データ)リストの型をパラメータに取ります。

// summarymap.cpp抜粋

template<class T, typename S, typename P>
QVariant getRangeValue(char* ptr, const QString& typeName)
{
  RANGE* pRange = (RANGE*)ptr;
  S* pSingle = (S*)(pRange + 1);
  P* pPair = (P*)(pSingle + pRange->ListEntries);
...

ポインタの最初の位置からRANGE型を取得します。ポインタにおいて、変数のサイズ分だけポインタを移動するには、データ型ポインタに1を足します(詳細はWebで)。RANGE型の次のポインタにあるのは「単一データのリスト」のスタート地点です。さらに、単一データからRANGE.ListEntries分ポインタを進めると、ペアデータ(範囲データ)のスタート地点を取得できます。

...
  QList<QVariant> list;
  for (USHORT i = 0; i < pRange->ListEntries; ++i, ++pSingle)
  {
    T item(*pSingle);
    list.append(item.toVariant());
  }
...

最初に単一データを取得します。RANGE.ListEntries分ループを回して、Tクラスのオブジェクトを作成し、リストにそのQVariant値を追加します。TクラスとはNumberクラスかTimeDateクラスを表します。T.toVariant()メソッドについては後述します。

...
  QList<QVariant> pairList;
  for (USHORT i = 0; i < pRange->RangeEntries; ++i, ++pPair)
  {
    T lower(pPair->Lower);
    T upper(pPair->Upper);
    QMap<QString, QVariant> pair;
    pair.insert("lower", lower.toVariant());
    pair.insert("upper", upper.toVariant());
    pairList.append(pair);
  }
...

続いてペアデータを取得します。RANGE.RangeEntries分ループを回して、TクラスのP.Lower、P.Upperそれぞれに対応したオブジェクトを作成し、それらをQVariant値に変換してQMapオブジェクトを作成し、リストに追加します。なお、ここでQPairを使う方がシンプルですが、デフォルトではQPairをQVariantに変換できないため、ここではQMapを使用しています。

...
  QMap<QString, QVariant> value;
  value.insert("list", list);
  value.insert("range", pairList);
  value.insert("type", typeName);
  return value;
}

最後に、取得した単一データリスト、ペアデータリストをQMapのlist、rangeのデータとして挿入します。単にこのマップデータを返しただけでは数値なのか日時なのかわからないので、最後にtypeとして型名文字列を挿入しています。

ここでさきほどのT.toVariant()メソッドについて。このメソッドはTオブジェクトからQVariantデータを取得するのが目的になりますが、実を言えば、このようなテンプレートのために設けたと言っても過言ではありません。TIMEDATEについては、TimeDateクラスで以下のように実装しています。

// timedate.cpp抜粋


QVariant TimeDate::toVariant() const
{
  return QVariant(toQDateTime());
}

TimeDate.toQVariantは、TIMEDATE値をQVariantに対応しているQDateTimeオブジェクトに変換してQVariantを返します。

もう一方のNUMBER(数値型)ですが、今までラッパークラスを設けてきませんでした。そこで、今回C++テンプレートのためにラッパークラスを設けました。

// ntlx/number.h

#ifndef NTLX_NUMBER_H
#define NTLX_NUMBER_H

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

class QVariant;

namespace ntlx
{

/**
 * @brief 数値ラッパークラス
 */
class NTLXSHARED_EXPORT Number
{
public:
  /**
   * @brief デフォルトコンストラクタ
   */
  Number();

  /**
   * @brief コンストラクタ(NUMBER型から)
   * @param value NUMBER値
   */
  Number(const NUMBER& value);

  /**
   * @brief コピーコンストラクタ
   * @param other コピー元
   */
  Number(const Number& other);

  /**
   * @brief 代入演算子
   * @param other 代入元
   * @return 自身への参照
   */
  Number& operator=(const Number& other);

  /**
   * @brief 文字列に変換する
   * @return 数値文字列
   */
  QString toString() const;

  /**
   * @brief QVariantオブジェクトに変換する
   * @return QVariant値
   */
  QVariant toVariant() const;

  /**
   * @brief QVariant型からNumber型に変換する
   * @param var QVariant値
   * @return Numberオブジェクト
   */
  static Number fromVariant(const QVariant& var);

private:
  NUMBER value_;
};

} // namespace ntlx

#endif // NTLX_NUMBER_H
// number.cpp

#include "ntlx/number.h"
#include <QVariant>

namespace ntlx
{

Number::Number()
  : value_(0.)
{
}

Number::Number(const NUMBER &value)
  : value_(value)
{
}

Number::Number(const Number &other)
  : value_(other.value_)
{
}

Number& Number::operator=(const Number& other)
{
  if (this == &other) return *this;
  value_ = other.value_;
  return *this;
}

QString Number::toString() const
{
  return QString::number(value_);
}

QVariant Number::toVariant() const
{
  return QVariant(value_);
}

Number Number::fromVariant(const QVariant &var)
{
  return Number(var.toDouble());
}

} // namespace ntlx

これにより、少なくとも関数テンプレートgetRangeValueにおいては、日時型TIMEDATEと数値型NUMBERは、範囲型TIMEDATE_PAIRとNUMBER_PAIRとともに、TimeDateクラスとNumberクラスを伴って同等の扱いをすることが可能になりました。

今回は、Cポインタ、関数テンプレート、QtのQVariantといういろいろな手法を取り入れて、数値と日時の「範囲型」データの取得に挑んでみました。NTLXライブラリの追加・変更点はまだいくつかありますので、順に説明していきたいと思います。

伝説のツール「NotesPeek」をQtでリメイクする(その10・リスト型/範囲型)

Notesの基本的な「型」は、テキスト(文字列)型、数値型、日時型になります。これら基本型は、いずれも複数値になることは皆さんご存じのことと思います。

  • テキストリスト型
  • 数値リスト型
  • 日時リスト型

このうちテキストリストは、LISTという構造体が使われています。

typedef struct {
  USHORT ListEntries; /* list entries following */
                      /* now come the list entries */
} LIST;

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

この図は文字列リストのメモリ配置を示していて、LIST.ListEntries=3の場合を表しています。L1〜L3はUSHORT型で、文字列1〜3の長さを表しています。

LISTが適用できるのはテキスト型で、TYPE_TEXT_LISTという型で判別します。では、数値や日時ではLIST型でなく、何が使われているのでしょう。

実は数値と日時には、単純なリストはなく、「RANGE」という構造体で複数値を表します。型の判別にはTYPE_NUMBER_RANGE、TYPE_TIME_RANGEという値でそれぞれ判別します。それではRANGE構造体を見てみましょう。

typedef struct {
  USHORT ListEntries;   /* list entries following */
  USHORT RangeEntries;  /* range entries following */
                        /* now come the list entries */
                        /* now come the range entries */
} RANGE;

ListEntriesの他にRangeEntriesというメンバーが存在します。このRangeEntriesは、「範囲」リストの数を表します。

「範囲」という概念は、日時でいう「何日から何日」、「何時から何時」というものを表し、主にカレンダーや予定などで使用されます。Notes APIから見ると、日時型の範囲はTIMEDATE_PAIRという構造体で表されます。

typedef struct { /* a timedate range entry */
  TIMEDATE Lower;
  TIMEDATE Upper;
} TIMEDATE_PAIR;

RangeEntriesは、このTIMEDATE_PAIR型のリストのアイテム数を表していたんですね。一方で数値の方は、NUMBER_PAIRという構造体があります。

typedef struct {  /* a float range entry */
  NUMBER Lower;
  NUMBER Upper;
} NUMBER_PAIR;

このNUMBER_PAIRですが、Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングによると「Notesクライアントからこれを入力することはできない」と書かれています。この本はNotes R5の頃に書かれていますが、現在のNotes 9でも数値範囲を入力できないのであれば、利用することはあまりないでしょう。ですが、APIでなら操作できるのであれば、扱えるようにしておこうと思います。

TYPE_NUMBER_RANGE、TYPE_TIME_RANGEのデータ構造は以下のようになります。

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

せっかく数値のRANGEと日時のRANGEが同じ構造をしているので、C++テンプレートの機能を使って、実装してみることにしましょう。(続く)

伝説のツール「NotesPeek」をQtでリメイクする(その9・UI見直し)

今回はUIの見直しです。

前回まではツリー構造のビューにひたすら追加情報を列として表示していましたが、いろいろな情報が同じ列に表示していては必ず破綻するので、今のうちに、アイテムの情報を別のペインにしておきます。

Mac

f:id:takahide-kondoh:20170611172332p:plain f:id:takahide-kondoh:20170611172349p:plain f:id:takahide-kondoh:20170611172402p:plain

Ubuntu

f:id:takahide-kondoh:20170611172418p:plain f:id:takahide-kondoh:20170611172434p:plain f:id:takahide-kondoh:20170611172445p:plain

Windows

f:id:takahide-kondoh:20170611172455p:plain f:id:takahide-kondoh:20170611172508p:plain f:id:takahide-kondoh:20170611172521p:plain

ntlxライブラリのソースコードリポジトリこちらです。タグ「v0.1.4」をチェックアウトしてください。

https://tkondoh2@bitbucket.org/tkondoh2/ntlx.git

NSFinder本体のソースコードリポジトリこちらです。タグ「v0.9.3」をチェックアウトしてください。

https://tkondoh2@bitbucket.org/tkondoh2/nsfinder.git

今回は画面とソースコードの紹介にとどめ、次回以降に今回のUI見直しについて、ポイントをご紹介していきます。

伝説のツール「NotesPeek」をQtでリメイクする(その8・更新日時)

日時を扱うクラスを設計してから少し時間がたちましたが、NSFinderで実際に活用してみたいと思います。

Mac

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

Ubuntu

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

Windows

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

ntlxライブラリのソースコードリポジトリはこちらです。タグ「v0.1.3」をチェックアウトしてください。

https://tkondoh2@bitbucket.org/tkondoh2/ntlx.git

NSFinder本体のソースコードリポジトリはこちらです。タグ「v0.9.2」をチェックアウトしてください。

https://tkondoh2@bitbucket.org/tkondoh2/nsfinder.git

今回のリリースでは、日時を扱うクラスTimeDateを使って、データベースの更新日時、文書の更新日時を取得してみます。

TimeDateクラス

TimeDateクラスはntlxライブラリの中で定義しています。ソースコードリポジトリを参考にしてください。

#ifndef NTLX_TIMEDATE_H
#define NTLX_TIMEDATE_H

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

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

#include <misc.h>
#include <ostime.h>

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

class QDateTime;
class QDate;
class QTime;

namespace ntlx
{

class Lmbcs;

class NTLXSHARED_EXPORT TimeDate
{
public:
  /**
   * @brief デフォルトコンストラクタ
   */
  TimeDate();

  /**
   * @brief TIMEDATE構造体を元にしたコンストラクタ
   * @param value TIMEDATE型の日時値
   */
  TimeDate(const TIMEDATE& value);

  /**
   * @brief コピーコンストラクタ
   * @param other コピー元
   */
  TimeDate(const TimeDate& other);

  /**
   * @brief 代入演算子
   * @param other 代入元
   * @return 自身への参照
   */
  TimeDate& operator=(const TimeDate& other);

  /**
   * @brief 現在の日時を取得する
   * @return 現在の日時データ
   */
  static TimeDate current();

  /**
   * @brief 最小値(特殊値)を取得する
   * @return 最小値(特殊値)
   */
  static TimeDate minimum();

  /**
   * @brief 最大値(特殊値)を取得する
   * @return 最大値(特殊値)
   */
  static TimeDate maximum();

  /**
   * @brief ワイルドカード(特殊値)を取得する
   * @return ワイルドカード(特殊値)
   */
  static TimeDate wildcard();

  /**
   * @brief 値をQDateTime型に変換して取得する
   * @return QDateTime型値
   */
  QDateTime toQDateTime() const;

  /**
   * @brief 値をLMBCSテキストに変換する
   * @param intlFormat 国際フォーマット
   * @param textFormat テキストフォーマット
   * @param status 結果ステータス
   * @return Lmbcsオブジェクト
   */
  Lmbcs toLmbcs(
      void* intlFormat = nullptr
      , TFMT* textFormat = nullptr
      , Status* status = nullptr
      ) const;

  /**
   * @brief QDateTime型の値をTimeDateに変換して取得する
   * @param from QDateTime型値
   * @return TimeDate型値
   */
  static TimeDate fromQDateTime(const QDateTime& from);

  /**
   * @brief LMBCSテキストからTimeDate型値を取得する
   * @param lmbcs Lmbcsオブジェクト
   * @param intlFormat 国際フォーマット
   * @param textFormat テキストフォーマット
   * @param status 結果ステータス
   * @return TimeDate型値
   */
  static TimeDate fromLmbcs(
      Lmbcs& lmbcs
      , void* intlFormat = nullptr
      , TFMT* textFormat = nullptr
      , Status* status = nullptr
      );

  TIMEDATE value_;

protected:
  template<WORD T>
  static TimeDate getConstant()
  {
    TimeDate td;
    TimeConstant(T, &td.value_);
    return td;
  }
};

} // namespace ntlx

#endif // NTLX_TIMEDATE_H

・・・不要な前方宣言(class QDate, class QTime)が残っていました。すみません。

メソッドについては、コメントを参考にしてください。メンバ変数value_は、プライベートにしていません。これは、Notes APIの利便性を優先させました。TIMEDATE構造体を引数に取るAPIは多く、それらを毎回、TIMEDATE型変数定義、APIコール、TimeDateオブジェクトに変換、としていては効率がよくありません。パブリックであれば、TimeDateオブジェクトを定義して、そのままメンバをAPIの引数にすればいいことになります。今回は、この方針で行きます。

これである程度、日時型変数が扱いやすくなったので、このTimeDateクラスを用いて、更新日時を取得し、UIに表示してみます。

データベースの更新日時

まずはデータベースの更新日時です。データベースの更新日時は、NSFDbModifiedTime関数を呼び出します。

#include <nsfdb.h>

STATUS LNPUBLIC NSFDbModifiedTime (DBHANDLE hDB, TIMEDATE far *retDataModified, TIMEDATE far *retNonDataModified);

この関数では、データベースハンドルを引数にして、文書の最終更新日時(retDataModified)、設計の最終更新日時(retNonDataModified)を取得することができます。

これを今回は、DatabaseクラスにgetModifiedPairメソッドとして定義します。

#include <ntlx/database.h>

/**
 * @brief 更新日付を取得する
 * @return 文書(1st)と設計(2nd)の更新日時
 */
QPair<ntlx::TimeDate, ntlx::TimeDate> ntlx::Database::getModifiedPair();

戻り値はQPairコンテナを使っています。QPairはC++のstd::pairのQt版で、1対のデータをまとめて扱えるテンプレートクラスです。コメントにもあるように、1つめを文書、2つめを設計の更新日時として取得できます。例えば、以下のように書くことができます。

QPair<TimeDate, TimeDate> modified = db.getModifiedPair();
cout << "Document: " << modified.first.toLmbcs().data()
     << ", Design: " << modified.second.toLmbcs().data()
     << std:endl;

firstやsecondは、QPair<T1,T2>として宣言した場合、firstがT1型、secondがT2型となるパブリック変数です。

文書の更新日時

文書の更新日時は、NSFNoteGetInfo関数を用いて取得します。この関数は、更新日時以外に作成日時、アクセス日時、ID情報などが取得できます。今回、この説明については後日にしたいので、この関数は用いず、フィールドからの取得にしたいと思います。

今までのところ、文書から取得できるデータは、256バイト以下(LMBCS換算)の文字列のみで、これを文書のフォーム名取得に使用しました。文書から取得できるデータの種類を少し広げて、日時、日時リスト、数値(小数点数、整数)まで扱えるようにします。数値に関しては今回、活用する場面はありませんが、あまり難しくないので、さくっと実装してしまいます。

#include <ntlx/note.h>

/**
 * @brief アイテムの日時データを取得する
 * @param itemName アイテム名
 * @return 日時データ
 */
ntlx::TimeDate ntlx::Note::getTimeDate(const QString& itemName);

/**
 * @brief アイテムの日時リストデータを取得する
 * @param itemName アイテム名
 * @return 日時リストデータ
 */
QList<ntlx::TimeDate> ntlx::Note::getTimeDateList(const QString& itemName);

/**
 * @brief アイテムの浮動小数点数データを取得する
 * @param itemName アイテム名
 * @param defaultValue データが取得できない時のデフォルト値
 * @return 浮動小数点数データ(倍精度)
 */
double ntlx::Note::getFloat(const QString& itemName, const double defaultValue = 0.);

/**
 * @brief アイテムの整数データを取得する
 * @param itemName アイテム名
 * @param defaultValue データが取得できない時のデフォルト値
 * @return 整数データ(long幅)
 */
long ntlx::Note::getLongInt(const QString& itemName, const long defaultValue = 0);

数値の実装についてはリポジトリを参照してください。ここでは、日時データと日時リストデータの取得について説明します。

単一の日時データを取得するには、NSFItemGetTime関数を使います。

TimeDate Note::getTimeDate(const QString &itemName)
{
  TIMEDATE td;
  open();
  if (!NSFItemGetTime(
        handle_
        , Lmbcs::fromQString(itemName).constData()
        , &td))
    TimeConstant(TIMEDATE_WILDCARD, &td);
  return TimeDate(td);
}

リポジトリ更新のタイミングで間に合っていませんが、後日、以下のように変更する予定です。

TimeDate Note::getTimeDate(const QString &itemName)
{
  TimeDate td;
  open();
  if (!NSFItemGetTime(
        handle_
        , Lmbcs::fromQString(itemName).constData()
        , &td.value_))
    TimeConstant(TIMEDATE_WILDCARD, &td.value_);
  return td;
}

itemNameフィールドの日時データを取得します。取得に失敗した場合は、ワイルドカードを返します。

日時のリストデータを取得する場合、汎用フィールド取得関数NSFItemInfoを使ってブロックIDを取得し、日時データリストの構造を解析しながら取得していきます。

QList<TimeDate> Note::getTimeDateList(const QString &itemName)
{
  QList<TimeDate> list;
  if (open().lastStatus().failure())
    return list;

  Lmbcs lmbcsItem = Lmbcs::fromQString(itemName);
  BLOCKID bidItem, bidValue;
  WORD type;
  DWORD valueLen;
  lastStatus_ = NSFItemInfo(
        handle_
        , lmbcsItem.constData()
        , lmbcsItem.size()
        , &bidItem
        , &type
        , &bidValue
        , &valueLen
        );

  switch (lastStatus_.error())
  {
  case NOERROR:
  {
    switch (type)
    {
    case TYPE_TIME_RANGE:
    {
      char* pValue = OSLockBlock(char, bidValue) + sizeof(WORD);
      RANGE* pRange = reinterpret_cast<RANGE*>(pValue);
      USHORT count = pRange->ListEntries;
      pValue += sizeof(RANGE);
      for (USHORT i = 0; i < count; ++i)
      {
        TimeDate td(*reinterpret_cast<TIMEDATE*>(pValue));
        list.append(td);
        pValue += sizeof(TIMEDATE);
      }
      OSUnlockBlock(bidValue);
    }
      break;
    }
  }
    break;
  }
  return list;
}

フィールドを取得する基本的なプロセスは、Note::getFormulaと同じです。NSFItemInfo関数でブロックIDを取得し、フィールドの種類を判別し、種類に応じた形式でポインタを操作しながら、目的のデータを取得していきます。

日時のリストは少々特殊な形をしています。日時リストはTYPE_TIME_RANGEという値で判別します。アイテムデータの先頭にはRANGE型という構造体が存在し、2つの配列の格納数が示されます。

#include <global.h>

typedef struct {
  USHORT ListEntries;  /* list entries following */
  USHORT RangeEntries; /* range entries following */
                       /* now come the list entries */
                       /* now come the range entries */
} RANGE;

ListEntriesは日時(TIMEDATE型)の配列のエントリー数です。一方のRangeEntriesは、Rangeと書かれていますが、実際にはTIMEDATE_PAIR型の配列のエントリー数です。

#include <global.h>

typedef struct {  /* a timedate range entry */
  TIMEDATE Lower;
  TIMEDATE Upper;
} TIMEDATE_PAIR;

TIMEDATE_PAIRは、「日時範囲」という位置づけのデータで、「何時から何時まで」や「何日から何日まで」というデータをひとくくりとして扱います。

これらをまとめると、日時リストフィールドのデータ構造は、例えば以下のようになります。

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

この構造に沿って、目的のデータを取得します。今回の日時リストでは、日時範囲リストは扱わないので、いったん無視することにします。

NSFinderへの実装

ここまで仕様を拡張したDatabaseクラス、Noteクラスを使って、実際にNSFinderに実装してみます。

現在、NSFinderに表示しているデータはツリー構造と、データベースのパス、タイトル、文書のIDとウィンドウタイトルです。列が足りないので、TreeModel::initializeメソッドで列を増やし、汎用的なヘッダを付けます。

// ヘッダーラベルを設定する
setHorizontalHeaderLabels(
      QStringList()
      << tr("Items")
      << tr("Parameter 1")
      << tr("Parameter 2")
      << tr("Parameter 3")
      );

あとは、TreeItem::collectFiles、TreeItem::collectNotesに以下のようなメソッドを追加していきます。

void TreeItem::collectFiles()
{
  ...

  // 取得したファイル情報を子アイテムに展開する
  for (auto it = files.begin(); it != files.end(); ++it)
  {
    QList<QStandardItem*> items;
    ntlx::FileMap& data = *it;

    ...

    ntlx::Database db(ntlx::PathSet(newPath, server, port));
    QPair<ntlx::TimeDate, ntlx::TimeDate> modTimes = db.getModifiedPair();
    items << new TreeItem(
              DataType::None
              , modTimes.first.toLmbcs().toQString()
              )
          << new TreeItem(
              DataType::None
              , modTimes.second.toLmbcs().toQString()
              );

    appendRow(items);
  }
}


void TreeItem::collectNotes()
{
  ...

  for (auto it = idTable.begin(); it != idTable.end(); ++it)
  {
    QList<QStandardItem*> items;

    ...

    QList<ntlx::TimeDate> revisions = note.getTimeDateList(ITEM_NAME_REVISIONS);
    if (note.lastStatus().success() && !revisions.isEmpty())
      items << new TreeItem(
                 DataType::None
                 , revisions.last().toLmbcs().toQString()
                 )
            << new TreeItem(
                 DataType::None
                 , revisions.first().toLmbcs().toQString()
                 );

    appendRow(items);
  }
}

collectNotesメソッドでは、NSFNoteGetInfo関数を使う代わりに、ITEM_NAME_REVISIONSアイテムから更新日時を取得しています。実体は"$Revisions"という名前で、Notesが自動で更新日時をリストとして追記していくフィールド名です。このフィールドから日時リストが取得できたら、最後の値を更新日時、最初の値を作成日時として列アイテムに追加していきます。

最後に

NSFinderプロジェクトを始めて、扱えるデータ型を増やしてきました。このまま列を増やしていくこともできますが、列のみの対応ではいずれ限界が来ます。また、データベース、文書、フィールドと階層を落とすごとに列の整合性も取りづらくなってきます。

次回はいったんNotes APIをはなれ、NotesPeekのようなツリー構造ペインと、データ表示のためのプロパティペインとに分けたUIに変更していこうと思います。

伝説のツール「NotesPeek」をQtでリメイクする(その7・日時その2)

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

この構造体「TIME」は、Notes APIの日時型(TIMEDATE構造体)と、C/C++言語を介した他の日時型と相互に変換するための媒介の役目をします。

上図のように、この構造体には以下のようなデータが含まれます。

このほかに「GM」という値がありますが、これはTIMEDATEになります。

TIMEDATE構造体が表す日時データを他の日時に変換するためには、まずTIME::GM変数にその日時を代入します。その後、TimeGMToLocal関数を呼び出します。

#include <misc.h>

// 前提としてnow変数に現在の日時が入っているものとする。
// TIMEDATE now;
// OSCurrentTIMEDATE(&now);

TIME time;
time.GM = now;
TimeGMToLocal(&time);

すると、TIME::GMに格納された日時に基づいて、年月日などの他のフィールドにデータがパースされます。後はこのフィールドデータを元に、他の日時データを構築すればいいわけです。

TIME構造体を元に、Qtの日時型であるQDateTimeを作るには、以下のようにします(ただし、サマータイムは考慮しません)。

QDate qDate(time.year, time.month, time.day);
QTime qTime(time.hour, time.minute, time.second, time.hundredth * 10);
QDateTime qDateTime(qDate, qTime, Qt::LocalTime, time.zone * -3600);
// OUTPUT: qDateTime

まず、日付型のQDate、時間型のQTimeを作ります。TIMEに保存される1秒以下の時間単位(TIME::hundredth)は10ミリ秒なので、10倍して1ミリ秒単位に変換します。次に、QDate、QTimeを元にQDateTimeを作ります。デフォルトでTIMEDATEが表す日時はローカル時間なので、ローカル時間であることを指定します(Qt::LocalTime定数)。最後にタイムゾーンの指定ですが、TIMEDATE構造体では時間(Hour)単位である一方、Qtのタイムゾーンは秒単位です。また正負も逆なので、60分×60秒×-1=-3600を掛けた値を指定します。

次は、他の日時データをTIMEDATEに変換するプロセスを見ていきます。

先ほどと同様にTIME構造体で変数を作り、今度はGM以外のフィールドに値を埋めていきます。その後で、TimeLocalToGM関数を呼び出します。QDateTimeに変換元の日時がある場合を見ていきます。

#include <misc.h>

// INPUT: const QDateTime& qDateTime

TIME time;
QDate qDate = qDateTime.date();
QTime qTime = qDateTime.time();
time.zone = qDateTime.offsetFromUtc() / -3600;
time.dst = qDateTime.isDaylightTime() ? 1 : 0;
time.year = qDate.year();
time.month = qDate.month();
time.day = qDate.day();
time.weekday = qDate.dayOfWeek() == 7 ? 1 : (qDate.dayOfWeek() + 1);
time.hour = qTime.hour();
time.minute = qTime.minute();
time.second = qTime.second();
time.hundredth = qTime.msec() / 10;
TimeLocalToGM(&time);
// OUTPUT: time.GM

基本的にはTimeGMToLocal関数の逆のことをしています。サマータイム(TIME::dst)については、日本がサマータイムではないのでぴんときませんので推測の域を出ません。あしからず。

以上が、TIMEDATE型と他の日時型との相互変換に関する基本的なプロセスです。次回は、ここまでのAPIをTimeDateクラスにまとめ、データベースに関する日時データ、文書に関する日時データを取得して、ツール上に表示するところまでを実装したいと思います。