読者です 読者をやめる 読者になる 読者になる
ようこそ。睡眠不足なプログラマのチラ裏です。

コントロールのTabIndex順で処理したいことってあるよね。階層のTabIndexを表現してみよう。

プログラミング C#3.0


またまたWindowsApplicationにおける標準コントロールが、空気読めなさすぎなのではないかというお話。
C#でTabIndexの階層構造順にControlを並べ替える方法の1つの例を示します。


TabIndexプロパティだけじゃ、TabIndexの階層構造わかんねーじゃん

各標準コントロールのTabIndexプロパティにて、[Tabキー]押下時のFocusの移動順を定義することができます。
画面にコンテナコントロールを利用しないような場合、TabIndex順は単純な数値の大小比較となるが、
コンテナコントロール内に入れ子にするようなレイアウトであれば、TabIndexの構造が階層化し複雑になります。
そのようなレイアウトのTabIndexを定義する場合、通常であればVisualStudioの[表示]→[タブオーダー]の操作によって、
TabIndexを割り当てることになります。これはそこそこ便利です。しかし、このTabIndexの階層構造とその順序は
実行時には直接知ることができないようになっている。・・・これは不便極まりないです。
完全に被害妄想ですが、ある種の嫌がらせとさえも思えます。TabIndex順序を決定するものが
階層構造であるのであれば、当然その値も取得できるようになっていて然るべきだと思うんだけど。非常に残念です。


レガシーなシステムや、旧システムからのマイグレーション
あるいは顧客要望という名のしがらみから、[Enterキー]押下によるコントロール間のFocus移動を
行わなければならないという仕様から未だに逃れられないこともあると思います。
そんなときに、TabIndexの階層構造が把握できたらどんなに嬉しいでしょう。
このケースに限らず、TabIndexの階層構造およびその順序を取得したくなるシナリオは多々ありそうです。


TabIndexの階層構造を取得できるインターフェイスを定義する
というわけで、ないものは作りましょう。
ぱっと思いついた限りでは、TabIndexの階層構造はイテレータブロックで表現するとよさそうです。
さっそくTabIndexの階層構造が取得できることを表すインターフェイスを定義してみましょう。
TabIndexの階層構造の値でソートを行いたいことなどを踏まえて、
最低限必要であるインターフェイスをそれぞれ継承しておきましょう。

using System;
using System.Collections.Generic;
using System.Windows.Forms;

namespace ClassLibrary1
{
    public interface IHasHierarchicalTabIndices : 
        IWin32Window, IEnumerable<int>, IComparable, IComparable<IHasHierarchicalTabIndices>
    {
        /// <summary>
        /// TabIndex階層を取得します。
        /// </summary>
        IEnumerable<int> HierarchicalTabIndices { get; }
    }
}

IWin32Windowインターフェイスを含めてある理由は、後ほど。


TabIndexの階層構造を取得する拡張メソッドを作る

Controlに、TabIndexの階層構造を取得できる拡張メソッド等を追加してみます。
とりあえず、楽なのでStackを使って書いてみましたが、LINQ等を使って書いたほうがクールかもしれません。

using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;

namespace ClassLibrary1
{
    public static class ControlExtentions
    {
        public static IEnumerable<Control> GetAllControls(this Control self)
        {
            foreach (Control c in self.Controls)
            {
                yield return c;
                foreach (Control a in GetAllControls(c))
                    yield return a;
            }
        }
        
        public static IEnumerable<int> GetHierarchicalTabindices(this Control self)
        {
            var s = new Stack<int>();
            s.Push(self.TabIndex);
            var parent = self.Parent;
            while (CheckParent(parent))
            {
                s.Push(parent.TabIndex);
                parent = parent.Parent;
            }

            while (s.Count != 0)
                yield return s.Pop();
        }

        public static string GetHierarchicalTabIndicesString(this Control self)
        {
            var sb = new StringBuilder();
            foreach (var item in self.GetHierarchicalTabindices())
                sb.AppendFormat("{0},", item.ToString());
            return Regex.Replace(sb.ToString(), ",$", "");
        }

        private static bool CheckParent(Control target)
        {
            //このあたりの実装は、特定のインターフェイスによって
            //判定するようにしたほうがよいかもしれません、今回はとりあえずこれで
            if (target == null) return false;
            if (target is Form) return false;
            return true;
        }
    }

}

TabIndexの階層構造の順序でソートするためのヘルパークラスの作成
IComparerインターフェイスを実装し、
TabIndexの階層構造でソートするヘルパークラスを作成します。
せっかくなのでソートの昇順と降順を選択可能なように作っておきましょう。


ここで問題となるのが、下記リンク(わんくまの掲示板)にあるように、
TabIndexの階層構造に全く同じ値が設定されていた場合どうするのかということです。

画面内のすべてのコントロールをTabIndex順に取得したいのですが。
http://bbs.wankuma.com/index.cgi?mode=al2&namber=7689&KLOG=7


ですが、この問題に対する答えは簡単です。MSDN - Control.TabIndex プロパティの仕様どおりに、
TabIndex階層構造が同一の場合は、コントロールのZオーダーによってコントロール内の循環順序を決定する仕様にしましょう。
しかし、コントロールのZオーダーの順序は.NET Frameworkだけでは取得することができません。
これは、WinAPIのGetWindow関数を用いて、Zオーダーの順序を判定する方法を用いることで解決することができます。
先ほど、IHasHierarchicalTabIndicesインターフェイスがIWin32Windowを継承していた理由はこのためです。

using System;
using System.Runtime.InteropServices;

namespace ClassLibrary1
{
    public static class PlatformInvoker
    {
        /// <summary>
        /// GetWindow関数のコマンド
        /// </summary>
        public enum GetWindowCmd
        {
            GW_HWNDFIRST = 0,
            GW_HWNDLAST = 1,
            GW_HWNDNEXT = 2,
            GW_HWNDPREV = 3,
            GW_OWNER = 4,
            GW_CHILD = 5,
            GW_ENABLEDPOPUP = 6
        }

        [DllImport("user32.dll")]
        public static extern IntPtr GetWindow(IntPtr hwd, uint uCmd);

    }
}
using System;
using System.Windows.Forms;
using System.Collections.Generic;

namespace ClassLibrary1
{
    public enum Sort
    {
        Asc,
        Desc
    }

    /// <summary>
    /// SortHelperOfHierarchicalTabIndices
    /// </summary>
    public class SortHelperOfHierarchicalTabIndices : IComparer<IHasHierarchicalTabIndices> 
    {
        private int togleNum = 1;
        public SortHelperOfHierarchicalTabIndices() { }
        public SortHelperOfHierarchicalTabIndices(Sort sort)
        {
            switch (sort)
            {
                case Sort.Asc:
                    break;
                case Sort.Desc:
                    togleNum = -1;
                    break;
                default:
                    togleNum = 1;
                    break;
            }
        }

        private int Compare(IntPtr hwdx, IntPtr hwdy)
        {
            var h = PlatformInvoker.GetWindow(hwdx, (uint)PlatformInvoker.GetWindowCmd.GW_HWNDNEXT);
            while (h != default(IntPtr))
            {
                if (h == hwdy) return -1 * togleNum;
                h = PlatformInvoker.GetWindow(h, (uint)PlatformInvoker.GetWindowCmd.GW_HWNDNEXT);
            }

            h = PlatformInvoker.GetWindow(hwdx, (uint)PlatformInvoker.GetWindowCmd.GW_HWNDPREV);
            while (h != default(IntPtr))
            {
                if (h == hwdy) return 1 * togleNum;
                h = PlatformInvoker.GetWindow(h, (uint)PlatformInvoker.GetWindowCmd.GW_HWNDPREV);
            }
            return 0;
        }

        #region IComparer<T1> メンバ

