9月2008

忙しいは最もつまらない言い訳

と思っていますので、とりあえず、忙しかったのでブログが更新できませんでした、とは言いません。ただ更新する気がなくて、ブログの更新の時間に割り当てなかったから、約2週間近く停滞していたわけです。

ところで、私はActiveBasicとCocoaを主にやって、それについてのことを適当にブログで語っているわけですが、両方同時に出来るわけは無いので、数ヶ月置きにABの話題を書いたり、Cocoaの話題を書いたりと、一定の期間ごとに切り替わっています。

自分では決してCocoaよりActiveBasicの記事が劣っているとは思っていないのですが、需要的な問題で、明らかにCocoaの記事を書いていた方がブログのアクセスが伸びるんですね。そういわけで、Cocoaの記事でアクセス集めた後に、ABの記事を書いて減ってたりすると、当然のこととわかっていながらも、多少モチベーションが下がるわけです。

さて、AB5のコンパイラはだいたい完成し、ライブラリも基礎的な文字列や配列などが揃ってきました。あとはこれらを使い、より高度なライブラリを作っていく作業に入っています。基礎的な部分は結構面倒だったりしましたが、より高度になるほどライブラリ作成は気分的には楽になっているので、今は前より面白い局面に入っています。

AB5は、なかなか良い出来になってきているので、今後ブログの需要が増えると期待して、今は頑張るしか無いですね。ちょっと今は訳ありでC++に奮闘中ですが、あと数日後にはネットワークライブラリ(主にソケット部分)の方を、コミットできる形に持っていこうと思っています。

iPhone – ソフトウェア上の入力デバイス

iPhoneは極力物理的な入力インターフェースは排除されており、主な入力を画面上のソフトウェアで作られたマルチタッチインターフェースで操作するようになっています。iPhoneがなぜこのようなインターフェースを採用しているかといえば、携帯デバイスの大きさでハードウェアキーボードを搭載すると、どうしても画面が小さくなる。逆に、画面を大きくするとキーボードが小さくなり、操作が難しくなってしまう。それを解決するために、思い切ってハードウェアキーボードを捨てて、画面いっぱいのディスプレイに直接触れることで入力する、ソフトウェアキーボードを導入したわけです。これがマルチタッチインターフェースを採用する最大の理由だと、少なくとも、iPhone発表時にそんなことを言っていた記憶が私の頭の中にあります。

マルチタッチインターフェースにはいくつかの欠点があります。もっとも深刻な欠点は、操作した時のフィードバックが物理的に感じることが出来ない点です。この点、iPhoneがうまくやっていることは、実機を手に取ってもらえばわかるかと思います。例えば、ボタンを押した時に音を鳴らすとともに、画面になんらかの効果を出す。これだけで、意外とボタンを押した感覚みたいなものが、擬似的に伝わってくる物なのです。この時に重要なことは、リアルタイムでこういった演出をすることです。

この欠点を克服することで、マルチタッチなソフトウェアインターフェースに不満を感じること無く、マルチタッチインターフェースの利点を享受することが出来るようになります。最初に述べた、大きな画面と使いやすいキーボード両立など。マルチタッチなソフトウェアインターフェースの最大の利点は、場面に応じてインターフェースを自由自在に変化させることが出来る点です。

次の2つの画像を見比べてみましょう。メールアドレス入力時と件名入力時では、キーボードに違いが見られます。

アドレス入力時:
メールアドレス

件名入力時:
件名

アドレス入力時には、Spaceキーが小さくなり、代わりにメールアドレスでよく使われる「.」「@」が追加されています。また、件名入力時は最初からシフトキーが押されており、大文字から始められるようにされています。

画面のマルチタッチデバイスにソフトウェアによってインターフェースを作成する。iPhoneは極力物理的なインターフェースを排除して、ソフトウェアで入力インターフェースを作成します。つまり、アプリケーションごとに、それ専用のデバイスにiPhoneをカスタマイズすることが可能なのです。少なくとも、入力インターフェースに関しては。

iPhoneは物理的な入力デバイスをソフトウェアから操作を可能にした、汎用的な携帯デバイスなのです。

計算機: どう見ても電卓です。
計算機

