[C#.NET][Winform][User Control] 使用 IExtenderProvider 擴充 Textbox 控制項 限制輸入字元 功能

[C#.NET][Winform][User Control] 使用 IExtenderProvider 擴充 Textbox 控制項 限制輸入字元 功能

相信這樣的功能我們常在寫,限制文字方塊輸入某些字元,比如下段程式碼只允許輸入數字鍵

private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
{
    //法一
    if (Char.IsNumber(e.KeyChar))
    {
        e.Handled = false;
    }
    else
    {
        e.Handled = true;
    }
    //法二
    int key = (int)e.KeyChar;
    if (key >= 48 && key <= 57)
    {
        e.Handled = false;
    }
    else
    {
        e.Handled = true;
    }
}

這樣的寫法應該很常出現在您我的專案,而且每次寫完都要重新測試過一遍,不然就是還要再查ASCII表,索性將這樣的功能包起來,

在此,我想要實現兩種模式,一種是內建的限制對應表,一種是可以由使用者自行加入ASCII Code,並且查詢當下按下的鍵盤屬於什麼ASCII Code

下圖是我心中內建定義表的操作方式

SNAGHTML792e71

下圖是我心中自定定義表的查詢功能

image

很好!如果你跟我想的一樣,就可以繼續往下看,如果你想學怎麼完成這樣的功能(雖然功能很鳥),但實作是很殘酷的,這個功能也花掉了我一天的時間。


外殼的作法我就不多講了跟前面幾篇一樣

[ProvideProperty("Filter", typeof(Control))]
public partial class FilterCharacterContainer : Component, IExtenderProvider
{
    public FilterCharacterContainer()
    {
        InitializeComponent();
    }
 
    public FilterCharacterContainer(IContainer container)
    {
        container.Add(this);
 
        InitializeComponent();
    }
 
    #region 實作 IExtenderProvider 成员
    public bool CanExtend(object target)
    {
        if (target is TextBox)
            return true;
        else
            return false;
    }
 
    #endregion
 
    Dictionary<Control, Filter> _Filters = new Dictionary<Control, Filter>();
    public void SetFilter(Control Ctrl, Filter Property)
    {
        if (Ctrl == null || Ctrl is FilterCharacterContainer || Ctrl is Form)
        {
            return;
        }
 
        if (this._Filters.ContainsKey(Ctrl))
        {
            this._Filters[Ctrl] = Property;
        }
        else
        {
            this._Filters.Add(Ctrl, Property);
        }
        if (Property.IsFilter)
        {
            Ctrl.KeyPress += new KeyPressEventHandler(Ctrl_KeyPress);
        }
        else
        {
            Ctrl.KeyPress -= new KeyPressEventHandler(Ctrl_KeyPress);
        }
    }
 
    public Filter GetFilter(Control Ctrl)
    {
        if (this._Filters.ContainsKey(Ctrl))
        {
            return this._Filters[Ctrl];
        }
        else
        {
            Filter property = new Filter()
            {
                IsFilter = false,
                DefaultASCIIs = new DefaultASCIIs(),
                CustomASCIIs = new CustomASCIIs(),
            };
 
            this._Filters.Add(Ctrl, property);
            return property;
        }
    }
}

我把對應表存放在Setting檔裡,本來想利用反射Char來處理,不過我一直沒辦法用反射取得IsNumber方法,只好用笨方法,建檔!

SNAGHTML8311ed

對應這些設定檔的列舉

[Serializable]
public enum ASCIIType
{
    Number,
    Lower,
    Upper,
    ESC,
    Backspace,
    Spack,
}

Set/Get要回傳的Filter類別以及相關類別

[Serializable, TypeConverter(typeof(ExpandableObjectConverter))]
public class DefaultASCII
{
    private ASCIIType _FilterType;
    public ASCIIType FilterType
    {
        get { return _FilterType; }
        set { _FilterType = value; }
    }
 
    private List<string> _FilterASCIIs;
    public ReadOnlyCollection<string> FilterASCIIs
    {
        get
        {
            switch (this.FilterType)
            {
                case ASCIIType.Number:
                    this._FilterASCIIs = Properties.Settings.Default.Numbers.Cast<string>().ToList();
                    break;
                case ASCIIType.Lower:
                    this._FilterASCIIs = Properties.Settings.Default.Lowers.Cast<string>().ToList();
                    break;
                case ASCIIType.Upper:
                    this._FilterASCIIs = Properties.Settings.Default.Uppers.Cast<string>().ToList();
                    break;
                case ASCIIType.ESC:
                    this._FilterASCIIs = Properties.Settings.Default.ESC.Cast<string>().ToList();
                    break;
                case ASCIIType.Backspace:
                    this._FilterASCIIs = Properties.Settings.Default.Backspace.Cast<string>().ToList();
                    break;
                case ASCIIType.Spack:
                    this._FilterASCIIs = Properties.Settings.Default.Spack.Cast<string>().ToList();
                    break;
            }
            return new ReadOnlyCollection<string>(this._FilterASCIIs);
        }
    }
}
 
[Serializable, TypeConverter(typeof(ExpandableObjectConverter))]
public class CustomASCII
{
    public string ASCII { get; internal set; }
    public string Char { get; internal set; }
 
    public override string ToString()
    {
        if (string.IsNullOrEmpty(this.Char) || string.IsNullOrEmpty(this.ASCII))
            return string.Format("");
        else
            return string.Format("Ascii:{0},Char:{1}", this.ASCII, this.Char);
    }
}
 
[Serializable, EditorAttribute(typeof(CollectionEditor), typeof(UITypeEditor))]
public class CustomASCIIs : List<CustomASCII>
{
 
}
 
[Serializable, EditorAttribute(typeof(CollectionEditor), typeof(UITypeEditor))]
public class DefaultASCIIs : List<DefaultASCII>
{
 
}
 
[Serializable, TypeConverter(typeof(ExpandableObjectConverter))]
public class Filter
{
    [DefaultValue(false)]
    public bool IsFilter { get; set; }
 
    private DefaultASCIIs _DefaultASCIIs=new DefaultASCIIs();
    public DefaultASCIIs DefaultASCIIs
    {
        get { return _DefaultASCIIs; }
        set { _DefaultASCIIs = value; }
    }
 
    private CustomASCIIs _CustomASCIIs = new CustomASCIIs();
    public CustomASCIIs CustomASCIIs
    {
        get { return _CustomASCIIs; }
        set { _CustomASCIIs = value; }
    }
 
    public override string ToString()
    {
        return string.Format("Is Filter:{0}", this.IsFilter);
    }
}

還有還有~回過頭去在外殼裡加上KeyPress事件

void Ctrl_KeyPress(object sender, KeyPressEventArgs e)
{
    TextBox ctrl = null;
    if (sender is Control)
        ctrl = sender as TextBox;
 
    if (ctrl == null || !this._Filters.ContainsKey(ctrl))
    {
        return;
    }
 
    bool isFilter = this._Filters[ctrl].IsFilter;
    if (!isFilter)
    {
        return;
    }
 
    DefaultASCIIs defaultList = this._Filters[ctrl].DefaultASCIIs;
    CustomASCIIs customList = this._Filters[ctrl].CustomASCIIs;
 
    int key = (int)e.KeyChar;
    List<string> ASCIIs = new List<string>();
    foreach (var item in defaultList)
    {
        foreach (var ascii in item.FilterASCIIs)
        {
            if (!ASCIIs.Contains(ascii))
            {
                ASCIIs.Add(ascii);
            }
        }
    }
    foreach (var item in customList)
    {
        if (!ASCIIs.Contains(item.ASCII))
        {
            ASCIIs.Add(item.ASCII);
        }
    }
 
    if (ASCIIs.Count == 0)
    {
        return;
    }
    if (ASCIIs.Contains(key.ToString()))
    {
        e.Handled = false;
    }
    else
    {
        e.Handled = true;
    }
}

到目前為止我們已經完成了預設定義功能,將IsFilter設為true,按下F5,就能依你想要的規則限制鍵盤輸入

SNAGHTML95a46c

喘口氣,喝杯茶,接下來要處理自定義的部份!


我們需要一個UserControl專案,並且規劃以下UI

SNAGHTML9070de

FilterScannerEditor 的程式碼長這樣,主要就是按下鍵盤後自動幫我們轉成ASCII Code,這樣一來就不用查表了,馬上按馬上加入限制規則

internal partial class FilterScannerEditor : UserControl
{
    public FilterScannerEditor()
    {
        InitializeComponent();
    }
 
    private CustomASCII _CustomASCII = new CustomASCII();
    public CustomASCII CustomASCII
    {
        get { return _CustomASCII; }
        set { _CustomASCII = value; }
    }
 
    private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
    {
        TextBox textBox = sender as TextBox;
        int key = (int)e.KeyChar;
 
        this.CustomASCII.ASCII = key.ToString();
        this.CustomASCII.Char = e.KeyChar.ToString();
 
        this.labelChar.Text = this.CustomASCII.ASCII;
        this.labelAscii.Text = this.CustomASCII.Char;
 
        textBox.Clear();
    }
 
    private void FilterScannerEditor_Load(object sender, EventArgs e)
    {
        this.labelChar.Text = "";
        this.labelAscii.Text = "";
    }
}

再來定義我們FilterASCIIScannerUITypeEditor類別,它繼承了UITypeEditor,詳細做法請參考http://www.dotblogs.com.tw/yc421206/archive/2010/07/04/16351.aspx

internal class FilterASCIIScannerUITypeEditor : UITypeEditor
{
    private CustomASCII _custom=new CustomASCII();
  
    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        IWindowsFormsEditorService editorService = null;
        if (context != null && context.Instance != null && provider != null)
        {
            editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
            FilterScannerEditor editor = new FilterScannerEditor();
            editorService.DropDownControl(editor);
            this._custom = editor.CustomASCII;
            return this._custom;
        }
        else
        {
            return null;
        }
    }
 
    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        if (context != null && context.Instance != null)
        {
            //return UITypeEditorEditStyle.Modal;
            return UITypeEditorEditStyle.DropDown;
            //return UITypeEditorEditStyle.None;
        }
        else
        {
            return base.GetEditStyle(context);
        }
    }
}

然後再回過頭去把CustomASCII類別的Attribute重新定義,加上我們剛寫好的FilterASCIIScannerUITypeEditor類別

[Serializable, TypeConverter(typeof(ExpandableObjectConverter))]
[EditorAttribute(typeof(FilterASCIIScannerUITypeEditor), typeof(UITypeEditor))]
public class CustomASCII
{
    public string ASCII { get; internal set; }
    public string Char { get; internal set; }
 
    public override string ToString()
    {
        if (string.IsNullOrEmpty(this.Char) || string.IsNullOrEmpty(this.ASCII))
            return string.Format("");
        else
            return string.Format("Ascii:{0},Char:{1}", this.ASCII, this.Char);
    }
}

如此一來便可以看到彈跳視窗,並實現我們想要的功能

image

SNAGHTMLaa3fa1


@故障排除

若你在開發時用來測試的Winform專案,會出錯,那是因為UserControl有修改並編譯,原本VS幫我們產生的Code並存放在Winform裡的.resx內容反射失敗,你可以把resx刪掉

image

然後,再刪掉VS幫我們產生的Code

image

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo