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用にコンパイルして、実行してみたいという小さな夢が実現できました。長編をおつきあいくださり、ありがとうございました。

NotesとQtでWindows、Mac OS X、Ubuntuのデスクトップアプリ(その10 - 共有ライブラリコンパイル・補足編)

前回は、MacOSX上でコーディングした共有ライブラリのコンパイルを、Qt Creatorで行う場合のqmakeプロジェクトファイルの書き方と、個別環境で指定する「追加の引数」についてお伝えしました。

Qt Creatorのビルド設定について、もう少し補足します。

追加の引数には、qmakeプロジェクトファイルに書くような変数の設定を、ひとくくりの文字列オプションとして指定できます。

"INCLUDEPATH += /My/Original/Include/1 /My/Original/Include/2"

今回のようなマルチプラットフォーム環境でのコンパイルを前提にする場合、qmakeではプロジェクトファイルに「win32:」「macx:」「unix:」のようなプラットフォーム別プリフィックスを使うこともできます。

macx:INCLUDEPATH += /My/Original/Include/1 /My/Original/Include/2

ですので、GitHubのような公開リポジトリを気にする必要がなければ、直接プロジェクトファイルにINCLUDEPATHやLIBSで個別環境の情報記述してしまってもいいでしょう。

win32:INCLUDEPATH += C:/Users/User01/MyWinLib/include
else:macx:INCLUDEPATH += /Users/User01/MyMacLib/include
else:unix:INCLUDEPATH += /home/User01/MyLinuxLib/include

また、Qt Creatorのビルド設定には、一時的に環境変数を変更する機能があります。実環境の変数は汚したくないけど、ビルド時にどうしても環境変数を追加、変更、削除したいことはままあります。そんな時は、「ビルド時の環境変数」を修正すればOKです。実環境を汚すことなくビルド時のみの環境変数を設定することができます。

次に、Notes C API Toolkitへのパスについてです。

API Toolkitは以下のようなフォルダ構成になっています。

cmp      コンパイラオプションの指定サンプル
doc      ヘルプデータベース
include  ヘッダファイル
lib      共有オブジェクト、共有ライブラリ
notedata サンプルで使用するNotesデータベース
samples  サンプルコード

includeに含まれるヘッダーファイルは、各プラットフォーム共通です。

プロジェクトファイルには、INCLUDEPATHに「+=」で追記します。

libに含まれるのは、NotesMainというエントリポイントを使いたいクライアントプログラム用の「notes0オブジェクトファイル」、AddInMainというエントリポイントを使ったサーバータスクプログラム用の「notesai0オブジェクトファイル」が、各プラットフォームごとに用意されています。Windowsのみ、notes.dllへのスタブとなるインポートライブラリ「notes.lib」も提供されています。MacOSXLinuxは、直接プラットフォーム上にインストールされているNotes共有ライブラリへのパスを指定すればリンクできます。

プロジェクトファイルには、LIBSに「+=」で、「-L」(大文字のエル)を頭に付けてパスを指定します。ライブラリの本体名は、「-l」(小文字のエル)を頭に付けます。

LIBS += -L'/Applications/IBM Notes.app/Contents/MacOS'"
LIBS += -lnotes

共有ライブラリは、各OSごとに呼び名や命名規約が異なります。以下は各OSごとのNotes共有ライブラリですが、いずれの場合も「-lnotes」と指定すればOKです。

Windows: nnotes.dll(notes.lib)
Mac: libnotes.dylib
Linux(Ubuntu): libnotes.so

最後に各OS固有のビルド設定における違いを示します。

Windows

Windowsは、コンパイルにおいて構造体のアラインメントに気を配る必要があります。Statusクラスの紹介記事でも書きましたが、#pragma pack(push, 1)で1バイトに変更した上で、Notesヘッダーファイルをインクルードし、#pragma pack(pop)で元に戻す作業が必要になります。

あとは、前述の通り、リンク先は直接nnotes.dllを見に行くのではなく、ツールキット内のインポートライブラリでリンクを済ませます。nnotes.dllとの実際のリンクは、実行時にPATHで検索することになります。

環境変数については特に何もしません。

MacOSX

