伝説のツール「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に変更していこうと思います。