nginxのWindows版バイナリをビルドする

nginxのWindows版バイナリは公式で配布されていますが、nginxに追加のモジュールを入れたい場合、どうしても自分でビルドする必要がでてきます。

ビルド方法は公式のドキュメントに書かれています。

How to build nginx on the Win32 Platform with Visual C

が、色々ハマるのでメモしておきます。ビルド環境はWindows7 Professional x64です。

必要なのは以下。

  • Microsoft Visual C++ Compiler
  • Microsoft Windows SDK
  • MinGW/MSYS
  • ActivePerl
  • PCRE
  • zlib
  • OpenSSL

まず、VCコンパイラが必要なので、Microsoft Visual C++ Expressをインストールします。

Microsoft Visual Studio Express

上記ページのVisual C++ 2010 ExpressのWebインストールをクリックすればvc_web.exeがダウンロードできるので、それを実行してあとは言われるままにインストールすればOKです。

続いてMicrosoft Windows SDK(旧Platform SDK)をインストールします。

Windows SDK: Download the Windows SDK for Windows 7 and More | MSDN

「Install Now」を押せばインストールできます。こちらもそのままインストールすればOK。

OpenSSLのコンパイルにPerlが必要なので、ActivePerlをインストールします。32bit版と64bit版がありますが、僕は64bit版をインストールしました。

ActivePerl Downloads – Perl Binaries for Windows, Linux and Mac | ActiveState

続けてMinGW/MSYSのインストールをします。

MinGW – Minimalist GNU for Windows – Browse Files at SourceForge.net

「Looking for the latest version?」と書かれている隣のファイルをダウンロードします。

インストールが終わったら環境変数の設定をします。

MinGW Shellを起動して、パスを設定します。

$ export LIB="C:\\Program Files (x86)\\Microsoft Visual Studio 10.0\\VC\\lib;C:\\Program Files\\Microsoft SDKs\\Windows\\v7.1\\Lib"
$ export INCLUDE="C:\\Program Files (x86)\\Microsoft Visual Studio 10.0\\VC\\include;C:\\Program Files\\Microsoft SDKs\\Windows\\v7.1\\Include"
$ export PATH="$PATH:/c/Perl64/bin:/c/Program Files (x86)/Microsoft Visual Studio 10.0/VC/bin:/c/Program Files (x86)/Microsoft Visual Studio 10.0/Common7/IDE:/c/Program Files/Microsoft SDKs/Windows/v7.1/Bin"

nginxのソースコードをダウンロードしましょう。tarボールで提供されているものには、Windows用のConfigureが入っていないらしいので、svnから取得します。

svnコマンドが使えるのであれば、
svn co svn://svn.nginx.org/tags/release-(バージョン番号)
でチェックアウトできます。僕はrelease-1.0.10を使いました。

なぜかTortoiseSVNではtags/release-1.0.10が表示できませんでした。

ダウンロードしたソースコードはMSYSのHOMEディレクトリに置きます。

C:\MinGW\msys\1.0\home\(ユーザー名)\nginx
としました。

PCRE、zlib、OpenSSLをダウンロードします。

PCRE – Browse /pcre at SourceForge.net

zlib Home Site

OpenSSL: Source, Tarballs

pcre-8.20.zip、zlib-1.2.5.tar.gz、openssl-1.0.0e.tar.gzをダウンロードしました。

nginxディレクトリの下にobjs/libディレクトリを作成します。ここに、先ほどダウンロードした3ファイルを解凍したものを設置します。

これで準備はできたので、ビルドします。

公式のビルド方法に載っているConfigureを実行します。が、PCREのバージョンは8.20になっていたので、–with-pcreの引数は変更します。あとは適宜合わせてください。