経験上ですが、Macの場合、追加の引数に共有ライブラリへのパスを指定するだけではビルドできませんでした。Qt Creatorの「ビルド設定」内にある「ビルド時の環境変数」というところで、DYLD_LIBRARY_PATHという環境変数に、Notesライブラリまでのパスを追加(なければ作成)します。また、Notes共有ライブラリへのパスには、スペースが含まれていることに注意します。

# 追加の場合
DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:`/Applications/IBM Notes.app/Contents/MacOS/`

# 変数がなければ変数自体を追加
DYLD_LIBRARY_PATH=/Applications/IBM Notes.app/Contents/MacOS/
Linux(Ubuntu)

これも経験上編み出したんですが、実のところ理由がよくわかりません。LIBSに追加する共有ライブラリへのパスは「-L〜」のみではビルドできません。Macの「DYLD_LIBRARY_PATH」のようなものなのでしょうか、以下のような指定が必要です。

# これだけではリンクできませんでした。
LIBS += -L/opt/ibm/notes

# こうするとリンクできました。
LIBS += -L/opt/ibm/notes -Wl,-rpath,/opt/ibm/notes

なお、環境変数については特に何もしません。

NotesとQtでWindows、Mac OS X、Ubuntuのデスクトップアプリ(その9 - 共有ライブラリコンパイル編)

ここまでをおさらいします。

最初に、Statusクラス(STATUS値のラッパークラス)を作成しました。

次に、Lmbcsクラス(LMBCS文字列のラッパークラス)をNLS版で作成しました。

最後に、Databaseクラス(NSFファイルのラッパークラス)を、必要最低限の実装で作成しました。

これを、ntlxライブラリとしてまとめていきます。

Qtでは、qmakeというツールを使って、補足コードを生成したり、プラットフォームに応じたコンパイラオプション、リンカオプションを展開し、そのあとで、各プラットフォームのコンパイラやリンカを使って、ソースコードをバイナリ形式にしていきます。

そのqmakeに渡す指示書のようなものが、proファイル(プロジェクトファイル)です。

QT       -= gui

TARGET = ntlx
TEMPLATE = lib

DEFINES += NTLX_LIBRARY

SOURCES += lmbcs.cpp \
    database.cpp

HEADERS += lmbcs.h\
        ntlx_global.h \
    status.h \
    database.h

win32 {
    DEFINES += W32 NT
}
else:macx {
#   DEFINES += MAC
# Mac環境でDWORDが32bitではなく、64bitになってしまうため、LONGIS64BITを識別子に加える。
    DEFINES += MAC LONGIS64BIT
}
else:unix {
    DEFINES += UNIX LINUX W32
    QMAKE_CXXFLAGS += -std=c++0x
    target.path = /usr/lib
    INSTALLS += target
}

LIBS += -lnotes

DISTFILES += \
    .gitignore

gitなどを使ってWindowsMacOSXLinuxに1つのコードを配布することを前提に、絶対パスを必要とするパラメータはここに記述しないこととしています。

そのため、リンク先となるNotesの共有ライブラリは、ファイル名のみ共通して使えるので、「LIB += -lnotes」として記述しています(拡張子は省略できます)が、場所(パス)については、Qt Creatorのプロジェクト管理機能で補完しています。

例えば、MacOSXでは、ビルドステップにおいて以下のような設定をします。

"INCLUDEPATH+=/Users/Shared/notesapi/include" "LIBS+=-L'/Applications/IBM Notes.app/Contents/MacOS'"

【追記】2017.3.24 記事をアップした時の追加の引数が間違えておりました。訂正してお詫びします。

【追記】2017.4.6 Mac環境でDWORDが64bit幅のunsigned longが使われてしまうことを発見しました。そのため、Qtプロジェクトファイルで、DEFINESに識別子LONGIS64BITを加えるようにします。

このうち、INCLUDEPATH、LIBSをqmake「追加の引数」で追記されているものです。

これができたらビルドをしていきましょう。

NotesとQtでWindows、Mac OS X、Ubuntuのデスクトップアプリ(その8 - データベース・基本クラス編)

前回紹介したデータベース関連のAPIを踏まえて、Databaseクラスを定義していきます。 方針としては、Notes/Domino APIプログラミング―C++とSTLによる実践的プログラミングを踏襲して、コピー不可のクラスとします。

