MSProjectをVBScriptまたはVBAで操作する

目的

MSProjectをVBScriptまたはVBAで操作する。

なお、MSProjectの評価版は下記からダウンロードすることができる。
http://technet.microsoft.com/ja-jp/evalcenter/default.aspx

リファレンス

Project 2013オブジェクト モデルのリファレンス
http://msdn.microsoft.com/ja-jp/library/office/dn175492%28v=office.15%29.aspx

Application and Project Object Map(Office 2010)
http://msdn.microsoft.com/en-us/library/office/ff863668%28v=office.14%29.aspx

VBAでの操作例

MSProjectはExcelなどの他のオフィス製品と同様にVBAで操作を行うことができる。

タスクの選択

プロジェクト中のタスクに参照する場合はProjectオブジェクトのTasksコレクションから参照する。
TasksコレクションはTaskのオブジェクトの集合だ。
下記のようにタスク名またはID名を指定してタスクを取得できる。

サンプル:

    Dim prj As Project
    Set prj = ThisProject
    Debug.Print prj.tasks.item(7).id
    Debug.Print prj.tasks.item("TEST11").id

タスクのプロパティ

タスクオブジェクトの詳細情報は下記のプロパティから設定、取得できる。

プロパティ名 説明
Name タスクの名称。ProjectのTasksオブジェクトはこの名称をキーに管理されている。
OutlineLevel タスクの階層のレベル。親がいない場合は1になる。OutlineLevelを指定しても階層構造を変更することはできない。
OutlineChildren サブタスクの一覧。Taskオブジェクトのコレクションになっている。このコレクションに対してAddを行なっても階層構造を考慮した追加はされない。
Notes タスクに対する注釈を記述できる。
Start タスクの開始日
Finish タスクの終了日
PercentComplete 達成率
ActualStarte 開始日実績
ActualFinish 終了日実績
BaselineStart タスクの割り当ての基準開始日
BaselineFinish タスクの割り当ての基準終了日
ResourceNames リソースの名称。複数存在する場合は,で区切る
Resouces Resourceオブジェクトのコレクション。
PredecessorTasks 先行タスクのTaskオブジェクトのコレクション。存在しない場合は、先行タスクがないことをあらわす。

サンプル:

    Dim prj As Project
    Set prj = ThisProject
    Dim tsk As task
    Dim pTsk As task
    Dim r As Resource
    For Each tsk In prj.tasks
        Debug.Print tsk.id
        Debug.Print "  Name:" & tsk.name
        Debug.Print "  OutlineLevel:" & tsk.OutlineLevel
        Debug.Print "  Notes:" & tsk.Notes
        Debug.Print "  Start:" & tsk.Start
        Debug.Print "  Finish:" & tsk.Finish
        Debug.Print "  PercentComplete:" & tsk.PercentComplete
        Debug.Print "  ActualStart:" & tsk.ActualStart
        Debug.Print "  ActualFinish:" & tsk.ActualFinish
        Debug.Print "  BaselineStart:" & tsk.BaselineStart
        Debug.Print "  BaselineFinish:" & tsk.BaselineFinish
        Debug.Print "  ResourceNames:" & tsk.ResourceNames
        Debug.Print "  Resources:"
        For Each r In tsk.Resources
            Debug.Print "    Resouce.Id :" & r.id
            Debug.Print "    Resouce.Name :" & r.name
        Next
        Debug.Print "  PredecessorTasks:"
        For Each pTsk In tsk.PredecessorTasks
            Debug.Print "    " & pTsk.id & "  " & pTsk.name
        Next
        ' Readonly
        Debug.Print "  OutlineChildren:"
        For Each pTsk In tsk.OutlineChildren
            Debug.Print "    " & pTsk.id & "  " & pTsk.name
        Next
    Next

タスクの追加

末尾への追加例

ThisProject.Tasks.Add "TaskName1"

指定の位置に追加例

ThisProject.Tasks.Add "TaskName2",3

タスクの階層については自分で指定しなければならない。
親となるタスクの直後に追加した後に、OutlineIndentまたは、OutlineOutdentを用いて階層を調整する必要がある。

' 指定のdepth に階層をあわせる場合
            diffLv = depth - task.OutlineLevel
            For i = 1 To Abs(diffLv)
                If diffLv > 0 Then
                    Call task.OutlineIndent
                Else
                    Call task.OutlineOutdent
                End If
            Next i

タスクの削除

TaskオブジェクトのDeleteメソッドを用いる
全て削除する場合は下記のとおり

    Dim tsk As task
    Dim i As Long
    For i = ThisProject.tasks.Count To 1 Step -1
        prjObj.tasks.item(i).Delete
    Next

リソースの割り当て

TaskオブジェクトのResourceNamesプロパティまたはAssignmentsプロパティよりタスクにアサインしたリソースの追加を行なえる。

task.ResourceNames = "owner"

リソースID 212のリソースをタスクに割り当てる例

ActiveProject.Tasks(1).Assignments.Add ResourceID:=212

VBScriptでの実行例

"MSProject.Application"をCreateObjectすることで操作が可能になる。
その他はVBAと同様の使用方法となる。

以下の例は指定のプロジェクトの内容をテキストに書き込む例である。

UnpackFile "C:\\Documents and Settings\\All Users\\Documents\\prjSample.mpp", "TEST.txt", True, True

Function UnpackFile(fileSrc, fileDst, pbChanged, pSubcode)
    Dim fo
    Dim fso
    Dim mp
    Dim prj
    Dim targetPrj
    Dim cmp
    Dim tsk
    Dim pTsk
    Dim r

    Set targetPrj = Nothing

    Set fso = CreateObject("Scripting.FileSystemObject")
    Set fo = fso.CreateTextFile(fileDst, True, True)

    Set mp = CreateObject("MSProject.Application")
    mp.DisplayAlerts = False
    mp.AutomationSecurity  = 3 ' msoAutomationSecurityForceDisable
    mp.FileOpenEx fileSrc, True

    For Each prj In mp.Projects
        If prj.Name = fso.GetFileName(fileSrc) Then
            Set targetPrj = prj
            Exit For
        End If
    Next
    If targetPrj Is Nothing Then
        Exit Function
    End If

    fo.WriteLine fileSrc
    fo.WriteLine fileDst
    fo.WriteLine targetPrj.Name
    For Each tsk In targetPrj.tasks
        fo.WriteLine "TaskID:" & tsk.id
        fo.WriteLine "  Name:" & tsk.name
        fo.WriteLine "  OutlineLevel:" & tsk.OutlineLevel
        fo.WriteLine "  Notes:" & tsk.Notes
        fo.WriteLine "  Start:" & tsk.Start
        fo.WriteLine "  Finish:" & tsk.Finish
        fo.WriteLine "  PercentComplete:" & tsk.PercentComplete
        fo.WriteLine "  ActualStart:" & tsk.ActualStart
        fo.WriteLine "  ActualFinish:" & tsk.ActualFinish
        fo.WriteLine "  BaselineStart:" & tsk.BaselineStart
        fo.WriteLine "  BaselineFinish:" & tsk.BaselineFinish
        fo.WriteLine "  ResourceNames:" & tsk.ResourceNames
        fo.WriteLine "  Resources:"
        For Each r In tsk.Resources
            fo.WriteLine "    Resouce.Id :" & r.id
            fo.WriteLine "    Resouce.Name :" & r.name
        Next
        fo.WriteLine "  PredecessorTasks:"
        For Each pTsk In tsk.PredecessorTasks
            fo.WriteLine "    " & pTsk.id & "  " & pTsk.name
        Next
        ' Readonly
        fo.WriteLine "  OutlineChildren:"
        For Each pTsk In tsk.OutlineChildren
            fo.WriteLine "    " & pTsk.id & "  " & pTsk.name
        Next
    Next

    For Each cmp In targetPrj.VBProject.VBComponents
        fo.WriteLine "[CodeModule." & cmp.Name & "]"
        If cmp.CodeModule.CountOfLines > 0 Then
            fo.WriteLine cmp.CodeModule.Lines(1, cmp.CodeModule.CountOfLines)
        End If
        fo.WriteLine ""
    Next
    mp.Quit
    Set mp = Nothing
    Set prj = Nothing

    fo.Close
    Set fo = Nothing
    Set fso = Nothing

    pbChanged = True
    pSubcode = 0
    UnpackFile = True

End Function

応用例

TracのチケットからMsProjectのタスクをインポートする
http://needtec.exblog.jp/21472581/

WinMergeのMSProject用のプラグイン
https://github.com/mima3/MppToText

Microsoft レポートによる帳票の作成

本ドキュメントはMicrosoftレポートを用いた.NET Frameworkによる帳票の作成について記述します。

PDF版は下記からダウンロードできます。
http://needtec.sakura.ne.jp/doc/msreport.pdf

WORDで80ページほどあるので、PDFで読んだ方が楽かもしれません。

MicroSoftレポートとは?

MicrosoftレポートとはVisual Studioが帳票の作成をサポートするために提供しているコントロールです。レポートのデザイン機能と、アプリケーションでレポート処理および表示をするためのReportViewerコントロールが提供されています。
次の図はMicrosoftレポートが、どのように帳票を作成するかを簡単に表したものです。

mr1.png

データソース:
アプリケーションが使用するデータのことで特にアプリケーションで操作する必要のあることが明確なデータです。

レポート定義:
帳票のレイアウトや、どのデータを作成するかをXMLで記述したものですVisual Studioのレポートデザイナを使用して変更が可能です。

ReportViewerコントロール:
データセットとレポート定義より帳票を作成します。

作成できる帳票

Microsoftレポートは形式の帳票を作成できます。以下に、いくつかの例を紹介します。

一覧形式

mr2.png

Excelのように一覧としてデータを表示します。
各フィールドでは書式の指定や数式の指定が行えます。必要であれば、開発者は自分の目的にあった数式を自作して利用することもできます。

単票形式

mr3.png

単票としてデータの詳細を自由なフォーマットで表現します。
数字や、文字だけでなく、画像も使用できます。

グラフ

mr4.png

データをグラフとして表示することができます。
棒グラフだけでなく、折れ線グラフや円グラフなど、様々な表現方法が実現できます。

動作環境

Visual Studio 2005以降でProfessional以上であれば使用できます。
ExpressにはReportViewerコントロールは提供されていません。
なお、マイクロソフトが提供するチュートリアルを行う場合、SQL Serverが別途必要になります。

このドキュメントで紹介するチュートリアルは以下の開発ツールがインストール済みであることを前提とします。
・Visual Studio2013 Professional
・SQL Server2012 Express
・SQL Server Management Studio(SSMSと略される場合がある)

チュートリアル

Microsoftは下記のページにサンプルとチュートリアルを用意しています。
サンプルとチュートリアル
http://msdn.microsoft.com/ja-jp/library/ms251686%28v=vs.90%29.aspx

VisualStudio2013とSQLSERVER2012では、このチュートリアル通りおこなっても、サンプルデータすら作成できません。
この章ではチュートリアルの補足を行います。

サンプルで使用するデータベースの構築

チュートリアルで使用するサンプルを動かすにはSQLSERVERでテスト用のデータベースを作成する必要があります。

チュートリアル : AdventureWorks データベースのインストール
http://msdn.microsoft.com/ja-jp/library/aa992075%28v=vs.90%29.aspx

ここではテストデータのはいったSQLSERVERのデータベースを開発者のPCに構築するための手順が記述してあります。
ただし、ここで記述している通りにやっても、ファイルのダウンロードすらできません。以下の手順を参考にしてSQLSERVERを構築してください。

1.まずテスト用のデータベースを下記のサイトから取得します。
AdventureWorks Sample Databases for the SQL Server 2012 Database Training Kit
http://sql2012kitdb.codeplex.com/releases/view/86805

mr5.png

2.ダウンロードしたMDFファイルをSQLSERVERにアタッチできる場所にコピーします。
SQLSERVERをデフォルトの設定でインストールした場合は以下のパスになるでしょう。
C:\Program Files\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQL\DATA

もし使用しているSQLSERVERがEXPRESSの場合は次のパスになります。
C:\Program Files\Microsoft SQL Server\MSSQL11.SQLEXPRESS\MSSQL\DATA

コピーを行うさい、ダウンロードしたMDFファイルの「ブロックの解除」が行われていることを確認してください。これはファイルを右クリックしてプロパティで確認できます。

mr6.png

3.SQLServer Management Studioを起動してローカルのSQLSERVERに接続します。
「localhost」のWindows認証で接続できるはずです。
もし、Expressの場合は「Localhost\SQLEXPRESS」を指定してください。
4.オブジェクトエクスプローラーで「データベース」ノードを右クリックして「アタッチ」を選択してください。
5.「追加」をクリックしてコピーしたMDFファイルを選択してOKボタンを押します。
6.「データベースの詳細」リストからファイルの種類で「ログ」を選択して削除します。
7.OKボタンを押せばデータベースはアタッチできます。
8.これをすべてのMDFファイルに対して行ってください

チュートリアル:ReportViewerレポートの作成

ここでは下記のチュートリアルを行うための補足説明を行います。

チュートリアル : ReportViewer レポートの作成
http://msdn.microsoft.com/ja-jp/library/ms252073%28v=vs.90%29.aspx
このチュートリアルではテーブルを用いて一覧形式の帳票を作成します。

SQLSERVER上のテーブルが最新のものと違うため、ドキュメント通りやっても動作しないので注意してください。
また、チュートリアルでの言語はVB.NETですが、C#でも同じ手順で動作します。

1.「WINDOWSフォームアプリケーション」を作成します
2.「データソース接続」と「データテーブル」を定義します
(1)ソリューションエクスプローラーから「新しい項目」の追加で「データセット」を追加します。

mr7.png

(2)データセットデザイナが表示されるので、ツールボックスの「TableAdapter」コントロールをドロップします。

mr8.png

これにより、TableAdapter構成ウィザードが開きます。

(3)[データ接続の選択] ページで [新しい接続] をクリックします。

(4)以下のような設定を行ってください

mr9.png

※VisualStudio2008とSQLSERVER2012の場合はエラーが発生します。この時は付録の「VS2008からSQLSERVER2012に接続する」を参照してください。

(5)接続を追加後、TableAdapter構成ウィザードの作成を進めていくとSQLステートメントの入力を求められます。
この際、チュートリアル通りにやるとエラーになります。次のように修正してください。

SELECT S.OrderDate,
 S.SalesOrderNumber,
 S.TotalDue AS TotalSales,
 C.FirstName,
 C.LastName
FROM HumanResources.Employee AS E INNER JOIN
 Person.Person AS C
ON E.BusinessEntityID = C.BusinessEntityID INNER JOIN
Sales.SalesOrderHeader AS S ON E.BusinessEntityID = S.SalesPersonID

(6)これにより、データセットに次のようなデータが作成されました。

mr10.png

これはデータソースウィンドウから使用できます。

mr11.png

3.新しいレポート定義ファイルを追加
(1)「プロジェクト」メニューの「新しい項目」を選択します。

(2)Repotingに存在する「レポート」を選択してください。

mr12.png

(3)[ファイル名] に「SalesOrders.rdlc」と入力し、[追加] をクリックすると、デザイン画面が開かれます。