$ auto/configure –with-cc=cl –builddir=objs –prefix= \
–conf-path=conf/nginx.conf –pid-path=logs/nginx.pid \
–http-log-path=logs/access.log –error-log-path=logs/error.log \
–sbin-path=nginx.exe –http-client-body-temp-path=temp/client_body_temp \
–http-proxy-temp-path=temp/proxy_temp \
–http-fastcgi-temp-path=temp/fastcgi_temp \
–with-cc-opt=-DFD_SETSIZE=1024 –with-pcre=objs/lib/pcre-8.20 \
–with-zlib=objs/lib/zlib-1.2.5 –with-openssl=objs/lib/openssl-1.0.0e \
–with-select_module –with-http_ssl_module –with-ipv6

Configureが成功したら、nmakeでビルドします。

$ nmake -f objs/Makefile

これで、何も問題がなければnginx/objsディレクトリにnginx.exeファイルが出来上がっているはずです。

このnginx.exeファイルを、ひとつ上のディレクトリ(nginx)に設置して、

$ mkdir logs
$ mkdir temp

として、

$ start nginx.exe

で起動できます。

これでやっと追加モジュールをインストールできるというもの。


SimpleDateFormatがメモリを食い尽くす件

java.text.SimpleDateFormatのインスタンスを毎回作ってRFC2822の日付文字列からDate型に変換していたら、GCされずにメモリを食いつぶしてしまったので対策。

	public static Date convertRfc2822toDate(String from) {
		String pattern = "EEE, dd MMM yyyy HH:mm:ss Z";
		SimpleDateFormat format = new SimpleDateFormat(pattern);
		Date date = null;
		synchronized (format) {
			try {
				date = (Date) format.parse(from);
			} catch (ParseException e) {
				date = new Date();
			}
		}
		format = null;
		return date;
	}

上は元のコード。下は修正版。

	private static SimpleDateFormat format = null;

	public static Date convertRfc2822toDate(String from) {
		String pattern = "EEE, dd MMM yyyy HH:mm:ss Z";

		Date date = null;
		if (format == null) {
			format = new SimpleDateFormat();
		}
		synchronized (format) {
			format.applyPattern(pattern);
			try {
				date = format.parse(from);
			} catch (ParseException e) {
				date = new Date();
			}
		}

		return date;
	}

完全なスレッドセーフにはなっていないけど、メモリリークは解消できたので良し。


PythonのPyDHCPLibを使ってDHCPパケットを読む

PyDHCPLibはPure Pythonで書かれたDHCPパケットのリード/ライトライブラリです。

Pythonで書かれているので、LinuxでもWindowsでも動きます。(Windowsのときはちょっと対策が必要)

まずはdebian Linuxの場合。(aptでインストールできました。追記参照)

$ wget http://pydhcplib.tuxfamily.org/download/pydhcplib-0.6.2.tar.gz
$ tar xvzf pydhcplib-0.6.2.tar.gz
$ cd pydhcplib-0.6.2
$ sudo /usr/bin/python2.6 ./setup.py install

/usr/local/lib/python2.6/dist-packages/pydhcplib
に入りました。

流れてくるパケットを読み取るためにはサーバとして動かします。
examplesディレクトリの中にサーバ用のサンプルが入っているので実行します。

$ sudo /usr/bin/python2.6 ./examples/server_example.py

これでOKです。

ちなみにroot権限で実行しないと、以下のエラーが出てポートが開けられません。
pydhcplib.DhcpNetwork.BindToAddress error : [Errno 13] Permission denied

ではWindowsの場合。

Windows 7にもPython 2.7.2をインストールして使って見ました。

> cd pydhcplib-0.6.2
> C:\Python27\python.exe .\setup.py install

C:\Python27\Lib\site-packages\pydhcplibにインストールされました。

> C:\Python27\python.exe .\examples\server_example.py
Traceback (most recent call last):
  File ".\examples\server_example.py", line 21, in <module>
    from pydhcplib.dhcp_network import *
  File "C:\Python27\lib\site-packages\pydhcplib\dhcp_network.py", line 22, in <module>
    import IN
ImportError: No module named IN

インポートエラー。INモジュールというものがないとのこと。

INモジュールはdebianではPythonのplat-linux2ディレクトリ以下に入っています。中身は
# Generated by h2py from /usr/include/netinet/in.h
netinetから変換した定数の定義でした。

