C#入門(データの表示)

10/9/2005 完成

10/12/2005 追記, 10/15/2005 追記分も完成

目標

データの列を作り、その中から選択したものを表示する。

オブジェクト分析

オブジェクトとして考えられるのは以下の 3 つである。

  1. データ
  2. 列(データ列)
  3. 表示されるもの

プログラムとデータは分離したいため、データ列オブジェクトをシリアライズ(ファイルに保存)する。 ファイルの形式は C# の機能である ADO.NET を使うと XML になるらしい。 従って、データの形式なども XML として扱っていく。 但し、この XML の構造自体はカプセル化する。 従って、以下のような流れが考えられる。

データの表示

   Data d = new Data("abc",123);
   Dout dout = new Dout();
   dout.show(d);
データの保存

   Data d1 = new Data("abc",123);
   Data d2 = new Data("def",456);
   DataList dl = new DataList();
   dl.load(@"c:\project\data.xml");
   dl.add(d1);
   dl.add(d2);
   dl.save(@"c:\project\newdata.xml");

XMLの検討

始めに C# による XML の機能が正常に動作するかを確かめるためのプログラ ムを作る。 最終的にシステム全体でも一つのデータ列しか使わないこととした。 ADO.NET の仕様を参考に Dout 以外を実装すると次のようになる。


using System;
namespace Example 
{
  public class Data 
  {
     private string name;
     private int value;
     public Data(string aName, int aValue)
     {
       name=aName;
       value=aValue;
     }
     public string getName()
     {
       return name;
     }
     public int getValue()
     {
       return value;
     }
  }
}

using System;
using System.Data;
namespace Example 
{
  public class DataList 
  {
    private DataSet ds;
    public DataList()
    {
      ds = new DataSet();
      ds.Tables.Add(new DataTable());
      ds.Tables[0].Columns.Add(
         new DataColumn("Name",Type.GetType("System.String")));
      ds.Tables[0].Columns.Add(
         new DataColumn("Value",Type.GetType("System.Int32")));
    }
    public void addData(Data aData)
    {
      DataRow row = ds.Tables[0].NewRow();
      row["Name"]=aData.getName();
      row["Value"]=aData.getValue();
      ds.Tables[0].Rows.Add(row);
    }
    public Data this[int n]
    {
      get
      {
        DataRow row = ds.Tables[0].Rows[n];
        return new Data((string) row["Name"], (int) row["Value"]);
      }
    }
    public void save(string filename)
    {
      ds.WriteXml(filename);
    }
    public void load(string filename)
    {
      ds.ReadXml(filename);
    }
  }
}

これらをテストするため、テスト用にコンソールアプリケーションを作る。 そのための Dout を用意してテストプログラムを走らせる。


using System;
namespace Example 
{
  public class TestDout 
  {
    public TestDout()
    {
    }
    public void show(Data aData)
    {
      Console.WriteLine("Name: {0}, Value: {1}",
                        aData.getName(),aData.getValue());
    }
  }
}

テストプログラム

以下の静的メソッド Main を一つずつ書き換え実行する。

  1. 
    static void Main(string[] args)
    {
      TestDout dout = new TestDout();
      Data d = new Data("abc",123);
      dout.show(d);
    }    
    
  2. 
    static void Main(string[] args)
    {
      TestDout dout = new TestDout();
      Data d1 = new Data("abc",123);
      Data d2 = new Data("def",456);
      DataList dl = new DataList();
      dl.addData(d1);
      dl.addData(d2);
      Data d3 = dl[1];
      dout.show(d3);
    }    
    
  3. 
    static void Main(string[] args)
    {
      TestDout dout = new TestDout();
      Data d1 = new Data("abc",123);
      Data d2 = new Data("def",456);
      DataList dl = new DataList();
      dl.addData(d1);
      dl.addData(d2);
      dl.save(@"c:\test.xml");
    }    
    
  4. 
    static void Main(string[] args)
    {
      TestDout dout = new TestDout();
      DataList dl = new DataList();
      dl.load(@"c:\test.xml");
      Data d = dl[1];
      dout.show(d);
    }    
    

