本稿は、連載『5分でわかるActiveReports帳票(2007年度版)』(渡辺俊史・宮本奈紗 著)の増補改訂版です。
セクションレポート:動的な処理をするレポート
本記事の第1回と第3回ではセクションレポートの使い方を紹介しましたが、そこで紹介したレポートはデータベースから取得したデータをそのまま表示するものでした。今回はそのデータを加工し、動的に表示内容を変更する方法を紹介します。
データを加工する方法としては、以下のようなものがあります。
1)TextBoxコントロールのOutputFormatプロパティに書式を設定する
TextBoxコントロールには出力する値の書式を設定するためにOutputFormatプロパティが用意されております。こちらについては第3回の「コラム:書式について」をご覧ください。
2)対象コントロールのDataFieldプロパティに式を記述する
DataFieldプロパティに記述する式を「フィールド式」と言います。
3)ActiveReportsが提供するイベントに、データを制御するコードを記載する
4)計算フィールドを作成し、それをDataFieldプロパティに設定する
「計算フィールド」は、2の「フィールド式」を使った処理を、レポートデータソースに含めるものです。今回は詳しい説明を省略いたしますが、興味がある方は製品ヘルプの[概念]-[セクションレポートの概念]-[データ連結]-[計算フィールド]をご覧ください。
今回は、フィールド式とコードの使い方について解説します。
フィールド式
TextBoxなどのセクションレポートのコントロールには、DataFieldプロパティがあります。通常はここにデータソース内のフィールド名を設定します。
このときはフィールド名そのものではなく、フィールドを使用した式を記述することで、データを加工して表示することができます。
フィールド式の先頭には「=」をつけます。=の後には、フィールドを含めたC#言語構文の「式」を記述します。
例えば、金額を表すフィールドProductSalesがあるとします。これを千円単位で表示したい場合には以下のようにします。
=ProductSales/1000
文字列の加工に使用することもできます。OutputFormatプロパティを使った処理でも加工は可能ですが、フィールド式を使って単位「千円」をつけて表示したい場合、以下のようになります。このときの「+」はC#言語の文字列連結演算子です。
=ProductSales/1000 + "(単位千円)"
C#の式が使用できるので、三項演算子を使って簡単な条件判断を行うことも可能です。例えば、フィールド「ProductSales」の値が10000以上の場合は「一万円以上」、小さい場合は「一万円未満」と表示したい場合、以下のようになります。
=ProductSales => 10000 ? "一万円以上" : "一万円未満"
「フィールド式」と言っていますが、必ずしもフィールドを使用しなければならないわけではなく、.NET Framework標準のクラスも一部使うことができます。例えば、DateTime.Nowプロパティから現在の日付を取得し、AddDaysメソッドで1日追加した日付を表示したい場合、以下のようになります。
=System.DateTime.Now.AddDays(1).ToString("明日はd日")
イベントでのコードによる処理 1
フィールド式だけでもいろいろな編集ができますが、複雑な条件判断を記述するのは現実的ではありません。
セクションレポートにはさまざまなイベントが用意されており、そのイベントにコードを記述することで、レポートの内容を変更できます。
セクションレポートのイベント
Windowsアプリケーションのプログラミングにはイベントを使用します。例えばフォーム全体の処理はForm.Loadイベントで処理するのが一般的です。セクションレポートのイベントもこれと似た感じのものです。
イベントは、レポートの実行と、データの読み込み、セクションの描画に従って発生します。
セクションレポートのイベントには、以下のものがあります。
レポート全体のイベント
ReportStartイベント
レポートを実行した時に、最初に1回だけ発生するイベントです。用紙サイズやデータソースの設定など、レポート全体にかかわる設定はここで行います。
DataInitializeイベント/FetchDataイベント
セクションレポートで使うデータの「フィールド」は、レポートのデータソースを元に自動的に生成されます。DataInitializeイベントは、自動的に生成されるフィールドの他に任意のフィールドを追加する際に使用します。ReportStartイベントの後に1回だけ発生します。
FetchDataイベントは、データソースからデータ行を1行読み込むたびに呼び出されます。ここでフィールドに値を設定することもできます。
DataInitializeイベントとFetchDataイベントを使って、データソースなしでセクションレポートを動作させることができます。この使用法は「アンバウンドレポート」と呼ばれます(詳細は後述します)。
ReportEndイベント
レポート生成処理の最後に1回だけ発生します。レポート処理中に使用した任意のオブジェクトを閉じるなど、主に終了処理に使用します。なお、この段階ではレポートのページがすべて生成されています。従って、このイベントでは、PageクラスのDraw系メソッドを使って、任意のページに文字や線などを直接描画することも可能です。
PageStartイベント/PageEndイベント
各ページの生成の最初と最後に発生するイベントです。
各セクションのイベント
Detailセクション、ページヘッダ、ページフッタ、グループヘッダ、グループフッタなどの各セクションにもイベントが用意されています。これらのイベントは、セクションが1回描画されるごとに、基本的にそれぞれ1回ずつ発生します。
Formatイベント
セクションの生成後に呼び出されるイベントです。セクションや、個々のコントロールの内容を変更する場合、通常はこのイベントを使います。
BeforePrintイベント
セクションの生成後、描画の直前に呼び出されるイベントです。Formatイベントと同様に使用することができます。
FormatイベントとBeforePrintイベントは同じように使われることもありますが、いくつか注意すべき違いがあります。
- セクション自体の高さを変更できるのはFormatイベントだけで、BeforePrintイベントではできない。
- CanGrow/CanShrinkプロパティによるセクション・コントロールの自動伸縮機能は、Formatイベントの後、BeforePrintイベントの前に実行される。このためBeforePrintイベントでコントロールに出力する内容を変更しても自動伸縮は実行されない。
- SummaryFuncプロパティなどによる集計結果は、BeforePrintイベントの段階でないと取得できない。
AfterPrintイベント
セクションの生成が終わった後に呼び出されるイベントです。
イベントを追加する方法
イベントを追加する方法を説明します。
デザイン画面で、イベントを使用したいセクション上でダブルクリックすると、.vbや.csのコードファイルに、Formatイベントのスタブが自動生成されます。
生成されるFormatイベントのスタブ
Private Sub Detail_Format(sender As System.Object, e As System.EventArgs) Handles Detail.Format End Sub
private void detail_Format(object sender, EventArgs e) { }
生成されるイベントハンドラ名は、イベントのセンダ(この場合はセクション)の名前と、イベント名をアンダーラインでつないだものになります。Windowsアプリケーションでフォームやコントロールにイベントを追加する場合と同様です。
セクションを選択していない状態でデザイン画面をダブルクリックすると、ReportStartイベントのスタブが生成されます。
Format、ReportStart以外のイベントを生成したい時には、プロパティウィンドウのイベント画面からイベントを追加します。
Visual Basic .NETでは、引数の後にあるHandles句によってイベントハンドラの登録が行われます。C#では.Designer.csファイルの中にイベントハンドラを登録するコードが追加されます。このためC#の場合は、サンプルコードをそのままコードに記述してもイベントとして働きません。注意してください。
イベントでのコードによる処理 2
イベント使用の実例
ここからは、イベントを使用した処理の例をいくつか見てみましょう。
コントロールの表示を変更する
表示されるフィールドの値をコードで加工します。
例えば、誕生日の情報から「干支」を表示する処理を以下に示します。
この時に注意すべき点としては、データソースの値を取得したい場合、フィールドを直接参照するのではなく、フィールドがバウンドされたコントロールを介して行う必要があるという点です。
以下の例では、TextBoxコントロール「txtDateOfBirth2」のDataFieldプロパティに参照したい日付型のフィールドを設定している前提です。バウンドされたコントロールのValueプロパティには元のデータの値が、Textプロパティにはフォーマットされた文字列が入っています。ValueプロパティはObject型なので、使用するときは型変換が必要です。
Private Sub Detail1_Format(sender As System.Object, e As System.EventArgs) Handles Detail1.Format Dim eto() As String = {"子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"} Dim birthDay As DateTime birthDay = CType(Me.txtDateOfBirth2.Value, DateTime) Dim year As Integer = birthDay.Year Me.txtDateOfBirth2.Text = eto((year - 1900) Mod eto.Length) & "年" End Sub
private void Detail1_Format(object sender, EventArgs e) { string[] eto = { "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥" }; DateTime birthDay; birthDay = (DateTime)this.txtDateOfBirth2.Value; int year = birthDay.Year; this.txtDateOfBirth2.Text = eto[(year - 1900) % eto.Length] + "年"; }
コントロールの色を変える
フィールドの値に応じてコントロールの描画色を変えます。以下のコードでは、TextBoxコントロール「txtGender1」に性別を表す値を表示することとし、男性なら表示文字を青色、女性なら表示文字を赤色にしています。
Private Sub Detail1_Format(sender As System.Object, e As System.EventArgs) Handles Detail1.Format Dim gender As String gender = Me.txtGender1.Text If gender = "M" Then Me.txtGender1.Text = "男性" Me.txtGender1.ForeColor = Color.Blue End If If gender = "F" Then Me.txtGender1.Text = "女性" Me.txtGender1.ForeColor = Color.Red End If End Sub
private void Detail1_Format(object sender, EventArgs e) { string gender; gender = this.txtGender1.Text; if (gender == "M") { this.txtGender1.Text = "男性"; this.txtGender1.ForeColor = Color.Blue; } if(gender == "F"){ this.txtGender1.Text = "女性"; this.txtGender1.ForeColor = Color.Red; } }
改ページ制御をする
セクションのNewPageプロパティを制御することで、改ページを制御することができます。以下のコードでは、1ページに出力する行数(Detailセクションの数)を10行にしています。
Dim RowNumber As Integer = 0 ' 件数カウンタ Private Sub Detail1_Format(sender As System.Object, e As System.EventArgs) Handles Detail1.Format RowNumber += 1 If RowNumber < 10 Then ' 件数が10件に満たない場合、改ページは行いません。 Me.Detail1.NewPage = NewPage.None Else ' 10件出力した後、改ページを行い、カウンタをリセットします。 Me.Detail1.NewPage = NewPage.After RowNumber = 0 End If End Sub
private int RowNumber = 0; private void Detail1_Format(object sender, EventArgs e) { RowNumber++; if (RowNumber < 10) { // 件数が10件に満たない場合、改ページは行いません。 this.Detail1.NewPage = NewPage.None; } else { // 10件出力した後、改ページを行い、カウンタをリセットします。 this.Detail1.NewPage = NewPage.After; RowNumber = 0; } }
1行ごとに色を変える
以下のコードでは、DetailセクションのBackColorプロパティを1行ごとに変えています。
Dim ColorFlag As Boolean = False Private Sub Detail1_Format(sender As System.Object, e As System.EventArgs) Handles Detail1.Format If ColorFlag = True Then Me.Detail1.BackColor = Color.DarkSeaGreen Else Me.Detail1.BackColor = Color.Transparent End If Me.ColorFlag = Not Me.ColorFlag End Sub
private Boolean ColorFlag = false; private void Detail1_Format(object sender, EventArgs e) { if (this.ColorFlag) { this.Detail1.BackColor = Color.DarkSeaGreen; } else { this.Detail1.BackColor = Color.Transparent; } ColorFlag = !ColorFlag; }
ページに直接描画をする
PageクラスにはDrawText、DrawLineなどのメソッドが用意されており、コードで文字列や線などを描画することができます。
以下のコードでは、ReportEndイベントの中で、レポートを通してページ番号を描画しています。
Private Sub ProductList_ReportEnd(sender As System.Object, e As System.EventArgs) Handles MyBase.ReportEnd For i As Integer = 0 To Me.Document.Pages.Count - 1 Dim p As GrapeCity.ActiveReports.Document.Section.Page p = Me.Document.Pages(i) ' 描画する領域 Dim rect As New RectangleF(0.5, 0.5, 1, 0.2) ' 描画に必要な設定をしておく p.BackColor = Drawing.Color.Transparent p.PenStyle = GrapeCity.ActiveReports.Document.Section.PenStyles.Solid p.TextAlignment = GrapeCity.ActiveReports.Document.Section.TextAlignment.Center ' ページ数を描画する p.DrawText((i + 1) & "ページ", rect) ' 枠を描画する p.DrawRect(rect) Next End Sub
private void ProductList_ReportEnd(object sender, EventArgs e) { for (int i = 0; i < this.Document.Pages.Count; i++) { GrapeCity.ActiveReports.Document.Section.Page p; p = this.Document.Pages[i]; RectangleF rect = new RectangleF(0.5f, 0.5f, 1f, 0.2f); // 描画に必要な設定をしておく p.BackColor = Color.Transparent; p.PenStyle = GrapeCity.ActiveReports.Document.Section.PenStyles.Solid; p.TextAlignment = GrapeCity.ActiveReports.Document.Section.TextAlignment.Center; // ページ数を描画する p.DrawText((i + 1) + "ページ", rect); // 枠を描画する p.DrawRect(rect); } }
イベントではできないこと
コードにはさまざまな処理を自由に実装できるため、あらゆる制御が可能である印象も受けますが、セクションレポートのイベントによる処理にはいくつか制約があります。主な制約としては、以下のようなものがあります。
- セクションのイベント(Format、BeforePrintなど)では、そのセクション上のコントロール以外にはアクセスしてはいけません。例えば、Detailセクションのイベントからページヘッダ上のコントロールの値を変更するのはご法度です。このような実装をすると、目的とは違うページのページヘッダの値が変更される場合があります。
イベントはセクションの配置順に起動されるとは限りません。Detailセクションのイベントと、グループヘッダ/フッタのイベントや、ページヘッダ/フッタのイベントでは、順序が前後する(配置順と逆になる)場合があります。イベントの発生順に依存するような処理を実装することはおすすめできません。
- 「セクションがページのどの位置に描画されているか」を検知することは原則的にはできません。そのため、例えば「セクションがページの一番下に描画される時だけ描画内容を変更する」という動作は実現できません。
これらの注意事項について、詳しくは製品ヘルプの[よくある質問]-[セクションレポート]-[イベント]をご参照ください。
データソースの変更
セクションレポートでは、デザイン時にデータソースアイコンからデータソースの設定を行うことが可能です(第1回参照)。
しかしながら、実際のアプリケーションでは、レポートのデータソースは出力したい条件にあわせて変更したいケースが多いと思われます。
セクションレポートでは、DataSourceプロパティにデータソースとなるオブジェクトを設定することで、簡単にデータソースを変更することができます。
データソースとして使用できるオブジェクトには、以下のようなものがあります。
- GrapeCity.ActiveReports.Data名前空間のデータソースクラス
- DataSet
- DataTable
- DataView
- DataRowCollection
- DataReader
- IListインターフェースを実装したクラス
これらをレポートのDataSourceプロパティに設定してから、レポートの実行(Runメソッドの呼び出しなど)を行います。
一例として、OleDbDataConnection、OleDbDataAdapterクラスを利用してAccessのmdbファイルに接続してDataTableを取得し、レポートに設定するコードを示します。
Dim filepath As String = "(ファイルへのパス)\Accessファイル.mdb" Dim cn As New OleDb.OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & filepath) Dim sqlstring As String = "Select * from Products order by CategoryID" Dim da As New OleDb.OleDbDataAdapter(sqlstring, cn) Dim ds As New DataSet Dim dt As New DataTable da.Fill(dt) ‘ 得られたDataTableをレポートに設定して実行 Dim rpt As New NewReport1() rpt.DataSource = dt rpt.Run()
string filePath = @"(ファイルへのパス)\Accessファイル.mdb"; System.Data.OleDb.OleDbConnection cn = new System.Data.OleDb.OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + filePath); string sqlString = "Select * from Products order by CategoryID"; System.Data.OleDb.OleDbDataAdapter da = new System.Data.OleDb.OleDbDataAdapter(sqlString, cn); System.Data.DataTable dt = new System.Data.DataTable(); da.Fill(dt); // 得られたDataTableをレポートに設定して実行 NewReport rpt = new NewReport(); rpt.DataSource = dt; rpt.Run();
Accessのmdbファイルも含め、データベースに接続する場合には接続文字列が必要になります。これはActiveReportsの組み込みデータソースを使う場合も同じです。上記の例では、OleDbConnectionのコンストラクタの引数に設定している文字列が接続文字列になります。
接続文字列はデータソースの種類ごとに仕様が異なるので、一から記述するのは難しい時があります。簡単に構成する方法としては、System.Data.OleDb.OleDbConnectionStringBuilderなどの接続文字列ビルダを使う方法があります。
また、ActiveReportsのデータソースアイコンで自動作成された接続文字列をコピーして使うという方法もあります。
アンバウンドレポート
レポートの要件によっては、DataTableなどのデータソースを使わずにレポートを生成しなければならない場合もあります。このような場合、「アンバウンドレポート」という手法が有効です。
デザイン時にデータソースアイコンからデータソースを設定した場合や、前述のようにDataSourceプロパティにデータオブジェクトを設定した場合、データソースへの接続やレコードの読み込み・移動などは、レポートエンジンがすべて自動的に行います、これを「バウンドレポート」と言います。
これに対して、アンバウンドレポートの場合、フィールドの追加やデータソースへの接続、レコードの移動、データのセット等の処理は、すべてコードで制御します。この手法は、テキストファイルや、配列に格納されたデータなど、さまざまなデータを元にレポートを生成するといった用途に向いています。
アンバウンドレポートの基本的な構成手順は、以下のようになります。
(1)DataInitializeイベントで、必要なフィールドを追加する。
(2)FetchDataイベントで、レポートの生成を続けるかどうかを、イベントの引数であるSectionReport.FetchEventArgsオブジェクトのEOFプロパティで設定する。同時に(1)で追加したフィールドに値を設定する。
簡単な例として、10行のみのデータを与えるレポートを以下に示します。
Private Sub SimpleReport_DataInitialize(sender As System.Object, e As System.EventArgs) Handles MyBase.DataInitialize ‘ フィールドを追加する。 Me.Fields.Add("Field1") Me.Fields.Add("Field2") End Sub Private Sub SimpleReport_FetchData(sender As System.Object, eArgs As GrapeCity.ActiveReports.SectionReport.FetchEventArgs) Handles MyBase.FetchData Static count As Integer = 0 Const limit As Integer = 10 count += 1 If count > limit Then ‘ レポートの終了 eArgs.EOF = True Else eArgs.EOF = False ' フィールドに値を設定する。 Me.Fields("Field1").Value = count Me.Fields("Field2").Value = "No." & count End If End Sub
private int count = 0; const int limit = 10; private void SimpleReport_DataInitialize(object sender, EventArgs e) { // フィールドを追加する。 this.Fields.Add("Field1"); this.Fields.Add("Field2"); } private void SimpleReport_FetchData(object sender, FetchEventArgs eArgs) { this.count++; if (count > limit) { // レポートの終了。 eArgs.EOF = true; } else { eArgs.EOF = false; // フィールドに値を設定する。 this.Fields["Field1"].Value = count; this.Fields["Field2"].Value = "No." + count; } }
XML形式のレポート(RPXファイル)
ActiveReportsのレポートは、XML形式のファイルとして定義することも可能です。この形式のファイルは、デフォルトの拡張子が「.rpx」なので、「RPXファイル」とも言われます。
この形式の場合、.vbファイルや.csファイルのコードを直接使用することはできません。この形式でコードによる処理を実装したい場合には、「スクリプト」を使用します。
スクリプトは、レポート編集画面のスクリプトタブから記述することができます。また、レポート全体のプロパティのScriptLanguageプロパティで、VBかC#のどちらの言語で記述するか選択できます。
スクリプトを使用する場合も、.vbや.csのコードファイルを使用する場合と同様に、イベントの内部に処理を記述します。
なお、スクリプトの記述方法は、.vbや.csファイルと基本的に同様であり、インテリセンスなども表示されますが、レポート全体のプロパティにアクセスするためには、「Me」や「this」ではなく識別子「rpt」を使う必要があるなど、いくつかの注意点があります。これについては製品ヘルプの[概念]-[セクションレポートの概念]-[スクリプト]を参照してください。
RPXファイルをViewerコントロールに表示するコードは、以下のようになります。
Dim rpt As New GrapeCity.ActiveReports.SectionReport rpt = New GrapeCity.ActiveReports.SectionReport Dim xtr As New System.Xml.XmlTextReader(RPXファイルのパス) rpt.LoadLayout(xtr) rpt.Run() Me.Viewer1.Document = rpt.Document
GrapeCity.ActiveReports.SectionReport rpt = new GrapeCity.ActiveReports.SectionReport(); // コードを実行する前に、report.rpxをプロジェクトのbin\debugフォルダに配置します。 System.Xml.XmlTextReader xtr = new System.Xml.XmlTextReader(RPXファイルのパス); rpt.LoadLayout(xtr); xtr.Close(); rpt.Run(); viewer1.Document = rpt.Document;
まとめ
セクションレポートでは、コードやスクリプトを使用することで、さまざまな処理を実装することができ、複雑なレイアウトを実現できます。
次回はページレポートのMatrixの使い方について説明します。