フルスクリーンのゲーム画面の上にオーバーレイ表示するアプリを作る方法の参考となる情報です。

具体的には FFXIV で試した方法です。参考のソースは以下を参照してください。

別のゲーム(シャドウ・オブ・ウォー)で同じ方法を試したところ上手く行かなかったので参考程度の情報となる点に注意してください。

それでも解決策を探すための糸口ぐらいにはなるかもしれません。

タイトルにある通り WPF で作った場合の事例です。WPF の機能だけでは実現できず Win32 API を駆使します。

FFXIV のフルスクリーンモードの画面の上にオーバーレイ表示するための必要最小限の条件は以下の通りでした。

  • Window に以下の属性を設定する。この処理を指定しないとゲーム画面の上に表示されない。
    • WindowStyle="None"
    • AllowsTransparency="true"
    • Topmost="true"
    • SourceInitialized="Window_SourceInitialized"
  • Window_SourceInitialized で以下の処理を実行する。この処理は表示したウィンドウがゲームのウィンドウからフォーカスを奪わないために指定する。
    • Windows API の SetWindowLong 関数を使って対象のウィンドウに WS_EX_NOACTIVATE を追加する。
  • Window の InitializeComponent 処理の後、任意のタイミングで Windows API の SetWindowPos を使って対象のウィンドウを最上部に表示するため第二引数に HWND_TOP を指定して呼び出す。この処理の呼び出しは必ずゲーム画面がフォーカスを持った状態で行う。

最後の SetWindowPos の呼び出しについて補足します。

HWND_TOP 指定の SetWindowPos は、実行タイミングが重要です。これは1度呼び出したらずーっと上に表示してくれるというものではなく、その「時点」で最上部に表示するという処理です。

従って、ゲーム画面がフォーカスを持った状態で行う必要があります。どのタイミングで実行するかは作るアプリ次第での工夫になると思います。

サンプルでは実行後、Application_Startup で 5 秒のスリープ後にウィンドウを表示するという実装になっています。

ウィンドウが表示されるまでにフルスクリーンのゲーム画面にフォーカスを移動してください。

ウィンドウが表示された後にゲーム画面にフォーカスを移動しても、ウィンドウは最上部に表示されないことも確認できます。

前提:UI の国際化

PukiWiki は LANG 定数の設定に応じて各種 UI 文字列の設定を読み替える機構をデフォルトで持っています。

LANG 定数の定義は PukiWiki.ini.php にあり、デフォルトの定義は日本語(ja)固定になっています。

/////////////////////////////////////////////////
// Language / Encoding settings

// LANG - Internal content encoding ('en', 'ja', or ...)
define('LANG', 'ja');

以上の設定を元に読み込まれる ja.lng.php と en.lng.php が切り替わります。

何らかのルールを作ってこれが動的に設定されるようにすれば UI の多言語化は簡単にできます。

例えばサブドメインの文字列(www.ownway.info の先頭の www の部分)によって切り替える簡単な実装は以下の通りです。

/////////////////////////////////////////////////
// Language / Encoding settings

// LANG - Internal content encoding ('en', 'ja', or ...)
list($subdomain) = explode('.', $_SERVER['HTTP_HOST']);
define('LANG', $subdomain);

以上のコードはドメインの指定に ja.ownway.info や en.ownway.info 等が来ることを想定しています。

それ以外の値が来た場合は考慮に入れていないのでもう少しコードを書く必要が出てくるかもしれません。

また、サブドメインを自由に設定できない場合は別の方法を考える必要がありますが、やるべきことはリクエストを元に適切な LANG を設定することです。

ここまでが前提となる UI の国際化です。

本題:コンテンツの国際化

LANG の指定に従って、UI だけではなく内部のコンテンツもその言語特有のものに切り替える方法のヒントを紹介します。

ちなみにコンテンツの国際化について PukiWiki-dev の以下のページに 2005 年頃の議論があります。

リンクが切れていて、この当時の再現ができませんが、この方法とは別の方法です。

PukiWiki の標準実装を使い、以下の単純な設定変更で実現します。

  • LANG の設定に従ってデータフォルダの読み込み先を切り替える。

具体的には PukiWiki.ini.php にあるディレクトリの設定を以下のように修正します。

// You may hide these directories (from web browsers)
// by setting DATA_HOME at index.php.

define('DATA_DIR', DATA_HOME . LANG . '/wiki/' ); // Latest wiki texts
define('DIFF_DIR', DATA_HOME . LANG . '/diff/' ); // Latest diffs
define('BACKUP_DIR', DATA_HOME . LANG . '/backup/' ); // Backups
define('CACHE_DIR', DATA_HOME . LANG . '/cache/' ); // Some sort of caches
define('UPLOAD_DIR', DATA_HOME . LANG . '/attach/' ); // Attached files and logs
define('COUNTER_DIR', DATA_HOME . LANG . '/counter/' ); // Counter plugin's counts

ポイントは、読み込みの起点を DATA_HOME . LANG にしているところです。

wiki, diff, backup, cache, attach, counter の設定を変更していますが、添付ファイルだけは共有したいということがあれば attach の設定は変えなくても OK です。

設定を変えたら PukiWiki 直下に ja と en というディレクトリを作って、wiki, diff, backup, cache, attach, counter をそれぞれにコピーします。

以上の説明は少し雑で PukiWiki 直下に ja と en を置くよりは data/ja や data/en という形にした方がすっきりするので、DATA_HOME 自体の定義も変えるなど配置は調整した方が良いです。

