目的
実験データの解析ソフトをC#で書いておりまして(C#でやることではない),出てきたパラメータの表をExcelやら何やらに貼り付けられるようにクリップボードに放り込もうと思いました。
Excelの方は割と簡単でタブ区切り(カンマではない)文字列をTextDataFormat.CommaSeparatedValue
として入れてあげると表として読んでくれます。
ところが,これをPowerPointに貼り付けようとすると表ではなくタブで区切られた文字列としてテキストボックスに入れられてしまいます。
これではあまり美しくないので,いい感じに表として渡せるようにしてみました。
コード
// 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のシンタックスハイライティングが生文字列の補間を解さないようでバグっていますが,コピペしたのでクォーテーションの数などは合っているはずです1。
解説
HTML
表はHTMLとして渡してあげます。
表の準備についてほとんど本質的ではないので,それぞれのケースに応じて良い感じにしていただければ結構です。
必要に応じてエスケープしてあげましょう(System.Web.HttpUtility.HtmlEncode
)。
注意すべき点として,貼り付け時に見て欲しい部分を<!--StartFragment-->
と<!--EndFragment-->
で囲う必要があります。
今回の例では<table>
全体を囲っています。
どうやらWord, PowerPointとExcelで囲い方が微妙に違って
<!--StartFragment-->
<table>
<!-- なかみ -->
</table>
<!--EndFragment-->
とするか
<table>
<!--StartFragment-->
<!-- なかみ -->
<!--EndFragment-->
</table>
とするかが異なるようですが,どちらでも問題はありません。
なお,<head>
タグは不要で,スタイルなどを当てる必要もないのであれば<html>
の中に<body>
だけ書いておけばOKです。
スタイルについては,何故か:first-child
などの疑似クラスが効かなかったので脳筋解決になっています。
メタデータ
HTMLだけ渡すのでよければ何も難しくないのですが,メタデータ2も用意する必要があり,ここが少しめんどくさい点になります。
内容はだいたい名前の通りで,メタデータも含めた全体の中での先頭からのオフセットバイト数になります。
値 | 内容 |
---|---|
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
の後であれば問題ありません。
これを,例えば
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
が破棄されてしまい,クリップボードに正しく値が格納されなくなってしまいます。
その他
これだけでも当初の目的は果たせているのですが,タブ区切り(何度も言いますがカンマ区切りではない)文字列も作ってあげて
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>
)などもクリップボードに入れられるようです。
なんだかとてもめんどくさいですね……
参考文献
-
//lang=html
なるコメントは,生文字列のハイライティングが実装されてないかなと期待して書いたけどまだだっただけのものです。いつか実装されたときに備えて残してあります。 ↩ -
本来なら「ヘッダー」などと呼ぶべきなのでしょうが,
<head>
とか<thead>
とかが出てきそうな文脈でこれはややこしいかなと思いこのように呼んでいます。正式な用語は'description header'。 ↩