<database.h>

#ifndef NTLX_DATABASE_H
#define NTLX_DATABASE_H

#include "ntlx_global.h"
#include <QString>
#include "status.h"

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

#include <nsfdb.h>

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

namespace ntlx {

class Lmbcs;

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

  /**
   * @brief パスによるコンストラクタ
   * @param path パス
   * @param server サーバ名(省略時はローカル)
   * @param port ポート名(省略時はデフォルトポート)
   */
  explicit Database(const QString& path
                    , const QString& server = QString()
                    , const QString& port = QString()
                    , Status* status = nullptr
      );

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

  /**
   * @brief DBHANDLEキャスト演算子
   */
  operator DBHANDLE() const { return handle_; }

  /**
   * @brief データベースのタイトルを取得する
   * @param status 関数の実行結果を取得したい場合はSTATUS変数へのポインタ
   * @return データベースタイトル
   */
  QString getTitle(STATUS* status = nullptr) const;

  /**
   * @brief データベースカテゴリ名を取得する
   * @param status 関数の実行結果を取得したい場合はSTATUS変数へのポインタ
   * @return データベースカテゴリ名
   */
  QString getCategories(STATUS* status = nullptr) const;

  /**
   * @brief データベースクラス(引き継ぎ元テンプレート名)を取得する
   * @param status 関数の実行結果を取得したい場合はSTATUS変数へのポインタ
   * @return データベースクラス(引き継ぎ元テンプレート名)
   */
  QString getClass(STATUS* status = nullptr) const;

  /**
   * @brief データベース設計クラス(自身のテンプレート名)を取得する
   * @param status 関数の実行結果を取得したい場合はSTATUS変数へのポインタ
   * @return データベース設計クラス(自身のテンプレート名)
   */
  QString getDesignClass(STATUS* status = nullptr) const;

  /**
   * @brief データベースを開く
   * @return 結果ステータス
   */
  Status open(
      const QString& path
      , const QString& server
      , const QString& port
      );

  /**
   * @brief データベースを閉じる
   * @return 結果ステータス
   */
  Status close();

  /**
   * @brief パス、サーバ名、ポート名からネットパスを構築する
   * @param path パス
   * @param server サーバ名
   * @param port ポート名
   * @param status 結果ステータス
   * @return ネットパス
   */
  static Lmbcs constructNetPath(
      const QString& path
      , const QString& server
      , const QString& port
      , STATUS* status = nullptr
      );

protected:
  /**
   * @brief データベースハンドルを返す
   * @return データベースハンドル
   */
  DBHANDLE handle() const { return handle_; }

  /**
   * @brief ハンドルを設定する
   * @param handle データベースハンドル
   */
  void setHandle(DBHANDLE handle) { handle_ = handle; }

  /**
   * @brief データベース情報を取得する
   * @param what データベース情報の種類(INFOPARSE_XXX)
   * @param status 結果ステータス
   * @return データベース情報
   */
  QString getInfo(WORD what, STATUS* status = nullptr) const;

private:
  DBHANDLE handle_;

  Database(const Database&);
  Database& operator=(const Database&);
};

} // namespace ntlx

#endif // NTLX_DATABASE_H



<database.cpp>

#include "database.h"
#include "lmbcs.h"

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

#include <osfile.h>

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