これだけで LANG の設定に従ったコンテンツの国際化対応が概ね完了です。

後は言語ごとにコンテンツを用意するという本来の作業をしましょう!

システム的には言語切り替え機能や alternate タグの設置機能なども欲しくなってくると思いますがそれはまた別の話です。

このページは自分と同じ穴に落ちた人向けへのページです。

onLongClick イベントへのバインドがうまくいかなかった。

初めて Button の onLongClick イベントにメソッドをバインドしようとした際に失敗しました。

以下のようなメッセージが出力されてコンパイルに失敗しました。

Error:Execution failed for task ':note-app-main:compileDevelopmentDebugJavaWithJavac'.
> java.lang.RuntimeException: failure, see logs for details.
cannot generate view binders java.lang.StackOverflowError
at android.databinding.tool.writer.Scope.access$getCurrentScope$cp(LayoutBinderWriter.kt:49)
at android.databinding.tool.writer.Scope$Companion.getCurrentScope(LayoutBinderWriter.kt:58)
at android.databinding.tool.writer.LayoutBinderWriterKt.scopedName(LayoutBinderWriter.kt:196)
at android.databinding.tool.expr.Expr.toCode(Expr.java:776)
at android.databinding.tool.writer.LayoutBinderWriterKt$callbackLocalName$2.invoke(LayoutBinderWriter.kt:203)
at android.databinding.tool.writer.LayoutBinderWriterKt$callbackLocalName$2.invoke(LayoutBinderWriter.kt)
at android.databinding.tool.ext.LazyExt.getValue(ext.kt:27)
at android.databinding.tool.writer.LayoutBinderWriterKt.getCallbackLocalName(LayoutBinderWriter.kt)
at android.databinding.tool.writer.LayoutBinderWriterKt.scopedName(LayoutBinderWriter.kt:197)
...

Android の以下の公式の説明ページに onLongClick が載っているので使えるという予測にも反しての原因不明の失敗でした。

バインドしようとしたメソッドの戻り値を確かめよう。

バインドしようとしたメソッドの戻り値を確かめてください。

void にしていませんか?

戻り値を boolean にしてみてください!!!

理由は簡単です。

View.OnLongClickListener の onLongClick の戻り値が boolean で、これに合わせる必要があるからです。

View.OnClickListener の戻り値が void なので、そのままの感覚で onLongClick 向けのイベントを用意するとこの落とし穴に落ちます(私は落ちました)。

単純なプログラムだとちゃんと「Error:(5, 16) エラー: 不適合な型: 予期しない戻り値」という表示が出るようです。

StackOverflowError になってしまう細かい条件まではわかりません。

参考

この話題は既に Android の開発チケットとして上がっているようです。

「You should return boolean in your Presenter/MainActivity method onLongClick.」のコメントを読んで、自分の間違いに気づきました。

この記事は日本人向けに書きました。

レコメンドエンジンの OpenSlopeOne について調べてみた。

簡単に使えるレコメンドエンジンがないか調べていたところ、OpenSlopeOne というのに行き着きました。

PHP で簡単に使えるものをということで OpenSlopeOne にたどり着いたのですが、 情報がほとんどなくよくわからないので諦めようと思ったのですが、ソースコードの中身を軽く読んでみたらものすごく小さいです。 大雑把だけどすぐに読み切れる程度で大よその使い方がわかったのでまとめてみようと思います。

ざっくり概要

OpenSlopeOne がどんなものなのかをざっくり箇条書きでまとめます。

  • とても単純な協調フィルタリングにより推薦アイテム一覧を取得できるようになる。
  • 必要なデータベースのテーブル定義を提供してくれる。
    • oso_user_ratings が入力データテーブルで、ユーザとアイテムの結び付きを表す。
    • oso_slope_one が出力データテーブルで oso_user_ratings のデータから2アイテム間の結び付きが計算されてここに入る。
  • 分析実施のためのインターフェイスを提供してくれる。
    • OpenSlopeOne::initSlopeOneTable は oso_user_ratings から oso_slope_one のデータを作成する処理である。
    • OpenSlopeOne::initSlopeOneTable は、全てのデータを再計算する実装となっているため、レコメンド取得時に毎回行うものとは違うように思われる。
    • 日次・週次・月次バッチなどで oso_slope_one を更新する想定のように思える。
  • レコメンド計算元のデータ入力用インターフェイスの提供はない。
    • oso_user_ratings へのデータ入力用インターフェイスは存在しないので、別途 oso_user_ratings へ入れる処理は自分で実装する必要がある。
    • oso_user_ratings は、ユーザ(user_id)とアイテム(item_id)の結び付き(rating)の情報である。
      • 何をユーザと考えるか、何をアイテムと考えるか、結び付きをどう設定するかを要件に応じて決め、そこに実際の値を当てはめる必要がある。
    • user_id も item_id も int(11) として定義されているので、状況に応じて型は変えてしまえば良いように思う。
      • 型の要件は一致判断ができることだけなので文字列に変えてしまっても問題ないと思う。
  • 推薦アイテム一覧取得のためのインターフェイスを提供してくれる。
    • 推薦アイテム一覧の取得方法にはアイテム指定とユーザ指定の2種類の方法が用意されている。
    • OpenSlopeOne::initSlopeOneTable により oso_slope_one に計算値が事前に入っていることが前提となる。

使うためには。