XML の検討(2)

ADO.NET で用いた XML は行と列を持つ二次元的なデータ構造しか扱えなかっ た。 ここでは次のようなもっと複雑なデータを扱うことを考える。

店のリスト
店の情報
名前
店の名前1
画像ファイル名
ファイル名1
メニュー
品目
料理名1-1
価格
料理の価格1-1
品目
料理名1-2
価格
料理の価格1-2

これをすべて要素により記述すると以下のようになる。


<? xml version="1.0" encoding="Shift_JIS"?>
<shoplist>
  <shop>
    <info>
      <name>店の名前1</name>
      <imagefile>ファイル名1</imagefile>
    </info>
    <menu>
      <item>料理名1-1</item>
      <price>料理の価格1-1</price>
      <item>料理名1-2 </item>
      <price>料理の価格1-2</price>
    </menu>
  </shop>
  <shop>
    <info>
...

一方、各 XML の要素は属性値を取ることができる。 子要素で情報を記述するのと属性値に記述する場合の長所短所は以下の通りで ある。

子要素
子要素の記述は自由である。従って、同じ子要素は複数存在でき、また子要素 はマークアップ可能である。 但し、複雑なデータ構造が可能ということはプログラムでの処理が複雑になる。
属性値
属性値は重複が許されてなく、またデータ形式をある程度指定できる。 特に特定の項目を選ぶような時は、それ以外を許さないように定義可能である。 属性値はプログラムで扱い易い。

以上の検討により属性値を考慮して XML 化すると以下のようになる。


<? xml version="1.0" encoding="Shift_JIS"?>
<shoplist>
  <shop name="店の名前1" image="ファイル名1" >
    <menu item="料理名1-1" price="料理の価格1-1"/>
    <menu item="料理名1-2" price="料理の価格1-2"/>
  </shop>
  <shop name="name2" image="filename2">
    <menu item="item2-1" price="price2-1"/>
  ...

この方がシンプルでプログラムの作成も楽であると思われるので、以後ではこ のフォーマットに対するプログラムを作成する。

XML の検討(3)

前章で示した XML を DOM(Document Object Model) を使用して解析する。 DOM とは XML の構造を解析し、あたかも配列を含む構造体をアクセスするよう に XML の要素にアクセスできるようにするものである。

XMLファイルの作成

始めに上記の XML を生成するプログラムを C# のコンソールアプリケーショ ンで作成する。


using System;
using System.Xml;
namespace Example
{
  class Ex1
  {
    static void Main(String[] args)
    {
      XmlDocument list = new XmlDocument();
      XmlElement shoplist, shop, menu1, menu2;
      shoplist = list.CreateElement("shoplist");
      list.AppendChild(shoplist);
// 1件目
      shop = list.CreateElement("shop");
      shop.SetAttribute("name","mise1");
      shop.SetAttribute("image",@"c:\work\image1.bmp");
      menu1 = list.CreateElement("menu");
      menu1.SetAttribute("item","幕の内");
      menu1.SetAttribute("price","500");
      menu2 = list.CreateElement("menu");
      menu2.SetAttribute("item","のり弁");
      menu2.SetAttribute("price","380");
      shop.AppendChild(menu1);
      shop.AppendChild(menu2);
      shoplist.AppendChild(shop);
// 2件目
      shop = list.CreateElement("shop");
      shop.SetAttribute("name","mise2");
      shop.SetAttribute("image",@"c:\work\image2.bmp");
      menu1 = list.CreateElement("menu");
      menu1.SetAttribute("item","幕の内");
      menu1.SetAttribute("price","480");
      menu2 = list.CreateElement("menu");
      menu2.SetAttribute("item","しゃけ弁");
      menu2.SetAttribute("price","420");
      shop.AppendChild(menu1);
      shop.AppendChild(menu2);
      shoplist.AppendChild(shop);

      list.Save(Console.Out);
      list.Save(@"c:\project\shoplist.xml");
    }
  }
}

この結果次のような XML ファイルができた。


