Windows 8 用のミニソフトキーボードを作る(2)

win8 の metro アプリケーションを作る前段階として WPF アプリケーションを作ればよいので(Windows Phone アプリでもいいのだけど、win phone の方は私はノータッチなので)、画像のタッチイベントを実装してみます。

■WPF コントロールに画像を貼りつけ

WPF アプリケーションに画像を貼り付ける時には、Image コントロールを使うのですが、ちょっとコツがいります。

<UserControl x:Class="MiniSoftKey.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DesignHeight="216" d:DesignWidth="320"
             TouchDown="UserControl_TouchDown"
             TouchUp="UserControl_TouchUp"
             TouchMove="UserControl_TouchMove">
    <Grid>
        <Image Height="216" HorizontalAlignment="Left" Name="image1" Stretch="Fill" VerticalAlignment="Top" Width="320"
               Source="/MiniSoftKey;component/Images/iphone_alpha_normal.bmp" />
    </Grid>
</UserControl>

ここでは、リソースから「iphone_alpha_normal.bmp」というファイルを貼り付けているんですが、普通に「Stretch=”None”」(伸長しない)で張り付けると、

のように拡大されてしまうんですよね。ビットマップファイルのサイズは、320×216 にしてあるので、丁度なはなずなのですが…って、これは画面のDPI(96dpi)に起因するそうで「デバイス非依存ビットマップ」というものを使っているからです。デバイス非依存ビットマップで表示すると、ディスプレイの1ドットとビットマップの1ドットは違う、という…DTP的にはいいんでしょうが、なんともプログラマ泣かせといったところです。

じゃあ「デバイス依存ビットマップ」で表示すれば良いのでは?と思ってネットを探してみたのですが、なんだかうまい方法が見つかりませんでした。なので、仕方がないので、Image コントロールの width と height を画像の縦横に合わせて「Stretch=”Fill”」にしておきます。リソースから直接 width/height を取る方法もあるのですが、今回は画像の切り替えも行うので、こんな風にしておきます。

ちなみに、画面が96DPIの場合には、画面に表示されるサイズは4/3に拡大されます。なので、画面表示から3/4すれば元のビットマップのドット数になりますね。

画像ファイル(bmpファイル)自体は、プロパティのビルドアクションを「Resource」に変更しておきます。そうすると、先の xaml のように「Source=”/MiniSoftKey;component/Images/iphone_alpha_normal.bmp”」でアクセスが可能です(これは自動的に変換されます。「MiniSoftKey」はプロジェクト名です)。
# ただし、metro の場合は、逆に「コンテンツ」のままにしておきます。「ms-xapp:////Images/iphone_alpha_normal.bmp」な感じでアクセスが可能です。これもハマりました orz

■タッチイベントの取得

タッチイベント自体は、TouchDown/TouchUp/TouchMove で取ります。これはマウスで操作するときの MouseDown/MouseUp/MouseMove にあたります。ただし、TouchMove のイベントは「タッチし続けている状態」に限られます。当然ですが、指が浮いている間はイベントは取れません。このあたりは MouseMove と違うところですね。ちなみに、metro アプリの場合は、PointerDown などのイベントになります。

private void UserControl_TouchDown(object sender, TouchEventArgs e)
{
	GlobalData.MainForm.label1.Text = "touch down";
	TouchPoint pos = e.GetTouchPoint(this);
	GlobalData.OnMouseDown(new System.Drawing.Point((int)pos.Position.X, (int)pos.Position.Y));
}

private void UserControl_TouchUp(object sender, TouchEventArgs e)
{

	GlobalData.MainForm.label1.Text = "touch up";
	TouchPoint pos = e.GetTouchPoint(this);
	GlobalData.OnMouseUp(new System.Drawing.Point((int)pos.Position.X, (int)pos.Position.Y));
}

private void UserControl_TouchMove(object sender, TouchEventArgs e)
{
	TouchPoint pos = e.GetTouchPoint(this);
	GlobalData.OnMouseMove(new System.Drawing.Point((int)pos.Position.X, (int)pos.Position.Y));
}

ミニキーボードの場合は、acer 上のタッチイベントだけを対象にしていますが、本来は mouse と touch の両方のイベントを取って整合性を合わせないとだめなんですよね。指でタッチをしても、mouse イベントは発生するので、このあたりが面倒です。逆に、デスクトップPCでマウス操作した時は、touch イベントは発生しません。
metro アプリの場合は、mouse/touch/pen のイベントが pointer というひとつのイベントにまとめられているので、このあたりはプログラミングが楽になっています…が、イベントの順序までは未調査なので、両用のアプリを作るときにはどうなるのやら。

■画像の切り替え

動的に画像を切り替えるときは、BitmapImage オブジェクトを作成した後に、Image コントロールの Source プロパティに設定します。