Karajan Beginner: 演奏用のアプリケーションではありませんが、鍵盤が出てきます。普通に弾けます。
karajan

NetNewsWire

私は普段からRSSリーダーを利用するのですが、Safariに付いてくる標準のRSSリーダー機能で十分な日々を送っていました。しかし、iPhoneを購入してから、SafariのRSS機能に不満を覚えるようになりました。どうしてかと言えば、なぜかMacとiPhoneのSafariでRSSを同期できない、もっと言えば、iPhoneのSafariのRSSリーダーは、RSSが読めるだけであり、自動更新機能がないのです。この程度のリーダーでは、iPhoneのSafariにRSS機能があるとは言えないでしょう。

そこで、AppStoreでRSSアプリを探してみると、見慣れたNetNewsWriteという文字が飛び込んできました。なぜ見慣れているのかと言えば、このブログのアクセス解析で、このブラウザからのアクセスがたまにあったからです。何のアプリケーションかは知らなかったのですが、このAppStoreの説明でRSSリーダーということがわかりました。そういうわけで、元々Mac用のアプリなので、iPhoneとMacのRSSを同期できるのではないかという期待を胸に、NetNewsWireをダウンロードしてみました。

アプリを立ち上げると、早速アカウントを作らされ、適当に同期されました。どうやら、iPhoneのNewNewsWireは読み専用で、MacまたはWebから登録したRSSを更新することができるようでした。なので、早速MacにもNetNewsWireをダウンロードし、SafariのブックマークをNetNewsWireに移しました。ちなみに、ブックマークをドラッグするだけで一括登録できるので楽に移行できました。そして、このMac版のNetNewsWireにもアカウントを設定すれば完了です。これで、MacとiPhoneのNetNewsWireでRSSが同期できるようになりました。

このiPhoneのNetNewsWireの特徴は、読み専用に特化されているということです。アプリを立ち上げると、サーバーからRSS購読リストを更新し、そして各RSSのチェックに入ります。更新チェックが終わると、更新されているものだけがリストに表示されます。そしてリストをたどっていけば、最終的にはブラウザを内蔵しているので、そのままWebページを表示することが可能です。もちろん、普通にRSSの部分だけを確認することが出来ます。

これで、プッシュノーティフィケーションサービスにも対応すれば、まるでメールを受信するようにRSSを読むことが出来るかもしれませんね。まあ、無料のサービスなので、そこまではやってくれない気がしますが。

ちなみに、Mac OS XにデフォルトのRSSリーダーを設定する機能があったことを、このとき始めて知りました。このデフォルトのRSSリーダーに登録すると、SafariとかでRSS購読するボタンを押した時に、NetNewsWireに登録されるようになります。便利ですね。

Mac RSS Reader – News Reader for Apple – NetNewsWire by NewsGator

非同期でTCP

前回は軽くDnsのメソッドを非同期化してみたので、今回はソケットのブロッキングが発生するメソッドの非同期版を実装してみました。前回は.NET風味な非同期の実装をしたのですが、今回は自分勝手にちょっとやってみました。ちなみに、前回IAsyncResultが理解できないとかなんとか言ってましたが、どうやら.NETにはBeginInvoke,EndInvokeとかいう大変便利そうなメソッドがあるらしく、これを使うと任意のメソッドが非同期で呼び出せるらしいです。これを使って他のBegin…,End…の非同期メソッドが実装されていると予測すると、IAsyncResultが出てくるのは当然だというわけです。

getting...
非同期なので取得中ウインドウを動かせます。当然のことですが。

コードはこちら。前回同様、最新のリポジトリ+展開したNet.zipをablib/src/Classesに放り込むと実行することが出来ます。

#require <Classes/ActiveBasic/Windows/UI/Form.ab>
#require <Classes/ActiveBasic/Windows/UI/Application.ab>
#require <Classes/ActiveBasic/Windows/UI/Button.ab>
#require <Classes/ActiveBasic/Windows/UI/EditBox.ab>
#require <Classes/ActiveBasic/Windows/UI/TaskMsg.ab>
#require <Classes/ActiveBasic/Windows/UI/ListBox.ab>

