實現類 Visual Studio 設計器體驗:
TargetSizeLine)+ 手柄(TargetSizeDot)組合為 TargetSizeGroup 統一管理。
+------------------+ 1..4 +-----------------+
| TargetSizeGroup | <>-------------- | TargetSizeLine | (四條邊:Top/Bottom/Left/Right)
| - Lines[] | | - Rect |
| - Dots[] | 8 | - Cursor |
| - OnMouseDown | <>-------------- | TargetSizeDot | (八個手柄:四角+四邊中點)
| - OnMouseMove | | - Direction |
| - OnMouseUp | | - Cursor |
+------------------+ +-----------------+
^
| 綁定
|
+------------------+
| UserControlTarget| (繼承 UserControl,作為被設計的目標控件)
| - SizeGroup |
+------------------+
public enum TargetDirection {
None, TopLeft, Top, TopRight, Right, BottomRight, Bottom, BottomLeft, Left
}
public static Cursor GetCursor(TargetDirection dir) => dir switch {
TargetDirection.TopLeft or TargetDirection.BottomRight => Cursors.SizeNWSE,
TargetDirection.TopRight or TargetDirection.BottomLeft => Cursors.SizeNESW,
TargetDirection.Top or TargetDirection.Bottom => Cursors.SizeNS,
TargetDirection.Left or TargetDirection.Right => Cursors.SizeWE,
_ => Cursors.Default
};
protected override void OnPaint(PaintEventArgs e) {
base.OnPaint(e);
var g = e.Graphics;
// 繪製虛線邊框
using (var pen = new Pen(Color.Gray, 1) { DashStyle = DashStyle.Dash }) {
g.DrawRectangle(pen, 0, 0, Width - 1, Height - 1);
}
// 繪製 8 個手柄 (6x6 實心方塊)
using (var brush = new SolidBrush(Color.White)) {
foreach (var dot in _dots) {
g.FillRectangle(brush, dot.Rect);
g.DrawRectangle(Pens.Gray, dot.Rect);
}
}
}
private TargetDirection _hitDirection;
private Point _mouseStart;
private Rectangle _targetStartRect;
protected override void OnMouseDown(MouseEventArgs e) {
_hitDirection = HitTest(e.Location); // 判斷點擊落在哪個手柄/邊框/內部
if (_hitDirection != TargetDirection.None) {
_mouseStart = e.Location;
_targetStartRect = new Rectangle(Left, Top, Width, Height);
Capture = true; // 獨佔滑鼠捕獲
}
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e) {
if (!Capture) return;
int dx = e.X - _mouseStart.X;
int dy = e.Y - _mouseStart.Y;
Rectangle newRect = _targetStartRect;
// 根據方向計算新矩形
switch (_hitDirection) {
case TargetDirection.TopLeft:
newRect = new Rectangle(_targetStartRect.X + dx, _targetStartRect.Y + dy,
_targetStartRect.Width - dx, _targetStartRect.Height - dy);
break;
case TargetDirection.Top:
newRect = new Rectangle(_targetStartRect.X, _targetStartRect.Y + dy,
_targetStartRect.Width, _targetStartRect.Height - dy);
break;
// ... 其他 6 向同理
case TargetDirection.None: // 內部拖拽移動
newRect = new Rectangle(_targetStartRect.X + dx, _targetStartRect.Y + dy,
_targetStartRect.Width, _targetStartRect.Height);
break;
}
// 對齊線吸附邏輯
newRect = ApplySnapLines(newRect, _hitDirection);
// 應用新位置/大小
SetBounds(newRect.X, newRect.Y, newRect.Width, newRect.Height);
Invalidate(); // 重繪手柄
base.OnMouseMove(e);
}
protected override void OnMouseUp(MouseEventArgs e) {
Capture = false;
_hitDirection = TargetDirection.None;
base.OnMouseUp(e);
}
private const int SnapThreshold = 5; // 像素
private Rectangle ApplySnapLines(Rectangle rect, TargetDirection dragDir) {
var lines = new List();
// 收集容器內其他控件的邊緣線
foreach (Control c in Parent.Controls) {
if (c == this) continue;
lines.Add(new SnapLine(SnapLineType.Vertical, c.Left, c.Top, c.Bottom)); // Left
lines.Add(new SnapLine(SnapLineType.Vertical, c.Right, c.Top, c.Bottom)); // Right
lines.Add(new SnapLine(SnapLineType.Horizontal, c.Top, c.Left, c.Right)); // Top
lines.Add(new SnapLine(SnapLineType.Horizontal, c.Bottom, c.Left, c.Right)); // Bottom
}
// 容器邊界也作為吸附線
lines.Add(new SnapLine(SnapLineType.Vertical, 0, 0, Parent.ClientSize.Height));
lines.Add(new SnapLine(SnapLineType.Vertical, Parent.ClientSize.Width, 0, Parent.ClientSize.Height));
lines.Add(new SnapLine(SnapLineType.Horizontal, 0, 0, Parent.ClientSize.Width));
lines.Add(new SnapLine(SnapLineType.Horizontal, Parent.ClientSize.Height, 0, Parent.ClientSize.Width));
int offsetX = 0, offsetY = 0;
var snapLinesToDraw = new List();
// 檢查水平吸附
if (dragDir != TargetDirection.Left && dragDir != TargetDirection.Right) {
foreach (var line in lines.Where(l => l.Type == SnapLineType.Vertical)) {
if (Math.Abs(rect.Left - line.Position) < SnapThreshold) {
offsetX = line.Position - rect.Left;
snapLinesToDraw.Add(line);
}
if (Math.Abs(rect.Right - line.Position) < SnapThreshold) {
offsetX = line.Position - rect.Right;
snapLinesToDraw.Add(line);
}
if (Math.Abs(rect.X + rect.Width / 2 - line.Position) < SnapThreshold) { // 中心線
offsetX = line.Position - (rect.X + rect.Width / 2);
snapLinesToDraw.Add(line);
}
}
}
// 檢查垂直吸附 (同理)
// ...
// 記錄需繪製的吸附線,供容器 OnPaint 繪製紅色虛線
_snapLinesToDraw = snapLinesToDraw;
return new Rectangle(rect.X + offsetX, rect.Y + offsetY, rect.Width, rect.Height);
}
// 容器級別處理框選
private Point _selectStart;
private bool _isSelecting;
protected override void OnMouseDown(MouseEventArgs e) {
if (e.Button == MouseButtons.Left && !_isDraggingControl) {
_selectStart = e.Location;
_isSelecting = true;
}
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e) {
if (_isSelecting) {
// 繪製橡皮筋矩形
_selectionRect = GetRect(_selectStart, e.Location);
Invalidate();
}
base.OnMouseMove(e);
}
protected override void OnMouseUp(MouseEventArgs e) {
if (_isSelecting) {
_isSelecting = false;
// 篩選框內控件
_selectedControls = Parent.Controls.Cast()
.Where(c => c != this && _selectionRect.IntersectsWith(c.Bounds))
.ToList();
Invalidate(); // 繪製選中邊框
}
base.OnMouseUp(e);
}
// 集體移動:記錄所有選中控件的初始位置,移動時同步偏移
// 集體縮放:以選中區域中心為基準,按比例縮放各控件
DoubleBuffered = true,消除拖動閃爍。MoveCommand/ResizeCommand 推入命令棧。PropertyGrid.SelectedObject,修改屬性時反向調用 SetBounds。WinForm 設計器核心在於 TargetSizeGroup 封裝「邊框+手柄」幾何邏輯,HitTest 判斷操作意圖,OnMouseMove 計算新幾何並套用 SnapLines 吸附。多選、對齊線、屬性網格聯動是工程化落地的關鍵細節。本文提供的核心算法與類結構可直接作為自定義報表設計器、流程圖編輯器、低代碼頁面構建器的基石。