namespace ntlx {

Database::Database()
  : handle_(NULLHANDLE)
{
}

Database::Database(const QString &path
                   , const QString &server
                   , const QString &port
                   , Status* status
                   )
  : handle_(NULLHANDLE)
{
  Status result = open(path, server, port);
  if (status != nullptr) *status = result;
}

Database::~Database()
{
  close();
}

QString Database::getTitle(STATUS* status) const
{
  return getInfo(INFOPARSE_TITLE, status);
}

QString Database::getCategories(STATUS* status) const
{
  return getInfo(INFOPARSE_CATEGORIES, status);
}

QString Database::getClass(STATUS* status) const
{
  return getInfo(INFOPARSE_CLASS, status);
}

QString Database::getDesignClass(STATUS* status) const
{
  return getInfo(INFOPARSE_DESIGN_CLASS, status);
}

Status Database::open(
    const QString& path
    , const QString& server
    , const QString& port
    )
{
  close();

  Lmbcs netPath = constructNetPath(path, server, port);
  Status result = NSFDbOpen(netPath.constData(), &handle_);
  if (result.failure()) handle_ = NULLHANDLE;
  return result;
}

Status Database::close()
{
  if (handle_ != NULLHANDLE)
  {
    Status result = NSFDbClose(handle_);
    handle_ = NULLHANDLE;
    return result;
  }
  return NOERROR;
}

Lmbcs Database::constructNetPath(const QString &path
                                 , const QString &server
                                 , const QString &port
                                 , STATUS* status
                                 )
{
  Lmbcs lmPath = Lmbcs::fromQString(path);
  Lmbcs lmServer = Lmbcs::fromQString(server);
  Lmbcs lmPort = Lmbcs::fromQString(port);
  char netPath[MAXPATH];

  Status result = OSPathNetConstruct(
        lmPort.isEmpty() ? 0 : lmPort.constData()
        , lmServer.constData()
        , lmPath.constData()
        , netPath
        );
  if (status != nullptr) *status = result;

  return result.success() ? Lmbcs(netPath) : Lmbcs();
}

/**
 * @note NSF_INFOは以下のような構成を取る
 * @note タイトル+改行+カテゴリ+改行+#1+設計クラス+改行+#2+クラス+終端0
 * @note 全体で128バイト以内になる
 */
QString Database::getInfo(WORD what, STATUS* status) const
{
  Q_ASSERT(handle());

  char infoData[NSF_INFO_SIZE];
  char nameData[NSF_INFO_SIZE];

  Status result = NSFDbInfoGet(handle(), infoData);
  if (status != nullptr)
    *status = result;

  if (result.failure())
    return QString();

  NSFDbInfoParse(infoData, what, nameData, NSF_INFO_SIZE - 1);
  nameData[NSF_INFO_SIZE - 1] = '\0';
  Lmbcs lmbcs(nameData);
  return lmbcs.toQString();
}

} // namespace ntlx

NotesとQtでWindows、Mac OS X、Ubuntuのデスクトップアプリ(その7 - データベース・基本編)

Notesデータベースは、Note(文書)の集合体です。文書はアイテムの集合体で、アイテムの中でもリッチテキストアイテムはコンポジットデータの集合体です。Notesデータにアクセスするには、何はなくともデータベースにアクセスする必要があります。データベースにアクセスするには、データベースハンドル(DBHANDLE型)を取得する必要があります。

Notesデータベースのハンドルを取得する基本は、パスを使った方法です。Notesのフルパス(以下、ネットパス)は、Notesポート、サーバ名、ファイルパスの3つから構成されます。Notesポートは、TCP/IP以外に思いつかない現在では、ポートを意識することはほとんどないですが、AppleTalkNetWare(IPX/SPX)、モデムを使った通信など、ネットワークプロトコルがいくつもあった時代の名残と言えるでしょう。おそらく今は、Notesポートはデフォルト(指定なし)で済むはずです。サーバ名はDominoサーバ名を指定するか、指定なし(=ローカル)とします。ファイルパスは、データディレクトリからの相対パスを指定することになります。ローカルであれば、フルパスも指定できます。これら3つの要素を組み合わせて、一つのテキストとして表されるのがネットパスです。

ネットパス自体は、文字列を特定の区切り文字でつなげればいいのですが、仕様が変更されることもありますし、移植性の観点からも、API関数を通じて作成した方が無難です。

ネットパスを作るには、OSPathNetConstructという関数を使います。逆に、できあがっているネットパスを3要素に分解するのがOSPathNetParse関数です。

STATUS LNPUBLIC OSPathNetConstruct (const char far *PortName,
                                    const char far *ServerName,
                                    const char far *FileName,
                                    char far *retPathName);

STATUS LNPUBLIC OSPathNetParse (const char far *PathName,
                                char far *retPortName,
                                char far *retServerName,
                                char far *retFileName);

Notesデータベースを、ネットパスを使ってオープンし、ハンドルを得るのがNSFDbOpen関数です。データベースへのアクセスを終了する場合はNSFDbClose関数を使います。

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

STATUS LNPUBLIC NSFDbClose (DBHANDLE hDB);

