気楽なソフト工房

プログラミングについていろいろな記事を書いています。



mykonos2008

Author:mykonos2008
システムエンジニアとして働いている30代の会社員です。
仕事や趣味でプログラムを書いている方の役に立つ記事を書いていきたいと思っています。
ご意見、ご感想はこちらまで
If you are an english speaker,Please visit my english blog.

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
私は「Visual Web Developer 2008」を使用して「Silverlight」の開発を行っているのですが、
1つ非常に不便に感じることがあります。

それは、作成したUserControlの親を変更したり、UserControlにインタフェースを実装させたりすることが
できない点です。

Visual StudioでUserControlを追加すると、「xxxx.xaml」ファイルと「xxxx.xaml.cs」ファイルが自動生成されます。
そして、実はこの他に「obj」ディレクトリの下に「****.g.cs」というファイルが作成されます。
(ソリューションエクスプローラーの上部のボタン「すべてのファイルを表示」をクリックすると
ソリューションエクスプローラーに表示されるようになります。)

「****.g.cs」は、UserControlの部分クラス定義ファイルです。自動生成された「xxxx.xaml.cs」ファイルのコンストラクタを見ると、
「InitializeComponent()」メソッドがコールされていますよね。これは「****.g.cs」の中に定義されているメソッドで
XAMLファイルを読み込んで、ツリー要素に該当するコンポーネントを作成する処理を行っています。

コード例)

[Page.xaml.cs]
  public Page()
  {
      InitializeComponent();
  }

[Page.g.cs]
  internal System.Windows.Controls.Grid LayoutRoot = null;
  private bool _contentLoaded;

  public void InitializeComponent() {
      if (_contentLoaded) {
          return;
      }

      _contentLoaded = true;
      //解説:LoadComponent()はXAML内で定義されているツリー要素のインスタンスを生成して、UserControlに追加します
      System.Windows.Application.LoadComponent(this, new System.Uri("/GuestBook;component/Page.xaml", System.UriKind.Relative));

      this.LayoutRoot = ((System.Windows.Controls.Grid)(this.FindName("LayoutRoot")));
  }

「x:Name」属性が指定された要素がコードの中から利用できるように、インスタンス変数として自動的に定義され、
FindName()メソッドにより該当するインスタンスが代入されています。

「****.g.cs」のおかげで、面倒な処理の記述をする必要が無くなっている反面一点問題があります。
例えば、UserControlを継承して作成したクラス「BasicControl」を拡張して、新しいUserControlを作成しようとした場合、
クラス宣言をする際、「public partial class ChildControl : BasicControl」のように記述します。

しかし、これが対応する自動生成クラス「xxxx.g.cs」の中では依然、UserControlを継承しているため、
ビルド時にコンパイルエラーが発生してしまいます。

「xxxx.g.cs」を直接編集して、その場でコンパイルを通しても、XAMLを編集すると再作成されて、元に戻って
しまうので、根本的な解決にはなりません。

そこで、部分クラスに記述されている処理を「xxxx.xaml.cs」に移し、部分クラスが自動生成されないようにしてみました。

まず、XAMLファイルのプロパティウィンドウで「カスタムツール」の値を空白に、「ビルドアクション」を「Resource」に変更します。
こうすることで、「****.g.cs」が自動生成されなくなります。既に作成されたファイルは削除します。



そして「xxxx.xaml.cs」ファイル内にInitializeComponent()を定義し、以下の処理を記述しました。