mr13.png

このようにGUIでレポートを編集するアプリケーションを「レポートデザイナ」といいます。レポート定義ファイルは「レポートデザイナ」だけでなく、下記のように「XML(テキストエディタ)」でも開くことができます

mr14.png

4.レポートレイアウトでテーブルを追加する
テーブルを利用して一覧形式の帳票を作成します。

(1)レポートにテーブルを追加するために、ツールボックスの「テーブル」を選択後、「レポートデザイナ」をクリックします。
これによって、「データセット」プロパティが起動するので、データソースにDataSet1を選択してOKボタンを押下してください。

mr15.png

以後、作成したテーブルとデータセットが関連づけられ、指定したデータセットのフィールドをテーブルのセルで使用することが可能になります。

また、データセットプロパティを閉じると「レポートデザイナ」には3列のテーブルが作成されます。

mr16.png

(2)作成されたテーブルをクリックすると列ハンドルと行ハンドルが表示されます。

mr17.png

(3)左端の列ハンドルを右クリックして左側に列を挿入します。

mr18.png

(4)列の幅を調整します。「プロパティ」ウィンドウで「tablibn」というコントロールを選択して、「Size」ノードの「Width」に任意の数値を入力します。

mr19.png

(5)テーブルに表示するデータを指定します。

(ア)最初の列の中央行にマウスオーバをすると「データソース」のアイコンが出現するのでクリックをします。

mr20.png

(イ)関連付けるフィールドの一覧がメニューとして表示されるので「LastName」を選択してください。

mr21.png

その結果、テーブルには以下のような値が書き込まれます。

mr22.png

1行目と2行目にどのような違いがあるのでしょうか?
実は1行目はただの「文字」ですが、2行目は「式」になります。
各セルを右クリックして「式…」を押下するとそのことが確認できます。

1列目-1行目

mr22.png

1列目-2行目

mr22.png

1行目は「Last Name」というただの文字で「=」がなく式ではありません。
2行目は「=Fields!LastName.Value」という式になっています。
この式は指定したデータセットのフィールドLastNameの値をこのセルに表示することを意味します。

(ウ)同様にOrderData,SalesOrderNumber、TotalSalesを2~4列目に指定します。

mr22.png

これによりテーブルのすべてのセルに何らかの値が入力されました。

5.フォームにReportViewerコントロールを追加する
フォームにReportViewerコントロールを追加して、作成した帳票を表示するようにします。

(1)フォームをデザイナで開いて、ツールボックスから「ReportViewerコントロール」を張り付けてください

mr22.png

(2)ReportViewer コントロールの「Dock」プロパティを「Fill」にすることでコントロールがFormの大きさに合わせて自動リサイズされるようになります。

mr22.png

(3)ReportViewerコントロールの右上隅の三角形をクリックします。

mr22.png

これにより「ReportViewerタスク」メニューが表示されます。

(4)レポートの選択でさっき作成したレポート定義ファイルを選択します。

mr22.png

これにより、Formには次のコントロールが追加されます。
・DataSet
・DataTableBindSource
・DataTableAdapter

これらのコントロールはレポート定義にデータベース上のデータを表示するために使用されます。

(5)この時点でビルドして実行すると次のようなウィンドウが起動されます。

mr22.png

主なアイコンの説明

アイコン 説明
mr22.png ページを遷移するのに使用します。
mr22.png 印刷を実行するためのダイアログを表示します。
mr22.png 印刷プレビューにします。解除するにはもう一度このボタンを押してください。
mr22.png ページの設定を行うためのダイアログを表示します。
mr22.png 別の形式でファイルを保存します。Excel,PDF,Wordのいずれかを選択できます。
mr22.png 任意の単語で検索を行えます。「次へ」を使用することで別ページの単語も検索します。

フィールドの書式を指定する方法

レポートデザイナでは、フィールドの書式を設定することができます。
まず、OrderDateの日付を日本語の書式に変更してみましょう。

1.書式を指定したいセルを右クリックして「テキストボックスのプロパティ」を選択します。

mr22.png

2.テキストボックスのプロパティダイアログでは「数値」項目を選択すると様々な書式を設定できます。
カテゴリで「日付」を選択して書式を選択して、OKでダイアログを終了してください。

mr22.png

3.ビルドして実行すると下記のようにOrderDateが選択した書式のものになっていることが確認できます。

mr22.png

4.同様にTotalSalesを選択して書式を「通貨」にします。

mr22.png

小数点以下桁数や区切り記号の有無などを指定できます。

5.これを再度ビルドして実行すると指定した書式でTotalSalesが表示されます。

mr22.png

セルの修飾

各セルは、それぞれフォント、背景色、罫線などで修飾してデータを見やすくすることが可能です。
1.修飾を行いたいセルを選択します。

mr22.png

2.この状態でプロパティのBackgroundColerで任意の背景色を選択します。

mr22.png

これにより選択した範囲の背景色が変更されました。

3.プロパティのColorに任意の値を指定すると文字の色が変更されます。

mr22.png

4.フォントについてはプロパティのFontを変更することで、フォントの種類やサイズなどが変更できます。

mr22.png

5.罫線はBorderStyleプロパティで変更できます。この例だと上下は実線、左右には線を引かない設定です。

mr22.png

6.ここまで変更したうえでビルドして実行すると、次のような表示になります。

mr22.png

ヘッダーとフッターの設定

印字した時のページ毎にヘッダーとフッターを指定できます。
レポートデザイナを編集中に表示される「レポート」メニューより「ページヘッダーの追加」「ページフッターの追加」を選択してください。

mr22.png
mr22.png

これにより、ページヘッダーとページフッターを表示できるようになりました。
ヘッダー、フッターの高さを変更するにはヘッダー、フッターのプロパティのHeightで変更ができます。

mr22.png

ヘッダー、フッターには下記のコントロールのみが配置可能です。テーブルやマトリックスといったコントロールは配置できません。

ヘッダー、フッターに配置可能なコントロール
・テキストボックス
・線
・四角形
・画像

ヘッダー、フッターに固定の文字を表示

この例ではヘッダーに帳票名を表示します。ヘッダーにツールボックスよりテキストボックスを選択してヘッダーに張り付けます。貼り付けたテキストボックスに任意の文字を入れてください。

mr22.png

テキストボックスの枠線が表示されていますが、実際、実行すると非表示になっています。そして、複数ページにわたり、入力した文字が出力されていることを確認できます。

mr22.png

ヘッダー、フッターに現在の日付を表示する

テキストボックスには固定の文字だけでなく式を入力することで、日付などを表示することが可能です。固定文字の時と同様にヘッダーまたはフッターにテキストボックスを張り付けます。その後、右クリックでメニューを表示して「式」を選択します。

mr22.png

「式」ダイアログを表示さるので、ここに式を入力することが可能です。式を直接入力することも可能ですし、「カテゴリ」一覧から使用できる数式を選択ですることも可能です。
今日の日付を表示するには「カテゴリ」の「共通の関数→日付と時刻」を選択し、アイテムで「Today」を選択します。

mr22.png

この時点で表示をすると「2013/12/16 0:00:00」というような書式になります。

mr22.png

テキストボックスの書式は、テキストボックスのプロパティから変更が可能です。

mr22.png

これにより以下のように適切な書式の日付が表示されます。

mr22.png

ヘッダー、フッターにページ数

日付と同様に、テキストボックスの式を用いてページ数を表示することが可能です。
ヘッダーにテキストボックスを張り付けて以下の式を入力します。

=Globals!PageNumber & "/" & Globals!TotalPages

式で文字として連結を行う場合は「&」演算子を使用します。
これにより、ヘッダーは次のように、「現在のページ数 / 総ページ」を表示します。

式には様々な機能が存在しており、次のページから確認することが可能です。

レポートの一般的な式 (Visual Studio レポート デザイナ)
http://msdn.microsoft.com/ja-jp/library/ms251668%28v=vs.90%29.aspx

式のリファレンス (レポート ビルダーおよび SSRS)
http://msdn.microsoft.com/ja-jp/library/ee240847.aspx

開発者は独自の数式を作成することも可能で、その方法は「カスタムコードの作成」にて説明します。

各ページにテーブルのヘッダーを表示する

ヘッダーとフッターの説明を行いました。しかし、説明した方法では大量のデータを保持するテーブルのヘッダーを表示ができません

mr22.png

テーブルのヘッダーをページ毎に表示するには、レポートデザイナの行グループのプロパティを修正する必要があります。そのために、まず、行、列グループの「詳細設定モード」にチェックをつけます。

mr22.png

詳細設定モードをチェックすることで、行グループ、列グループに「(静的)」というアイテムが表示されます。各ページでテーブルの行のヘッダーを繰り返し表示するには、行グループの「(静的)」を選択して、プロパティの「RepeatOnNewPage」をTrueにしてください。

mr22.png

これにより、複数ページにわたりテーブルのヘッダーを表示することができます。

mr22.png

テーブルのグループを定義する方法

ReportViewerコントロールのテーブルでは、与えられたデータをグループ化して表示することができます。今回の例では、LastNameとFirstNameでデータをグループ化して各個人の売り上げの帳票を出力します。

1.行ハンドルを右クリックして「グループの挿入」>「親グループ」を選択します。

mr22.png

2.グループダイアログが表示されるので、グループ化のコンボボックスで「LastName」を選択し、「グループヘッダーの追加」と「グループフッターの追加」にチェックをします。

mr22.png

これより、テーブルにはグループの名称を表示する列と、行グループにグループが追加されました。

mr22.png

3.この例ではグループ化の条件が[LastName]だけです。これでは同じLastNameを持つ別の人物も同じグループにしてしまいます。そのため、グループ化の条件に「FirstName」も追加する必要があります。
まず、行グループで条件を追加したいグループを選択して右クリックして「グループプロパティ」を選択します。

mr22.png

グループプロパティ」ダイアログではグループ化についてのプロパティを表示確認できます

mr22.png

条件を追加するには、グループ化の追加ボタンを押して条件に「FirstName」を追加します。

mr22.png

4.この状態でレポートを出力すると次のようになります。

mr22.png

各グループの先頭と、末尾にグループヘッダーとグループフッターが付与されていることが確認できます

5.この帳票ではグループ化を行う際に、テーブルに列が自動的に挿入されましたが、これは表示に使用するだけなので、削除してもグループ化は解除されません。
テーブルで挿入された列を右クリックして、「列の削除」を選択します。

mr22.png

この状態でレポートを作成すると次のように、グループ用の列が存在しなくても、グループ化されていることがわかります。

mr22.png

グループの削除方法

グループを削除するには、削除したい行グループを選択して、右クリックで「グループの削除」を行います。

mr22.png

グループ別のデータ集計

グループ化を行うことで、グループ別にデータを集計することができます。今回はセールスマン毎での売り上げの合計を求めます。

1.グループヘッダーで合計を出力したい箇所を右クリックして「式」を選択します。

mr22.png

2.式ダイアログが表示されるので、「式の設定」に次の式を入力します。

=Sum(Fields!TotalSales.Value)

3.追加を行ったら、このセルに対して通貨型の書式を設定します。「フィールドの書式を指定する」を参考にしてください。

4.これを実行することで、各グループのヘッダーに売り上げの総計が表示されるようになります。

mr22.png

次にグループヘッダーにフルネームとデータ件数を表示します。

1.テーブルの1行目、1列目に次の式を入力します。

=Fields!FirstName.Value + " " + Fields!LastName.Value + ": " + vbCrLf + Count(Fields!SalesOrderNumber.Value).ToString()

mr22.png

2.「LastName」が行毎に表示されているので、それを削除します。これにより、名前が行ごとにでるのではなく、グループヘッダーのみに表示されます。

mr22.png

3.この状態でレポートを出力することでグループヘッダーにフルネームとデータ件数が表示されます。

mr22.png

4.レポートで、次のページをみてみます。以下のように先ほど設定したはずのテーブルのヘッダーが表示されなくなっています。

mr22.png

これはグループ化を行ったため、行グループの状態が、さっきと変更されたために発生します。

5.行グループの一番上の「静的」のプロパティで「RepeatOnNewPage」を「True」、「KeepWIthGroup」を「After」にしてください。

mr22.png

6.その後、実行すると次ページ以降にもテーブルのヘッダーが表示されます。

mr22.png

7.次にグループが複数ページにまたがった場合に、グループヘッダーを同様にページの先頭に表示するようにします。行グループで、グループ名の直下の「静的」を選択して同様にプロパティの「RepeatOnNewPage」を「True」、「KeepWIthGroup」を「After」にしてください。

mr22.png

8.これにより、各ページの先頭にグループのヘッダーが表示されるようになります。

mr22.png

表形式のレポート内のグループを並べ替える

ReportViewerでは並び替えの機能を提供しています。これにより、作成したグループを並べ替えることができます。この例ではグループが保持するデータの数順に並び替えます。

1.行グループの最上位のアイテムを選択して右クリックして、「グループプロパティ」を選択します。

mr22.png

2.「グループプロパティ」ダイアログが開くので「並べ替え」を選択して「追加」ボタンをおします。

mr22.png

並び替えの条件には以下の式、順序は「昇順」としてOKを押してください。

```

3.ここで実行を行うと販売数の少ない人ほど最初のページに表示されるようになります。

#### 表形式のレポートのグループ内の詳細行を並べ替える
1.行グループで、グループ内の詳細を選択して「グループプロパティ」を選択します。
![mr22.png](https://needtec.sakura.ne.jp/wod07672/wp-content/uploads/2020/03/de623a53-da2c-d4c8-1a9a-bbe4d2a44b57.png)

2.グループプロパティダイアログで、「並び替え」に縦棒を追加します。
[TotalSales]をコンボボックスで選択して、順序を「降順」にしてください。
![mr22.png](https://needtec.sakura.ne.jp/wod07672/wp-content/uploads/2020/03/9799fb2f-c25d-2154-2bb2-b92186b1956d.png)

3.ここで実行すると、売り上げの降順でグループ内が並び替えされているようになります。
![mr22.png](https://needtec.sakura.ne.jp/wod07672/wp-content/uploads/2020/03/09caafae-2f22-00a8-aad8-9b92399669d7.png)

## マトリックスを用いたクロス集計
この項目ではマトリックスを用いたクロス集計を行います。クロス集計とは項目を掛け合わせて、それぞれの項目が交わるセルに合計値や数などを記載した表です。
このチュートリアルは下記のMicrosoftのチュートリアルを説明しています。

 __チュートリアル : ローカル処理モードでのデータベース データ ソースと ReportViewer Windows フォーム コントロールの使用__
http://msdn.microsoft.com/ja-jp/library/ms251724%28v=vs.90%29.aspx

このサンプルでは縦軸に部署、横軸にシフト(朝、夕、夜)をとり、それぞれ従業員が何人出勤しているかを表示します。![mr22.png](https://needtec.sakura.ne.jp/wod07672/wp-content/uploads/2020/03/c615ac95-a7be-1d98-6d50-20f8f7a1b944.png)

1.データソース接続とDATATABLE定義

(1)[プロジェクト]メニューの[新しい項目の追加]をクリックします

(2)[新しい項目の追加]ダイアログで[データセット]を選択して追加します。この際、任意の名称を入力できます。ここでは規定の名前であるDataSet1.xsdを使用します

(3)ツールボックスから[TableAdapter]をデータセットデザイン画面にドラッグします。これによりウィザード画面が表示されます。先のチュートリアルで実行した『「データソース接続」と「データテーブル」を定義します』を参照してAdventureWorks2012に接続をします。

(4)SQLステートメントの入力まで進めて次のSQLを入力します。

```sql
SELECT d.name as Dept, s.Name as Shift, e.BusinessEntityID as EmployeeID
FROM (HumanResources.Department d
INNER JOIN HumanResources.EmployeeDepartmentHistory e
ON d.DepartmentID = e.DepartmentID)
INNER JOIN HumanResources.Shift s
ON e.ShiftID = s.ShiftID

