Visual Studio 2008でQtの開発環境を作る

最近,諸般の事情からVisual Studio 2008という古い処理系でQtの開発環境を構築することになったのですが,幾らかの事情でつっかえたのでメモとして残しておくことにします.今回必要だったのはQT 4系だったので大雑把な手順としては以下です.

  1. Qt 4.8.4のインストールして環境変数PATHbin(例えばC:\Qt\4.8.4\bin\bin)を追加
  2. Debugging Tools for Windowsのインストール
  3. Qt Visual Studio Add-in 1.11.1のインストール
  4. QtCreator 2.7.1のインストール
  5. 環境変数WindowsSDKDirの設定

最後以外は特殊な手続きではありません.Qt本体,Add-inとQtCreatorQt の公式サイトからインストーラを取得して実行すれば十分です.Debugging Tools for WindowsWindows SDKWindows Software Development Kit (SDK) for Windows 8のサイトから取得し,インストーラからセットアップを行います.後はQtCreatorのメニューのツールオプションビルドと実行キットと進んでコンパイラやデバッガの値を適切に設定します.

さて,問題となるのは最後の部分です.前述の方法で設定を行えばQtCreatorから以下のようなHello Worldプログラムがビルドできるようになるはずですが,場合によってはエラーとなっていまいます.

#include <iostream>

int main()
{
    std::cout << "Hello World!" << std::endl;
    return 0;
}

内容は以下です.

エラー: LNK1104: ファイル 'kernel32.lib' を開くことができません。

結論から言うと,この問題は環境変数を追加することで解決できます.

WindowsSdkDir=C:\Program Files\Microsoft SDKs\Windows\v6.0A\

ただし,パスは実際にSDKをインストールしたパスとします.問題の原因はQtCreatorが処理系の特定に用いるvsvarsall32.batが適切に記述されておらず,SDKが正しく使えない状態になっている事によります.既定ではC:\Program Files\Microsoft Visual Studio 9.0\Common7\Tools\vsvarsall32.batを見ることで,上記の環境変数を設定すれば良いと分かります.

Eigenの特異値分解を利用した主成分分析

今日はEigenを利用してデータ分析の手法として良く知られた主成分分析を実装してみます.基本的な部分は以下の記事と同じで,C言語VBAから利用可能なDLLをビルドしようというものです.

さて,主成分分析はm次元のデータベクタ{\bf a}_i,(i=1,\dots,n)について,その共分散行列の固有値問題を解くことで行うことができます.しかしながら,特にm>>nである場合,共分散行列の計算に時間がかかるなどの問題があります.こうした場合は特異値分解を利用した方法が推奨されます.具体的には平均減算を行ったデータベクタを横に並べた以下のm\times n行列{\bf M}の特異値問題を解くものです.
{\bf M} = \begin{bmatrix} ({\bf a}_1-{\bf\mu})& ({\bf a}_2-{\bf\mu})& \dots & ({\bf a}_n-{\bf\mu})\end{bmatrix}
ただし,{\bf \mu}{\bf a}_i,(i=1,\dots,n)の平均ベクタです.この{\bf M}の左特異行列が主成分ベクタに,特異値の二乗が主成分に対応します.

Eigenを利用する場合,実装は非常に簡単であり,その骨子は以下のようになります.

  1. 配列を受け取ってMapオブジェクトで行列とみなす
  2. rowwise()mean()で平均ベクタを計算
  3. 平均減算した行列をJacobiSVDオブジェクトで特異値分解
  4. 左特異行列から主成分ベクタを,特異値の二乗から主成分を得る

Mapを利用することでコピー操作が不要になり,また,組み込み関数を利用するだけでfor文などを利用した反復処理さえ不要になります.以下はデータベクタを列優先でm\times n行列として与え,平均ベクタとr本の主成分ベクタ,それに対応する主成分を計算する関数です.

// This code is licensed under the GPL.
// simplepca.cpp
#include "eigen_vba.h"

// Eigen
#include <Eigen/Dense>
using namespace Eigen;

#ifdef _DEBUG
#include <iostream>
using namespace std;
#endif