<?xml version="1.0" encoding="shift_jis"?>
<shoplist>
  <shop name="mise1" image="c:\work\image1.bmp">
    <menu item="幕の内" price="500" />
    <menu item="のり弁" price="380" />
  </shop>
  <shop name="mise2" image="c:\work\image2.bmp">
    <menu item="幕の内" price="480" />
    <menu item="しゃけ弁" price="420" />
  </shop>
</shoplist>

XML フォーマットに対応したclass の作成

この仕様に基づいてデータのやりとりを行うこととする。 始めに Data クラスと同様の Shop クラスの定義をする。


using System;
using System.Collections;
using System.Collections.Specialized;
namespace Example 
{
  public class Shop 
  {
     private string name;
     private string filename;
     private StringDictionary menu;
     public Shop(string aName, string aFilename)
     {
       name=aName;
       filename=aFilename;
       menu=new StringDictionary();
     }
     public void addMenu(string item, string price)
     {
        menu.Add(item,price);
     }
     public string getName()
     {
       return name;
     }
     public string getFilename()
     {
       return filename;
     }
     public StringDictionary getMenu()
     {
       return menu;
     }
     public void show()
     {
       Console.WriteLine("Name {0}, Filename: {1}",name,filename);
       foreach(string key in menu.Keys)
       {
         Console.WriteLine("  {0}: {1}円",key,menu[key]);
       }
     }
  }
}

テストプログラム


    static void Main(String[] args)
    {
       Shop s = new Shop("ほか弁",@"c:\project\hokaben.bmp");
       s.addMenu("幕の内","500");
       s.addMenu("のり弁","350");
       s.show();
    }

XMLへのデータの保存

次にこのインスタンスを貯え、XML ファイルとやりとりする ShopList クラス を作る。 貯えるのは Shop.getName() の値をキーにした HashTable とし、 save メソッ ドで XML に変換して保存する。 とりあえずここまでを実装してテストプログラムを動かす。


using System;
using System.Xml;
using System.Collections;
using System.Collections.Specialized;
namespace Example {
  public class ShopList {
     private Hashitable list;
     public ShopList()
     {
       list= new Hashtable();
     }
     public void add(Shop aShop)
     {
       list.Add(aShop.getName(),aShop);
     }
     public Shop this[string key]
     {
       get
       {
         return (Shop) list[key];
       }
     }
     public void save(string filename)
     {
       XmlDocument sl = new XmlDocument();
       XmlElement shoplist = sl.CreateElement("shoplist");
       sl.AppendChild(shoplist);
       foreach(object key in list.Keys)
       {
         Shop aShop = (Shop) list[key];
	 XmlElement xshop = sl.CreateElement("shop");
	 xshop.SetAttribute("name",aShop.getName());
	 xshop.SetAttribute("image",aShop.getFilename());
	 StringDictionary menu = aShop.getMenu();
	 foreach(string item in menu.Keys)
	 {
	   XmlElement xmenu = sl.CreateElement("menu");
	   xmenu.SetAttribute("item",item);
	   xmenu.SetAttribute("price",menu[item]);
	   xshop.AppendChild(xmenu);
	 }
	 shoplist.AppendChild(shop);
       }
       sl.Save(filename);
     }
   }
}

テストプログラム


    static void Main(string[] args)
    {
      Shoplist shoplist = new ShopList();
      Shop s = new Shop("ほか弁",@"c:\project\hoka.bmp");
      s.addMenu("幕の内","500");
      s.addMenu("のり弁","350");
      shoplist.add(s);
      s = new Shop("学食",@"c:\project\gakushoku.bmp");
      s.addMenu("スタミナ","450");
      s.addMenu("しゃけ弁","420");
      shoplist.add(s);
      shoplist["ほか弁"].show();
      shoplist.save(@"c:\project\shoplist.xml");
    }

XML からのデータの取り出し

さらにこれに XML ファイルの読み込みを実装する。 また、ロードしてできた ShopList をすべて表示するため列挙子 ShopEnumerator クラスも作成する。 始めは列挙子のクラス ShopEnumerator である。


using System.Collections;