最後にNotesデータベースの基本情報についてです。Notesデータベースの情報バッファは1つの文字列、128バイトで構成されます。データベースタイトルはこの中の1つで、ほかにカテゴリ、クラス(引き継ぎ元テンプレート名)、設計クラス(マスターテンプレート名)も、この128バイト中に共存します。4つとも設定すると、平均32バイト、日本語の全角LMBCSで設定すると、1つあたり全角10文字程度しか設定できません。何とも窮屈なデータです。

タイトルを含む4つの基本情報を取得するには、NSFDbInfoGet関数を使います。長さはNSF_INFO_SIZEとして定義されているので、バッファを用意して取得すればOKです。

取得したい基本情報から、目的のデータ(例えばタイトル)を取り出したい時はNSFDbInfoParse関数を使います。データの種類はINFOPARSE_XXXとして定義されています。バッファサイズはNSF_INFO_SIZEを用意すればいいでしょう。必ずしも全体の4分の一とは限らないですし。

基本情報を変更する場合は、先の2つに加えて、NSFDbInfoModifyとNSFDbInfoSetを使いますが、詳しくは折を見て説明したいと思います。

STATUS LNPUBLIC NSFDbInfoGet (DBHANDLE hDB, char far *retBuffer);

void LNPUBLIC NSFDbInfoParse(char far *Info, WORD What, char far *Buffer, WORD Length);

#define INFOPARSE_TITLE 0
#define INFOPARSE_CATEGORIES 1
#define INFOPARSE_CLASS 2
#define INFOPARSE_DESIGN_CLASS 3

void LNPUBLIC NSFDbInfoModify(char far *Info, WORD What, const char far *Buffer);

STATUS LNPUBLIC NSFDbInfoSet (DBHANDLE hDB, char far *Buffer);

ここまでの関数を踏まえて、Notesデータベースクラスを定義していきましょう。

NotesとQtでWindows、Mac OS X、Ubuntuのデスクトップアプリ(その6 - LMBCS変換・応用編)

LMBCSを扱うにあたり、前回のコードでは、LMBCSの区切り位置を計測できずにいました。

APIを詳しく見ていくと、LMBCS文字列を含む言語サービスが用意されています。ヘッダファイルnls.hで提供されるNational Language Services(NLS)には、多国語のキャラクタセットに対応した文字列処理関数が用意されていて、OSTranslate関数の代替となるNLS_Translate関数もあります。 LMBCSの文字区切り判定についても、文字数を返すNLS_string_chars、バイト数を返すNLS_string_bytesを組み合わせることで、LMBCSを正しい位置で区切ることが可能になり、WORD幅を超えるLMBCS文字列も、範囲ごとに変換してつなぎ合わせていけば処理が可能になります。

それでは、NLSを活用した新しいLmbcsクラスを紹介します。ヘッダファイルは変更がないので、ソースファイルのみ紹介します。

<lmbcs.cpp>

#include "lmbcs.h"

#include <QString>

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

#include <osmisc.h>
#include <nls.h>

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