        public int Compare(IHasHierarchicalTabIndices x, IHasHierarchicalTabIndices y)
        {
            using (IEnumerator<int> enumerator1 = x.GetEnumerator())
            using (IEnumerator<int> enumerator2 = y.GetEnumerator())
            {
                bool e1 = enumerator1.MoveNext();
                bool e2 = enumerator2.MoveNext();

                while (e1 && e2)
                {
                    int compare = enumerator1.Current.CompareTo(enumerator2.Current) * togleNum;
                    if (compare != 0) return compare;

                    e1 = enumerator1.MoveNext();
                    e2 = enumerator2.MoveNext();
                }
                if (!e1 && !e2) return Compare(x.Handle, y.Handle);
                if (!e1) return -1 * togleNum;
                if (!e2) return 1 * togleNum;
            }
            return 0;
        }

        #endregion
    }
}


各コントロールでの実装
IHasHierarchicalTabIndicesインターフェイスを実装してみます。
今回はテキストボックスとコンボボックスの実装のみにとどめていますが、
実際にはアプリケーション内で利用するすべてのコントロールに実装することになります。


テキストボックスでの実装

using System.Collections;
using System.Collections.Generic;
using System.Windows.Forms;

namespace ClassLibrary1
{
    public class TextBoxEx : TextBox,
        IHasHierarchicalTabIndices
    {
        private IEnumerable<int> _hierarchicalTabIndices;

        public TextBoxEx()
        {
            _hierarchicalTabIndices = this.GetHierarchicalTabindices();
        }

        public IEnumerable<int> HierarchicalTabIndices
        {
            get { return _hierarchicalTabIndices; }
        }

        protected override void OnCreateControl()
        {
            this.Text = this.Name;
            base.OnCreateControl();
        }

        #region IComparable メンバ

        public int CompareTo(object obj)
        {
            return CompareTo((IHasHierarchicalTabIndices)obj);
        }

        #endregion

        #region IComparable<IEnterMove> メンバ

        public int CompareTo(IHasHierarchicalTabIndices other)
        {
            return new SortHelperOfHierarchicalTabIndices().Compare(this, other);
        }

        #endregion

        #region IEnumerable<int> メンバ

        public IEnumerator<int> GetEnumerator()
        {
            return this.HierarchicalTabIndices.GetEnumerator();
        }

        #endregion

        #region IEnumerable メンバ

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return (IEnumerator)this.HierarchicalTabIndices.GetEnumerator();
        }

        #endregion
    }
}


コンボボックスでの実装

using System.Collections;
using System.Collections.Generic;
using System.Windows.Forms;

namespace ClassLibrary1
{
    public class ComboBoxEx : ComboBox,
        IHasHierarchicalTabIndices
    {
        private IEnumerable<int> _hierarchicalTabIndices;

        public ComboBoxEx()
        {
            _hierarchicalTabIndices = this.GetHierarchicalTabindices();
        }

        public IEnumerable<int> HierarchicalTabIndices
        {
            get { return _hierarchicalTabIndices; }
        }

        protected override void OnCreateControl()
        {
            this.Text = this.Name;
            base.OnCreateControl();
        }

        #region IComparable メンバ

        public int CompareTo(object obj)
        {
            return CompareTo((IHasHierarchicalTabIndices)obj);
        }

        #endregion

        #region IComparable<IEnterMove> メンバ

        public int CompareTo(IHasHierarchicalTabIndices other)
        {
            return new SortHelperOfHierarchicalTabIndices().Compare(this, other);
        }

        #endregion

        #region IEnumerable<int> メンバ

        public IEnumerator<int> GetEnumerator()
        {
            return this.HierarchicalTabIndices.GetEnumerator();
        }

        #endregion

        #region IEnumerable メンバ

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return (IEnumerator)this.HierarchicalTabIndices.GetEnumerator();
        }

        #endregion
    }
}


TabIndex階層構造順にソートしてみます

では、以下のようなレイアウトの画面で実際に試してみます。