2.レポートのデザイン

(1)[プロジェクト]メニューの[新しい項目の追加]をクリックします

(2)Reportingの[レポート]を選択します。これでレポートデザイナが起動します。

(3)ツールボックスのマトリックスを選択してレポートデザイナにドロップしてください。

mr22.png

(4)データセットのプロパティが起動するので、先ほど追加したDataSetを選択してください。

mr22.png

これでOKを押すことでマトリックスがレポートに追加されます。

mr22.png

(5)「行」と記述しているセルに部署を表すDeptを選択します。

mr22.png

(6)「列」と記述しているセルにシフトを表すShiftを追加します

mr22.png

(7)データ」とあるセルを右クリックして式プロパティダイアログを表示して式を入力します。

mr22.png

=Count(Fields!EmployeeID.Value)

mr22.png

3.アプリケーションへのReportViewerコントロールの追加
(1)FormデザイナにおいてツールボックスのReportViewerコントロールをFormに張り付けます。

mr22.png

(2)ReportViewerコントロールのプロパティの[Dock]を[Fill]に指定します。これでReportViewerコントロールはForm大きさに一致するようになります。

(3)右上隅の三角形をクリックしてReportViewerタスクメニューを表示して、レポート選択コンボボックスで作成したレポートを選択します。

mr22.png

(4)指定したレポートにデータを表示するためにDataSetコントロール、DataTableBindingSourceコントロール、DataTableAdapterコントロールが追加されます。

mr22.png

(5)この時点でビルドして実行すると次のようなクロス集計を行うレポートが作成されます。

mr22.png

単票形式の帳票の作成

このチュートリアルでは一覧形式では表現できない帳票を作成します。
この例では以下のようなユーザー情報を名刺として印字するような帳票を作成します。
この例では画像データを使用するので、下記からダウンロードするか、ご自分でPNGファイルをご用意してください。
http://needtec.sakura.ne.jp/doc/ReportCard.zip

データソースとして使用するビジネスオブジェクトの作成

今回の使用するデータはデータベースではなく、クラスで定義した情報を使用します。
Users.csというクラスを追加して以下のような実装をします。

using System;
using System.Collections.Generic;
using System.Drawing;

class User
{
 public string Name { get; set; }
 public DateTime Birth { get; set; }
 public string Tel { get; set; }
 public string Address { get; set; }
 public Byte[] ImageData { get; set; }

 public User(string name, string birth, string tel, string address, string imagepath)
 {
 Name = name;
 try
 {
 Birth = DateTime.Parse(birth);
 }
 catch (Exception ex)
 {
 throw new ArgumentException("birthの書式が不正です\n" + ex.Message);
 }
 Tel = tel;
 Address = address;
 try
 {
 Image img = Image.FromFile(imagepath);
 ImageConverter imgcv = new ImageConverter();
 ImageData = (byte[])imgcv.ConvertTo(img, typeof(byte[]));
 }
 catch (Exception ex)
 {
 throw new ArgumentException("imagepathの指定が不正です\n" + ex.Message);
 }

 }
}
class Users
{
 private List users;
 public Users()
 {
 users = new List();
 users.Add(new User("やる夫", "2005/1/1", "000-000-1111", "2ch ニュー即 VIPスレ 000-333-555 紙スレ", @"..\..\Data\img001.png"));
 users.Add(new User("やらない夫", "2005/2/1", "000-000-1112", "2ch VIPスレ", @"..\..\Data\img002.png"));
 users.Add(new User("ゆっくり霊夢", "2008/1/1", "000-001-1111", "ニコニコ 実況", @"..\..\Data\img003.png"));
 users.Add(new User("ゆっくり魔理沙", "2008/2/1", "000-001-1112", "ニコニコ 実況", @"..\..\Data\img004.png"));
 users.Add(new User("はちゅねミク", "2009/3/1", "000-001-1113", "ニコニコ 実況", @"..\..\Data\img005.png"));

 }
 public List GetUsers()
 {
 return users;
 }
}

レポート定義にオブジェクトを関連づける

ここではレポート定義ファイルを作成してデータソースとしてオブジェクトを関連付ける方法を記述します。

1.プロジェクトメニューの「新しい項目の追加」でレポートを追加してください。

2.レポートのデザイナを選択中に「表示」メニューから「レポートデータ」を選択することで「レポートデータ」ウィンドウが表示されます。これはCTRL+ALT+Dで代替できます。

3.レポートデータウィンドウにて、「データセット」を右クリックして「データセットの追加」を選択してください。

mr22.png

4.データソース構成ウィザードが起動するので、ウィザードを利用してデータソースを作成します。
まずオブジェクトを選択して「次へ」を押します

mr22.png

オブジェクトの一覧が表示されるので作成したUserオブジェクトが表示されるまで展開をしてUserオブジェクトにチェックをつけ「完了」します。

mr22.png

5.データセットプロパティ ダイアログでデータセットが作成されるのでOKを押します。

mr22.png

以降、レポート定義ではデータベースをデータソースとした時と同様に、データを用いてレポートを作成できます。

レポートの大きさを名刺サイズにする

レポートの大きさを9.1cm x 5.5cmの名刺サイズに変更します。

1.レポートデザイナで灰色の余白部分を右クリックして「レポートのプロパティ」を選択します。

mr22.png

2.ページ設定で用紙のサイズを「カスタム」にして「幅」9.1cm、「高さ」5.5cmとします。また、余白はすべて0cmとします。

mr22.png

3.レポートデザイナで本文を選択してプロパティのSizeで用紙の大きさを指定します。用紙と同じサイズだとはみ出て、改行してしまうので、2,3mm程度少なく設定します。

mr22.png

一覧データによる繰り返しデータの自由形式でのレイアウト

ここでは一覧データを用いて、繰り返しデータを一覧表という形式ではなく自由なレイアウトでデザインします。

1.レポートデザイナにツールボックスから「一覧」を選択してドロップします。この際、作成された一覧の大きさを本文いっぱいに広げてください。

mr22.png

2.名前を表示するテキストボックスを追加します。
データのアイコンをクリックして「Name」を選択してください。

mr22.png

表示するデータを指定したら、レイアウトを指定します。テキストボックスのプロパティを開いて配置で縦、横を[中央揃え]にします。

mr22.png

次にフォントを指定します。MS Pゴシック、サイズを20pt、太字にチェックをつけます。

mr22.png

3.Nameと同様に、Address、Tel、Birthに関してテキストボックスで表示します。Birthに関してはテキストボックスのプロパティの「数値」で書式を設定して、日付型の書式にします。

mr22.png

4.プロフィール画像を表示するために画像をドロップします。

mr22.png

ドロップをすると画像のプロパティが表示されるので、画像ソースの選択で「データベース」、次のフィールドを適用で「[ImageData]」を選択して、MIMEの種類は「image/png」にします。

mr22.png

ここでOKを押すことにより、画像が表示されるようになります。

mr22.png

アプリケーションへのREPORTVIEWERコントロールの追加

1.FormデザイナにおいてツールボックスのReportViewerコントロールをFormに張り付けます。

2.ReportViewerコントロールのプロパティの[Dock]を[Fill]に指定します。これでReportViewerコントロールはForm大きさに一致するようになります。

3.右上隅の三角形をクリックしてReportViewerタスクメニューを表示して、レポート選択コンボボックスで作成したレポートを選択します。
指定したレポートにデータを表示するためにUserBindSourceコントロールが追加されます。

mr22.png

4.Form_LoadイベントでRefreshReportを行う前に、UserBindSourceにデータを追加します。

private void Form1_Load(object sender, EventArgs e)
{
 Users users = new Users();
 UserBindingSource.DataSource = users.GetUsers();
 this.reportViewer1.RefreshReport();
}

5.この状態でビルドを実行して印刷プレビューを見ると以下のようになります。

mr22.png

このようにReportViewrを使用することで単票形式の帳票が容易に作成できることが確認できました。

プレビューを使用しないローカルレポートの印字

ReportViewerコントロールでプレビューを表示しないで直接印字するには、ReportViewコントロールのLocalReportを画像にExportしてそのデータをプリンタに送ります。
この方法は、以下に記述してあります。

チュートリアル : プレビューを使用しないローカル レポートの印刷
http://msdn.microsoft.com/ja-jp/library/ms252091%28v=vs.90%29.aspx

このチュートリアルを実行するまえに次のページのページを参照して、Report.rdlcファイルとData.xmlを作成してください。

印刷チュートリアルのサンプル データおよびレポート
http://msdn.microsoft.com/ja-jp/library/ms251734%28v=vs.90%29.aspx

レポートパラメータの使用方法

レポート定義ファイルでレポートパラメータを使用することで、実行時に任意の値を与えて、それをレポート中に表示することができます。

1.レポートのデザイナを選択中に「表示」メニューから「レポートデータ」を選択することで「レポートデータ」ウィンドウが表示されます。これはCTRL+ALT+Dで代替できます。

2.レポートデータウィンドウで「パラメーター」を選択して右クリックを押し、「パラメーターの追加」を押下します。

mr22.png

3.「レポート パラメーターのプロパティ」ダイアログで任意の名称を付与します。

mr22.png

4.以後、このレポート定義ファイルでは式に追加したパラメーターが使用できます。以下のように式ダイアログのカテゴリ「パラメーター」に今追加したパラメーターが増えているのを確認できます。

mr22.png

この例では帳票の左上にレポートパラメータを表示するテキストボックスを追加します。

mr22.png

5.レポートコントロールが表示される前に、レポートパラメータを以下のようにして与えます。

private void Form1_Load(object sender, EventArgs e)
{
 this.DataTable1TableAdapter.Fill(this.DataSet1.DataTable1);
 // レポートパラメータを作成
 ReportParameter param = new ReportParameter("ReportParameter1","レポート変数");
 reportViewer1.LocalReport.SetParameters(param);
 // レポート表示
 this.reportViewer1.RefreshReport();
}

6.実行をするとプログラムで与えた文字がレポートに表示されます。

mr22.png

カスタムコードの作成

カスタムコードを用いることで、独自の数式をReportViewer内で使用できます。カスタムコードの書き方は2種類存在しており、レポート定義ファイルにコードを組み込む方法と、グローバルアセンブリを使用する方法があります。

レポート定義ファイルにコードを組み込む

1.レポートデザイナ使用中に「レポート」メニューの「レポートのプロパティ」を選択して「レポートのプロパティ」を表示します。

2.コードを選択してカスタムコードに任意のコードを入力します。

mr22.png

このコードは指定した文字列中にBikeという文字があったら、Bicycleに変更します。

Public Function ChangeWord(ByVal s As String) As String
 Dim strBuilder As New System.Text.StringBuilder(s)
 If s.Contains("Bike") Then
 strBuilder.Replace("Bike", "Bicycle")
 Return strBuilder.ToString()
 Else : Return s
 End If
End Function

3.レポート定義にテキストボックスを追加して式に数式を入力します。

mr22.png

=Code.ChangeWord("TEST Bike da")

なお、インテリセンスは効きません

4.この状態でレポートを表示すると次のようになります。

mr22.png

グローバルキャッシュアセンブリを使用したカスタムコード

VB.NETなどでクラスモジュールを作成して、それをグローバルキャッシュアセンブリとして登録することで、ReportViewerはそのカスタムコードを利用できます。

.NET 4.0より前では以下のCustom Assembliesのやり方で作成できるはずです。
http://www.codeproject.com/Articles/38554/Microsoft-Reporting-Services-Part-II

.NET 4.0以降において、このやり方は通用しません。
以降、.NET4.0以降でカスタムアセンブリを追加する方法を紹介します。

厳密な名前を持つクラスライブラリを作成する

1.クラスライブラリを作成します。今回は「ReportViewerCustomAsmTest」という名称で作成します。

mr22.png

2.追加した「ReportViewerCustomAsmTest」のプロジェクトのメニューで「ReportViewerCustomAsmTestのプロパティ」を選択してください。

3.署名タブにて「アセンブリに署名する」をチェックして「厳密な名前のキーファイルを選択してください」で<新規作成>を選択します。

mr22.png

4.「厳密な名前キーの作成」ダイアログが表示されるのでキーファイル名とパスワードを入力してOKを押してください。

mr22.png

5.デフォルトで作成されたCustomCode.csを次のように置き換えます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ReportViewerCustomAsmTest
{
 public class CustomCode
 {
 public string CutText(string txt)
 {
 if (txt.Length >= 30)
 {
 return txt.Substring(0, 26) + "...";
 }
 else
 {
 return txt;
 }
 }
 public static String SharedCutText(string txt)
 {
 if(txt.Length >= 30 )
 {
 return txt.Substring(0,26) + "...";
 }
 else
 {
 return txt;
 }
 }
 }
}

5.AllowPartiallyTrustedCallers属性を指定します。
C#の場合は、AssemblyInfo.csに下記を追記してください。

[assembly: System.Security.AllowPartiallyTrustedCallers()]

もしVB.NETで作成していた場合、AssemblyInfo.vbはデフォルトで表示されていません。表示するにはソリューションエクスプローラーで「すべてのファイルを表示」を押してください。

mr22.png

これによりAssemblyInfo.vbが表示されるので、次を追記してください。

6.ビルドしてクラスライブラリを作成します

7.「開発者コマンドプロンプト for Visual Studio 2013」を管理者モードで起動します。
このツールはVisualStudioをインストールすると次のフォルダに作成されます。

C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\Shortcuts

8.コマンドプロンプト上で下記のコマンドを実行してグローバルキャッシュアセンブリとして登録します。

gacutil /i ReportViewerCustomAsmTest.dll

ただしく実行されると次のようなメニューが表示されます。

Microsoft (R) .NET Global Assembly Cache Utility. Version 4.0.30319.33440
Copyright (c) Microsoft Corporation. All rights reserved.

アセンブリが正しくキャッシュに追加されました

グローバルキャッシュアセンブリを削除するには?

アセンブリ名(ファイル名ではない)を指定して/uオプションをつけてgacutilを実行してください。

gacutil /u ReportViewerCustomAsmTest

グローバルキャッシュされたアセンブリをレポートで使用する方法

1.レポートデザイナを編集中に「レポート」メニューより「レポートのプロパティ」を選択します。

2.「レポートのプロパティ」ダイアログの参照タブを選択して、アセンブリを追加します。

mr22.png

[…]ボタンを押すと、「参照の追加」ダイアログが開くので作成したDLLを選択してOKボタンを押してください。