public void Form1_MouseUp(object sender, MouseEventArgs e)
{
	//label1.Text = "mouse up";
	if (_pop != null)
	{
		_pop.Close();
		_pop = null;

		BPos pos = CalcBCode(e.X, e.Y);
		_send.Sendkey(pos.Code);
		if (pos.Code == "shift")
		{
			if (_send.shift_mode == true)
			{
				BitmapImage bi = new BitmapImage(new Uri(@"Images/iphone_alpha_shift.bmp", UriKind.Relative));
				userControl11.image1.Source = bi;
			}
			else
			{
				BitmapImage bi = new BitmapImage(new Uri(@"Images/iphone_alpha_normal.bmp", UriKind.Relative));
				userControl11.image1.Source = bi;
			}
		}
	}
}

ちょっと無駄なコードが入っていますが、こんな感じ。UriKind.Relative を付けて画像リソースを相対パスで拾ってきます。多分、内部的に「Source=”/プロジェクト名;component/」が付けられたと同じような動作になるのだと思います。

■画像のどのキーが押されているのか?

ここからは、ちょっとおまけ。プログラミングノウハウの話。
先のキーボードは単なる画像ファイルです。ボタン自体をちまちまと作っても良かったのですが、ひとまず某iPhoneの真似をしたかったので、そのままキャプチャしました。で、どのボタンが押されているのかを調べるのに、普通はキーのサイズをちまちまと測って、Rectange をチェックすれば…良いのですが、結構な手間になります。まあ、仕事ならそれでもいいけど。

という訳で、次のように少しトリッキーな方法を使います。

private void InitBPos()
{
	/// 標準キーボード
	_blist = new List<BPos>();
	_blist.Add(new BPos { Code = "Q", X = 15, Y = 30 });
	_blist.Add(new BPos { Code = "W", X = 47, Y = 30 });
	_blist.Add(new BPos { Code = "E", X = 82, Y = 30 });
	_blist.Add(new BPos { Code = "R", X = 111, Y = 30 });
	_blist.Add(new BPos { Code = "T", X = 145, Y = 30 });
	_blist.Add(new BPos { Code = "Y", X = 174, Y = 30 });
	_blist.Add(new BPos { Code = "U", X = 208, Y = 30 });
	_blist.Add(new BPos { Code = "I", X = 238, Y = 30 });
	_blist.Add(new BPos { Code = "O", X = 271, Y = 30 });
	_blist.Add(new BPos { Code = "P", X = 301, Y = 30 });
	_blist.Add(new BPos { Code = "A", X = 31, Y = 85 });
	_blist.Add(new BPos { Code = "S", X = 65, Y = 85 });
	_blist.Add(new BPos { Code = "D", X = 94, Y = 85 });
	_blist.Add(new BPos { Code = "F", X = 127, Y = 85 });
	_blist.Add(new BPos { Code = "G", X = 161, Y = 85 });
	_blist.Add(new BPos { Code = "H", X = 191, Y = 85 });
	_blist.Add(new BPos { Code = "J", X = 223, Y = 85 });
	_blist.Add(new BPos { Code = "K", X = 256, Y = 85 });
	_blist.Add(new BPos { Code = "L", X = 286, Y = 85 });
	_blist.Add(new BPos { Code = "Z", X = 61, Y = 137 });
	_blist.Add(new BPos { Code = "X", X = 94, Y = 137 });
	_blist.Add(new BPos { Code = "C", X = 125, Y = 137 });
	_blist.Add(new BPos { Code = "V", X = 161, Y = 137 });
	_blist.Add(new BPos { Code = "B", X = 191, Y = 137 });
	_blist.Add(new BPos { Code = "N", X = 223, Y = 137 });
	_blist.Add(new BPos { Code = "M", X = 256, Y = 137 });
	_blist.Add(new BPos { Code = "BS", X = 298, Y = 137 });
	_blist.Add(new BPos { Code = " ", X = 155, Y = 193 });
	_blist.Add(new BPos { Code = "n", X = 279, Y = 193 });

	_blist.Add(new BPos { Code = "shift", X = 20, Y = 137 });
}
private BPos CalcBCode(int x, int y)
{
	// 距離が近いものを取得
	int len2 = 10000;
	BPos pos = _blist[0];
	foreach (var p in _blist)
	{
		int l2 = (p.X - x) * (p.X - x) + (p.Y - y) * (p.Y - y);
		if (l2 < len2)
		{
			len2 = l2;
			pos = p;
		}
	}
	return pos;
}

各ボタンの中央の座標だけを取得したリストを作っておいて、タッチした座標と一番近いボタンを返す、という CalcBCode 関数ですね。これは、画像処理のクラスタリングの応用で「画面の何処をタッチしても、なんとなくボタンが押せる」という技です(笑)。全スキャンするので、数が多い場合にはスピードが問題になるのですが、これぐらいならば大丈夫。ただし、スペースバーのように横に長い場合は、中央の座標だけでは足りないので、適当に左右にも点を必要としますが(上記のソースにはそれが入っていません)。

