伝説のツール「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ライブラリの追加・変更点はまだいくつかありますので、順に説明していきたいと思います。