SignalRとTypeScriptとknockout.jsで簡単なチャットを作成
最近の.netは素晴らしい!!
ずっと封鎖的だった.net環境。
最近は、超オープンですね。C#を愛する人間としては、嬉しい限りです。
SignalRで簡単なチャット
とりあえず、クライアントが文章を入力して送信したら
リアルタイムで配信されるだけの簡単なチャットの作成方法をご紹介します。
環境
- Visual Studio Community 2013
技術的な話
SignalRの話を追い求めてこのブログに辿り着いたあなたも、
きっと.net大好きなハズなので、
以下の技術を使用します。
- SignalR
- TypeScript
- Web API
- knockout.js (個人的に大好き)
スタート
では、早速
新しいプロジェクト > Web > ASP.NET Web アプリケーション
を作成しましょう。
名前は適当に『MyChat』にしておきます。
テンプレートは、Emptyを選んで、Web APIにチェックをいれておきましょう。
そしたら、NuGetで以下を取得します。
- knockoutjs
- knockout.TypeScript.DefinitelyTyped
- jquery.TypeScript.DefinitelyTyped
- signalr.TypeScript.DefinitelyTyped
- Bootstrap
取得できたら、SignalR ハブクラス(v2)を追加します。
名前は、ChatHub.csにしておきましょう。
デフォルトでHelloというメソッドがありますが、消しちゃいましょう。
そして、using Microsoft.AspNet.SignalR.Hubs;を追加して、
HubNameという属性を付加します。
この属性は、jsサイドでHubの扱いが楽になるので必須です。
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; namespace MyChat { [HubName("ChatHub")] public class ChatHub : Hub { } }
いい感じです。
混乱するかもしれませんが、以降はこのHubに編集は行いません。
次に、Hubのラッパーを作成します。
なんでもいいんだけど、ChatHubHandlerllerというクラスを追加します。
こいつが、Hubを使ってチャットに新規メッセージが追加された事を
クライアントに送信します。
using Microsoft.AspNet.SignalR; namespace MyChat { public class ChatHubHandlerller { /// <summary> /// クライアントに新規投稿メッセージを通知します。 /// </summary> /// <param name="msg">新規投稿メッセージ</param> public void NotifyChatAdd(string msg) { var context = GlobalHost.ConnectionManager.GetHubContext<ChatHub>(); context.Clients.All.SubscribeChatAdd(msg.Replace("\n", "<br />")); // とりあえず、改行だけ置換 } } }
SubscribeChatAdd?
と思われるかもしれませんが、こいつは動的な式なので
実行時に解決されます。
実は、SignalRはこれでは動きません。
Owinを使用して立ち上げる必要があります。
StartUp.csというクラスを追加して下さい。
using Microsoft.Owin; using Owin; [assembly: OwinStartupAttribute(typeof(MyChat.StartUp))] namespace MyChat { public partial class StartUp { public void Configuration(IAppBuilder app) { app.MapSignalR(); } } }
partialなのに、注意!!
次に、Viewから呼び出されるAPIを作成しましょう。
ChatV1Controllerという名前で、Web API コントローラー クラス(v2.1)
を追加します。
今回は、メッセージの新規投稿できれば良いので、Get等は消しておきます。
一応、属性マッピングを使ってAPIのバージョニングを可能にしておきます。
属性ルーティングしたりして、以下の形にして下さい。
using System.Web.Http; namespace MyChat { public class ChatV1Controller : ApiController { [Route("api/v1/chat")] public void Post([FromBody]string value) { var chatHub = new ChatHubHandlerller(); chatHub.NotifyChatAdd(value); } } }
/api/v1/chat
でアクセスできるようになります。
こうしておくことで、APIのバージョンを共存させられます。
Web API: The Good Parts、凄く良い本でした。
- 作者: 水野貴明
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/11/21
- メディア: 大型本
- この商品を含むブログ (3件) を見る
お次に、knockout.jsを使用してViewModelを作ります。
Scriptの中に、viewmodelsというフォルダを作成して、
ChatViewModel.tsというTypeScriptを作成しましょう。
/// <reference path="../typings/knockout/knockout.d.ts" /> /// <reference path="../typings/jquery/jquery.d.ts" /> /// <reference path="../typings/signalr/signalr.d.ts" /> /** * Chat機能を提供するViewModelです */ class ChatViewModel { /** * ViewModelのバインドを開始します。 */ public static StartBinding(elementName: string): void { var _vm = new ChatViewModel(); ko.applyBindings(_vm, $(elementName)[0]); } /** * メッセージ一覧を格納します。 */ public ChatMessages: KnockoutObservableArray<string> = ko.observableArray([]); /** * ユーザのインプットメッセージです。 */ public InputMessage: KnockoutObservable<string> = ko.observable(""); /** * SingalRの接続状況のメッセージです。 */ public SingalRConnectStateMsg: KnockoutObservable<string> = ko.observable("SingalR未接続"); // SignalR Hub public _Hub: HubProxy = null; constructor() { this._Hub = $.connection.hub.createHubProxy('ChatHub'); // チャットメッセージ配信を購読 this._Hub.on('SubscribeChatAdd', (msg) => { this.ChatMessages.push(msg); this.InputMessage(""); }); // 接続待機 $.connection.hub.start().done(() => { this.SingalRConnectStateMsg("SingalR接続済み"); }); } /** * メッセージの投稿を実行します。 */ public PostMessage(): void { var self = this; $.ajax({ type: "POST", url: "/api/v1/chat", contentType: "application/json", data: JSON.stringify(self.InputMessage()), success: function () { } }); } }
続いて、View。
Viewに関しては、Razorみたいに動的でも良いですし、
静的なhtmlでも良いです。
今回はhtmlで。
index.htmlを追加しましょう。
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title></title> <link href="Content/bootstrap.min.css" rel="stylesheet" /> </head> <body> <div id="chat-wrap" style="width: 500px; margin: 0 auto;"> <h2>My Chat</h2> <p id="connection-satus" data-bind="text: SingalRConnectStateMsg"></p> <div id="chat_messages" data-bind="template: { name: 'chatMsgTmpl', foreach: ChatMessages }"></div> <script id="chatMsgTmpl" type="text/html"> <div class="panel panel-default"> <div class="panel-body" data-bind="html: $data"> </div> </div> </script> <div class="text-center"> <div style="margin-bottom: 11px;"> <textarea id="txtChatInput" class="form-control" data-bind="value: InputMessage" rows="5"></textarea> </div> <button id="btnPostChat" type="button" class="btn btn-primary" data-bind="click: PostMessage">送信</button> </div> </div> <script src="Scripts/jquery-1.10.2.min.js"></script> <script src="Scripts/knockout-3.2.0.js"></script> <script src="Scripts/bootstrap.min.js"></script> <script src="Scripts/jquery.signalR-2.1.2.min.js"></script> <script src="/signalr/hubs"></script> <script src="Scripts/viewmodels/ChatViewModel.js"></script> <script> $(function () { var _vm = new ChatViewModel(); ko.applyBindings(_vm, $("#chat-wrap")[0]); }); </script> </body> </html>
scriptのバージョンは、各自のバージョンに合わせてくださいね。 それでは、実行してみましょう!
他のブラウザから同じURLにアクセスしてみて、投稿ボタンを押してみましょう。
リアルタイムで内容が反映されます。
すげぇぇぇぇぇえええ!
Mono.Cecilで黒魔術を行う。〜トレースログをインジェクション編〜
.net開発者のみなさん。
時々、こんな事を思ったりしませんか?
あー、全部のメソッドをフックして開始時と終了時にトレースログ吐きたいー。
特に、
運用チーム > バグガー、バグガー
ボク > どういったオペレーションで発生したのでしょうか?
運用チーム > バグガー、バグガー
ボク > なにそれ怖い。
って状況の時に。
AOPの範疇なのでしょうが、そんな設計が全くされていないソフトウェアが目の前にあるのが現状でしょう。
今から全部のメソッドにログを仕込む?
・・・無理。
今回はそんな時に使える黒魔術をお教えしましょう。
※黒魔術なので、まっとうにコードを書きたい人は見ない方が良いかもしれません。
黒魔術をするために、Mono.Cecilというライブラリを使用します。
まず、Monoとはいったい何なのかというと、
マルチプラットフォームで.NET Freamworkを動かすという狂気じみたプロジェクトであります。
私は、普段mac使いなので良くお世話になって・・・・いません。(homebrewがエラー吐くんだもん)
このMonoを利用して、C#でiOSを作っちゃうXamarinなんてのもあります。
で、このMonoの一部にCecilというライブラリがある。
Cecilは一体何なのかというと、大雑把に言ってしまえば
既存のアセンブリを読み込む
↓
任意のIL(中間言語)を注入
↓
アセンブリを出力
が行えるライブラリです。
ILとは何ぞやというと、奥深く深淵なる世界なので、
自分で調べるか、良くわかんないけどとりあえず続きを読むか考えて下さい。
今回やろうとしていることは、
- 既存のexe(dllでも可)を読み込む
- 全てのメソッドを抽出する
- メソッドの先頭にトレースログを出力するコードをインジェクションする
- 引数がある場合は、引数の名前と値をログ出力する(暫定)コードをインジェクションする
- メソッドの終了時にトレースログを出力するコードをインジェクションする
- exeを上書きする
ってな感じです。
ログ出力にはlog4netを利用します。
環境は
Windows7のVisual Studio Express 2012 for Windows Desktopです。
では、まずインジェクションされる側のexeを用意しましょう。
TestAppという名前のWindowsフォームアプリケーションを作成しましょう。
そして、
ツール > ライブラリパッケージマネージャ > ソリューションのNugetパッケージの管理
の右上のテキストボックスに log4net と入れてGetしましょう。
Getしたら、ソリューションエクスプローラから
TestApp > Properties > AssemblyInfo.cs
を選択して、一番下に
[assembly: log4net.Config.XmlConfigurator(Watch = true)]
と追加して下さい。
追加したら、App.configを開いて、
適当にlog4netの設定をします。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" /> </configSections> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <log4net> <!-- コンソール出力用 (VS 上 [出力] ウインドウへの出力) --> <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender"> <!-- 出力文字列のフォーマット --> <layout type="log4net.Layout.PatternLayout"> <!--^「日付、スレッド、レベル、logger名、メッセージ」が出力^--> <!--^「%-5p」というのは5文字以下の場合、右側に空白を入れる設定^--> <param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n"/> </layout> </appender> <!-- ファイル出力用 --> <appender name="DailyFileAppender" type="log4net.Appender.RollingFileAppender"> <!-- ログファイルの切替 { サイズ: Size, 日付: Date } --> <param name="RollingStyle" value="Date"/> <!-- ファイル名 --> <param name="File" value="log/"/> <!-- ファイル名に付加する日付パターン --> <param name="DatePattern" value="yyyyMMdd"_log.log""/> <!-- ファイル名の固定 --> <param name="StaticLogFileName" value="false"/> <!-- ファイル書き込み { 追記: true, 上書き: false } --> <param name="AppendToFile" value="true"/> <!-- 最大保存ファイル数 (古い順に削除) --> <param name="MaxSizeRollBackups" value="32"/> <!-- 出力文字列のフォーマット --> <layout type="log4net.Layout.PatternLayout"> <header type="log4net.Util.PatternString" value="[task time = "%date{HH:mm:ss,fff}"]%newline"/> <footer type="log4net.Util.PatternString" value="[/task]%newline"/> <conversionPattern value="%-5level %date{yyyy/MM/dd HH:mm:ss, fff} [%thread] %logger - %message%newline"/> </layout> <!-- 出力するログ レベルのフィルタ --> <!-- Level : Fatal, Error, Warn, Info, Debug --> <filter type="log4net.Filter.LevelRangeFilter"> <levelMin value="DEBUG"/> <levelMax value="FATAL"/> </filter> </appender> <!-- イベント ログ出力用 --> <appender name="EventLogAppender" type="log4net.Appender.EventLogAppender"> <!-- イベント ログ上のアプリケーション名 --> <applicationName value="AppName"/> <!-- 出力文字列のフォーマット --> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%-5level %date{yyyy/MM/dd_HH:mm:ss,fff} [%thread] %logger [%property{NDC}] - %message%newline"/> </layout> <!-- 出力するログ レベルのフィルタ --> <filter type="log4net.Filter.LevelRangeFilter"> <levelMin value="DEBUG"/> <levelMax value="FATAL"/> </filter> </appender> <!-- デフォルトの出力設定 --> <root> <appender-ref ref="DailyFileAppender"/> </root> </log4net> </configuration>
log4netも深遠なる世界が広がっているので、よく解らない人は調べて下さい。
今回はこれが本題では無いので・・・。
Form1にボタンを2つ設置して、ソースコードを下記のように書き換えます。
using System; using System.Windows.Forms; namespace TestApp { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnMethod1Call_Click(object sender, EventArgs e) { Greet("hello", "Cecil"); } private void btnMethod2Call_Click(object sender, EventArgs e) { Greet("byebye", "contextbound"); } private void Greet(string greeting, string name) { MessageBox.Show(string.Format("{0} {1}!!", greeting, name)); } } }
それから、log4netが使用できるように、
TestAppにUtilというフォルダを追加してLogging.csを追加します。
ソースは下記
namespace TestApp.Util { /// <summary> /// ログ出力クラスです。 /// </summary> internal static class Logging { internal static readonly log4net.ILog Logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); } }
これで一旦、インジェクションされる側の準備は終わりです。
このコードには、ログを吐くソースコードは一切記述されていない事を確認して下さい。
では、いよいよインジェクションする側です。
後で拡張するために、インジェクター本体と、インジェクター実行は別プロジェクトで用意します。
まず、インジェクター本体
黒魔術らしく、VooDooInjectというクラスライブラリを用意します。
プロジェクトを用意したら、NugetでMono.CecilをGetしましょう。
あっさりインストールされますね。
さらに、log4netのリファレンスを取得する必要があるので、
ツール > ライブラリパッケージマネージャ > ソリューションのNugetパッケージの管理
インストール済みのパッケージからVooDooInjectにもlog4netをインストールしておきましょう。
AssemblyInfo.csとapp.confgの編集は必要ありません。
(こいつがログを吐くわけでは無いので。)
ソースを用意します。
Injector.cs
using System; using System.Collections.Generic; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using log4net; namespace VooDooInject { /// <summary> /// コンパイル後のアセンブリに特定のILを挿入する機能を提供します。 /// </summary> public class Injector { /// <summary> /// Type毎のConvert.ToStringキャッシュ /// </summary> private Dictionary<Type, Instruction> _dicTypeToString = new Dictionary<Type, Instruction>(); /// <summary> /// メソッドにログ出力機能をインジェクションします。 /// </summary> /// <param name="assemblyPath">インジェクション対象のアセンブリのパス</param> public void InjectMethodTraceLog(string assemblyPath, string assemblyWritePath = "") { // アセンブリを読み込む var assembly = AssemblyDefinition.ReadAssembly(assemblyPath); // 使用するメソッドのリファレンスを取得する // 文字列結合('+'オペレータが実際に呼び出しているメソッド) var concat = typeof(string).GetMethod("Concat", new[] { typeof(string), typeof(string) }); // log4netのDebug var log4Debug = typeof(ILog).GetMethod("Debug", new[] { typeof(object) }); // log4netを定義しているフィールドを取得する。 var loggingtypeDef = assembly.MainModule.Types.First(t => t.Name == "Logging"); var loggerFldRef = loggingtypeDef.Fields.First(f => f.Name == "Logger"); // log4netの静的コンストラクタはインジェクション対象外とする var log4CctrName = string.Format("System.Void {0}::.cctor()", loggingtypeDef.FullName); // 全メソッドを抽出する assembly.Modules .SelectMany(x => x.Types) .SelectMany(x => x.Methods) .ToList() .ForEach(method => { // log4netのコンストラクタは処理対象外とする if (log4CctrName == method.FullName) return; // 繰り返し使うオペコードを生成しておく var il = method.Body.GetILProcessor(); var callCon = il.Create(OpCodes.Call, method.Module.Import(concat)); var ldsfldLogger = il.Create(OpCodes.Ldsfld, loggerFldRef); var callLog4Debug = il.Create(OpCodes.Callvirt, method.Module.Import(log4Debug)); // メソッド開始時 var first = method.Body.Instructions.First(); var ldstr_Start = il.Create(OpCodes.Ldstr, "Start => " + method.FullName); il.InsertBefore(first, ldsfldLogger); // Loggerフィールドをロード il.InsertBefore(first, ldstr_Start); // 文字列をロード il.InsertBefore(first, callLog4Debug); // Logger.Debugを呼び出す // 引数の数だけ、Ldarg_nオペコードを生成する for (int i = 0; i < method.Parameters.Count; i++) { // type毎のToStringをキャッシュ化する。 var t = Type.GetType(method.Parameters[i].ParameterType.FullName); Instruction callToString = null; if (t!= null && !_dicTypeToString.TryGetValue(t, out callToString)) { var ts = typeof(Convert).GetMethod("ToString", new[] { t }); callToString = il.Create(OpCodes.Call, method.Module.Import(ts)); _dicTypeToString.Add(t, callToString); } var ldastrPrmInfo = il.Create(OpCodes.Ldstr, "\t" + method.Parameters[i].Name + ":" + method.Parameters[i].ParameterType.ToString() + " => "); il.InsertBefore(first, ldsfldLogger); // Loggerフィールドをロード il.InsertBefore(first, ldastrPrmInfo); // 文字列をロード if (t != null) { il.InsertBefore(first, il.Create(OpCodes.Ldarg, method.Parameters[i])); // パラメータをロード il.InsertBefore(first, il.Create(OpCodes.Box, method.Module.Import(t))); // Box化(Guid等の参照対策) il.InsertBefore(first, callToString); // ToStringを呼び出す il.InsertBefore(first, callCon); // 文字を結合する } il.InsertBefore(first, callLog4Debug); // Logger.Debugを呼び出す } // メソッド終了時 var ldstr_End = il.Create(OpCodes.Ldstr, "End => " + method.FullName); method.Body.Instructions .Where(i => i.OpCode == OpCodes.Ret) .ToList() .ForEach(end => { il.InsertBefore(end, ldsfldLogger); // Loggerフィールドをロード il.InsertBefore(end, ldstr_End); // 文字列をロード il.InsertBefore(end, callLog4Debug); // Logger.Debugを呼び出す }); }); // 書き込み assembly.Write(string.IsNullOrEmpty(assemblyWritePath)? assemblyPath: assemblyWritePath); } } }
インジェクター実行はVDIExcuterという名前のコンソールアプリケーションを用意します。
参照設定で、VooDooInjectを参照して下さい。
Program.cs
namespace VDIExcuter { class Program { static void Main(string[] args) { var injector = new VooDooInject.Injector(); injector.InjectMethodTraceLog("TestApp.exe"); } } }
VDIExcuterを実行すると、TestApp.exeにインジェクションが実行される仕組みです。
自動化については、次回の記事で書こうと思っています。
とりあえず、今回はTestAppの出力先をVDIExcuterのフォルダーに設定します。
ソリューションエクスプローラから
TestAppを右クリック > プロパテイ > ビルドタブ > 出力パス
に
..\VDIExcuter\bin\Debug
と入力してビルドして下さい。
これで、VDIExcuter\VDIExcuter\bin\Debugフォルダの中にTestApp.exeが出力されます。
VDIExcuter.exeとTestApp.exeが同一階層にある状態で
VDIExcuter.exeを実行しましょう。
TestApp.exeが書き換えられます。
TestApp.exeを実行してみると・・・。
VDIExcuter\VDIExcuter\bin\Debug\log
にログが出力されているはず!!
このVooDooInjectの利用は自己責任でお願いします。
因みに、引数の値を出力する部分は未完成です。
IL部分の解説はそのうちするかもしれません。
最近メタプログラミングしてる時間が、子どもと遊ぶ時間の次くらいに楽しい・・。フヒヒ。
この黒魔術に迷える技術者が魂を売りますように・・・!!
javascriptで直線を描画(非canvas)
javascriptで線を描画したい。
斜めにも描画したい。
しかし、諸事情でcanvasは使えない。
そんな時、どうしますか?
1pxのDivを繋げて線を描画する手法もありますが、
なんかギザギザになるし・・・。
で、考えたのですがborder-topを指定したDivをrotateさせれば
斜めの線も引けるんじゃない?
点1(x1, y1)と点2(x2, y2)の2点に線を描画しようと考える場合、
まず線の長さは、
Math.sqrt(Math.pow((param.x1 - param.x2) ,2) + Math.pow((param.y1 - param.y2) ,2))
で求められます。
次に、2点間の角度ですが逆三角関数を使用することで求められます。
Math.atan(高さ / 底辺)
求められる数値はラジアンなので、℃に変換すれば、
CSS3のtransform(deg)
で傾きを表現できます。
transform-origin
で回転軸を左端にすることを忘れずに!!
では、まずcss
.line{ position: absolute; border-top: solid 1px #FF4981; -moz-transform-origin:0% 0%; -webkit-transform-origin:0% 0%; transform-origin:0% 0%; float: left; }
次にjs
function DrawLine(params) { var param = jQuery.extend({ x1: 0 , y1: 0 , x2: 0 , y2: 0 , line_style: "solid" , line_color: "#000" , line_width: "1px" , parent: $("body") , callback: function(){} }, params); var w = Math.sqrt(Math.pow((param.x1 - param.x2) ,2) + Math.pow((param.y1 - param.y2) ,2)); // 線の長さを算出する(2点間の距離を算出する) var base = Math.max(param.x1, param.x2) - Math.min(param.x1, param.x2); // 底辺長を算出する(aTanの算出のため) var tall = Math.max(param.y1, param.y2) - Math.min(param.y1, param.y2); // 高さを算出する(aTanの算出のため) var aTan = Math.atan(tall / base); // 逆三角関数で tanのθを算出する var deg = aTan * 180 / Math.PI; // rad => degrees deg = params.y1 > param.y2 ? 0 - deg: deg; // 回転方向 var line = $("<div></div>") .addClass("line") .css({ "left": param.x1, "top": params.y1, "width": w, "transform": "rotate(" + deg + "deg)", "-webkit-transform": "rotate(" + deg + "deg)", "border-top-style": param.line_style, "border-top-color": param.line_color, "border-top-width": param.line_width, }); $(param.parent).append(line); }
呼出し側
<script type="text/javascript"> $(document).ready(function() { DrawLine({ x1: 50, y1: 50, x2: 190, y2: 120, }); DrawLine({ x1: 150, y1: 250, x2: 390, y2: 120, line_color: "#999", line_width: "3px", }); }); </script>
A*(A Star)
最近、アルゴリズムの勉強がちょっとした趣味になっている。
もちろん、最強最速アルゴリズマー養成講座を購入した。
おもしろい。
アルゴリズム体操はおもしろい!!
で、タイトルになっているA*というのもアルゴリズムの一つで、
"エースター"と読む。
最短経路を求めるアルゴリズムで、
あのダイクストラ法の改良版である。
実際の内容やら、名前の由来なんかは、wikipediaに詳しく載ってます。
ソースは、ソースを表示すれば丸見えです。
(数行下に載せてあるリンクからの方が見やすいかも)
このアルゴリズム、何が素敵かというと、
ゴールノードまでの最小コストの推定値を算出する関数の名前が
ヒューリスティック関数
という。
ヒューリスティック・・。
なんとも中二病な香り・・・。
あまりにかっこいいから、javascript(jQuery)でビジュアライズしてやったぜ。
ヒュー・・リス・・・テッィク。
白いマスが通路、黒いマスが壁。
オレンジがスタートで、緑がゴールです。
移動可能な方向は上下左右(僕はFC版FF3世代です。)
スタートとゴールの位置は、テキストボックスで変更できます。
通路をクリックすると、壁に
壁をクリックすると通路になります。
Searchボタンで探索開始!!
無理やりブログ上で動くようにしてみたけど、
もうちょっと綺麗なVerを公開しておきます。
こちら
http://www9.ocn.ne.jp/~sggksoft/2/
C#とWebkit.netでアプリ開発 〜webkit.netで作ってみる編〜
webkit.netで作ってみる編ということで、
C#とWebkit.netを使用して、何かアプリを作ってみましょう。
html + CSS3を使用したViewと、C#のネイティブソースを連携させます。
とりあえずクリップボードを監視して、一覧表示するアプリでも作ってみましょう。
機能としては、
- クリップボードを監視 (C#のお仕事)
- 変更が発生した場合は、画面に表示 (C#とWetbitのお仕事)
- 表示は直近5件まで (Wetbitのお仕事)
- 一覧からコピーが可能 (C#とWetbitのお仕事)
って感じです。
ちなみに、この記事長いです。
ソース一式は、一番したにURL載せてあるので、そこから取得可能です。
さっそく
この記事の状態からスタートしましょう。
開発環境は、Visual Studio Express 2012 for Windows Desktopです。
まず、Form1の名称を frmMain に変更します。
そしてこいつに、クリップボードを監視させましょう。
今のところこんな感じですね。
public partial class frmMain : Form { public frmMain() { InitializeComponent(); webKitBrowser1.Navigate("http://google.com"); } }
クリップボードの監視に関しては、クリップボードの内容をリアルタイムに取得するには?[C#、VB]を参考にしましょう。
リンク先のソースをClipboardViewer.csという名前で保存します。
frmMainで監視を開始しましょう。
public partial class frmMain : Form { /// <summary> /// クリップボード監視 /// </summary> private MyClipboardViewer _clipboardViewer { get; set; } public frmMain() { _clipboardViewer = new MyClipboardViewer(this); _clipboardViewer.ClipboardHandler += ClipboardViewer_ClipboardHandler; InitializeComponent(); webKitBrowser1.Navigate("http://google.com"); } /// <summary> /// クリップボードにテキストがコピーされた際に呼び出されます。 /// </summary> /// <param name="sender"></param> /// <param name="ev"></param> void ClipboardViewer_ClipboardHandler(object sender, ClipboardEventArgs ev) { Console.WriteLine(ev.Text); } }
クリップボードにテキストがコピーされると、VSの出力ウィンドウにコピーしたテキストが表示されるようになりました。
次に、このテキストを表示するためのViewを用意しましょう。
いろいろフォルダとファイルを用意します。
jQueryは適当なバージョンをダウンロードして下さい。
img/bg.pngはアプリの背景画像です。
各自お好みで素敵な画像を使用して下さい。
僕はiOS7チックなブラーな背景を適当にダウンロードしました。
(blur free backgroundでぐぐってみましょう。)
では、各ファイルの中身を記述していきましょう。
まずは、main.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="content-language" content="ja" /> <title></title> <link rel="stylesheet" type="text/css" href="../assets/css/common.css" /> <link rel="stylesheet" type="text/css" href="../assets/css/main.css" /> </head> <body> <div id="div-body"> <ol id="list"> </ol> </div> <script type="text/javascript" src="../assets/js/jquery-2.0.3.min.js"></script> <script type="text/javascript" src="../assets/js/main.js"></script> </body> </html>
listというidを持った、ol要素がいますね。
クリップボードにテキストがコピーされたら、WebkitBrowserからJavascriptを実行して、
このol要素にli要素を追加していきます。
では、li要素を追加するスクリプトを作成しましょう。
main.jsに以下を追記します。
var MAX_LIST_COUNT = 5; // 受け取った結果をリストに追加します。 function AddList(id, txt, date) { if (MAX_LIST_COUNT <= GetListItemCount()) { // 最大値を超過する場合は、古いデータを消す $('#list').children("li").eq(MAX_LIST_COUNT - 1).fadeTo(300, 0, function () { $(this).remove(); ExecAddList(id, txt, date); }); } else { ExecAddList(id, txt, date); } } // リスト追加実処理です。 function ExecAddList(id, txt, date) { // 表示用の要素を用意していきます。 var li = $('<li></li>'); var txtDiv = $('<div></div>'); var copyDiv = $('<div></div>'); var dateDiv = $('<div></div>'); $(li).attr('id', id); $(txtDiv).addClass('list_txt').html(txt).fadeTo(0, 0); $(dateDiv).addClass('list_date').text(date).fadeTo(0, 0); $(copyDiv).addClass('copy_info').text('copy!!').hide(); // アニメーションさせるために、一度非表示にします。 $(li).append(txtDiv).append(dateDiv).append(copyDiv).fadeTo(0, 0).on({ click: function (e) { // クリックされた際のイベントを設定します。 $('title').text($(this).attr('id')); var copyInfo = $(this).children('.copy_info'); var copyWidth = $(copyInfo).width(); var copyLeft = $(copyInfo).css('left').replace('px', ''); var liWidth = $(this).width(); var liPad = $(this).css('padding').replace('px', '') * 2; $(copyInfo).show().css('left', copyLeft - copyWidth) .one('transitionend', function(){ $(this).css('left', liWidth + liPad); }); } }); // 先頭に追加します。 $('#list').prepend(li); // 追加時にアニメーションさせます。 var liHeight = $(li).height(); $(li).css('height', '0px').fadeTo(500, 1, function () { $(txtDiv).fadeTo(700, 1); $(dateDiv).fadeTo(700, 1); }).css('height', liHeight + 'px'); // click時に表示するcopydivのポジションを設定します。 var innerHeight = $(txtDiv).height() + $(dateDiv).height(); var liPad = $(li).css('padding').replace('px', '') * 2; $(copyDiv).css({'height': liHeight + liPad , 'top': '-' + (innerHeight + liPad - 4) + 'px', 'left': $(li).width() + liPad }); } // リストに表示されているliの数を取得します。 function GetListItemCount() { return $('#list').children('li').length; }
次に、見た目を整えます。
見た目は大事です。Windows Formのダサさにウンザリしている人が
このブログを見ていることを願っています。
common.cssに下記を追記します。
body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, form, fieldset, input, textarea, p, blockquote, th, td { margin: 0; padding: 0; } img { border: 0; } ol, ul { list-style: none; } * { -webkit-user-select: none; } input[type=text], textarea { -webkit-user-select: auto; } html, body { height: 100%; } body { height: 100%; width: 100%; font-family: 'Lucida Grande','Hiragino Kaku Gothic ProN', Meiryo, sans-serif; min-height: 100%; margin: 0 auto; padding: 0; color: #222222; font-size: 14px; line-height: 1.3; font-weight: normal; }
続いて、main.cssに下記を追記します。
#div-body { background-image: url("../img/bg.png"); background-size: cover; width: 320px; min-width: 320px; min-height: 260px; padding: 11px; } #list li { border-bottom: 1px solid rgba(0, 0, 0, 0.1); -webkit-box-shadow: white 0 1px 0; -moz-box-shadow: white 0 1px 0; box-shadow: white 0 1px 0; padding: 8px; cursor: pointer; transition: background-color .8s ease, height .8s ease; -moz-transition: background-color .8s ease, height .8s ease; -webkit-transition: background-color .8s ease, height .8s ease; background-color: rgba(255, 255, 255, .3); overflow: hidden; } #list li:last-child { border-bottom: 0px; -webkit-box-shadow: white 0 0px 0; -moz-box-shadow: white 0 0px 0; box-shadow: white 0 0px 0; } #list li:hover { background-color: rgba(255, 255, 255, .7); } .list_txt { margin: 0 0 5px 0; width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -webkit-text-overflow: ellipsis; -o-text-overflow: ellipsis; } .list_date { font-size: 9px; } .copy_info{ width: 80px; color: #eee; padding: 3px 0 0 8px; position: relative; transition: left .8s ease; -moz-transition: left .8s ease; -webkit-transition: left .8s ease; background: rgba(221, 75, 57, .7); }
では、さっそくWebkitBrowserに読み込ませてみましょう。
追加したファイルは、
ソリューションエクスプローラで右クリック > プロパティ > 出力ディレクトリにコピー で
新しい場合はコピーするに変更して下さい。
(背景画像も忘れずにね!!)
frmMain.csのコンストラクタを修正します。
public frmMain() { _clipboardViewer = new MyClipboardViewer(this); _clipboardViewer.ClipboardHandler += ClipboardViewer_ClipboardHandler; InitializeComponent(); var htmlPath = new StringBuilder(); htmlPath.Append(@"file:///").Append(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, @"htdocs\views\main.html")); webKitBrowser1.Navigate(htmlPath.ToString()); }
実行前に、
frmMainをデザイナで開いて
フォームのサイズを359, 321
WebkitBrowserのDockをFill
に設定しましょう。
それでは、実行してみましょう。
読み込みましたね。
(main.htmlまでのパスに日本語が入っていと、読み込みに失敗するかもしれません。)
では、クリップボードでテキストコピーのイベントをハンドルした際に、
Javascriptを実行してみましょう。
frmMain.csを修正します。
/// <summary> /// クリップボード監視 /// </summary> private MyClipboardViewer _clipboardViewer { get; set; } /// <summary> /// クリップボードにコピーされた文字列をキャッシュします。 /// </summary> private Lazy<Dictionary<string, string>> _dicClipChace { get; set; } public frmMain() { _clipboardViewer = new MyClipboardViewer(this); _clipboardViewer.ClipboardHandler += ClipboardViewer_ClipboardHandler; _dicClipChace = new Lazy<Dictionary<string, string>>(() => new Dictionary<string, string>()); InitializeComponent(); var htmlPath = new StringBuilder(); htmlPath.Append(@"file:///").Append(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, @"htdocs\views\main.html")); webKitBrowser1.Navigate(htmlPath.ToString()); } /// <summary> /// クリップボードにテキストがコピーされた際に呼び出されます。 /// </summary> /// <param name="sender"></param> /// <param name="ev"></param> void ClipboardViewer_ClipboardHandler(object sender, ClipboardEventArgs ev) { Console.WriteLine(ev.Text); var id = Guid.NewGuid(); _dicClipChace.Value.Add(id.ToString(), ev.Text); var txt = ev.Text.Replace(Environment.NewLine, "<br/>"); webKitBrowser1.StringByEvaluatingJavaScriptFromString(string.Format("AddList('{0}', '{1}', '{2}')", id.ToString(), txt, DateTime.Now.ToString("yyyy/MM/dd hh:mm:ss"))); }
StringByEvaluatingJavaScriptFromStringを実行することで、WebkitBrowserのJavascriptを実行できます。
とりあえず、改行コードを
に変換していますが、ブラウザで表示できない文字はエスケープする必要があります。
今回はとりあえずスルーします。
上記の理由から、クリップボードの中身を加工する必要があるため、
無加工のテキストをディクショナリにキャッシュします。
その際に、KeyとしてGuidを発行しています。
このGuidをWebkitBrowserに渡し、Javascript側でli要素のidとして設定します。
実行してみましょう。
クリップボードにテキストがコピーされる度に、一覧にアイテムが追加されます。
次にli要素をクリックされた際に、対応するテキストをクリップボードにコピーする処理を実装します。
通常のWebBrowserコントロールであれば、DOMにイベントをバインドできるのですが、
Webkit.netではそう簡単ではありません。
もっとお手軽に、
li要素がクリックされる
↓
Javascriptで、titleを書き換える
↓
C#でWebkit.netのDocumentTitleChangedイベントをハンドルする
↓
何か処理
という手順を踏みます。
main.jsの
$('title').text($(this).attr('id'));
という部分で、クリックされたli要素のidをtitleに設定しています。
C#でDocumentTitleChangedをハンドルして、DocumentTitleを参照すれば、
クリックされたli要素のidが判定できる仕組みです。
では、frmMain.csを修正しましょう。
これで完成なので、全ソースを載せておきます。
using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Windows.Forms; namespace WebkitSample { public partial class frmMain : Form { /// <summary> /// クリップボード監視 /// </summary> private MyClipboardViewer _clipboardViewer { get; set; } /// <summary> /// クリップボードにコピーされた文字列をキャッシュします。 /// </summary> private Lazy<Dictionary<string, string>> _dicClipChace { get; set; } public frmMain() { _clipboardViewer = new MyClipboardViewer(this); _clipboardViewer.ClipboardHandler += ClipboardViewer_ClipboardHandler; _dicClipChace = new Lazy<Dictionary<string, string>>(() => new Dictionary<string, string>()); InitializeComponent(); var htmlPath = new StringBuilder(); htmlPath.Append(@"file:///").Append(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, @"htdocs\views\main.html")); webKitBrowser1.Navigate(htmlPath.ToString()); } /// <summary> /// クリップボードにテキストがコピーされた際に呼び出されます。 /// </summary> /// <param name="sender"></param> /// <param name="ev"></param> void ClipboardViewer_ClipboardHandler(object sender, ClipboardEventArgs ev) { Console.WriteLine(ev.Text); var id = Guid.NewGuid(); _dicClipChace.Value.Add(id.ToString(), ev.Text); var txt = ev.Text.Replace(Environment.NewLine, "<br/>"); webKitBrowser1.StringByEvaluatingJavaScriptFromString(string.Format("AddList('{0}', '{1}', '{2}')", id.ToString(), txt, DateTime.Now.ToString("yyyy/MM/dd hh:mm:ss"))); } /// <summary> /// WebkitBrowserのタイトルが変更された場合に発生するイベントです。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void webKitBrowser1_DocumentTitleChanged(object sender, EventArgs e) { try { var id = ((WebKit.WebKitBrowserCore)sender).DocumentTitle; if (id == null) return; string txt; if(_dicClipChace.Value.TryGetValue(id, out txt)) { _clipboardViewer.ClipboardHandler -= ClipboardViewer_ClipboardHandler; Clipboard.SetText(txt); _clipboardViewer.ClipboardHandler += ClipboardViewer_ClipboardHandler; } } catch (Exception ex) { MessageBox.Show(ex.ToString()); } } } }
長かったですね。
今回は、わかりやすくするためにいろいろ省略していますが、
フォームのサイズをviewの大きさに合わせて、勝手に調整したり、
WebKitBrowser自体を拡張して、DocumentTitleChanged発生時に、
jQueryで検知したイベントに対応するC#のイベントを発行したり(あるいは、匿名デリゲード実行とか)した方が
クールですね。
このアプリのように、
Viewのスタイルや、アニメーションをjsやCSSで定義できるのは
非常に楽です。
Windows Fromだとボタンを作るのにも一苦労ですし、
アニメーションとか・・・・超めんどくさいですよね。
でも、こんなに楽に実装できるんです。そう、Webkit.netならね。
全ソースはGitBreakに公開しています。
適当に見たり編集してね!!
質問、疑問、適当なお話はこちらまで => a.shinada@chorus.ocn.ne.jp
c#でショートカットの日本語名を取得する
例えば、
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories
をエクスプローラで開いてみ欲しい。
ペイント、電卓・・・等、日本語のショートカットが並んでいる。
このフォルダに対して、下記コードを実行する。
var files = Directory.GetFiles(@"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories", "*", SearchOption.AllDirectories);
取得結果は、
ペイント => C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories\Paint.lnk
電卓 => C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories\Calculator.lnk
あれ?
そ、そんな!!
あ…ありのまま 今 起こった事を話すぜ!
僕の網膜には、ペイントって文字が写っているのに
実際はPaint.lnkだと!?
どうも、こういう事らしい
http://snow-white.cocolog-nifty.com/first/2010/01/windows-7-8b1c.html
諸事情あって、僕は"ペイント"とか"電卓"って文字列が欲しいのです。
まいったね。嵌ったね。恐怖したね。
ってわけで、解決策。
まず参照設定で Microsoft Shell Controls And Automation を追加します。
んで、
using Shell32;
を追加します。
そしたら、
var path = @"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Accessories\Paint.lnk"; var dirNm = Path.GetDirectoryName(path); var fileNm = Path.GetFileName(path); var shell = new Shell(); var f = shell.NameSpace(dirNm); var item = f.ParseName(fileNm); var jpName = item.Name;
で日本語名が取得できます。
ふぅ。。
64bit環境で、Process.GetProcesses()を使用しウィンドウをもつプロセスのMainModule.FileNameを取得したい。
やたらと長いタイトルだが、やりたいことは、
- 起動中のプロセスから、ウィンドウをもっているプロセスを抽出したい。
- 抽出したプロセスの実行ファイル名を取得したい。
の2点である。
そして、前提条件として64bit環境で実行しているが、
32bitでビルドされたdllを使用しており、ビルドの構成はX86になっている。
素直にやると、こうなる
foreach (var p in Process.GetProcesses()) { // ウィンドウを持つプロセスのみ取得する if (p.MainWindowHandle != IntPtr.Zero) { Console.WriteLine(p.MainModule.FileName); } }
しかし、これを64bit環境で実行すると、
p.MainModuleにアクセスした時点で
32 ビット プロセスは、64 ビット プロセスのモジュールにアクセスできません。
とエラーが出る。
しょうがないので、ManagementClassから一度必要な情報を抽出して、
後からウィンドウ持ちのプロセスIDで引き当てる事にした。
using (var mc = new System.Management.ManagementClass("Win32_Process")) using (var moc = mc.GetInstances()) { var dic = new Dictionary<string, string>(); foreach (var mo in moc) { if (mo["ProcessId"] != null && mo["ExecutablePath"] != null && !dic.ContainsKey(mo["ProcessId"].ToString())) dic.Add(mo["ProcessId"].ToString(), mo["ExecutablePath"].ToString()); mo.Dispose(); } foreach (var p in Process.GetProcesses()) { // ウィンドウを持つプロセスのみ取得する if (p.MainWindowHandle != IntPtr.Zero) { if (dic.ContainsKey(p.Id.ToString())) Console.WriteLine(p.MainModule.FileName); } } }
どなたか、他に良い方法ご存じないですか?