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

TextBoxってReadOnly = trueでもフォーカスあたる。空気読めなさすぎだよね。

プログラミング C#3.0

WindowsApplicationの標準コントロールってKYだよねというお話

C#でもVB.NETでも当然同じなんだけど、TextBoxコントロールのReadOnlyプロパティをtrueに設定したら、
入力を受け付けなくなるので、背景色をグレーに設定したりする。でもフォーカスは普通に受け付ける。
TabStopプロパティをfalseに設定しても、当然マウスでクリックしたらGotFocusしてしまう。
ReadOnlyがtrueであれば、「べ、別にあんたのためにフォーカス移動したんじゃないんだからねッ!」って、
ツンデレ口調で表現した意味は全くないんだけど、フォーカスなんて別に移動して欲しくないんだよね実際*1
そーゆー話は、WindowsApplicationを開発している現場であれば、あちこちであるもんだろうと思う。
Enabledプロパティをfalseにしたところで、文字が薄いグレーになっちゃったりで・・・、文字薄いのはお客さんが嫌だっつーし。
ホント空気読めないコントロールで辟易だよ(今に始まったことではないけど)。
他のコントロールに関してもそれは同様だったりで、そもそもReadOnlyプロパティ自体がなかったりする。
作った人のデザインセンスを疑わざるを得ない。MSなにやってんの。MS好きだけどね。



Selectableプロパティを設けるという対応方法

というわけで、コントロールへフォーカスを移したくないケースについて、その対策方法の一例を示します。


まず、コントロールが選択可能であるかを表すプロパティを設けます。
肝心の選択不可の状態にする方法ですが、簡単に説明すると、
WndProcをオーバーライドして、飛んできたウィンドウメッセージを判断し、
P/Invokeなどを利用して自分のお好みどおりに制御する手法です。


ただ、多種多様なコントロールすべてに対してWndProcを個別に実装するのはとても面倒ですし、
コントロール独自の仕様によりオーバーライドしたWndProc内の操作が煩雑化するのは避けたいところです。
そこで、NativeWindowクラスを用いる手法をとります。NativeWindowクラスは、
ウィンドウハンドルとウィンドウプロシージャの下位についてカプセル化します。
また、同じような実装を何度も行うのは面倒ですので、この操作に対する専用のInterfaceを作成し、
そのInterfaceに拡張メソッド提供しMix-inっぽくすることで、実装をなるべく簡素化します。


では、以下サンプルです。どうぞ。


選択可能か否かを設定できることを表す、ISelectableインターフェイスを定義する

選択可能か否かを設定できることを表すインターフェイスを定義します。
また、インターフェイスに拡張メソッド提供しMix-inっぽくすることで、実装をなるべく簡素化します。
InitializeSelectableメソッドでは、NativeWindowの派生クラスへ
ウィンドウハンドルとウィンドウプロシージャを割り当てるための操作を行っています。
DesignMode時までフォーカスを受け付けなくなると困っちゃいます。今回は実装しやすさを優先して、
このインターフェイスでDesignModeの判定も出来るように定義しちゃいました。ちょっとダサい気もしますけど。

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

namespace ConsoleLibrary1
{
    /// <summary>
    /// コントロールを選択不可能な状態にすることが可能であることを
    /// 明示するためのインターフェイス
    /// 
    /// ※元々選択可能であるコントロールについて、
    ///  選択不可能な状態にすることを提供するためのインターフェイスです。
    /// ※これは選択不可能なコントロールを
    ///  選択可能状態へすることを提供するためのものではありません。
    /// 
    /// </summary>
    public interface ISelectable
    {
        /// <summary>
        /// コントロールが選択可能であるかどうかを取得または設定します。
        /// </summary>
        bool Selectable { get; set; }

        /// <summary>
        /// NativeWindowList
        /// </summary>
        List<SelectableNativeWindow> NativeWindowList { set; }

