今日は、科目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()メソッドでこれを行うことが出来ました。
TranslatePoint()は自身のコントロールの任意の座標(第1引数)が、指定されたUIElement(第2引数)からのどの位置にあるかを返してくれます。
これにより、TextBoxが表示されている行の中の何番目の行にいるかが分かりました。
後は、WrapPanelの表示位置(2行目か11行目)と取得した行のインデックスの差分を、TextBoxの表全体のなかでの行位置に足した値がWrapPanelを貼り付ける行の
インデックスになります。例えば、取得した行の表示上のインデックスが2行目、表全体での行位置が16行目の場合、11行目 - 2行目 + 16行目 = 25行目がWrapPanelの
貼り付け位置になります。
ちょっと、説明が分かりづらくなってしまったかと思いますが、参考になればうれしいです。
本日のソース
実現方法を紹介します。
(クリックして拡大)

会計のソフトにとって勘定科目の入力がしやすいことはとても重要な要件になります。今回は、検証用のソフトとは言え、
そこにはこだわりたいと思い、いろいろ方法を考えました。
そこで思いついたのが携帯電話のメール入力の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); } } }
ちょっと、説明が分かりづらくなってしまったかと思いますが、参考になればうれしいです。
本日のソース
コメント
コメントの投稿
トラックバック
http://csfun.blog49.fc2.com/tb.php/32-346e3d6e