Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9725

[Chrome拡張機能]ローカルのフォルダ/ファイルを開く機能を作ってみた

$
0
0

はじめに

自分が勤めている会社に、社内Wikiみたいなものがあるのですが、各ページに書かれている参考資料へのパスが、社内のファイルサーバやローカルのパスになっていることが、多々あります。
パスを毎回コピーしてエクスプローラに貼り付けるのが、だんだん面倒になってきました。
そこで、Chrome拡張機能で自動化することにしました。

  • 文字列を選択し、右クリックした時のコンテキストメニューから実行する
  • 選択文字列がフォルダ/ファイルのパスなら開く
  • 選択文字列の中にfile:とか<とか>とかあったら、事前に取り除く

先に言っておきますが、苦労の割にあまり自動化されません。。。

しかも、Chromeウェブストアにないため、Chromeを起動する度に毎回「無効化する」かどうか聞かれます。
作った機能からして、Chromeウェブストアに置かせてもらえる気がしません。

そのため、毎回聞かれても無効化せずに利用いただくか、投稿が部分的にでも何かの参考になれば幸いです。

やったこと

以下を丸パク、いや、参考にさせていただき、やったことを挙げていきます。
- Native Message1(外部ソフト登録)
- Native Message2(拡張機能)
- Native Message3(通信設定 拡張側)
- Native Message5(2byte文字等の対応)

(1) 拡張機能のマニフェストファイル作成

namedescriptionversionはお好きな値で。
あと、以下の例なら、48x48の好きなアイコン画像も必要です。

manifest.json
{"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.が発生しないようにするためです。

background.js
//コンテキストメニューのクリック時イベントハンドラ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に修正します。

OST_Host.json
{"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窓をチラ見せしたくないので、筆者は以下の手順で作り始めました。
(この手順が正しいかは不明ですが)

  1. Visual Studio起動(筆者は、PCにまだ入っていたVisual C# 2008 Express使用)
  2. 新規プロジェクトの作成で、「Windowsフォームアプリケーション」を選択
  3. 作成したプロジェクトから、「Form1.cs」を削除
  4. プロジェクトのプロパティにて、スタートアップを「Program」に変更

Chromeから来るデータはJSONなので、(ただ使ってみたかっただけですが)DataContractJsonSerializerを使ってみます。

作成したプロジェクトには以下3点、参照を追加します。
- Microsoft.JScript
- System.Runtime.Serialization
- System.ServiceModel.Web(これだけは.NET Framework 4以降なら不要)

新規クラス「NativeMessage.cs」を追加して、「Program.cs」とともに、以下のように実装します。

NativeMessage.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();}}}
Program.cs
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がやってくれるのか、バッチか何かを用意しないといけないか、それとも手動しかないか


Viewing all articles
Browse latest Browse all 9725

Trending Articles