以下のような作業をする必要があります。

  • ユーザとアイテムと結び付きを何にするのか考える。
  • データ登録用インターフェイスを実装し、アクセス箇所でデータの登録が行われるようにする。
  • OpenSlopeOne::initSlopeOneTable を呼びレコメンド情報を計算するためのバッチを実装する。cron 等で日次・週次・月次等で実行されるようにする。
    • 推薦アイテム一覧取得時に毎回実行するにはオーバーヘッドが大きいように思う。特に本体ページの PHP に埋め込んで使うのは問題がありそうに見える。
    • 他の方法としては推薦アイテム一覧表示部分を Ajax 等で本体から切り離し非同期にした上で、擬似 cron 的な実装で OpenSlopeOne::initSlopeOneTable 呼び出しを行う工夫をする。
  • 推薦相手有無一覧取得 API を使ってレコメンド表示箇所にアイテムを表示する機能を実装する。
    • PHP 埋め込みでも良いし、Ajax 等で画面部品として切り離しても良い。

とあるパスからの相対パスでリソースを特定したい場合があります。

そんなときどうするかというと PEAR の Net_URL2 にある resolve メソッドを使います。

たとえば以下のような感じに書けます。

<?php
require_once "Net/URL2.php";

$url = new Net_URL2("http://abc.def.ghi/jkl/mno/");
$new_url = $url->resolve("../pqr/xyz");
?>

複数の PukiWiki サイトを管理している/管理しようとしているあなたへ送る PukiWiki サイトを一括管理する方法のまとめです。

概要

以下、前提です。

  • 1つのウェブサーバ上に複数の PukiWiki サイトを設置・管理する場合を想定している。
  • PukiWiki システム本体を1つにし、複数のサイトで共有する。
    • PukiWiki のカスタマイズやバージョンアップ対応などは、この PukiWiki システム本体に対して集中的に行うことで対応できるようにする。
    • 全てのサイトで利用可能なデザイン(スキン・CSS)・プラグインを提供する形にする。
  • 各サイトは独自の設定・データ・デザイン・プラグインだけを管理する。
    • PukiWiki システム本体部分に持たせたデザイン・プラグインはもちろん使える。
    • 必要に応じて独自デザインの適用や独自プラグインの利用ができるようにする。