public void Form1_MouseUp(object sender, MouseEventArgs e)
{
	//label1.Text = "mouse up";
	if (_pop != null)
	{
		_pop.Close();
		_pop = null;

		BPos pos = CalcBCode(e.X, e.Y);
		_send.Sendkey(pos.Code);

具体的にキーの送信自体は、keybd_event 関数を使います。UpとDownをきちんと対応させないと駄目なのですが、普通にIMEも使えるので、これがベストな方法でしょう。

class SendKeyCode
{
	[DllImport("user32.dll")]
	public static extern uint keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);
	const byte VK_SHIFT = 0x10;
	const byte VK_CONTROL = 0x11;
	const byte VK_MENU = 0x12;
	const byte VK_LWIN = 0x5B;
	const byte VK_RWIN = 0x5C;
	const byte VK_APPS = 0x5D;

	public bool shift_mode = false;

	public void Sendkey(string code)
	{
		// return;

		if (code == "shift")
		{
			shift_mode = !shift_mode;
		}
		#region 特殊キーダウン処理
		/*keybd_event(VK_SHIFT, 0, 0, (UIntPtr)0);
            keybd_event(VK_CONTROL, 0, 0, (UIntPtr)0);
            keybd_event(VK_MENU, 0, 0, (UIntPtr)0);
            keybd_event(VK_LWIN, 0, 0, (UIntPtr)0);
             */
		if (shift_mode) keybd_event(VK_SHIFT, 0, 0, (UIntPtr)0);
		#endregion

		byte cd ;
		switch (code)
		{
			case "n": cd = 13; break;
			case "BS": cd = 8; break;
			default:
				cd = (byte)char.ConvertToUtf32(code, 0);
				break;
		}

		keybd_event(cd, 0, 0, (UIntPtr)0);
		keybd_event(cd, 0, 2/*KEYEVENTF_KEYUP*/, (UIntPtr)0);

		#region 特殊キーアップ処理
		/*keybd_event(VK_SHIFT, 0, 2, (UIntPtr)0);
            keybd_event(VK_CONTROL, 0, 2, (UIntPtr)0);
            keybd_event(VK_MENU, 0, 2, (UIntPtr)0);
            keybd_event(VK_LWIN, 0, 2, (UIntPtr)0);
             */
		if (shift_mode) keybd_event(VK_SHIFT, 0, 2, (UIntPtr)0);
		#endregion
	}

}

とこんな感じで作っていきます。

アルファベットキーボードで打っていたのですが、

  • アルファベットと数字の切り替えが面倒くさい。
  • カーソル移動やIME変換の場合には、カーソルキーがあったほうが良い

ので、単純に某iPhoneキーボードを流用するだけでは操作性に難点が出ます。acer の画面自体、iPhone よりも広いので、もっと大きくキーボードの領域が取れます。
ひとまず、某iPhoneキーボードの移植が終わったら、独自のキーボード(101か106キーボード)のデザインを考えてみましょうか。

~~
追記 2013/07/05

コメントで要望があってので、サンプルソースをアップしておきます。
http://sdrv.ms/17On4Fc
からダウンロードできます。
(中にあるキーボードのキャプチャは、まあ、試験的な画面キャプチャということで)

カテゴリー: 開発, C# パーマリンク

Windows 8 用のミニソフトキーボードを作る(2) への6件のフィードバック

  1. Mon のコメント:

    初めまして。
    最近、Windows8のタブレットを買い、
    タッチが考慮されていない古いゲームを無理やりタッチでプレイするために
    ゲームで使うキーのみに絞ったミニスクリーンキーボードを作ろうと思っていたら、
    ここにたどり着きました。

    もしよかったら、このソフトキーボードのソースを公開してもらうことはできませんか?
    まだプログラミングスキルが未熟で、一から作るのがしんどいので…。
    初めてのコメントでいきなり図々しいお願いをしてすみません。

    • masuda のコメント:

      ちょっと、ソースを紛失中…なので、見つかったらupしておきます。

    • Mon のコメント:

      ありがとうございます。
      一人で制作に取り組んでいたのですが、やはり難航中で…。
      お暇な時にでもよろしくお願いします。

  2. masuda のコメント:

    ソースが見つかったのでアップしておきます。
    http://sdrv.ms/17On4Fc
    からダウンロードしてください。

  3. Mon のコメント:

    ありがとうございました!
    WFのアクティブにせず最前面にするコードと、
    WPFのタッチイベントをどうやって両立させるのかで詰まっていたのですが、
    WFの上にコントロールとしてWPFを置けるんですね。

  4. masuda のコメント:

    ここではWPFを使って画像を貼り付けているのですが、あまり意味はなかったと思います。ちょっと理由は忘れましたが、確かWPFだけでやりたかったけど、うまくいかなかったのでWinフォームで試してからと考えたのだと思います。
    あまり使われていませんが、Winフォーム上のWPFコントロールは便利ですよ。標準的なコントロールをWPFで作ろうとすると二度手間になったりするので、きれいな画像(プレビューとか)を作りたい時に結構使えます。

コメントは停止中です。