// 特異値分解を用いた単純な主成分分析
EIGEN_VBA_API int CALL_VBADLL SimplePCA_(int m, int n, int r,
					 double *_a,
					 double *_mu, double *_lambda, double *_v){
  
  // Map オブジェクト (既定通りの列優先)
  Map<MatrixXd> a(_a, m, n);
  Map<VectorXd> mu(_mu, m);
  Map<VectorXd> lambda(_lambda, r);
  Map<MatrixXd> u(_v, m, r);

  // 平均を計算する
  mu = a.rowwise().mean();
 
  // 平均減算した行列を特異値分解する
  // (ComputeThinUを与えて特異値と左特異ベクタだけを計算する)
  JacobiSVD<MatrixXd> SVD((a.colwise() - mu), ComputeThinU);
  
  // 特異値の二乗と左特異ベクタを返す
  lambda = (SVD.singularValues()).block(0, 0, r, 1).array().square();
  u =  (SVD.matrixU()).block(0, 0, m, r);

#ifdef _DEBUG
  cout << "A=[" << a << "];" << endl;
  cout << "mu=[" << mu << "];" << endl;
  cout << "lambda=[" << lambda << "];" << endl;
  cout << "V=[" << u << "];" << endl;
#endif

  return 0;
}

ビルドの方法は前回の記事を参照してください.乱数で構成した4\times 3行列の第二主成分までの計算結果は以下です.

A=[0.423394  0.104033 0.561979
   0.988998  0.477972 0.18587
   0.0909832 0.281566 0.924881
   0.155299  0.271587 0.481722];

mu=[0.363135
    0.550947
    0.432477
    0.302869];

lambda=[0.749016
        0.128381];

V=[ 0.170522 0.830388
   -0.631153 0.5002
    0.70674  0.245461
    0.270346 0.00231725];

平均ベクタ,主成分,主成分ベクタが正しく計算されていることが分かります.

FreeBSD 9.1-RC2 公開

FreeBSD 9.1 Releaseに向けたRC2が昨日よりソースコードの形で入手可能になっていたようです(SVNWebのnewvers.sh).前回のRC1はRelease AnnouncementCVSにエクスポートしないという方針が取られるとのことでした.

With both the doc and ports repositories now moved to SVN it has been
decided to not export the 9.1 release branch activity to CVS. So
csup/cvsup update mechanisms are not available for updating to 9.1-RC1.
If you would like to use SVN the branch to use is releng/9.1.

しかしながら,CVSWebのnewvers.shを見る限り,今回はエクスポートが行われているようです.RC2と同時に開始されるportsのfeature freezeに合わせてローカルのtinderboxを更新するつもりでしたので,既定のcsupでもソースコードが入手できるのは良いのですが,著者は現在のところ

という方法で更新を行っているのでそろそろtinderboxも既定でsubversionを使うようになって欲しいとは思います.

am-utilsを使ったUDF形式のDVDのマウント

今日はFreeBSDでUDF形式のDVDをマウントする際,どのような設定をするのが便利かと言うことについて考えてみます.ここでの論点は以下です.

  1. root権限を用いないマウント操作は可能か
  2. DVDをマウントする際にudfcd9660のいずれを用いるか
  3. 日本語ファイル名を文字化けさせずにマウントするにはどのようにするか

このようなことを考えるのは,先頃,日本語のファイル名を含むディスクからのコピーで幾らかの問題に遭遇したためです.二三の方法を試した限りでの結論を述べれば,以下の方法が良さそうです.

  1. Auto Mount Daemonを使う
  2. udfバイスを使う
  3. Kernel iconvを使う

まず,root権限の必要性ですが,FreeBSDにはam-utilsがamd,Auto Mount Daemonとして添付されていますので,これを使えば良さそうです.二番目のデバイスの選択ですが,UDF Bridgeによる互換性機能を使えば9660バイスでもマウントは可能ですが,三番目の日本語ファイル名との兼ね合いからするとudfバイスが良さそうです.要するに最も面倒なのは日本語ファイル名を文字化けさせない,という要請につきるようです.

