HTMLのtable要素を作るFunction(2)

HTMLのtable要素を作成するマクロの改良

前回の

akashi-keirin.hatenablog.com

こいつを改良してみた。

行・列結合への対応

HTMLのtable要素で、行結合とか列結合をする場合は、タグとかタグの中に、それぞれ「rowspan="2"」とか、「colspan="3"」などと書き込むだけで良いので、そんなに難しくないのではないか、と思った。

確認

まず、Excelのシート上に

f:id:akashi_keirin:20180114204722j:plain

こんな表を作ってみた。

んで、次のようなコードを書いて、それぞれのセルの値がどのように取得されるのか確認してみた。

リスト1 標準モジュール
Public Sub testMethod(targetRange As Range)
  Dim Sh As Worksheet
  Set Sh = targetRange.Parent
  Dim startRow As Integer
  Dim startColumn As Integer
  Dim endRow As Integer
  Dim endColumn As Integer
  With targetRange
    With .Cells(1, 1)
      startRow = .Row
      startColumn = .Column
    End With
    endRow = startRow + .Rows.Count - 1
    endColumn = startColumn + .Columns.Count - 1
  End With
  Dim i As Integer
  Dim j As Integer
  Dim str As String
  Dim tmp As String
  For i = startRow To endRow
    str = ""
    For j = startColumn To endColumn
      With Sh.Cells(i, j)    '……(1)'
        If .Value = "" Then    '……(2)'
          tmp = "*"
        Else
          tmp = .Value
        End If
        If j <> endColumn Then    '……(3)'
          str = str & tmp & " | "
        Else
          str = str & tmp
        End If
      End With
    Next
    Debug.Print str    '……(4)'
  Next
End Sub

えらくタテ長になってしまった。

簡単なコードなので、説明は最小限にとどめる。

(1)からの6行

With Sh.Cells(i, j)    '……(1)'
  If .Value = "" Then    '……(2)'
    tmp = "*"
  Else
    tmp = .Value
  End If
'……(中略)……'
End With

では、対象の表の各セルの値について調べ、(2)の

If .Value = "" Then
  tmp = "*"
Else
  tmp = .Value
End If

で、値がなかったら変数tmpに「*」をぶち込み、値が入っていたら変数tmpにその値をぶち込むようにしている。

(3)の

If j <> endColumn Then
  str = str & tmp & " | "
Else
  str = str & tmp
End If

では、最終列に到達するまでは、変数strにtmpの値と「 | 」を付け加え、最終列に到達したときだけ「 | 」を付け加えないようにしている。まあ、ここは、別に最後に右端の「 | 」を取り除くような処理でも良いと思う。

内側のForループ(イテレータが j の方)を抜けたら、1行分完成ということなので、(4)の

Debug.Print str

でstrの内容、すなわち1行分の要素をイミディエイトに書き出す。

ちなみに、strの内容は、外側のForループ先頭で""に初期化される。

こいつを実行してみる。

f:id:akashi_keirin:20180114204730j:plain

これを見ると、「b」の下2つと「o」の右1つが「*」になっていることが分かる。

つまり、結合されたセルに含まれるセルの値を参照すると、TopLeftのセル以外は全て「""」を返しているということだ。

んで、結合されたセルをアクティブにして、イミディエイトでいくつか確認してみる。

f:id:akashi_keirin:20180114204838j:plain

?activecell.MergeArea.Columns.Count
1
?activecell.MergeArea.Rows.Count
3
?activecell.MergeCells
True
?activecell.MergeArea.Count
3

要するに、

  • MergeCellsプロパティがTrueで、Valueが""でないセルに当たったら、
  • rowspanを「="[MergeArea.Rows.Countプロパティの値]"」にして、
  • colspanを「="[MergeArea.Columns.Countプロパティの値]"」にし、
  • MergeCellsプロパティがTrueで、Valueが""のセルに当たったら、
  • rowspanとcolspanを「=""」にしたらよい

ということだ。

ちょいと条件が複雑だけれど、まあ簡単な方だと思う。

コードの改変

前回のリスト1を改変する。

