本稿の内容を増補改訂したアップデート版が提供されています。詳しくは連載の目次「5分でわかるActiveReports帳票」をご参照ください。
はじめに
ActiveReports for .NET(以下ActiveReports)はVisual Studioと統合された使いやすいレポートデザイナや高機能なレポートビューワ、多彩な出力形態をサポートする帳票作成コンポーネントです。
今回は、複数のデータソースを利用した帳票作成の例と、ActiveReportsのグラフコントロールについて紹介していきます。
これまでの記事
- 第1回:5分でわかるActiveReports帳票-らくらく始める帳票作成
- 第2回:5分でわかるActiveReports帳票-集計処理と改ページ
- 第3回:5分でわかるActiveReports帳票-改ページ制御と多段組レイアウト
- 第4回:5分でわかるActiveReports帳票-罫線・折り返しとプレビュー・印刷
対象読者
- Visual Basic 2005またはVisual C# 2005を使ってプログラムを作ったことのある方。
- 帳票作成ツールに興味のある方。
必要な環境
- Visual Studio 2005、Visual Studio .NET 2003でプログラムが作れる環境。
本記事のサンプルコードはC# 2.0/Visual Basic 2005で記述しています。
ActiveReportsからさまざまなデータソースを利用する
最終的な完成イメージ
今回のサンプルは、学力テストなどでおなじみの「成績一覧表」です。
帳票には診断テストを受けた各生徒の氏名、クラスおよび志望校と、これまでの成績がグラフで表示されます。また、末尾には各テストの平均点と偏差値が表形式で表示されます。
データ構造
今回は「複数のデータソースを組み合わせて1つの帳票を作成する」ことを目的とし、データソースとして「XMLファイル」「Accessデータベース」「SQL Serverデータベース」「Excelワークシート」の4種類を使用します。
生徒データ:XMLファイル(生徒ID、生徒名、クラス、受講科目)
<?xml version="1.0" encoding="Shift-JIS" ?> <Year name="2007"> <Grade id="3"> <student id="0001"> <name>青木愛子</name> <class>A</class> <subject>EMJ</subject> </student> <student id="0002"> <name>伊藤美佐子</name> <class>A</class> <subject>EM</subject> </student> <student id="0003"> <name>遠藤修作</name> <class>B</class> <subject>EMJ</subject> </student> <student id="0004"> <name>小野田健</name> <class>A</class> <subject>EMJ</subject> </student> <student id="0005"> <name>斉藤はじめ</name> <class>A</class> <subject>EMJ</subject> </student> <student id="0006"> <name>佐藤翔</name> <class>A</class> <subject>EMJ</subject> </student> <student id="0007"> <name>佐藤祐樹</name> <class>B</class> <subject>EM</subject> </student> </Grade> </Year>
志望校データ:SQL Serverデータベース(生徒ID、第1志望校、第2志望校)
テスト結果データ:Accessデータベース(生徒ID、テスト月、各科目の点数)
平均偏差データ:Excelワークシート(過去のテストの平均点・平均偏差)
帳票レイアウトの概要
ページヘッダには帳票タイトルを、ページフッタにはページ番号を表示します。次に「学年」単位のグループヘッダ/フッタを追加します。グループヘッダには学年を表示し、グループフッタには統計情報(平均点や偏差値など)を表示します。グループフッタに表示される表にはOleObjectコントロールを使用します。Detailセクションには生徒の氏名、クラス、受講科目、第1、第2志望校とこれまでの成績をグラフで表示するようにします。
詳細についてはこの後のセクションで順を追って解説していきます。
データソースとしてXMLファイルを利用する
ActiveReportsでは、RDB(リレーショナルデータベース)やExcelで使われているような「表形式」のデータだけでなく、XMLファイルをデータソースとしてそのまま利用することができます。
データソースにXMLファイルを設定するには、これまでと同様、Detailセクションに表示されているアイコンをクリックして「レポートデータソース」ダイアログを設定します。「レポートデータソース」ダイアログでは「XML」タブを選択して、ファイルのURLとレコードセットパターンを指定します。レコードセットパターンには、基準となるノードのXPathを設定します。今回はstudentタグの部分が繰り返し項目になるので、レコードセットパターンに「//student」と入力します。
レポートデータソースにXMLファイルとレコードセットパターンを設定すると、レポートエクスプローラの「フィールド」にXMLのデータ構造が展開され、ツリー形式で表示されます。黄色の矢印部分がレコードセットパターンとして設定した箇所です。
レポートエクスプローラに表示されている「name」や「@id」などのノードをレポートデザイナにドラッグ&ドロップすると、値をバインドさせたTextBoxコントロールを配置できます。ドロップされたTextBoxコントロールにはコントロール名やプロパティが自動的に設定され、DataFieldプロパティにはバインド先のXPathが設定されます。ここではDetailセクションの「クラス」「受講科目」「氏名」のTextboxコントロールをドラッグ&ドロップで貼り付けます。
また、他のデータソースから参照したいので生徒のIDもTextBoxとして貼り付けていますが、帳票に表示させる必要はないのでVisibleプロパティをfalseに設定し、非表示の項目とします。このような非表示項目は他の表示項目と見た目が同じでは紛らわしいため、背景色にあえて鮮やかな色を使い、区別できるようにしています。
基準よりも上位のノードのコントロールを張り付けると、DataFieldプロパティには「../@id」のように相対パス形式でXPathが指定されます。今回はgradeHeaderセクションの「学年」の部分に上位ノードのコントロールを使用しています。
XMLファイルを動的にデータソースとして指定する
レポートデータソースにXMLファイルを指定すると、レポートエクスプローラに表示されるノードをドラッグ&ドロップしてコントロールを配置できるため大変便利なのですが、ファイルの配置場所やレコードセットパターンがアプリケーション実行時の条件によって変更される場合、事前に固定の値を与えることができません。
アプリケーション実行時にXML形式のデータソースを動的に設定したい場合は、ActiveReports帳票を実行するアプリケーション側でDataDynamics.ActiveReports.DataSources.XMLDataSource
のインスタンスを生成します。この場合もレポートデザイナでデータソースを設定するのと同じように、FileURLプロパティにはXMLファイルのパスを、RecordSetPatternプロパティにはレコードセットパターンをそれぞれ設定します。以下のサンプルコードでは、ボタンを押下するとビューワにXMLデータソースをバインドした帳票が表示されます。
private void btnExecute_Click(object sender, EventArgs e) { NewActiveReport1 rpt = new NewActiveReport1(); DataDynamics.ActiveReports.DataSources.XMLDataSource xmlds = new DataDynamics.ActiveReports.DataSources.XMLDataSource(); try { // XML文書のファイル名を設定します。 xmlds.FileURL = @"C:\ActiveReports3\Sample_05\2007Student.xml"; //データセットパターンを設定します。 xmlds.RecordsetPattern = "//student"; if (xmlds.Count == 0) { rpt.Dispose(); Console.WriteLine("NoDataError"); return; } rpt.DataSource = xmlds; rpt.Show(); } catch (DataDynamics.ActiveReports.ReportDataException rde) { Console.WriteLine(rde.Message); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles Button1.Click Dim report As New NewActiveReport1 Dim xmlds As _ New DataDynamics.ActiveReports.DataSources.XMLDataSource Try xmlds.FileURL = _ "C:\\ActiveReports3\\Sample_05\\2007Student.xml" xmlds.RecordsetPattern = "//student" If xmlds.Count = 0 Then report.Dispose() Console.WriteLine("NoDataError") Return End If report.DataSource = xmlds report.Show() Catch rdex As DataDynamics.ActiveReports.ReportDataException Console.WriteLine(rdex.Message) Catch ex As Exception Console.WriteLine(ex.Message) End Try End Sub
XMLDataSourceを使用するときにRecordSetPatternプロパティを設定していないと、ReportDataExceptionが発生し「FileURLが設定されていないか、空です。」というエラーメッセージが表示されます。また、XMLファイルの中身が空であった場合もエラーになります。しかし、設定してあるRecordsetPatternが「間違っていた場合」はエラーが発生せず、データのない(白紙の)レポートが作成されてしまいます。
この問題を回避するには、上記サンプルコードのようにXMLDataSourceのCountプロパティでデータ件数を確認するか、ActiveReport側のNoDataイベントにコードを記述して処理を停止させる必要があります。
レポートデザイナでXMLを設定した場合は、設定後すぐにファイルの中身が確認されるので、指定したレコードセットパターンが間違っているとエラーがポップアップ表示されます。また、レポートエクスプローラーのフィールドにも赤字でエラー表示されます。
ChartControlを利用したグラフの作成
ここからは、データソースとして指定したXMLファイルに含まれていない項目の表示部分について説明していきます。
ActiveReportsで帳票にグラフを描画するには、ChartControlコントロールを利用します。ツールバーからレポートデザイナ上のグラフを描画したい場所へChartControlコントロールをドロップすると、自動的にグラフウィザードが起動します。グラフウィザードでは、画面の指示に従って必要な項目を設定していくことでグラフを作成できます。
今回はグラフにバインドするデータソース(成績データを取得する部分)をまだ作っていないので、そのままの状態で「完了」ボタンを押し、いったんグラフウィザードを終了します。
今回の帳票ではX軸(横軸)に「テスト実施月」を、Y軸(縦軸)にテストの点数を取り、科目ごとに点数をバーで表示する簡単な棒グラフを作成します。
グラフを表示させるためのデータは、Accessデータベースに次のような形式で格納されています。
グラフに表示するためのデータを取得する
グラフは生徒ごとに別々のものを表示する必要があるので、データを取得するには生徒のID番号を指定して試験結果を取得する、以下のようなSQLを考えます。「?」の部分は実行時に生徒のID番号がバインドされます。
SELECT ExamName, MONTH(ExamDate) as ExamDate, StudentID, JapaneseScore, MathScore, EnglishScore FROM ExamResults WHERE StudentID = ? ORDER BY ExamDate
接続文字列は設定ファイルに書こう
前回までは便宜のため、ソースコード中に接続文字列を直接記述(ハードコーディング)していましたが、接続先データベースやアカウントの情報をハードコーディングしてしまうと、設定を変えるたびにソースを修正して再ビルドしなくてはならず、非常に不便です。
今回は、アプリケーション設定ファイル(App.config)に接続文字列を記述しておき、実行時に読み出す方法を紹介します。App.configには接続文字列を記述するためのconnectionStringsセクションが用意されているので、ここに接続文字列と設定名を記述します。ここではAccessデータベースへの接続と、SQL Serverへの接続を追加します。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <!--Accessデータベースへの接続--> <add name="access" connectionString="Provider=Microsoft.Jet.OLEDB.4.0; Data Source=C:\ActiveReports3\Sample_05\Students.mdb; Persist Security Info=False"/> <!--SQL Serverデータベースへの接続--> <add name="sqlserver" connectionString="Data Source=localhost\sqlexpress; Database=Sample05;Trusted_Connection=SSPI; Persist Security Info=True;"/> </connectionStrings> </configuration>
App.configに定義した接続文字列をアプリケーション側で取得するには、System.Configuration.ConfigurationManagerクラスを使用します。ConfigurationManagerクラスを使う場合、ソースの先頭にUsing(VBの場合はImports)ディレクティブを追加するだけでなく、プロジェクトにSystem.Configuration.dllへの参照を追加する必要があります。
string connStr = ConfigurationManager.ConnectionStrings["access"].ConnectionString;
Dim connStr = _ ConfigurationManager.ConnectionStrings("access").ConnectionString
グラフに必要なデータの取得処理
App.configに接続文字列を記述したら、今度はActiveReports帳票のDetailFormatイベントに、生徒1人分のテスト結果データを取得してグラフを描画するためのコードを追加します。
private void detail_Format(object sender, EventArgs e) { this.chartControl1.Visible = true; //XMLの科目コード(EMJなど)を「英・数・国」などの表記に変更 //subjects(Dictionary型)の初期化はReportStartイベントで行う this.txtsubject1.Text = subjects[this.txtsubject1.Value.ToString()]; //クエリの定義 string query = "SELECT ExamName, MONTH(ExamDate) as ExamDate, StudentID, " + "JapaneseScore, MathScore, EnglishScore " + "FROM ExamResults " + "WHERE StudentID = ? " + "ORDER BY ExamDate "; //設定ファイルから接続文字列を取得する string connStr = ConfigurationManager.ConnectionStrings["access"].ConnectionString; //Accessデータベースに接続し、取得したデータをDataTableに格納する DataTable dt = new DataTable(); using (OleDbConnection conn = new OleDbConnection(connStr)) { OleDbCommand cmd = new OleDbCommand(query, conn); cmd.Parameters.Add(new OleDbParameter( "id", this.txtIdHidden.Value)); OleDbDataAdapter da = new OleDbDataAdapter(cmd); da.Fill(dt); } //DataTableをChartControlのデータソースに設定する this.chartControl1.DataSource = dt; }
Private Sub Detail_Format(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Detail.Format Me.ChartControl1.Visible = True 'XMLの科目コード(EMJなど)を「英・数・国」などの表記に変更 'subjects(Dictionary型)の初期化はReportStartイベントで行う Me.TextSubject1.Text = subjects(Me.TextSubject1.Value.ToString()) '設定ファイルから接続文字列を取得する Dim connStr = _ ConfigurationManager.ConnectionStrings("access").ConnectionString Dim query _ = "SELECT ExamName, MONTH(ExamDate) as ExamDate, StudentID, " _ & "JapaneseScore, MathScore, EnglishScore " _ & "FROM ExamResults " _ & "WHERE StudentID = ? " _ & "ORDER BY ExamDate " 'Accessデータベースに接続し、取得したデータをDataTableに格納する Dim dt As New DataTable Using conn As New OleDbConnection(connStr) Dim cmd As New OleDbCommand(query, conn) cmd.Parameters.Add(New OleDbParameter( _ "id", Me.TxtIdHidden.Value)) Dim da As New OleDbDataAdapter(cmd) da.Fill(dt) End Using 'DataTableをChartControlのデータソースに設定する Me.ChartControl1.DataSource = dt End Sub
取得したデータとChartControlコントロールのマッピング
グラフ表示に使うデータ取得処理ができあがったら、次はChartControlコントロールとグラフデータの紐付けを行います。プロパティウィンドウの右下に表示されている「グラフ デザイナ」のリンクをクリックすると、グラフデザイナが起動されます。
グラフデザイナのウィンドウが表示されたら、左側に表示されている「系列」ボタンをクリックし、グラフに表示するアイテム(国語、数学、英語)を追加していきます。左上に表示されている「アイテム追加ボタン」をクリックしてアイテムを追加し、オブジェクト名を「Japanese」とします。また、X軸のデータバインドには「ExamDate」を、Y軸のデータバインドには「JapaneseScore」を入力します。同様の手順で数学(Math)、英語(English)の系列も追加します。
グラフデザイナではこの他「グラフエリア」「タイトル」「凡例」「概観」などの設定項目を変更することで、グラフの見栄えを設定できます。
今回グラフデザイナで設定した項目
- グラフエリア:3Dをオフにする
- グラフエリア-軸(AxisX)「全般」タブ:種類=カテゴリ軸、最大値=6、タイトル=なし
- グラフエリア-軸(AxisY)「全般」タブ:種類=数値軸、最大値=100、タイトル=Score
- グラフエリア-軸(AxisY)「ラベル」タブ:「表示」にチェック
- タイトル:ヘッダ、フッタを非表示
- 凡例:「タイトル」タブ→ヘッダ、フッタを非表示
- 概観:パレットを「Greens」に変更、グラフ全体の背景を白(単色)に変更
OleObjectコントロールによるExcelワークシートの貼り付け
帳票フッタでは、これまでに実施した学力テストの平均偏差値とテストの平均点を表形式で表示しています。これは、特に複雑なことをやっているわけではなく、Excelワークシートをそのまま表示させています。ActiveReports帳票でExcelワークシートなどのOLEオブジェクトを取り込んで表示するには、OleObjectコントロールを使用します。
OleObjectコントロールをレポートデザイナへドラッグ&ドロップすると、挿入オブジェクトを指定するダイアログが表示されるので、目的のファイルを読み込みます。
このコントロールは実行環境にExcelがインストールされていなくてもコントロールの描画を行いますが、実行環境にActiveReports.Interop.dll がないとエラーメッセージが表示されてしまいますので注意が必要です。
このコントロールはAccessデータベースなどのDB内のOleObjectをバインドする用途でも利用できますが、CanGrowプロパティがないので、固定幅で表示されます。
データソースとしてDataReaderを使用する
これまではレポートデザイナの「レポートデータソース」ダイアログを利用して帳票に表示するためのデータソースを指定してきましたが、ActiveReportsでは、DataReaderを利用して実行時にデータを取得しながら動的に帳票を生成していくこともできます。ここでは、DataReaderを使用してSQL Serverデータベースに格納されている志望校データにアクセスし、「第1志望校」「第2志望校」に表示する値を取得する手順について説明します。
DataReaderを使って1レコードづつデータを読み込む場合は、ActiveReports帳票にアンバウンドフィールドを設定し、実行時に読み込んだ値を動的にセットする必要があります。細かい点でいくつか違いはあるものの、大まかには前々回に紹介したようなテキストファイルの読み込みと似たような方法になるといえるでしょう。DataInitializeイベントでアンバウンドフィールドを作成し、FetchDataイベントでデータを1件ずつ取得していきます。
DataReaderを使って帳票を生成していく手順を大雑把にまとめると、次のようになります。
- ReportStartイベントではデータベースに接続し、DataReaderを生成する。
- DataInitializeイベントではデータ格納用のアンバウンドフィールドを作成する。
- FetchDataイベントでは1件分のデータを取得し、アンバウンドフィールドへ格納する。
- ReportEndイベントではDataReaderとデータベース接続をクローズする。
ここからは、各イベントごとに処理のコードを見ながら説明していきます。
ReportStartイベントの実装
ReportStartイベントでは、設定ファイル(App.config)から読み込んだ接続文字列をもとにデータベースとの接続を確立し、SQLを発行してDataReaderのインスタンスを取得します。
//SqlConnection, SqlDataReader, Dictionaryは //他のメソッドから参照するのでクラスメンバとして定義 private SqlConnection conn; private SqlDataReader dataReader; private Dictionary<string, string> subjects = new Dictionary<string, string>(); private void NewActiveReport_ReportStart(object sender, EventArgs e) { //科目コードを名称に変換するためのDictionaryを作る subjects["EMJ"] = "英・数・国"; subjects["EM"] = "英・数"; //設定ファイル(App.config)から接続文字列を取得し、 //データベースに接続する conn= new SqlConnection(); ConnectionStringSettings setting = ConfigurationManager.ConnectionStrings["sqlserver"]; conn.ConnectionString = setting.ConnectionString; conn.Open(); //SqlCommandを生成して実行し、DataReaderを取得する string query = "SELECT C.STUDENT_ID, S1.SCHOOL_NAME AS CHOICE1, " + "S2.SCHOOL_NAME AS CHOICE2 " + "FROM CHOICE C " + "LEFT OUTER JOIN SCHOOL AS S1 ON S1.SCHOOL_CODE = C.CHOICE1 " + "LEFT OUTER JOIN SCHOOL AS S2 ON S2.SCHOOL_CODE = C.CHOICE2 " + "ORDER BY C.STUDENT_ID "; SqlCommand cmd = new SqlCommand(query, conn); dataReader = cmd.ExecuteReader(); }
'SqlConnection, SqlDataReader, Dictionaryは '他のメソッドから参照するのでクラスメンバとして定義 Private conn As SqlConnection Private dataReader As SqlDataReader Private subjects As New Dictionary(Of String, String) Private Sub NewActiveReport1_ReportStart(_ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.ReportStart '科目コードを名称に変換するためのDictionaryを作る subjects("EMJ") = "英・数・国" subjects("EM") = "英・数" '設定ファイル(App.config)から接続文字列を取得し、 'データベースに接続する conn = New SqlConnection Dim setting As ConnectionStringSettings setting = ConfigurationManager.ConnectionStrings("sqlserver") conn.ConnectionString = setting.ConnectionString conn.Open() 'SqlCommandを生成して実行し、DataReaderを取得する Dim query _ = "SELECT C.STUDENT_ID, S1.SCHOOL_NAME AS CHOICE1, " _ & "S2.SCHOOL_NAME AS CHOICE2 " _ & "FROM CHOICE C " _ & "LEFT OUTER JOIN SCHOOL AS S1 ON S1.SCHOOL_CODE = C.CHOICE1 " _ & "LEFT OUTER JOIN SCHOOL AS S2 ON S2.SCHOOL_CODE = C.CHOICE2 " _ & "ORDER BY C.STUDENT_ID " Dim cmd As New SqlCommand(query, conn) dataReader = cmd.ExecuteReader() End Sub
DataInitializeイベントの処理
DataInitializeイベントでは、「第1志望校」「第2志望校」を格納するためのアンバウンドフィールドを生成します。ここではフィールド名を「SchoolOfChoice1」「SchoolOfChoice2」とします。
private void NewActiveReport1_DataInitialize(object sender, EventArgs e) { this.Fields.Add("SchoolOfChoice1"); this.Fields.Add("SchoolOfChoice2"); }
Private Sub NewActiveReport1_DataInitialize(_ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.DataInitialize Me.Fields.Add("SchoolOfChoice1") Me.Fields.Add("SchoolOfChoice2") End Sub
コードの記述が終わったら、志望校を表示するTextBoxコントロールのDataFieldプロパティに、先ほど定義した「SchoolOfChoice1」「SchoolOfChoice2」を設定します。
FetchDataイベントの処理
FetchDataイベントではDataReaderのReadメソッドを実行して、1件分のデータを取得します。データを取得できた場合は、DataInitializeイベントで生成したアンバウンドフィールドに値を設定します。次のデータがなくなったタイミングでFetchEventArgsのEOFプロパティをtrueとし、FetchDataのループ処理を終わります。
private void NewActiveReport1_FetchData(object sender, FetchEventArgs eArgs) { if (dataReader.Read()) { Fields["SchoolOfChoice1"].Value = dataReader["CHOICE1"].ToString(); Fields["SchoolOfChoice2"].Value = dataReader["CHOICE2"].ToString(); eArgs.EOF = false; //まだ次のデータがある } else { eArgs.EOF = true; //もう次のデータはない } }
Private Sub NewActiveReport1_FetchData( _ ByVal sender As System.Object, _ ByVal eArgs As _ DataDynamics.ActiveReports. ActiveReport3.FetchEventArgs) _ Handles MyBase.FetchData If datareader.Read() Then Me.Fields("SchoolOfChoice1").Value = _ dataReader("CHOICE1").ToString() Me.Fields("SchoolOfChoice2").Value = _ dataReader("CHOICE2").ToString() eArgs.EOF = False 'まだ次のデータがある Else eArgs.EOF = True 'もう次のデータはない End If End Sub
ReportEndイベントの処理
最後に、ReportEndイベントでは、SqlDataReaderとSqlConnectionをクローズします。
private void NewActiveReport1_ReportEnd(object sender, EventArgs e) { if (dataReader.IsClosed) { dataReader.Close(); } if (conn.State != ConnectionState.Closed) { conn.Close(); } }
Private Sub NewActiveReport1_ReportEnd(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.ReportEnd If datareader.IsClosed Then datareader.Close() End If If conn.State <> ConnectionState.Closed Then conn.Close() End If End Sub
DataReader使用時の注意点
DataReaderを使うと、最初にデータを1件取得した時点から帳票生成を開始できるため、高速な帳票生成を実現することができます。
しかし、帳票の生成処理が途中で異常終了するとすべてのデータが揃わず、不完全な帳票が出力されてしまうというデメリットもあります。実際の帳票アプリケーション開発では、別処理で表示するデータの件数をあらかじめ取得しておき、作成された帳票が途中で終了していないかチェックするなどの工夫が必要になります。
また、データアクセスのコードがActiveReportsの複数のイベントにまたがって実行されるため、エラー処理が複雑になりがちです。万が一エラーが発生してもデータベース接続やDataReaderのクローズ、リソースの解放がが確実に実行されるようにしてください。
おわりに
今回はXMLファイルやDataReaderをデータソースとして使用する方法と、ActiveReportsでのグラフ表示について紹介しました。
次回は、今回扱えなかったネットワーク帳票の話題や、ActiveReports帳票の構造の組み方とレスポンスの違いについて紹介する予定です。