なお,本記事の内容はFreeBSD 9.1-RC1 (amd64)で行い,端末のロケールUTF-8の設定になっています.

まず,日本語のファイル名を含むUDF形式のDVDを用意し,マウント時に用いるデバイスとKernel iconv使用の違いで文字化けの有無を調べます.

kldload udf
kldload libiconv
kldload udf_iconv
kldload cd9660_iconv

mount -t udf /dev/cd0 /cdrom/ && ls -lat /cdrom && umount /cdrom
mount -t cd9660 /dev/cd0 /cdrom/ && ls -lat /cdrom && umount /cdrom
mount -t udf -o -C=UTF-8 /dev/cd0 /cdrom/ && ls -lat /cdrom && umount /cdrom
mount -t cd9660 -o -C=UTF-8 /dev/cd0 /cdrom/ && ls -lat /cdrom && umount /cdrom

結果は以下です.

device kiconv 日本語ファイル名の表示
udf - ×
cd9660 - ×
udf
cd9660 ×

UDF形式のファイル名の文字コードはそもそもUTFなのですから,Kernel iconvの設定は不要になるように思うのですが,最上段を見る限りそうでもないようです.したがって,udfとKernel iconvの設定でam-utilsを使うのが良さそうです.

さて,実際の設定方法です.まず,起動時にUDFやKernel iconvのモジュールを読み込むために/boot/loader.confに以下を追記します.

udf_load="YES"
libiconv_load="YES"
udf_iconv_load="YES"

次に以下の内容を/etc/amd.mapに追記します.

udf             type:=program;\
                fs:=/cdrom;\
                mount:="/sbin/mount mount -t udf -o -C=UTF-8 /dev/cd0 ${fs}";\
                unmount:="/sbin/umount umount ${fs}";

ただし,空白は適切な数のtabspaceに置き換えます.最後に/etc/rc.confに以下を追記します.

amd_enable="YES"
amd_flags="-a /.amd_mnt -l syslog /am-utils /etc/amd.map"

以下のようにしてamdを起動します.

/etc/rc.d amd restart

ユーザー権限でマウントを行うには以下のようにします.

cd /am-utils/udf # 自動的にマウントが行われる

マウントを解除するには以下のようにします.

cd                   # 別のディレクトリに移動する
amq -u /am-utils/udf # アンマウント操作
df                   # 確認する

以上がam-utilsを使って日本語ファイル名を含むUDF形式のディスクを文字化けさせずにマウントするための手順です.

最後になりますが,-C=UTF-8の部分を-C=EUCJPにしたところ,lsの出力をlvに渡すことで正常な表示が行われました.したがって,端末のロケールEUC-JPならばKernel iconvを使いさえすればudfcd9660いずれのデバイスを使っても日本語の表示は問題ないと思われます.

findを使った再帰的なファイル属性変更

CD-Rなどの読み出し専用のメディアからコピーしたファイルや,FreeBSDとは異なる処理系で作成されたもので実行属性がついてしまっているファイルの属性を変更したい場合があります.こうした場合はchmodを利用することになるのですが,数が多い場合やディレクトリの階層が深い場合,作業に一考を要することになります.典型的には以下の要望がchmodへの再帰オプション-Rでは解決できないためです.

  • ディレクトリに与える属性は755
  • ファイルに与える属性は644

この場合,findxargsを利用することになります.find-type引数によって列挙するファイルの種類を指定できるからです.したがって,あるディレクトtargetdir再帰的に探索し,下位の階層にあるディレクトリとファイルにそれぞれ755644なる属性を与えるには以下のようにします.

find /path/to/targetdir -type f -print0 | xargs -n 100 -0 chmod 644
find /path/to/targetdir -type d -print0 | xargs -n 100 -0 chmod 755

xargsを使っているのはfindが列挙したファイル名が多すぎる場合にこれを適当な(ここでは100)に分割してchmodに与えるためです.findxargsそれぞれに与えている-print0-0引数は空白類をエスケープするためのものです.著者個人は空白類をファイル名に含めることはないのですが,他者から提供を受けたデータ類にはそういったものもあるためです.