...

    class ShopEnumerator : IEnumerator
    {
      private Hashtable list;
      private IEnumerator ienum;
      public ShopEnumeratora(Hashtable ht)
      {
        list=ht;
	Reset();
      }
      public object Current
      {
        get
	{
	  DictionaryEntry de = (DictionaryEntry) ienum.Current;	
	  return de.Value;
	}
      }
      public void Reset()
      {
        ienum = list.GetEnumerator();
      }
      public bool MoveNext()
      {
        return ienum.MoveNext();
      }
    }

ShopList のクラス宣言を class ShopList : IEnumerableに変 更し、 以下を ShopList クラスに追加する。 なお、この load メソッドはエラーチェックを行ってないため、誤ったファイ ルを入力した場合誤動作する可能性がある。


    public IEnumerator GetEnumerator()
    {
      return new ShopEnumerator(list);
    }
    public void load(string filename)
    {
      XmlDocument xdoc = new XmlDocument();
      xdoc.Load(filename);
      XmlNode root = xdoc.DocumentElement;
      IEnumerator ienum = root.GetEnumerator();
      XmlNode xshop;
      while(ienum.MoveNext())
      {
        xshop = (XmlNode) ienum.Current;
	XmlAttributeCollection info = xshop.Attributes;
	Shop s = new Shop(info["name"].Value,info["image"].Value);
	IEnumerator items = xshop.GetEnumerator();
	XmlNode xitem;
	while(items.MoveNext())
	{
	  xitem = (XmlNode) items.Current;
	  XmlAttributeCollection menu = xitem.Attributes;
	  x.addMenu(menu["item"].Value,menu["price"].Value);
	}
	add(s);
      }
    }

テストプログラム


using System;
using SystemCollections;

...

    static void Main(string[] args)
    {
      ShopList shoplist = new ShopList();
      shoplist.load(@"c:\project\shoplist.xml");
      IEnumerator ienum = shoplist.GetEnumerator();
      Shop s;
      while(ienum.MoveNext())
      {
        s = (Shop) ienum.Current;
	s.show();
      }
    }

フォームへのデータ表示

ここでは Data クラスのインスタンスを Windows の フォームに表示すること を考える。

ボタンとサブフォーム

ここでは制御ウィンドウのボタンにより別のウィンドウが開くようなものを作 る。

始めに Form を二つプロジェクトに加える。 そして、Form1.cs の方のクラス定義に private Form2 dataForm; を加え、 コンストラクタ public Form1()の最後に dataForm = new Form2(); を加えオブジェクトを作る。

次に Form1 のデザイン画面でボタンを 1 つ加える。 ボタンを選択し、プロパティ画面の Text 欄に「Test」などの文字を加える。 さらにボタンをダブルクリックしてプログラムの画面を開く。


private void button1_Click(object sender, System.EventArgs e)
{

}

ここに dataForm.Show(); を入れる。

これだけで動かしてみると、ボタンを押すと Form2 が開くことがわかる。 また、 Form2 は閉じてもボタンを押せばまた開き、Form1 を閉じると Form2 も一緒に閉じることがわかる。

データの出力

次に前章の Data クラスと DataList クラスをこの Windows アプリケーショ ンに移動してくる。

Form1.cs 中の変数宣言に static private DataList dl; を加え、static void Main() を次のようにする。


static void Main()
{
   dl.load(@"c:\project\data.xml");
   Application.Run(new Form1());
}

さて、ここでは 0 番目の内容と、 1 番目の内容を切替えて表示することとす る。 まず、 Form2 に label を二つ追加する。 次に Form2 に次のメソッドを追加する。


public void setData(Data aData)
{
  label1.Text = aData.getName();
  label2.Text = (aData.getValue()).ToString();
}

最後に Form1 にボタン一つを追加する。 もともとあった Test と名付けられたボタンの Text を 0に, もう一つの Text を 1 にする。 そして、それぞれをダブルクリックして Form1.cs に対応するメソッドを追加 する。 そのメソッドの中に以下のように動作を記述する。


private void button1_Click(object sender, System.EventArgs e)
{
    dataForm.setData(dl[0]);
    dataForm.Show();
}
private void button2_Click(object sender, System.EventArgs e)
{
    dataForm.setData(dl[1]);
    dataForm.Show();
}