なので、必要なところだけコピーしてくることにします。

$ cat /usr/lib/python2.6/plat-linux2/IN.py |grep SO_BINDTODEVICE
SO_BINDTODEVICE = 25

このSO_BINDTODEVICEの定義だけを書いたIN.pyファイルをexamplesディレクトリに置けばOKです。

IN.py
SO_BINDTODEVICE = 25

これで試しにiPod touchをLANに接続して見ました。

> C:\Python27\python.exe .\examples\server_example.py
# Header fields
op : BOOTREQUEST
htype : 1
hlen : 6
hops : 0
xid : 3472825851
secs : 1
flags : 0
ciaddr : 0.0.0.0
yiaddr : 0.0.0.0
siaddr : 0.0.0.0
giaddr : 0.0.0.0
chaddr : 00:26:bb:XX:XX:XX
sname :
file :
# Options fields
client_identifier :
parameter_request_list : subnet_mask,router,domain_name_server,domain_name,domain_search,252
host_name : homma2
request_ip_address : 172.18.0.21
dhcp_message_type : DHCP_REQUEST
ip_address_lease_time : 7776000
maximum_dhcp_message_size : 1285

これでネットワークに接続された端末のリースタイム・MACアドレス・IPアドレスが取れました。

追記

debian squeezeではパッケージ登録されていました。
なので、aptで簡単にインストールできます。

$ aptitude search pydhcplib
p   python-pydhcplib                                                                                 – Python DHCP client/server library
v   python2.5-pydhcplib                                                                              –
v   python2.6-pydhcplib                                                                              –
$ sudo aptitude install python-pydhcplib

以上。


DebianからWindows7の共有フォルダにアクセスする

実際の構成としてはVirtual PC内のdebian(squeeze)からホストOSのWindows7の共有フォルダにアクセスするという要件なのだけど、特に問題なくできたので。
ちなみにLoopback Adapter接続でした。

# apt-get install samba smbfs
# mkdir /media/videos
# mount -t cifs -o username=Windows側のユーザー名,codepage=cp932,iocharset=utf8 //Windows側のIP/Users/Public/Videos /media/videos

パスワードの入力を求められます。

Windows7だと、Publicフォルダが「パブリック」とか日本語になっていたりするので、コマンドプロンプトを使ってあらかじめ名前を確認しておきます。

SCPでVirtualPCと巨大なファイルをやり取りしようとするとなぜかやたら遅くて…
マウントしてしまえばさっくり行けます。


loadImage時にcontains a path separatorのIllegalArgumentExceptionが発生する原因

Processing for Android 1.5.1でPApplet.loadImageを実行するときにエラー。

java.lang.IllegalArgumentException: File /mnt/sdcard/Android/data/com.sample.android.apptest/hoge.jpg contains a path separator

ファイルが存在しない場合に発生します。
エラーメッセージわかりにくいね。
loadImageメソッドは色々なソースからファイルが取得できるかを確認しに行くのでこういうことになっているのだと思います。


ProcessingをAndroidで使う(導入編)

ひと通りProcessingを利用してAndroidアプリを作ってみたので、色々書いておこうと思います。

まずは一番簡単なProcessingのIDEを使った方法です。

Download \ Processing.org

まずはProcessingをダウンロードします。執筆時点での最新安定バージョンは1.5.1です。Android SDKはインストールしておく必要があります。

実行すると、ProcessingのIDE(統合開発環境)が開きます。

WS000025

Processingについてはあまり説明しません。とりあえずはAndroidモードに切り替えます。右上の「STANDARD」と表示されているボタンを押すと、リストが表示されます。ただのラベルじゃないので、押せます。

WS000026

WS000027

Androidを選ぶと、IDEの色が変わります。右上のモードが「ANDROID」になっていることを確認します。

WS000028

メニューのFileからExamplesを開きます。

WS000003

ここで適当にサンプルを選んで、ダブルクリックで開きます。

