LoginSignup
6
6

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

Posted at

目的

実験データの解析ソフトを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で囲い方が微妙に違って

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

とするか

Excelの場合
<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で問題ないでしょう。

MemoryStreamusing 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[]StreamSetDataが共通なので切り出したくなってしまう…)

さいごに

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

参考文献

  1. //lang=htmlなるコメントは,生文字列のハイライティングが実装されてないかなと期待して書いたけどまだだっただけのものです。いつか実装されたときに備えて残してあります。

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

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6