C# Programming

Image簡単なモバイルアプリケーション開発1 〜 WZero3StatusMeter

開発環境: Visual Studio 2005 

1.目次

1.目次
2.目的
3.参考書
4.作り方
5.ダウンロード
6.ソースコード
7.さいごに

2.目的

W-Zero3用のWindows Mobile の簡単なアプリケーションとして、よくある機能でCPU の使用率、メモリ使用量、ディスク使用量をグラフィカルに表示するようにします。

Image

ここでは単機能のものを作る中で 、Windows Mobileでの開発で注意すべきことなどを説明したいと思います。

3.参考書

  1. C#研究室.Live Space / W-ZERO3
  2. MSDN .NET Compact Framework 向けの表示方向切り替え対応および高dpi対応アプリケーションの開発

4.作り方

4.1 基本的なデザインや動作を決める

単にCPUの状態を表示するといっても、表示方法としては、Task Managerみたいに時間軸で表示していく方法や、タコメータみたくリアルタイムに表示する方法があります。また、表示方法も、棒グラフにするのか、折れ線グラフにするのか、メーター表示にするのかLED表示にするのか、いろいろな表示方法があります。

これから作ろうとするアプリケーションの基本的なデザインや動作などの基本設計がふらふらしていると、あとからの修正が非常に大変になり、場合によっては作り直しということになります。まずは、どのようなデザインや動作にするのかしっかり決めます。

ここでは、棒グラフでCPU の使用率、メモリ使用量、ディスク使用量を表示することにします。

4.2 部品の動作確認

CPU の使用率、メモリ使用量、ディスク使用量を取得できることが必須です。そこで、そのためのクラスの実装と、テストを 先行して行います。このクラス単体で正しく動作することを確認できないと、あとでUI側の問題なのか切り分けが面倒になりますので、単体テストを実施し、確実に動作するようにします。

なお、今回のCPU の使用率、メモリ使用量、ディスク使用量を取得するためのクラスは、6.1章にソースコードを記載しています。InterOpしている簡単なラッパークラスなので、特に説明は必要はないと思います。

4.3 画像パーツの作成

基本的なデザインに従って、画面パーツを作成します。私の場合、高価なデザインツールなど持っていないので、PowerPointで代用しました。お手軽に作る分にはそれなりに使えます。次のように、1枚目がメインページのデザイン、2枚目がグラフがどのように表示するかのデザイン、3枚目がAbout画面のデザインです。

Image

このようなUIのアプリケーションを作る際に気をつけなければいけないのが、UIのデザインに意外と時間がかかるということです。変なところに凝ったり、色が気に入らなかったり、サイズが合わなかったり、コントロールやフォントのサイズの微調整が必要だったりして、非常に時間がかかります。

今回使用する画像のパーツは次のような、背景画像、LEDの赤、LEDの青、About画面の4つだけです。 \

Image

今回の程度のアプリケーションであれば、Visual StudioやC#の実装に慣れた方なら、2−3時間で 実装できるでしょう。しかし、このたった4つの画像のデザインやコントロールとの位置合わせのほうが時間がかかっています。

一人で作っていてもこの始末ですから、複数の人で作る場合や、別の人に提供する場合には、あとから「ああじゃない」、「こうじゃない」などと言ってきますので注意が必要です。

画像のパーツのサイズも大事です。大きすぎると、アプリケーションのサイズが大きくなります。小さすぎると、ぎざぎざしてしまいます。また、場合によっては解像度の違いによりモアレが発生してしまう場合もあるでしょう。できれば、表示サイズと実際のデザインのサイズが一致しているのが望ましいです。

4.4 プロジェクトの作成

  1. プロジェクトの新規作成
    Visual Studioのメニューからプロジェクトの新規作成で、プロジェクトの種類で [スマートデバイス] → [デバイスアプリケーション] を選択します。
     
  2. ターゲットデバイスの設定
    エミュレータでも可能ですが、エミュレータではレスポンスが悪く、かったるいです。そこで、Windows Mobile 5.0 Pocket PC Deviceを指定して、実機上で開発します。
     
  3. Formの [FormFactor]プロパティ
    [Windows Mobile 5.0 Pocket PC VGA] を指定します。
     

4.5 実装