        /// <summary>
        /// NativeWindowのAssignHandle対象ハンドルを取得します。
        /// </summary>
        /// <returns></returns>
        IEnumerable<IntPtr> GetAssignHandles();

        /// <summary>
        /// デザインモードか否かを取得します。
        /// </summary>
        bool DesignMode { get; }
    }

    public static class ISelectableExtensions
    {
        /// <summary>
        /// コントロールの選択可否についての初期化処理
        /// </summary>
        /// <param name="self"></param>
        public static void InitializeSelectable(this ISelectable self)
        {
            var control = self as Control;
            if (control == null) throw new ArgumentException("コントロールである必要があります。");

            var list = new List<SelectableNativeWindow>();
            foreach (var handle in self.GetAssignHandles())
            {
                var snw = new SelectableNativeWindow(control, handle);
                list.Add(snw);
            }
            self.NativeWindowList = list;
        }
    }
}


コントロールを選択不可能な状態にすることを提供するNativeWindowサブクラス

オーバーライドしたWndProc内の操作はお好みで。
例えば、WM_SETFOCUSのみを判定して操作するようにした場合、
フォーカスは受け付けないが、その他の操作はすべて受け付けるようになります。
例えば、クリックしてもフォーカスを受け取らないボタンを作ることができます。

using System;
using System.Security.Permissions;
using System.Windows.Forms;

