Designing and Implement ButtonEdit Control for Windows Forms

 
Designing and Implement ButtonEdit Control
 
 
/ 黃忠成
 
 
What’s ButtonEdit Control
 
  在撰寫商用應用程式時,我們常常會制作一種介面,利用一個 TextBox 控件及一個 Button 控件,允許使用者按下 Button 後開啟一個視窗,於該視窗中選取所要的資料,方便查詢及減少輸入錯誤的情況。由於這種介面在商用程式常常出現,許多 3rd 控件廠商都會提供整合性的控件,將 TextBox Button 組合成為單一控件,以提供 ButtonClick 事件的方式,協助設計師設計此種介面,這種控件通常稱為 ButtonEdit
 
The Requirement
 
  需求上, ButtonEdit 控件是一個由 TextBox 控件及 Button 控件組合而成的複合性控件,其必須提供一個 ButtonClick 事件,允許設計師透過撰寫 ButtonClick 事件,在使用者按下按鈕後開出查詢視窗,供使用者挑選需要的資料。舉個實例來說,當使用者輸入訂單時,必須鍵入客戶編號,此時多數的商用程式都會選擇使用 ButtonEdi t 控件,預先制作一個內含一個DataGridView控件的Form,用來顯示所有的客戶,然後於ButtonEdit控件的ButtonClick事件中開啟此Form,當使用者於DataGridView控件中選取某筆資料時,將該客戶編號回填至ButtonEdit控件,如圖1所示。
圖1
Designing and Implement ButtonEdit Control for Windows Forms ButtonEdit 控件也可以當成一個更好的 ComboBox 控件來使用,允許使用者於按下按紐時,以下拉盒的方式,將視窗開在 ButtonEdit 控件下方,如圖 2
2
Designing and Implement ButtonEdit Control for Windows Forms
2 中, ButtonEdit 控件所拉出的視窗中放了一個 DataGridView 控件,允許使用者選取所要的資料,這個截圖同時也帶出了此種介面的強處,由於其拉出的是一個 Form ,這意味著任何可放入 Form 的控件,都可以用這種方式呈現。
 
Designing
 
  結構上, ButtonEdit 控件是由 TextBox Button 兩個控件所組成,這點可以利用 Windows Forms 所提供的 UserControl 模式來達到,不過多數的 3rd 控件廠商並不是這麼做的,他們選擇了較低階的方式,透過 Windows API 來達到,這種模式可以讓設計者得到更多的控制權,本文即是使用此種模式來開發 ButtonEdit 控件。
 
The Problem
 
  透過 Windows API 來開發 ButtonEdit 控件時,首先必須選擇該繼承何種既有控件,這個答案很明顯, ButtonEdit 控件是一種內含 Button 控件的 TextBox 控件,因此選擇 TextBox 類別做為繼承標的是當然的。第二個問題是 Button 控件該如何加到 TextBox 控件中?在 Windows 架構中,所有的 Window 皆可以擁有子 Window ,這意味著 TextBox 控件也可以擁有子控件,所以只要讓 Button 控件成為 TextBox 控件的子控件即可,以 Windows Forms 架構來看,只要呼叫 TextBox 控件的 Controls.Add 函式即可達到此目的。最後一個必須注意的問題是,一旦將 Button 控件變成 TextBox 的子控件後,那麼 TextBox 控件的文字輸入區域便會受到 Button 控件的覆蓋,簡略的說,原本可輸入 10 個字的 TextBo x 控件,會因為Button控件的加入,導致6個字後的輸入皆為不可見,這點,必須透過縮減TextBox控件中的文字輸入區域來解決。
 
Implement
 
  實作上,要解決的第一個問題是如何令 Button 控件成為 TextBox 控件的子控件,這點可透過 TextBox.Controls.Add 函式來完成,問題是,這個 Button 控件該如何選擇,內建的 Button 控件是一個可接收焦點的控件,當使用者點選時,焦點會到達此 Button 控件上,引發上一個取得焦點控件的 LostFocus 事件,將其應用於 ButtonEdit 控件上時,就會發生使用者按下內部的按鈕後,焦點由 ButtonEdit 控件中的 TextBox 區,移到了內部的 Button 控件上,連帶引發了 LostFocus 事件及 Validation 動作,這些都會造成 ButtonEdit 控件使用上的困擾。因此最好的情況是,自行開發一個不會引發焦點切離,也就是不接收焦點的 Button 控件,做為 ButtonEdit 控件所需的 Button 子控件,不過為了不增加本文的複雜度,此處仍然選擇使用內建的 Button 控件,待日後的文章中再以自定的 Button 控件來取代,另外,為了方便日後替換,這裡以内建的 Button 控件為基礎類別,設計了一個 OrpDropDownButton 控件。