CPUの使用率の実装を例にとり説明を行います。メモリ使用量、ディスク使用量の実装は、ほぼ同じです。

  1. PictureBoxの貼り付け
    背景画像を張り付けるために、Picture をFormに貼り付けます。Dockプロパティは Top とします。これにより、縦型、横型でのデザインが楽になります。
     
  2. 背景画像の貼り付け
    1で張り付けた PictureBoxの Image プロパティに、4.3章で作成した画像を貼り付けます。ここまでで、つぎのようになります。

    Image
     
  3. CPUの使用率クラスの追加
    CPUの使用率、メモリ使用量、ディスク使用量ですが、.NET Framework から直接取得できません。そこで、InterOpで取得する必要があります。そこで、それぞれのラッパークラスを作成することにします。ソリューションエクスプローラーで、CPUStatus.cs、DiskStatus.cs、MemoryStatus.cs の3つのC# クラスを追加します。クラスの実装については、6.1章に記載します。
     
  4. タイマーの追加
    ある一定時間ごとに、CPUの使用率を更新するために、タイマーを追加します。タイマーの Interval プロパティは 1000(1秒)とします。
     
  5. タイマーのイベントハンドラを追加
    タイマーにイベントハンドらを追加します。CPUの棒グラフは、PictureBoxに描画することにします。そこで、pictureBoxCPUを追加します。タイマーの処理では、pictureBoxCPUにCPUの棒グラフを描画し、それをRefresh()で再描画指示をします。

    private void timer1_Tick(object sender, EventArgs e)
    {
        RedrawCPU();
        pictureBoxCPU.Refresh();
    }
     
  6. CPU棒グラフのためのPictureBoxを貼り付ける
    CPU棒グラフを表示するために、pictureBoxCPUを次のように貼り付けます。この pictureBoxCPU に赤いLEDをCPUの使用率の分だけ描画し、残りは非点灯の青いLEDを描画 するようにします。背景色は、黒にします。
    Image

     
  7. CPU棒グラフの実装
    pictureBoxCPUに赤いLEDをCPUの使用率だけ描画し、残りは非点灯の青いLEDを描画します。ロジックは簡単で次の通りです。ここでは、将来的な拡張(するのか?)を考えて、ImageListで、赤いLEDと青いLEDを追加していますが、直接Bitmapを使用しても問題ありません。

    private
    CPUStatus cpuStat = new CPUStatus();

    private void RedrawCPU()
    {
     
    using (Graphics gimg = Graphics.FromImage(pictureBoxCPU.Image))
      {
       
    CPUStatus cpuStat = new CPUStatus();

       
    uint val = cpuStat.GetCPU();
       
    for (int i = 0; i < pictureBoxCPU.Image.Width; i += 20)
        {
         
    if (val > 100 * i / pictureBoxCPU.Image.Width)
            gimg.DrawImage(imageList1.Images[0], i, 0);
         
    else
           
    gimg.DrawImage(imageList1.Images[1], i, 0);
        }
      }
    }

     
  8. 動作の確認
    ここまでで、実行してみてください。CPU棒グラフが表示されればOKです。
     
  9. メモリ使用量、ディスク使用量の棒グラフの実装
    あとは同様に、メモリ使用量、ディスク使用量の棒グラフを実装します。ただ、CPUと同様に1秒ごとに棒グラフを描画するようにすと、かなりCPUを食われてしまいます。メモリ使用量、ディスク使用量は、1秒ごとに更新する必要はないですよね?そこで、もう1つタイマーを追加して、10秒ごとに更新するようにして、負荷を下げてあげます 。

4.6 縦型、横型レイアウトの調整

Windows Mobile デバイスの場合、縦型、横型のレイアウトが可能なので、画面のレイアウトが変わると、コントロールの配置も変える必要があります。 このため、縦型、横型のどちらでも正しくレイアウトできるように、なるべく早い段階で、画面の設計を固めておくことがとても重要です。 縦型、横型のレイアウトは、次のボタンで変更が可能です。どちらの場合でも正しく表示できるように調整しましょう。

Image

また、プロパティだけで縦型、横型のレイアウトの調整ができない場合は、実装で対応する必要があります。その方法は参考文献(2)を参照してください。