ソースコードを読み込んだあとに、左上の再生ボタンを押せばエミュレータで動作を試せます。

WS000004

が、残念ながら、今はAndroid SDKのバージョンが上がったことで、エラーが出て実行できなくなっています。

BUILD FAILED
C:\Users\test\AppData\Local\Temp\android397981578064449760.pde\build.xml:16:

Error. You are using an obsolete build.xml
You need to delete it and regenerate it using
    android update project

Total time: 0 seconds

このままProcessing IDEで遊びたい場合は、まだ開発中のProcessing 2.0a4などを使えば上記の問題も解決しているようです。恐らく近日中にリリース版が出るかと。

tadpolizemedia.blog | processing1.5.1 と Android エミュレータ(android-sdk_r14-windows.zip)が連動しない件

詳しくは上記サイトにありましたので参考に。2.0a4でも、再生ボタンでそのまま実行はできませんでした。先にエミュレータを起動しておけば大丈夫です。

実機に転送する場合はメニューのSketchから、Run on Deviceを実行してください。

Processing IDEで開発するのは本質ではないので、この辺で。次回はProcessing IDEで書いたコードをEclipseのプロジェクト形式にエクスポートする方法を紹介します。


AndroidとTwitter4Jで公式画像アップロードAPIを使う

Twitter4Jが2.2.5になって、Twitter APIのstatuses/update_with_mediaが使えるようになったというので、さっそく実験してみました。

Twitter4J 2.2.5 released – Twitter4J | Google グループ

とりあえずダウンロードして解凍します。twitter4j-android-2.2.5.zipの方です。

http://twitter4j.org/en/index.html#download

Eclipseで適当にプロジェクト作ります。プロジェクト作ったらプロジェクトのプロパティを開いて、Java Build Pathの設定画面を表示します。

image

Librariesタブを開いて、Add External JARsをクリックします。

image

先ほど展開したフォルダから、libの下にあるtwitter4j-core-android-2.2.5.jarとtwitter4j-media-support-android-2.2.5.jarを選択してJARを取り込みます。

image

あとはこんな感じで実験。ギャラリーの画像を選択してメッセージつけてポストします。

UpdateWithMediaActivity.java

package info.loadlimits.android.updatewithmedia;

import java.io.File;

import twitter4j.TwitterException;
import twitter4j.conf.Configuration;
import twitter4j.conf.ConfigurationBuilder;
import twitter4j.media.ImageUpload;
import twitter4j.media.ImageUploadFactory;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

public class UpdateWithMediaActivity extends Activity implements
		OnClickListener {
	private final static int REQUEST_PICK = 1;
	private final static String CONSUMER_KEY = "(書き換えてください)";
	private final static String CONSUMER_SECRET = "(書き換えてください)";
	private final static String ACCESS_TOKEN = "(書き換えてください)";
	private final static String ACCESS_TOKEN_SECRET = "(書き換えてください)";

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		Button buttonPost = (Button) findViewById(R.id.buttonPost);
		buttonPost.setOnClickListener(this);
	}

	@Override
	public void onClick(View v) {
		Intent intent = new Intent(Intent.ACTION_PICK);
		intent.setType("image/*");
		startActivityForResult(intent, REQUEST_PICK);
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);
		if (requestCode == REQUEST_PICK && resultCode == RESULT_OK) {
			Uri uri = data.getData();
			ContentResolver cr = getContentResolver();
			String[] columns = { MediaStore.Images.Media.DATA };
			Cursor c = cr.query(uri, columns, null, null, null);

			c.moveToFirst();
			File path = new File(c.getString(0));
			if (!path.exists())
				return;

			ConfigurationBuilder builder = new ConfigurationBuilder();
			builder.setOAuthConsumerKey(CONSUMER_KEY);
			builder.setOAuthConsumerSecret(CONSUMER_SECRET);
			builder.setOAuthAccessToken(ACCESS_TOKEN);
			builder.setOAuthAccessTokenSecret(ACCESS_TOKEN_SECRET);
			// ここでMediaProviderをTWITTERにする
			builder.setMediaProvider("TWITTER");

			Configuration conf = builder.build();

			ImageUpload imageUpload = new ImageUploadFactory(conf)
					.getInstance();

			EditText textTweet = (EditText) findViewById(R.id.textTweet);
			String tweet = textTweet.getText().toString();

			try {
				imageUpload.upload(path, tweet);
			} catch (TwitterException e) {
				e.printStackTrace();
			}
		}
	}
}

