用户可以拖放和调整大小的停靠控件






4.68/5 (17投票s)
2001年10月23日
6分钟阅读

239338

1010
此控件允许用户调整大小并将您的停靠控件拖动到不同的窗体边缘。
引言
C# 最早吸引我的特性之一是能够将 Control
Dock
到 Form
的边缘。现在我可以将一个 Control
(或更可能是一个派生自 UserControl
的复合控件)附加到 Form
边缘,并快速构建一个看起来有用的应用程序。但是,在这种情况下,缺少了一个关键因素。用户无法自行决定此停靠控件的大小或位置。我希望用户能够将控件拖动到不同的边缘,并能够调整控件的大小,以便他们可以根据自己的偏好来自定义应用程序区域。
用于停靠的复合控件
为了解决这个问题,我们需要创建一个新的复合控件 DockingControl
,它能够接收调用者提供的控件并管理其位置和大小。我们的复合控件需要一个调整大小栏;一个可用于移动其停靠位置的抓手区域;以及一个用于显示调用者提供的控件的位置。
class DockingControl : UserControl
{
private Form _form;
private DockingResize _resize;
private DockingHandle _handle;
private BorderControl _wrapper;
public DockingControl(Form form, DockStyle ds, Control userControl)
{
// Remember the form we are hosted on
_form = form;
// Create the resizing bar, gripper handle and border control
_resize = new DockingResize(ds);
_handle = new DockingHandle(this, ds);
_wrapper = new BorderControl(userControl);
// Wrapper should always fill remaining area
_wrapper.Dock = DockStyle.Fill;
// Define our own initial docking position for when we are added to
// host form
this.Dock = ds;
Controls.AddRange(new Control[]{_wrapper, _handle, _resize});
}
public Form HostForm { get { return _form; } }
实例构造函数中的最后一行代码会添加三个子控件 _wrapper
、_handle
和 _resize
。控件在初始化列表中的顺序至关重要,因为当 DockingControl
(或任何其他 Control
)的 Dock
样式发生更改时,此顺序决定了子控件的位置和大小。计算从最后添加的控件(等同于初始化列表中的最后一个条目)开始,向第一个控件回溯,这与我预期的完全相反。
_resize
栏首先被定位(因此在初始化列表中最后),因为它应该始终显示以跨越停靠控件的整个长度。接下来是 _handle
,因为它应该定位在调整大小栏下方。最后是 _wrapper
控件,它排在最后,因为它始终具有 Fill
的 Dock
样式,并且我们希望它占据其他控件布局完成后剩余的空间。
停靠位置更改
当我们的复合控件的停靠位置发生更改时,我们需要确保我们的子控件也为新的停靠样式正确地定位。因此,我们 override
了继承的 Dock
属性,并根据需要重新计算正确的大小和位置。
// Override the base class property to allow extra work
public override DockStyle Dock
{
get { return base.Dock; }
set
{
// Our size before docking position is changed
Size size = this.ClientSize;
// Remember the current docking position
DockStyle dsOldResize = _resize.Dock;
// New handle size is dependant on the orientation of the new
// docking position
_handle.SizeToOrientation(value);
// Modify docking position of child controls based on our new
// docking position
_resize.Dock = DockingControl.ResizeStyleFromControlStyle(value);
_handle.Dock = DockingControl.HandleStyleFromControlStyle(value);
// Now safe to update ourselves through base class
base.Dock = value;
// Change in orientation occurred?
if (dsOldResize != _resize.Dock)
{
// Must update our client size to ensure the correct size is
// used when the docking position changes. We have to
// transfer the value that determines the vector of the
// control to the opposite dimension
if ((this.Dock == DockStyle.Top) ||
(this.Dock == DockStyle.Bottom))
size.Height = size.Width;
else
size.Width = size.Height;
this.ClientSize = size;
}
// Repaint our controls
_handle.Invalidate();
_resize.Invalidate();
}
}
两个 static
函数(ResizeStyleFromControlStyle
和 HandleStyleFromControlStyle
)用于查找 _resize
和 _handle
控件的正确停靠样式,这取决于新的 Dock
样式。特别需要注意的是检查停靠方向变化的代码,然后更改控件的 Width
或 Height
。请记住,当我们的控件停靠在窗体的顶部或底部时,窗体会为我们计算控件的 Width
,而 Height
决定了停靠控件向内延伸的距离。当方向移动到左侧或右侧时,我们需要更新控件的 Width
以反映我们希望控件从边缘延伸的距离。因此,新的 Width
应该等于旧的 Height
,否则 Width
将保持不变,即窗体的整个宽度,因此它将填充整个客户端区域。
DockingControl
类的其余部分遵循此模式,包括用于恢复 GDI+ 对象(供子控件绘图使用)的 static
属性,以及前面提到的 static
方法,用于根据 DockingControl
的新位置计算每个子控件的新停靠位置。
// Static variables defining colors for drawing
private static Pen _lightPen =
new Pen(Color.FromKnownColor(KnownColor.ControlLightLight));
private static Pen _darkPen =
new Pen(Color.FromKnownColor(KnownColor.ControlDark));
private static Brush _plainBrush = Brushes.LightGray;
// Static properties for read-only access to drawing colors
public static Pen LightPen { get { return _lightPen; } }
public static Pen DarkPen { get { return _darkPen; } }
public static Brush PlainBrush { get { return _plainBrush; } }
public static DockStyle ResizeStyleFromControlStyle(DockStyle ds)
{
switch(ds)
{
case DockStyle.Left:
return DockStyle.Right;
case DockStyle.Top:
return DockStyle.Bottom;
case DockStyle.Right:
return DockStyle.Left;
case DockStyle.Bottom:
return DockStyle.Top;
default:
// Should never happen!
throw new ApplicationException("Invalid DockStyle argument");
}
}
public static DockStyle HandleStyleFromControlStyle(DockStyle ds)
{
switch(ds)
{
case DockStyle.Left:
return DockStyle.Top;
case DockStyle.Top:
return DockStyle.Left;
case DockStyle.Right:
return DockStyle.Top;
case DockStyle.Bottom:
return DockStyle.Left;
default:
// Should never happen!
throw new ApplicationException("Invalid DockStyle argument");
}
}
}
调整大小
我们的第一个子控件名为 DockingResize
,它提供了一个供用户拖动进行调整大小的停靠控件区域。请注意,当鼠标被单击时,OnMouseDown
会记住父 DockingControl
的当前大小以及鼠标的屏幕位置。这很有必要,以便在收到 OnMouseMove
时,它能够计算自按下鼠标以来鼠标移动的距离,从而确定 DockingControl
的新大小。还要注意,它会将光标设置为允许调整大小的指示。
// A bar used to resize the parent DockingControl
class DockingResize : UserControl
{
// Class constants
private const int _fixedLength = 4;
// Instance variables
private Point _pointStart;
private Point _pointLast;
private Size _size;
public DockingResize(DockStyle ds)
{
this.Dock = DockingControl.ResizeStyleFromControlStyle(ds);
this.Size = new Size(_fixedLength, _fixedLength);
}
protected override void OnMouseDown(MouseEventArgs e)
{
// Remember the mouse position and client size when capture occurred
_pointStart = _pointLast = PointToScreen(new Point(e.X, e.Y));
_size = Parent.ClientSize;
// Ensure delegates are called
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e)
{
// Cursor depends on if we a vertical or horizontal resize
if ((this.Dock == DockStyle.Top) ||
(this.Dock == DockStyle.Bottom))
this.Cursor = Cursors.HSplit;
else
this.Cursor = Cursors.VSplit;
// Can only resize if we have captured the mouse
if (this.Capture)
{
// Find the new mouse position
Point point = PointToScreen(new Point(e.X, e.Y));
// Have we actually moved the mouse?
if (point != _pointLast)
{
// Update the last processed mouse position
_pointLast = point;
// Find delta from original position
int xDelta = _pointLast.X - _pointStart.X;
int yDelta = _pointLast.Y - _pointStart.Y;
// Resizing from bottom or right of form means inverse
// movements
if ((this.Dock == DockStyle.Top) ||
(this.Dock == DockStyle.Left))
{
xDelta = -xDelta;
yDelta = -yDelta;
}
// New size is original size plus delta
if ((this.Dock == DockStyle.Top) ||
(this.Dock == DockStyle.Bottom))
Parent.ClientSize = new Size(_size.Width,
_size.Height + yDelta);
else
Parent.ClientSize = new Size(_size.Width + xDelta,
_size.Height);
// Force a repaint of parent so we can see changed appearance
Parent.Refresh();
}
}
// Ensure delegates are called
base.OnMouseMove(e);
}
此类中唯一其他需要的工作是重写 OnPaint
,用于绘制调整大小栏本身的三维外观。它使用 DockingControl
中的 static
方法来恢复要使用的正确 GDI+ 对象。
protected override void OnPaint(PaintEventArgs pe)
{
// Create objects used for drawing
Point[] ptLight = new Point[2];
Point[] ptDark = new Point[2];
Rectangle rectMiddle = new Rectangle();
// Drawing is relative to client area
Size sizeClient = this.ClientSize;
// Painting depends on orientation
if ((this.Dock == DockStyle.Top) ||
(this.Dock == DockStyle.Bottom))
{
// Draw as a horizontal bar
ptDark[1].Y = ptDark[0].Y = sizeClient.Height - 1;
ptLight[1].X = ptDark[1].X = sizeClient.Width;
rectMiddle.Width = sizeClient.Width;
rectMiddle.Height = sizeClient.Height - 2;
rectMiddle.X = 0;
rectMiddle.Y = 1;
}
else if ((this.Dock == DockStyle.Left) ||
(this.Dock == DockStyle.Right))
{
// Draw as a vertical bar
ptDark[1].X = ptDark[0].X = sizeClient.Width - 1;
ptLight[1].Y = ptDark[1].Y = sizeClient.Height;
rectMiddle.Width = sizeClient.Width - 2;
rectMiddle.Height = sizeClient.Height;
rectMiddle.X = 1;
rectMiddle.Y = 0;
}
// Use colors defined by docking control that is using us
pe.Graphics.DrawLine(DockingControl.LightPen, ptLight[0], ptLight[1]);
pe.Graphics.DrawLine(DockingControl.DarkPen, ptDark[0], ptDark[1]);
pe.Graphics.FillRectangle(DockingControl.PlainBrush, rectMiddle);
// Ensure delegates are called
base.OnPaint(pe);
}
}
拖动
我们的下一个子控件 DockingHandle
有三个任务需要执行。首先,它必须确保其大小正确,以反映父 DockingControl
的当前方向。由于我们停靠在父控件的一个边缘,因此我们的一个尺寸将总是被计算出来。但是,另一个尺寸应始终固定,以反映绘图所需的空间并允许用户抓取它。SizeToOrientation
例程执行此决策。
class DockingHandle : UserControl
{
// Class constants
private const int _fixedLength = 12;
private const int _hotLength = 20;
private const int _offset = 3;
private const int _inset = 3;
// Instance variables
private DockingControl _dockingControl = null;
public DockingHandle(DockingControl dockingControl, DockStyle ds)
{
_dockingControl = dockingControl;
this.Dock = DockingControl.HandleStyleFromControlStyle(ds);
SizeToOrientation(ds);
}
public void SizeToOrientation(DockStyle ds)
{
if ((ds == DockStyle.Top) || (ds == DockStyle.Bottom))
this.ClientSize = new Size(_fixedLength, 0);
else
this.ClientSize = new Size(0, _fixedLength);
}
第二个任务,也是最有趣的任务,是在 OnMouseMove
中完成的。在这里,我们需要将鼠标位置从我们自己的客户端位置转换为主机窗体中的客户端位置。通过测试光标离窗体每个边缘的距离,我们可以决定哪个边缘应该成为父 DockingControl
的新停靠位置。目前,代码使用常量值 _hotLength
来决定鼠标是否足够靠近某个边缘以便更改停靠边缘。实际上引起停靠更改非常简单,只需更改 DockingControl
上的 Dock
属性即可。
protected override void OnMouseMove(MouseEventArgs e)
{
// Can only move the DockingControl is we have captured the
// mouse otherwise the mouse is not currently pressed
if (this.Capture)
{
// Must have reference to parent object
if (null != _dockingControl)
{
this.Cursor = Cursors.Hand;
// Convert from client point of DockingHandle to client of
// DockingControl
Point screenPoint = PointToScreen(new Point(e.X, e.Y));
Point parentPoint =
_dockingControl.HostForm.PointToClient(screenPoint);
// Find the client rectangle of the form
Size parentSize = _dockingControl.HostForm.ClientSize;
// New docking position is defaulted to current style
DockStyle ds = _dockingControl.Dock;
// Find new docking position
if (parentPoint.X < _hotLength)
{
ds = DockStyle.Left;
}
else if (parentPoint.Y < _hotLength)
{
ds = DockStyle.Top;
}
else if (parentPoint.X >= (parentSize.Width - _hotLength))
{
ds = DockStyle.Right;
}
else if (parentPoint.Y >= (parentSize.Height - _hotLength))
{
ds = DockStyle.Bottom;
}
// Update docking position of DockingControl we are part of
if (_dockingControl.Dock != ds)
_dockingControl.Dock = ds;
}
}
else
this.Cursor = Cursors.Default;
// Ensure delegates are called
base.OnMouseMove(e);
}
最后,该控件需要绘制装饰控件区域的两条线。
protected override void OnPaint(PaintEventArgs pe)
{
Size sizeClient = this.ClientSize;
Point[] ptLight = new Point[4];
Point[] ptDark = new Point[4];
// Depends on orientation
if ((_dockingControl.Dock == DockStyle.Top) ||
(_dockingControl.Dock == DockStyle.Bottom))
{
int iBottom = sizeClient.Height - _inset - 1;
int iRight = _offset + 2;
ptLight[3].X = ptLight[2].X = ptLight[0].X = _offset;
ptLight[2].Y = ptLight[1].Y = ptLight[0].Y = _inset;
ptLight[1].X = _offset + 1;
ptLight[3].Y = iBottom;
ptDark[2].X = ptDark[1].X = ptDark[0].X = iRight;
ptDark[3].Y = ptDark[2].Y = ptDark[1].Y = iBottom;
ptDark[0].Y = _inset;
ptDark[3].X = iRight - 1;
}
else
{
int iBottom = _offset + 2;
int iRight = sizeClient.Width - _inset - 1;
ptLight[3].X = ptLight[2].X = ptLight[0].X = _inset;
ptLight[1].Y = ptLight[2].Y = ptLight[0].Y = _offset;
ptLight[1].X = iRight;
ptLight[3].Y = _offset + 1;
ptDark[2].X = ptDark[1].X = ptDark[0].X = iRight;
ptDark[3].Y = ptDark[2].Y = ptDark[1].Y = iBottom;
ptDark[0].Y = _offset;
ptDark[3].X = _inset;
}
Pen lightPen = DockingControl.LightPen;
Pen darkPen = DockingControl.DarkPen;
pe.Graphics.DrawLine(lightPen, ptLight[0], ptLight[1]);
pe.Graphics.DrawLine(lightPen, ptLight[2], ptLight[3]);
pe.Graphics.DrawLine(darkPen, ptDark[0], ptDark[1]);
pe.Graphics.DrawLine(darkPen, ptDark[2], ptDark[3]);
// Shift coordinates to draw section grab bar
if ((_dockingControl.Dock == DockStyle.Top) ||
(_dockingControl.Dock == DockStyle.Bottom))
{
for(int i=0; i<4; i++)
{
ptLight[i].X += 4;
ptDark[i].X += 4;
}
}
else
{
for(int i=0; i<4; i++)
{
ptLight[i].Y += 4;
ptDark[i].Y += 4;
}
}
pe.Graphics.DrawLine(lightPen, ptLight[0], ptLight[1]);
pe.Graphics.DrawLine(lightPen, ptLight[2], ptLight[3]);
pe.Graphics.DrawLine(darkPen, ptDark[0], ptDark[1]);
pe.Graphics.DrawLine(darkPen, ptDark[2], ptDark[3]);
// Ensure delegates are called
base.OnPaint(pe);
}
}
缩小用户提供的控件
在开发此代码时,我注意到将用户提供的控件放置到 DockingControl
中会将其推到调整大小栏和抓取区域的边缘。虽然这本身没有问题,但看起来不太整洁,因此我改用这个小辅助类,它在提供的控件周围放置一个边框。DockingControl
创建这个 BorderControl
的一个实例,并将用户提供的控件传递给它。然后,这个 BorderControl
被用来填充 DockingControl
,而不是用户提供的控件。
// Position the provided control inside a border to give a portrait picture
// effect
class BorderControl : UserControl
{
// Instance variables
private int _borderWidth = 3;
private int _borderDoubleWidth = 6;
private Control _userControl = null;
public BorderControl(Control userControl)
{
_userControl = userControl;
Controls.Add(_userControl);
}
// Must reposition the embedded control whenever we change size
protected override void OnResize(EventArgs e)
{
// Can be called before instance constructor
if (null != _userControl)
{
Size sizeClient = this.Size;
// Move the user control to enforce the border area we want
_userControl.Location = new Point(_borderWidth, _borderWidth);
_userControl.Size = new Size(sizeClient.Width - _borderDoubleWidth,
sizeClient.Height - _borderDoubleWidth);
}
// Ensure delegates are called
base.OnResize(e);
}
}
结论
此代码是使用文本编辑器开发的,然后通过命令行调用 C# 编译器。因此,外观模仿了 VC6 环境(我拥有的)的外观,而不是 VC7(我没有的)新式停靠控件/窗口的外观和感觉。我认为可以很容易地在此代码基础上构建,以允许浮动控件和停靠栏中的多个停靠控件。如果您有任何想法或对代码进行了任何修改,请随时与我联系,我将非常感兴趣。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。