.NET で MSHTML をロードすると、IEコンポーネントで動作しない機能がある
.NET 2.0 で tlbimp した MSHTML.dll をロードすると、IEコンポーネントの印刷(プレビュー)機能や、IShellUIHelper.ShowBrowserUI の「お気に入りの整理」などが使用できなくなる。 解決 単に COM 参照で、 Microsoft HTML Object Library を追加したら動作した。
.NET 2.0 で tlbimp した MSHTML.dll をロードすると、IEコンポーネントの印刷(プレビュー)機能や、IShellUIHelper.ShowBrowserUI の「お気に入りの整理」などが使用できなくなる。 解決 単に COM 参照で、 Microsoft HTML Object Library を追加したら動作した。
IE7 では、ShellUIHelper に検索プロバイダの登録機能などを定義している IShellUIHelper2 が追加実装されているので SHDocVw が次のようになる。Guid は A7FE6EDA-1932-4281-B881-87B31B8BC52C である。 using System.Runtime.InteropServices; namespace SHDocVw { \[Guid("A7FE6EDA-1932-4281-B881-87B31B8BC52C")\] \[CoClass(typeof(ShellUIHelperClass))\] public interface ShellUIHelper : IShellUIHelper2 { } } このインスタンスをIE6環境で生成しようとすると、IShellUIHelper2 がないため InvalidCastException で落ちる。そのため、個別に COM コクラスを定義する。以前のGuid は、64AB4BB7-111E-11D1-8F79-00C04FC2FBE1 である。 using System.Runtime.InteropServices; namespace net.tilfin { \[ComImport(), Guid("64AB4BB7-111E-11D1-8F79-00C04FC2FBE1")\] public class ShellUIHelper { } } 「お気に入りの整理」を呼び出すサンプル .NET 2.0 で IDocHostUIHandler ではなくそこから参照している MSHTML をロード後に ShowBrowserUI や IE コンポーネントで印刷機能がなぜか効かなくなる。(調査中) object oHelper = new net.tilfin.ShellUIHelper(); SHDocVw.IShellUIHelper helper = (SHDocVw.IShellUIHelper)oHelper; helper.ShowBrowserUI("OrganizeFavorites", ref arg);
.NET 2.0 になり、アンマネージドとの相互変換時の変数型の定義を明確にしないと、InvalidVariant MDA メッセージが投げられるようになった。動作確認した定義は以下の通り。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 using System; using System.Runtime.InteropServices; using MSHTML; namespace net.tilfin { [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), GuidAttribute("bd3f23c0-d43e-11cf-893b-00aa00bdce1a")] public interface IDocHostUIHandler { [PreserveSig] uint ShowContextMenu( uint dwID, ref tagPOINT ppt, [MarshalAs(UnmanagedType.IUnknown)] object pcmdtReserved, [MarshalAs(UnmanagedType.IDispatch)] object pdispReserved ); void GetHostInfo(ref DOCHOSTUIINFO pInfo); void ShowUI(uint dwID, [In, MarshalAs(UnmanagedType.Interface)] IOleInPlaceActiveObject activeObject, [In, MarshalAs(UnmanagedType.Interface)] IOleCommandTarget commandTarget, [In, MarshalAs(UnmanagedType.Interface)] IOleInPlaceFrame frame, [In, MarshalAs(UnmanagedType.Interface)] Object doc); void HideUI(); void UpdateUI(); void EnableModeless(int fEnable); void OnDocWindowActivate(int fActivate); void OnFrameWindowActivate(int fActivate); void ResizeBorder(ref tagRECT prcBorder, int pUIWindow, int fFrameWindow); \[PreserveSig\] uint TranslateAccelerator(ref tagMSG lpMsg, ref Guid pguidCmdGroup, uint nCmdID); void GetOptionKeyPath(\[MarshalAs(UnmanagedType.BStr)\] ref string pchKey, uint dw); void GetDropTarget( \[MarshalAs(UnmanagedType.Interface)\] IDropTarget pDropTarget, \[Out, MarshalAs(UnmanagedType.Interface)\] out IDropTarget ppDropTarget); void GetExternal(\[MarshalAs(UnmanagedType.IDispatch)\] out object ppDispatch); \[PreserveSig\] uint TranslateUrl( uint dwTranslate, \[MarshalAs(UnmanagedType.BStr)\] string pchURLIn, \[MarshalAs(UnmanagedType.BStr)\] ref string ppchURLOut ); System.Windows.Forms.IDataObject FilterDataObject(System.Windows.Forms.IDataObject pDO); } }
log4j.xml の設定を動的に切り替えるため、リロードメソッドを実装してみる。 1 2 3 4 5 6 7 8 import org.apache.log4j.xml.DOMConfigurator; public class LogManager { … public static reload() { URL url = getLogConfigURL() DOMConfigurator.configure(url); } }
他にも方法はあるだろうがレジストリから取得するのが一番良さそう。 文字列でIEのバージョンを返す。エントリがなく取得に失敗したら null を返す。 1 2 3 4 5 6 7 8 9 10 11 12 13 public static string getInternetExplorerVersion() { string rKeyName = @"SOFTWARE\\Microsoft\\Internet Explorer"; string rValueName = "Version"; try { Microsoft.Win32.RegistryKey rKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(rKeyName); string sVersion = (string)rKey.GetValue(rValueName); rKey.Close(); return sVersion; } catch (NullReferenceException) { return null; } }
ディレクトリパスをファイル名の結合をするときに、/ \ といった区切り文字を気にしないで行いたい。.NET では、Path という便利なクラスが用意されているが、Java ではそのようなユーティリティクラスはない。File のコンストラクタがその役目を担ってくれる。 Java String filePath = new File(dirPath, fileName).getPath(); C# string filePath = Path.Combine(dirPath, fileName);
検索部分の実装。markupで全文のうち最初にマッチする単語が出てくる部分を抜き出してハイライト化する。 ページ処理はしていない。検索時間は JavaScript で後から表示。 phpにあまり慣れていなかったものの、LAMP を改めて実感する手軽さだった。 find.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 <?php mb\_internal\_encoding("UTF-8"); mb\_regex\_encoding("UTF-8"); header('Content-Type:text/html;charset=utf-8'); require("db.php"); ?> <html> <head> <title>Full Text Search Result</title> <style type="text/css"> h1 { font-weight:normal; color:#2530e5; font-size:200%; font-family:'Arial'; margin-top:20px; } ol,li,p { margin:0; padding:0; } li { list-style:none; margin:1.75em 0; } a { font-weight:bold; } p { margin-top:0.25em; font-size:80%; line-height:1.5; width:530px; } em { font-weight:bold; font-style:normal; background-color:#ff0; } #hitcount { font-size:80%; border-top:#3366cc 1px solid; background-color:#e5ecf9; padding:3px; text-align:right; } </style> </head> <body> <h1>全文検索</h1> <form method="get" action="find.php"> <input type="text" name="find" value="<?php echo($_GET\['find'\]) ?>" style="width:300px"/> <input type="submit" value=" 検索 "/><br/> <input type="checkbox" id="titleonly" name="titleonly" value="1"/><label for="titleonly">タイトルのみ</label> </form> <?php function markup($content, $words) { $disp = ""; $ctnstart = mb_strpos($content, "\\n") + 1; $ret = $ctnstart; foreach ($words as $word){ $ret = mb_stripos($content, $word, $ret); if ($ret === false) { continue; } elseif ($ret < 30) { $ret = 0; } else { $disp = "..."; $ret -= 30; } if (mb_strlen($content) - $ctnstart <= 130) { $disp = mb_substr($content, $ctnstart); break; } $disp .= mb_substr($content, $ret, 130); if (mb_strlen($disp) >= 133) { $disp .= "..."; } break; } if (strlen($disp) == 0) { if (mb_strlen($content) - $ctnstart > 130) { return mb_substr($content, $ctnstart, 130)."..."; } else { return mb_substr($content, $ctnstart); } } $kk = array(); $kk\[\] = "#ffff00"; $kk\[\] = "#00ffff"; $kk\[\] = "#ff00ff"; $kk\[\] = "#00ff00"; $k = 0; foreach ($words as $word){ if (mb_eregi($word, $content, $wdarray)) { foreach ($wdarray as $soeji => $wd){ $disp = mbereg_replace($wd, "<em style=\\"background-color:".$kk\[$k\]."\\">".$wd."</em>", $disp); } } if (++$k == 4) { $k = 0; } } return $disp; } if ($_GET\['find'\]) { $starttime = (float)microtime(); ?> <div id="hitcount"></div> <ol> <?php $dbm = new DBManager("livedocs_ft"); if ($_GET\['titleonly'\] == 1) { $stmt = $dbm->findTitle($_GET\['find'\]); } else { $stmt = $dbm->find($_GET\['find'\]); } $hitcount = 0; if ($stmt->execute()) { while ($record = $stmt->fetch(PDO::FETCH_ASSOC)) { echo "<li><a href=\\"".$record\['uri'\]."\\">".$record\['title'\]."</a>\\n"; if ($_GET\['titleonly'\] == 1) { } else { echo "<p>"; echo markup($record\['content'\], $dbm->getMarkupWords()); echo "</p>"; } echo "<p>"; echo $record\['score'\]; echo "</p></li>"; $hitcount++; } } $endtime = (float)microtime(); ?> </ol> <?php } ?> <script type="text/javascript"><!-- document.getElementById('hitcount').innerHTML = '<?php echo "<b>".$_GET\['find'\]."</b> で検索した結果 <b>".$hitcount."</b> 件 (<b>"; printf("%.3f", ($endtime - $starttime)); echo "</b> 秒)"; ?>'; //--!> </script> </body> </html>
htmlfiles は HTMLファイルのパスが書かれたテキストリストをコマンドラインでから流し込む。 $ php into.php < htmlfiles into.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <?php mb\_internal\_encoding("UTF-8"); mb\_regex\_encoding("UTF-8"); require_once("db.php"); require_once("htmlindex.php"); class Register { var $dbm; var $uri; var $title; var $content; var $hie; public function Register() { $this->dbm = new DBManager("livedocs_ft"); $this->hie =new HtmlIndexExtractor(); } public function regist($htmlfile) { if (!$this->readFile($htmlfile)) { echo "\[ERR\] Can't open file. : $file\\n"; return; } $this->hie->extract($this->htmltext); if ($this->dbm->insertFullTextIndexPrimary($htmlfile, $this->hie->getTitle(), $this->hie->getContent())) { echo "\[OK\] Inserted into file : $htmlfile.\\n"; } else { echo "\[NG\] Inserted into file : $htmlfile.\\n"; } } private function readFile($file) { $fh = fopen($file, 'r'); if ($fh == FALSE) { return false; } $ctn = ""; while (! feof($fh)) { $ctn .= fgets($fh); } fclose($fh); $this->htmltext = $ctn; return true; } } $rg = new Register(); $stdin = fopen('php://stdin', 'r'); if ($stdin == FALSE) { echo "No STDIN\\n"; exit; } while (!feof($stdin)) { $idxfile = rtrim(fgets($stdin), "\\n"); $idxfile = trim($idxfile); if (strlen($idxfile) > 0) $rg->regist($idxfile); } fclose($stdin); echo "Registing Completed.\\n"; ?>
PHPからMySQLへの接続にはPDOを使うことにした。テーブルは、単純にURI, タイトルとコンテントをフィールドとして用意、全文用のフィールドを分けたのは、ngram_prim の内容は ngram_sub にも入り重みが増すようにしてみた(一応、効いてると思われる)。Boolean Mode を使うとスコアは利用できないのでその対策は後述。 テーブル ft 1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE ft ( id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, uri VARCHAR(255) NULL, title VARCHAR(512) NULL, content MEDIUMTEXT NULL, ngram_prim MEDIUMTEXT NULL, ngram_sub MEDIUMTEXT NULL, PRIMARY KEY(id), FULLTEXT INDEX ft_ftindex(ngram_prim, ngram_sub), INDEX ft_uri_index(uri) ); MySQL接続の設定を別ファイルに切り出す。 ...
FullTextの登録用にHTMLからタイトルとbodyのテキスト抜き出す。XML_HTMLSax でパースして前述のテキストを抜き出すクラス HtmlIndexExtractor を作成した。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 <?php require_once('XML/XML_HTMLSax.php'); interface IndexExtractor { function extract($target); } class HtmlIndexExtractor implements IndexExtractor { private $parser; private $handler; private $title; private $content; private $str; private $getstr; public function HtmlIndexExtractor() { $this->parser=& new XML_HTMLSax(); $this->parser->set_object($this); $this->parser->set_option('XML\_OPTION\_TRIM\_DATA\_NODES'); $this->parser->set\_element\_handler('openHandler', 'closeHandler'); $this->parser->set\_data\_handler('dataHandler'); } public function extract($target) { /* $orgenc = mb\_detect\_encoding($target); if ($orgenc != "UTF-8") { $target = mb\_convert\_encoding($target, "UTF-8", $orgenc); } */ $this->parser->parse($target); } public function getTitle() { return $this->title; } public function getContent() { return $this->content; } function openHandler(& $parser,$name,$attrs) { $tagname = strtolower($name); if ($tagname == 'title' || $tagname == 'body') { $this->str = ""; $this->getstr = true; } } function closeHandler(& $parser,$name) { $tagname = strtolower($name); if ($tagname == 'title') { $this->title = $this->str; $this->getstr = false; } elseif ($tagname == 'body') { $this->content = $this->str; $this->getstr = false; } } function dataHandler(& $parser, $data) { if ($this->getstr) { $this->str .= htmlspecialchars_decode($data); } } function escapeHandler(& $parser,$data) {} function piHandler(& $parser,$target,$data) {} function jaspHandler(& $parser,$data) {} } ?>