res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <EditText
        android:id="@+id/textTweet"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <requestFocus />
    </EditText>

    <Button
        android:id="@+id/buttonPost"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/btn_upload" />

</LinearLayout>

manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.loadlimits.android.updatewithmedia"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="9" />

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:label="@string/app_name"
            android:name=".UpdateWithMediaActivity" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

ポイントはsetMediaProviderでTWITTERを選択することだけです。

image

image

こんな感じでpic.twitter.comにアップロードできました。

OAuth認証ページとか組み込みたければ、こちらを参考に。

[Android] Android+Twitter4JでOAuthするためのソースコード – adakoda


オプションメニューのアイコンと文字を横並びにしたかった

前回オプションメニューの文字色とフォントを変更するエントリを書きましたが、アイコンの隣に文字を表示したい要件が出てきました。

前回
OptionsMenuの文字色を変更する : blog.loadlimit – digital matter –

で、色々やってみましたが、手詰まりしてしまいました。やっぱりアイコンと文字をセットにした画像を用意してしまって、文字は消してしまう方が確実です。

惜しいところまで行った現状の記録を残しておきます。

device-2011-10-22-174653

なんですかね、心が離れてしまったんですかね。

ソース

public class CustomizedMenuActivity extends Activity {
	public static final int MENU_ID_COFFEE = 1;
	public static final int MENU_ID_LOVE = 2;
	public static final int MENU_ID_RECYCLE = 3;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.customized_menu);
	}

	protected TextView getMenuItemView(MenuItem item) {
		try {
			Class<?> c = item.getClass(); // MenuItemImplのインスタンス
			Class<?>[] paramTypesGetItemView = { int.class, ViewGroup.class };
			Method method;
			method = c.getDeclaredMethod("getItemView", paramTypesGetItemView);
			// getItemViewはprivateメソッドなのでアクセス可能に変更する
			method.setAccessible(true);
			// IconMenuItemViewを取得できる
			TextView view = (TextView) method.invoke(item, new Object[] { 0,
					null });
			return view;
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// メニューアイテムの追加
		MenuItem menuItemCoffee = menu.add(Menu.NONE, MENU_ID_COFFEE,
				Menu.NONE, this.getText(R.string.menu_coffee)).setIcon(
				R.drawable.icon_coffee);
		MenuItem menuItemLove = menu.add(Menu.NONE, MENU_ID_LOVE, Menu.NONE,
				this.getText(R.string.menu_love)).setIcon(R.drawable.icon_love);
		MenuItem menuItemRecycle = menu.add(Menu.NONE, MENU_ID_RECYCLE,
				Menu.NONE, this.getText(R.string.menu_recycle)).setIcon(
				R.drawable.icon_recycle);

		try {
			TextView viewCoffee = getMenuItemView(menuItemCoffee);
			TextView viewLove = getMenuItemView(menuItemLove);
			TextView viewRecycle = getMenuItemView(menuItemRecycle);

			// テキストの色を変える
			viewCoffee.setTextColor(0xFFB5985A);
			viewCoffee.setTextSize(14);
			viewCoffee.setTextScaleX(0.8f);

			viewLove.setTextColor(0xFFCF7D5B);
			viewLove.setTextSize(22);
			viewLove.setTypeface(Typeface.DEFAULT_BOLD);

			viewRecycle.setTextColor(0xFF8A6381);
			viewRecycle.setTextSize(16);
			viewRecycle.setTypeface(Typeface.create(Typeface.SERIF,
					Typeface.BOLD_ITALIC));

			// アイコン画像を取得
			Field field = viewLove.getClass().getDeclaredField("mIcon");
			field.setAccessible(true);
			Drawable icon = (Drawable) field.get(viewLove);
			// アイコンの描画領域を取得
//			Rect iconRect = icon.getBounds();
//			iconRect.left += 50;
//			iconRect.right += 50;
//			icon.setBounds(iconRect); // ※IconMenuItemViewクラスのonLayoutで上書きされてしまう
			// アイコンを文字の左側に表示する
			viewLove.setCompoundDrawables(icon, null, null, null);
			// 文字の配置を中央寄せにする
			viewLove.setGravity(Gravity.CENTER_VERTICAL
					| Gravity.CENTER_HORIZONTAL);

		} catch (NoSuchFieldException e) {
			e.printStackTrace();
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}

		return super.onCreateOptionsMenu(menu);
	}

}