4.7 メインメニューの追加

  1. メインメニュー: 左ボタン
    左ボタンを押したときのイベントハンドらで、Application.Exit()を実行するようにします。

    private void menuItemTerminate_Click(object sender, EventArgs e)
    {
       
    Application.Exit();
    }
     
  2. メインメニュー: 右ボタン
    About ボタン をおしたら、About画面を表示するようにします。プロジェクトに、FormAboutフォームを追加します。そのフォームに、PictureBoxをDOCKプロパティ=FILLで貼り付けます。PictureBoxのImageプロパティには、先に作ったAboutの画像を貼り付けます。
    メインメニュー右ボタンのイベントハンドラを追加し、FormAboutの ShowDialog() を呼び出します。

    private void menuItemAbout_Click(object sender, EventArgs e)
    {
       
    FormAbout formAbout = new FormAbout();
        formAbout.ShowDialog();
    }

    これで、次のような About画面を追加します。About画面は、画像である必要は全くありません。ただ、カッコ良く作ろうと思うと、画像を張り付けたほうが簡単です。


    Image

4.8 アイコンの追加

新しい項目の追加で、アイコンを追加します。

Image

アイコンのサイズは、16x16と32x32を用意し、ビルドアクションを"埋め込まれたリソース"にします。

Image

4.9 バージョン情報の表示

  1. バージョン情報の表示
    アプリケーションのバージョンを表示します。ラベル(labelVersion)を貼り付け、次のようにバージョン情報を表示するようにします。

    Assembly
    assembly = Assembly.GetExecutingAssembly();
    labelVersion.Text =
    "V." + assembly.GetName().Version.ToString();
     
  2. このためには、アセンブリ情報でアセンブリバージョンを正しく入力しておく必要があります。
    アセンブリ情報は、[プロジェクト]→[プロパティ]→[アプリケーション]を表示します。
    Image
     
  3. [アセンブリ情報]ボタンを押し、ダイアログから設定できます。

    Image

4.11 インストーラの追加

新しいプロジェクトで [その他のプロジェクトの種類]→[セットアップウィザード]を選択する。このときのプロジェクト名に "WZero3StatusMeterCab" という名前を付ける。この場合、"WZero3StatusMeterCab.cab"というファイルが作られることになる。

Image

"WZero3StatusMeterCab"プロジェクトを選択し、メインメニュー→[表示]→[プロパティウィンドウ]でプロパティウィンドウを開きます。ここで、Manufacturer、ProductNameを修正します。特に ProductName に従って、"Program Files\ProductName"フォルダが作られますので注意してください。

Image

同様に、ソリューションエクスプローラから "WZero3StatusMeterCab"プロジェクトを選択し、右クリックを押し、プロパティを選択します。すると次のような、プロパティページが開きます。ここで、出力ファイル名が、"WZero3StatusMeterCab.cab"という間抜けなファイル名になっているので、適切に修正します。

Image

アプリケーションフォルダに、"プライマリ出力"を追加します。

Image

5. ダウンロード

Version 1.0.0.1  2007/3/27 WZero3StatusMeter.cab

6. ソースコード

主なソースコードです。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics;
using System.Reflection;

namespace Uchukamen.WZero3
{
    public partial class FormWZero3Meter : Form
    {
        public FormWZero3Meter()
        {
            InitializeComponent();

            CreateImages();

            Assembly assembly = Assembly.GetExecutingAssembly();
            labelVersion.Text = "V." + assembly.GetName().Version.ToString();
        }

        private void CreateImages()
        {
            if (pictureBoxCPU.Image == null)
                pictureBoxCPU.Image = new Bitmap(640, 64);

            if (pictureBoxMem.Image == null)
                pictureBoxMem.Image = new Bitmap(640, 64);

            if (pictureBoxDisk.Image == null)
                pictureBoxDisk.Image = new Bitmap(640, 64);
        }


        private void timer1_Tick(object sender, EventArgs e)
        {
            RedrawCPU();
            pictureBoxCPU.Refresh();
        }

        private void timer2_Tick(object sender, EventArgs e)
        {
            RedrawMem();
            pictureBoxMem.Refresh();
            RedrawDisk();
            pictureBoxDisk.Refresh();
            System.GC.Collect();
        }