namespace ntlx {

const WORD UNICODE_BYTE = (sizeof(ushort) / sizeof(char));
const WORD LMBCS_DELTA = (1024 * 31);
const int UNICODE_DELTA = (1024 * 42 / UNICODE_BYTE);

Lmbcs::Lmbcs()
  : QByteArray()
{
}

Lmbcs::Lmbcs(const char *s, int len)
  : QByteArray(s, len)
{
}

Lmbcs::Lmbcs(STATUS status)
  : QByteArray()
{
  char buffer[MAXWORD] = "";
  WORD lmbcsLen = OSLoadString(0, Status(status).error(), buffer, MAXWORD);
  *this = Lmbcs(buffer, lmbcsLen);
}

Lmbcs::Lmbcs(const Lmbcs& other)
  : QByteArray(other)
{
}

Lmbcs& Lmbcs::operator=(const Lmbcs& other)
{
  if (this == &other) return *this;
  QByteArray::operator=(other);
  return *this;
}

QString Lmbcs::toQString() const
{
  // 変換後の文字列をセットする変数
  QString result;
  char buffer[MAXWORD] = "";

  // キャラクタセットを設定する
  NLS_PINFO pLmbcsInfo = OSGetLMBCSCLS();
  NLS_PINFO pUnicodeInfo;
  NLS_load_charset(NLS_CS_UNICODE, &pUnicodeInfo);

  // 現在の文字列のポインタとサイズ(バイト数)を計算する
  BYTE* ptr = (BYTE*)constData();
  int restSize = size();

  while (restSize > 0)
  {
    // 想定する境界値を計算する
    WORD delta = restSize < (int)LMBCS_DELTA ? (WORD)restSize : LMBCS_DELTA;

    // 境界値から文字数を割り出す
    WORD chars = 0;
    NLS_string_chars(ptr, delta, &chars, pLmbcsInfo);

    // 文字数から実際のバイト数を割り出す
    WORD bytes = 0;
    NLS_string_bytes(ptr, chars, &bytes, pLmbcsInfo);

    // LMBCSからUNICODEに変換する
    WORD retLen = MAXWORD;
    NLS_STATUS status = NLS_translate(
          ptr, bytes
          , (BYTE*)buffer, &retLen
          , NLS_NONULLTERMINATE | NLS_SOURCEISLMBCS | NLS_TARGETISUNICODE
          , pUnicodeInfo
          );
    Q_ASSERT(status == NLS_SUCCESS);

    // UNICODE配列からQStringを作成してストリームに追加する
    result += QString::fromUtf16(reinterpret_cast<ushort*>(buffer)
                                 , (int)retLen / UNICODE_BYTE
                                 );

    // ポインタを進め、残りサイズを減らす
    ptr += (int)bytes;
    restSize -= bytes;
  }

  // ロードしたUNICODEキャラクタセットをアンロードする
  NLS_unload_charset(pUnicodeInfo);

  // ストリームを介して作成したQString文字列を返す
  return result;
}

Lmbcs Lmbcs::fromQString(const QString &qs)
{
  // 変換後の文字列をセットする変数
  QByteArray result;
  char buffer[MAXWORD] = "";

  // キャラクタセットを設定する
  NLS_PINFO pLmbcsInfo = OSGetLMBCSCLS();

  // 開始文字位置とサイズ(文字数)を計算する。
  int index = 0, restSize = qs.size();

  while (restSize > 0)
  {
    // 定義済みのUNICODE処理文字数と比較して変換する文字数を決める
    int chars = restSize < (int)UNICODE_DELTA ? restSize : (int)UNICODE_DELTA;

    // 開始位置と変換文字数から文字列を抜き出す
    QString input = qs.mid(index, chars);

    // 抜き出した文字列をUTF-16の配列に変換する
    const ushort* unicode = input.utf16();

    // 変換するバイト数を計算する
    WORD unicodeLen = (WORD)input.size() * UNICODE_BYTE;

    // UNICODEからLMBCSに変換する
    WORD retLen = MAXWORD;
    NLS_STATUS status = NLS_translate(
          reinterpret_cast<BYTE*>(const_cast<ushort*>(unicode)), unicodeLen
          , (BYTE*)buffer, &retLen
          , NLS_NONULLTERMINATE | NLS_SOURCEISUNICODE | NLS_TARGETISLMBCS
          , pLmbcsInfo
          );
    Q_ASSERT(status == NLS_SUCCESS);

    // LMBCS配列をQByteArrayに追加する
    result.append(buffer, (int)retLen);

    // 開始文字位置を進め、残りサイズを減らす
    index += chars;
    restSize -= chars;
  }

  // ストリームを介して作成したバイト列をLmbcsオブジェクトにして返す
  return Lmbcs(result.constData(), result.size());
}

} // namespace ntlx

LMBCS_DELTAとUNICODE_DELTAは、変換元文字列の単位を表します。 LMBCSをUNICODEに変換する場合は、半角英数字が最大2倍になるので、WORD幅の半分、より気持ち少なめをLMBCSの変換バイト単位にしています。 UNICODEをLMBCSに変換する場合は、全角文字や半角カナが最大1.5倍になるので、WORD幅の約2/3をUNICODEの変換バイト単位にしています。

次に静的メソッドfromQStringを見ていきます。

NLSの関数を使う場合、目的のキャラクタセット(CS)に応じた構造体NLS_INFOへのポインタを用意しておきます。LMBCSとネイティブ文字列(日本語ならShift-JISか?)については、OSGetLMBCSCLSとOSGetNativeCLSで取得できます。それ以外のCSを使う場合については、後述します。