程式 1
[ToolboxItem(false)]
public class OrpDropDownButton : Button
{
        protected override void OnEnter(EventArgs e)
        {
            base.OnEnter(e);
            if (Parent != null)
                Parent.Focus();
        }
 
        public OrpDropDownButton ()
            : base()
        {
            Image = LCBResource.DROPDOWNBTN1;
            ImageAlign = ContentAlignment.MiddleCenter;
        }
}
OrpDropDownButton ButtonEdit 控件內部的子控件,所以此處為她標上了 TooboxItem(false) 這個 Attribute ,這個動作可以讓此控件不會出現在 VS 2005 Toolbox Pattern 上。於建構子中, OrpDropDownButton 讀入了內建的 Bitmap 檔案,也就是一個往下的箭頭,如圖 3
3
Designing and Implement ButtonEdit Control for Windows Forms
理論上,當使用者點選 OrpDropDownButton 時,她不應該獲得焦點,所以此處覆載了 OnEnter 函式,在其取得焦點後,立即將焦點還給父控件,也就是 ButtonEdit 。完成了這個簡單的 Button 後,接下來是處理 ButtonEdit 控件中的文字輸入框,這裡有一個問題必須先解決,那就是前面所提及,如何裁切可輸入的文字寬度,避免因 OrpDropDownButton 在成為 ButtonEdit 控件的子控件後,導致部份的文字輸入不可見,這點必須依賴 Windows API SendMessage 函式,遞送一個 EM_SETRECT 訊息至 TextBox 控件,明確告知可輸入的文字區域。
程式 2
using System;
using System.Drawing;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
 
namespace LookupComboBox
{
    internal class NativeAPI
    {
        [Serializable, StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
 
            public RECT(int left_, int top_, int right_, int bottom_)
            {
                Left = left_;
                Top = top_;
                Right = right_;
                Bottom = bottom_;
            }
 
            public int Height { get { return Bottom - Top; } }
            public int Width { get { return Right - Left; } }
            public Size Size { get { return new Size(Width, Height); } }
 
            public Point Location { get { return new Point(Left, Top); } }
 
            // Handy method for converting to a System.Drawing.Rectangle
            public Rectangle ToRectangle()
            { return Rectangle.FromLTRB(Left, Top, Right, Bottom); }
 
            public static RECT FromRectangle(Rectangle rectangle)
            {
                return new RECT(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Bottom);
            }
 
            public override int GetHashCode()
            {
                return Left ^ ((Top << 13) | (Top >> 0x13))
                  ^ ((Width << 0x1a) | (Width >> 6))
                  ^ ((Height << 7) | (Height >> 0x19));
            }
 
            #region Operator overloads
 
            public static implicit operator Rectangle(RECT rect)
            {
                return Rectangle.FromLTRB(rect.Left, rect.Top, rect.Right, rect.Bottom);
            }
 
            public static implicit operator RECT(Rectangle rect)
            {
                return new RECT(rect.Left, rect.Top, rect.Right, rect.Bottom);
            }
 
            #endregion
        }
 
        public const uint EM_SETRECT = 0xb3;
        public const int WS_CLIPCHILDREN = 0x02000000;
        public const int WS_CLIPSIBLINGS = 0x04000000;
        public const int ES_MULTILINE = 0x0004;
 
        [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, ref RECT lParam);
    }
}
當遞送 EM_SETRECT 訊息至 TextBox 控件時,必須傳入一個 RECT 的結構體,由於 .NET Framework 並未提供 RECT 結構的定義,因此此處以 P/Invoke 的規範定義此結構。另外!當使用 EM_SETRECT 訊息時,該 TextBox 控件必須標示為 MULTILINE ,這有兩種方式可以達到,一是設定 TextBox 控件的 MultiLine 屬性為 True ,二是於建立 TextBox 控件時以 ES_MULTILINE 做為 Style 參數,此處採用第二種方式,見程式 3
程式 3
[ToolboxItem(false)]
    public class OrpCustomButtonEdit:TextBox
    {
        ..................
 
        protected override void CreateHandle()
        {
            CreateParams.Style = CreateParams.Style |
                                 NativeAPI.ES_MULTILINE |
                                NativeAPI.WS_CLIPCHILDREN |
                                 NativeAPI.WS_CLIPSIBLINGS;
            base.CreateHandle();
        }
 
        ............................
    }
