Wednesday, January 17, 2007

Designing and Implement ButtonEdit Control

Designing and Implement ButtonEdit Control



文/黃忠成



What's ButtonEdit Control



在撰寫商用應用程式時,我們常常會制作一種介面,利用一個TextBox控件及一個Button控件,允許使用者按下Button後開啟一個視窗,於該視窗中選取所要的資料,方便查詢及減少輸入錯誤的情況。由於這種介面在商用程式常常出現,許多3rd控件廠商都會提供整合性的控件,將TextBox及Button組合成為單一控件,以提供ButtonClick事件的方式,協助設計師設計此種介面,這種控件通常稱為ButtonEdit。


The Requirement



需求上,ButtonEdit控件是一個由TextBox控件及Button控件組合而成的複合性控件,其必須提供一個ButtonClick事件,允許設計師透過撰寫ButtonClick事件,在使用者按下按鈕後開出查詢視窗,供使用者挑選需要的資料。舉個實例來說,當使用者輸入訂單時,必須鍵入客戶編號,此時多數的商用程式都會選擇使用ButtonEdit控件,預先制作一個內含一個DataGridView控件的Form,用來顯示所有的客戶,然後於ButtonEdit控件的ButtonClick事件中開啟此Form,當使用者於DataGridView控件中選取某筆資料時,將該客戶編號回填至ButtonEdit控件,如圖1所示。

圖1

ButtonEdit控件也可以當成一個更好的ComboBox控件來使用,允許使用者於按下按紐時,以下拉盒的方式,將視窗開在ButtonEdit控件下方,如圖2。

圖2


圖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個字的TextBox控件,會因為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


理論上,當使用者點選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事件,而是設計了一個虛擬函式:EmbedButtonClick,這個設計的目的很簡單,就是將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控件的效果。

像Component的Control

像Component的Control


當我第一次接觸到ToolStrip控件時,我很好奇,為何此控件的行為就像是Component一樣,意思是當你將其拖到Form上後,除了在Form上看到她外,你還可以在Component Tray上找到她,如下圖:


那這是如何達到的呢?在經過探索.NET Designer內部行為後,我發現到DocumentDesigner會針對ToolStrip控件做特別的處理,除了運行一般的控件處理外,DocumentDesigner最後會將ToolStrip加到ComponentTray中,這個動作使得ToolStrip控件可以出現在Component Tray(下方的元件區)中。好了!知道.NET Designer是如何達到這個效果了之後,接下來的問題是,我們自訂的控件也可以做到這點嗎?答案是肯定的,雖然DocumentDesigner屬於Form Designer層級,我們無法在不建立自訂Form的情況下改變其行為,但我們可以由ComponentDesigner下手,將該控件加到Component Tray中,要達到這點,第一件事是要取得ComponentTray的實體,這可透過以下的程式碼達到。

public
class
MyTextBoxDesigner : ControlDesigner

{


public
override
void Initialize(IComponent component)

{


IDesignerHost host = (IDesignerHost)component.Site.GetService(typeof(IDesignerHost));


ComponentTray tray = (ComponentTray)host.GetService(typeof(ComponentTray));


........................


base.Initialize(component);

}

}

程式中首先透過componet.Site.GetService函式來取得DesignerHost物件,透過這個物件,我們可以取得目前作用中的ComponentTray物件,然後就能將自訂的控件加到Component Tray中,這樣就能讓控件出現在Component Tray區中。

public
class
MyTextBoxDesigner : ControlDesigner

{


public
override
void Initialize(IComponent component)

{


...................


if (tray == null)

{


IComponent comp = host.CreateComponent(typeof(MyComponent));

tray = (ComponentTray)host.GetService(typeof(ComponentTray));


if (tray == null)


throw
new
SystemException("can't create component tray.");

tray.AddComponent(component);

host.DestroyComponent(comp);

}


else

tray.AddComponent(component);


base.Initialize(component);

}

}

由於ComponentTray物件在該Form上無任何Component的情況下,是不會被建立的,因此當ComponentTray未被建立時,程式會透過DesignHost物件來建立一個DummyComponent,這個DummyComponent是我們自訂的Component,其只是一個極為簡單、無作用的Component。

[ToolboxItem(false)]

public
class
DummyComponent : Component

{

}

當這個DummyComponent被加入後,ComponetTray物件就會被建立起來,剩下的工作就是將控件加到ComponentTray中,並移除DummyComponent,這樣一來,我們就完成了一個可出現於Component Tray中的控件了,以下是完整的程式碼。

using System;

using System.ComponentModel;

using System.ComponentModel.Design;

using System.Collections.Generic;

using System.Text;

using System.Windows.Forms;

using System.Windows.Forms.Design;


namespace ClassLibrary1

{

[Designer(typeof(MyTextBoxDesigner))]


public
class
MyTextBox:TextBox

{


private
string _information;



public
string Information

{


get

{


return _information;

}


set

{

_information = value;

}

}

}


[ToolboxItem(false)]


public
class
MyComponent : Component

{

}



public
class
MyTextBoxDesigner : ControlDesigner

{


public
override
void Initialize(IComponent component)

{


IDesignerHost host = (IDesignerHost)component.Site.GetService(typeof(IDesignerHost));


ComponentTray tray = (ComponentTray)host.GetService(typeof(ComponentTray));


if (tray == null)

{


IComponent comp = host.CreateComponent(typeof(DummyComponent));

tray = (ComponentTray)host.GetService(typeof(ComponentTray));


if (tray == null)


throw
new
SystemException("can't create component tray.");

tray.AddComponent(component);

host.DestroyComponent(comp);

}


else

tray.AddComponent(component);


base.Initialize(component);

}

}

}

下圖是執行結果。



x