問題はIconMenuItemViewクラスでpositionIconメソッドがアイコンの位置を決定しているのですが、onLayoutのタイミングで呼び出されるので、上書きされてしまうようなのですね。

ActivityのViewでonLayoutをオーバーライドして、うまくいけばアイコンが描画される前に位置変更差し込めるのかなぁ?

ちょっともう面倒なので画像にしてしまいます。

icon_love2

device-2011-10-22-182542

あ、いいですね。まずは画像登録とテキストを空に。

		MenuItem menuItemLove = menu.add(Menu.NONE, MENU_ID_LOVE, Menu.NONE,
				null).setIcon(R.drawable.icon_love2);

文字の分の空白が下に空いてしまうので、文字サイズを1にしておきます。0は効かないようです。

viewLove.setTextSize(1);

これで一応できました。

MenuView.ItemViewを実装すればこんな事しなくてもViewをまるごとカスタマイズできるのかな…

ちょっともう少し調べてみます。

Menu::add→Menu::addInternal→new MenuItemImpl

で、MenuItemImplのgetItemViewを呼び出したタイミングでMenuItemImpl::createItemViewが呼び出されます。

    private MenuView.ItemView createItemView(int menuType, ViewGroup parent) {
        // Create the MenuView
        MenuView.ItemView itemView = (MenuView.ItemView) getLayoutInflater(menuType)
                .inflate(MenuBuilder.ITEM_LAYOUT_RES_FOR_TYPE[menuType], parent, false);
        itemView.initialize(this, menuType);
        return itemView;
    }

getLayoutInflaterは同じクラスのメソッド。

    public LayoutInflater getLayoutInflater(int menuType) {
        return mMenu.getMenuType(menuType).getInflater();
    }

リフレクションで呼んで何が返ってくるのか見て見ました。menuTypeはアイコンメニューなので0。

			Class<?>[] param = {int.class};
			method = c.getDeclaredMethod("getLayoutInflater", param);
			LayoutInflater mLayoutInflater = (LayoutInflater) method.invoke(item, new Object[] { 0 });
			Log.d(TAG, "" + mLayoutInflater.getClass().getName());

com.android.internal.policy.impl.PhoneLayoutInflater

public class PhoneLayoutInflater extends LayoutInflater

えー…

PhoneLayoutInflaterでinflateメソッドはオーバーライドされていないので、LayoutInflaterのinflateを見る。呼ばれたのはこれかな。

    public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        if (DEBUG) System.out.println("INFLATING from resource: " + resource);
        XmlResourceParser parser = getContext().getResources().getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

resourceは、com.android.internal.R.layout.icon_menu_item_layoutですね。

frameworks/base/core/res/res/layout/icon_menu_item_layout.xml

<com.android.internal.view.menu.IconMenuItemView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/title"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="1dip"
    android:paddingLeft="3dip"
    android:paddingRight="3dip"
    android:singleLine="true"
    android:ellipsize="marquee"
    android:fadingEdge="horizontal" />

あー、ここにあったのか。