覆載 CreateHandle 函式可以讓我們於 Windows Forms 建立 Control ,也就是 Windows UI 物件時,修改其 Style 定義。接下來是要處理將 OrpDropDownButton 控件變成 ButtonEdit 控件的子控件後的文字輸入區裁切動作,見程式 4
程式 4
[ToolboxItem(false)]
    public class OrpCustomButtonEdit:TextBox
    {
        private OrpDropDownButton _dropBtn = null;
 
        private void AdjustTextSize()
        {
            _dropBtn.Top = 0;
            _dropBtn.Left = Width - 20;
            _dropBtn.Height = Height - 5;
            _dropBtn.Width = 16;
            Rectangle rect = new Rectangle(0, 0, _dropBtn.Left-2,
ClientRectangle.Bottom - ClientRectangle.Top);
            NativeAPI.RECT r = NativeAPI.RECT.FromRectangle(rect);
            NativeAPI.SendMessage(Handle, NativeAPI.EM_SETRECT, (IntPtr)0, ref r);
        }
 
............................
 
        protected override void OnFontChanged(EventArgs e)
        {
            base.OnFontChanged(e);
            AdjustTextSize();
        }
 
        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);
            AdjustTextSize();
        }
 
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            AdjustTextSize();
        }
 
        protected override void OnEnter(EventArgs e)
        {
            base.OnEnter(e);
            AdjustTextSize();
        }
 
        protected override void InitLayout()
        {
            base.InitLayout();
            AdjustTextSize();
        }
 
        public OrpCustomButtonEdit()
            : base()
        {
.................
        }
    }
AdjustTextSize 函式負責裁切輸入區域,令其不會被內部的 OrpDropDownButton 控件所覆蓋。另外當 ButtonEdit 控件的字型、大小改變時,也意味著輸入區域必須重新計算,所以此處覆載了 FontChange Resize InitLayout OnEnter 函式,確保在 ButtonEdit 控件的部份屬性變動後,能重新裁切文字輸入區域。最後一個重要的部份是 OrpCustomButtonEdit 控件如何建立 OrpDropDownButton 控件,並令其成為 OrpCustomButtonEdit 的子控件,請見程式 5
程式 5
[ToolboxItem(false)]
    public class OrpCustomButtonEdit:TextBox
    {
        private OrpDropDownButton _dropBtn = null;
 
        .......................
 
        protected override void OnKeyDown(KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Return || e.KeyCode == Keys.F4)
                ButtonClick(this, EventArgs.Empty);
            else
                base.OnKeyDown(e);
        }
 
        protected virtual void EmbedButtonClick(EventArgs args)
        {
        }
 
        private void ButtonClick(object sender,EventArgs args)
        {
            EmbedButtonClick(args);
        }
 
        public OrpCustomButtonEdit()
            : base()
        {
            _dropBtn = new OrpDropDownButton();
            _dropBtn.Cursor = Cursors.Hand;
            _dropBtn.CausesValidation = false;
            _dropBtn.Click += new EventHandler(ButtonClick);
            _dropBtn.TabStop = false;
            Controls.Add(_dropBtn);
        }
    }
OrpCustomButtonEdit 控件在建立 OrpDropDownButton 控件後,設定了其 Cursor Custsors.Hand ,這使得使用者將滑鼠移到此按鈕上後,游標會顯示為 。接著將 CausesValidation 設為 False ,這關閉了當焦點移到此按鈕時,不會引發任何的 Validation 事件。然後將 TabStop 設為 False ,這避免當使用者於 OrpCustomButtonEdit 控件上按下 Tab 鍵時,焦點移到此 OrpDropDownButton 控件上,而是移到下一個可接受焦點的控件上。細心的讀者或許已經察覺, OrpCustomButtonEdit 控件被標上了 ToolboxItem(false) Attribute ,這意味著她不會出現在 VS 2005 Toolbox Pattern 上,同時 OrpCustomButtonEdit 控件也未開放 ButtonClick 事件,而是設計了一個虛擬函式: EmbedButtonClic k 這個設計的目的很簡單,就是將 OrpCustomButtonEdit 控件定義成一個基底類別,留下最大的彈性給子代類別,見程式 6
程式 6
[ToolboxItem(true)]
    public class OrpButtonEdit : OrpCustomButtonEdit
    {
        private static object _onButtonClick = new object();
 
        [Category("Behavior")]
        public virtual event EventHandler ButtonClick
        {
            add
            {
                Events.AddHandler(_onButtonClick, value);
            }
            remove
            {
                Events.RemoveHandler(_onButtonClick, value);
            }
        }
 
        protected virtual void OnButtonClick(EventArgs args)
        {
            EventHandler handler = (EventHandler)Events[_onButtonClick];
            if (handler != null)
                handler(this, args);
        }
 
        protected override void EmbedButtonClick(EventArgs args)
        {
            OnButtonClick(args);
        }
    }