ちなみに,findの引数には有用なものが多くあり,単純な内容であれば-exec引数によって直接的に何らかの操作を行うことも可能です.また,ファイルの削除程度であれば専用の引数が用意されています.例えばディレクトtargetdir再帰的に探索して*~なるファイル名を持つものを削除するには以下のようにします.

find /path/to/targetdir -type f -name "*~" -print -delete

rsyncによる文字コード変換を伴うファイル同期

ファイル同期に良く用いられるrsyncにはiconvを利用してファイルの文字コードを変換する機能があります.今日はFreeBSD上でこの機能を利用してみます.

さて,rsyncFreeBSDportsに取り込まれており,net/rsyncからインストール可能なのですが,件のオプションは既定では有効になっていません.iconvによる文字コード変換を有効にしてインストールを行うには以下のようにします.

cd /usr/ports/net/rsync
make WITH_ICONV=true install clean
rehash

ファイル名の文字コードShift-JISからUTF-8に変換しつつフォルダを同期するには以下のようにします.

rsync -av --iconv=Shift-JIS,UTF-8 /path/to/srcdir/. /path/to/dstdir/.

なお,iconvで利用可能な文字コードは以下のようにすることで調べることができます.

iconv -l

ちなみに実際に行いたかったことは日本語ファイル名を含むCD-Rからのデータのコピーです.このディスクは古いデータの整理を行っている最中に出土したもので,色々試したのですが文字コードが何であるかいまいち良く分からなかったという事情のためです.結論だけ述べるとFreeBSDのKernel iconvと組み合わせることでUTF-8のファイル名でのデータのコピーができました.

kldload libiconv
kldload cd9660_iconv
mount -t cd9660 -o -C=Shift-JIS /dev/cd0 /media/cdrom
rsync -av --iconv=Shift-JIS,UTF-8 /media/cdrom/. /path/to/dstdir/.

Shift-JISを経由するのは何となく非効率な気もしますが,こうしたことを行う理由はKernel iconvにおけるファイル名の文字コード変換で変換先をUTFにする場合,その機能に制限があるためです.

Excel VBAのインターフェース継承を試す

Excel VBAには一応クラスの機能がありオブジェクト志向でプログラムを作成することもできます.さて,今日はImplementsキーワードを利用したインターフェース継承を試してみます.インターフェース継承をざっくりと説明すると,クラスモジュールを「扱う側」の処理を共通化することに利用できる考え方となるように思います.多相性の実現が可能となると言えるかも知れません.

実装例として,ある関数で記述される数値の配列をクラス化し,それをグラフとして描画する処理を共通化してみます.そこで,以下のようなものを実装してみます.

  • グラフを描画するための関数 (DrawGraph())
  • 上記関数に与えるインタフェースを定義したクラス(抽象クラスiGraphData)
  • 抽象クラスを継承し,データを保持するクラス(正弦波クラスcSinusoidal,直線クラスcLinear)

まず,グラフを描画するのに必要なデータを考えると以下のようなものがありそうです.

  • データの数とデータ
  • 横軸と縦軸の名前
  • 関数名

これらを取得するためのプロパティを定義した抽象クラスを作れば良さそうです.例えば以下のようにします.

' iGraphData.bas
Option Explicit
' データの数を取得する
Public Property Get nD() As Long
End Property
' i番目のXを取得する
Public Property Get XV(tIdx As Long) As Double
End Property
' i番目のYを取得する
Public Property Get V(tIdx As Long) As Double
End Property
' 横軸の名称を取得する
Public Property Get strXVName() As String
End Property
' 縦軸の名称を取得する
Public Property Get strVName() As String
End Property
' 関数の名称を取得する
Public Property Get strFName() As String
End Property

次に上記のようなクラスを与えて,グラフとして描画する関数DrawGraphを以下のように記述します.