#require <api_ws2tcpip.sbp>
#require <Classes/System/Net/misc.ab>
#require <Classes/System/Net/Dns.ab>
#require <Classes/System/Net/EndPoint.ab>
#require <Classes/System/Net/IPAddress.ab>
#require <Classes/System/Net/IPHostEntry.ab>
#require <Classes/System/Net/Sockets/misc.ab>
#require <Classes/System/Net/Sockets/Socket.ab>

Imports ActiveBasic.Windows.UI
Imports System
Imports System.Collections.Generic
Imports System.Net
Imports System.Net.Sockets
Imports System.IO

#resource "UI_Sample.rc"

Class MyApplication
	Inherits Form
Public
	Sub MyApplication()
		AddCreate(AddressOf(OnCreate))
	End Sub

Protected
	Override Sub GetCreateStruct(ByRef cs As CREATESTRUCT)
		Super.GetCreateStruct(cs)
		With cs
			.style = WS_OVERLAPPED Or WS_CAPTION Or WS_SYSMENU Or WS_DLGFRAME Or WS_MINIMIZEBOX
			.cx = 400
			.cy = 400
		End With
	End Sub

Private
	Sub OnCreate(sender As Object, e As CreateArgs)
		Dim wpHFontControl = GetStockObject(DEFAULT_GUI_FONT) As WPARAM

		getButton = New Button
		With getButton
			.Create(This)
			.Text = "取得"
			.Move(280, 5, 80, 30)
			.AddClick(AddressOf(Get_Click))
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		textField = New EditBox
		With textField
			.Create(This, 0, WS_EX_CLIENTEDGE)
			.Move(10, 10, 175, 20)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		requestField = New EditBox
		With requestField
			.Create(This, ES_MULTILINE Or ES_WANTRETURN Or ES_AUTOVSCROLL Or WS_VSCROLL, WS_EX_CLIENTEDGE)
			.Move(10, 40, 375, 100)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		textView = New EditBox
		With textView
			.Create(This, ES_READONLY Or ES_MULTILINE Or ES_WANTRETURN Or ES_AUTOHSCROLL Or ES_AUTOVSCROLL Or WS_HSCROLL Or WS_VSCROLL, WS_EX_CLIENTEDGE)
			.Move(10, 150, 375, 190)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With
	End Sub

	Sub Get_Click(sender As Object, e As Args)
		InvalidForms = True
		text = New Text.StringBuilder
		textView.Text = text.ToString()
		Dns.BeginGetHostEntry(textField.Text, "http", AddressOf(GetHostCallback), Nothing)
	End Sub

	Sub GetHostCallback(ar As IAsyncResult)
		Dim host = Dns.EndGetHostEntry(ar).Host As IPEndPoint
		Dim socket = New Socket(host.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
		socket.ErrorDelegate = AddressOf(SocketDidError)
		socket.BeginConnect(host, AddressOf(SocketDidConnect))
	End Sub

	Sub SocketDidConnect(s As Socket)
		Dim request = requestField.Text + Ex"¥n¥n" As String
		s.BeginSend(request, request.Length, AddressOf(SocketDidSend))
	End Sub

	Sub SocketDidSend(s As Socket, n As Long)
		s.BeginReceive(AddressOf(SocketDidReceive))
	End Sub

	Sub SocketDidReceive(s As Socket, buffer As *Byte, len As Long)
		If len Then
			Dim receive = New String(buffer As PCTSTR, len)
			text.Append(receive)
			s.BeginReceive(AddressOf(SocketDidReceive))
		Else
			s.Close()
			textView.Text = text.ToString()
			InvalidForms = False
		End If
	End Sub

	Sub SocketDidError(s As Socket, ex As Exception)
		MessageBox(0, ex.ToString(), "エラー", MB_OK)
		s.Close()
		InvalidForms = False
	End Sub

	Sub InvalidForms(flag As Boolean)
		textField.Enabled = Not flag
		requestField.Enabled = Not flag
		getButton.Enabled = Not flag
		If flag Then
			getButton.Text = "取得中..."
		Else
			getButton.Text = "取得"
		End If
	End Sub

	getButton As Button
	textField As EditBox
	requestField As EditBox
	textView As EditBox

	text As Text.StringBuilder
End Class

Control.Initialize(GetModuleHandle(0))
Dim f = New MyApplication
f.CreateForm()
Winsock.Initialize()
Application.Run(f)
Winsock.Finalize()

「取得」イベントとかの部分。

Sub Get_Click(sender As Object, e As Args)
	InvalidForms = True
	text = New Text.StringBuilder
	textView.Text = text.ToString()
	Dns.BeginGetHostEntry(textField.Text, "http", AddressOf(GetHostCallback), Nothing)
End Sub

Sub GetHostCallback(ar As IAsyncResult)
	Dim host = Dns.EndGetHostEntry(ar).Host As IPEndPoint
	Dim socket = New Socket(host.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
	socket.ErrorDelegate = AddressOf(SocketDidError)
	socket.BeginConnect(host, AddressOf(SocketDidConnect))
End Sub

Sub SocketDidConnect(s As Socket)
	Dim request = requestField.Text + Ex"¥n¥n" As String
	s.BeginSend(request, request.Length, AddressOf(SocketDidSend))
End Sub

Sub SocketDidSend(s As Socket, n As Long)
	s.BeginReceive(AddressOf(SocketDidReceive))
End Sub

Sub SocketDidReceive(s As Socket, buffer As *Byte, len As Long)
	If len Then
		Dim receive = New String(buffer As PCTSTR, len)
		text.Append(receive)
		s.BeginReceive(AddressOf(SocketDidReceive))
	Else
		s.Close()
		textView.Text = text.ToString()
		InvalidForms = False
	End If
End Sub

Sub SocketDidError(s As Socket, ex As Exception)
	MessageBox(0, ex.ToString(), "エラー", MB_OK)
	s.Close()
	InvalidForms = False
End Sub

Sub InvalidForms(flag As Boolean)
	textField.Enabled = Not flag
	requestField.Enabled = Not flag
	getButton.Enabled = Not flag
	If flag Then
		getButton.Text = "取得中..."
	Else
		getButton.Text = "取得"
	End If
End Sub

DNSの名前解決は前回のコードと同様です。そのあとのソケットの部分を今回作りました。今回の実装は、デリゲートの引数に戻り値を入れる実装にしてみました。途中でエラーが発生した場合は、あらかじめソケットに設定しておいたエラー用のデリゲートに飛ぶようにしてあります。

さて、ソケットの基本的な部分がほとんど実装できてきたので、次は何を実装しようか迷います。TCP,UDPソケットのクラス(.NETのTCPClientとか)って必要あるのか、ちょっと疑問だったりします。とりあえず、NetworkStreamの実装には入ろうかどうかってところでしょうか。

非同期に挑戦してみる

今日は初めて非同期に挑戦してみました。マルチスレッド関連のライブラリは既に出来上がっているので、それを利用したマルチスレッドプログラムで非同期処理をします。とりあえず非同期の雰囲気をつかむために、単純な非同期関数であるDns.BeginGetHostEntryを実装してみました。

非同期的にアドレスを取得

もうライブラリにあたるコードを載せるのは面倒なので、こちらにzipでまとめました。ablic/src/Classesの中に入れてください。
Net.zip

以下、開発中のネットワークライブラリを使用したサンプルコードです。名前空間は暫定System.Netに入れてますが、どこに入れるか考え中です。

#require <Classes/ActiveBasic/Windows/UI/Form.ab>
#require <Classes/ActiveBasic/Windows/UI/Application.ab>
#require <Classes/ActiveBasic/Windows/UI/Button.ab>
#require <Classes/ActiveBasic/Windows/UI/EditBox.ab>
#require <Classes/ActiveBasic/Windows/UI/TaskMsg.ab>
#require <Classes/ActiveBasic/Windows/UI/ListBox.ab>

#require <api_ws2tcpip.sbp>
#require <Classes/System/Net/misc.ab>
#require <Classes/System/Net/Dns.ab>
#require <Classes/System/Net/EndPoint.ab>
#require <Classes/System/Net/IPAddress.ab>
#require <Classes/System/Net/IPHostEntry.ab>
#require <Classes/System/Net/Sockets/misc.ab>
#require <Classes/System/Net/Sockets/Socket.ab>

Imports ActiveBasic.Windows.UI
Imports System
Imports System.Collections.Generic
Imports System.Net
Imports System.Net.Sockets
Imports System.IO

#resource "UI_Sample.rc"

Class MyApplication
	Inherits Form
Public
	Sub MyApplication()
		AddCreate(AddressOf(OnCreate))
	End Sub

Protected
	Override Sub GetCreateStruct(ByRef cs As CREATESTRUCT)
		Super.GetCreateStruct(cs)
		With cs
			.style = WS_OVERLAPPED Or WS_CAPTION Or WS_SYSMENU Or WS_DLGFRAME Or WS_MINIMIZEBOX
			.cx = 300
			.cy = 200
		End With
	End Sub

Private
	Sub OnCreate(sender As Object, e As CreateArgs)
		Dim wpHFontControl = GetStockObject(DEFAULT_GUI_FONT) As WPARAM

		getButton = New Button
		With getButton
			.Create(This)
			.Text = "取得"
			.Move(205, 5, 75, 30)
			.AddClick(AddressOf(Get_Click))
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		textField = New EditBox
		With textField
			.Create(This, 0, WS_EX_CLIENTEDGE)
			.Move(10, 10, 175, 20)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With

		addressList = New ListBox
		With addressList
			.Create(This, WS_VSCROLL, WS_EX_CLIENTEDGE, 0)
			.Move(10, 40, 275, 140)
			.SendMessage(WM_SETFONT, wpHFontControl, 0)
		End With
	End Sub

	Sub Get_Click(sender As Object, e As Args)
		getButton.Enabled = False
		getButton.Text = "取得中..."
		addressList.Items.Clear()
		Dns.BeginGetHostEntry(textField.Text, String.Empty, AddressOf(GetHostCallback), Nothing)
	End Sub

	Sub GetHostCallback(ar As IAsyncResult)
		Try
			Dim address = Nothing As IPAddress
			Foreach address In Dns.EndGetHostEntry(ar).IPAddressList
				addressList.Items.Add(address.ToString())
			Next
		Catch ex As Exception
			TaskMsg(This, "取得に失敗しました", ex.ToString(), "URLが正しいか確認してください。")
		End Try
		getButton.Text = "取得"
		getButton.Enabled = True
	End Sub

	getButton As Button
	textField As EditBox
	addressList As ListBox
End Class

Control.Initialize(GetModuleHandle(0))
Dim f = New MyApplication
f.CreateForm()
Winsock.Initialize()
Application.Run(f)
Winsock.Finalize()

以下、メインであるボタンを押した時のイベントの部分だけを抜き出したもの

Sub Get_Click(sender As Object, e As Args)
	getButton.Enabled = False
	getButton.Text = "取得中..."
	addressList.Items.Clear()
	Dns.BeginGetHostEntry(textField.Text, String.Empty, AddressOf(GetHostCallback), Nothing)
End Sub

Sub GetHostCallback(ar As IAsyncResult)
	Try
		Dim address = Nothing As IPAddress
		Foreach address In Dns.EndGetHostEntry(ar).IPAddressList
			addressList.Items.Add(address.ToString())
		Next
	Catch ex As Exception
		TaskMsg(This, "取得に失敗しました", ex.ToString(), "ホスト名が正しいか確認してください。")
	End Try
	getButton.Text = "取得"
	getButton.Enabled = True
End Sub

注意:例外処理がうまくいかないので、取得できるホスト名やアドレスをしていた方がいいです。

やー、いいものですね、非同期は。GUIが固まりませんからね。画像じゃわからないのが残念です。BeginGetHostEntryで関数を呼び、指定したコールバックに終了が通知されます。そしてその完了通知の情報をEndGetHostEntryに渡すことで、戻り値を取得することが出来ます。こんな感じです。

ところで、.Netの非同期処理でよく見かける、IAsyncResultを使ったBegin… – End…のパターンが、とても理解し難いです。使うのにも理解し難い物がありますが、なんでこんな設計なのかも意味が分かりません。素直にコールバックで戻り値を投げればいいと思うのは私だけでしょうか?IAsyncResultは何が嬉しいのでしょう。