OrpButtonEdit 控件才是本文最後的成品,讀者們或許會有點疑惑,為何如此大費週章將一個控件拆成兩階段完成,原因是延展性及易用性,並不是每一種 ButtonEdit 控件都需要 ButtonClick 事件,如前面所提及的以下拉盒方式,用 DataGridView 控件來讓使用者選取資料的介面,就不需要設計師來撰寫 ButtonClick 事件,只需要他們設定 DataSource 及欲顯示的欄位即可。面對這種應用,如果將 OrpButtonEdit 整合到 OrpCustomButtonEdit 後,設計師將會看到 ButtonClick 事件,這很容易引發誤用,尤其是在他們沒有原始碼的情況下。
 
OrpButtonEdit 的完整程式
using System;
using System.Drawing;
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Reflection;
 
namespace LookupComboBox
{
    [ToolboxItem(false)]
    public class OrpDropDownButton : Button
    {
        protected override void OnEnter(EventArgs e)
        {
            base.OnEnter(e);
            if (Parent != null)
                Parent.Focus();
        }
 
        public OrpDropDownButton()
            : base()
        {
            Image = LCBResource.DROPDOWNBTN1;
            ImageAlign = ContentAlignment.MiddleCenter;
        }
    }
 
    [ToolboxItem(false)]
    public class OrpCustomButtonEdit:TextBox
    {
        private OrpDropDownButton _dropBtn = null;
 
        private void AdjustTextSize()
        {
            _dropBtn.Top = 0;
            _dropBtn.Left = Width - 20;
            _dropBtn.Height = Height - 5;
            _dropBtn.Width = 16;
            Rectangle rect = new Rectangle(0, 0, _dropBtn.Left-2,
ClientRectangle.Bottom - ClientRectangle.Top);
            NativeAPI.RECT r = NativeAPI.RECT.FromRectangle(rect);
            NativeAPI.SendMessage(Handle, NativeAPI.EM_SETRECT, (IntPtr)0, ref r);
        }
 
        protected override void CreateHandle()
        {
            CreateParams.Style = CreateParams.Style |
                                 NativeAPI.ES_MULTILINE |
                                 NativeAPI.WS_CLIPCHILDREN |
                                 NativeAPI.WS_CLIPSIBLINGS;
            base.CreateHandle();
        }
 
        protected override void OnFontChanged(EventArgs e)
        {
            base.OnFontChanged(e);
            AdjustTextSize();
        }
 
        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);
            AdjustTextSize();
        }
 
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            AdjustTextSize();
        }
 
        protected override void OnEnter(EventArgs e)
        {
            base.OnEnter(e);
            AdjustTextSize();
        }
 
        protected override void InitLayout()
        {
            base.InitLayout();
            AdjustTextSize();
        }
 
        protected override void OnKeyDown(KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Return || e.KeyCode == Keys.F4)
                ButtonClick(this, EventArgs.Empty);
            else
                base.OnKeyDown(e);
        }
 
        protected virtual void EmbedButtonClick(EventArgs args)
        {
 
        }
 
        private void ButtonClick(object sender,EventArgs args)
        {
            EmbedButtonClick(args);
        }
 
        public OrpCustomButtonEdit()
            : base()
        {
            _dropBtn = new OrpDropDownButton();
            _dropBtn.Cursor = Cursors.Hand;
            _dropBtn.CausesValidation = false;
            _dropBtn.Click += new EventHandler(ButtonClick);
            _dropBtn.TabStop = false;
            Controls.Add(_dropBtn);
        }
    }
 
    [ToolboxItem(true)]
    public class OrpButtonEdit : OrpCustomButtonEdit
    {
        private static object _onButtonClick = new object();
 
        [Category("Behavior")]
        public virtual event EventHandler ButtonClick
        {
            add
            {
                Events.AddHandler(_onButtonClick, value);
            }
            remove
            {
                Events.RemoveHandler(_onButtonClick, value);
            }
        }
 
        protected virtual void OnButtonClick(EventArgs args)
        {
            EventHandler handler = (EventHandler)Events[_onButtonClick];
            if (handler != null)
                handler(this, args);
        }
 
        protected override void EmbedButtonClick(EventArgs args)
        {
            OnButtonClick(args);
        }
    }
}
 
What’s Next
 
  在計畫中, ButtonEdit 控件的設計會分成兩個階段,本文是第一階段,做出 ButtonEdit 的基礎及簡單應用,第二階段將引導讀者,撰寫前面所提及的以 DataGridView 來做出類似 ComboBox 控件的效果。


Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1138674


你可能感兴趣的:(windows)