可改變大小組件ResizableComponent
實現原理
1、這裡將控件分成9個區域,上左、上中、上右、中左、中央、中右、下左、下中、下右。中央區域 被其他8個區域包圍形成一個虛擬的邊框。邊框的寬度可以自定義,中央區域不響應操作,其他8個區域可 以選擇性響應操作。
2、鼠標移動過程中檢測鼠標坐標。如果處在邊緣處,則根據不同的位置設置不同的改變大小的鼠標樣 式。
3、在鼠標按下事件中記錄下當前鼠標坐標
4、鼠標移動過程中,如果鼠標左鍵按下,則根據當前位置和之前記錄的位置計算位移
5、根據鼠標位移和鼠標所處的區域,調整控件的大小和位置
6、鼠標移開時恢復默認鼠標樣式
實現要點
1、內部控件可能覆蓋邊緣,內部控件也需要處理鼠標事件。和可移動組件一樣通過擴展屬性指示內部 控件是否允許響應操作。
2、可響應改變大小的位置可以自定義,實現自定義UITypeEditor,可視化設置。
3、向上或向右改變大小需要同時改變控件的位置,非對角線方向改變大小時要忽略與當前移動方向垂 直的位移。
下面介紹詳細的實現過程。
枚舉:
DirectionEnum:方向枚舉,All-所有方向,Horizontal-水平方向,Vertical-垂直方向。該枚舉在移 動操作和改變大小操作中都可以用到。
ResizeHandleAreaEnum:改變大小可處理區域枚舉,把需要處理改變大小的控件分成3*3的區域,除了 Center區域,其他區域都允許響應鼠標操作。該枚舉變量用自定義UITypeEditor進行編輯,後面再詳細介 紹。
MovableComponent組件的類圖和類詳細信息
MovableComponent組件包含5個屬性:
Enable:指示組件是否可用
EnableInnerControl:指示是否允許HandleControl控件的內部控件響應鼠標操作。
HandleControl:響應鼠標操作的控件,可以和被移動的控件不一致,一般是被移動控件內部的控件。
MovableControl:被移動的控件。
MoveableDirection:控件可以被移動的方向,默認為All,不限制移動方向。
該組件需要處理的鼠標事件有鼠標移入、鼠標按下、鼠標移動和鼠標離開,實現代碼如下:
/// <summary>
/// 鼠標離開事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void HandleControl_MouseLeave(object sender, EventArgs e)
{
if (this.m_Enable)
this.HandleControl.Cursor = Cursors.Default;
}
/// <summary>
/// 鼠標進入事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void HandleControl_MouseEnter(object sender, EventArgs e)
{
if (this.m_Enable)
{
switch (this.m_MovableDirection)
{
case DirectionEnum.All:
this.HandleControl.Cursor = Cursors.SizeAll;
break;
case DirectionEnum.Horizontal:
this.HandleControl.Cursor = Cursors.SizeWE;
break;
case DirectionEnum.Vertical:
this.HandleControl.Cursor = Cursors.SizeNS;
break;
default:
break;
}
}
}
/// <summary>
/// 之前的鼠標位置
/// </summary>
private Point m_PreviousLocation;
/// <summary>
/// 鼠標按下事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void HandleControl_MouseDown(object sender, MouseEventArgs e)
{
if (this.m_Enable)
m_PreviousLocation = Control.MousePosition;
}
/// <summary>
/// 鼠標移動事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void HandleControl_MouseMove(object sender, MouseEventArgs e)
{
if (this.m_Enable && e.Button == MouseButtons.Left && this.m_MovableControl != null)
{
Point PositionOffset = Control.MousePosition;
PositionOffset.Offset(-this.m_PreviousLocation.X, - this.m_PreviousLocation.Y);
int intNewX = this.m_MovableControl.Location.X + PositionOffset.X;
int intNewY = this.m_MovableControl.Location.Y + PositionOffset.Y;
switch (this.m_MovableDirection)
{
case DirectionEnum.All:
this.m_MovableControl.Location = new Point (intNewX, intNewY);
break;
case DirectionEnum.Horizontal:
this.m_MovableControl.Location = new Point (intNewX, this.m_MovableControl.Location.Y);
break;
case DirectionEnum.Vertical:
this.m_MovableControl.Location = new Point (this.m_MovableControl.Location.X, intNewY);
break;
default:
break;
}
m_PreviousLocation = Control.MousePosition;
}
}
另外為了實現擴展屬性,必須實現IExtenderProvider接口,關於IExtenderProvider接口的詳細介紹 請參考MSDN。這裡默認允許內部控件響應鼠標操作,只記錄不響應操作的內部控件。實現該接口後還要在 組件上添加特性,格式為[ProvideProperty("HandleMove", typeof(Control))]。將組件放到窗體上,設 置好HandleControl之後,就可以看到HandleControl的內部控件都會增加一個movableComponent1 上的 HandleMove屬性,和ToolTip控件類似。
該接口的實現如下:
/// <summary>
/// 不響應操作的控件的列表
/// </summary>
private List<Control> m_NoHandleControls = new List<Control> ();
/// <summary>
/// IExtenderProvider成員方法-是否可擴展
/// </summary>
public bool CanExtend(object extendee)
{
if (m_HandleControl != null && IsContainSubControl (m_HandleControl, extendee as Control))
return true;
else
return false;
}
/// <summary>
/// 是否包含下級控件
/// </summary>
/// <param name="Parent">上級控件</param>
/// <param name="Child">下級控件</param>
/// <returns></returns>
private bool IsContainSubControl(Control Parent, Control Child)
{
bool blnResult = false;
if (Parent == null || Child == null)
blnResult = false;
else
{
if (Parent.Controls.Contains(Child))
blnResult = true;
else
{
foreach (Control item in Parent.Controls)
{
if (IsContainSubControl(item, Child))
{
blnResult = true;
break;
}
}
}
}
return blnResult;
}
/// <summary>
/// IExtenderProvider成員方法-設置響應移動屬性
/// </summary>
public void SetHandleMove(Control control, bool value)
{
if (value)
{
if (m_NoHandleControls.Contains(control))
m_NoHandleControls.Remove(control);
}
else
{
if (!m_NoHandleControls.Contains(control))
m_NoHandleControls.Add(control);
}
}
/// <summary>
/// 成員方法-獲取響應移動屬性
/// </summary>
[DefaultValue(true)]
[Description("指示控件是否響應改變位置操作。")]
public bool GetHandleMove(Control control)
{
if (m_HandleControl != null && (control == this.m_HandleControl || IsContainSubControl(m_HandleControl, control)))
{
if (this.m_NoHandleControls.Contains(control))
return false;
else
return true;
}
else
return false;
}
實現IExtenderProvider接口後,將組件拖放到窗體上,設置相關HandleControl之後,則會為其內部 控件增加HandleMove屬性,效果如下圖:
下面介紹ResizableComponent可改變大小組件的實現(類圖和類詳細信息)。
ResizableComponent組件的屬性有:
Enable:指示組件是否可用
EnableInnerControl:當內部控件覆蓋目標可縮放控件的邊緣時,是否允許內部控件響應鼠標改變大 小操作
MinSize:可縮放控件可以調整的最小尺寸
ResizableControl:目標可改變大小的控件
ResizeBorderWidth:響應改變大小操作的邊框寬度,對應可縮放控件的內部虛擬邊框,當鼠標移動到 這一個虛擬邊框中會改變樣式
ResizeDirection:可改變大小的方向,水平、垂直和不限制
ResizeHandleAreas:響應改變大小操作的控制區域,用自定義UITypeEditor實現。效果如下圖所示:
該組件處理目標控件的三個鼠標事件,MouseMove、MouseLeave和MouseDown。
MouseMove處理方法中,檢測鼠標的坐標所處的區域,然後根據區域和允許調整大小的方向設置不同的 鼠標樣式。
如果鼠標左鍵按下,則檢測鼠標的位移量,再根據所處的區域調整控件的大小和位置。
MouseDown處理方法中,記錄下鼠標的位置,供調整大小時計算位移量。
MouseLeave處理方法中,恢復鼠標樣式。
/// <summary>
/// 鼠標按下事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SizableControl_MouseDown(object sender, MouseEventArgs e)
{
if (!m_Enable)
return;
m_ResizeOriginalPoint = Control.MousePosition;
}
/// <summary>
/// 鼠標移動事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SizableControl_MouseMove(object sender, MouseEventArgs e)
{
if (!m_Enable)
return;
if (e.Button == MouseButtons.None)
{
this.CheckMousePoint(sender as Control, e.Location);
return;
}
if (e.Button != MouseButtons.Left)
return;
Point OffsetPoint = Control.MousePosition;
OffsetPoint.Offset(-m_ResizeOriginalPoint.X, - m_ResizeOriginalPoint.Y);
switch (m_HandleArea)
{
case ResizeHandleAreaEnum.TopLeft:
this.SetControlBound(OffsetPoint.X, OffsetPoint.Y, - OffsetPoint.X, -OffsetPoint.Y);
break;
case ResizeHandleAreaEnum.TopCenter:
this.SetControlBound(0, OffsetPoint.Y, 0, - OffsetPoint.Y);
break;
case ResizeHandleAreaEnum.TopRight:
this.SetControlBound(0, OffsetPoint.Y, OffsetPoint.X, -OffsetPoint.Y);
break;
case ResizeHandleAreaEnum.CenterLeft:
this.SetControlBound(OffsetPoint.X, 0, - OffsetPoint.X, 0);
break;
case ResizeHandleAreaEnum.CenterRight:
this.SetControlBound(0, 0, OffsetPoint.X, 0);
break;
case ResizeHandleAreaEnum.BottomLeft:
this.SetControlBound(OffsetPoint.X, 0, - OffsetPoint.X, OffsetPoint.Y);
break;
case ResizeHandleAreaEnum.BottomCenter:
this.SetControlBound(0, 0, 0, OffsetPoint.Y);
break;
case ResizeHandleAreaEnum.BottomRight:
this.SetControlBound(0, 0, OffsetPoint.X, OffsetPoint.Y);
break;
case ResizeHandleAreaEnum.Center:
default:
break;
}
this.m_ResizeOriginalPoint = Control.MousePosition;
}
/// <summary>
/// 鼠標離開事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SizableControl_MouseLeave(object sender, EventArgs e)
{
if (!m_Enable)
return;
(sender as Control).Cursor = Cursors.Default;
this.m_ResizableControl.Cursor = Cursors.Default;
}
其他方法都是輔助檢測和調整坐標用的。下面介紹如何實現自定義的UITypeEditor。這裡定義了一個 枚舉ResizeHandleAreaEnum,用來標識調整大小的區域。因為設置的響應操作的區域允許有多個,所以這 些枚舉值必須都是2的次方數,在二進制中表示則都只有一位是1的,這樣就可以通過位操作來解析值了。
/// <summary>
/// 改變大小控制區域枚舉
/// </summary>
[Flags]
[Serializable]
[Editor(typeof(ResizeHandleAreaUITypeEditor), typeof (System.Drawing.Design.UITypeEditor))]
public enum ResizeHandleAreaEnum
{
/// <summary>
/// 中央區域,不響應操作
/// </summary>
Center = 0,
/// <summary>
/// 頂端靠左
/// </summary>
TopLeft = 1,
/// <summary>
/// 頂端居中
/// </summary>
TopCenter = 2,
/// <summary>
/// 頂端靠右
/// </summary>
TopRight = 4,
/// <summary>
/// 中間靠左
/// </summary>
CenterLeft = 8,
/// <summary>
/// 中間靠右
/// </summary>
CenterRight = 16,
/// <summary>
/// 底部靠左
/// </summary>
BottomLeft = 32,
/// <summary>
/// 底部居中
/// </summary>
BottomCenter = 64,
/// <summary>
/// 底部靠右
/// </summary>
BottomRight = 128,
}
枚舉定義好之後,在項目中添加一個自定義控件,在其中放置8個CheckBox,設置Appearance屬性為 Button外觀。然後排布為虛擬邊框的效果,如下圖:
該控件主要是將ResizeHandleAreaEnum枚舉值和CheckBox控件的選中狀態對應起來,通過位操作來解 析和設置響應操作的區域枚舉,內部代碼如下:
//原始響應區域
private ResizeHandleAreaEnum m_OldAears;
/// <summary>
/// 改變大小的響應區域枚舉
/// </summary>
public ResizeHandleAreaEnum ResizeHandleAreas
{
get
{
ResizeHandleAreaEnum Areas = ResizeHandleAreaEnum.Center;
if (chkTopLeft.Checked)
Areas |= ResizeHandleAreaEnum.TopLeft;
if (chkTopCenter.Checked)
Areas |= ResizeHandleAreaEnum.TopCenter;
if (chkTopRight.Checked)
Areas |= ResizeHandleAreaEnum.TopRight;
if (chkCenterLeft.Checked)
Areas |= ResizeHandleAreaEnum.CenterLeft;
if (chkCenterRight.Checked)
Areas |= ResizeHandleAreaEnum.CenterRight;
if (chkBottomLeft.Checked)
Areas |= ResizeHandleAreaEnum.BottomLeft;
if (chkBottomCenter.Checked)
Areas |= ResizeHandleAreaEnum.BottomCenter;
if (chkBottomRight.Checked)
Areas |= ResizeHandleAreaEnum.BottomRight;
if (Areas == ResizeHandleAreaEnum.Center)
return m_OldAears;
else
return Areas;
}
}
/// <summary>
/// 設置響應改變大小的區域
/// </summary>
/// <param name="ResizeHandleArea"></param>
public void SetValue(ResizeHandleAreaEnum ResizeHandleArea)
{
m_OldAears = ResizeHandleArea;
chkTopLeft.Checked = ((m_OldAears & ResizeHandleAreaEnum.TopLeft) != 0);
chkTopCenter.Checked = ((m_OldAears & ResizeHandleAreaEnum.TopCenter) != 0);
chkTopRight.Checked = ((m_OldAears & ResizeHandleAreaEnum.TopRight) != 0);
chkCenterLeft.Checked = ((m_OldAears & ResizeHandleAreaEnum.CenterLeft) != 0);
chkCenterRight.Checked = ((m_OldAears & ResizeHandleAreaEnum.CenterRight) != 0);
chkBottomLeft.Checked = ((m_OldAears & ResizeHandleAreaEnum.BottomLeft) != 0);
chkBottomCenter.Checked = ((m_OldAears & ResizeHandleAreaEnum.BottomCenter) != 0);
chkBottomRight.Checked = ((m_OldAears & ResizeHandleAreaEnum.BottomRight) != 0);
}
為了讓該枚舉值在PropertyGrid中編輯時顯示自定義的UI界面,需要繼承UITypeEditor類,關於 UITypeEditor的具體介紹請參考MSDN,這裡的實現代碼如下:
internal class ResizeHandleAreaUITypeEditor : UITypeEditor
{
private ResizeHandleAreaEditorControl m_EditorControl = null;
public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.DropDown;
}
public override object EditValue (ITypeDescriptorContext context, IServiceProvider provider, object value)
{
if (m_EditorControl == null)
m_EditorControl = new ResizeHandleAreaEditorControl ();
m_EditorControl.SetValue((ResizeHandleAreaEnum) value);
IWindowsFormsEditorService edSvc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
edSvc.DropDownControl(m_EditorControl);
return m_EditorControl.ResizeHandleAreas;
}
}
在該枚舉上添加Editor特性[Editor(typeof(ResizeHandleAreaUITypeEditor), typeof (System.Drawing.Design.UITypeEditor))],之後只要使用到該屬性,在PropertyGrid中顯示的就是UI編 輯界面。
ResizableComponent組件也用到了擴展屬性,和上面的MovableComponent組件的實現方法類似,這裡 不再介紹。
示例中用一個下拉框調用一個浮動層,浮動層的標題欄可以拖動,但標題欄邊框不響應改變大小操作 。因為將標題欄的相關控件的HandleResize屬性設置為了False,否則會造成移動的同時改變大小。要實 現本篇開頭給出的多列下拉框的效果,可以做一個自定義控件,然後綁定數據源即可。至於數據項的綁定 ,會在以後的示例中介紹到。
另外這裡有個小bug,當快速移動鼠標時,改變大小和移動操作都會產生滯後的效果,希望有解決方法 的朋友留言。