mr22.png

参照対象のアセンブリが追加されます。

mr22.png

3.スタティックなメソッドを使用するだけならこれでいいのですが、もし、インスタンスを作成する必要がある場合は、クラス名の追加をする必要があります。
今回は以下のような内容を追加します。

クラス名:ReportViewerCustomAsmTest.CustomCode
インスタンス名:myClass

mr22.png

4.レポート定義でテキストボックスに式を追加します。
スタティックなメソッドを利用する例:

=ReportViewerCustomAsmTest.CustomCode.SharedCutText("tttttttttttttttttttttttttttttttttttttttt")

インスタンスメソッドを利用する例:

=Code.myClass.CutText("dddddddddddddddddddddddddddasdfasdfadfd")

インスタンスを使う場合はプロパティで追加したインスタンス名を使用する必要があることがわかります。なお、やはりインテリセンスはききません。

5.この状態で実行することで、カスタムコードが実行されていることが確認できます。

mr22.png

改ページのTips

ここでは改ページに関するTipsを紹介します。

特定の行数毎ごとに改行をする

ここでは特定の行数毎に改行する方法を紹介します。

1.「チュートリアル:ReportViewerレポートの作成」と同じ手順でデータセットを作成します。

2.レポートデザイナでテーブルを追加して以下のような一覧を作成します。

mr22.png

3.行グループでグループを追加します。

mr22.png

グループ化をする際の条件にRowNumberを用いて行数でグループ化するようにします。この例では20行おきでグループかします。

mr22.png

=CEILING(RowNumber(Nothing)/20)

4.グループのプロパティで「並び替え」タブを選択して「式」を削除します。
グループ化の条件と並び替えの条件の式は先に入力したものと同じになります。しかし、並び替えにおいてRownumber数式は使用できないので、これを削除します。

mr22.png

5.グループ用の列が表示されるのでグループを残して列のみを削除します。

mr22.png

列を削除しようとすると「列の削除」ダイアログが表示されるので「列のみの削除」を選択します。

mr22.png

これにより、グループを残して列を削除できます。

mr22.png

6.グループごとに改ページを行うようにします。
グループ プロパティーを開いたのち、改ページのタブで「グループの各インスタンスの間」にチェックを付与します。

mr22.png

7.これにより、1ページあたり20行で改行されるようになります。

mr22.png

一覧形式で最後のページにおいて最後まで罫線を引く

特定の行数毎に改ページができることは説明しました。この方法でも、最後のページに大きな余白が作成されます。ここでは最後のページにおいても最後まで罫線を引く方法を説明します。

mr22.png

Excelの場合は、Excelオブジェクト経由で罫線の描画ができるので、プログラムでいかようにも調整できます。
ReportViewerの場合はどうしたらいいのでしょうか?
レイアウトで何かをするのは無理です。これはページがちょうどに終わるように、DataSetに格納するレコードの数を調整するしかありません。
まず、DataSetのデザインで使用するすべてのフィールドのプロパティでAllowDbNullをTrueにします。

mr22.png

あとは、ReportViewを表示するまえに、ダミーのデータを追加します。

 private void Form1_Load(object sender, EventArgs e)
 {
 // TODO: このコード行はデータを 'DataSet1.DataTable1' テーブルに読み込みます。必要に応じて移動、または削除をしてください。
 this.DataTable1TableAdapter.Fill(this.DataSet1.DataTable1);

 // 末尾にダミーデータ追加
 int addcnt = 20 - (DataSet1.DataTable1.Count % 20);
 for (int i = 0; i < addcnt; ++i)
 {
 DataRow r = DataSet1.DataTable1.NewRow();
 DataSet1.DataTable1.Rows.Add(r);
 }

 this.reportViewer1.RefreshReport();
 }

これで実行することで最後まで罫線が引かれていることがわかります。

mr22.png

ExcelとReportViewerの比較

帳票の出力方法として、Excelオブジェクトを操作する方法があります。この章ではExcelを用いる方法とReportViewerを使用する方法で、どのような差があるか比較してみます。

ReportViewerのメリット

ここではReportViewrを採用することで、Excelで帳票を作成する場合に比べてどのような利点が発生するかを説明します。

1.ReportViewerではユーザ環境にOFFICE製品が不要になる。
ReportViewerでは動作環境でOffice製品が不要になります。
Excelを使用する場合、動作環境にExcelが存在しなければなりません。
このことは動作する環境を選ぶことになります。特にパッケージソフトの場合、初めから、Office製品をもっていない層を販売対象から外すという選択が適切とは考えられません。
なるべく多くの環境で動作させる必要がある場合、ReportViewerの採用は大きなメリットがあります。

2.単票形式の帳票を作りやすい
Excelは表計算ソフトなので、当然、単票形式の帳票を作るのに向いていません。
シートをコピーするか、出力する際にレイアウトをプログラムで一々記述するか、1ページ出力するたびにExcelオブジェクトから作り直すか・・・いずれの方法も、開発の手間またはパフォーマンスに影響を与えます。

それにくらべて、チュートリアルを読んでいただけると、ReportViewerでは単票形式のデータを作成しやすいことがわかります。

ReportViewerのデメリット

ここでは逆にReportViewrにどのようなデメリットがあるか説明します。

1.Excelとまっとく同じにはならない。
ReportViewrはExcelにエクスポートする機能も有していますが、Excelとまったく同じものは作成できません。エンドユーザーがすでにExcelを駆使して業務をおこなっているシステムの場合、そのことが不満につながる場合があります。
Excelで出力を提供できるなら、むりにそれを変更するのは実際使用するエンドユーザーのメリットにはならないでしょう。

2.認知度の低さ
ReportViewrはMicrosoftが提供しているコントロールですが、意外と使われていません。
おそらく、検索しても大した情報を得ることはできないでしょう。
いくら優れた機能を提供していても認知度が低いといくつかのリスクが発生します。
A
1つはトラブルシュートの事例の少なさです。Excelなどは大量にユーザーがいて様々な問題が報告され、それの対策がいくらでも取得できます。しかし、導入事例が少ないと、そのような情報を入手することが困難になります。
事実、この資料を作成するときに、発生したいくつかのトラブルの解決方法は日本語ではヒットすらしません。

次に開発要員の確保のしづらさです。Excelを触った人間はいくらでもいますが、ReportViewerを触ったことのある人間はそんなに多くありません。
Excelで開発をおこなっている場合、いざとなったら、データを特定のシートにコピーするとこまでプログラマが開発して、実際のデザインなどはExcel使えるだけの人員にお願いするという開発体制がとれます。
ReportViewerの場合、その方法はとれません。どんな簡単なデザインの修正であれ、プログラミングの知識がないと無理でしょう。

付録

VS2008からSQLSERVER2012に接続する

VisualStudio2008+SQLSERVER2012の場合、接続しようとしただけで下記のエラーが発生します。

mr22.png

この場合は以下の点を確認してください。
・VisualStudio2008SP1が適用されており、かつ、すべての重要な更新プログラムが当たっていること。
・OLE DB用のデータプロバイダを使用する
OLEDB用のデータプロバイダはデータソースの変更で選択できます。

mr22.png

帳票のテスト方法についての考察

帳票のテストを行う場合、実際に紙に出力するのは、安定するまで待った方がいいでしょう。
不安定な状況でのテストは紙と時間の無駄になります。

幸いWindows Vista以降のOSにはXPS(XML Paper Specification)と呼ばれるドキュメントファイルがサポートされています。コントロールパネルのデバイスとプリンタを見ると、「Microsoft XPS Document Writer」と呼ばれるものがあります。

mr22.png

通常のプリンタに出力するかわりにXPS Document Writerに出力することにより、紙ではなくファイルとして保存できます。

動作が安定するまではこれを用いて動作確認をするといいでしょう。

また、ファイルに保存できるということは機械的に以前出力した結果と変わっていないか確認することができます。これは回帰テストを自動で行えるということを意味します。

印字のたびにファイル名を効かれるのを抑制するには、下記のプロパティに任意のファイル名を指定するといいでしょう。

System.Drawing.Printing.PrinterSettings

ただし、単純にXPSを比較した場合、まったく同じ出力結果であっても、作成日や作成者によるメタデータによって違う結果とみなされます。これを防ぐにはXPSを一旦、画像に変換して比較を行うとよいでしょう。

XpsToImg.zip
http://needtec.sakura.ne.jp/release/XpsToImg.zip
任意のXPSファイルをPNGファイルに変換するツールです。

当然、数式などで出力日、作成日を表示すると異なる結果になるので、レポートパラメータなどを用いて、テスト用に固定の文字を指定できるようにします。

一つ注意しなければならないのは、XPSで出力できるからといって紙で出力する試験をやらなくていいわけではありません。
出力するプリンタにより、思わぬ出力結果になることがあります。すべての帳票の改ページを含むデータで必ず1度は動作確認すべきです。

.NETにおける64ビットプロセスと32ビットプロセスについて

この記事では.NETにおける64ビットプロセスと32ビットプロセスについて説明をおこなう。

1プロセスにおける32ビットと64ビットの混在

ネィティブアプリケーションの場合、プラットフォームの異なるExeとDllは共存できません。

・x64のExeと x64のDLL ⇒動作する
・x86のExeと x86のDLL ⇒動作する
・x64のExeと x86のDLL ⇒動作しない
・x86のExeと x64のDLL ⇒動作しない

ネイティブアプリケーションはビルド時に、どちらにするか指定してビルドする必要があります。

.NET の場合、ビルド時にプラットフォームに"x86","x64"以外に"Any CPU"が選択できます。

Any CPUを選択した場合次のような挙動になります。

・Exeの場合、OSが32ビットの場合、32ビットのプロセスとして動作します。
・Exeの場合、OSが64ビットの場合、64ビットのプロセスとして動作します。
・DLLの場合、呼び出したExeが32ビットで動作している場合、32ビットで動作します。
・DLLの場合、呼び出したExeが64ビットで動作している場合、64ビットで動作します。

Any CPUでExeを作成した場合、64bitOSで32bitプロセスを動作させることは出来ません。

この問題を解決するために、.NET4.5以降では「32ビットを優先にする」というオプションを指定できます。
これはAny CPUではありますが、32bitで動作が可能な場合は32bitで動作します。

64bitOSでAnyCPUでコンパイルした32bitプロセスをビルドなしに動かす方法

CorFlags を用いてExeを修正することが可能です。
このコマンドはVisualStudio2008の場合は下記にあります。

C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0A\bin\NETFX 4.0 Tools\CorFlags.exe

VisualStdioをインストールしたときにプログラムファイルに登録されるされるコマンドプロンプトではパスが通っています。

使用例:

CorFlags /32BIT+ TestR.exe

VS2012では /32BITREQ+ フラグになります。

もしDLLを含む場合、そのDLLがx64でビルドされているようなら一緒に変更しないといけません。
AnyCPUでビルドされているDLLは、ExEのプラットフォームに合わせて動作するので不要です。

もし、強い名称で署名をされている場合は、/Force オプションを付与する必要があります。

CorFlags /32BIT+ /Force RDotNet.dll

プラットフォームの異なるプロセスの相互運用

1つのプロセスで32bitと64bitは混在して動作しないことは説明しました。

しかし、64ビット版のWindowsでは64bitプロセスと32ビットプロセス間のプロシージャコールはサポートされています。
つまり64ビットのアウトプロセスのCOMサーバーは32ビットのクライアントと通信でき、逆に32ビットのアウトプロセスのCOMサーバーは64ビットのクライアントと通信できます

.NETでCOMサーバーを作成するのは下記を参考にしてください。
How to develop an out-of-process COM component by using Visual C++, Visual C#, or Visual Basic .NET
http://support.microsoft.com/kb/977996

実際にCOMサーバーを作成するには、上記のページからCSExeCOMServerをダウンロードします。

プロジェクトのプロパティーを以下のいづれかに修正してください。

32ビットのアウトプロセスを作成する場合

「ビルド」タブのプラットフォームターゲット
x86

「ビルドイベント」タブの「ビルド後に実行するコマンドライン」
C:\Windows\Microsoft.NET\Framework\v2.0.50727\regasm.exe /tlb "$(TargetPath)"

64ビットのアウトプロセスを作成する場合

「ビルド」タブのプラットフォームターゲット
x64

「ビルドイベント」タブの「ビルド後に実行するコマンドライン」
C:\Windows\Microsoft.NET\Framework64\v2.0.50727\regasm.exe /tlb "$(TargetPath)"

実際にビルドしてCOMを登録すると、32ビットのCOMサーバー、または64ビットのCOMサーバーどちらであっても、32ビット、64ビットの両方のクライアントから使用できることがわかります。

詳しい詳細は、ダウンロードしたReadme.txtを参考にしてください。実際の作り方が記述してあります。