ざっくりと以下のようにファイルを配置して運用する形を想定します。

  • ServerRoot/PukiWiki(PukiWiki システム本体)
    • ja.lng.php
    • en.lng.php
    • rules.ini.php
    • image, lib, plugin, skin
  • ServerRoot/Site1(http://xxx.yyy.zzz/Site1/
    • index.php
    • attach, backup, cache, counter, diff, image, plugin, skin, trackback, wiki
  • ServerRoot/Site2(http://xxx.yyy.zzz/Site2/
    • index.php
    • attach, backup, cache, counter, diff, image, plugin, skin, trackback, wiki
  • ServerRoot/Site3(http://xxx.yyy.zzz/Site3/
    • index.php
    • attach, backup, cache, counter, diff, image, plugin, skin, trackback, wiki
  • ...

カスタマイズ概要

どこをどういう風にカスタマイズするか簡単に説明します。

  • 定数のカスタマイズ
    • PukiWiki のディレクトリ構成を決めている各種定数(XXX_DIR)を書き換えてシステム全体がアクセスする場所を制御する。
    • Web リソースアクセスがコントロールしやすいよう URI 定数群を導入する。
    • ディレクトリ定数と URI 定数を PukiWiki 本体とサイト独自の2系統用意し、状況に応じて使い分けができるようにする。
  • exist_plugin メソッドを書き換えて、PukiWiki 本体とサイト独自の両方のプラグインを認識してくれるようにする。

カスタマイズ内容

具体的なカスタマイズ箇所を見ながら説明します。

まずはディレクトリ構成に関する定数は index.php と pukiwiki.ini.php に定義されています。

  • index.php
// Directory definition
// (Ended with a slash like '../path/to/pkwk/', or '')
define('DATA_HOME',	'');
define('LIB_DIR',	'lib/');
  • pukiwiki.ini.php
/////////////////////////////////////////////////
// Directory settings I (ended with '/', permission '777')

// You may hide these directories (from web browsers)
// by setting DATA_HOME at index.php.

define('DATA_DIR',      DATA_HOME . 'wiki/'     ); // Latest wiki texts
define('DIFF_DIR',      DATA_HOME . 'diff/'     ); // Latest diffs
define('BACKUP_DIR',    DATA_HOME . 'backup/'   ); // Backups
define('CACHE_DIR',     DATA_HOME . 'cache/'    ); // Some sort of caches
define('UPLOAD_DIR',    DATA_HOME . 'attach/'   ); // Attached files and logs
define('COUNTER_DIR',   DATA_HOME . 'counter/'  ); // Counter plugin's counts
define('TRACKBACK_DIR', DATA_HOME . 'trackback/'); // TrackBack logs
define('PLUGIN_DIR',    DATA_HOME . 'plugin/'   ); // Plugin directory

/////////////////////////////////////////////////
// Directory settings II (ended with '/')

// Skins / Stylesheets
define('SKIN_DIR', 'skin/');
// Skin files (SKIN_DIR/*.skin.php) are needed at
// ./DATAHOME/SKIN_DIR from index.php, but
// CSSs(*.css) and JavaScripts(*.js) are needed at
// ./SKIN_DIR from index.php.

// Static image files
define('IMAGE_DIR', 'image/');
// Keep this directory shown via web browsers like
// ./IMAGE_DIR from index.php.

ここを以下のように書き換えます。

  • index.php
    • PKWK_HOME は PukiWiki 本体の場所を表す。
    • DATA_HOME は サイト独自の場所を表す。
    • URI は HTTP でアクセスする場合に使用する URL の値を表す。
    • PKWK_URI が PukiWiki 本体の場所を表す。すなわち共有リソースの場所である。
    • ROOT_URI はサイト独自の場所を表す。すなわち独自リソースの場所である。
define('PKWK_HOME', '../PukiWiki/');
define('DATA_HOME', '');
define('LIB_DIR', PKWK_HOME . 'lib/');

// Base URL
define('ROOT_URI', preg_replace('#[^/]*$#', '', $_SERVER['SCRIPT_NAME']));
define('PKWK_URI', ROOT_URI . PKWK_HOME);
  • pukiwiki.ini.php
    • 先頭 C_ なしのディレクトリや URI が PukiWiki 本体の場所である。
    • 先頭 C_ 付きのディレクトリや URI がサイト独自の定義である。
    • ただし、data, diff, backup ... などはサイト独自のものしかないので、そのままである。

サイト独自のものに先頭 C_ 付きの新しい名前をつけるのは、PukiWiki 本体のリソースを標準として使うためです。

/////////////////////////////////////////////////
// Directory/URI (ended with '/')

define('DATA_DIR',      DATA_HOME . 'data/wiki/'     ); // Latest wiki texts
define('DIFF_DIR',      DATA_HOME . 'data/diff/'     ); // Latest diffs
define('BACKUP_DIR',    DATA_HOME . 'data/backup/'   ); // Backups
define('CACHE_DIR',     DATA_HOME . 'data/cache/'    ); // Some sort of caches
define('UPLOAD_DIR',    DATA_HOME . 'data/attach/'   ); // Attached files and logs
define('COUNTER_DIR',   DATA_HOME . 'data/counter/'  ); // Counter plugin's counts
define('TRACKBACK_DIR', DATA_HOME . 'data/trackback/'); // TrackBack logs
define('PLUGIN_DIR',    PKWK_HOME . 'plugin/'        ); // Plugin directory
define('C_PLUGIN_DIR',  DATA_HOME . 'plugin/'        ); // Plugin directory

define('SKIN_DIR',      PKWK_HOME . 'skin/'          );
define('C_SKIN_DIR',    DATA_HOME . 'skin/'          );
define('CSS_DIR',       PKWK_HOME . 'css/'           ); // CSS directory
define('C_CSS_DIR',     DATA_HOME . 'css/'           ); // CSS directory
define('IMAGE_DIR',     PKWK_HOME . 'image/'         );
define('C_IMAGE_DIR',   DATA_HOME . 'image/'         );

define('CACHE_URI',     PKWK_URI . 'cache/'          );
define('SKIN_URI',      PKWK_URI . 'skin/'           );
define('C_SKIN_URI',    ROOT_URI . 'skin/'           );
define('IMAGE_URI',     PKWK_URI . 'image/'          );
define('C_IMAGE_URI',   ROOT_URI . 'image/'          );
define('CSS_URI',       PKWK_URI . 'css/'            );
define('C_CSS_URI',     ROOT_URI . 'css/'            );
define('JS_URI',        PKWK_URI . 'js/'             );
define('C_JS_URI',      ROOT_URI . 'js/'             );

定数を定義したら使っている箇所を必要に応じて書き換えます。

まず最初に default.ini.php の先頭です。

if (defined('TDIARY_THEME')) {
	define('SKIN_FILE', SKIN_DIR . 'tdiary.skin.php');
} else {
	define('SKIN_FILE', SKIN_DIR . 'pukiwiki.skin.php');
}

SKIN_DIR は PukiWiki システム本体のスキンを使うことを意味します。 もしもサイト独自のスキンを使う場合は SKIN_DIR を C_SKIN_DIR に書き換えます。

スキン全般の Web リソースアクセス個所の書き換えも必要です。以下事例です。

  • skin/pukiwiki.skin.php

書き換え前

 <link rel="stylesheet" type="text/css" media="screen" href="skin/pukiwiki.css.php?charset=<?php echo $css_charset ?>" charset="<?php echo $css_charset ?>" />

書き換え後

 <link rel="stylesheet" type="text/css" media="screen" href="<?php echo SKIN_URI ?>/pukiwiki.css.php?charset=<?php echo $css_charset ?>" charset="<?php echo $css_charset ?>" />

などです。

以上の書き換え例は PukiWiki システム本体の CSS スキンを使う場合の例ですが、 もしもサイト独自の CSS を使う場合は SKIN_URI を C_SKIN_URI に書き換えます。

ファイル構成としては PukiWiki システム本体に標準スキンを置いておき、サイト独自のスキンは必要に応じて設置します。不要なら削除してかまいません。

画像や CSS や JavaScript などの各種リソース群もスキンと同様の考え方をします。 標準のものを使うか独自のものを使うかはアクセスする場所で使用する定数の違いによって表現します。

最後にプラグインに関する調整を行います。

このままではサイト独自のプラグイン定義ができません。標準の PukiWiki はプラグインが単一のディレクトリに入っていることを想定しているからです。

一カ所書き換えれば対応できます。それが lib/plugin.php の exist_plugin 関数です。

// Check plugin '$name' is here
function exist_plugin($name)
{
	global $vars;
	static $exist = array(), $count = array();

	$name = strtolower($name);
	if(isset($exist[$name])) {
		if (++$count[$name] > PKWK_PLUGIN_CALL_TIME_LIMIT)
			die('Alert: plugin "' . htmlsc($name) .
			'" was called over ' . PKWK_PLUGIN_CALL_TIME_LIMIT .
			' times. SPAM or someting?<br />' . "\n" .
			'<a href="' . get_script_uri() . '?cmd=edit&amp;page='.
			rawurlencode($vars['page']) . '">Try to edit this page</a><br />' . "\n" .
			'<a href="' . get_script_uri() . '">Return to frontpage</a>');
		return $exist[$name];
	}

	// 書き換え開始(C_PLUGIN_DIR を先に見て、なければ PLUGIN_DIR を見る)
	if (preg_match('/^\w{1,64}$/', $name)) {
		if (file_exists(C_PLUGIN_DIR . $name . '.inc.php')) {
			$exist[$name] = TRUE;
			$count[$name] = 1;
			require_once(C_PLUGIN_DIR . $name . '.inc.php');
			return TRUE;
		}

		if (file_exists(PLUGIN_DIR . $name . '.inc.php')) {
			$exist[$name] = TRUE;
			$count[$name] = 1;
			require_once(PLUGIN_DIR . $name . '.inc.php');
			return TRUE;
		}
	}

	$exist[$name] = FALSE;
	$count[$name] = 1;
	return FALSE;
	// 書き換え終了
}

これでサイト独自のプラグイン導入ができるようになります。

スキン等で直接プラグインを呼び出したい場合は、以下の書き方をすれば OK です。

<?php if (exist_plugin("pluginname")) { echo plugin_example_inline(); } ?>

このページを書くにいたった経緯

自分はホームページ全般を PukiWiki で作成しています。

Wiki でページが掛けるのはとても楽だし、特別なページが必要であればプラグイン製作で大抵のことが実現できます。

アクセスさえできればどこからでも編集できる気軽さも嬉しいです。パソコンからでもスマホからでも編集できます。

デザインを変えたいと思えばスキンを変えればそれだけでサイト全体のデザインが一括で変わります。

そんなこともあってたくさんの PukiWiki を設置していました。たくさん設置していたのはテーマ毎に分けて設置していたからです。

まずまず満足してましたが PukiWiki の開発が止まっていることだけが気がかりでした。他の Wiki に乗り換えようかと思う日々が続きました。

しかし、その不安を打ち破る PukiWiki 1.5.0 の公開に心躍りました!新しいのに乗り換えよう!すぐにそう思ったけど今日までできずにいました。

たくさんの PukiWiki が立っていたからです。メンテナンスの問題がそこにあったのです。

この解決策により今後のバージョンアップ対応時の不安も払拭されました。安心してバージョンアップできるようになりました。

ヒント:複数ウェブサーバで PukiWiki を運営しているとしたらどうすればいいか。

複数のウェブサーバで PukiWiki を使っている人がいるでしょう。

自分は実際にやってないのでなんともいえないですがヒントだけ書いておきます。

まず、全てのウェブサーバでここで書いている方式をとりましょう。

そして PukiWiki システム本体部分をバージョン管理システムに載せて同期が取れるようにしましょう。

PukiWiki システム本体とサイト独自の部分が分離されるので、このようなことがしやすくなるはずです。

Android 標準で使える SQLite にアクセスするには SQLiteDatabase クラスを使います。

そして、SQLiteDatabase クラスの query メソッドなどで SELECT 文を発行する場合、WHERE 句に ? パラメータを使うことができます。

query メソッドの定義を以下に示します。

public Cursor query (String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)

selection 引数が WHERE 句です。そして、selectionArgs 引数に ? パラメータに埋め込む値を配列で渡します。

例えば 10 歳から 20 歳までの人の名前を検索する getNames メソッドというのを書いてみましょう。

query メソッドの呼び出しに注目してください。

public class HumanDao {
	public List<String> getNames(SQLiteDatabase database, int minAge, int maxAge) throws Exception {
		Cursor cursor = database.query(
			"human",
			new String[]{"name"},
			"age <= ? AND age < ?",
			new String[]{Integer.toString(minAge), Integer.toString(maxAge)},
			null, null, null, null);

		List<String> result = new ArrayList<>();
		while (cursor.moveNext()) {
			result.Add(cursor.getString("name"));
		}

		return result;
	}
}

さて、以上の処理を進化させて、年齢の上限と下限の条件を必須ではなくオプションにしたいと思います。

ここでいうオプションとは、指定があれば条件に含めるし、無ければ条件から外すということを表しています。

getNames メソッドでは上限・下限に int 型を使っていまいたが、条件なしを null で表せるように Integer 型に変更します。

その上でオプションをどうやって表すか、一つの方法は値の有無に応じて selection / selectionArgs 引数の値を組み立てることです。

public class HumanDao {
	public List<String> getNames2(SQLiteDatabase database, Integer minAge, Integer maxAge) throws Exception {
		Cursor cursor = database.query(
			"human",
			new String[]{"name"},
			"age <= ? AND age < ?",
			getSelectionForGetNames(minAge, maxAge),
			getSelectionArgsForGetNames(minAge, maxAge),
			new String[]{Integer.toString(minAge), Integer.toString(maxAge)},
			null, null, null, null);

		List<String> result = new ArrayList<>();
		while (cursor.moveNext()) {
			result.Add(cursor.getString("name"));
		}

		return result;
	}

	private String[] getSelectionForGetNames(Integer minAge, Integer maxAge) {
		if (minAge == null && maxAge == null) {
			null;
		}
		else if (minAge != null && maxAge == null) {
			return "age < ?";
		}
		else if (minAge == null && maxAge != null) {
			return "? <= age";
		}
		else if (minAge != null && maxAge != null) {
			return "? <= age AND age < ?;
		}
	}

	private String[] getSelectionArgsForGetNames(Integer minAge, Integer maxAge) {
		if (minAge == null && maxAge == null) {
			return null;
		}
		else if (minAge != null && maxAge == null) {
			return new String[]{Integer.toString(maxAge)};
		}
		else if (minAge == null && maxAge != null) {
			return new String[]{Integer.toString(minAge)};
		}
		else if (minAge != null && maxAge != null) {
			return new String[]{Integer.toString(minAge), Integer.toString(maxAge)};
		}
	}
}

とても長いですね。条件をオプションにするというのは良くあることなのでもっとスマートに書きたいと思います。

以下がそれです。

public class HumanDao {
	public List<String> getNames3(SQLiteDatabase database, int minAge, int maxAge) throws Exception {
		Cursor cursor = database.query(
			"human",
			new String[]{"name"},
			"(? = '1' OR age <= ?) AND (? = '1' OR age < ?)",
			new String[]{isNull(minAge), format(minAge), isNull(maxAge), format(maxAge)},
			null, null, null, null);

		List<String> result = new ArrayList<>();
		while (cursor.moveNext()) {
			result.Add(cursor.getString("name"));
		}

		return result;
	}

	public String isNull(Object value) {
		return value == null ? "1" : "0";
	}

	public String format(Integer value) {
		return value != null ? Integer.toString(value) : "dummy";
	}
}

isNull と format の2つのメソッドはヘルパーメソッドです。

ポイントは、

  • selection 引数に (? = '1' OR ? を使ったオプション条件) という形で WHERE 句を書くこと。
  • selectionArgs 引数に isNull(param), format(param) という形でパラメータを書くこと。
  • null 判定は IS NULL ではなく null だったら '1' を渡すようにして判定すること。
  • format では値がなかった場合でも null ではなくダミーの文字列を返すこと。

です。

null だったらオプション条件を無条件で満たすようにするというアイデアです。

また、SQLiteDatabase クラスの特殊な事情として各種メソッドの selectionArgs 引数には null を指定できないという特殊事情も考慮してあります。

SQLiteDatabase クラスの selectionArgs 引数に null の値を1つでも入れると以下のようなエラーメッセージで怒られます。

Bind Value At Index 1 is Null

いろいろ調べたのですが、これは避けようのない問題らしいです。

当初は selection 引数の WHERE 句を (? IS NULL OR ? を使ったオプション条件) で組み立てようとしたのですが null を渡すことができないのであきらめました。

変わりに isNull ヘルパーメソッドを定義し '1' で null を表現することにしました。

また、? を使ったオプション条件の部分にも当然 null を渡せないため、本当はあまりやりたくないですが "dummy" の文字列を渡すことにしました。

null であれば当然 ? = '1' が満たされるため "dummy" でもなんでも影響はないという割り切りをした結果です。

ちなみに null が渡せないのは SQLiteDatabase クラスの実装がそうなっているからであって SQLite が null を扱えないわけではありません。

どっかの英語サイトでこれは重大な欠陥だという書き込みを見ましたが、例えそうであったとしても現状がそうなってしまっているし、直ったとしても旧い Android をサポートするために考慮がいらなくなることはないと思います。

難読化とは?

.NET アプリケーションはバイナリから可読なソースコード形式への逆コンパイルが比較的容易にできます。

難読化はアプリの解読・改ざんを防ぐためのあらゆる措置のことです。

難読化の最も単純な事例はクラス名・メソッド名の置換です。名前から役割が類推されることを防ぎます。

難読化の必要性は?

オープンソースで開発しているアプリなら特に必要はないと思います。

そうでないのであれば多かれ少なかれやっておいた方が良いのではないかと思います。

ツールの使い方を一度覚えてしまえば簡単ですし、ビルドプロセスに組み込んでしまえばツールを直接使うこともほとんどなくなります。

この記事では自分が使っている難読化ツールの使い方を紹介したいと思います。

難読化ツール Obfuscar

難読化をサポートするツールは数多くあります。今回紹介する難読化ツールはその中の1つです。

  • Obfuscar, The Open Source Obfuscator for .NET Applications

これはオープンソースで開発されている難読化ツールです。

ざっくりまとめると以下の難読化ができます。

  • クラス名・メソッド名の難読化
  • 文字列リテラルの難読化

それぞれがどんなものか簡単に説明します。

クラス名・メソッド名の難読化は、メソッド名を A, B, C とかに置換してくれる機能のことです。

置換結果をファイルに出力する機能も持っています。

元のクラス名・メソッド名を逆引きが必要になる場合があります。例外発生時やログ解析時などです。そのためこの置換結果はとても重要です。

次に文字列リテラルの難読化は、プログラム中に直接書き込んだ固定文字列を逆コンパイルで直接読めなくする機能のことです。

特に後者の文字列リテラルの難読化が自分にとっては欲しい機能でした。

その他 Obfuscarの特徴を簡単に説明します。

  • コマンドラインで動作する。
  • 難読化の設定や処理対象とするバイナリの指定などは設定ファイルに記述する。

コマンドラインで実行できるためビルドプロセスに簡単に組み込めます。

本記事では簡単な設定ファイルの例を示します。特別なことがない限りはそのままで大体いけるはずです。

Obfuscar の使い方

まずは Obfuscar をインストールしましょう。以下、手順です。

  1. Obfuscar のページで DOWNLOADS タブを選択する。
  2. 2.0_rc7_bin.zip をダウンロードする。
  3. ダウンロードした ZIP ファイルを解凍する。
  4. 解凍したフォルダにパスを通す(PATH 環境変数に追加する)。

次に設定ファイルを用意します。

以下のファイルを obfuscar.xml とでも名前を付けて保存してください。ァイル名は任意です。

<?xml version='1.0'?>

<Obfuscator>
  <Var name="InPath" value=".\" />
  <Var name="OutPath" value=".\Out" />
  <Var name="KeepPublicApi" value="true" />
  <Var name="HidePrivateApi" value="true" />
  <Var name="HideStrings" value="true" />

  <Module file="$(InPath)\Xxx.exe" />
</Obfuscator>

Xxx.exe の部分は対象となる .NET アプリケーションのファイル名を指定します。

.NET アプリケーションと同じディレクトリにファイルを置き、そのディレクトリで以下のコマンドを実行してください。

Obfuscar.Console obfuscar.xml

Out ディレクトリが作成され、その中に難読化された .NET アプリケーションが格納されているはずです。

これだけです。

本当に難読化はうまくいっているの?

難読化がうまくいっているかどうかを確認するには実際に逆コンパイルをして確認してみるのが良いでしょう。

以下の記事を参考に ILSpy というツールを使ってみてください。

難読化前のファイルと難読化後のファイルを逆コンパイルして、ソースを見てみてください。

設定ファイルに書いてある内容の説明

設定ファイルに書いてある内容は以下の通りです。

  • KeepPublicApi: 公開 API の難読化を行うかどうかを設定する。
    • true の場合は公開クラス・メソッド名の置換を行わない。
    • true に設定しているので Public な API を難読化対象から除外している。
  • HidePrivateApi: 非公開 API の難読化を行うかどうかを設定する。
    • true の場合は公開クラス・メソッド名の置換を行う。
    • true に設定しているので Private な API を難読化対象にしている。
  • HideStrings: 文字列リテラルの難読化を行うかどうかを設定する。
    • true の場合は文字列リテラルの難読化を行う。
    • true に設定しているので文字列リテラルの難読化を行っている。

このような設定にしているのには意味があります。

できる限り難読化をしたいので KeepPublicApi を false にしたいところなのですがうまくいかない場合があります。

自分が作っている .NET アプリケーションは WPF アプリケーションなのですが、KeepPublicApi を false にすると正常に動作しませんでした。

Windows フォームアプリケーションなら動作するかもしれません。解決は難しく Public API の難読化はあきらめるのが無難と判断しました。

Module には難読化対象となるバイナリを指定します。

もしも DLL があり、その DLL も難読化の対象にしたい場合はその分だけ DLL を並べれば大丈夫です。

ビルドプロセスに組み込む

以下の設定を行うことで Visual Studio のビルドプロセスに難読化の作業を組み込むことができます。

  • 設定ファイルをプロジェクトに追加する。
  • 設定ファイルのプロパティ「出力ディレクトリにコピー」を「新しい場合はコピーする」に設定する。
  • プロジェクトの設定の「ビルドイベント」「ビルド後に実行するコマンドライン」に以下の記述を追加する。
    • Obfuscar.Console 設定ファイル名

以降、ビルド時に難読化が自動で行われるようになります。

この記事で紹介している設定ファイルを使っているのであれば、 ビルド後、Debug/Release のディレクトリを見ると Out ディレクトリができているはずです。

中に難読化後のバイナリができているはずです。

.NET Framework のアプリケーションで画像リソースの扱いが面倒だったのでツールを1つ作りました。

その紹介です。

ディレクトリの中身をまるごと1つの resx ファイルに固めるお手軽ツールです。


Microsoft Visual C# 2010 Express Edition で1万点ほどの画像リソースを扱うアプリケーションを作っていました。

Visual Studio で画像リソースを扱う方法はいくつかあって

  1. exe の配下に画像を配置することを前提にし動的に読み込む。
    • 画像を Visual Studio で全く管理しないためビルドプロセスがとにかく簡単である。
    • インストーラーに画像を別個で含める必要があり、配布が少し面倒になる。
  2. resx ファイルにしコンパイルで exe に組み込む。
    • リソースの数が多いと resx ファイルの管理がとにかく面倒である。
    • ビルドが楽である。
    • exe に組み込まれるので配布が楽になる。
  3. プロジェクトにリソースとして組み込む。
    • Visual Studio での操作は難しく、プロジェクトファイルをテキストエディタ等で直接書き換える等の措置をしないとまともに作業できない。
    • ビルドプロセスが少し長くなる。
    • exe に組み込まれるので配布が楽になる。

最初は 1 番の方法を採っていたのですが正式にリリースして配布するとなったときに画像ファイルがばらばらにならずに1つの exe にまとまっていて欲しいという要件が出てきました。

かといって最後の方法はビルドがちょっと長くなるのが嫌で、resx ファイルを使いたいけど管理が面倒でどうにかならないかなぁと思っていました。

ツールを探したけども簡単には見つからず(本当はあるかもしれない)、そうしている内に .NET って普通に resx ファイルを扱うクラスがあるってことに気づいて、ならツールを作ってしまえと思ったわけです。

最初は自分だけのツールを作り、画像を1つのリソースファイルに固めて、無事アプリケーションは出来上がり、リリースも済ませました。

せっかく作ったツールだけども自分が必要になったということは他にも必要になる人がいるんじゃないかという可能性もちょっとだけ考えて汎用的なツールに作り直したものが今回公開したツールです。

背景

Microsoft Visual C# 2010 Express Edition を使って Windows アプリケーションの開発を行っています。

C# を使って Windows アプリケーションを開発する場合、大きく分けて以下2つの選択肢があります。

  • Windows フォームアプリケーション
  • WPF アプリケーション(Windows Presentation Foundation アプリケーション)

今回は後者の WPF で Windows アプリケーションの開発を行っています。

Windows フォームには Chart コントロールというものが存在し、これを使うことで簡単にグラフが描けます。

しかし、WPF には Chart コントロールが無く、外部のライブラリを利用する必要があるということがわかってきました。

今回描きたいグラフは線グラフです。以下の要件を満たしてくれれば良いという程度のものです。

  • 目盛り表示がある。
  • 1つのグラフに複数の線が描ける。
  • 線の色が指定できる。
  • 横軸の目盛りに日付(DateTime 型)が使える。
  • 縦軸の目盛りは線型で良い。

加えて無料であることもポイントになります。

グラフ描画ライブラリに何を利用するか?

インターネット上で情報を収集してよく見かけるのが以下のライブラリです。

しかし、重要な Chart Controls の機能は Stable ではなく Preview となったまま開発が 2010 年 の 2 月で止まっているように見えます。

他に無いかと探して見つけたのが以下のライブラリです。

最終バージョンが 2014 年の 1 月と比較的新しく、WPF Toolkit よりは信頼に足ると判断しました。

ライセンスは MIT License で無料で比較的自由に使えます。

線グラフしか試していませんが、棒グラフ・円グラフにも対応しているように見えます。

OxyPlot を利用する準備

Visual Studio の Professional Edition で開発を行っている人は NuGet を使ってインストールできるようですが、 Express Edition を使っている人は以下の操作を手動で行いましょう。

  1. OxyPlot のバイナリーをダウンロードする。
    1. CodePlex のページからダウンロードする。
  2. OxyPlot.dll と OxyPlot.Wpf.dll をプロジェクトに参照として追加する。

準備はこれだけです。

サンプル紹介

線グラフを表示する簡単なサンプルを紹介します。

以下、関連するコードです。

MainWindow.xaml

<Window x:Class="OxyPlotExample1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:oxy="clr-namespace:OxyPlot.Wpf;assembly=OxyPlot.Wpf"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
    <oxy:PlotView>
        <oxy:PlotView.Series>
            <oxy:LineSeries oxy:Name="line1" />
        </oxy:PlotView.Series>
    </oxy:PlotView>
</Window>

名前空間 oxy として OxyPlot.Wpf アセンブリを組み込んでいます。

oxy:PlotView はグラフを描画するビューです。

oxy:PlotView.Series にグラフに表示する要素(今回は線を引くために LineSeries というのを使っている)を追加します。複数の線を引きたい場合はその数だけ追加しましょう。

標準では XY 座標ともに線形のグラフ表示となります。

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using OxyPlot;

namespace OxyPlotExample1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            IList<DataPoint> data = new List<DataPoint>
            {
                new DataPoint(0, 1),
                new DataPoint(1, 5),
                new DataPoint(2, 3),
            };
            line1.ItemsSource = data;
        }
    }
}

