帳票エンジンの仕組み
帳票出力フロー
yagisan-reportsの帳票エンジンは、概念的には以下のように帳票テンプレートを処理して、帳票PDFを出力します。
- レイアウトXMLとスタイルXMLの構文解析を行う。
- レイアウトXMLの構文解析結果にテンプレート変数や制御構造を適用し、Resolved Layoutを生成する。
<Table>の可変行の展開もこのフェーズで行う。
- Resolved LayoutにスタイルXMLの構文解析結果を適用し、Render Treeを生成する。
- スタイル定義が衝突している場合は、優先度に従って解決する。
- Render Treeをもとにレイアウト計算を行い、帳票PDFを生成する。
- グリッドライク要素の罫線の衝突解決や、改ページ制御もこのフェーズで行う。
属性の衝突解決
スタイルXMLがあることで、複数の場所で要素の属性を指定することが可能です。 このため、属性値の指定が衝突することがあります。
衝突解決の基本ルール
属性値の衝突は、基本的には以下の優先度に従って解決します。
- レイアウトXML側の要素に直接指定された属性値
- レイアウトXML側の要素に
style属性で指定されたスタイル要素の属性値 - スタイルXMLで定義された
key属性を持たないスタイル要素の属性値 - デフォルトスタイル(レイアウトXMLの各要素が持つ規定の属性値)
以下の例を考えます。
<LinearLayout>
<LayoutBody>
<Text style="heading" size="30" />
</LayoutBody>
</LinearLayout>
<Style>
<Text fontFamily="mincho" size="10" />
<Text key="heading" size="24" bold="true" />
</Style>
この場合、レイアウトXML側に記述されている <Text style="heading" size="30" /> 要素の属性は、以下のように処理されます。
- fontSize :
30 - fontFamily :
mincho - bold :
true - これ以外の属性は既定値を適用
4値を指定する属性の衝突解決
4値を指定する属性では「未指定」を表す _ が指定可能です。
この場合、衝突解決の際にその値は無視され、次の優先度の値が適用されます。
以下のスタイル定義では、key="customPadding" の padding 属性の1つ目の値が _ となっています。
よって key="customPadding" のスタイルを使用すると、padding 属性には 5 2 3 4 が適用されます。
<Style>
<GridCell padding="5" />
<GridCell key="customPadding" padding="_ 2 3 4" />
</Style>
グリッドライク要素での衝突解決
グリッドライク要素(<Grid> <Table> <ColumnText>)の衝突解決は複雑です。
まず、考慮すべき点としては以下が挙げられます。
- スタイルXMLだけではなく、要素全体とセル個別で罫線や背景色を指定できる。
- 隣接するセルの境界線は定義が衝突する場合がある。
rangeStyle属性(<CellRange><CellRangeList>要素)での指定は最優先で適用する。
これらを満たすように、以下のような順序で衝突解決を行います。
- 属性の衝突解決
- 罫線属性を各セルに反映
- セル境界線の衝突解決
rangeStyle属性の適用
グリッドライク要素の内部表現
帳票エンジンの内部では、グリッドライク要素(<Grid> <Table> <ColumnText>)は、全て <Grid> <GridCell> 要素と同様のものとして扱っています。
<Table>要素<Table>要素は変数を展開した時点で、描画される行数が確定する。- 内部的には
<TableColumnTemplate><TableColumnHeader><TableColumnFooter>要素の定義に従って、静的な<Grid><GridCell>要素相当のデータ構造に変換してから処理している。
<ColumnText>要素<ColumnText>要素は1行の<Grid>としてみなして処理している。- レイアウトXMLの上では
<GridCell>要素に相当する要素は存在しないが、内部的には1桁ごとに<GridCell>要素が存在するものとして扱う。
属性の衝突解決
前述の「属性の衝突解決」で解説した通り、まずは各要素の属性の衝突解決を行います。 これはグリッドライク要素で特別なことはなく、他の要素と同様に処理します。
罫線属性を各セルに反映
グリッドライク要素では、罫線を3系統から指定できるようになっています。 以下の優先度で罫線属性を各セルに反映します。
- セルに直接指定された
borderXxx属性 - グリッドライク要素全体の
outerBorderXxx属性 - グリッドライク要素全体の
borderXxx属性 - デフォルトスタイル(各要素が持つ既定の属性値)
この処理は、「変数の展開や属性の衝突解決が完了した後」に行われることに注意が必要です。
以下のような場合、一見奇妙ですが全てのセルは borderThickness="regular" となります。
<Grid
cols="*"
rows="30"
borderThickness="thick"
outerBorderThickness="extrathick"
>
<GridCell col="0" row="0" />
</Grid>
<Style>
<GridCell borderThickness="regular" />
</Grid>
これは処理をする直前の中間状態が、以下のように「セルで borderThickness 属性が指定されている」状態であるため、これが優先されます。
<Grid
cols="*"
rows="30"
borderThickness="thick"
outerBorderThickness="extrathick"
>
<GridCell col="0" row="0" borderThickness="regular" />
</Grid>
このように key 属性なしの <GridCell> スタイルは、意図せず優先度が高くなってしまうことがあるため、注意が必要です。
セル境界線の衝突解決
属性の衝突解決が完了した時点で、隣り合うセルの罫線の定義が衝突する場合があります。
以下の例では、隣り合うセルで borderThickness 属性が異なっています。
このため、セルの境界線をどちらの太さで描画するかを決定する必要があります。
<Grid cols="* *" rows="30">
<GridCell col="0" row="0" borderThickness="thick" />
<GridCell col="1" row="0" borderThickness="regular" />
</Grid>
帳票エンジンは、セルの境界線の衝突を以下の通りに解決します。
borderThickness属性の太い方を優先する。- 太さが同じ場合は、後から定義されたセルの罫線を優先する。
<Table><ColumnText>要素の場合、下の行・右の列のセルが後から定義されたものとして扱う。
例1 : 隣り合うセルで罫線の太さが異なる
以下のように隣接セル境界で罫線の太さが異なる場合は、太い方を優先します。
<Grid cols="* *" rows="30">
<GridCell col="0" row="0" borderThickness="thick" />
<GridCell col="1" row="0" borderThickness="regular" />
</Grid>
つまり、以下のようにセルの境界線の太さは thick として扱います。
<Grid cols="* *" rows="30">
<GridCell col="0" row="0" borderThickness="thick" />
<GridCell col="1" row="0" borderThickness="regular regular regular thick" />
</Grid>
例2 : 隣り合うセルで罫線の太さは同じだが色が異なる
以下のように隣接セル境界で罫線の太さが同じ場合は、後から定義されたセルの罫線を優先します。
<Grid cols="* *" rows="30">
<GridCell col="0" row="0" borderThickness="thick" borderColor="blue" />
<GridCell col="1" row="0" borderThickness="thick" borderColor="red" />
</Grid>
つまり、以下のようにセルの境界線の色は red として描画します。
<Grid cols="* *" rows="30">
<GridCell col="0" row="0" borderThickness="thick" borderColor="blue red blue blue" />
<GridCell col="1" row="0" borderThickness="thick" borderColor="red" />
</Grid>
rangeStyle 属性の適用
rangeStyle 属性(<CellRange> <CellRangeList> 要素)で指定されたスタイルは、一番優先度が高くなります。
前述までの衝突解決を行った中間状態に対して、rangeStyle 属性で指定されたスタイルを適用します。
以下のルールに従って適用します。
<CellRange>で指定されたセル範囲の属性を上書きする。<CellRange>のセル範囲が重複する場合は、<CellRange>の定義順に従って上書きしていく。
罫線・境界線とレイアウト計算
境界線や罫線を持つコンポーネントでは、線の太さを考慮してレイアウト計算を行います。 基本的な考え方は以下の通りです。
- 境界線(外側の罫線)は、要素のサイズに含めて計算し、外側にはみ出さないようにします。
- 内側の罫線はセル境界を中心に描画し、罫線の太さは常に半分ずつ左右・上下のセルに分配します。
- セルのサイズには、罫線の半分の太さ(上下左右それぞれ
1/2)を含めて計算します。
- セルのサイズには、罫線の半分の太さ(上下左右それぞれ
境界線を持つコンポーネントのレイアウト計算
境界線を持つコンポーネント(グリッドライク要素を除く)では、要素の内側に向かうように線を描画します。
サイズが明示されている場合は、以下のように計算します。
<StackBlock
width="200"
height="100"
borderThickness="1 2 3 4"
padding="1"
>
<!-- コンテンツ省略 -->
</StackBlock>
- 定義から要素の描画サイズを取得
- width : 200
- height : 100
- 罫線とpaddingのサイズを差し引いて、内側の子コンテンツが使える描画領域を算出
- width : 200 - (2 + 4) - (1 + 1) = 192
- height : 100 - (1 + 3) - (1 + 1) = 94
heightが可変の場合は、以下のようにまず幅から計算して、高さを算出します。
<LinearBlock
width="200"
borderThickness="1 2 3 4"
padding="1"
>
<!-- コンテンツ省略 -->
</LinearBlock>
- 定義から要素の描画サイズを取得
- width : 200
- height : 不定
- 子コンテンツを描画に使用できる幅を計算
- width : 200 - (2 + 4) - (1 + 1) = 192
- 子コンテンツの高さを計算
- height : 2で算出した幅(この例では192)に収まるようにコンテンツをレイアウトして、描画に必要な高さを算出
- 3の高さに上下の罫線とpaddingのサイズを加算して、要素の最終的な描画サイズを決定
- width : 200
- height :
子コンテンツの高さ+ (1 + 3) + (1 + 1)
グリッドライク要素のレイアウト計算
グリッドライク要素のレイアウト計算は非常に複雑ですが、基本的には以下のような計算方法で行・列のサイズを決定しています。
- 要素全体の幅から、外枠罫線の左右の太さの半分(セルの外側に描画される分)を差し引いて、セルに割り当て可能な幅を算出
- 1.で算出した幅に対して、先に固定幅の列を割り当て、残りを
*の列に均等に割り当て - 行の高さの算出 : 固定行なら定義された高さを使用し、可変行なら行内の各セルごとに高さを計算して最大値を行の高さとする
- 割り当てられた列幅から左右の罫線の太さの半分(セルの内側に描画される分)と
paddingを差し引いて、セル内の子コンテンツが使用可能な幅を算出 - 算出した幅に収まるように子コンテンツをレイアウトして、描画に必要な高さを算出
- 子コンテンツの高さに上下の罫線の太さの半分(セルの内側に描画される分)と
paddingを加算して、セル描画に必要な高さを算出
- 割り当てられた列幅から左右の罫線の太さの半分(セルの内側に描画される分)と
- 各行の高さを合計し、外枠罫線の上下の太さの半分(セルの外側に描画される分)を加算して、要素全体の最終的な描画高さを決定
実際のレイアウト計算では colspan rowspan 属性の処理や改ページ制御が発生するため、もっと複雑な計算を行っています。
グリッドライク要素のレイアウト計算例を以下に示します。
<Grid
width="300"
cols="100 50 *"
rows="30 auto"
borderThickness="1"
outerBorderThickness="2"
padding="1"
>
<!-- GridCell省略 -->
</Grid>
- セルに割り当て可能な幅の算出
- 300 - ((2 + 2) / 2) = 298
- 列幅の割り当て
- 固定列の幅 : 100 + 50 = 150
*列の幅 : 298 - 150 = 148
- 行の高さの算出
- 1行目 : 固定行のため、30
- 2行目 : 可変行のため、各セルごとに高さを算出して、最大値を採用
- 各セルのコンテンツが使用可能な幅の算出
- 1列目 : 100 - ((2 + 1) / 2) - (1 + 1) = 96.5
- 2列目 : 50 - ((1 + 1) / 2) - (1 + 1) = 47
- 3列目 : 148 - ((1 + 2) / 2) - (1 + 1) = 144.5
- 各セルの子コンテンツの高さを算出
- それぞれ、ch1、ch2、ch3とする
- 各セルの高さを算出
- 1列目 : ch1 + ((2 + 1) / 2) + (1 + 1)
- 2列目 : ch2 + ((1 + 1) / 2) + (1 + 1)
- 3列目 : ch3 + ((1 + 2) / 2) + (1 + 1)
- 各セルのコンテンツが使用可能な幅の算出
- 要素全体の高さの算出
- 30 + 算出した2行目の高さ + ((2 + 2) / 2)