namespace ConsoleLibrary1.NativeWindow
{
    /// <summary>
    /// コントロールを選択不可能な状態にすることを提供するNativeWindow
    /// 
    /// NativeWindowはウィンドウ ハンドルとウィンドウ プロシージャの下位のカプセル化を提供します。
    /// </summary>
    [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    public sealed class SelectableNativeWindow : System.Windows.Forms.NativeWindow
    {
        /// <summary>
        /// 対象コントロール
        /// </summary>
        private ISelectable _parentControl;

        /// <summary>
        /// サブクラス化対象コントロール
        /// </summary>
        private IntPtr _target;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="parent">対象コントロール</param>
        /// <param name="target">サブクラス化対象ハンドル</param>
        public SelectableNativeWindow(Control parent,IntPtr target) 
        {
            var notSelectable = parent as ISelectable;
            if (notSelectable == null) throw new ArgumentException("ISelectableインターフェイスを実装している必要があります。");

            _target = target;

            parent.HandleCreated += new EventHandler(this.OnHandleCreated);
            parent.HandleDestroyed += new EventHandler(this.OnHandleDestroyed);

            this._parentControl = notSelectable; 
        } 

        /// <summary>
        /// AssignHandle
        /// </summary>
        /// <param name="sender">senderオブジェクト</param>
        /// <param name="e">イベントデータ</param>
        internal void OnHandleCreated(object sender, EventArgs e) 
        {
            AssignHandle(_target);
        }

        /// <summary>
        /// ReleaseHandle
        /// </summary>
        /// <param name="sender">senderオブジェクト</param>
        /// <param name="e">イベントデータ</param>
        internal void OnHandleDestroyed(object sender, EventArgs e)
        {
            ReleaseHandle();
        } 
        
        /// <summary>
        /// WndProcのオーバーライド
        /// </summary>
        /// <param name="m">Windows Message</param>
        protected override void WndProc(ref System.Windows.Forms.Message m)
        {
            if (_parentControl.DesignMode) 
            {
                base.WndProc(ref m);
                return;
            } 

            if (!_parentControl.Selectable)
            {
                // このあたりはお好みの挙動にあわせて、取ったり付けたりして利用しましょう
                if (m.Msg == (int)PlatformInvoke.WM_MOUSE.WM_LBUTTONDOWN) return;
                if (m.Msg == (int)PlatformInvoke.WM_MOUSE.WM_RBUTTONDOWN) return;
                if (m.Msg == (int)PlatformInvoke.WM_MOUSE.WM_MBUTTONDOWN) return;

                if (m.Msg == (int)PlatformInvoke.WM_MOUSE.WM_LBUTTONDBLCLK) return;
                if (m.Msg == (int)PlatformInvoke.WM_MOUSE.WM_RBUTTONDBLCLK) return;
                if (m.Msg == (int)PlatformInvoke.WM_MOUSE.WM_MBUTTONDBLCLK) return;

                if (m.Msg == (int)PlatformInvoke.WM_MOUSE.WM_MBUTTONDOWN) return;

                if (m.Msg == PlatformInvoke.WM_PASTE) return;

                if (m.Msg == PlatformInvoke.WM_SETFOCUS)
                {
                    PlatformInvoke.SetFocus(m.WParam);
                    return;
                }
            }
            base.WndProc(ref m);
        }
    } 
}


P/Invoke関連の実装

そのままの内容です。コメントなどをご覧ください。
これといった説明は特にありません。

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;

namespace ConsoleLibrary1
{
    [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    public static class PlatformInvoke
    {
        #region 定数
        /// <summary>
        /// フォーカスを取得のメッセージ
        /// </summary>
        public const uint WM_SETFOCUS = 0x0007;
        /// <summary>
        /// フォーカス喪失のメッセージ
        /// </summary>
        public const uint WM_KILLFOCUS = 0x0008;
        /// <summary>
        /// キーDownのメッセージ
        /// </summary>
        public const uint WM_KEYDOWN = 0x0100;
        /// <summary>
        /// キーUpのメッセージ
        /// </summary>
        public const uint WM_KEYUP = 0x0101;

        /// <summary>
        /// ペーストのメッセージ
        /// </summary>
        public const uint WM_PASTE = 0x302;
        #endregion

        #region 列挙型
        /// <summary>
        /// マウスに関するウィンドウメッセージ
        /// </summary>
        public enum WM_MOUSE : int
        {
            WM_MOUSEFIRST = 0x200,
            WM_MOUSEMOVE = 0x200,
            WM_LBUTTONDOWN = 0x201,
            WM_LBUTTONUP = 0x202,
            WM_LBUTTONDBLCLK = 0x203,
            WM_RBUTTONDOWN = 0x204,
            WM_RBUTTONUP = 0x205,
            WM_RBUTTONDBLCLK = 0x206,
            WM_MBUTTONDOWN = 0x207,
            WM_MBUTTONUP = 0x208,
            WM_MBUTTONDBLCLK = 0x209,
            WM_MOUSEWHEEL = 0x020A
        }   
        #endregion

        #region 構造体
        /// <summary>
        /// ComboBoxInfo構造体
        /// </summary>
        [StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
        public struct ComboBoxInfo 
        {
            public int Size;
            public Rectangle RectItem;
            public Rectangle RectButton;
            public int ButtonState;
            public IntPtr ComboBoxHandle;
            public IntPtr EditBoxHandle;
            public IntPtr ListBoxHandle;
        }
        #endregion

        #region DllImport

        /// <summary>
        /// ComboBoxの情報を取得します。
        /// </summary>
        /// <param name="comboBoxHandle">対象コンボボックスのハンドル</param>
        /// <param name="pComboBoxInfo">ComboBoxInfo構造体</param>
        /// <returns></returns>
        [SuppressUnmanagedCodeSecurity()]
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public extern static bool GetComboBoxInfo(
            IntPtr comboBoxHandle,
            ref ComboBoxInfo pComboBoxInfo
        );

        /// <summary>
        /// ウィンドウにメッセージを送信します。この関数は、
        /// 指定したウィンドウのウィンドウプロシージャが処理を終了するまで制御を返しません。
        /// メッセージを送信して直ちに制御を返すには、 SendMessageCallback 関数または SendNotifyMessage 関数を使います。
        /// メッセージをスレッドのメッセージキューにポストして直ちに制御を返すには、 
        /// PostMessage 関数または PostThreadMessage 関数を使います。
        /// </summary>
        /// <param name="hWnd">
        /// メッセージを受け取るウィンドウのハンドルを指定します。0xFFFF (HWND_BROADCAST) を指定すると、すべてのトップレベルウィンドウに送られます。
        /// 0xFFFF (HWND_BROADCAST) を指定すると、システムにあるすべてのトップレベルウィンドウに送られます。
        /// 子ウィンドウに対してはメッセージはメッセージは送られません。
        /// </param>
        /// <param name="Msg">
        /// 送信されるメッセージコードを指定します。
        /// </param>
        /// <param name="wParam">メッセージ固有情報(ウィンドウプロシージャの wParam パラメータ)を指定します。</param>
        /// <param name="lParam">メッセージ固有情報(ウィンドウプロシージャの lParam パラメータ)を指定します。</param>
        /// <returns>メッセージ処理の結果(ウィンドウプロシージャの戻り値)が返ります。戻り値は送られたメッセージによって異なります。</returns>
        [SuppressUnmanagedCodeSecurity()]
        [DllImport("User32.dll", EntryPoint = "SendMessageA", SetLastError = true)]
        private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

        /// <summary>
        /// 指定されたウィンドウを作成したスレッドに関連付けられているメッセージキューにメッセージをポストします。
        /// この関数は、メッセージの処理の完了を待たずに制御を返します。
        /// 
        /// スレッドに関連付けられているメッセージキューにメッセージをポストするには、 PostThreadMessage 関数を使います。
        /// </summary>
        /// <param name="hwnd">
        /// メッセージを受け取るウィンドウのハンドルを指定します。
        /// 0xFFFF (HWND_BROADCAST) を指定すると、システムにあるすべてのトップレベルウィンドウにポストされます。
        /// 子ウィンドウに対してはメッセージはメッセージはポストされません。
        /// 0 (NULL) を指定すると、 dwThreadId パラメータに現在のスレッド ID を設定して PostThreadMessage 関数を呼び出したかのように動作します。
        /// </param>
        /// <param name="wMsg">
        /// ポストされるメッセージコードを指定します。
        /// </param>
        /// <param name="wParam">メッセージ固有情報(ウィンドウプロシージャの wParam パラメータ)を指定します。</param>
        /// <param name="lParam">メッセージ固有情報(ウィンドウプロシージャの lParam パラメータ)を指定します。</param>
        /// <returns>
        /// 成功すると 0 以外の値が返ります。
        /// 失敗すると 0 が返ります。拡張エラー情報を取得するには、 GetLastError 関数を使います。
        /// </returns>
        [SuppressUnmanagedCodeSecurity()]
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private extern static IntPtr PostMessage(
            IntPtr hwnd, 
            uint wMsg, 
            IntPtr wParam, 
            IntPtr lParam
        );

        /// <summary>
        /// 指定されたウィンドウをキーボードフォーカスを持つウィンドウにします。
        /// 
        /// hWndが示すウィンドウは、この関数を呼び出したスレッドが持つウィンドウである必要があります。
        /// 他のスレッドが持つウィンドウに対するSetFocusの呼び出しは、効果がありません。 
        /// </summary>
        /// <param name="hwnd">対象のウィンドウハンドル</param>
        /// <returns></returns>
        [SuppressUnmanagedCodeSecurity()]
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public extern static IntPtr SetFocus(
            IntPtr hwnd
        );

        #endregion
    }
}

各コントロールでの実装例

WndProcをオーバーライドしていない点に注目してください。
また、このサンプルでは実装していませんが、
ReadOnlyプロパティ、TabStopプロパティ、BackColorプロパティ等々について、
Selectableプロパティの挙動と連動させる必要がある場合は、適宜実装しましょう。


拡張TexBoxの実装

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using ConsoleLibrary1.NativeWindow;

namespace ConsoleLibrary1
{
    /// <summary>
    /// 拡張テキストボックス
    /// </summary>
    public sealed partial class TextBoxEx : TextBox , ISelectable
    {
        #region コンストラクタ
        /// <summary>
        /// デフォルトコンストラクタ
        /// </summary>
        public TextBoxEx() : base() 
        {
            InitializeComponent();
            
        }
        #endregion

        #region プロパティ
        /// <summary>
        /// デザインモードか否かを取得します。
        /// </summary>
        public new bool DesignMode
        {
            get
            {
                bool design = base.DesignMode;
                Control parent = this.Parent;
                while (parent != null)
                {
                    ISite site = parent.Site;
                    if (site != null) design |= site.DesignMode;
                    parent = parent.Parent;
                }
                return design;
            }
        }
        #endregion

        #region ISelectable メンバ

        /// <summary>
        /// コントロールが選択可能であるか否かを取得または設定します。
        /// </summary>
        private bool _Selectable = true;
        [Category("動作")]
        [DefaultValue(true)]
        [Description("コントロールが選択可能であるか否かを取得または設定します。")]
        public bool Selectable 
        {
            get { return this._Selectable; }
            set { this._Selectable = value; }
        }

        private List<SelectableNativeWindow> _NativeWindowList = null;
        /// <summary>
        /// NativeWindowList
        /// </summary>
        public List<SelectableNativeWindow> NativeWindowList
        {
            set { _NativeWindowList = value; }
        }

        /// <summary>
        /// NativeWindowのAssignHandle対象ハンドルを取得します。
        /// </summary>
        /// <returns></returns>
        public IEnumerable<IntPtr> GetAssignHandles()
        {
            yield return this.Handle;
        }

        #endregion

        #region オーバーライド
        /// <summary>
        /// OnHandleCreated
        /// </summary>
        /// <param name="e"></param>
        protected override void OnHandleCreated(EventArgs e)
        {
            //ISelectable実装時に行うべき初期化処理
            this.InitializeSelectable();
            base.OnHandleCreated(e);
        }
        #endregion

    }
}


拡張ComboBoxの実装
ComboBoxの場合、内部的にもっているEditBoxおよびListBoxのハンドルを取得し、
それぞれに対して、NaitiveWindowを用いてWndProcをカプセル化する必要があります。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using ConsoleLibrary1.NativeWindow;

namespace ConsoleLibrary1
{
    /// <summary>
    /// 拡張コンボボックス
    /// </summary>
    public sealed partial class ComboBoxEx : ComboBox, ISelectable
    {
        #region メンバ
        /// <summary>
        /// ComboBox内のComboBoxInfo構造体
        /// </summary>
        PlatformInvoke.ComboBoxInfo _comboInfo = default(PlatformInvoke.ComboBoxInfo);
        #endregion

        #region コンストラクタ
        /// <summary>
        /// デフォルトコンストラクタ
        /// </summary>
        public ComboBoxEx()
            : base()
        {
            InitializeComponent();
        }
        #endregion

        #region プロパティ
        /// <summary>
        /// デザインモードか否かを取得します。
        /// </summary>
        public new bool DesignMode
        {
            get
            {
                bool design = base.DesignMode;
                Control parent = this.Parent;
                while (parent != null)
                {
                    ISite site = parent.Site;
                    if (site != null) design |= site.DesignMode;
                    parent = parent.Parent;
                }
                return design;
            }
        }
        #endregion

        #region ISelectable メンバ
        /// <summary>
        /// コントロールが選択可能であるか否かを取得または設定します。
        /// </summary>
        private bool _Selectable = true;
        [Category("動作")]
        [DefaultValue(true)]
        [Description("コントロールが選択可能であるか否かを取得または設定します。")]
        public bool Selectable
        {
            get { return this._Selectable; }
            set { this._Selectable = value; }
        }

        private List<SelectableNativeWindow> _NativeWindowList = null;
        /// <summary>
        /// NativeWindow
        /// </summary>
        public List<SelectableNativeWindow> NativeWindowList
        {
            set { _NativeWindowList = value; }
        }

        /// <summary>
        /// NativeWindowのAssignHandle対象ハンドルを取得します。
        /// </summary>
        /// <returns></returns>
        public IEnumerable<IntPtr> GetAssignHandles()
        {
            yield return this.Handle;
            _comboInfo.Size = Marshal.SizeOf(_comboInfo);
            PlatformInvoke.GetComboBoxInfo(this.Handle, ref _comboInfo);
            yield return _comboInfo.EditBoxHandle;
            yield return _comboInfo.ListBoxHandle;
        }

        #endregion

        #region オーバーライド
        /// <summary>
        /// OnHandleCreated
        /// </summary>
        /// <param name="e"></param>
        protected override void OnHandleCreated(EventArgs e)
        {
            //ISelectable実装時に行うべき初期化処理
            this.InitializeSelectable();
            base.OnHandleCreated(e);
        }
        #endregion
    }
}


拡張DateTimePickerでの実装
DateTimePickerの場合、内部的にもっているListBoxのハンドルを取得し、
それ対して、NaitiveWindowを用いてWndProcをカプセル化する必要があります。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using ConsoleLibrary1.NativeWindow;

namespace ConsoleLibrary1
{
    /// <summary>
    /// 拡張DateTimePicker
    /// </summary>
    public sealed partial class DateTimePickerEx : DateTimePicker, ISelectable
    {
        #region メンバ
        /// <summary>
        /// DateTimePicker内のComboBoxInfo構造体
        /// </summary>
        PlatformInvoke.ComboBoxInfo _comboInfo = default(PlatformInvoke.ComboBoxInfo);
        #endregion

        #region コンストラクタ
        /// <summary>
        /// デフォルトコンストラクタ
        /// </summary>
        public DateTimePickerEx()
            : base()
        {
            InitializeComponent();
        }
        #endregion

        #region プロパティ
        /// <summary>
        /// デザインモードか否かを取得します。
        /// </summary>
        public new bool DesignMode
        {
            get
            {
                bool design = base.DesignMode;
                Control parent = this.Parent;
                while (parent != null)
                {
                    ISite site = parent.Site;
                    if (site != null) design |= site.DesignMode;
                    parent = parent.Parent;
                }
                return design;
            }
        }
        #endregion

        #region ISelectable メンバ
        /// <summary>
        /// コントロールが選択可能であるか否かを取得または設定します。
        /// </summary>
        private bool _Selectable = true;
        [Category("動作")]
        [DefaultValue(true)]
        [Description("コントロールが選択可能であるか否かを取得または設定します。")]
        public bool Selectable
        {
            get { return this._Selectable; }
            set { this._Selectable = value; }
        }

        private List<SelectableNativeWindow> _NativeWindowList = null;
        /// <summary>
        /// NativeWindowList
        /// </summary>
        public List<SelectableNativeWindow> NativeWindowList
        {
            set { _NativeWindowList = value; }
        }

        /// <summary>
        /// NativeWindowのAssignHandle対象ハンドルを取得します。
        /// </summary>
        /// <returns></returns>
        public IEnumerable<IntPtr> GetAssignHandles()
        {
            yield return this.Handle;
            PlatformInvoke.GetComboBoxInfo(this.Handle, ref _comboInfo);
            yield return _comboInfo.ListBoxHandle;
        }

        #endregion

        #region オーバーライド
        /// <summary>
        /// OnHandleCreated
        /// </summary>
        /// <param name="e"></param>
        protected override void OnHandleCreated(EventArgs e)
        {
            //ISelectable実装時に行うべき初期化処理
            this.InitializeSelectable();
            base.OnHandleCreated(e);
        }
        #endregion
    }
}


以上です。このようにシンプルな実装で簡単に対応することが可能です。
それにつけてもおやつはカール、標準コントロールの空気の読めなさにはひどいものがあるのは事実。
みなさんどーしてらっしゃるんでしょうか。

*1:ありがた迷惑だ