なお、アウトプロセスのCOMサーバーなので以下のようにスタティックな変数を使用すれば、異なるプラットフォームのプロセス間でのデータの共有が可能です。

    public class CSSimpleObject : ReferenceCountedObject, ICSSimpleObject
    {

        private static int _staticVal = 0;
        private static Object thisLock = new Object();
       public int StaticValue
        {
            get 
            { 
                return _staticVal; 
            }
            set
            {
                lock(thisLock)
                {
                    _staticVal = value;
                }
            }
        }

WindowsではこのようにアウトプロセスのCOMサーバーを利用することにより、異なるプラットフォームであっても、労力をかけずに連携できると思います。

アウトプロセスのCOMではプラットフォームターゲットでAnyCPUを使用してはいけない。

COMオブジェクト作成中にアプリケーションがハングします。
PEヘッダ中のIMAGE_NT_HEADERS.FileHeader.Machineに IMAGE_FILE_MACHINE_I386がセットされているためです。

64ビットOSの場合、COMとしては32ビットとして動作することを期待していますが、実際64bitとして動作してしまうのです。

この値はVisualStdioに付属しているbumpbinを用いて確認できます。以下のようなコマンドを実行してください。

dumpbin CSExeCOMServer.exe /headers

この結果を見てみましょう

x86を明示した場合:

File Type: EXECUTABLE IMAGE

FILE HEADER VALUES
             14C machine (x86)
               3 number of sections

x64を明示した場合

File Type: EXECUTABLE IMAGE

FILE HEADER VALUES
            8664 machine (x64)
               2 number of sections

Any Cpu

File Type: EXECUTABLE IMAGE

FILE HEADER VALUES
             14C machine (x86)
               3 number of sections

Any Cpuのヘッダにはx86と記述されていますが、実際、.NETとしては64ビットで動作するために不整合が出てしまいます。
アウトプロセスのCOMを作成する場合は、プラットフォームは絶対に指定してビルドをしなければなりません。

応用例

32ビットプロセスと64ビットプロセスはCOMを用いれば連携して動作させることはレガシーなコードの1部をCOMとして切り出して、.NETで置き換えるという手法がとれます。

たとえばVisualBasic6.0のプロセスは64bitプロセスでは動作しません。しかし、大量にメモリを使用する機能を追加するにはどうするか考えましょう。

一つの回答として、メモリを大量に消費する箇所のみCOMサーバーとして切り出すという選択肢がとれます。

Excel VBAからDoxygenを用いてドキュメントを出力する

概要

この記事ではExcelVBAのソースコードからDoxygenを用いてドキュメントを出力する方法について記述する。

前提知識

Doxygen とは?

Doxygenはソースコードからドキュメントを生成することができる。
http://www.doxygen.jp/

デフォルトではC++、C、Java、Objective-C、Python、IDL (Corba、Microsoft 風)、Fortran、VHDL、PHP、C#などが対象である。

また、INPUT_FILTERを用いることで、上記以外のプログラミング言語のドキュメントを生成することが可能だ。

VB6用のINPUT_FILTER

VBAはVB6と仕様がほぼ同じなので、VB6用のINPUT_FILTERを用いることで、ドキュメントの作成が可能である。

InputFilterには、いくつかの種類が存在している。

・だらろぐ 「vbfilter.pyを改造してみた」
http://r-satsuki.air-nifty.com/blog/2008/02/vbfilter_61f1.html

・VbDoxygen
https://code.google.com/p/doxy-filter/wiki/VbDoxygen

上記の例は、いずれも、Pythonのコードになっており、動作させるにはPythonをインストールする必要がある。
前者の方が解析の精度はよい。

InputFilterの使用方法

ExcelVBAのコードを何らかの手段でエクスポートする。
そのフォルダに対してDoxygenを実行することにより、ドキュメントを出力することができる。
InputFilterを用いるには次の設定が必要である。

Project:

項目名 設定値
EXTENSION_MAPPING frm=c
cls=c
bas=c

※Doxygen1.8.8以降はこのオプションがないと、クラスを認識しません。

Input:

項目名 設定値
INPUT_ENCORDING CP932
FILE_PATTERNS *.bas
*.cls
*.frm
INPUT_FILTER フルパスで指定すること
python C:\work\code\VBA\Report\doc\vbfilter.py

制限事項

VBFilterはVBのコードをC++のコードに変換してDoxygenに渡している。
この時、関数の中までは、変換していないので、本来Doxygenで作成される関数のコールグラフなどは作成できない。
つまり、使われていない関数の抽出などには使用できない。

改善案

上記の方法でもExcelVBAのコードをDoxygenのドキュメントとして出力することができる。

しかし、以下の問題がある。
・VbFilterを動作させるのにPythonをインストールせねばならない。
・ExcelVBAからファイルの出力せねばならない。

PythonのコードをExe化する

Pythonのコードはpy2exeを用いる事でexeに変換することができる。
これにより、Pythonをインストールしていない端末でもVbFilterを使用できる。

Python-izm exe変換 (py2exe)
http://www.python-izm.com/contents/external/exe_conversion.shtml

ExcelVBAからファイルの出力

VBSを用いてExcelからform,cls,basファイルを出力する方法を説明する

  1. 「VBAプロジェクトオブジェクトモデルへのアクセスを信頼する」をONにする。
    (1)開発タブを選択して「マクロのセキュリティ」をクリックする
    (2)「VBAプロジェクトオブジェクトモデルへのアクセスを信頼する」をONにする。
  2. 下記のようなVBS使用することでExcel中のソースコードを抽出できる。
Option Explicit
Dim xl
Set xl = CreateObject("Excel.Application")
ExportExcelVBA "C:\\Users\\test\\Desktop\\VbaDoxygen\\Sample.xlsm", "C:\\Users\\test\\Desktop\\VbaDoxygen\\output\\src"

'* ExcelからVBAのコードを抽出する
'* @param[in] fileSrc Excelファイルのパス
'* @param[in] dirDst  ソースコードを出力する先
'*
Private Sub ExportExcelVBA(Byval fileSrc, Byval dirDst)
    Dim fso         ' FileSystemObject
    Dim fo          ' 出力ファイル
    Dim xl          ' Excelオブジェクト
    Dim wbk         ' ワークブック
    Dim cmp         ' VBProject.VBComponents

    Dim sFormat     ' 拡張子
    Dim fileDst     ' 保存先のファイル

    Set fso = CreateObject("Scripting.FileSystemObject")
    Set xl = CreateObject("Excel.Application")
    Set wbk = xl.Workbooks.Open(fileSrc)

    xl.DisplayAlerts = False

    On Error Resume Next

    For Each cmp In wbk.VBProject.VBComponents

        Select Case cmp.Type
            Case 1 
                sFormat = "bas"
            Case 2
                sFormat = "cls"
            case 100
                sFormat = "cls"
            Case 3
                sFormat = "frm"
            Case Else
                sFormat = "unkwon" & cmp.Type
        End Select
        If sFormat <> "" Then
            fileDst = dirDst + "\" + cmp.Name + "." + sFormat
            Set fo = fso.CreateTextFile(fileDst, True)

            If cmp.CodeModule.CountOfLines > 0 Then
                fo.WriteLine "Attribute VB_Name = """ & cmp.Name & """"
                fo.WriteLine cmp.CodeModule.Lines(1, cmp.CodeModule.CountOfLines)
            End If
            fo.WriteLine ""
            fo.Close
        End If
    Next

    wbk.Close
    Set wbk = Nothing
    xl.Quit
    Set xl = Nothing
    Set fo = Nothing
    Set fso = Nothing
End Sub

もし64bitの端末で32bitのExcelを操作する場合は次のように実行する。

C:\Windows\SysWOW64\CScript.exe test.vbs test.vbs

それ以外は、下記のようにして実行するとよい。

CScript test.vbs

ExcelVBAのDoxygen出力を容易にする方法

b0232065_3103513.png

上記の図のように、VBSを用いてCLSファイル、BASファイルなどを抽出する。
そして、Doxygenの設定ファイルのテンプレートと、VBFilterのExe化したものを使用してDoxygenを実行する。

これを容易にできるようにしたスクリプトは下記からダウンロードできる。
https://github.com/mima3/VbaDoxygen

ファイルの説明:

ファイル名 説明
Sample.xlsm テスト出力用のエクセルファイル
python27.dll VBFilter.exeを動かすのに必要
w9xpopen.exe VBFilter.exeを動かすのに必要
vbfilter.exe VBFilter.pyをpy2exeでExe化したもの
doxyfile_template Doxygenの設定ファイルのテンプレート。必要に応じて修正すること
ExcelVBADoxygen.vbs Excelからファイルを抽出してDoxygenを実行するスクリプト

次のように実行すればよい。

C:\Windows\SysWOW64\CScript.exe ExcelVBADoxygen.vbs "C:\dev\VbaDoxygen\Sample.xlsm" "C:\dev\VbaDoxygen\output" "C:\Program Files\doxygen\bin\doxygen.exe"

引数の説明:

第一引数:Excelのパス
第二引数:出力フォルダ
第三引数:doxygen.exeへのフルパス

Node.jsの開発時に必要な情報

この記事では、Node.jsの開発時にデバッグ~継続的インテグレーションが行えるようにするために必要な情報について記述する。

Node.jsのインストール方法

http://nodejs.org/

ここから、インストーラなりソースなり取得してインストールする。
Windows,Macはインストーラが存在しており、debianとかはソースから作成する。
Windowsの場合、VS2008や、VS2012などの複数のVisualStudioが混在している環境だとnpm installが失敗することがある。
その場合は、環境変数を調整してVS2010が動作するようにしておくこと。

Node.jsのデバッグ方法

node-inspectorによるデバッグ

Node.jsはnode-inspectorとChromeを用いることでデバッグが可能である。

https://github.com/node-inspector/node-inspector

node-inspectorのインストール方法

下記のコマンドを実行する。

npm install -g node-inspector

node-inspectorによるデバッグ方法

次のような手順となる。

  1. デバッグ対象のアプリケーションの起動
  2. node-inspectorの起動
  3. Chromeによってnode-inspectorにアクセスしてデバッグを行う。

デバッグ対象のアプリケーションの起動方法

>node --debug server.js
debugger listening on port 5858

--debugフラグを使用して対象のアプリケーションを起動すると、デバッグポートが表示される。
もしデバッグポートを変更したい場合は下記のようにする

node --debug=5859 app.js

もし、一行目でブレークを賭けたい場合は次の通り

node --debug-brk=5859 app.js

node-inspectorの起動

デバッグ対象のアプリケーションを起動したらnode-inspectorを起動する。
これにより、ブラウザ経由でデバッグが可能になる。

>node-inspector --web-port 8081 --debug-port 5859

--debug-portはデバッグ対象のプロセスのポート
--web-portはブラウザでデバッグするために使用するポートである。

Chromeによるデバッグ

chromeでnode-inspectorを実行時に支持されたURLにアクセスするとデバッグが可能になる。
先の例だと、以下のようなURLになる。

http://ホスト名:8081/debug?port=5859

b0232065_17150540.png

Console.logによるオブジェクトの出力

Console.logを用いることでオブジェクトをコンソールに出力することができるが、この際、一部の内容しか表示されない。

この場合はutilモジュールを用いることですべての内容を表示させることができる。

var util = require('util');
console.log(util.inspect(obj,false,null));

Node.jsのメモリー使用状況の調査方法

メモリ使用状況を調べる

Node.jsにおいて、どのオブジェクトが、どの処理でメモリを確保したかなどの情報を調べるにはnode-webkit-agentを使用する。

https://github.com/c4milo/node-webkit-agent

(1) node-webkit-agentのインストール

npm install webkit-devtools-agent

(2)下記の命令を任意のソースに記述。

var agent = require('webkit-devtools-agent');

(3)アプリケーションを起動

(4)以下のコマンドを実行

kill -SIGUSR2 <the process id of your nodejs app>

(5)その後、Chromeで以下のURLにアクセスする
http://c4milo.github.io/node-webkit-agent/26.0.1410.65/inspector.html?host=デバッグ対象のホスト名:9999&page=0

その後は、Profileにてメモリのスナップショットを取得すればよい。

b0232065_17515347.png

メモリーの増加を調べる方法

特定の処理において、メモリーが異常に増加していないか調べる方法について説明する。

Node.jsは世代別のガベージコレクションなので、解放したつもりでもメモリが残っている場合がある。そのときは、任意のタイミングでガベージコレクトを行ったあと、メモリのスナップショットを確認する。

ガベージコレクト後も残っているメモリーは確実に、現在使用しているものとなる。

ガベージコレクションを確実に走らせたい場合は以下のような処理が必要である。
まず、プログラム中でガベージクレクタを実行したい箇所に次のコードを埋め込む。

if(global.gc) {
  global.gc();
}

そして、アプリを起動する際に--expose_gcを指定する

node --expose_gc server.js

静的解析

実際にプログラムを実行しないで、リスクのある記述方法を検知することができる。
これらの処理はJenkinsなどで定期的に実行するとよい。

gjslintによる静的解析

gjslintは指定のコードがGoogle JavaScript Style Guideに違反していないか調べるツールである。

gjslintのインストール方法

gjslintはPythonで動作するので、Pythonがインストールされていることが前提になる。

(1)easy_installを使用できるようにする。

wget http://peak.telecommunity.com/dist/ez_setup.py
python ez_setup.py

(2)gjslintをインストールする

easy_install http://closure-linter.googlecode.com/files/closure_linter-latest.tar.gz 

gjslintの使用方法

使用例:

gjslint --disable 110,1 -r src

--disable コンマ区切りで無視するエラーを指定できる。
-r ディレクトリを再帰的に操作できる。

closure-linter-wrapperによるXML出力

gslintのみでは検査結果をXMLとして保存することはできない。XMLとして出力できないと、jenkinsによる定期ビルドに組み込むことができない。
これを行うためにはclosure-linter-wrapperが必要である。
https://github.com/jmendiara/node-closure-linter-wrapper

(1)closure-linter-wrapperのインストール

npm install -g closure-linter-wrapper

(2)次のようなtest.jsファイルを作成する。

var argv = process.argv;
var gjslint = require('closure-linter-wrapper').gjslint;
var flagsArray = [
  '--nostrict',
  '--nojsdoc',
  '--disable 14'
];
console.log(argv[2] + 'src/*.js');

gjslint({
    src: [
      argv[2] + 'src/*.js'
    ],
    flags: flagsArray
    ,reporter: {
      name: 'gjslint_xml',
      dest: argv[2] + 'gjslint.xml'
    }
  },
  function (err, result) {
  }
);

この例では引数でしたフォルダのsrc以下を解析してgjslint.xmlに出力する。
あまりに大量のデータを一気にgjslint関数に渡すと正常にXMLが作成されないので注意すること。

Jenkinsによるgjslintの集計

Jenkinsによりgjslintを定期的に実行し、その結果を集計することができる。

(1) jslintの結果を集計できるようにViolationsを入手する
Jenkinsの管理 > プラグインマネージャで利用可能タブから「Violations」を指定してインストールする。

(2) Jenkinsでシェルスクリプトを実行するようにして先に作成したclosure-linter-wrapperを使用するスクリプトを実行する

NODE_PATH=/usr/local/lib/node_modules
export NODE_PATH
node ${WORKSPACE}/test.js ${WORKSPACE}/

(3)ビルド後の処理のjslintに出力ファイルを指定しておく。

b0232065_21404268.png

(4)ビルドを行うと次のようなレポートが作成される。

b0232065_21425396.png
b0232065_21430497.png

platoによるコードメトリックスの収集

platoを用いることで、ソースコードの行数や複雑度を計測できる。
https://github.com/es-analysis/plato

インストール方法:

npm install -g plato
plato -r -d report ./src

これによりreportフォルダ以下にHTMLが作成される。

b0232065_18012551.png
b0232065_18040282.png

Complexityが高いものがバグを発生しやすいので、テストケースの数や、リファクタリングの目安として監視する。
これもjenkinsで日々作成するといい。

テストの自動化

ここではテストの自動化について記述する。
テストコードについて、JavaScriptで記述でき、学習コストが低くなる方法について述べている。

jasmine-nodeによるサーバーサイドの単体テスト

サーバサイドの単体テストはjasmine-nodeで記述するといい。
https://github.com/mhevery/jasmine-node

--junitreportと--outputfolderオプションを用いることでjunitの形式でXMLを出力できる。
これによりjenkinsでの集計が可能になる

jasmine-nodeのXML出力で日本語のファイル名にならない場合

jasmine-nodeは--junitreportを使用することでXML形式としてテスト結果を出力できる。
しかし、jasmien-nodeが使用しているjasmine-reportersの不具合で日本語が正常に動作しない。

以下のようなファイルが存在するとする。

describe("test\\足し算の確認", function() {
  beforeEach(function() {
    // テスト前処理
  });

  afterEach(function() {
    // テスト後処理
  });

  it("足し算が正しい", function() {
    expect(addition(1, 2)).toEqual(3);
  });
});

この場合、期待の出力結果としては、記号が除去されたファイル名「TEST-test足し算の確認.xml」が作成されることが期待される。
しかし、現状は「TEST-test.xml」となる。

これを修正するパッチは以下のとおりである。

jasmine-nodeのバージョン:1.14.3
修正パッチ:
http://needtec.sakura.ne.jp/release/jasmine.junit_reporter.js.patch

パッチ適用のコマンド例:

patch -u ../node_modules/jasmine-node/node_modules/jasmine-reporters/src/jasmine.junit_reporter.js < jasmine.junit_reporter.js.patch

jasmine1.3とjs-test-driverによるブラウザサイドの自動化

ブラウザサイドのテストは同じテストを異なるブラウザで確認しないといけないので、工夫が必要である。

これはjs-test-driverとjasmine1.3とjasmine-jstd-adapterを組み合わせることで実現可能だ。

WebブラウザでJavaScriptをテストする「js-test-driver」とQUnit、Jasmineを連携してテストするには (4/4)

jasmineは現在2.0も存在するが、js-test-driverと組み合わせて使うには1.3である必要がある。
これにより、サーバーサイドと同じテストコードの書き方でクライアントも記述できる。

seleniumによるUIテストの自動化

seleniumを用いることで、UIのテストが自動化できる。
seleniu-webdriverを用いればNode.jsでテストを記述することが可能である。

seleniumを使ってJavaScriptのみでWebUIを自動操作する

ただし、これは導入コストと維持コストが極めて高いので、ここぞという時だけに絞ること。

Trac0.12系へのニコニコカレンダーの移植

目的

ニコニコカレンダーとはチームのチームのモチベーションやムードを可視化したものです。

http://www.geocities.jp/nikonikocalendar/index_ja.html

この記事ではTracのプラグインとしてニコニコカレンダーを使用できるようにします。

b0232065_12251021.png

このプラグインはBrett Smith氏がTrac0.10系で作成したプラグインをTrac0.12系へ移植して若干の修正を行ったものです。

Niko-Niko Calendar
http://trac-hacks.org/wiki/NikoNikoPlugin

導入方法

前提

Trac-Lightningの3.2.0により、Trac0.12がインストールされているものとします。
http://sourceforge.jp/projects/traclight/

インストール方法

(1)下記よりダウンロードを行います。
https://github.com/mima3/nikonikoplugin

(2)Zipを解凍すると「0.10」と「0.12」が存在します。
今回はTrac0.12が対象なので「0.12」フォルダのsetup.pyを使用します。

(3)TracLightningをインストールしたときに作成されたスタートメニューからコマンドプロンプトを起動してください。

b0232065_12341098.png

このメニューから起動された場合、Tracで使用している環境へのパスが通っています。
Pythonを複数いれている方は必ず、ここから実行しましょう。

(4)2で解凍したフォルダにカレントディレクトリを移動させて、下記のコマンドを実行して、インストールを行います。

python setup.py install

(5)Apachを再起動して、Tracの管理者権限でログインします。
「一般設定」の「プラグイン」を選択して「nikoniko」を探してください。

b0232065_1240533.png

nikonikoプラグインをみつけたらNikoNikoComponentにチェックをつけて変更を適用してください。

b0232065_12422987.png

適用後、ニコニコカレンダーのページに移動するとエラーがでる可能性があります。
これはニコニコカレンダーで使用しているテーブルが作成されていないために発生します。

(6)テーブルを作成するために、一旦、Apachを停止してください。
その後、3で使用したコマンドプロンプトを起動してTrac-Adminを用いてデータベースのアップグレードをおこないます。
下記のコマンドを入力してください。

Trac-Admin "プロジェクトのFullPath" upgrade 

TracLightningを既定の値でインストールした場合、Sampleプロジェクトのフルパスは「C:\TracLight\projects\trac\SampleProject」になっていると思います。

(7)Apachを再起動して管理者権限でログインします。
 管理メニューの権限で「NIKONIKO_CHANGE」と「NIKONIKO_VIEW」を任意のユーザーに付与してください。

b0232065_12505856.png

(8)するとメニューの右はじに「二コカレ」というメニューが作成されます。
あとは今日のきもちにあわせて「やる夫」を選んで必要なら、なんかコメントをのこしてください。

オリジナルとの違い

・Trac0.10からTrac0.11に移行するさいにテンプレートエンジンがGenshiに変わりました。そのため、テンプレートの周りを修正しました。

・本来ニコニコカレンダーは3つから選択しますが、つい5つにしてしまいました。

・コメントを残せるようにしました。これはマウスでオーバーすることで表示されます。愚痴をかくもよし、笑いをとるもよし。

TracPluginをつくる場合のメモ

以下は、TracPluginを改造した時にとったメモです。

・致命的なエラーはTracがログとして出力する。これは、プロジェクトのlogフォルダにtrac.logという名前で作成される。
ログの出力レベルはtrac.iniで指定できる。

・NikoNikoComponentなどのプラグイン中でログを任意のタイミングで出力できる。

from trac.log import logger_factory

# class NikoNikoComponent(Component)中で・・・
self.log.info("days %s %s", locale.getlocale(),days[0])

・プラグインのページのURLをテンプレートから利用するのには癖がある
まず、web_ui.pyで次のようなメソッドを用意しておく

    def get_htdocs_dirs(self):
        """
        Return a list of directories with static resources (such as style
        sheets, images, etc.)

        Each item in the list must be a (prefix, abspath) tuple. The
        prefix part defines the path in the URL that requests to these
        resources are prefixed with.

        The abspath is the absolute path to the directory containing the
        resources on the local file system.
        """
        from pkg_resources import resource_filename
        return [('nn', resource_filename(__name__, 'htdocs'))

このreturn時に指定している'nn'という記号を使うことになる。
たとえば、テンプレートファイル上で現在のプロジェクトのニコニコプラグインがインストールした画像を使用したい場合、以下のような記述になる

<img src="${href.chrome('nn/images/worstMood.png')}" alt="Worst" title="最悪・・・"/></a>

「nn」という記号をもとに実際のパスをつくっている。

・genshiでテンプレートを書く場合、きちんと書かないとだめだ。たとえば「<」と書くところを「&lt」と記述しても、多くのブラウザで正常にうごくが、genshiのテンプレートではエラーになる。

・ディレクトリを値とするディレクトリをテンプレートで使用する場合、注意が必要だ。
たとえば次のような判定が必要な場合があるとする

 <img py:if = "mood_data[user][day] == 'worstMood'" src="${href.chrome('nn/images/worstMood.png')}" alt="最悪・・・"/>

userまたは、dayのキーがない場合、エラーとなり落ちる。
以下のように、ディレクトリを値とするディレクトリの場合、それぞれで存在チェックをすること。

                <py:if test="mood_data[user]">
                  <py:if test="mood_data[user][day]">
                    <img py:if = "mood_data[user][day] == 'worstMood'" src="${href.chrome('nn/images/worstMood.png')}" alt="最悪・・・"/>
                  </py:if>
                </py:if>

・Windows固有の問題だとおもうが、strftimeを使ってロケールにあった日付を使用すると正常に動作しないことがある。これは、帰ってくる文字がCP932であり、UTF8が前提としているTracと相性がわるい。
今回はgetlocaleでロケールを確認して必要ならばUTF8に戻す処理をいれている

    def getWeekStr(self, d):
      ret = d.strftime("%A")
      l,c = locale.getlocale()
      if(c=="932"):
        return ret.decode('cp932').encode('utf-8')
      else:
        return ret

Sequelizeを使用してデータベースを操作するための基本的な情報

Sequelizeとは

SequelizeはMYSQL,MariaDB,SQLite,Postgresに簡単にアクセスするためのNode.jsのライブラリである。

http://sequelizejs.com

以下の機能を有している。
・オブジェクトとDBの関連を取り持ってくれる。これは1テーブルだけの関係ではなく、複数のテーブルの関連を定義することができる。
・入力されたデータが適切かどうかのバリデーションチェックを行う。
・トランザクションのサポートしている。 
・マイグレーションの機能をサポートしている。これにより、データベースのスキーマの更新が容易になる。
・1.7.8ではロックの機能は有していないので、自前でSELECT FOR UPDATEなどをしなければならない。しかし、開発中の2.0.0-devにはロックの機能をサポートしている。

導入方法

SQLiteを操作する場合

npm install --save sequelize
npm install --save sqlite3

Postgresを操作する場合

npm install --save sequelize
npm install --save pg

その他DBについては下記を参照
http://sequelizejs.com/articles/getting-started

実装のサンプル

単純なSQL文の実行

SQLiteに接続し単純なSQLを発行する例を以下に示す

var Sequelize = require('sequelize');
var sequelize = new Sequelize('sample','','',{dialect:'sqlite',storage:'./sample.db'});

sequelize.query('select * from test',null,{raw:true}).success(function(rows) {
  console.log(rows);
});

モデルを使った例:

defineメソッドでオブジェクトの構造を定義できる。
定義したオブジェクトを経由して、追加、更新、削除が可能だ。

以下の例ではUserモデルを作成し、データを追加後、参照している。

var Sequelize = require('sequelize');
var sequelize = new Sequelize('sample','','',{dialect:'sqlite',storage:'./sample_development.db'});

var User = sequelize.define('User', {
  username: Sequelize.STRING,
  password: Sequelize.STRING
});

sequelize
  .sync({force: true}) // trueだとテーブルを再構築する
  .complete(function(err) {
    if (err) {
      console.log('An error occurred while creating the table:', err);
    } else {
      console.log('It worked');
      // モデル名.buildでインスタンスを新しく作成
      var user = User.build({
        username: 'username',
        password: 'pass'
      });
      // 作成したインスタンスのsaveで保存をする
      user
        .save()
        .complete(function(err) {
          // findまたはfindAllでデータを取得
          User.findAll({where:['id>?',0]}).success(function(result) {
            console.log(result);
          });
        }
      );
    }
  }
);

バリデーションによる値チェックの例:

バリデーションにより、各フィールドに対する入力制限を指定できる。
これは、フィールド毎だけでなく、パスワードとユーザ名が同じだったらエラーとするというような柔軟な入力規制も記述できる。
また、getter,setterを記述することで、必ず小文字に変換するなどという処理が容易に実装可能だ。

その他詳細は下記を参考のこと。
http://sequelizejs.com/docs/1.7.8/models#validations

var Sequelize = require('sequelize');
var sequelize = new Sequelize('sample','','',{dialect:'sqlite',storage:'./sample_development.db'});
var util = require('util');

var User = sequelize.define('User', {
  username: {
    type     : Sequelize.STRING,
    allowNull: false,
    get      : function()  {
      // getするときにデータを補正できる
      return this.getDataValue('username').toString();
    },
    set      : function(v) {
      // setするときにデータを補正できる この例だと常時小文字で保存
      return this.setDataValue('username', v.toString().toLowerCase());
    },
    validate: {
      // not の場合 @ を含む場合エラーとなる。配列の値はRegExpに渡す値となる。
      // not以外の演算子については下記参照
      // http://sequelizejs.com/docs/latest/models#validations
      not: ['@','i']
    }
  },
  password: Sequelize.STRING
},{
  validate: {
    sameValue: function() {
      // パスワードとユーザ名が両方がひとしければエラーとする
      console.log(this.password);
      console.log(this.username);
      if (this.password == this.username) {
        throw new Error ('sameValue');
      }
    }
  }
});

sequelize
  .sync({force: true}) // trueだとテーブルを再構築する
  .complete(function(err) {
    if (err) {
      console.log('An error occurred while creating the table:', err);
    } else {
      console.log('It worked');
      // bulkCreateを使用すると一度についかできる
      // その他、一気に操作できるものとして、update,destroyなどがある
      // http://sequelizejs.com/docs/latest/instances#bulk
      User.bulkCreate([
        {username: 'ABC1', password:'xxx1'},
        {username: 'abc2', password:'xxx2'},
        {username: 'abc3', password:'xxx3'},
        {username: 'a@c4', password:'xxx4'}, // @が混ざっている
        {username: 'abc5', password:'abc5'}, // パスワードとユーザ名が等しい
      ],null, {validate:true}).success(function() {
        console.log('OK');
      }).error(function(err) {
        // この例だとa@c4とabc5がエラーになる
        console.log(util.inspect(err, true, null));
      });
    }
  }
);

テーブルの関連付けとEager loadingの例:

外部キーなどで接続してあるテーブルを取得する例を示す。
http://sequelizejs.com/docs/latest/models#eager-loading

以下のように記述することで、User,Task,TaskGroupの3テーブルをJOINして取得してくれる。

var Sequelize = require('sequelize');
var sequelize = new Sequelize('sample','','',{dialect:'sqlite',storage:'./sample_development.db'});
var async = require('async');

var User = sequelize.define('User', { name: Sequelize.STRING })
  , Task = sequelize.define('Task', { name: Sequelize.STRING })
  , TaskGroup = sequelize.define('TaskGroup', { name: Sequelize.STRING });

Task.belongsTo(User);
User.hasMany(Task);

Task.belongsTo(TaskGroup);
TaskGroup.hasMany(Task);

var tasks = [];

// DBの構築
tasks.push(function(next) {
  sequelize.sync({force: true}).done(function() {
    next(null);
  });
});

// ユーザの追加
tasks.push(function(next) {
  User.bulkCreate([
    {name: 'user1'},
    {name: 'user2'}
  ],null, {validate:true}).success(function() {
    next(null);
  }).error(function(err) {
    next(err);
  });
});

// TaskGroupの追加
tasks.push(function(next) {
  TaskGroup.bulkCreate([
    {name: 'gp1'},
    {name: 'gp2'},
    {name: 'gp3'}
  ],null, {validate:true}).success(function() {
    next(null);
  }).error(function(err) {
    next(err);
  });
});

// Taskの追加
tasks.push(function(next) {
  Task.bulkCreate([
    {name: 'task1', UserId:1, TaskGroupId:1},
    {name: 'task2', UserId:1, TaskGroupId:2},
    {name: 'task3', UserId:1, TaskGroupId:3},
    {name: 'task4', UserId:2, TaskGroupId:1}
  ],null, {validate:true}).success(function() {
    next(null);
  }).error(function(err) {
    next(err);
  });
});
async.series(tasks, function(err, results) {
  if (err) {
    console.log(err);
    return;
  }
  User.findAll({ include: [
    {
      model:Task,
      as: 'Tasks',
      attributes: ['id','name'],
      include: [TaskGroup]
    }]}).success(function(users) {
    console.log('User------------------------------------');
    console.log(JSON.stringify(users, null, '  '));
  });
});

Sequelizeでキーを組み合わせてユニーク制約を使う場合

Sequelizeでキーを組み合わせてユニーク制約を使う場合、次のようにする。

uniqueプロパティはboolean型ならば、その列にユニーク制約を与える。
文字列の場合は、同じ文字列の列と組み合わせてユニーク制約となる。

もし、別のテーブルと関連づけがあり、自動で生成される列の場合でも、制約をつけたい場合は、明示的に列を指定しなければならない。
明示的に列を作成した場合は、hasManyなどを行う場合、asでどの列が外部キーであるか指定する必要がある。

module.exports = function(sequelize, DataTypes) {
  var RootPath = sequelize.define('RootPath', {
    path: {
      type: DataTypes.STRING,
      unique: 'projectPathIndex'
    }, 
    ProjectId: {
      type: DataTypes.INTEGER,
      unique: 'projectPathIndex',
      allowNull: false
    }
  }, {
    classMethods: {
      associate: function(models) {
        RootPath.hasMany(models.Project, {as:'ProjectId'});
      }
    }
  });

  return RootPath;
};

トランザクションの例:

Sequelizeではトランザクションを使用することができる。
http://sequelizejs.com/docs/latest/transactions

以下の例では2つのトランザクションが同時に動いていることが確認できる。

var Sequelize = require('sequelize');
var sequelize = new Sequelize('testdb',
                              'postgres',
                              'postgres',
                              {
                                pool: { maxConnections: 10, maxIdleTime: 10000},
                                host:'127.0.0.1',
                                port:5432,
                                dialect:'postgres'
                              });
var async = require('async');

var User = sequelize.define('User', {
  username: Sequelize.STRING,
  password: Sequelize.STRING
});

var tasks = [];

for(var i = 0 ; i < 1; ++i) {
  tasks.push(function(next) {
    doTransaction(10, next);
  });
  tasks.push(function(next) {
    doTransaction(15, next);
  });
}

async.parallel(tasks, function(err, result) {
  User.count().success(function(max) {
    console.log(max);
  }).error(function(error) {
      console.log(error);
  });
});

function doTransaction(cnt, callback) {
  var tasks = [];
  var trn = null;

  tasks.push(function(next) {
    sequelize.transaction(function(t) {
    console.log(t.constructor );
    console.log(t.constructor.LOCK );
      trn = t;
      next(null);
    }).error(function(error) {
      console.log('---------------------------------');
      console.log(error);
      next(error);
    });
  });
  tasks.push(function(next) {
    User.findAll({where: ['id=?',1]}, {transaction:trn}).success(function(data) {
      console.log(data);
      next(null);
    }).error(function(error) {
      console.log(error);
      next(error);
    });
  });
  tasks.push(function(next) {
    User.count({transaction:trn}).success(function(max) {
      console.log(max);
      next(null);
    }).error(function(error) {
      console.log(error);
      next(error);
    });
  });

  for(var i = 0; i < cnt;++i) {
    tasks.push(function(next) {
      User.create({username:'foo'},{transaction:trn}).complete(function(err) {
        next(err);
      });
    });
  }

  tasks.push(function(next) {
    User.count({transaction:trn}).success(function(max) {
      console.log(max);
      next(null);
    }).error(function(error) {
      console.log(error);
      next(error);
    });
  });

  async.series(tasks, function(err, results) {
    console.log(err);
    if (err) {
      callback(err);
      return;
    }
    if(trn) {
      trn.commit().success(function() {
        console.log('OK');
        callback(null);
      }).error(function(error) {
        console.log(error);
        callback(error);
      });
    }
  });
}

sequelizeによるマイグレーションの方法

sequelize-cliをインストールすることにより、DBのマイグレーションが可能になる。
この機能により、本来は困難であるはずのDBスキーマーの更新を容易にすることが可能になる。

下記を参考にすること。
http://sequelizejs.com/docs/latest/migrations

インストール方法

npm install -g --save sequelize-cli

以降、sequelizeというコマンドが使用可能になる。
※使用するDBのライブラリもインストールすること

初期化処理

コマンド

sequelize init

説明
このコマンドにより、カレントフォルダの初期化を行う。
configディレクトリとmigrationフォルダを作成する。
configディレクトリはDBへの接続情報を記述するconfigである。
test用、development用、production用の3つが作成できる。デフォルトはdevelopmentとなり、これを変更するには--envオプションを使用する。
migrationディレクトリには、バージョンアップ用、バージョンダウン用の実装を行うためのマイグレーション用のファイルを格納する。

スケルトンの作成

コマンド

sequelize migration:create

説明
migrationフォルダに新規のマイグレーション用のマイグレーションのファイルを作成する。
作成されるテンプレートは以下のようになる。

module.exports = {
  up: function(migration, DataTypes, done) {
    // add altering commands here, calling 'done' when finished
    done()
  },
  down: function(migration, DataTypes, done) {
    // add reverting commands here, calling 'done' when finished
    done()
  }
}

upメソッドにはバージョンアップ時の処理を記述する。
downメソッドにはバージョンを元に戻すための処理を記述する。
この際、処理がすべて終わったらdone()を行うこと。

実際のdbに対する操作は、migrationの下記のメソッドを用いて操作を行う

crateTable: テーブルの作成
dropTable: テーブルの削除
dropAllTables : 全てのテーブルの削除
renameTable: テーブルの名称を変更
addColumn:列の追加
removeColumn:列の削除
changeColumn:列の属性情報を変更する
renameColumn:列の命名変更
addIndex:インデックスの追加
removeIndex: インデックスの削除

以下はバージョンアップでテーブルを追加する例になる

module.exports = {
  up: function(migration, DataTypes, done) {
    // add altering commands here, calling 'done' when finished
    migration.createTable(
      'nameOfTheNewTable',
      {
        id: {
          type: DataTypes.INTEGER,
          primaryKey: true,
          autoIncrement: true
        },
        createdAt: {
          type: DataTypes.DATE
        },
        updatedAt: {
          type: DataTypes.DATE
        },
        attr1: DataTypes.STRING,
        attr2: DataTypes.INTEGER,
        attr3: {
          type: DataTypes.BOOLEAN,
          defaultValue: false,
          allowNull: false
        }
      }
    );
    done()
  },
  down: function(migration, DataTypes, done) {
    // add reverting commands here, calling 'done' when finished
    migration.dropTable('nameOfTheNewTable')
    done()
  }
}

マイグレーションの実行

コマンド

sequelize db:migrate

説明
実行していないマイグレーションファイルを実行する。
もし複数マイグレーションのファイルが存在がある場合は、一回のコマンドですべて実行する。

このコマンドを実行した際に、DBにSequelizeMetaテーブルを作成する。
このテーブルには、このDBにどのマイグレーションファイルが適用されたかを記述するテーブルである。

マイグレーションの取り消し

コマンド

sequelize db:migrate:undo

説明
最後に実行したマイグレーションの処理を取り消す。
複数回繰り返すことにより、最初の状態まで復帰できる。

Excel VBAコーディング ガイドライン案

Excel VBAコーディング ガイドライン案

ここで記述する内容はあくまでガイドライン、指針の案にすぎない。この規約を守るためにコードを作るのでなく、よいコードを作るためのガイドラインにすぎない。このガイドラインがよいコードを作るのに障害になる場合は、ガイドラインを変えるか、ガイドラインを使用しない。

つまり、このガイドラインは必要に応じて、変更されることがある。

もし、ガイドラインを考える場合、VB6の規約が参考になる。
Visual Basic Coding Conventions

宣言について

変数の宣言を強制する

モジュールの先頭に下記の構文を記述して型の宣言を強制すること。

Option Explicit

この宣言は下記の手順で自動で作成することもできる。

  1. 【ツール】→【オプション】
  2. 【編集】のタブを選択
  3. 【変数の宣言を強制する】をチェックする。

理由

変数名の記述ミスがあった場合、コンパイル時にそのミスを検知できる。

暗黙の型は使用しない

VBAで変数の型を指定しない場合、Variant型として扱われる。
暗黙型の例:

Dim A ' Aは Variant型になる

また、型変換を行う場合はCStrなどの変換関数を使用して暗黙の型変換はおこなわないこと。

理由

可読性を上げる

一行で複数の宣言を行う場合、それぞれに型を明示すること。

VBAでは一行で複数の型は指定できるが、暗黙の型にならないようにすること。

悪い例:

Dim UserMin, UserMax As Integer

この場合、Variant型のUserMinとInteger型のUserMaxが作成される。
両方をInteger型にしたい場合、下記のようにすること。

正しい例:

Dim UserMin As Integer , UserMax As Integer

スコープは明示すること

関数、変数ともにPublic/Privateを明示すること。
省略した場合はPublicになるが、省略は禁止する。
なおGlobalは使用を行わないこと。

理由

省略時に他のプログラミング言語経験者が混乱しやすいのでスコープは明示する必要がある。

スコープはなるべく小さくする

関数内の変数ですむ場合は、それで済ます。
Private変数ですむ場合はそれで済ます。
クラスモジュールの変数は、なるべく関数を経由すべきだが、Public変数も認めてもよい。
標準モジュールのPublic変数は基本的に使用しないこと。

理由

変更時のリスクを減らすため

変更の可能性のあるマジックナンバーはConstで宣言すること。

「よくわからない数字だが、とにかく動く、まるで魔法のようだ」という皮肉から下記のような物をマジックナンバーという。

    If l > 1024 Then
        MsgBox "上限エラー"
    End If

これは下記のようなConstを用いた定数に置き換えること

    Const MAX_LENGTH As Long = 1024
    If l > MAX_LENGTH Then
        MsgBox "上限エラー"
    End If

理由

可読性をあげること。
変更が発生した場合の変更を1か所にするため。

この理由のため、直角三角形の公式などでは、変更の可能性もなく、数値の方が可読性が高いので、定数を使用しない。

変数について

Integer型の使用は避ける

VB6ならびにVBAのIntger型は16bitである。
メモリ節約の意図がない限りLong型を使用すること。

理由

通常32bitOSのIntは32bitとして扱われることが多いため、混乱を抱かせる。
また、パフォーマンス的にLong型より若干落ちる。この説明は下記を参照のこと。
MSDN The Integer, Long, and Byte Data Types

変数の初期化を明示的に行う

VB6やVBAでは規定の初期値があるが、なるべく明示的に初期化を行うこと。
クラスモジュールを使用した場合は、Class_Initializeにて全てのメンバ変数の初期化を行う。

変数の宣言と同時のNewは禁止

宣言と同時にNewを行った場合:(禁止例)

Dim reimu As New clsYukkuri
reimu.name = "ゆっくり霊夢" ' ここでインスタンスを生成

後でNewを行った場合:(正しい例)

Dim reimu As clsYukkuri
Set reimu = New clsYukkuri ' ここでインスタンスを生成
reimu.name = "ゆっくり霊夢"

以下の理由により、変数宣言と同時のNewは行わない。
インスタンスの作成のタイミングを制御できなくなり、予期しないバグを発生する可能性がある
オブジェクトが使用されるたびにインスタンスの有無をチェックするのでオーバーヘッドがある

解説

変数と同時にNewを行った場合、最初にオブジェクトにアクセスした場合にインスタンスが生成される。
これは、インスタンスの生存期間がプログラマの意図しないものになる可能性があることを表す。以下のサンプルはその例になる。

    Dim reimu As New clsYukkuri
    reimu.Name = "れいむ"
    Call reimu.TakeItEasy
    Set reimu = Nothing
    If reimu Is Nothing Then ' Nothingチェックした時点でインスタンスが作成されてしまう
        Debug.Print "削除されている"
    End If

また、オブジェクトにアクセスするたびに存在チェックを行うので当然パフォーマンスも落ちる。
下記の動画で、これらのパフォーマンスについての計測を行っている。
オプーナとゆっくりのExcelVBA講座 その11 「クラスモジュール」

参考

MSDN クラスの新しいインスタンスを作成する

[MSDN Dim x As New MyClass](http://msdn.microsoft.com/ja-jp/library/dd297716.aspx
"MSDN Dim x As New MyClass")

配列について

配列の最大と最小は明示する

VBAの配列の範囲はCなどと違う。

Dim buf() As String
ReDim buf(2) As String

上記のような指定の場合、Cなどはbuf(0)~(1)までしか使用できない。VBAの場合、デフォルトでは(0)~(2)まで使用できる。
Cなどと同じ範囲にしたい場合、下記のようにとりうる範囲を明示する。

Dim buf() As String
ReDim buf(0 To 1) As String

また、VBAは下記のコードで配列のデフォルトの最小値を変更できる。

Option Base 0 ' 添え字の最小値は常時0

もし、配列の最小値を明示しない場合、コードを移植した場合に正常に動作しない可能性が発生する。

このように、多言語経験者に余計な混乱を与えないため、Toを用いて配列の最小範囲、最大範囲は明示すること。

配列の大きさが頻繁に変更される場合、Collectionを使用する

Collectionを用いることで、配列を使用するより容易に要素の追加、削除を行うことができる。
Collectionは配列と異なり、別の型も格納できる。

Public Sub CollectionTest001()
   Dim cll As New Collection

   Call cll.Add("オプーナ")
   Call cll.Add(CDate("2008/1/3 3:32"))
   Call cll.Add(CLng(23432))
   Call cll.Add("すぺらんかー")

   Dim vData As Variant
   For Each vData In cll
       Debug.Print TypeName(vData) & ":" & vData
   Next
   Set cll = Nothing
End Sub

また、以下のように連想配列としても使用可能だ。

Public Sub CollectionTest002()
   Dim cll As New Collection

   Call cll.Add("りんご", "赤")
   Call cll.Add("みかん", "黄")
   Call cll.Add("ぶどう", "紫")
   Dim vData As Variant
   Debug.Print "(1)-------------------------"
   ' 値の列挙
   For Each vData In cll
       Debug.Print TypeName(vData) & ":" & vData
   Next

   Debug.Print "-------------------------"
   ' キーに赤を指定することによりりんごが表示
   Debug.Print cll.Item("赤")

   Debug.Print "(2)-------------------------"
   ' キーを指定して黄を削除
   Call cll.Remove("黄")
   ' 値の列挙
   For Each vData In cll
       Debug.Print TypeName(vData) & ":" & vData
   Next

   Set cll = Nothing
End Sub

なお、Collectionにはキーの存在チェックはサポートしていないので、On Error Gotoでエラートラップするキーの存在チェック用の関数を自前で実装する。

別解

Scripting.Dictionaryを使用する。
キーの存在チェックはあるが、参照設定またはCreateObjectが必要なので使用には注意。

関数について

関数の引数のキーワードを明示化する

VBAの関数では参照渡しと値渡しの2種類が存在するので、これを明示すること。

参照渡しの例:

Sub ShowUser( ByRef User As String )

値渡しの例:

Sub ShowUser( ByVal User As String )

理由

明示を行わない場合、関数の呼び出し方によって参照渡し、値渡しが決定される。 混乱の原因になりやすい。

dim lVal as long
SubA lVal           '参照渡しになる
SubA(lVal)          '値渡しになる
Call SubA(lVal)     '参照渡しになる
Call SubA((lVal))   '値渡しになる

参照渡しの場合、引数で与えたパラメータが更新される可能性がある。

Dim a As String
a = "abc"
ShowUser( a ) ' ByRefの場合
' この時点でaの値は"abc"とは限らない

構造体やオブジェクト、大きな文字列を関数に渡す場合は参照渡しとして、それ以外は値渡とする

構造体やオブジェクト配列はそもそも参照渡しでしか関数に渡せないので、参照渡しとする。
また、大きい文字列を渡す場合は参照渡しとする。
それ以外は、値渡しとする。

理由

大きなサイズの文字オブジェクトを値渡しで扱うとコピーにリソースを消費してしまうので、参照渡しの方が望ましい。
それ以外の場合、関数内で引数の変更が行われても呼び出し元に悪影響を与えない値渡しの方が安全である。

演算

文字の結合には&演算子を使用する

文字の結合は+演算子でも可能であるが、&演算子を使用すること。

理由

+演算子の場合、演算対象に数字が混ざった場合、計算を行ってしまうため。

Round関数の丸め処理の違いに注意する。

VBA の Round 関数は、Excel のワークシート関数 Round は挙動が異なる。
Excel のワークシート関数 Round は、"算術型" の丸め処理を行う。この "算術型" 丸め処理では ".5" は常に切り上げらる。

これに対して VBA の Round 関数は "銀行型" の丸め処理を行う。"銀行型" の丸め処理の場合は ".5" は、結果が偶数になるように丸め処理が行われ、切り上げられることも、切り捨てられることもある。

VBA と Excel の Round 関数の違いは、以下の表のようになる。

数値 算術型 銀行型
1.5 2 2
2.5 3 2
3.5 4 4
4.5 5 4
5.5 6 6
6.5 7 6

Excel のワークシート関数と一貫性のある Round 関数をVBA内で使用したい場合は、以下の例のように Applicationプロパティを使用する。

x = Application.Round(y ,0)

この例では、y に 8.5 を代入すると 9 を返す

参考

丸めを行うカスタム プロシージャを実装する方法
[OFFXP]VBAのRound関数について

剰余の計算でMod演算子とMod数式の結果が違う

VBAのMod演算子とExcelのサポートしているMod数式の計算結果は異なる。

Mod演算子の例

Debug.print 10 Mod -3

Mod数式の例

=Mod(10, -3)

計算結果:

A B Mod演算子 Mod数式
10 3 1 1
10 -3 1 -2
-10 3 -1 2
-10 -3 -1 -1

もしVBAでMod数式と同じ結果が欲しい場合は、下記のようにする。

Function fmod(ByVal a As Double, _
                     ByVal b As Double)
    fmod = a - b * Int(a / b)
End Function

参考

MSDN MOD 関数、Mod 演算子は異なる値を返す

誤差が発生して困る計算にはCurrency型またはDecimalを用いる

Single型、Double型の演算では誤差が発生する。

浮動小数点の演算で誤差がでる例:

Public Sub test()
    Dim a As Double
    Dim i As Long
    For i = 1 To 10
        a = a + 0.1
    Next i
    Debug.Assert a = 1 ' NG
End Sub

上記のコードを正常に動作させるには次のように変更する

例:通貨型(固定小数点)で計算する

'Dim a As Double
Dim a As Currency

例:Decimal型として扱う

    Dim a As Variant
    Dim i As Long
    For i = 1 To 10
        a = a + CDec(0.1)
    Next i
    Debug.Assert a = 1

Fix、Int関数では浮動小数点レジスタの精度が影響するので変数に格納してから計算する。

Int,Fix関数は、その実行タイミングで計算結果が変わるので注意を払って使用すること。

    Dim a As Double
    Dim b As Double
    a = Fix(0.6 * 10)
    Debug.Print a '5

    b = 0.6 * 10
    b = Fix(b)
    Debug.Print b '6

これはInt,Fix関数のバグなどではなく、浮動小数点レジスタの精度が影響している。
浮動小数点レジスタの精度がDobule型のものより高いため、計算結果が異なってしまう。
これをさけるには、FixやIntを実行する前にCDblでキャストするか、変数に代入してから行うと、浮動小数点レジスタの精度の違いによる誤差はでなくなる。

参考:VBAのFixやIntの計算誤差は浮動小数点レジスタの精度がかかわっている

制御処理

判定文は全ての判定処理が実行されることに注意する

下記のような判定文があった場合、A()でFalseがかえってもB()も実行される

IF A("TEST") = True And B("TEST") = True Then
End IF

このため、B()が処理時間のかかる処理の場合は以下のようにする。

IF A("TEST") = True Then
  IF B("TEST") = True
  End If
End IF

また、下記のように一行で、インスタンスの有無と、インスタンスの使用を行う判定文を記述した場合アプリケーションエラーとなる。

IF Not A Is  Nothing And A.Test("TEST") = True Then
End IF

With ~ End With の途中で抜けない

エラーが発生する可能性があるので、下記のようなコードは禁止

With hoge
.A = 2
.B = 3
Exit Sub
End With

終了条件のGOTOは認める

VBAにはtry-catch-finallyが存在しないので、終了処理の共通化のためのGOTOは認める。

Dim a as Object
Set a = new Hoge
   If Not a.Test Then
      Goto Finish
   End If
   Call a.Test2

Finish:
  a.Finish()
       Set a = Nothing

深さを減らすようにする

タブの深さを減らすような制御構造にする

修正前:

If A = True Then
  X = False
Else
  For i = 0 To 100
    If Z = 3 Then
      Call Hoge
    End if
  Next i
End If
X = True

修正後:

If A = True Then
  X = False
  Exit Function
End If
For i = 0 To 100
  If Z = 3 Then
    Call Hoge
  End if
Next i
X = True

環境依存

なるべく多くの環境で動作することを目指す場合、以下のようなことに注意すること。

参照設定やCreateObjectの使用は慎重に行う。

動作環境が指定できない場合、参照設定で指定されたCOMなどが存在しないで、正常に動作しない場合がある。

動作環境を慎重に考慮する。

XLAの使用は慎重に行う。

XLAを用いることで、異なるワークブックで処理の共通化が可能である。
しかし、特定のXLAが存在しなければ、動作しなくなるということは忘れてはいけない。

日本語の変数や関数名は使用しない

日本語の変数名を宣言できるが、他の地域で使用することを考えた場合、使用してはいけない。

Date型は日付リテラルで指定する。

Date型に文字を入力した場合、その解釈は地域によって異なることになる。

  Dim a As Date
  a = "12/1/1 12:0:0"
  Debug.Print a

この実行結果は日本と米国で異なる。
日本では「2012/1/1 12:00」と解釈するが、米国では「2001/1/12 12:00」と解釈する。

これらを避けるため、日付リテラルとして指定すること。
以下のように入力すると・・・

a = #12/1/1 12:0:0#

VBEが以下のように変換してくれる。

a = #12/1/2001 12:00:00 PM#

DLLの呼び出しの際に32bit,64bitプロセスのいづれかであるか注意する。

Declare 句を用いる事でDLLを使用できるが、32bitか64bitか常に考えて使用すること。

32ビットプロセスのExcelからは64bitのDLLは使用できないし、64ビットプロセスのExcelから32bitのDLLは使用できない。

「Win64」という条件付きコンパイル定数の使用を検討すること。

よくあるトラブルと対策

実行前に必ず保存する

VBAはコンパイル時に保存されない。
プログラムを実行してプロセスが異常終了した場合、その成果物はすべて消える。

そのため、VBAを実行する前は必ず保存をすること。

終了ボタンを押してから再実行すること。

デバッグなどを行っている場合、必ず終了ボタンを押してから再開すること。
標準モジュールのモジュールレベルの変数はプログラムが中断しただけでは初期化されない。終了ボタンをおして初めて初期化される。

メモリ不足が頻発する場合は、ワークブックを作り直す

特別にメモリを使うプログラムでないのにVBAの実行でメモリ不足が頻発する場合、ワークブックを作り直してみる。

Excel2003の時は、行の挿入と削除を繰り返すとゴミデータが蓄積されて上記のような事態に遭遇する場合があった。

Excel VBA固有のガイドライン

WorkBookを明示する

WorkBookの指定方法には下記の方法がある。

ActiveWorkbook.Sheets(1).Cells(1, 1).Value = "TEST"      ' 1 アクティブのワークブックの操作
Workbooks("Book1").Sheets(1).Cells(1, 1).Value = "TEST"  ' 2 Book1の操作
ThisWorkbook.Sheets(1).Cells(1, 1).Value = "TEST"        ' 3 マクロを含むブックの操作

なにも指定しない場合はActiveWorkbookに対して操作を行っている。

ActiveWorkbookにすると、コードの成否はマクロ実行前、またはマクロ実行中のユーザの操作に依存することになる。
このため、どのワークブックに対して処理を行うか明示しておいた方が望ましい。

WorkSheetを明示する

シートの選択を省略した場合、暗黙的にActiveSheetになる。

ユーザーがシートを切り替える可能性があるので、指定のシートを操作させたい場合は、必ずシート名を指定すること。

Sheet("HOGE").Cells(1,1).Value = 5 ' OK
ActiveSheet.Cells(1,1).Value = 5 ' OK
Cells(1,1).Value = 5 ' NG

規定のプロパティは使用禁止

いくつかのオブジェクトでは既定のプロパティが用意されているが、省略は禁止とする。

Cells(1,1)=5  ' NG
Cells(1,1).Value = 5 ' OK

既定のプロパティを省略すると意図した動作にならない場合がある。以下の例では、選択したセルをDebug.Printして、その内容を書き換えようとしている。

    Dim c As Variant
    For Each c In ActiveSheet.Range(Selection.Address)
        Debug.Print c
        c = "test"
    Next c

この例では、Debug.printは表示されるが、セルの内容は書き変わらない。

Valueを明示しておけば、この問題は発生しない

    Dim c As Variant
    For Each c In ActiveSheet.Range(Selection.Address)
        Debug.Print c.Value
        c.Value = "test"
    Next c

速度を求める場合、画面の更新を行わない

画面の更新を中止することにより、計算途中の無駄な描画を省き速度をあげることができる。

画面の更新を中止する例:

Dim i As Integer, j As Integer
Application.ScreenUpdating = False
For i = 1 To 100
   For j = 1 To 10
       Cells(j + 54, 18).Select
       Selection.Value = j
   Next j
Next i
Application.ScreenUpdating = True

意味のないSelectを使用しない

セルの選択は速度に影響を与えるので不要な選択はしない。

Selectを使用している例:

Dim i As Integer, j As Integer
For i = 1 To 100
   For j = 1 To 10
       Cells(j + 53, 4).Select
       Selection.Value = j
   Next j
Next i

Selectを使用しない例:

Dim k As Integer, l As Integer
For k = 1 To 100
   For l = 1 To 10
       Cells(l + 53, 5).Value = l
   Next l
Next k

Selectを使用しない例では5倍以上速度が改善される。

シートのアクセスに配列を使用するとパフォーマンスは改善する

ワークシートを直接操作するより、配列を経由した方が速い。ただし、メモリの使用量には注意する。

直接操作の例:

Dim i As Long, j As Long, buf As Long
For i = 1 To 10000
   For j = 1 To 100
       buf = Cells(i + 80, j + 2).Value
   Next j
Next i

配列の例:

Dim i As Long, j As Long, buf As Long, C() As Variant
Let C = Range("C81:CX10080").Value
For i = 1 To 10000
   For j = 1 To 100
       buf = C(i, j)
   Next j
Next i

配列を使用すると14倍以上速度が改善された。
ただし、配列を使用することにより、メモリを余分に使用する副作用については下記を参照。

配列を経由したセルの値設定の副作用

クリップボードを使用するCopyは使用しない

Copyには2種類の方法があるが、特にクリップボードを経由する方法は使用禁止。
直接コピーする方法もなるべく避ける。

クリップボードを使用する例:

wkSheet.Rows("1:3").Copy
wkSheet.Cells(4, 1).Select
wkSheet.Paste

直接コピーする例:

wkSheet.Rows("1:3").Copy Destination:=Cells(4, 1)

クリップボードを経由すると速度に悪影響を与える。

また、直接コピーするコードの場合でもクリップボードの中身はクリアされるため、ユーザの操作に影響を与える。

ExcelVBAのバージョン管理方法

目的

この記事ではExcelVBAでバージョン管理を行う方法について説明する。
以下のようにWinMergeの差分機能を用いることで、VBAの差分を表示することが可能である。

b0232065_21525328.png

手順

WinMergeの準備

  1. WinMergeのダウンロードを行う
  2. ダウンロードページより「Excelからテキストへの変換プラグイン」のExcelToText.sctをWinMergeのMergePluginsフォルダにコピーする。
    例:"C:\Program Files\WinMerge\MergePlugins"
  3. WinMergeを起動してプラグインの自動展開を選択する
    b0232065_21443344.png

Excelの準備

Excel 2003の場合

  1. Excelを起動
  2. [ツール]→[マクロ]→[セキュリティ]
  3. [信頼できる発行元タブ]を選択する。
  4. [Visual Basic プロジェクトへのアクセスを信頼する]にチェックを付与する。
    b0232065_21472719.png

Excel 2010の場合

  1. Excelを起動
  2. 開発タブを選択して「マクロのセキュリティ」をクリックする
    b0232065_21482034.png
  3. 「VBAプロジェクトオブジェクトモデルへのアクセスを信頼する」をONにする。
    b0232065_21504100.png

構成管理ソフトの準備

差分を外部プログラムを用いて行うようにする。

TrotoiseSVNの準備

  1. [TrotoiseSVN]→[設定]を表示する
  2. 差分ビュワーを選択してWinMergeを選択する。
    b0232065_2155966.png
    設定例:

    C:\Program Files\WinMerge\WinMergeU.exe -e -x -ub -dl %bname -dr %yname %base %mine

別解

Ariawaseを用いてファイルをテキストファイルに変更する。
https://github.com/vbaidiot/Ariawase
http://igeta-diary.blogspot.jp/2014/03/what-is-vbac.html

build.batはsrcフォルダの内容をインポートしてXLSファイルを更新する。

以下のコマンドでXLSの内容をエクスポートしてソースをテキストとして出力作成する

cscript //nologo vbac.wsf decombine

構成管理にテキストとしてソースコードを格納できることと、複数人が同時に1ファイルのソースを修正することができるのは強み。

幕末のおやじ~壬生義士伝

昨今、チョイ悪親父とかいうクッソ軟弱なワードが流行っていますが、幕末を駆け抜けた幕末の親父である 吉村貫一郎の姿をみると、士道不覚後で切腹ものの軟弱ワードだと思うのですよ。

https://www.amazon.co.jp/dp/B00FW5HPYC

壬生義士伝(上)

私は原作の小説を2002年頃よんで、2020年に映画版を見てみました。おそらくは尺の関係で色々改変されたり、あえてカットしているんだろうなという描写がありますが、これはこれでありだと思っています。

原作には新選組の元同僚で居酒屋の主人がいるのですが、彼から見た吉村さんの描写や、見合いのエピソードあたりの彼の動きがばっさりカットされたのは残念ではあります。ただし、80年代に流行った年末時代劇ならともかく2時間しばりのなかではやむ得ないとも理解します。

話の筋でいうと、奥州の学校の先生だった人が、教え子の手前、地元で金策をするわけにもいかず、家族を食わせるために上京して新選組で頑張るという話です。彼の家族のための守銭奴っぷりが大義名分を振りかざす幕末において滑稽でもあり、また、家族のための守銭奴っぷりを発揮しながらも、吉村先生なりの一線というものがあって、ややこしいことになっています。

家族のための守銭奴なら、そもそも脱藩せずに恥も外聞もなく藩の中で金策に走っていればなんとかなったでしょうし、新選組の旗色が悪くなったところで上手く逃げるという手段もありました。それができなくても、全員が腰が引けている最後の突撃で皆と一緒に逃げるという手段もあったわけです。

そういった吉村先生のやっかいさがしっかり長男にも遺伝しており、わざわざ函館までいっちゃったりするわけで、このあたり単純な家族愛ではなく、ある種の男の意地的なものがあって、なんとも言えないものがあります。

なお、水木しげる先生も幕末のおやじって漫画を描いてらしたらしいっすよ。

https://t.co/q6KuLFsaOY?amp=1