スト2 標準モジュール
Public Function createHTMLTable( _
                  ByVal targetRange As Range, _
                  ByVal captionText As String, _
                  Optional ByVal hasHeader As Boolean = True, _
                  Optional ByVal hasBorder As Boolean) _
                    As String
  Dim rowCount As Long
  rowCount = targetRange.Rows.Count
  Dim columnCount As Long
  columnCount = targetRange.Columns.Count
  Dim i As Long
  Dim j As Long
  Dim stringOfCell() As String
  ReDim stringOfCell(0 To rowCount - 1, 0 To columnCount - 1)
  For i = 0 To rowCount - 1
    For j = 0 To columnCount - 1
      stringOfCell(i, j) = targetRange.Cells(i + 1, j + 1).Value
    Next
  Next
  Dim str As String
  Dim rowSpan As String
  Dim colSpan As String
  If hasBorder Then
    str = "<table border=""1"">"
  Else
    str = "<table border="""">"
  End If
  If Not IsMissing(captionText) Then _
    str = str & "<caption>" & captionText & "</caption>"
  For i = 1 To rowCount
    str = str & "<tr>"
    For j = 1 To columnCount
      With targetRange.Cells(i, j)                       '……(1)'
        If stringOfCell(i - 1, j - 1) <> "" And _
           .MergeCells Then                              '……(2)'
          rowSpan = CStr(.MergeArea.Rows.Count)          '……(3)'
          colSpan = CStr(.MergeArea.Columns.Count)
        Else
          rowSpan = ""                                   '……(4)'
          colSpan = ""
        End If
      End With
      If hasHeader And i = 1 Then
        str = str & "<th rowspan=""" & rowSpan & _
                       """ colspan=""" & colSpan & """>" & _
                       stringOfCell(i - 1, j - 1) & "</th>"    '……(5)'
      Else
        str = str & "<td rowspan=""" & rowSpan & _
                       """ colspan=""" & colSpan & """>" & _
                       stringOfCell(i - 1, j - 1) & "</td>"
      End If
    Next
    str = str & "</tr>"
  Next
  str = str & "</table>"
  str = Replace(str, " rowspan=""""", "")          '……(6)'
  str = Replace(str, " rowspan=""1""", "")
  str = Replace(str, " colspan=""""", "")
  str = Replace(str, " colspan=""1""", "")
  str = Replace(str, "<th></th>", "")
  str = Replace(str, "<td></td>", "")
  str = Replace(str, vbLf, "<br>")
  createHTMLTable = str
End Function

後から機能を追加するような形になったので、ややこしくなってしまったことは堪忍してほしい。

またヒマなときにリファクタリングは必要だと思っている。

まず、(1)からの10行(実質9行)

With targetRange.Cells(i, j)
  If stringOfCell(i - 1, j - 1) <> "" And _
    .MergeCells Then                              '……(2)'
    rowSpan = CStr(.MergeArea.Rows.Count)          '……(3)'
    colSpan = CStr(.MergeArea.Columns.Count)
  Else
    rowSpan = ""                                   '……(4)'
    colSpan = ""
  End If
End With

では、そのセルが結合されているかどうかを調べ、さらにそのセルが値を持っていたら(つまり結合セルのTopLeftだったら)変数rowSpanとcolSpanに設定すべき値をぶち込み、値を持っていなかったら変数rowSpanとcolSpanに「""」をぶち込んでいる。

(2)の

If stringOfCell(i - 1, j - 1) <> "" And _
  .MergeCells Then

の条件式がTrueになるのは、

結合されたセルであり、かつ値を持っている場合

なので、(3)の

rowSpan = CStr(.MergeArea.Rows.Count)
colSpan = CStr(.MergeArea.Columns.Count)

で変数rowSpanとcolSpanに値をString型に変換してぶち込んでおく。

それ以外の場合(条件式がFalseになる場合)は、(4)の

rowSpan = ""
colSpan = ""

で変数の値を「""」にしておく。

これで、たとえば

2行3列の結合セルのTopLeft(左上)のセル

に行き当たった場合は、

<td rowspan="2" colspan="3">hogehoge</td>

となれば良いし、TopLeftのセル以外ならば

<td rowspan="" colspan="">fugafuga</td>

となれば良いわけだ。

そのために、たとえば(5)の

str = str & "<th rowspan=""" & rowSpan & _
            """ colspan=""" & colSpan & """>" & _
            stringOfCell(i - 1, j - 1) & "</th>"

のような形で文字列を組み立てる。「"(ダブルクォーテーション)」のエスケープがあるのでちょっと分かりづらいな。

あとは(6)からの5行で仕上げ。

str = Replace(str, " rowspan=""""", "")
str = Replace(str, " colspan=""""", "")
str = Replace(str, "<th></th>", "")        '……(*)'
str = Replace(str, "<td></td>", "")
str = Replace(str, vbLf, "<br>")

Replace関数を用いて、不要な文字列を削除する。

「rowspan=""」とか「colspan=""」はなくても良いわけだから、削除する。あったらむやみやたらと長くなるし。

(*)からの2行

str = Replace(str, "<th></th>", "")
str = Replace(str, "<td></td>", "")

では、「<th></th>」と「<td></td>」を削除している。

これをしないと、

f:id:akashi_keirin:20180114204740j:plain

こんなふうになってしまう(理由は自分で考えよう!)。

最後にセル内改行を「<br>」に置き換えるのは前回同様。

実行結果

次のコードで実行。

リスト3
Public Sub testHTMLTable()
  Dim Sh As Worksheet
  Set Sh = ThisWorkbook.Worksheets("Sheet2")
  Debug.Print createHTMLTable(targetRange:=Sh.Range("A9").CurrentRegion, _
                              captionText:="ち~んw", _
                              hasHeader:=True, _
                              hasBorder:=True)
End Sub

イミディエイトに出力された文字列をHTMLソースに貼り付けて、ブラウザで開くと、

f:id:akashi_keirin:20180114204904j:plain

ほれ、この通り、うまく行った。

もうちょっとややこしい表でも実験。

f:id:akashi_keirin:20180114204914j:plain
しまった……。「常荷金」だよ……。

こんな表を作って、同じように実行してみた。

f:id:akashi_keirin:20180114204922j:plain

こんな風になります。

おわりに

何やってんだろ、オレ……orz

@akashi_keirin on Twitter