' DrawGraph.bas
Option Explicit
' グラフを描画する関数
Public Sub DrawGraph(tSheet As String, tGD As iGraphData)
    Dim i As Long
        
    ' シートを作成する
    With ThisWorkbook
        .Sheets.Add After:=.Sheets(.Sheets.Count)
        .Sheets(.Sheets.Count).Name = tSheet
    End With
    
    With ThisWorkbook.Worksheets(tSheet)
        ' 前に出す
        .Activate
        
        ' データを書き出す
        For i = 0 To tGD.nD - 1
            .Cells(i + 1, 1) = tGD.XV(i)
            .Cells(i + 1, 2) = tGD.V(i)
        Next
    
        ' グラフを描画する
        .Shapes.AddChart.Select
        With ActiveChart
            '.HasTitle = True
            .ChartType = xlLine
            
            .Axes(xlCategory, xlPrimary).HasTitle = True
            .Axes(xlCategory, xlPrimary).AxisTitle.Characters.Text = tGD.strXVName
            .Axes(xlValue, xlPrimary).HasTitle = True
            .Axes(xlValue, xlPrimary).AxisTitle.Characters.Text = tGD.strVName
            
            .SetSourceData Source:=Range("A1")
            With .SeriesCollection(1)
                .Values = Range(Cells(1, 2), Cells(1 + tGD.nD - 1, 2))
                .XValues = Range(Cells(1, 1), Cells(1 + tGD.nD - 1, 1))
                .MarkerStyle = xlMarkerStyleCircle
                .Name = tGD.strFName
                .MarkerStyle = xlMarkerStyleNone ' マーカーを消す
            End With
            
            .Parent.Top = Range("C2").Top
            .Parent.Left = Range("C2").Left
        End With
    End With
End Sub

やっていることは横軸の値と縦軸の値を1列目と2列目に書き出し,線グラフとして描画するものです.

さて,どちらかと言えばここからが本題です.クラスiGraphDataは関数DrawGraphとのデータのやり取りを記述するもので,何の処理も記述していませんから,数値データを保持するための具体的なクラスとしてcSinusoicalを実装します.

' cSinusoical.bas
Option Explicit

Private mNData As Long
Private mValue() As Double
Private mXValue() As Double
Private mScale As Double
Private mTheta0 As Double
Private mIsCalculated As Boolean

' インターフェース継承
Implements iGraphData

' データの数を取得する
Public Property Get iGraphData_nD() As Long
    If mIsCalculated Then
        iGraphData_nD = mNData
    End If
End Property

' i番目のXを取得する
Public Property Get iGraphData_XV(tIdx As Long) As Double
    If mIsCalculated Then
        iGraphData_XV = mXValue(tIdx)
    End If
End Property

' i番目のXを取得する
Public Property Get iGraphData_V(tIdx As Long) As Double
    If mIsCalculated Then
        iGraphData_V = mValue(tIdx)
    End If
End Property
    
' 横軸の名称を取得する
Public Property Get iGraphData_strXVName() As String
    iGraphData_strXVName = "Theta"
End Property

' 縦軸の名称を取得する
Public Property Get iGraphData_strVName() As String
    iGraphData_strVName = "Y"
End Property

' 関数の名称を取得する
Public Property Get iGraphData_strFName() As String
    iGraphData_strFName = "Sinusoidal Curve"
End Property

' コンストラクタ
Private Sub Class_Initialize()
    mIsCalculated = False
End Sub

' デストラクタ
Private Sub Class_Terminate()
End Sub

' 関数値を計算する
Public Sub Calc(tXVmin As Double, tXVMax As Double, tDiv As Long, _
                tScale As Double, tTheta0 As Double)
    Dim i As Long

    ' 領域確保
    ReDim mValue(tDiv - 1) As Double
    ReDim mXValue(tDiv - 1) As Double
    
    mScale = tScale
    mTheta0 = tTheta0
    mNData = tDiv
    
    ' 計算を実行する
    For i = 0 To mNData - 1
        mXValue(i) = tXVmin + i * (tXVMax - tXVmin) / tDiv
        mValue(i) = mScale * Sin(mXValue(i) + mTheta0)
    Next
    
    mIsCalculated = True
End Sub

