宇宙仮面の C# プログラミング |
|||||||||||||||||||||||||||||||||||||||||||||||||
|
| |||||||||||||||||||||||||||||||||||||||||||||||||
| ||
|
|
|
簡単なモバイルアプリケーション開発9
|
開発環境: Visual Studio 2005
/2008
1.目次 |
||||||||||||
2.目的 |
||||||||||||
|
簡単なモバイルアプリケーション開発7 Amedas 情報の表示を 作りましたが、予想外に反響が大きく、地域などのプリセット、画像の拡大縮小、画像の保存位置情報等いくつか要望があがってきました。もともと、そこまで作るつもりはなかったのですが、自分自身でもこの1週間使ってみたところ、結構使えるので、 設定の保存ぐらいしたくなり、V2.0 で拡張することにしました。と、予告してから後悔している今日この頃・・・ 画像情報の拡大縮小、画像情報の保存を実施しようと思うと、WebBrowser コントロールでブラウズする方法では対応できません。このため、そもそもの設計を変更し、気象情報を画像としてダウンロードして、PictureBox コントロールに表示させる必要が出てきます。この場合、URL を組み立てて、ファイルを取得して、あとはその画像のビューアーを作るという形になります。しかし、ファイルに落とすには、わかりやすい名前でファイルをダウンロードする必要があります。そのためには、種類、地域、時間情報からファイル名を作成し、再度オープンするような仕組みが必要になります。 このような画像を表示するような実装に変更するとなると、画像ビューアーを作ることになるので、かなりの拡張が必要になります。 画像は次のように固定倍率ですが、拡大・縮小と縦横の移動も可能にします。ちなみに、次の画像は 気象衛星→全球→可視光 で、お気に入り。
おまけに、拡張しているうちに、海釣りに行くことになり、夏なので紫外線も知りたいとか、台風5号が発生しそうなので衛星写真も全球、北半球を見たい、 津波があっても怖いので地震も知りたい、見れるものは何でも見たいと、ほとんどお天気フェチに走ってしまいました。w このため、結局当初の想定を大幅に超えて、以下の拡張を行いました。
注意:
|
||||||||||||
3.参考書 |
||||||||||||
(1) 気象庁の防災気象情報 |
||||||||||||
4.作り方 |
||||||||||||
|
|
||||||||||||
4.1 時間との戦い |
||||||||||||
|
さて、これだけ機能を追加しようと思うと、さすがに作り方をステップバイステップで解説するのは気が遠くなるので、わかりにくいクラスの概要を説明して、あとは ソースコード、インストーラを一式つけておきます。 何が、大変だったかというと、やはり画像を取り出すための URL に規則性がなく、それを個別に実装していくと、いたずらに同じようなコードが並ぶことになります。そこで、どこまで一般化できるか(するかどうか)が悩みました 。あまり凝った構成にすると全体が把握しづらくなるし、べたに書くとコードのメインテナンス性が落ちるし、ちょうどいい構成が難しいですね。 URLに関しては、たとえばアメダスの降水量であれば、 "http://www.jma.go.jp/jp/amedas/imgs/rain/{0}/{1}.png" のように場所と時間がパラメータになります。時間は、過去から現在まで1時間おきです。 一方、警報は、次のように場所のみで、時間のパラメータは取りません。 "http://www.jma.go.jp/jp/warn/imgs/{0}/99.png" また、予測: 紫外線はさらに複雑で、場所と時間のパラメータを取りますが、昼間と、18時〜翌日の6時の夜間では、与える時間のパラメータが違います。 "http://www.jma.go.jp/jp/uv/imgs/uv_color/forecast/{0}/{1}.png" さらに、天気図では、24時間予報図、48時間予報図、実況天気図(アジア)では、過去3時間おき(3, 6, 9,・・・)であったり、過去6時間おき (3, 9, 15, 21)であったり、過去12時間おき (9, 21)であったり、と様々です。おまけに、これらの時間が、気象情報の表示種類が変わるごとに変更しなければなりません。 これらを単純に1つのメソッドで実装してしまうと、if...else if .... else if ... みたいになるか、switch が延々と並び、さらにそれらをメニューに登録しなければならず、とんでもないコードになります。 URLの時間のパラメータを見てみると、最低限 メニューに表示する文字列、url に時間パラメータを渡すための文字列(TimeCommand)があればよいことがわかります。しかし、MenuItem には、この時間パラメータを渡すための文字列(TimeCommand)を持たす場所がありません。そこで、MenuItemを継承して、MenuItemTime クラスを作成し、そこにTimeCommandを保持するようにします。 public class MenuItemTime : MenuItem{ private string timeCommand; public string TimeCommand { get { return timeCommand; } set { timeCommand = value; } } public MenuItemTime(string menuText, string timeCmd) { Text = menuText; TimeCommand = timeCmd; } } このMenuItemTimeを MenuItemCollection クラスに追加して、Menu.MenuItems に直接セットすることができるととても楽なのですが、このメソッドが用意されていません。しかたないので、MenuItemTimeの配列を用意し、それを順番にMenu.MenuItems に Add してく必要があります。 そこで、MenuItemCollection のかわりに、List<MenuItemTime> のジェネリックスを使用して、一度このリストに時間メニューの値を用意し、次にMenu.MenuItems にセットするという方法を使います。そのために、まず List<MenuItemTime> のラッパークラスとして、ListMenuItemTime クラスを次のように定義します。このメソッドとしては、時間経過に伴って、時間メニューを更新しなければならないので、そのためのUpdateメソッド、それからMenu に値をセットするための、SetTimeMenu(Menu menu, EventHandler eh) を定義します。ClearMenuCheck(Menu menuItem) は、メニューのチェックをまとめて外すための補助メソッドです。 public class ListMenuItemTime{ protected List<MenuItemTime> timeList = new List<MenuItemTime>(); public void Set(List<MenuItemTime> tList) { timeList = tList; } /// <summary> /// 時間のリストList<MenuItemTime>を更新する。 /// </summary> virtual public void Update() { timeList.Clear(); } /// <summary> /// メニューに時間をセットする /// </summary> /// <param name="menu">セットするメニュー</param> /// <param name="eh">メニューのイベントハンドラ</param> /// <returns></returns> virtual public string SetTimeMenu(Menu menu, EventHandler eh) { menu.MenuItems.Clear(); foreach (MenuItemTime at in timeList) { int i = menu.MenuItems.Add(at); menu.MenuItems[i].Click += new System.EventHandler(eh); } ClearMenuCheck(menu); return null; } virtual public string GetDefaultTimeCommand() { return null; } protected void ClearMenuCheck(Menu menuItem) { // メニューのチェックをクリアする foreach (MenuItem mi in menuItem.MenuItems) { if (mi.Checked) mi.Checked = false; if (mi.MenuItems.Count != 0) ClearMenuCheck(mi); } } } あとは、それぞれの気象情報に合わせて、ListMenuItemTime というリストを作成します。まとめると次のような構成になります。 public class TimeListNormal : ListMenuItemTime{ /// <summary> /// レーダー・降水ナウキャスト以外の場合の時間メニューを作成 /// 過去12時間まで1時間刻み /// </summary> public override void Update() { base.Update(); // 中略 for (int i = 0; i < 12; i++) { // 過去12時間まで1時間刻みで追加する。 DateTime t = baseHour.Subtract(new TimeSpan(i, 0, 0)); string timeCmd = t.ToString("yyyyMMddHHmm") + "-00"; timeList.Add(new MenuItemTime(t.ToString("HH:mm"),timeCmd)); } } /// <summary> /// メニューに時間をセットする /// </summary> public override void SetTimeMenu(Menu menu, EventHandler eh) { base.SetTimeMenu(menu, eh); timeList[0].Checked = true; } public override string GetDefaultTimeCommand() { return timeList[0].TimeCommand; } } このように各時間メニューごとに ListMenuItemTimeから継承したクラスを作成します。(下図) このクラスダイアグラムは一部で、全体としてはこのぐらい(下図)になります。 ファイルは、ClassMenuTime.cs にまとめてあり(下図)、500行程度です。 このままだと、これらのインスタンスがバラバラなので、MenuItemHanderクラスでインスタンスをまとめて管理します。(下図) |
||||||||||||
4.2 地域メニュー |
||||||||||||
|
幸い、地域メニューは地方だけに絞り込んだので、シンプルにすみました。時間メニューと同様に、MenuItemを継承して、locationCode を持たせた MenuItemLocation クラスを作成します(下図)。これをMenuLocation クラスで、場所メニューに追加するだけです。 using System;using System.Windows.Forms; namespace Uchukamen.WZero3.WZero3DeAmedas { public class MenuItemLocation : MenuItem { private string locationCode; public string LocationCode { get { return locationCode; } set { locationCode = value; } } public MenuItemLocation(string locCode, string name, EventHandler eh) { locationCode = locCode; Text = name; Click += new System.EventHandler(eh); } } public class MenuLocation { public void Set(MenuItem mi, EventHandler eh) { mi.MenuItems.Add(new MenuItemLocation("000", "全国", eh)); mi.MenuItems.Add(new MenuItemLocation("201", "北海道地方(北西部)", eh)); 中略 mi.MenuItems.Add(new MenuItemLocation("219", "宮古・八重山地方", eh)); } } } |
||||||||||||
4.3 種類メニュー |
||||||||||||
|
種類メニューも基本的に地域メニューと同じ考え方で、V1.2.0.0から大きな変更はありません。 各MenuItemは、画像をアクセスするための Url の文字列フォーマット UrlString と、その文字列フォーマットに位置情報が必要かどうか(bool LocationMenu)、場所情報が必要かどうか(bool TimeMenu)を持たせます。 種類に関するメニューは、MenuForType クラスが管理します(下図)。ここで、SetAmedasMenu で気象関連の種類のメニューを追加し、SetEarthQuakeMenu メソッドで地震関連の種類のメニューを追加します。 using System;using System.Collections.Generic; using System.Windows.Forms; namespace Uchukamen.WZero3.WZero3DeAmedas { public class MenuItemType : MenuItem { private bool timeMenu; public bool TimeMenu { get { return timeMenu; } set { timeMenu = value; } } 中略 public
MenuItemType(string
name, bool
loc, bool
time, 種類のメニューの初期設定は、次のように呼び出します。 private
void CreateMenuForType() これで、種類メニュー関連をこの1つのクラスに集中させます。 |
||||||||||||
4.4 画像を読み込む |
||||||||||||
|
さて、ここまででメニュー関連の実装はほぼ終わり、メニューを選択することにより、種類、地域、時間に応じたパラメータを扱うことができるようになりました。次に、これらのパラメータから、画像のURLを計算し、画像を読み込みます。画像を読み込む際には、メインフォームと同じスレッドで読み込むと、メインフォームが画像の読み込み中にフリーズしてしまいます。そこで、画像を読み込む処理はメインスレッドとは別のスレッドで実行する必要があります。 .NET Framework 2.0ではスレッド関連の機能も強化されていて、パラメータを渡してスレッドを実行することができます。詳細は、http://www.microsoft.com/japan/msdn/netframework/skillup/core/article6.aspx を見てください。 しかし、.NET Compact Framework 2.0では、この機能がサポートされていません。したがって、次のような ThreadImageLoader クラスを作成する必要がありました。実装している内容は簡単で、ThreadImageLoader クラスにパラメータを渡しておき、その中でパラメータを使用しながらスレッドを実行して画像を読み込むという処理になります。 class ThreadImageLoader{ private Thread thread = null; public ThreadImageLoader(FormMain fMain, PictureBoxWeb pBox, ProgressBar pBar, Uri param, bool save) { // パラメータをセット } // スレッドを実行 public void Start() { thread = new Thread(AsyncLoadImage); thread.Start(); } public void Abort() { thread.Abort(); } public void Join() { thread.Join(); } // 画像を読み込む public void AsyncLoadImage() { ・・・ } } ここで注意しなければならないのが、メインフォームは別のスレッドで画像の読み込みを行い、その画像をメインスレッドの PictureBox に書き込んでやる必要があります。しかし、メインスレッドと同一のスレッドでなければ、メインスレッドのPictureBoxに書き込みできません。このような別スレッドから、コントロールと同じスレッドに処理を渡す方法として、ほとんどのコントロールに Invoke メソッドが用意されています。別スレッドに、コントロールのインスタンスを渡しておいて、そのコントロールから Invoke メソッドを呼び出してあげると、コントロールと同一のスレッドで動作してくれます。同様に、非同期の BeginInvoke, EndInvoke もサポートされています。ここでは、Invoke メソッドでコントロールと同じスレッドをキックしてあげます。.NET Framework 2.0からは、コントロールに対して別のスレッドから書き込みが行われると例外が上がるようになっています。 具体的な AsyncLoadImage() の骨格は、次のような流れになっています。 public void AsyncLoadImage(){ try { WebRequest request = HttpWebRequest.Create(urlAmedas.ToString()); 中略 using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) using (BinaryReader dataStream = new BinaryReader(response.GetResponseStream())) using (MemoryStream memStream = new MemoryStream((int)(response.ContentLength))) { byte[] buff = new byte[response.ContentLength + 10000]; 中略 while (true) { 画像をメモリーストリームに読み込む } // Bitmap をメモリストリームから作成する。 bmp = new Bitmap(memStream); // 必要に応じて、メモリストリームからファイルに書き出す。 } picBox.Invoke(new DelegSetImage(picBox.SetImage), new object[] { bmp }); picBox.Invoke(new DelegScale(picBox.scale), new object[] { 1.0f }); } catch (WebException exc) { 中略 } finally { 中略 } } これに、あとはプログレスバーを表示したり、ファイルに書き出したりの処理が入るだけですので、1つ1つ追っていけば理解できると思います。 |
||||||||||||
4.5 画像の表示 |
||||||||||||
|
さて、画像読み込み用のスレッドで画像の読み込みが終わると、picBox.Invoke(new DelegVoid(picBox.Invalidate)) が最後に呼び出されて、picBox の再描画が必要になったことをメインフォームに教えます。メインフォームでは、Invalideate()が呼び出されると、OnPaint イベントハンドらが呼び出されて、PictureBox の再描画処理が始まります。この OnPaint で実際の描画処理を実装してあげればよいことになります。 ところで、今回は拡大・縮小、移動を行わせようとすると、メインフォームでいろいろ実装する必要が出てきてしまいます。しかし、このような拡大・縮小、移動であれば PictureBox 内で閉じて実行することができるので、PictureBox から継承したクラスを作成し、そこにそのような面倒な処理を隠ぺいすることにします。 そこで、次のような PictureBoxから継承した PictureBoxWeb クラスを作成し、その中で拡大・縮小 ScaleImage メソッド、移動のための MoveUp/MoveDown/MoveLeft/MoveRight メソッドを実装します。
拡大縮小も同様に、スケールのパラメータからオリジナルの画像の座標と描画先の座標を計算して、再描画をかけるだけですが、ちょっとめんどうなのでPowerPointに拡大・縮小の説明をまとめておきました。
///
<summary> そして、最後にInvalidate を呼び出すことにより、次の OnPaint イベントハンドらが呼び出されます。そこでは、基本的に、計算したオリジナルの画像の座標と描画先の座標をもとに、originalの画像をもとにDrawImage で描画を行います。 protected
override
void OnPaint(PaintEventArgs
pe) |
||||||||||||
4.6 地震情報の取扱い |
||||||||||||
|
||||||||||||
4.7 FormMain の処理 |
||||||||||||
|
||||||||||||
4.8 作ってみての感想 |
||||||||||||
|
||||||||||||
5. ダウンロード |
||||||||||||
|
||||||||||||
6. ソースコード |
||||||||||||
|
||||||||||||
7. まとめ |
||||||||||||
|