        private CPUStatus cpuStat = new CPUStatus();

        private void RedrawCPU()
        {
            using (Graphics gimg = Graphics.FromImage(pictureBoxCPU.Image))
            {
                uint val = cpuStat.GetCPU();
                labelCPU.Text = val.ToString();

                for (int i = 0; i < pictureBoxCPU.Image.Width; i += 20)
                {
                    if (val > 100 * i / pictureBoxCPU.Image.Width)
                        gimg.DrawImage(imageList1.Images[0], i, 0);
                    else
                        gimg.DrawImage(imageList1.Images[1], i, 0);
                }
            }
        }

        private MemoryStatus mstat = new MemoryStatus();

        private void RedrawMem()
        {
            using (Graphics gimg = Graphics.FromImage(pictureBoxMem.Image))
            {
                mstat.Get();
                uint val = 100 - (uint)(100 * mstat.AvailPhysicalMB / mstat.TotalPhysicalMB);
                labelMem.Text = mstat.AvailPhysicalMB.ToString("n2") + " MB / " +
                    mstat.TotalPhysicalMB.ToString("n2") + " MB";

                for (int i = 0; i < pictureBoxMem.Image.Width; i += 20)
                {
                    if (val > 100 * i / pictureBoxMem.Image.Width)
                        gimg.DrawImage(imageList1.Images[0], i, 0);
                    else
                        gimg.DrawImage(imageList1.Images[1], i, 0);

                }
            }
        }

        private DiskStatus dstat = new DiskStatus();

        private void RedrawDisk()
        {
            using (Graphics gimg = Graphics.FromImage(pictureBoxDisk.Image))
            {
                dstat.Get("\\");
                uint val = 100 - (uint)(100 * dstat.FreeMB / dstat.TotalMB);
                labelDisk.Text = dstat.FreeMB.ToString("n2") + " MB / "
                    + dstat.TotalMB.ToString("n2") + " MB";

                for (int i = 0; i < pictureBoxDisk.Image.Width; i += 20)
                {
                    if (val > 100 * i / pictureBoxDisk.Image.Width)
                        gimg.DrawImage(imageList1.Images[0], i, 0);
                    else
                        gimg.DrawImage(imageList1.Images[1], i, 0);

                }
            }
        }

        private void menuItemTerminate_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        private void menuItemAbout_Click(object sender, EventArgs e)
        {
            FormAbout formAbout = new FormAbout();
            formAbout.ShowDialog();
        }

        private void FormWZero3Meter_Activated(object sender, EventArgs e)
        {
            timer1.Enabled = true;
            timer2.Enabled = true;
        }

        private void FormWZero3Meter_Deactivate(object sender, EventArgs e)
        {
            timer1.Enabled = false;
            timer2.Enabled = false;
        }

        private void FormWZero3Meter_Load(object sender, EventArgs e)
        {
            RedrawCPU();
            RedrawDisk();
            RedrawMem();
        }
    }
}

6.1 CPU の使用率、メモリ使用量、ディスク使用量の取得クラス

さて、CPUの使用率、メモリ使用量、ディスク使用量ですが、.NET Framework から取得できません。そこで、InterOpで取得する必要があります。そこで、それぞれのラッパークラスを作成することにします。

ソリューションエクスプローラーで、次の3つのC# クラスを追加します。

CPUの使用率取得クラス

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Diagnostics;

namespace Uchukamen.WZero3
{
    class CPUStatus
    {
        [DllImport("COREDLL.DLL")]
        private extern static uint GetTickCount();

        [DllImport("COREDLL.DLL")]
        private extern static uint GetIdleTime();

        private static uint dwStartTick = 0;
        private static uint dwIdleSt = 0;

        /// 
        /// CPU使用率
        /// 
        /// uint 0:最小-100:最大
        public uint GetCPU()
        {
            uint dwStopTick = GetTickCount();
            uint dwIdleEd = GetIdleTime();
            uint idle = ((100 * (dwIdleEd - dwIdleSt)) / (dwStopTick - dwStartTick));

            dwStartTick = dwStopTick;
            dwIdleSt = dwIdleEd;
            uint cpu = 100 - idle;
            Debug.WriteLine(cpu.ToString());
            return cpu;
        }
    }
}