与えられたQString文字列を、UNICODE_DELTAごとに区切りながら、LMBCSに変換していきます。NLSの文字列変換の使い方は、おおよそOSTranslate関数と同じです。変換元、変換先の指定は、5番目の引数で指定します。NLS_SOURCEISUNICODE | NLS_TARGETISLMBCSという組み合わせが、UNICODEからLMBCSへの変換を示します。NLS_NONULLTERMINATEは、変換結果にNULL文字を付加しなくてもいいことを指示します。6番目の引数には、変換後のCSを表すNLS_INFOを指定します。戻り値はNLS特有のNLS_STATUS型のステータス値で、NLS_SUCCESSであれば成功です。

最終的に、つなぎ合わせたLMBCSをLmbcsオブジェクトにして返します。

最後にtoQStringメソッドです。

このメソッドは、変換後のCSがUNICODEになるので、UNICODE用のNLS_INFOを用意します。関数NLS_load_charsetでは、任意のCSをロードして、そのNLS_INFOへのポインタを取得できます。使い終わったらNLS_unload_charsetでアンロードしておきます。

LMBCS文字列も、fromQString同様に変換バイト単位に区切りながら変換、つなぎを繰り返します。その際の区切り位置は、前述の通りNLS_string_charsとNLS_string_bytesを組み合わせて算出します。まず、区切りたいバイト位置まででNLS_string_charsを使って文字数を計算します。区切り位置に次の文字のプレフィックス¥x10が引っかかっていればこの文字は計算に入りません。次に文字数からバイト数を求めるNLS_string_bytesを使ってバイト数を計算します。こうすることで、LMBCS文字列でも正しく区切って処理することが可能になります。

LmbcsクラスはQByteArrayの派生クラスなので、Lmbcsクラス、QStringクラスともに文字数としては、理論上int幅の2Gバイトまで使うことができます。対してOSTranslate関数、NLS_Translate関数ともに処理可能なバイト数はWORD幅の64kバイトです。私が見つけられていないだけかもしれませんが、処理可能なバイト数についてのAPIレベルでの改善が望まれます。(続く)

NotesとQtでWindows、Mac OS X、Ubuntuのデスクトップアプリ(その5 - LMBCS変換・基本編)

それでは、LMBCSをラップしたクラス、Lmbcsを定義していきます。

<ntlx_global.h>

#ifndef NTLX_GLOBAL_H
#define NTLX_GLOBAL_H

#include <QtCore/qglobal.h>

#if defined(NTLX_LIBRARY)
#  define NTLXSHARED_EXPORT Q_DECL_EXPORT
#else
#  define NTLXSHARED_EXPORT Q_DECL_IMPORT
#endif

#endif // NTLX_GLOBAL_H
<lmbcs.h>

#ifndef NTLX_LMBCS_H
#define NTLX_LMBCS_H

#include "ntlx_global.h"
#include "status.h"

#include <QByteArray>

namespace ntlx {

const int MAX_UNICODE_LEN = (1024 * 42 / 2);

class NTLXSHARED_EXPORT Lmbcs
    : public QByteArray
{
public:
  /**
   * @brief コンストラクタ
   */
  Lmbcs();

  /**
   * @brief コピー元LMBCSの文字列ポインタと長さによるコンストラクタ
   * @param コピー元LMBCSのポインタ
   * @param len コピー元LMBCSの長さ、0終端している場合は-1
   */
  Lmbcs(const char* s, int len = -1);

  /**
   * @brief ステータス値からエラーメッセージのLMBCSを生成する
   * @param status ステータス値
   */
  Lmbcs(STATUS status);

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

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

  /**
   * @brief QStringに変換する
   * @return LMBCSから変換されたQString文字列
   */
  QString toQString() const;

  /**
   * @brief QStringからLmbcsに変換する
   * @note 変換できる文字列の長さはMAX_UNICODE_LENに制限
   * @param qs 変換元のQString
   * @return QStringから変換されたLmbcsオブジェクト
   */
  static Lmbcs fromQString(const QString& qs);

};

} // namespace ntlx