以上により制御用のフォーム Form1 でボタン 0 と 1 を押すことで Form2 上 の表示が切り替わるプログラムを作ることができた。

Singleton デザインパターン

前章では Form1 のコンストラクタで Form2 のコンストラクタを呼ぶというこ とで、Form1 一枚に対して Form2 も一枚のみとした。 しかし、Form2 を一度消してしまうと、次に Form1 のボタンを押すと Show() メソッドが呼べずにエラーになってしまった。

そこで Singleton デザインパターンを使い、常に Show() メッセージが一つ であることを保証する。 アイディアは Form2 のインスタンスはコンストラクタを使わずに静的メソッ ド getInstance を使用すると言うことである。 この静的メソッドは Form2 インスタンスを参照する静的変数を持っている。 その変数は getInstance が呼ばれる度に Form2 インスタンスを参照するよう に処理される。

実装はまず、Form2 のクラス定義の中に static Form2 aInstance = null; を入れる。 そして静的メソッドとして次を定義する。


public static Form2 getInstance()
{
  if((instance == null) || (instance.IsDispose))
  {
    instance = new Form2();
  }
  return instance;
}

次に Form1.cs を修正する。 コンストラクタ内にある dataForm = new Form2(); を取り除く。 そして、 Form2 に対する処理(つまり dataForm の参照)をする前に、必ず dataForm = Form2.getInstance(); を行うようにする。 具体的には次の二つのメソッドの先頭に dataForm = Form2.getInstance(); を入れる。

これで Form2 を一旦消しても、Form1 のボタンを押せば再び Form2 が復活す るようになった。

画像の表示方法

Form にファイル名を指定して画像を表示させる方法を示す。

準備

新しい Windows アプリケーションプロジェクトを作る。 Windows Form Form2.cs を追加し、Singleton デザインパターンを実装する。 具体的には以下のようにコードを追加する。

  1. Form2 クラスに静的変数宣言 private static Form2 instance = null; を追加する。
  2. Form2 クラスに静的メソッド getInstance() を追加する。
    
        public static Form2 getInstance()
        {
          if((instance == null)||(instance.IsDisposed))
          {
            instance = new Form2();
          }
          return instance;
        }
    
  3. Form1 クラスにインスタンス変数宣言 private Form2 form2 =null; を追加する。
  4. 以後、Form1 で Form2 をコントロールする際は必ず冒頭で form2 = Form2.getInstance();を実行する。

設定

ファイル c:\image.bmp をあらかじめ作っておき、これを Form2 で表示する ことを考える。 そのため、Form2 のデザインにおいて pictureBox1 を追加しておく。

Form2 でファイル名により画像を表示させるためのメソッド setImage を追加 する。


    public void setImage(string filename)
    {
        picutureBox1.Image = Image.FromFile(filename);
    }

Form1 に Test という Text を持つボタン button1 を追加する。そのボタン をダブルクリックしてメソッド button1_Click を追加する。


private void button1_Click(object sender, System.EventArgs e)
{
    form2 = Form2.getInstance();
    form2.setImage(@"c:\image.bmp");
    form2.Show();
}

名前による画像の選択

まず表示する画像を c:\image1.bmp, c:\image2.bmp, c:\image3.bmp とし、 それらの名前を gazou1, gazou2, gazou3 とする。 これらをハッシュテーブルに格納することにする。 静的変数としてprivate static Hashtable ht; を追加する。 これを以下のように Main 静的関数で初期化する。


    static void Main()
    {
      ht = new Hashtable();
      ht.add("gazou1",@"c:\image1.bmp");
      ht.add("gazou2",@"c:\image2.bmp");
      ht.add("gazou3",@"c:\image3.bmp");
      Application.Run(new Form1());
    }

つぎに Form1 のデザインにリストボックス listBox1 を追加する。そしてそ れをダブルクリックし、次のコントロールを付け加える。


    private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e)
    {
      form2 = Form2.getInstance();
      string index = (string) listBox1.SelectedItem;
      form2.setImage((string)ht[index]);
      form2.Show();
    }