メモリの使用量取得クラス

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace Uchukamen.WZero3
{
    public class MemoryStatus
    {
        private class MEMORYSTATUS
        {
            public uint dwLength = 0;
            public uint dwMemoryLoad = 0;
            public uint dwTotalPhys = 0;
            public uint dwAvailPhys = 0;
            public uint dwTotalPageFile = 0;
            public uint dwAvailPageFile = 0;
            public uint dwTotalVirtual = 0;
            public uint dwAvailVirtual = 0;
        }

        [DllImport("CoreDll.dll")]
        private static extern void GlobalMemoryStatus
        (
        MEMORYSTATUS lpBuffer
        );

        [DllImport("CoreDll.dll")]
        public static extern int GetSystemMemoryDivision
        (
        ref uint lpdwStorePages,
        ref uint lpdwRamPages,
        ref uint lpdwPageSize
        );

        private float totalPhys = 0;
        public float TotalPhysicalMB
        {
            get { return totalPhys; }
        }

        private float availPhys = 0;
        public float AvailPhysicalMB
        {
            get { return availPhys; }
        }

        private float totalVirtual = 0;
        public float TotalVirtualMB
        {
            get { return totalVirtual; }
        }

        private float availVirtual = 0;
        public float AvailVirtualMB
        {
            get { return availVirtual; }
        }

        public void Get()
        {
            uint storePages = 0;
            uint ramPages = 0;
            uint pageSize = 0;
            int res = GetSystemMemoryDivision(ref storePages, ref ramPages, ref pageSize);

            MEMORYSTATUS memStatus = new MEMORYSTATUS();
            GlobalMemoryStatus(memStatus);

            totalPhys = memStatus.dwTotalPhys / 1024F / 1024F;
            availPhys = memStatus.dwAvailPhys / 1024F / 1024F;
            totalVirtual = memStatus.dwTotalVirtual / 1024F / 1024F;
            availVirtual = memStatus.dwAvailVirtual / 1024F / 1024F;
        }
    }
}

ディスクの使用量取得クラス

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace Uchukamen.WZero3
{
    public class DiskStatus
    {
        [DllImport("coredll.dll")]
        public static extern bool GetDiskFreeSpaceEx
           (
           string directory,
           ref UInt64 lpFreeBytesAvailableToCaller,
           ref UInt64 lpTotalNumberOfBytes,
           ref UInt64 lpTotalNumberOfFreeBytes
           );

        #region プロパティ
        /// 
        /// The total number of free bytes on the disk
        /// that are available to the user associated with the calling thread. 
        /// 
        private UInt64 freeBytesAvailableToCaller = 0;
        public UInt64 FreeBytesAvailableToCaller
        {
            get { return freeBytesAvailableToCaller; }
        }

        public float FreeMB
        {
            get { return (float)freeBytesAvailableToCaller / 1024F / 1024F; }
        }

        /// 
        /// The total number of bytes on the disk
        /// that are available to the user associated with the calling thread. 
        /// 
        private UInt64 totalNumberOfBytes = 0;
        public UInt64 TotalNumberOfBytes
        {
            get { return totalNumberOfBytes; }
        }

        public float TotalMB
        {
            get { return (float)totalNumberOfBytes / 1024F / 1024F; }
        }


        /// 
        /// The total number of free bytes on the disk. 
        /// 
        private UInt64 totalNumberOfFreeBytes = 0;
        public UInt64 TotalNumberOfFreeBytes
        {
            get { return totalNumberOfFreeBytes; }
        }
        #endregion

        /// 
        /// ディスク容量を取得する。
        /// 
        /// ディスクのパス
        public void Get(string path)
        {
            bool res = GetDiskFreeSpaceEx(
                path,
                ref freeBytesAvailableToCaller,
                ref totalNumberOfBytes,
                ref totalNumberOfFreeBytes);
        }
    }
}

7. さいごに

少し手を加えれば、TaskManagerのような表示も簡単にできます。

Image

ただし、CPUやメモリの制約が強く、あまり凝ったことをすると、メモリやCPUを食いつぶしてしまいますので、見た目と機能のバランスや、性能を考えた設計が必要になります。