はじめに
自分が勤めている会社に、社内Wikiみたいなものがあるのですが、各ページに書かれている参考資料へのパスが、社内のファイルサーバやローカルのパスになっていることが、多々あります。
パスを毎回コピーしてエクスプローラに貼り付けるのが、だんだん面倒になってきました。
そこで、Chrome拡張機能で自動化することにしました。
- 文字列を選択し、右クリックした時のコンテキストメニューから実行する
- 選択文字列がフォルダ/ファイルのパスなら開く
- 選択文字列の中に
file:とか<とか>とかあったら、事前に取り除く
先に言っておきますが、苦労の割にあまり自動化されません。。。
しかも、Chromeウェブストアにないため、Chromeを起動する度に毎回「無効化する」かどうか聞かれます。
作った機能からして、Chromeウェブストアに置かせてもらえる気がしません。
そのため、毎回聞かれても無効化せずに利用いただくか、投稿が部分的にでも何かの参考になれば幸いです。
やったこと
以下を丸パク、いや、参考にさせていただき、やったことを挙げていきます。
- Native Message1(外部ソフト登録)
- Native Message2(拡張機能)
- Native Message3(通信設定 拡張側)
- Native Message5(2byte文字等の対応)
(1) 拡張機能のマニフェストファイル作成
name、description、versionはお好きな値で。
あと、以下の例なら、48x48の好きなアイコン画像も必要です。
{"manifest_version":2,"name":"OpenSelectedText","description":"Open Selected Text","version":"1.0","background":{"scripts":["background.js"],"persistent":false},"permissions":["contextMenus","nativeMessaging"],"icons":{"48":"icon48.png"}}(2) 拡張機能の本体となるスクリプト作成
今回は、パスとなる文字列を選択後の、コンテキストメニューから呼ぶことにしました。
manifest.jsonで"persistent": falseにしているため、chrome.contextMenus.create()の中でonclickは指定しないで、メニューのIDから判断することにします。
スクリプトの終わり際にある、以下2点が重要です。
- chrome.runtime.sendNativeMessage()を呼んでいること
- chrome.runtime.sendNativeMessage()の第1引数を、後述するレジストリキーと合わせること
特に1点目は、参考サイトの方法
(chrome.runtime.connectNative()で取得したportに対してport.postMessage()を呼ぶ)
と異なります。
その理由は、ホスト側のプロセスが勝手に終わっても、Chrome側にエラーNative host has exited.が発生しないようにするためです。
//コンテキストメニューのクリック時イベントハンドラfunctiononClickHandler(info,tab){if(info.menuItemId=="OpenSelectedText"){sendText(info,tab);}};chrome.contextMenus.onClicked.addListener(onClickHandler);//拡張機能インストール時のみ、自メニュー追加chrome.runtime.onInstalled.addListener(function(){chrome.contextMenus.create({id:"OpenSelectedText",title:"選択文字列をパスとして開く",type:"normal",contexts:["selection"]});});//選択文字列を送信functionsendText(info,tab){varSelectedText=encodeURIComponent(info.selectionText.replace(/\\/g,'/'));chrome.runtime.sendNativeMessage("host1",{SelectedText},function(response){varmessage=decodeURIComponent(response);console.log(message);if(message!="OK"){alert(message);}});}(3) 拡張機能の読み込み
作ったマニフェストファイルとスクリプト(とアイコン画像)を、ローカルの任意フォルダに集めます。
(以降、フォルダをC:\Work\OpenSelectedTextと仮定しますが、各自読み替えてください)
そして、Chromeのメニュー「その他のツール」-「拡張機能」から、
「パッケージ化されていない拡張機能を読み込む」ボタンを押し、上記フォルダを指定します。
読み込んだ拡張機能に表示されたIDの値が次に必要なので、控えておいてください。
(4) 拡張機能と通信するホストのマニフェストファイル作成
拡張機能と通信するホスト用に、任意名称のマニフェストファイルを作ります。
(以降、ファイル名をOST_Host.jsonと仮定しますが、各自読み替えてください)
nameは後述のレジストリキーと合わせます。descriptionはお好きな値で。pathには、この後作るホスト(*.exe)へのパスを書きます。allowed_originsには、下記の例からID部分を、読み込んだ拡張機能のIDに修正します。
{"name":"host1","description":"Open Selected Text Host","path":"OST_Host.exe","type":"stdio","allowed_origins":["chrome-extension://bgkppcgfghmbmlfljpldaaddklfeaafg/"]}で、作ったマニフェストファイルもC:\Work\OpenSelectedTextに置いちゃいます。(本当はどこでもいいと思いますが)
(5) ホストを登録するためのレジストリ編集
レジストリ上、HKEY_CURRENT_USER\SOFTWARE\Google\ChromeにキーNativeMessagingHostsがなければ、作成しておきます。
さらに、NativeMessagingHosts直下に、スクリプトで呼ぶchrome.runtime.sendNativeMessage()の第1引数と同じ名称のキー(今回ならhost1)を作成します。
作成したキー(今回ならhost1)の値に、ホストのマニフェストファイルへの絶対パスを設定します。
レジストリ登録用のファイル(*.reg)風に書くと、こんな感じです。
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\SOFTWARE\Google\Chrome\NativeMessagingHosts\host1]
@="C:\\Work\\OpenSelectedText\\OST_Host.json"
(6) ホスト作成
C#で作ります。参考サイトには「コンソールアプリケーション」とあったのですが、DOS窓をチラ見せしたくないので、筆者は以下の手順で作り始めました。
(この手順が正しいかは不明ですが)
- Visual Studio起動(筆者は、PCにまだ入っていたVisual C# 2008 Express使用)
- 新規プロジェクトの作成で、「Windowsフォームアプリケーション」を選択
- 作成したプロジェクトから、「Form1.cs」を削除
- プロジェクトのプロパティにて、スタートアップを「Program」に変更
Chromeから来るデータはJSONなので、(ただ使ってみたかっただけですが)DataContractJsonSerializerを使ってみます。
作成したプロジェクトには以下3点、参照を追加します。
- Microsoft.JScript
- System.Runtime.Serialization
- System.ServiceModel.Web(これだけは.NET Framework 4以降なら不要)
新規クラス「NativeMessage.cs」を追加して、「Program.cs」とともに、以下のように実装します。
usingMicrosoft.JScript;usingSystem;usingSystem.IO;usingSystem.Runtime.Serialization;usingSystem.Runtime.Serialization.Json;usingSystem.Text;namespaceOST_Host{[DataContract]publicclassMessage{[DataMember]publicstringSelectedText{get;set;}}classNativeMessage{publicstaticstringStringRead(){// JSONデータの受信stringinStr=OpenStandardStreamIn();inStr=GlobalObject.decodeURIComponent(inStr);// JSONデータのデシリアライズvarserializer=newDataContractJsonSerializer(typeof(Message));using(varms=newMemoryStream(Encoding.UTF8.GetBytes(inStr))){vardata=(Message)serializer.ReadObject(ms);returndata.SelectedText;}}publicstaticvoidStringWrite(stringstringData){intlimit=1024*1024-2;stringstringText=GlobalObject.encodeURIComponent(stringData);while(stringText.Length>=limit){OpenStandardStreamOut("\""+stringText.Substring(0,limit)+"\"");stringText=stringText.Substring(limit);}OpenStandardStreamOut("\""+stringText+"\"");}privatestaticstringOpenStandardStreamIn(){Streamstdin=Console.OpenStandardInput();byte[]bytes=newbyte[4];stdin.Read(bytes,0,4);intlength=BitConverter.ToInt32(bytes,0);stringinput="";for(inti=0;i<length;i++)input+=(char)stdin.ReadByte();stdin.Close();returninput;}privatestaticvoidOpenStandardStreamOut(stringstringData){byte[]bytes=BitConverter.GetBytes(stringData.Length);Streamstdout=Console.OpenStandardOutput();for(inti=0;i<4;i++)stdout.WriteByte(bytes[i]);Console.Write(stringData);stdout.Close();}}}usingSystem;usingSystem.Diagnostics;usingSystem.IO;namespaceOST_Host{staticclassProgram{[STAThread]staticvoidMain(string[]args){// 受信文字列(¥が全て/になっている)stringinStr=NativeMessage.StringRead();intindex;string[]prefixes=newstring[2]{"file://","file:"};if(inStr==string.Empty){NativeMessage.StringWrite("選択文字列が空です。");}else{// 不要な文字の削除index=inStr.LastIndexOf("<");if(index>=0){inStr=inStr.Substring(index+1);}index=inStr.IndexOf(">");if(index>=0){inStr=inStr.Substring(0,index);}// "file:"の削除("FILE:"と書く方はまれだと思うが、一応は考慮)for(index=0;index<prefixes.Length;index++){if(inStr.StartsWith(prefixes[index],StringComparison.OrdinalIgnoreCase)){inStr=inStr.Substring(prefixes[index].Length);}}// UNCパス先頭の"//"と、"file://"の"//"が合体していたケースの対処if(!inStr.StartsWith("//")&&!inStr.Contains(":")){inStr="//"+inStr;}// 通信~デシリアライズ前とは逆の変換inStr=inStr.Replace("/","\\");if(Directory.Exists(inStr)){try{Process.Start("explorer.exe","/e, \""+inStr+"\"");NativeMessage.StringWrite("OK");}catch(Exception){NativeMessage.StringWrite("フォルダを開けません。");}}elseif(File.Exists(inStr)){try{ProcessStartInfopsi=newProcessStartInfo(inStr);psi.WorkingDirectory=Directory.GetParent(inStr).FullName;Process.Start(psi);NativeMessage.StringWrite("OK");}catch(Exception){NativeMessage.StringWrite("ファイルを開けません。");}}else{NativeMessage.StringWrite("不正なパスです。");}}}}}ビルドして生成したファイルも、C:\Work\OpenSelectedTextに置いちゃいます。
(ホストのマニフェストファイルに書いたpathと合わせます)
終わりに
選択範囲を狭くすれば、ファイルパスに対しても親フォルダを表示できるので、我ながら便利だと思います。
あとは、以下2点だけが気になります。
- Chromeウェブストアに置かせてもらえるか
- 置かせてもらえたとしても、他PCにインストールするとき、レジストリ編集はChromeがやってくれるのか、バッチか何かを用意しないといけないか、それとも手動しかないか