[Page.xaml.cs」

  private System.Windows.Controls.Grid LayoutRoot = null;

  private void InitializeComponent()
  {
      System.Windows.Application.LoadComponent(this, new System.Uri("/Kronos;component/Page.xaml", System.UriKind.Relative));
      this.LayoutRoot = ((System.Windows.Controls.Grid)(this.FindName("LayoutRoot")));
  }

クラス宣言から「partial」を消すこともお忘れなく。
そして、継承先クラスを「BasicControl」に変更し完了です。
もちろんコンパイルもとおりますし、XAMLファイルをデザイナで編集することも可能です。
ただ一点、「x:Name」属性を持つ要素をXAMLファイルに追加した場合は、
それに該当するインスタンス変数を宣言し、FindName()を使ってインスタンスを設定する必要がありますので
ご注意ください。

たくさんの画面を持つ業務システムを作成する場合、「ポリモーフィズム」を活用するため、
抽象クラスやインターフェースを使用するケースが考えられます。

そんな場合、この方法がお役に立つかと思います。

一方で、Visual Studioには、この点を考慮して欲しいです。

スポンサーサイト
Silverlightのアプリケーションを開発していると、デザイン時にWidthやHeightを指定しなかったコントロールの
描画時のサイズが必要になるケースがよくあると思います。

例えば、ウィンドウの「閉じる」ボタンのようにCanvasの右上のコーナーにボタンを配置する場合、
Canvasの幅を取得して、ボタンの左端の位置(Canvas.Left)を指定する必要があります。

このようなケースに使用できるプロパティとして「ActualWidth」と「ActualHeight」があります。
「ActualWidth」と「ActualHeight」にはレイアウトシステムによって算出された結果の値が
格納されます。

しかし、これがちょっと曲者なのです。コンストラクタやLoadedイベントのハンドラなどで、値を取得しようと
すると、「0」が返されてしまいます。(Loadedイベントのハンドラの場合、何度かやってるとたまに取得できたり
しますが。)

「ActualWidth」、「ActualHeight」が「0」を返す理由

この現象の原因は、レイアウトシステムの処理が非同期で行われているためなのです。
例として以下のコードを見てください。

  public Page()
  {
      InitializeComponent();

      Canvas.SetTop(imageButton, 5);
      Canvas.SetLeft(imageButton, LayoutRoot.ActualWidth - 25);
  }

上記はUserControlクラスを継承するクラスのコードサンプルです。

InitializeComponent()はVisual Studioが自動的に挿入するメソッドですが、その内部では
XAMLの内容を解析して、XAMLに定義されているコンポーネントを生成するという処理を行っています。

そして、サンプルではXAMLに定義されている「imageButton」のCanvas上の位置を指定する処理を
続けて行っています。

しかし、残念ながらこのコードは上手くいきません。コンストラクタからLoadedイベントのハンドラに
この処理を移しても結果は同じです。

上手くいかない理由は、InitialzeComponent()の処理が終了した時点では、レイアウトシステムがまだ処理を行っていないからです。
InitialzeComponent()を抜けた時点で、レイアウト処理はコントロールに関連づけらているスレッドの待ち行列に、
処理されるべき処理として登録されていると考えられますが、この時点では、まだコンストラクタのコードブロック内に
いるため、レイアウト処理は未実行のままなのです。

「ActualWidth」、「ActualHeight」を確実に取る方法

この問題に対処するためには、先ほどのサンプルを以下のようなコードに変更します。

  public Page()
  {
      InitializeComponent();

      Dispatcher.BeginInvoke(
         () =>
         {
            Canvas.SetTop(imageButton, 5);
            Canvas.SetLeft(imageButton, LayoutRoot.ActualWidth - 25);
         }
      );

  }

「ActualWidth」を取得する処理を非同期処理に変更しています。Dispatcherはコントロールに関連付けられた
スレッドが処理するべき処理の待ち行列を管理するクラスで、BeginInvoke()は、待ち行列に処理を登録する
ためのメソッドです。

レイアウトシステムによる計算処理は、InitializeComponent()のいずれかの場所で、Dispatcherに登録されていると
考えられます。ActualWidthの取得の処理はその後にDispatcherに登録しているための、レイアウト処理よりも
待ち行列内の後ろの位置に登録されます。

したがって、ActualWidthを取得するタイミングでは、レイアウトシステムによる処理が完了していることが
保証されるのです。

レイアウトシステムによる処理は、Childrenに子要素が追加された場合や、Width、Hegihtなどの値が変更された場合などに起動されますが、
上記のコードを応用することでいずれのケースにおいても「ActualWidth」や「ActualHeight」を確実に取得することが出来ます。

Dispatcherの処理順に関する補足

SilverlightのDispatcher.BeginInvoke()には優先順位を指定する引数が有りません(WPF版にあります。)ので、単純にキューへの登録順に
処理が実行されると考えられます。実際、それを確認するために以下のようなコードを書き、検証して
みましたが、順番が変わることはありませんでした。

  Dispatcher.BeginInvoke(
      () =>
      {
          Debug.WriteLine("1");
      }
  );

  Dispatcher.BeginInvoke(
      () =>
      {
          Debug.WriteLine("2");
      }
  );

  Dispatcher.BeginInvoke(
      () =>
      {
          Debug.WriteLine("3");
      }
  );

何度か実行してみましたが、1,2,3の順番で表示されました。
当サイトでは、第三者配信(Google Adsense)による広告サービスを利用しています。
このような広告配信事業者は、ユーザーの興味に応じた商品やサービスの広告を表示するため、
当サイトや他サイトへのアクセスに関する情報 (氏名、住所、メール アドレス、電話番号は含まれません)
を使用することがあります。
このプロセスの詳細やこのような情報が広告配信事業者に使用されないようにする方法については、ここをクリックしてください。

「Google App EngineとSilverlightによるゲストブックアプリ」の解説の第2弾です。

1回目の解説では、ユーザが入力した訪問記録をデータストアに保存するところまでを解説しました。
今回はデータストアから記録の一覧を抽出し、SilverlightのDataGridに表示するところを解説します。

データストアから訪問記録の一覧を取得する

「Google App Engine」のデータストアからデータを抽出する方法は大きく分けると
Query(SQLではない)とGQLの2通りあるのですが、今回はGQLの方を使用しました。

GQLはSQLと似た文法で記述するデータ抽出用の言語で、「GqlQuery」クラスを介して、実行することが
出来ます。

class Book(db.Model):
  handleName = db.StringProperty();
  comment = db.StringProperty();
  created = db.DateTimeProperty(auto_now_add=True);

class BookList(webapp.RequestHandler):
  def get(self):
    self.response.headers['Content-Type'] = 'text/xml'
    self.response.out.write('')
    self.response.out.write('')

    books = db.GqlQuery("SELECT * FROM Book ORDER BY created DESC LIMIT 100")
    for book in books:
      self.response.out.write('')
      self.response.out.write('%s' % book.handleName)
      self.response.out.write('%s' % book.comment)  ← %sは%の後に続く、文字列の値に置換されます。
      self.response.out.write('%s' % book.created)     C#のString.Format()と同じ役割をします。
      self.response.out.write('')

    self.response.out.write('')

「books = db.GqlQuery」をC#に慣れた方が見ると、dbクラスのGqlQueryメソッドを
コールしているように見えるのですが、実際はパッケージ「google.appengine.ext.db」に定義されている
GqlQueryクラスのインスタンスを生成しています。その戻り値をいきなりfor文で利用しているのは
どういう文法の仕組みなのか理解できません。おそらくC#でもあるようなインデクサを使っているのかと。。

GQL自体はSQLと非常に良く似ています。テーブル名の代わりにModelのクラス名(Book)を、
列名の代わりにModelクラスの属性(createdなど)を指定しています。

結果は、XML形式でクライアントに返します。

■XMLフォーマット

<BookList>
  <Books>
    <Book>
      <HandleName>ハンドル名</HandleName>
      <Comment>コメント</Comment>
      <Created>記録日時<Created>
    </Book>
  </Books>
</BookList>

SilverlightのDataGridに表示する

SilverlightでREST通信する方法は「Silverlight Tips」で紹介していますので、詳しくはそちらを参照していただきたいのですが、
XMLの形式がTipsより少し複雑になっているので、まず、その部分を少し説明します。

今回のXMLのフォーマットは、XML中に複数回出現する可能性がある[Book]要素を含んでいます。
そのため、デシリアライズして生成されるクラスも以下のように2つに分けて定義する必要があります。

    public class BookList
    {
        public Book[] Books { get; set; }
    }

    public class Book
    {
        public String HandleName { get; set; }
        public String Comment { get; set; }
        public String Created { get; set; }
    }

XMLとクラスの両方を見比べると対応していることが分かると思います。

[Page.xaml.cs]
  BookList bookList = null;

  using (StreamReader reader = new StreamReader(stream as Stream))
  {
      //※「System.Xml.Serialization.dll」を参照設定する必要がある
      XmlSerializer serializer = new XmlSerializer(typeof(BookList));

      //XMLからBookListクラスのインスタンスを生成する
      bookList = serializer.Deserialize(reader) as BookList;
      
      foreach (Book book in bookList.Books)
      {
          //UTCを日本時間に変更し、日時のフォーマットを変換する
          book.Created = String.Format("{0:F}", DateTime.Parse(book.Created).ToLocalTime());
      }
  }

デシリアライズの方法は、XMLのフォーマットが複雑になっても変わりません。
また、「Google App Engine」で扱われている時間はUTCなので、ローカルの時間に変更する処理を行っています。

さて、本題のDataGridを使用する方法なのですが、これについてはとても簡単です。

  //画面に取得した値を設定する
  _bookList.ItemsSource = bookList.Books;  ←[_bookList]はDataGridの変数名

「BookList」クラスに定義されている「Book」クラスの配列を
DataGridのItemSourceに設定するだけです。

ItemSourceには列挙できる(foreachできる)タイプのものであれば何でも指定できます。
今回みたいに配列を指定する方法が簡単だと思います。
列名には属性の名前が自動的に設定されます。(もちろん変更する方法もあります。)

バインドされるクラス(今回はBook)の属性の型がboolだったら、セルがチェックボックスになり、
それ以外だったら、TextBoxになります。

今度は「Silverlight」と「Google App Engine」を使った本格的な
アプリ開発にトライしてみたいと思います。

本日は、先日公開させて頂いた「Google App Engine」と「Silverlight」で
作ったゲストブックアプリの解説をします。

ゲストブックアプリのURL

ゲストブックアプリの仕様

サンプルアプリなので仕様はとてもシンプルにしています。機能は以下の2つです。
・ページを訪問したユーザがハンドル名とコメントを入力し、訪問の記録として残す。
・ユーザが残した記録を閲覧する。

PythonでDataStoreに保存する

「Google App Engine」のデータストア(データの入れ物)はRDBではありません。
テーブルの代わりとなるデータモデルクラスを定義し、そのインスタンスを登録します。
インスタンスがRDBでいうところの行(レコード)になります。

まず、ブックの記録1レコードを表すデータモデルクラスを以下のように定義しました。

class Book(db.Model):
  handle_name = db.StringProperty();
  comment = db.StringProperty();
  created = db.DateTimeProperty(auto_now_add=True);

このブログを訪問してくださるみなさまはおそらくPythonに不慣れな(筆者もそうなのです。)
方がいらっしゃると思いますので、少し文法を補足します。

1行目はクラス宣言でクラス名の横の()の中身は継承しているクラスを表します。
2行目以降は変数に値を代入しているのですが、Pythonでは変数宣言は有りませんので、
代入された値の型がその変数の型になります。

これ以降は「Google App Engine」特有の話になるのですが、それぞれの変数に代入している値は、
属性(RDBでいうところの列)の型を定義するためのクラスのインスタンスです。
(Pythonではコンストラクタ呼び出しはnewなどをつけずクラス名()で行います。)

以下のページに「Google App Engine」のデータストアで扱える型の一覧が記載されています。

型の一覧

[created]は登録時の日時を管理するための属性で、登録時の日時が自動登録されるように
[auto_now_add=True]としています。

次にレコードを登録する部分のコードです。

class RegistBook(webapp.RequestHandler):
  def post(self):
    book = Book()
    book.handle_name = self.request.get('handle_name')
    book.comment = self.request.get('comment')
    book.put()

defはメソッドを定義する際に使用するキーワードで、post()メソッドはPOSTリクエストを
処理するためのメソッドです。親クラスのRequestHandlerに定義されているメソッドで
それをオーバーライドしています。

put()の中では定義したBookクラスのインスタンスを生成し、クライアントから受け取ったパラメータを
その属性に設定しています。そしてput()をコールすると、データストアへの保存を行います。

SilverlightからPOSTする

HTTPのPOSTで「ハンドル名」と「コメント」を送信するようにします。
サーバ側で定義したBookクラスの属性、[handle_name]と[comment]の型に設定した
StringProperty()は500バイト以下の文字列なので、その制限を超えないように
まず、入力チェックを行っています。

  if (_handleName.Text.Length == 0 || _comment.Text.Length == 0)
  {
      MessageBox.Show("ハンドル名とコメントを入力してください。", "メッセージ", MessageBoxButton.OK);
      return;
  }

  if (Encoding.Unicode.GetBytes(_handleName.Text).Length > 500
      || Encoding.Unicode.GetBytes(_comment.Text).Length > 500)
  {
      MessageBox.Show("ハンドル名とコメントが長すぎます。", "メッセージ", MessageBoxButton.OK);
      return;
  }

POSTリクエストの送信は「WebRequest」クラスを使って行います。
まずは、POSTするデータを送信するためのストリームの生成をサーバに要求します。
非同期のBeginGetRequestStream()をコールしています。
Tipsでも紹介しましたが、Silverlightの「WebRequest」には同期のメソッドがありません。

  //Webリクエストを生成する
  WebRequest req = WebRequest.Create(new Uri(_registUrl));
  req.Method = "POST";
  req.ContentType = "application/x-www-form-urlencoded";

  //POSTするデータを送信するようのストリームを要求する
  req.BeginGetRequestStream(new AsyncCallback(GetRequestStreamCallback), req); 

リクエストが成功すると、POSTするデータをサーバに送信します。
この処理は、リクエスト送信時に指定したコールバックメソッドの中で行います。
POSTデータの送信が完了すると、POSTリクエストをこれまた非同期で発行し、
レスポンスを待ちます。

  //データ送信用Streamが生成されるとコールされるメソッド
  private void GetRequestStreamCallback(IAsyncResult ar)
  {
      Byte[] postData = null;

      Dispatcher.BeginInvoke(
          () =>
          {
              //POSTするデータを作成する
              String postString = String.Format("handle_name={0}&comment={1}",
                  HttpUtility.UrlEncode(_handleName.Text), HttpUtility.UrlEncode(_comment.Text));

              postData = Encoding.UTF8.GetBytes(postString);
          });

      //ストリームを生成する
      WebRequest req = ar.AsyncState as WebRequest;
      using (Stream writer = req.EndGetRequestStream(ar))
      {
          writer.Write(postData, 0, postData.Length);
      }

      //リクエストを発行する
      req.BeginGetResponse(new AsyncCallback(PostResponseCallback), req);
  }

そして、POSTのレスポンスが帰った時点で、入力用のテキストボックスをクリアし、
記録の一覧を更新します。

  //登録が完了するとコールされる
  private void PostResponseCallback(IAsyncResult ar)
  {
      Dispatcher.BeginInvoke(
          () =>
          {
              _handleName.Text = "";
              _comment.Text = "";
              MessageBox.Show("登録が完了しました。", "メッセージ", MessageBoxButton.OK);
          });

      //一覧を更新する
      GetBookList();
  }

コールバックメソッドは非同期処理なので、コントロールを操作する処理については、
Dispatcherを使用してメインスレッドで実行しています。

次回の記事に続く。




⇒続きを読む

「Silverlight」と「Google App Engine」を連携させたゲストブックのサンプルアプリを作成しました。



サンプルアプリのURL

また、「Silverlight Tips(1) Google App EngineにSilverlightをホストする」と
「Silverlight Tips(2) SilverlightでREST通信をする」を併せて
ソースコードを以下に置きましたので、是非参考にしてください。

ソースのダウンロード


ゲストブックアプリのソース説明については、後日記事にします。


前回のTipsでSilverlightを「Google App Engine」にホストする
方法を紹介しました。

今回は、ホストされたSilverlightアプリと「Google App Engine」上のPythonモジュール間で
Rest通信を行う方法を紹介します。

アプリの内容はサーバ側で出力した、氏名を管理するためのXMLの内容をSilverlightの
TextBoxに表示するというシンプルなものです。

サーバ側のPythonモジュールはXMLを出力するだけのシンプルなものです。

(本格的な「Google App Engine」を使用したアプリを開発する場合は、Python側で様々なデータを簡単に
XML形式で出力する方法が課題になってきそうです。それはまた別途記事を書いてみたいと思います。)

  #!-*- coding:utf-8 -*-
  from google.appengine.ext import webapp
  from google.appengine.ext.webapp.util import run_wsgi_app

  class MainPage(webapp.RequestHandler):
    def get(self):
      self.response.headers['Content-Type'] = 'text/xml'
      self.response.out.write('<?xml version="1.0" encoding="UTF-8" ?>')
      self.response.out.write('<Person>')
      self.response.out.write('<LastName>田中</LastName>')
      self.response.out.write('<FirstName>一郎</FirstName>')
      self.response.out.write('</Person>')

  application = webapp.WSGIApplication(
                                     [('/', MainPage)],
                                     debug=True)

  def main():
    run_wsgi_app(application)

  if __name__ == "__main__":
    main()

出力されるXMLはこんな感じになります。

  <?xml version="1.0" encoding="UTF-8" ?> 
  <Person>
  <LastName>田中</LastName> 
  <FirstName>一郎</FirstName> 
  </Person>

次はSilverlight側です。C#は元々、XMLを扱うための強力なAPIを備えています。他の言語でも
備えているようなDomなどの他、XMLとC#のクラスを相互変換(シリアライズ/デシリアライズ)
する仕組みを持っています。

今回のサンプルではそのデシリアライズの仕組みを使ってXMLから値を取得するようにしました。

まずは、XMLの内容を管理するための「Person」クラスを定義します。後ほど、サーバから取得したXMLの情報から
このクラスのインスタンスを作成する処理を行います。

[Person.cs]
  public class Person
  {
     public String LastName { get; set; }
     public String FirstName { get; set; }
  }

クラス名がXMLのルート要素に、プロパティ名がXMLの子要素にそれぞれ対応しています。
クラス名や、プロパティ名は原則、XMLの要素名と同じにしておく必要があるのですが、
[System.Xml.Serialization.XmlElement()]属性などを指定することによって、異なる
プロパティ名をつけることも可能です。

  [System.Xml.Serialization.XmlElement("姓")]
  public String LastName { get; set; }

※上記コードはC# 3.0から導入された「自動プロパティ」を使用しています。

次に、サーバからXMLをダウンロードする部分です。

Http通信には「WebRequest」クラスを使用しました。Getによる単純なデータ取得の場合、
「WebClient」クラスの方が簡単に扱えるのですが、本格的なアプリを開発する際は、
POSTする必要も出てくると思ったのでこちらにしました。

まずはリクエストを発行する部分。

[MainPage.xaml.cs]
  private String _svrUrl = "http://localhost:8080/";

  //Loadedイベントのハンドラ
  private void OnLoaded(Object sender, RoutedEventArgs e)
  {
      try
      {
          //リクエストを生成する
          WebRequest req = WebRequest.Create(new Uri(_svrUrl));
          req.Method = "GET";

          //リクエストを発行する
          req.BeginGetResponse(new AsyncCallback(GetResponseCallback), req);
      }
      catch { }
  }

コンストラクタか、Loadedイベントのハンドラでやればよいか分からなかったので両方でやってみましたが
今回のサンプルでは結果は同じでした。

.Net Frameworkの「WebRequest」には同期のメソッドであるGetResponse()があるのですが、
Silverlightでは非同期のBeginGetResponse()しかないので、こちらを使っています。

非同期処理は、スレッドを使ってまとめてやりたいので、個人的には、
同期メソッドも提供してほしいです。

次がレスポンスが帰った際のコールバックメソッド。

[MainPage.xaml.cs]
  //サーバからレスポンスが帰るとコールされるコールバックメソッド
  private void GetResponseCallback(IAsyncResult ar)
  {
      try
      {
          //レスポンスオブジェクトを取得する
          WebRequest req = ar.AsyncState as WebRequest;
          WebResponse res = req.EndGetResponse(ar);

          //コンテンツをダウンロードするためのストリーム
          Stream stream = res.GetResponseStream();

          //別スレッドを起動して、ダウンロードを開始する
          ThreadPool.QueueUserWorkItem(new WaitCallback(ReadContents), stream);
      }
      catch { }
  }

コンテンツのダウンロードも非同期でやりたいので、ここでThreadPoolを使っています。

[MainPage.xaml.cs]
  //Streamからコンテンツを読み込む
  private void ReadContents(Object stream)
  {
      Person person = null;

      using (StreamReader reader = new StreamReader(stream as Stream))
      {
          //※「System.Xml.Serialization.dll」を参照設定する必要がある
          XmlSerializer serializer = new XmlSerializer(typeof(Person));

          //XMLからPersonクラスのインスタンスを生成する
          person = serializer.Deserialize(reader) as Person;
      }

      //コントロールの生成スレッドを呼び出す
      Dispatcher.BeginInvoke(
          () => {
              //画面に取得した値を設定する
              _lastName.Text = person.LastName;
              _firstName.Text = person.FirstName;
          }
      );
  }

ダウンロードしたXMLの内容から、先ほど定義したPersonクラスのインスタンスを生成しています。
そして、別スレッドからコントロールにアクセスするとエラーが発生するので、コントロールの生成スレッドを
呼び出して、TextBoxに値を設定しています。WindowsフォームでいうInvoke()です。
(匿名メソッドはラムダ形式で記述しています。)

このアプリはExpressの自動生成ページでデバックするとセキュリティ違反になるので
「Google App Engine」の開発用サーバ以下に配置して実行する必要があります。
デバックが少々不便でした。何か良い方法があるといいのですが。

ソース全体




最近、「Google App Engine」とSilverlightを組みあせて何かソフトを作ろうと
いろいろと調べたりしています。そこで得たTipsを「Silverlight Tips」として紹介していこうと思います。

そこで、今日は、「Google App Engine」にSilverlightをホストする方法を紹介します。

※この記事はローカルPCに「Google App Engine」のSDKがインストールされていて、SDK付属の開発用Webサーバで
helloworld.pyが動く環境が整えられていることを前提とします。

と言っても、面倒なことはほとんどありませんでした。

まず、「app.yaml」と「helloworld.py」を置いているディレクトリの下にSilverlightの
コンテンツを置く「slight」というディレクトリを作ります。そして、作成した
Silverlightアプリケーションのコンテンツをその下に置きます。

ディレクトリ構成はこんな感じになっています。

 [c:\python]
      |--app.yaml
      |--helloworld.py
    [slight]
        |--Moley.xap
        |--AppManifest.xaml
        |--moley.html

[Moley」はSilverlightアプリケーションの名前です。「moley.html」は
Visual Studioが自動生成する「TestPage.html」の名前を変えただけのファイルです。

そして、「app.yaml」に静的ファイルを扱うための設定を追加します。

application: blue-island
version: 1
runtime: python
api_version: 1

handlers:
↓ここを追加しました。
- url: /slight
  static_dir: slight

- url: /.*
  script: helloworld.py

全てのURLパターンに該当してしまう「helloworld.py」の設定よりも
上に追加する必要があります。
次に以下のコマンドでテスト用のWebサーバを起動します。

  dev_appserver.py c:\python

そして、ブラウザから「http://localhost:8080/slight/moley.html」にアクセスすると
見事にSilverlightアプリケーションが動作しました。

mimeタイプなどを指定する必要があると思っていたので、ちょっと拍子抜けした
感じでした。

本当にこれでいいのかと半信半疑だったので、「Google App Engine」のサーバに
インストールしてみました。

  appcfg.py update c:\python

上記、コマンドを打つと、

  Could not guess mimetype for slight/Moley.xap.  Using application/octet-stream.
  Could not guess mimetype for slight/AppManifest.xaml.  Using application/octet-stream.

というメッセージが表示されるので、やっぱり何かまずいのかなって思ったのですが、
ブラウザで表示してみると難なく、動作しました。

アプリケーション自体はボタンを配置して、そのボタンがクリックされたら、
メッセージボックスを表示するだけのシンプルなものです。
以下がサンプルのURLです。

サンプルのURL




2008年のIT業界のトレンドとして、「クラウドコンピューティング」という言葉が
頻繁に聞かれるようになりました。筆者はIT関連のNewsを毎日チェックしているのですが、
今年に入り、その頻度はますます増えてきているように感じています。

「クラウドコンピューティング」の定義は結構あいまいな気がしていますが、
以下は筆者なりの解釈です。

-----------------------------------------------------------------------

もの凄くざっくり言うと「ユーザがネットワーク上にあるハードウェアやソフトウェアを利用すること
」になるかと思います。

これだけを行ってみると従来のレンタルサーバなどと何が違うのかって感じなのですが、
「クラウド」がレンタルサーバと大きく異なる点は、クラウドの場合有る特定の
ハードウェアやソフトウェアを意識する必要がないということです。
レンタルサーバの場合、自分がどのようなスペックのサーバを間借りしているのかを
意識している必要がありますよね。

クラウドは何か分からないネットワーク上のリソースからサービスを受けるという
特徴があると思います。

-----------------------------------------------------------------------

突然、なぜこんな記事を書いているかと言いますと、筆者もこの時流に
乗り遅れないようにしっかりと技術習得をして、できれば自分でも
なんらかのサービスを作ってみたいと思ったからです。

エンジニアの視点でクラウドを見た場合、結構、ワクワクするようなことがあります。

まず、1点目はこれまでは何かしらのサービスを開発する技術やアイデアを持っていても、
それを実際に始めるには、運用していくためのインフラが必要でした。
しかし、「Google App Engine」のように無料で利用できるクラウドの出現によって、
インフラを持たない小さな企業や個人でもサービスを始めることが可能になりました。

2点目はこれまでだとシステム化することによる費用対効果が出ないと考えられてきた
中小企業や大企業の部門単位での小規模システムをクラウドにより実現できるのでは
ないかということです。

筆者は、社員10人程度の小企業や、大企業の1部門からのシステム構築案件の引き合いを
担当したことがありました。Excelを共有サーバに置いたり、メールでやり取りしたりして、
情報を共有していたところを、システムを構築して効率化したいと言ったような話でした。

ヒアリングを行ってみるとやっぱりサーバ上のデータベースで情報を一元管理すると
いう方向になり、こちらとしてはそういった提案になるのですが、
そうすると、サーバのハードウェアの費用や置き場所の問題、それを保守運用する人員の問題に
直面し、結局断念するといったケースが何度かありました。

そのときを思い返し、クラウドがその当時あればなと思ったりします。

さて現在、提供されているいわゆるクラウドと呼ばれるサービスは以下のようなものがあります。

・Force.com
・Amazon Web Services
・Google App Engine
・Windows Azure CTP版

筆者は、今回、「Google App Engine」を利用してソフトウェアを作ってみようと考えています。
「Google App Engine」は現在、Pythonしかサポートされていないので選択肢は無いのですが、
これに前からチャレンジしたかった「silverlight」を組み合わせてみたいと思います。

近々、開発日記を始めますのでよろしくお願いします。


いつも当ブログを訪問していただきありがとうございます。

WPFの検証用ソフト「Paella」のソースを公開いたします。
現時点では、「残高試算表」の実装が完了していませんので
引き続き開発を続けていきます。

「仕訳入力」と参照の機能については、
一通り動作する状態です。

ご参考にしていただけると幸いです。

今後ともよろしくお願いいたします。

ソースのダウンロード



今日は、科目CDの入力欄に数字の入力が行われた際に表示している勘定科目の候補パネルの
実現方法を紹介します。

(クリックして拡大)


会計のソフトにとって勘定科目の入力がしやすいことはとても重要な要件になります。今回は、検証用のソフトとは言え、
そこにはこだわりたいと思い、いろいろ方法を考えました。
そこで思いついたのが携帯電話のメール入力のUIです。筆者の使っている携帯電話はメールの件名や内容を入力する際、1文字入力すると
画面の下半分にその文字から始まる候補の単語が複数表示されます。

作成中の仕訳入力ソフトでもこれと同じ仕様で、科目CDが1文字入力されるたびにその数字から始まる勘定科目が
同じGrid内に表示されるようにしました。

思いついた時はちょっと難しいのかなと思ったのですが、やってみると意外と簡単に出来ました。

WPFの描画システムでは後に追加したコントロールがそれよりも前に追加したコントロールよりも前面に表示されます。
この仕組みを利用して、TextBoxのTextChangedEventのハンドラで、既にTextBoxを配置している
Grid内の同じ位置に重ねて、勘定科目の候補を表示するためのWrapPanelを貼り付けるようにしました。
その際、ColumnSpanとRowSpanを指定してWrapPanelが複数のセルにまたがるようにします。
そして、TextBoxからフォーカスが外れた際に、貼り付けたWrapPanelをGridから削除しています。

最初はZIndexを指定しないといけないと勘違いしていたのですが、WrapPanelは後から追加されることになるので、指定の必要はありませんでした。
ZIndexは前に追加したコントロールを後から追加するコントロールよりも前面に表示したい場合に使用します。

ここまでは簡単にできました。しかし一点問題がありました。明細行は30行有りますが、画面に表示している明細行は16行です。
そのため、明細部分はScrollViewerで囲ってスクロールできるようにしています。

したがって、例えば勘定科目の候補は常に10行目に表示すればよいということでなく、表示されている行の中で10行目に表示すると
いう処理にする必要が有りました。つまり、現在表示されている行が10行目~25行目だとすると、WrapPanelを貼り付けるのは
20行目ということになります。

この点を考慮し、画面に表示している16行中の8行目以前の行に入力が行われた場合は、表示している行中の11行目~15行目にWrapPanelを配置、
9行目以降に入力された場合、2行目~6行目にWrapPanelを配置する仕様としました。

これを実現するためには、まず入力が行われたTextBoxが、表示されている行の中で何行目にいるのかを調べる必要がありました。

行を定義した際に明細行の高さを[25]と設定しているので、入力が発生したテキストボックスのScrollViewerからの相対座標が分かれば
何行目に表示されているかを判定できます。UIElementクラスのTranslatePoint()メソッドでこれを行うことが出来ました。

  //テキストボックスが表示されている行の中の行位置を取得する
  double textY = accountCD.TranslatePoint(new Point(0, 0), _scrollViewer).Y;
  int rowNum = (int)(textY / RowHeight); 

TranslatePoint()は自身のコントロールの任意の座標(第1引数)が、指定されたUIElement(第2引数)からのどの位置にあるかを返してくれます。
これにより、TextBoxが表示されている行の中の何番目の行にいるかが分かりました。

後は、WrapPanelの表示位置(2行目か11行目)と取得した行のインデックスの差分を、TextBoxの表全体のなかでの行位置に足した値がWrapPanelを貼り付ける行の
インデックスになります。例えば、取得した行の表示上のインデックスが2行目、表全体での行位置が16行目の場合、11行目 - 2行目 + 16行目 = 25行目がWrapPanelの
貼り付け位置になります。

   private void ShowAccountBoard(TextBox accountCD)
   {
       //候補を表示するパネルが表示されていない場合
       if (!_showAccountBoard)
       {
           //パネルを初期化する
           _accountBoard.Orientation = Orientation.Horizontal;
           _accountBoard.Background = Brushes.LightGray;

           //パネルをGridに追加する
           _detailLines.Children.Add(_accountBoard);

           //表示位置を設定する

           //入力が行われたテキストボックスのScrollViewerから
           //の相対位置を取得し、それに元に候補パネルを表示する
           //位置を決定する。

           //テキストボックスが表示されている行の中の行位置を取得する
           double textY = accountCD.TranslatePoint(new Point(0, 0), _scrollViewer).Y;
           int rowNum = (int)(textY / RowHeight);  

           //Grid内の行位置を取得する
           GridCell cell = accountCD.Tag as GridCell;

           //表示されている行(16行)の真ん中より上にいる場合
           if (rowNum < 8)
           {
               //表示されている行の11行目に表示されるようにする
               Grid.SetRow(_accountBoard, cell.RowIndex + (10 - rowNum));
           }
           //表示されている行(16行)の真ん中より下にいる場合
           else
           {
               //表示されている行の2行目に表示されるようにする
               Grid.SetRow(_accountBoard, cell.RowIndex - (rowNum - 1));
           }

           Grid.SetColumn(_accountBoard, 1);

           Grid.SetColumnSpan(_accountBoard, 7);
           Grid.SetRowSpan(_accountBoard, 5);

           //前面に表示するためにZIndexを設定する
           //Grid.SetZIndex(_accountBoard, 1);

           _showAccountBoard = true;
       }

       //SQLiteのテーブルから勘定科目を検索し、候補パネルの内容を更新する
       using (SQLiteConnection connection = DBConnector.GetInstance().GetConnection())
       {
           _accountBoard.Children.Clear();

           Dao dao = new Dao(connection);
           connection.Open();
           DataTable accounts = dao.GetAccounts(accountCD.Text);

           foreach (DataRow row in accounts.Rows)
           {
               Label label = new Label();
               label.Content = String.Format("{0} {1}", row["account_cd"], row["account_name"]);
               _accountBoard.Children.Add(label);
           }
       }
   }

ちょっと、説明が分かりづらくなってしまったかと思いますが、参考になればうれしいです。

本日のソース






前回、仕訳入力画面の表を表示するところまでのソースを説明しました。
本日は、表の中のセルを方向キーやエンターキーで移動できるようにする部分を紹介します。

前回、紹介しましたように仕訳入力用の表は、Gridパネルの中にTextBoxコントロールを配置する
仕組みをとっています。WindowsフォームのDataGridViewのようなコントロールを使用していません。
ですので、方向キーを使ったセル間の移動なども自分で実装する必要がありました。

以下は前回紹介しましたC#のコードから表に行を追加する部分の一部ですが、
行を追加する際にTextBoxのTagにGridCellというクラスのインスタンスを設定しています。
これはテキストボックスの表の中での位置(列と行)を管理するためのクラスとして定義したものです。

方向キーが押された場合のセル移動処理を行う際にこのクラスの情報を使用しています。
また、表の中に配置されたコントロールは、表の行と列の構成をそのまま保持できるように
2次元配列(_cells)に入れて管理しています。

 TextBox drAccountCD = new TextBox();
 drAccountCD.BorderThickness = new Thickness(0, 0, 1, 1);
 drAccountCD.VerticalContentAlignment = VerticalAlignment.Center;
 drAccountCD.Tag = new GridCell(1, rowNum);
 InputMethod.SetIsInputMethodEnabled(drAccountCD,false);

 _cells[rowNum, 1] = drAccountCD;

本題の方向キーやエンタキーが押された場合の処理に入ります。
この処理は「JournalGrid.xaml.cs」の中でOnPreviewKeyDownメソッドをオーバーライドすることで
行います。方向キーなどの特殊キーの処理は、要素ツリーの上から下の順番に伝えられる「Tunnel」方向の
RoutedEventを処理する必要があります。(「Bubble」方向では、キーが押されたことが認識されません。)

以下はOnPreviewKeyDown()メソッドのコードの一部です。
 //イベントソースを取得する
 TextBox textBox = e.OriginalSource as TextBox;

 //ソースがTextBox以外の場合、処理しない
 if (textBox == null)
 {
     return;
 }

 //Grid内の位置情報を取得する
 GridCell cell = textBox.Tag as GridCell;

 //左キー
 if (e.Key == Key.Left)
 {
     for (int i = cell.ColIndex - 1; i >= 0; i--)
     {
         if (_cells[cell.RowIndex, i] is TextBox)
         {
             if (!(_cells[cell.RowIndex, i] as TextBox).IsReadOnly)
             {
                 _cells[cell.RowIndex, i].Focus();
                 (_cells[cell.RowIndex, i] as TextBox).CaretIndex = 0;

                 //フォーカスが移動した後、カレットが移動しないように処理済にする
                 e.Handled = true;

                 break;
             }
         }
     }
 }

明細行を追加する際に、設定しておいたGridCellの情報と表の中のコントロールを管理する2次元配列を
使用してフォーカスを移動する処理を行っています。WPFのルーティングイベントシステムでは
明示的にイベントを処理済みにしないと要素ツリーの次の要素で処理が継続して行われます。
今回、フォーカスを移した後に、イベントを処理済にしているのは、「JournalGrid」の子要素である
セルに配置されたTextBoxが、方向キーのイベントを処理することによりTextBox内のカレット位置が移動することを防止するためです。

次回は勘定科目CDが入力された際に勘定科目の候補をGrid内に表示する部分を紹介します。

本日のソース






このソフトの肝になる仕訳伝票入力画面が様になってきましたので、今日はその部分のコードを紹介します。
画面はこんな感じにしました。

(クリックして拡大)


仕訳の明細は「明細番号」、「借方勘定科目コード」、「借方勘定科目名」、「借方金額」、
「貸方勘定科目コード」、「貸方勘定科目名」、「貸方金額」、「摘要」で構成しています。

そのうち、ユーザが入力をするのは「借方勘定科目コード」、「借方金額」、「貸方勘定科目コード」、
「貸方金額」、「摘要」になります。
本来、会計システムは大量のトランザクションが入力されるシステムになるので、
入力がし易いことがとても重要な要件になります。

特に勘定科目に関しては、検索のしやすさが重要です。そこでパエリアでは、
勘定科目コードに1桁入力される度に、科目コードがその数字から始まる勘定科目の一覧を
同一のGrid内に表示するようにしました。

(クリックして拡大)


さて、少しずつコードを説明していきます。
仕訳伝票入力画面は「JournalEntry」というUserControlとして作成しています。
そして、その「JournalEntry」は以下のような要素で構成しています。

  <ScrollViewer>
    <Grid>
      <StackPanel>←年月日を表示するヘッダ部分
      <JournalGrid>←仕訳を入力する表の部分
      <StackPanel>←保存ボタンを表示するフッタ部分
    </Grid>
  </ScrollViewer>

3行1列のグリッドを作ってその各セルに、ヘッダ、表部分、フッタを構成するコントロールを
配置しています。
仕訳を入力する表の部分は伝票の参照画面でも使いまわしたいのでUserControlを拡張した
「JournalGrid」というクラスを別に作成しました。

「JournalGrid」では、さらにGridを使って全体を構成しています。
列と行は以下のように定義しています。

 <Grid.ColumnDefinitions>
     <ColumnDefinition Width="30" /><!-- 明細番号 -->
     <ColumnDefinition Width="70"/><!-- 借方勘定科目コード-->
     <ColumnDefinition Width="150"/><!-- 借方勘定科目名-->
     <ColumnDefinition Width="100"/><!-- 借方金額 -->
     <ColumnDefinition Width="70"/><!-- 貸方勘定科目コード-->
     <ColumnDefinition Width="150"/><!-- 貸方勘定科目名-->
     <ColumnDefinition Width="100"/><!-- 貸方金額 -->
     <ColumnDefinition Width="200"/><!-- 摘要 -->
 </Grid.ColumnDefinitions>

 <Grid.RowDefinitions>
     <RowDefinition Height="30"/><!-- ヘッダ1行目 -->
     <RowDefinition Height="30"/><!-- ヘッダ2行目 -->
     <RowDefinition Height="400"/><!-- 明細部分 -->
     <RowDefinition Height="10"/><!-- 余白 -->
     <RowDefinition Height="25"/><!-- フッタ -->
     <RowDefinition Height="10"/><!-- 余白 -->
 </Grid.RowDefinitions>

そして、以下が表のヘッダ部分です。

 <!-- ヘッダ1行目 -->
 <Label Grid.Column="0" Grid.Row="0" Style="{StaticResource HeaderText}" BorderThickness="1"/>
 <Label Grid.Column="1" Grid.Row="0" Grid.ColumnSpan="3" HorizontalContentAlignment="Center" 
                          Style="{StaticResource HeaderText}" BorderThickness="0,1,1,1">借方</Label>
 <Label Grid.Column="4" Grid.Row="0" Grid.ColumnSpan="3" HorizontalContentAlignment="Center" 
                          Style="{StaticResource HeaderText}" BorderThickness="0,1,1,1">貸方</Label>
 <Label Grid.Column="7" Grid.Row="0" Style="{StaticResource HeaderText}" BorderThickness="0,1,1,0"/>
 
 <!-- ヘッダ2行目 -->
 <Label Grid.Column="0" Grid.Row="1" Style="{StaticResource HeaderText}" BorderThickness="1,0,1,1"/>
 <Label Grid.Column="1" Grid.Row="1" Style="{StaticResource HeaderText}" BorderThickness="0,0,1,1">科目コード</Label>
 <Label Grid.Column="2" Grid.Row="1" Style="{StaticResource HeaderText}" BorderThickness="0,0,1,1">科目名</Label>
 <Label Grid.Column="3" Grid.Row="1" Style="{StaticResource HeaderText}" BorderThickness="0,0,1,1">金額</Label>
 <Label Grid.Column="4" Grid.Row="1" Style="{StaticResource HeaderText}" BorderThickness="0,0,1,1">科目コード</Label>
 <Label Grid.Column="5" Grid.Row="1" Style="{StaticResource HeaderText}" BorderThickness="0,0,1,1">科目名</Label>
 <Label Grid.Column="6" Grid.Row="1" Style="{StaticResource HeaderText}" BorderThickness="0,0,1,1">金額</Label>
 <Label Grid.Column="7" Grid.Row="1" Style="{StaticResource HeaderText}" BorderThickness="0,0,1,1">摘要</Label>

ヘッダの1行目は「借方」、「貸方」のラベルをColumnSpanを指定して複数列にまたいで配置しています。
続いて明細部分。

  <ScrollViewer Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="8" Height="400" Name="_scrollViewer"
                Focusable="False" VerticalScrollBarVisibility="Auto">
      <Grid Name="_detailLines">
          <Grid.ColumnDefinitions>
              <ColumnDefinition Width="30" /><!-- 明細番号 -->
              <ColumnDefinition Width="70"/><!-- 借方勘定科目コード-->
              <ColumnDefinition Width="150"/><!-- 借方勘定科目名-->
              <ColumnDefinition Width="100"/><!-- 借方金額 -->
              <ColumnDefinition Width="70"/><!-- 貸方勘定科目コード-->
              <ColumnDefinition Width="150"/><!-- 貸方勘定科目名-->
              <ColumnDefinition Width="100"/><!-- 貸方金額 -->
              <ColumnDefinition/><!-- 摘要 -->
          </Grid.ColumnDefinitions>
      </Grid>
  </ScrollViewer>

明細部分だけスクロールさせたいので、「ScrollViewer」を使ってその中に明細部分だけのGridを配置しています。
そして外側の表と同じ列を定義しています。

明細行に関しては数が多いのでコードの方から作成しています。

 //コンストラクタ
 public JournalGrid()
 {
     InitializeComponent();

     //明細行を追加する
     for (int i = 0; i < _lineCount; i++)
     {
         AddRow(i);
     }
 }

 /// Gridに明細行を追加する
 public void AddRow(int rowNum)
 {
     //行を追加する
     RowDefinition row = new RowDefinition();
     row.Height = new GridLength(RowHeight);
     _detailLines.RowDefinitions.Add(row);

     //明細番号
     Label num = new Label();
     num.Content = rowNum + 1;
     num.BorderThickness = new Thickness(1, 0, 1, 1);
     num.Style = Resources["HeaderText"] as Style;
     num.Tag = new GridCell(0, rowNum);

     _detailLines.Children.Add(num);
     Grid.SetColumn(num, 0);
     Grid.SetRow(num, rowNum);

     _cells[rowNum, 0] = num;

     //借方勘定コード
     TextBox drAccountCD = new TextBox();
     drAccountCD.BorderThickness = new Thickness(0, 0, 1, 1);
     drAccountCD.VerticalContentAlignment = VerticalAlignment.Center;            
     drAccountCD.Tag = new GridCell(1, rowNum);
     InputMethod.SetIsInputMethodEnabled(drAccountCD,false);

     _detailLines.Children.Add(drAccountCD);
     Grid.SetColumn(drAccountCD, 1);
     Grid.SetRow(drAccountCD, rowNum);

     _cells[rowNum, 1] = drAccountCD;

     //借方勘定科目名
     TextBox drAccountName = new TextBox();
     drAccountName.BorderThickness = new Thickness(0, 0, 1, 1);
     drAccountName.VerticalContentAlignment = VerticalAlignment.Center;
     drAccountName.IsReadOnly = true;
     drAccountName.IsTabStop = false;
     drAccountName.Tag = new GridCell(2, rowNum);
     
     _detailLines.Children.Add(drAccountName);
     Grid.SetColumn(drAccountName, 2);
     Grid.SetRow(drAccountName, rowNum);

     _cells[rowNum, 2] = drAccountName;

     //借方金額
     TextBox drAmount = new TextBox();
     drAmount.BorderThickness = new Thickness(0, 0, 1, 1);
     drAmount.VerticalContentAlignment = VerticalAlignment.Center;
     drAmount.HorizontalContentAlignment = HorizontalAlignment.Right;
     drAmount.Tag = new GridCell(3, rowNum);
     InputMethod.SetIsInputMethodEnabled(drAmount, false);

     _detailLines.Children.Add(drAmount);
     Grid.SetColumn(drAmount, 3);
     Grid.SetRow(drAmount, rowNum);

     _cells[rowNum, 3] = drAmount;

     //貸方勘定コード
     TextBox crAccountCD = new TextBox();
     crAccountCD.BorderThickness = new Thickness(0, 0, 1, 1);
     crAccountCD.VerticalContentAlignment = VerticalAlignment.Center;
     crAccountCD.Tag = new GridCell(4, rowNum);
     InputMethod.SetIsInputMethodEnabled(crAccountCD, false);

     _detailLines.Children.Add(crAccountCD);
     Grid.SetColumn(crAccountCD, 4);
     Grid.SetRow(crAccountCD, rowNum);

     _cells[rowNum, 4] = crAccountCD;

     //貸方勘定科目名
     TextBox crAccountName = new TextBox();
     crAccountName.BorderThickness = new Thickness(0, 0, 1, 1);
     crAccountName.VerticalContentAlignment = VerticalAlignment.Center;
     crAccountName.IsReadOnly = true;
     crAccountName.IsTabStop = false;
     crAccountName.Tag = new GridCell(5, rowNum);

     _detailLines.Children.Add(crAccountName);
     Grid.SetColumn(crAccountName, 5);
     Grid.SetRow(crAccountName, rowNum);

     _cells[rowNum, 5] = crAccountName;

     //貸方金額
     TextBox crAmount = new TextBox();
     crAmount.BorderThickness = new Thickness(0, 0, 1, 1);
     crAmount.VerticalContentAlignment = VerticalAlignment.Center;
     crAmount.HorizontalContentAlignment = HorizontalAlignment.Right;
     crAmount.Tag = new GridCell(6, rowNum);
     InputMethod.SetIsInputMethodEnabled(crAmount, false);

     _detailLines.Children.Add(crAmount);
     Grid.SetColumn(crAmount, 6);
     Grid.SetRow(crAmount, rowNum);

     _cells[rowNum, 6] = crAmount;

     //摘要
     TextBox memo = new TextBox();
     memo.BorderThickness = new Thickness(0, 0, 1, 1);
     memo.VerticalContentAlignment = VerticalAlignment.Center;
     memo.Tag = new GridCell(7, rowNum);

     _detailLines.Children.Add(memo);
     Grid.SetColumn(memo, 7);
     Grid.SetRow(memo, rowNum);

     _cells[rowNum, 7] = memo;

 }

明細行に配置したコントロールを管理するために、2次元配列「Control[,] _cells」をインスタンス変数としてあらかじめ定義し、
行を追加する過程で、配置したコントロールをそれにつめていきます。

表の動作については次回説明します。

本日のソース






上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。