このクラスは以下の関数値を計算し配列として保持するものです.
y = A\sin\left(\theta - \theta_0\right)
同様に以下の直線の関数値を計算して配列として保持するクラスcLinearを実装します.
y = Ax + b

' cLinear.bas
Option Explicit

Private mNData As Long
Private mValue() As Double
Private mXValue() As Double
Private mAValue As Double
Private mBValue As Double
Private mIsCalculated As Boolean

' インターフェース継承
Implements iGraphData

' データの数を取得する
Public Property Get iGraphData_nD() As Long
    If mIsCalculated Then
        iGraphData_nD = mNData
    End If
End Property

' i番目のXを取得する
Public Property Get iGraphData_XV(tIdx As Long) As Double
    If mIsCalculated Then
        iGraphData_XV = mXValue(tIdx)
    End If
End Property

' i番目のXを取得する
Public Property Get iGraphData_V(tIdx As Long) As Double
    If mIsCalculated Then
        iGraphData_V = mValue(tIdx)
    End If
End Property
    
' 横軸の名称を取得する
Public Property Get iGraphData_strXVName() As String
    iGraphData_strXVName = "X"
End Property

' 縦軸の名称を取得する
Public Property Get iGraphData_strVName() As String
    iGraphData_strVName = "Y"
End Property

' 関数の名称を取得する
Public Property Get iGraphData_strFName() As String
    iGraphData_strFName = "Linear Function"
End Property

' コンストラクタ
Private Sub Class_Initialize()
    mIsCalculated = False
End Sub

' デストラクタ
Private Sub Class_Terminate()
End Sub

' 関数値を計算する
Public Sub Calc(tXVmin As Double, tXVMax As Double, tDiv As Long, _
                tAvalue As Double, tBvalue As Double)
    Dim i As Long

    ' 領域確保
    ReDim mValue(tDiv - 1) As Double
    ReDim mXValue(tDiv - 1) As Double
    
    mAValue = tAvalue
    mBValue = tBvalue
    mNData = tDiv
    
    ' 計算を実行する
    For i = 0 To mNData - 1
        mXValue(i) = tXVmin + i * (tXVMax - tXVmin) / tDiv
        mValue(i) = mAValue * mXValue(i) + mBValue
    Next
    
    mIsCalculated = True
End Sub

最後に呼び出し側である関数ExecDrawGraphを以下のように実装します.

' Main.bas
Option Explicit

Public Sub ExecDrawGraph()
    Dim cSin As cSinusoidal
    Dim cLin As cLinear
    
    Set cSin = New cSinusoidal
    Call cSin.Calc(0, 2 * 3.14, 100, 1.5, -3.14 / 2)
    Call DrawGraph("sin_curve", cSin)
    
    Set cLin = New cLinear
    Call cLin.Calc(0, 10, 100, 3, 2)
    Call DrawGraph("linear_func", cLin)
End Sub

これらを実行するとシートsin_curveに正弦波の関数が,linear_funcに直線の関数が描画されます.

内部処理がそれぞれ微妙に異なる二つのクラスcSinusoidalcLinearについてグラフを描画する関数を共通化できたことが分かります.この程度の例では何が利点かは分かりづらいのですが,クラスモジュールと継承を使うことは以下の利点があります.

  • データとデータを扱う手段をクラスとしてまとめ込める
  • インターフェース継承によってクラスを「扱う側」の処理を共通にできる

データに対する処理が複雑化した場合,似たような処理に共通の実装を用いるにはクラスモジュールを使うべきでしょう.重要なのはインターフェースを継承したクラスの内部処理がどのように実装されていても良いという点です.今回の例ではcSinusoidalcLinearは双方ともデータを配列で保持していましたが,ファイルやシートに記述されたデータを読み出すクラスであっても,データ列が二次元であればiGraphDataを継承することで関数DrawGraph()によってグラフを描画できます.

ちなみに,cSinusoidalcLinearの中身がほとんど同じであるわけですが,実装継承ができればこの辺りもかなり綺麗になるように思います.とは言え,VBAの仕様上それはできないようなので仕方がないのかも知れません.