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を用意しましょう。
いろいろフォルダとファイルを用意します。
f:id:alocoholic_babay:20131011093714p:plain

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
に設定しましょう。


それでは、実行してみましょう。
f:id:alocoholic_babay:20131010201120p:plain
読み込みましたね。
(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として設定します。


実行してみましょう。
クリップボードにテキストがコピーされる度に、一覧にアイテムが追加されます。
f:id:alocoholic_babay:20131010202249p:plain


次に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