また Form1 のコンストラクタ Form1 で listBox1 に項目を登録する。 なお、Hashtable の Keys アクセッサは要素を整列させないので、整列させた い時はリストボックスに登録する前に整列させる必要がある。


    public Form1()
    {
      InitializeComponent();
      foreach(string key in ht.Keys)
      {
        listBox1.Items.Add(key);
      }
    }

以上で Form1 のリストボックスの項目を選択することで、 Form2 に対応する 画像を表示するアプリケーションが作成できた。

動的なフォームのコントロール

複数のボタンの生成と制御

ここではプログラムにより Form 上にボタンを生成してコントロールすること を考える。 そのため Windows アプリケーションとして Form を一枚だけ用意する。 ボタンの生成は以下の手順による。

  1. コンストラクタによりインスタンス生成
  2. Form のコントロールに Controls.Add() により追加

さらに位置を制御するため Button の Left, Top アクセッサにアクセスし、 また、ボタンに書くテキスト Text アクセッサも使用する。

以下に 5 個のボタンを生成するための手続きを示す。 これを Form1.cs のコンストラクタ public Form1() の後半に付け加える。 なお、ボタンの大きさをテキストの大きさになるように調整している。


Graphics g = CreateGraphics();
for(int i=0; i<5 ; i++)
{
  Button b = new Button();
  b.Left = i*50;
  b.Top = i*50;
  b.Text = i.ToString();
  b.ClientSize = g.MesureString(b.Text,b.Font).ToSize()
                 + new Size(10,5);
  Controls.Add(b);
}

次に押したボタンを赤くすることを考える。 そのメソッドを OnButtonClicked と名付けることにする。 各ボタンが押された時、Click イベントが発生するが、その時に呼ぶように上 記のプログラムの Controls.Add(b) の後ろに以下を追加する。


  b.Click += new EventHandler(OnButtonClicked);

さて、OnButtonClicked の実装を考える。以下のような手続きとする。

  1. 異なるボタンが押された時以下の処理をする。
  2. 初回でない場合以前に押されたボタンの色を元に戻す。
  3. 今回押されたボタンを赤にする。

これを実現するため、インスタンス変数として以前に押されたボタンへの参照 を保存することとする。 そして、押されたボタンの参照を保存することとする。 これを実現したのが以下のプログラムである。これを class Form1 内に追加 する。


  private Button lastButton = null;
  private void OnButtonClicked(object sender, EventArgs e)
  {
    Button newButton = (Button) sender;
    if(lastButton != newButton)
    {
      if(lastButton != null)
      {
        lastButton.BackColor = Button.DefaultBackColor;
      }
      newButton.BackColor = Color.Red;
      lastButton = newButton;
    }
  }

ラベルと位置を持つクラスとボタン

まず名前と相対位置を持つクラスを作り、名前をラベルに持ち相対位置により 表示位置が指定されるボタンを作る。


class Sample 
{
  private string name;
  private double rel_x;
  private double rel_y;
  public Sample(string _name, float _x, float _y)
  {
     name = _name ;
     rel_x = _x;
     rel_y = _y;
  }
  public double x
  {
    get
    {
      return rel_x;
    }
  }
  public double y
  {
    get
    {
      return rel_y;
    }
  }
}

次に Form1 クラス内に Hashtable samples を定義して、複数の Sample オブ ジェクトを入れる。


  private static Hashtable samples;
  static void Main ()
  {
    samples = new Hashtable();
    samples.Add("abc", new Sample("abc",0.1,0.1));
    samples.Add("def", new Sample("def",0.5,0.8));
    samples.Add("ghi", new Sample("ghi",0.3,0.6));
    samples.Add("jkl", new Sample("jkl",0.7,0.4));
    samples.Add("mno", new Sample("mno",0.2,0.5));
    Application.Run(new Form1());
  }