#endif // NTLX_LMBCS_H
<lmbcs.cpp>

#include "lmbcs.h"

#include <QString>

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

#include <osmisc.h>

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

namespace ntlx {

const WORD UNICODE_BYTE = (sizeof(ushort) / sizeof(char));

Lmbcs::Lmbcs()
  : QByteArray()
{
}

Lmbcs::Lmbcs(const char *s, int len)
  : QByteArray(s, len)
{
}

Lmbcs::Lmbcs(STATUS status)
  : QByteArray()
{
  char buffer[MAXWORD];
  WORD lmbcsLen = OSLoadString(0, Status(status).error(), buffer, MAXWORD);
  *this = Lmbcs(buffer, lmbcsLen);
}

Lmbcs::Lmbcs(const Lmbcs& other)
  : QByteArray(other)
{
}

Lmbcs& Lmbcs::operator=(const Lmbcs& other)
{
  if (this == &other) return *this;
  QByteArray::operator=(other);
  return *this;
}

QString Lmbcs::toQString() const
{
  char buffer[MAXWORD];
  int unicodeLen = OSTranslate(
        OS_TRANSLATE_LMBCS_TO_UNICODE
        , constData(), size() <= (int)MAXWORD ? (WORD)size() : MAXWORD
        , buffer, MAXWORD
        );
  return QString::fromUtf16(reinterpret_cast<ushort*>(buffer)
                            , unicodeLen / UNICODE_BYTE);
}

Lmbcs Lmbcs::fromQString(const QString &qs)
{
  char buffer[MAXWORD];
  QString input = qs.left(MAX_UNICODE_LEN);
  const ushort* unicode = input.utf16();
  WORD unicodeLen = (WORD)input.size() * UNICODE_BYTE;
  int lmbcsLen = OSTranslate(
        OS_TRANSLATE_UNICODE_TO_LMBCS
        , reinterpret_cast<char*>(const_cast<ushort*>(unicode))
        , unicodeLen
        , buffer, MAXWORD
        );
  return Lmbcs(buffer, lmbcsLen);
}

} // namespace ntlx

コンストラクタには、以下の4つを用意しました。

  • デフォルトコンストラクタ(空のLMBCS)
  • LMBCSポインタと長さによるコンストラクタ
  • ステータス値によるコンストラクタ
  • コピーコンストラクタ

3番目の「ステータス値によるコンストラクタ」は、API関数OSLoadStringを利用したコンストラクタです。OSLoadStringは、指定したモジュール(指定しなければNotes)からエラーコードに対応した文字列をロードします。

<osmisc.h>

WORD LNPUBLIC OSLoadString (HMODULE hModule
                            , STATUS StringCode
                            , char far *retBuffer
                            , WORD BufferLength);

hModuleハンドルは、文字列を読み込むリソースDLLで、見つからなければNotesが保有する文字列リソースを検索します。リソースDLLがなかったり、MacLinuxなどの非Windowsプラットフォームで利用する場合は0を指定します。

StringCodeがステータス値です。ここにはERR()マクロを使ってエラーコードのみにマスクする必要があります。先に定義したntlx::Status::errorメソッドが役に立ちます。

retBufferとBufferLengthは、読み込んだ文字列を書き込む領域とそのサイズを指定します。

戻り値は、実際に書き込んだ文字列の長さになります。

次に、OSTranslateを使ってどのようにLMBCSからQString、QStringからLMBCSに変換しているかを見ていきます。

Unicode(UTF-16)の1文字は、全角半角に関係なく、1文字2バイトです。一方LMBCS日本語は、半角英数字は1バイト、全角と半角カナ文字は3バイトです。UnicodeからLMBCSに変換する場合1/2〜3/2倍になります。そこで、64k÷3×2≒42kを入力Unicodeの最大値とし、文字数としては21,504文字とします。

逆に、LMBCSからUnicodeに変換する場合、2/3〜2倍になるという式は成り立ちますが、LMBCSの文字の区切りを手動で判別するのは大変な手間を要します。そこで今回は、入力LMBCSの最大値はWORD最大値まで使用するものとし、バッファあふれは無視します。

次回は、このLMBCS文字列を適切に区切り、WORD幅より大きいサイズの文字列が扱えないか考察します。(続く)