using System;
using System.Collections.Generic;
using System.Windows.Forms;
using ClassLibrary1;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click_1(object sender, EventArgs e)
        {
            List<IHasHierarchicalTabIndices> list = new List<IHasHierarchicalTabIndices>();
            foreach (var item in this.GetAllControls())
                if (item is IHasHierarchicalTabIndices)
                    list.Add(item as IHasHierarchicalTabIndices);

            Console.WriteLine("Name  : HierarchicalTabIndices Asc");
            list.Sort(new SortHelperOfHierarchicalTabIndices());
            foreach (Control item in list)
                Console.WriteLine("{0} : {1}", item.Name, item.GetHierarchicalTabIndicesString());

            Console.WriteLine();
            Console.WriteLine("Name  : HierarchicalTabIndices Desc");
            list.Sort(new SortHelperOfHierarchicalTabIndices(Sort.Desc));
            foreach (Control item in list)
                Console.WriteLine("{0} : {1}", item.Name, item.GetHierarchicalTabIndicesString());
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    static class Program
    {
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}


実行結果

Name  : HierarchicalTabIndices Asc
txt01 : 0
txt02 : 1
txt03 : 2,0,0
txt04 : 2,0,1
txt05 : 2,1
txt06 : 2,2
txt07 : 2,3,0
txt08 : 2,3,1
txt09 : 3,0
txt10 : 3,1,0,0,0
txt11 : 3,1,0,0,1
txt12 : 3,1,0,1
txt13 : 3,1,1
txt14 : 3,2
txt15 : 4,0
cmb16 : 4,0
txt17 : 4,0
cmb18 : 4,0
txt19 : 4,0
txt20 : 4,0
cmb21 : 4,0

Name  : HierarchicalTabIndices Desc
cmb21 : 4,0
txt20 : 4,0
txt19 : 4,0
cmb18 : 4,0
txt17 : 4,0
cmb16 : 4,0
txt15 : 4,0
txt14 : 3,2
txt13 : 3,1,1
txt12 : 3,1,0,1
txt11 : 3,1,0,0,1
txt10 : 3,1,0,0,0
txt09 : 3,0
txt08 : 2,3,1
txt07 : 2,3,0
txt06 : 2,2
txt05 : 2,1
txt04 : 2,0,1
txt03 : 2,0,0
txt02 : 1
txt01 : 0


お、いい感じですね。同一のTabIndex階層構造を持つコントロールについて、
正しくZオーダーの順序にソートされることが確認できました。めでたしめでたし。


以下オマケです。
今回はZオーダーによる判定が必要だったので、ヘルパークラスについて
上手く抽象化ができませんでしたが、特殊な判定を伴わないイテレータのCompareについては、
例えば以下のように抽象化することができますね。

using System;
using System.Collections.Generic;

namespace ClassLibrary1
{
    /// <summary>
    /// SortHelperOfEnumerable
    /// </summary>
    /// <typeparam name="T1"></typeparam>
    /// <typeparam name="T2"></typeparam>
    public class SortHelperOfEnumerable<T1, T2> : IComparer<T1>
        where T1 : IComparable<T1>, IComparable, IEnumerable<T2>
        where T2 : IComparable
    {
        private int togleNum = 1;
        public SortHelperOfEnumerable() { }
        public SortHelperOfEnumerable(Sort sort)
        {
            switch (sort)
            {
                case Sort.Asc:
                    break;
                case Sort.Desc:
                    togleNum = -1;
                    break;
                default:
                    togleNum = 1;
                    break;
            }
        }

        #region IComparer<T1> メンバ

        public int Compare(T1 x, T1 y)
        {
            using (IEnumerator<T2> enumerator1 = x.GetEnumerator())
            using (IEnumerator<T2> enumerator2 = y.GetEnumerator())
            {
                bool e1 = enumerator1.MoveNext();
                bool e2 = enumerator2.MoveNext();

                while (e1 && e2)
                {
                    int compare = enumerator1.Current.CompareTo(enumerator2.Current) * togleNum;
                    if (compare != 0) return compare;

                    e1 = enumerator1.MoveNext();
                    e2 = enumerator2.MoveNext();
                }
                if (!e1 && !e2) return 0;
                if (!e1) return -1 * togleNum;
                if (!e2) return 1 * togleNum;
            }
            return 0;
        }

        #endregion
    }
}

てゆうか、ググってみたけど、意外に誰もやってないのね。
何かの参考になれば幸いです。