DataPoint クラスは OxyPlot 標準の座標を表すデータクラスです。

oxy:LineSeries の ItemsSource プロパティにリストとして渡すことでグラフを描画できます。

備考

今回は Loaded イベントで ItemsSource プロパティに値を設定していますが、xaml 上で ItemsSource にデータをバインドすることもできます。

また、DataPoint クラスではなく独自のデータクラスを利用することもできます。その場合は oxy:LineSeries の DataFieldX, DataFieldY プロパティに X, Y 座標値となるデータクラスのプロパティ名を指定します。

例えば以下のような感じです。

// MainWindow.xaml.cs
// ...
public class ExampleData {
    // ...
    public double XPoint { get; set; }
    public double YPoint { get; set; }
}

<!-- MainWindow.xaml -->
    <oxy:PlotView>
        <oxy:PlotView.Series>
            <oxy:LineSeries oxy:Name="line1" DataFieldX="XPoint" DataFieldY="YPoint" />
        </oxy:PlotView.Series>
    </oxy:PlotView>

また、X と Y の座標系を変更することもできます。例えば今回は横軸の目盛りに日付(DateTime 型)にしたいのですが、その場合はデータクラスのプロパティを DateTime にした上で横軸の目盛りに DateTimeAxes というのを指定します。

例えば以下のような感じです。

// MainWindow.xaml.cs
// ...
public class ExampleData {
    // ...
    public DateTime XPoint { get; set; }
    public double YPoint { get; set; }
}

<!-- MainWindow.xaml -->
    <oxy:PlotView>
            <oxy:PlotView.Axes>
                <oxy:DateTimeAxis Position="Bottom" />
            </oxy:PlotView.Axes>
            <oxy:PlotView.Series>
            <oxy:LineSeries oxy:Name="line1" DataFieldX="XPoint" DataFieldY="YPoint" />
        </oxy:PlotView.Series>
    </oxy:PlotView>

後はドキュメント読みましょう!!!