そしてコンストラクタでボタンを追加する。 以下を public Form1() の後半に入れる。 (前節のコードを変更する場合は//変更点とかかれている部分だけ挿入 する。


  Graphics g = CreateGraphics();
  foreach(string key in sampels.Keys) //変更点
  {
    Button b = new Button();
    b.Left = (int)(Width * ((Sample) samples[key]).x); //変更点
    b.Top = (int)(Height * ((Sample) samples[key]).y); //変更点
    b.Text = key;                                      //変更点
    b.ClientSize = g.MeasureString(b.Text,b.Font).ToSize()
                   + new Size(10,5);
    Controls.Add(b);
  }

リストボックスとの連係

この節ではさらに ListBox を持つ Form を追加して、 ボタンを押すと ListBox も変化し、 ListBox を選択すると該当するボタンの 色が変わるようにする。

新たに付け加える Form を Form2 とする。 Form2 上の ListBox を変化させるのに、選択された名前を引数として持つメ ソッド selectByName が定義されているとする。 Form2 のインスタンスは変数 form2 が参照するとして、 Singleton デザイン パターンを使い getInstance クラス変数によりインスタンスを獲得する。 但し、これと同様の議論を Form2 上の ListBox の選択にも考える。 Form2 上で Form1 上のボタンをコントロールするには Form2 は Form1 のイ ンスタンスを参照し、同様に選択した値から selectByName が Form1 に定義 されてなければならない。

話をまとめると Form1 にも selectByName でボタンの色を変更できるメソッ ドが必要である。 また Form2 は Form1 のインスタンスを参照でき、selectByName メソッドで 選択が変更できなければならない。 そこで、Form2 のインスタンスを生成する時は Form1 の参照を与えることと する。つまり、 getInstance 静的メソッドは Form1 の参照を引数としてとる ということである。

Form1 の selectByName

Form1 では既にボタンを押すとボタンの色を変えるメソッド OnButtonClicked がある。これをこのまま流用することを考える。 つまり文字列が与えられたら該当する Button オブジェクトを選び OnButtonClicked を呼べば良い。 そのために文字列を鍵とする Button オブジェクトの Hashtable buttonList が定義されているとすると selectByName は次のようになる (EventArgs は使われてないので null を渡せば十分)。


private Hashtable buttonList; //登録は後述
public void selectByName(string index)
{
  OnButtonClicked(buttonList[index],null);
}

buttonList は文字列をキーにして Button のオブジェクトの参照を登録する。 これは Form1 のコンストラクタ中の foreach の手前に buttonList = new Hashtable();を挿入し、foreach ブロックの 最後に以下を挿入すれば良い。


    buttonList.Add(key,b);

一方、OnButtonClicked の中では form2 の selectByName を呼び出す。 そのため OnButtonClicked 中の色の変更 newButton.BackColor = Color.Red; の直後に以下を挿入する。


    form2 = Form2.getInstance(this);
    form2.selectByName(newButton.Text);
    form2.Show();

なお Form1 のコンストラクタの最後にも同様に Form2 を生成するコードを付 け加える。


    form2 = Form2.getInstance(this);
    form2.Show();

Form2 の設計

Form2 は Singleton デザインパターンを組み込み、 ListBox をもつ。 Form2 のコンストラクタが動作する際に ListBox を初期化するため、 Form2 のコンストラクタの引数に持たせるか、あらかじめデータを持っておく必要が ある。 また Singleton デザインパターンを使用するため、コンストラクタを使用す るのは唯一 getInstance 静的メソッドだけである。 getInstance の引数は Form1 のインスタンスのみと定めたため、 getInstance から呼び出される Form2 のコンストラクタが必要とするデータ は別に与える必要がある。 そのため、 setContents 静的メソッドを用意してデータをあらかじめ渡して もらう。 Form1 の Main 関数の最後は次のようになる。


static void Main()
{
  samples = new Hashtable();
  samples.Add(...);
  ...
  Form2.setContents(samples);
  Application.Run(new Form1());
}

さて、Form2 の selectByName だが文字列を引数にして ListBox を選択しな ければならない。 ListBox には SetSelected メソッドが用意されているが、これは何行 目という整数のインデックスと On か Off を示す論理値が引数になっている。 そのため、文字列から行数が取り出せるよう文字列をキーとした Hashtable contents を用意すると以下のようになる。


  private Hashtable contents;
  public void selectByName(string index)
  {
    listBox1.SetSelected((int)contents[index],true);
  }

静的メソッド setContents ではこの contents を作成すれば良い。 したがって次のようになる。


  public static void setContents(Hashtable ht)
  {
    int i=0;
    contents = new Hashtable();
    foreach(string key in ht.Keys)
    {
      contents.Add(key,i++);
    }
  }

この contents を使用して Form2 のコンストラクタで ListBox1 を初期化す る。


  public Form2()
  {
    InitializeComponent();
    foreach(string key in contents.Keys)
    {
      listBox1.Items.Add(key);
    }
  }

結局 getInstance では Form2 のコンストラクタを呼ぶだけでうまくいきそう である。


  private static Form2 instance = null;
  private Form1 form1;
  public static Form2 getInstance(Form1 _form1)
  {
    if((instance==null)||(instance.IsDisposed))
    {
      instance = new Form2();
    }
    instance.form1 = _form1;
    return instance;
  }

そして残るは ListBox を選択した時の動作である。 デザインの画面で ListBox をダブルクリックして listBox1_SelectedIndexChanged メソッドを追加する。そして Form1 の selectByName を呼び出す。


  private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
  {
    form1.selectByName((string)listBox1.SelectedItem);
  }

連鎖の切断

以上で必要なものは揃ったが、これでは正常に動作しない。 というのは、ボタンが押されると、 ListBox が選択されるが、ListBox が選 択されるとボタンが押されてしまうのである。 つまり、一度アクションが起こると Form1 と Form2 の selectByName を相互 に呼び出すことが繰返し起きてしまう。

これを避けるには、それぞれの selectByName が呼ばれた時は相手方の selectByName を呼び出さないようにする必要がある。 そのため論理変数 reflection を定義する。 これは通常 true であり、 OnButtonClicked と listBox1_SelectedIndexChanged では true の時に selectByName を呼び出す。 但し、 selectByName では reflection を false にしてから処理を行う。 このようにすると、 selectByName の作用で相手方の selectByName が呼び出 されることはない。 まず Form1 の selectByName と OnButtonClicked をこのように修正したもの を示す。


private bool reflection = true;
private void OnButtonClicked(object sender, EventArgs e)
{
  Button newButton  = (Button) sender;
  if(lastButton != newButton)
  {
    if(lastButton != null)
    {
      lastButton.BackColor = Button.DefaultBackColor;
    }
    newButton.BackColor = Color.Red;
    if(reflection)
    {
      form2 = Form2.getInstance(this);
      form2.selectByName(newButton.Text);
      form2.Show();
    }
    lastButton = newButton;
  }
  reflection = true;
}
public void selectByName(string index)
{
  reflection = false;
  OnButtonClicked(buttonList[index],null);
}

一方 Form2 の selectByName と listBox1_SelectedIndexChanged を修正した ものは次の通りである。


private bool reflection = true;
private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
{
  if(reflection)
  {
    form1.selectByName((string) listBox1.SelectedItem);
  }
  reflection = true;
}
public void selectedByName(string index)
{
  reflection = false;
  listBox1.SetSelected((int)contents[list], true);
}

参考文献

  1. Mickey Williams著、(有)トップスタジオ訳「プログラミング Microsoft Visual C#.NET」日経BPソフトプレス(2002)
  2. Didier Martin, Mark Birbeck, Michael Kay, Brian Loesgen, Jon Pinnock, Steven Livingstone, Peter Stark, Kevin Willams, Richard Anderson, Stephen Mohr, David Baliles, Bruce Peat, Nikola Ozu 著、石川 直太監修 / 鷺谷好輝訳「プロフェッショナルXML」インプレス(2001)
  3. 日立ソフトウェアエンジニアリング(株)インターネットビジネス部 著「Java デザインパターン徹底攻略」技術評論社(2002)
  4. 緑のバイク 「初めてのC#」 http://homepage3.nifty.com/midori_no_bike/CS/form.html

坂本直志 <[email protected]>
東京電機大学工学部情報通信工学科