あらかじめ日記

2chまとめサイト「ソノウソホント?」の雑記と、ブログで使えるスクリプトとかテクニックとか。その他、音楽やアニメ、漫画の話題とかも。

まとめサイト「ソノウソホント?」と「ムードもりあげ楽団」はこちら。

【C#】WebBrowser上の選択範囲の文字列位置と、HTML(ソース)の位置を見つける方法

以前の記事で、WebBrowser上の選択範囲の見つけ方を書きました。

allthewayfrom.hatenablog.com

この方法は範囲の先頭と終端に特定の文字を挿入し、その文字を検索して位置を特定していますが、これだと元になるHtmlElementのHTML自体に一旦文字を入れるため、Undoするとその挿入した文字が現れてしまう、、と言う問題があります。
なんとかその問題を解決し、少し改善してみました。

 
まずそもそもテキスト上の位置は特定できないのか?

色々と情報を漁ってみると、以下で紹介されている方法を発見。

stackoverflow.com

これは選択範囲のIHTMLTxtRangeと、その選択範囲のあるHtmlElementに対して全体範囲のIHTMLTxtRangeを作成して、moveStartで先頭の選択位置を1文字ずつ狭めて行き、compareEndPointsを利用して一致したところが選択範囲の文字位置であると言う方法で見つけています。

ここではcompareEndPoints"StartToStart"で先頭を見つけていますが、そのまま狭めて行き"StartToEnd"で一致すれば選択範囲の終端も分かります。

さて、表示されているテキスト(InnerText)の位置は分かりましたが、さらに欲しいのはこれに対するソース(InnerHtml)の位置です。

これが中々曲者。

色々と考えた挙句、結局は上記同様の文字列を挿入する方法に行き着くのですが、今回範囲の文字位置は分かっているため、ダミーでHTMLDocumentを作成し同じHTMLで作成したものを使えば、元になるHtmlElementは汚さない、と言う訳です。

これならなんとか行けたか、と思って利用しているとある条件のHTMLで選択位置がズレる場合がありここでも苦戦しました。。

ダミーデータは対象のHtmlElementに対し同じInnerHtmlで内容を設定しているのですが、この時divタグが存在する時、勝手にその直前のHTMLソースに改行(\r\n)を入れてしまう、、と言う動きをします。

ただ、対象のHtmlElementはこの勝手に改行が入れられた状態がベースとなっているのでそれに関しては良いのですが、なんとこの状態のInnerHtmlを別のInnerHtmlに設定すると、入れられた改行とタグの間にさらに半角スペースが入り「だったら最初から半角スペース+改行を入れろよ!」と言う謎の動き。

そう、同じInnerHtmlで中身を作ったつもりなのに半角スペースが追加されたことで位置がズレていました。

苦肉の解決策として、元となるHtmlElementに対し一度InnerHtmlを設定した後(この時点で改行が追加)にもう一度自身のInnerHtmlを再設定する、と言う方法。そうすれば元になる方も半角スペース+改行となるHTMLで同じとなる訳です。
(さすがに、3回目でも中のHTMLが変わるケースは考えたくも無いですが、確実に対応するのであれば前回のInnerHtmlと同じになるまでループさせる、と言う方法になるのかもしれませんね)


以下が選択範囲の、対象HtmlElementの表示されているテキスト位置を見つけるサンプルです。

// targetElm <-チェック対象のHtmlElement

IHTMLDocument2 htmlDoc  = webBrowser.Document.DomDocument as IHTMLDocument2;
IHTMLTxtRange  txtRange = htmlDoc.selection.createRange();

IHTMLTxtRange tempRange = ((IHTMLBodyElement)htmlDoc.body).createTextRange();
tempRange.moveToElementText((IHTMLElement)targetElm.DomElement);

int selStartIdx = 0;
while (tempRange.compareEndPoints("StartToStart", txtRange) != 0) {

	tempRange.moveStart("character", 1);
	selStartIdx++;

	if (selStartIdx >= targetElm.InnerText.Length) break;
}

int selEndIdx = selStartIdx;
while (tempRange.compareEndPoints("StartToEnd", txtRange) != 0) {

	tempRange.moveStart("character", 1);
	selEndIdx++;

	if (selEndIdx >= targetElm.InnerText.Length) break;
}


そして、見つけた位置からHTML上の位置を特定するサンプル。

// ダミーデータの作成
HTMLDocument tempDoc = new HTMLDocument();
((IHTMLDocument2)tempDoc).write("" + targetElm.InnerHtml + "");

// 選択範囲を探すための適当な文字列
const string selStartIDString = "@start@";
const string selEndIDString   = "@end@";

tempRange = ((IHTMLBodyElement)tempDoc.body).createTextRange();

tempRange.moveStart("textedit", -1);
tempRange.moveEnd("textedit", -1);
tempRange.moveStart("character", selEndIdx);
tempRange.pasteHTML(selEndIDString);

tempRange.moveStart("textedit", -1);
tempRange.moveEnd("textedit", -1);
tempRange.moveStart("character", selStartIdx);
tempRange.pasteHTML(selStartIDString);

selStartIdx = tempDoc.body.innerHTML.IndexOf(selStartIDString);
selEndIdx   = tempDoc.body.innerHTML.IndexOf(selEndIDString);


ご参考まで。
もっと良い方法があれば是非ご教授願います。