割り込める可能性があるとすれば、MenuItemImplクラスのgetItemViewメソッド。

    View getItemView(int menuType, ViewGroup parent) {
        if (!hasItemView(menuType)) {
            mItemViews[menuType] = new WeakReference<ItemView>(createItemView(menuType, parent));
        }
        
        return (View) mItemViews[menuType].get();
    }

つまり、mItemViewsにリフレクションで先にViewを入れておけばいいってことですかね。

うーん…


OptionsMenuの文字色を変更する

Activityで端末のメニューボタンを押したときに出てくるオプションメニューですが、文字の色が変えられなくて困っていました。

Themeのandroid:panelTextAppearanceで変えられるのかと思っていたのだけど、なぜかうまく行かず。

背景画像はテーマで変更できるのですが。

リフレクションを使ってテキストカラーの変更をしてみました。Android API 8で確認しています。

デフォルト

device-2011-10-22-120919

文字色変更

device-2011-10-22-135742

Activityのコード

public class CustomizedMenuActivity extends Activity {
	public static final int MENU_ID_COFFEE = 1;
	public static final int MENU_ID_LOVE = 2;
	public static final int MENU_ID_RECYCLE = 3;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.customized_menu);
	}

	private TextView getMenuItemView(MenuItem item) {
		try {
			Class<?> c = item.getClass(); // itemはMenuItemImplのインスタンス
			Class<?>[] paramTypesGetItemView = { int.class, ViewGroup.class };
			Method method = c.getDeclaredMethod("getItemView", paramTypesGetItemView);

			// getItemViewはprivateメソッドなのでアクセス可能に変更する
			method.setAccessible(true);

			// IconMenuItemViewを取得できる
			TextView view = (TextView) method.invoke(item, new Object[] { 0, null });

			return view;
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// メニューアイテムの追加
		MenuItem menuItemCoffee = menu.add(Menu.NONE, MENU_ID_COFFEE,
				Menu.NONE, this.getText(R.string.menu_coffee)).setIcon(
				R.drawable.icon_coffee);
		MenuItem menuItemLove = menu.add(Menu.NONE, MENU_ID_LOVE, Menu.NONE,
				this.getText(R.string.menu_love)).setIcon(R.drawable.icon_love);
		MenuItem menuItemRecycle = menu.add(Menu.NONE, MENU_ID_RECYCLE,
				Menu.NONE, this.getText(R.string.menu_recycle)).setIcon(
				R.drawable.icon_recycle);

		try {
			TextView viewCoffee = getMenuItemView(menuItemCoffee);
			TextView viewLove = getMenuItemView(menuItemLove);
			TextView viewRecycle = getMenuItemView(menuItemRecycle);

			// テキストの色を変える
			viewCoffee.setTextColor(0xFFB5985A);
			viewCoffee.setTextSize(14);
			viewCoffee.setTextScaleX(0.8f);

			viewLove.setTextColor(0xFFCF7D5B);
			viewLove.setTextSize(22);
			viewLove.setTypeface(Typeface.DEFAULT_BOLD);

			viewRecycle.setTextColor(0xFF8A6381);
			viewRecycle.setTextSize(16);
			viewRecycle.setTypeface(Typeface.create(Typeface.SERIF,
					Typeface.BOLD_ITALIC));

		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		}

		return super.onCreateOptionsMenu(menu);
	}
}

MenuItemの実体はMenuItemImplクラスで、getItemViewメソッドを使ってIconMenuItemViewクラスを取り出しています。

IconMenuItemViewクラスはTextViewを継承しているので、キャストすればテキストカラーやフォントサイズなどの変更ができます。

ちなみに、このあと、メニューのセパレータ(divider)画像の変更とアイコンと文字の配置の変更もやりました。それは次以降のエントリで。

オプションメニューのアイコンと文字を横並びにしたかった : blog.loadlimit – digital matter –


Cannot connect to VMと表示されてデバッグができないとき

EclipseでAndroidの開発をしていて、上記ダイアログが出て唐突にデバッグで起動できなくなったら、慌てずにそのF11を押す手を離して、パッケージマネージャのプロジェクトを選択して、Debug As Android Applicationだ。

OK?