【C#】クリップボードに表を入れたい


# 目的

実験データの解析ソフトをC#で書いておりまして(C#でやることではない)，出てきたパラメータの表をExcelやら何やらに貼り付けられるようにクリップボードに放り込もうと思いました。
Excelの方は割と簡単でタブ区切り(カンマではない)文字列を`TextDataFormat.CommaSeparatedValue`として入れてあげると表として読んでくれます。
ところが，これをPowerPointに貼り付けようとすると表ではなくタブで区切られた文字列としてテキストボックスに入れられてしまいます。
これではあまり美しくないので，いい感じに表として渡せるようにしてみました。

# コード

```cs:シンタックスハイライト頑張れ
// using System.IO;
// using System.Text;
// using System.Windows.Forms;

// Mainに [STAThread] 必須

var table_rows =
    dataGridView.Rows
                .Select((row, i) => $"<tr id=\"row-{i+1}\"><td>{row.Cells[0].Value}</td><td>{row.Cells[1].Value}</td></tr>");
                
//lang=html
var html = $$"""
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <style>
        table { border-collapse:collapse; font-family:Arial; }
        tr th, tr td { vertical-align:middle; text-align:right; padding-right:25px; }
        #row-header th { border-top:1.0pt solid black; border-bottom:1.0pt solid black; }
        #row-{{table_rows.Count()}} td { border-bottom:1.0pt solid black; }
    </style>
</head>
<body>
<!--StartFragment-->
<table>
<tr id="row-header">
    <th>Value 1</th>
    <th>Value 2</th>
</tr>
{{string.Join(string.Empty, table_rows)}}
</table>
<!--EndFragment-->
</body>
</html>
""";

var startHtml = 97;
var endHtml = startHtml + html.Length;
var startFragment = startHtml + html.IndexOf("<!--StartFragment-->");
var endFragment = startHtml + html.IndexOf("<!--EndFragment-->");
var metadata = $"""
    Version:1.0
    StartHTML:{startHtml:00000000}
    EndHTML:{endHtml:00000000}
    StartFragment:{startFragment:00000000}
    EndFragment:{endFragment:00000000}

    """;

var data = new DataObject();
var bytes = Encoding.UTF8.GetBytes(metadata + html);
using var stream = new MemoryStream(bytes);
data.SetData(DataFormats.Html, stream);
Clipboard.SetDataObject(data, true);
```

