最近のいわゆる OS 上で直接動作するデスクトップアプリも、iPhone の影響かぴょこぴょこ動くのが多くなってきた気がします。 Linux のアプリでもこの手のがありますが、だいたいは WebKit + Python でやっている模様。 じゃー、Groovy でもやってやろうじゃないかというのがこの企画でありまして、でもって結果 SWT + WebKit はなかなか強力な感じな気がします。 🙂
どういう風にするかというと、まずウインドを作成して WebKit のコンポーネントをぺたっと張ります。 でもって、Groovy 側から処理したいタイミングがあれば、JavaScript 経由で処理結果を WebKit の HTML に描画。 WebKit 側から Groovy を呼びたければ、これまた JavaScript を経由して処理をして値をもらう、みたいな感じです。
Groovy 側では JavaScript では処理できないような、ローカル資源を扱う処理をかいてあげて、WebKit 側にはビューを担当させます。 ここでは HDD にある画像ファイル一覧を読み出して画面に表示するようにしています。 まぁ HTML5 とか使うとこれはできるのかもですが、なんちゃってサンプルということで。 Java のライブラリで EXIF でも読めばそれっぽかったですね。
さて、HTML + JavaScript をビューに使うメリットは多くの派手な画面を描けるライブラリが使えることで、ここでは jQuery + jQuery Mobile を使って iPhone ちっくな画面とアニメにしてみました。 🙂
画像サイズが不揃いでかっこわるくてすいません、すいません。。
デバッグ用にツールバーとか、タブとかつけていますが、本来的にはこれはなくなるもので、ミニブラウザと言うべきものがアプリの体裁となります。
上の画面から下の拡大画面にアニメで遷移、、ということでこれは動画にて。 😀
Groovy – JavaScript インターフェースは、このスクラップコードでいいますと「HTML ソース表示」の部分が Groovy –> JavaScript 呼び出し。 ローカルファイル表示の部分が JavaScript –> Groovy 呼び出しとしています。
Groovy –> JavaScript は簡単で SWT の browser コンポーネントにメソッドが用意されています。
Groovy 側。(タブ切り替えたときのイベント)
tabFolder.addSelectionListener([
widgetSelected : { SelectionEvent e ->
if(e.item == tabSource) {
source.setText(screen.evaluate('return getContentHtml()'))
}
},
widgetDefaultSelected : {}] as SelectionListener)
JavaScript 側。
// タブ切替時にソース表示を行う。
function getContentHtml() {
return $('html').html();
}
抜粋コードなのであれですが、Groovy から evaluate を発行して JavaScript から返値を string でもらっています。 evaluate の中身に return をかくのを忘れてしばらくはまっていたのはここだけの秘密です(笑)
今度は逆パターン。 (たとえば) jQuery の ready を契機に Groovy のメソッドを呼び出すパターン。
JavaScript 側。 getFileList() がそれです。 値もらって HTML くみたてています。
$(document).ready(function() {
var fileList = eval("(" + getFileList() + ")");
for (var name in fileList) {
$("#filelist").prepend(
"<li class='ui-li-has-thumb ui-li ui-li-static ui-body-b'>"
+ "<a href='#bar' class='ui-link-inherit'"
+ "onclick='imageChange(\"" + fileList[name] + "\")'>"
+ "<img src='" + fileList[name] + "' class='ui-li-thumb'/>"
+ "<h3 class='ui-li-heading'>" + name + "</h3></li>\n")
+ "</a>";
}
$('ul').listview('refresh');
});
Groovy 側、 JavaScript とのインターフェースをとるために SWT には BrowserFunction というクラスが用意されています。 これがエロい。
event = new BrowserFunction(screen, "getFileList") {
public Object function(Object[] arguments) {
return getFileList(arguments);
}
}
def getFileList(args) {
def files = [:]
new File("${imagePath}").eachFileMatch(~/.*\.jpg/) {
files[it.name] = it.path
}
return new JsonBuilder(files).toString()
//return [10, 20] as Object[]
}
ここで我らが Groovy。 ということで返値に JSON を使っています。 JSON で Map を構築して return して JavaScript 側で eval すれば良い感じでデータの受け渡しができます。
インターフェース的には、org.eclipse.swt.browser.WebKit:convertToJS() あたりをみると分かりますが、String、Boolean、Number、Object[] がやりとりできるようです。 Object[] はこれらの再起です。 上の最後のソースの再下位行が Object[] で返す場合の記述です。
以上、まぁアプリの作りとしては若干強引な気もしますが、既存 GUI コンポーネントを使わずに、特にリキットデザインにしたい UI の場合はこの方式は楽かもしれません。
てなわけで、、シルバーらい子ちゃんをだしておいて、なぜ Silverlight じゃないんだっ、WPF じゃないんだっ。 しかも Windows じゃないし! ゆ、ゆるさないんだからね! となど聞こえてきそうですが、ここはご勘弁ください。。
あぁ、らい子ちゃん・・・。(←はまりすぎ
あ、そうそう SWT + WebKit は Ubuntu 11.04 だと SWT 3.7 の現在 head を使わないと貼り付けられないのでご注意ください。 あとね、、なぜかその WebKit で jQuery Mobile を使うとアプリ終了時に WebKitGTK+ がセグるようです。 なんだろうなぁ。 ちょっと調べてみます。
—-
以下、スクラップソースなので全然アプリケーションの体裁になっていませんが、一応動くコードをおいておきます。
Groovy 側、
import groovy.json.JsonBuilder
import org.eclipse.swt.*
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.BrowserFunction
import org.eclipse.swt.browser.ProgressListener
import org.eclipse.swt.custom.CTabFolder
import org.eclipse.swt.custom.CTabItem
import org.eclipse.swt.events.SelectionEvent
import org.eclipse.swt.events.SelectionListener
import org.eclipse.swt.layout.GridData
import org.eclipse.swt.layout.GridLayout
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text
import org.eclipse.swt.widgets.ToolBar
import org.eclipse.swt.widgets.ToolItem
class BrowserApplication {
Display display
Shell shell
CTabFolder tabFolder
CTabItem tabScreen
CTabItem tabSource
Browser screen
Text source
Label statusBar
ToolItem toolbarReload
boolean ready
def event
def url
def imagePath = "/home/hiromasa/ピクチャ/wallpaper/"
def init() {
display = new Display()
// シェル
shell = new Shell(display)
shell.setSize(540, 640)
shell.setLayout(new GridLayout())
shell.setText("シルバーらい子ちゃん")
// ツールバー作成
ToolBar toolbar = new ToolBar(shell, SWT.NULL)
toolbarReload = new ToolItem(toolbar, SWT.PUSH)
toolbarReload.setText("リロード")
// CTabFolder
tabFolder = new CTabFolder(shell, SWT.BORDER)
tabFolder.setTabPosition(SWT.BOTTOM)
tabFolder.setSimple(false)
tabFolder.setTabHeight(20)
tabScreen = new CTabItem(tabFolder, SWT.NONE)
tabScreen.setText("スクリーン")
tabSource = new CTabItem(tabFolder, SWT.NONE)
tabSource.setText("ソース")
tabFolder.setSelection(0)
// CTabFolder - Layout
GridData tabLayout = new GridData()
tabLayout.horizontalAlignment = GridData.FILL
tabLayout.verticalAlignment = GridData.FILL
tabLayout.grabExcessHorizontalSpace = true
tabLayout.grabExcessVerticalSpace = true
tabFolder.setLayoutData(tabLayout)
// WebKit
url = new File(".").getAbsolutePath()
screen = new Browser(tabFolder, SWT.WEBKIT)
screen.setUrl("file:///${url}/script/screen.html")
tabScreen.setControl(screen)
// ソース
source = new Text(tabFolder, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL)
tabSource.setControl(source)
// ステータスバー
statusBar = new Label(shell, SWT.LEFT)
statusBar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL))
}
def handle() {
// screen.html 読み込み終了検知
screen.addProgressListener([
changed : { },
completed : {
ready = true
statusBar.setText("Ready")
}] as ProgressListener)
// リロードボタン
toolbarReload.addSelectionListener([
widgetSelected : {
screen.refresh()
},
widgetDefaultSelected : {}] as SelectionListener
)
// Groovy からブラウザの JavaScript を呼び出して HTML テキストをもらう。
tabFolder.addSelectionListener([
widgetSelected : { SelectionEvent e ->
if(e.item == tabSource) {
source.setText(screen.evaluate('return getContentHtml()'))
}
},
widgetDefaultSelected : {}] as SelectionListener)
// JavaScript から Groovy を呼び出すための JS 関数を登録する。
event = new BrowserFunction(screen, "getFileList") {
public Object function(Object[] arguments) {
return getFileList(arguments);
}
}
}
// ファイル一覧を取得して JSON 形式で返却
def getFileList(args) {
def files = [:]
new File("${imagePath}").eachFileMatch(~/.*\.jpg/) {
files[it.name] = it.path
}
return new JsonBuilder(files).toString()
//return [10, 20] as Object[]
}
def loop() {
try {
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) display.sleep();
}
} catch (Exception e) {
// TODO:
e.printStackTrace()
} finally {
display.dispose()
}
}
def BrowserApplication() {
init()
handle()
loop()
}
def static main(def args) {
new BrowserApplication();
}
}
JavaScript + HTML。
<!DOCTYPE html>
<html>
<head>
<title>jQuery Moblie test</title>
<link rel="stylesheet" href="./css/jquery.mobile-1.0a4.1.css" />
<link rel="stylesheet" href="./css/style.css" />
<script src="./js/jquery-1.6.1.js"></script>
<script src="./js/jquery.mobile-1.0a4.1.js"></script>
<script language="JavaScript">
$(document).ready(function() {
// 右クリックでコンテキストメニューを表示しない。
$('body').live('contextmenu', function(e){
e.preventDefault();
});
// ファイル一覧取得
var fileList = eval("(" + getFileList() + ")");
for (var name in fileList) {
$("#filelist").prepend(
"<li class='ui-li-has-thumb ui-li ui-li-static ui-body-b'>"
+ "<a href='#bar' class='ui-link-inherit'"
+ "onclick='imageChange(\"" + fileList[name] + "\")'>"
+ "<img src='" + fileList[name] + "' class='ui-li-thumb'/>"
+ "<h3 class='ui-li-heading'>" + name + "</h3></li>\n")
+ "</a>";
}
$('ul').listview('refresh');
});
// 詳細画面に遷移
function imageChange(path) {
$("#image").attr({src: path});
return true;
}
// タブ切替時にソース表示を行う。
function getContentHtml() {
return $('html').html();
}
</script>
</head>
<body>
<!-- メインページ -->
<div data-role="page" data-theme="b" class="setting">
<div data-role="header" data-position="fixed">
<h1>らい子ちゃん</h1>
</div>
<ul data-role="listview" data-theme="b" id="filelist">
</ul>
<div data-role="footer" data-id="footer" id="footer" data-position="fixed">
<h4>ばよえ〜ん</h4>
</div>
</div>
<!-- サブページ -->
<div data-role="page" id="bar" data-theme="b" class="setting">
<div data-role="header">
<h1>らい子ちゃん</h1>
</div>
<div data-role="content">
<img id="image" src="" />
</div>
<div data-role="footer" data-id="footer" id="footer" data-position="fixed">
<h4>ぱんなこった〜</h4>
</div>
</div>
</body>
</html>