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