Qiitaのシンタックスハイライティングが[生文字列の補間](https://ufcpp.net/study/csharp/st_string.html?p=2#raw-string)を解さないようでバグっていますが，コピペしたのでクォーテーションの数などは合っているはずです[^1]。

[^1]: `//lang=html`なるコメントは，[生文字列のハイライティング](https://ufcpp.net/blog/2022/2/embedded-languages/)が実装されてないかなと期待して書いたけどまだだっただけのものです。いつか実装されたときに備えて残してあります。

# 解説

## HTML

表はHTMLとして渡してあげます。
表の準備についてほとんど本質的ではないので，それぞれのケースに応じて良い感じにしていただければ結構です。
必要に応じてエスケープしてあげましょう(`System.Web.HttpUtility.HtmlEncode`)。

注意すべき点として，貼り付け時に見て欲しい部分を`<!--StartFragment-->`と`<!--EndFragment-->`で囲う必要があります。
今回の例では`<table>`全体を囲っています。
どうやらWord, PowerPointとExcelで囲い方が微妙に違って

```html:Word, PowerPointの場合
<!--StartFragment-->
<table>
    <!-- なかみ -->
</table>
<!--EndFragment-->
```

とするか

```html:Excelの場合
<table>
    <!--StartFragment-->
    <!-- なかみ -->
    <!--EndFragment-->
</table>
```

とするかが異なるようですが，どちらでも問題はありません。

なお，`<head>`タグは不要で，スタイルなどを当てる必要もないのであれば`<html>`の中に`<body>`だけ書いておけばOKです。
スタイルについては，何故か`:first-child`などの疑似クラスが効かなかったので脳筋解決になっています。

## メタデータ

HTMLだけ渡すのでよければ何も難しくないのですが，メタデータ[^2]も用意する必要があり，ここが少しめんどくさい点になります。

[^2]: 本来なら「ヘッダー」などと呼ぶべきなのでしょうが，`<head>`とか`<thead>`とかが出てきそうな文脈でこれはややこしいかなと思いこのように呼んでいます。正式な用語は'description header'。

内容はだいたい名前の通りで，メタデータも含めた全体の中での先頭からのオフセットバイト数になります。

| 値            | 内容 |
|:--------------|:--- |
| Version       | クリップボードのバージョン。`0.9`で始まってWindows 10 20H2で`1.0`となったらしい。 |
| StartHTML     | HTMLがどこから始まるか。メタデータの長さとも言える。 |
| EndHTML       | HTMLがどこで終わるか。要するに全体の長さ。 |
| StartFragment | `<!--StartFragment-->`の開始位置 |
| EndFragment   | `<!--EndFragment-->`の開始位置 |

コードを見てもらうとわかると思いますが，それぞれの値は(`Version`は別として)コンテンツの長さに応じて計算していますが，`StartHTML`だけは固定値としています。
これは，各要素の先頭からの位置が，メタデータの長さが決まらないと分からないのに対して，メタデータの長さはそれぞれの位置が何桁になるかわからないと決まらないためです。
そこで，全体の長さが8桁を超えることはなかろうという仮定の下に全ての値を8桁で書くと決めてしまい，メタデータ長を固定しています。
(Office製品では8桁では不安なのか10桁固定でStartHTML=105としています)
なお，`StartHTML:00000097`などと0で埋めることでメタデータ長を固定していますが，頭に0がついていても8進数にはならず10進数として解釈されます。

## コピー

`DataObject`を作成し，バイト列を入れた上でクリップボードに食わせます。(書いてある通り)
`Clipboard.SetDataObject`の第2引数`true`はアプリケーション終了後にもクリップボードの内容を保持するかを示す値で，よほど巨大なデータでもなければ`true`で問題ないでしょう。

`MemoryStream`は`using var`で宣言することで確実に破棄されるようにしていますが，破棄されるのが`Clipboard.SetDataObject`の後であれば問題ありません。
これを，例えば

```cs:良くない例
var data = new DataObject();
void AddData(string format, string text)
{
    var bytes = Encoding.UTF8.GetBytes(text);
    using var stream = new MemoryStream(bytes);
    data.SetData(format, stream);
}
AddData(DataFormats.Html, metadata + html);
Clipboard.SetDataObject(data, true);
```

などとしてしまうと，`AddData`を抜けた時点(`Clipboard.SetDataObject`の前!)で`stream`が破棄されてしまい，クリップボードに正しく値が格納されなくなってしまいます。

## その他

これだけでも当初の目的は果たせているのですが，タブ区切り(何度も言いますがカンマ区切りではない)文字列も作ってあげて

```cs
var tabSeparated = "hoge\tfuga";
data.SetData(DataFormats.UnicodeText, tabSeparated);

var ts_bytes = Encoding.UTF8.GetBytes(tabSeparated);
using var ts_stream = new MemoryStream(ts_bytes);
data.SetData(DataFormats.CommaSeparatedValue, ts_stream);
```

なども加えてあげると，メモ帳にも貼り付けられるようになります。
(このときに`byte[]`→`Stream`→`SetData`が共通なので切り出したくなってしまう…)

# さいごに

ここで紹介した方法を少しいじれば表だけでなく箇条書き(`<ul>`)などもクリップボードに入れられるようです。
なんだかとてもめんどくさいですね……

# 参考文献

- [HTML Clipboard Format](https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format)