はじめに
自分が勤めている会社に、社内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.
が発生しないようにするためです。
//コンテキストメニューのクリック時イベントハンドラ
function onClickHandler(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"]
});
});
//選択文字列を送信
function sendText(info, tab)
{
var SelectedText = encodeURIComponent(info.selectionText.replace(/\\/g, '/'));
chrome.runtime.sendNativeMessage(
"host1",
{ SelectedText },
function(response)
{
var message = 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」とともに、以下のように実装します。
using Microsoft.JScript;
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
namespace OST_Host
{
[DataContract]
public class Message
{
[DataMember]
public string SelectedText { get; set; }
}
class NativeMessage
{
public static string StringRead()
{
// JSONデータの受信
string inStr = OpenStandardStreamIn();
inStr = GlobalObject.decodeURIComponent(inStr);
// JSONデータのデシリアライズ
var serializer = new DataContractJsonSerializer(typeof(Message));
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(inStr)))
{
var data = (Message)serializer.ReadObject(ms);
return data.SelectedText;
}
}
public static void StringWrite(string stringData)
{
int limit = 1024 * 1024 - 2;
string stringText = GlobalObject.encodeURIComponent(stringData);
while (stringText.Length >= limit)
{
OpenStandardStreamOut("\"" + stringText.Substring(0, limit) + "\"");
stringText = stringText.Substring(limit);
}
OpenStandardStreamOut("\"" + stringText + "\"");
}
private static string OpenStandardStreamIn()
{
Stream stdin = Console.OpenStandardInput();
byte[] bytes = new byte[4];
stdin.Read(bytes, 0, 4);
int length = BitConverter.ToInt32(bytes, 0);
string input = "";
for (int i = 0; i < length; i++) input += (char)stdin.ReadByte();
stdin.Close();
return input;
}
private static void OpenStandardStreamOut(string stringData)
{
byte[] bytes = BitConverter.GetBytes(stringData.Length);
Stream stdout = Console.OpenStandardOutput();
for (int i = 0; i < 4; i++) stdout.WriteByte(bytes[i]);
Console.Write(stringData);
stdout.Close();
}
}
}
using System;
using System.Diagnostics;
using System.IO;
namespace OST_Host
{
static class Program
{
[STAThread]
static void Main(string[] args)
{
// 受信文字列(¥が全て/になっている)
string inStr = NativeMessage.StringRead();
int index;
string[] prefixes = new string[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("フォルダを開けません。");
}
}
else if (File.Exists(inStr))
{
try
{
ProcessStartInfo psi = new ProcessStartInfo(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がやってくれるのか、バッチか何かを用意しないといけないか、それとも手動しかないか