GitHub地址点此
图标使用了nuget库MahApps.Metro.IconPacks.Modern
实现拖动的方式有很多,本文使用了装饰器(Adorner)。使用装饰器的好处在与可以实现控件的选中效果显示。
布局容器使用了Canvas,比较方便实现控件的绝对定位。继承自Canvas并添加SelectedItems属性
Xaml代码:
主窗体代码:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="200"/>
Grid.ColumnDefinitions>
<DockPanel>
<StackPanel DockPanel.Dock="Top" Height="24" Margin="8 4 8 0" Orientation="Horizontal">
<Button Margin="0" Width="24" Padding="0" Click="AglinLeftBtn_Click" ToolTip="左对齐">
<icon:PackIconModern Kind="AlignLeft"/>
Button>
<Button Margin="4 0 0 0" Width="24" Padding="0" Click="AglinRightBtn_Click" ToolTip="右对齐">
<icon:PackIconModern Kind="AlignRight"/>
Button>
<Button Margin="4 0 0 0" Width="24" Padding="0" Click="AglinTopBtn_Click" ToolTip="上对齐">
<icon:PackIconModern Kind="BorderTop"/>
Button>
<Button Margin="4 0 0 0" Width="24" Padding="0" Click="AglinBottomBtn_Click" ToolTip="下对齐">
<icon:PackIconModern Kind="BorderBottom"/>
Button>
<Button Margin="16 0 0 0" Width="24" Padding="0" Click="HorizontalLayoutBtn_Click" ToolTip="水平分布">
<icon:PackIconModern Kind="BorderHorizontal"/>
Button>
<Button Margin="4 0 0 0" Width="24" Padding="0" Click="VerticalLayoutBtn_Click" ToolTip="垂直分布">
<icon:PackIconModern Kind="BorderVertical"/>
Button>
StackPanel>
<Border BorderThickness="1" BorderBrush="#2B79E2" Margin="8">
<local:CanvasPanel x:Name="cav" Background="WhiteSmoke" ClipToBounds="True">
<Button Canvas.Left="100" Canvas.Top="100" Width="80" Height="30" Content="s"/>
<Button Canvas.Left="300" Canvas.Top="150" Width="80" Height="30" Content="1"/>
<Button Canvas.Left="300" Canvas.Top="250" Width="80" Height="30" Content="1"/>
local:CanvasPanel>
Border>
DockPanel>
<Grid Grid.Column="1">
Grid>
Grid>
主窗体CodeBehind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void AglinLeftBtn_Click(object sender, RoutedEventArgs e)
{
cav.AlignLeft();
}
private void AglinBottomBtn_Click(object sender, RoutedEventArgs e)
{
cav.AlignBottom();
}
private void AglinTopBtn_Click(object sender, RoutedEventArgs e)
{
cav.AlignTop();
}
private void AglinRightBtn_Click(object sender, RoutedEventArgs e)
{
cav.AlignRight();
}
private void VerticalLayoutBtn_Click(object sender, RoutedEventArgs e)
{
cav.VertialLayout();
}
private void HorizontalLayoutBtn_Click(object sender, RoutedEventArgs e)
{
cav.HorizontalLayout();
}
}
CanvaPanel代码
public class CanvasPanel : Canvas
{
public CanvasPanel()
{
}
#region 单击选中项处理
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
{
if (visualAdded is Control ctrl)
{
ctrl.PreviewMouseLeftButtonDown += Ctrl_MouseLeftButtonDown;
}
if (visualRemoved is Control ctr)
{
ctr.PreviewMouseLeftButtonDown -= Ctrl_MouseLeftButtonDown;
}
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
}
private void Ctrl_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (sender is Control ctl)
{
var cp = GetParentObject<CanvasPanel>(ctl);
cp.SelectedItems = new ObservableCollection<Control>() { ctl };
}
}
public static T GetParentObject<T>(DependencyObject obj) where T : FrameworkElement
{
DependencyObject parent = VisualTreeHelper.GetParent(obj);
while (parent != null)
{
if (parent is T)
{
return (T)parent;
}
parent = VisualTreeHelper.GetParent(parent);
}
return null;
}
#endregion
#region 绘制选择框
Border selectionBorder = new Border()
{
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#637DB7F4")),
BorderThickness = new Thickness(1),
BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFBBBBBB")),
};
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
this.Children.Add(selectionBorder);
selectionStart = e.GetPosition(this);
this.CaptureMouse();
}
Point selectionStart = default;
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.LeftButton == MouseButtonState.Pressed)
{
var nowPoint = e.GetPosition(this);
var offsetX = nowPoint.X - selectionStart.X;
var offsetY = nowPoint.Y - selectionStart.Y;
Clear();
selectionBorder.Width = Math.Abs(offsetX);
selectionBorder.Height = Math.Abs(offsetY);
// 分四种情况绘制
if (offsetX >= 0 && offsetY >= 0)// 右下
{
SetLeft(selectionBorder, selectionStart.X);
SetTop(selectionBorder, selectionStart.Y);
}
else if (offsetX > 0 && offsetY < 0)// 右上
{
SetLeft(selectionBorder, selectionStart.X);
SetBottom(selectionBorder, ActualHeight - selectionStart.Y);
}
else if (offsetX < 0 && offsetY > 0)// 左下
{
SetRight(selectionBorder, ActualWidth - selectionStart.X);
SetTop(selectionBorder, selectionStart.Y);
}
else if (offsetX < 0 && offsetY < 0)// 左上
{
SetRight(selectionBorder, ActualWidth - selectionStart.X);
SetBottom(selectionBorder, ActualHeight - selectionStart.Y);
}
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
if (double.IsNaN(GetLeft(selectionBorder)))
{
SetLeft(selectionBorder, ActualWidth - GetRight(selectionBorder) - selectionBorder.ActualWidth);
}
if (double.IsNaN(GetTop(selectionBorder)))
{
SetTop(selectionBorder, ActualHeight - GetBottom(selectionBorder) - selectionBorder.ActualHeight);
}
FrameSelection(GetLeft(selectionBorder), GetTop(selectionBorder), selectionBorder.Width, selectionBorder.Height);
selectionBorder.Width = 0;
selectionBorder.Height = 0;
this.Children.Remove(selectionBorder);
this.ReleaseMouseCapture();
}
private void Clear()
{
SetLeft(selectionBorder, double.NaN);
SetRight(selectionBorder, double.NaN);
SetTop(selectionBorder, double.NaN);
SetBottom(selectionBorder, double.NaN);
}
#endregion
#region 框选
public ObservableCollection<Control> SelectedItems
{
get { return (ObservableCollection<Control>)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(ObservableCollection<Control>), typeof(CanvasPanel), new PropertyMetadata(null, OnSelectedItemsChanged));
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => (d as CanvasPanel)?.RefreshSelection();
private void RefreshSelection()
{
foreach (var item in Children)
{
var ele = item as Control;
if (ele == null) continue;
var layer = AdornerLayer.GetAdornerLayer(ele);
var arr = layer.GetAdorners(ele);//获取该控件上所有装饰器,返回一个数组
if (arr != null)
{
for (int i = arr.Length - 1; i >= 0; i--)
{
layer.Remove(arr[i]);
}
}
}
if (SelectedItems != null)
{
foreach (var item in SelectedItems)
{
var layer = AdornerLayer.GetAdornerLayer(item);
layer.Add(new SelectionAdorner(item));
}
}
}
///
/// 计算框选项
///
private void FrameSelection(double x, double y, double width, double height)
{
SelectedItems = new ObservableCollection<Control>();
foreach (var item in Children)
{
if (item is Control ctrl)
{
var left = GetLeft(ctrl);
var top = GetTop(ctrl);
if (left >= x && left <= x + width && top >= y && top <= y + height)
{
SelectedItems.Add(ctrl);
}
}
}
RefreshSelection();
}
#endregion
#region 外部调用
public void MoveControls(int offsetX, int offsetY)
{
ClearAlignLine();
// 获取可对齐的点
List<Point> points = new List<Point>();
foreach (Control ctrl in Children)
{
if (!SelectedItems.Contains(ctrl))
{
Point item = new Point(GetLeft(ctrl), GetTop(ctrl));
points.Add(item);
}
}
foreach (var item in SelectedItems)
{
SetLeft(item, GetLeft(item) + offsetX);
SetTop(item, GetTop(item) + offsetY);
// 计算是否显示对齐线
var lefAlign = points.FirstOrDefault(x => Math.Abs(x.X - GetLeft(item)) <= 1);
if (lefAlign != default)
{
SetLeft(item, lefAlign.X);
var layer = AdornerLayer.GetAdornerLayer(this);
layer.Add(new SelectionAlignLine(this, lefAlign, new Point(GetLeft(item), GetTop(item))));
}
var topAlign = points.FirstOrDefault(x => Math.Abs(x.Y - GetTop(item)) <= 1);
if (topAlign != default)
{
SetTop(item, topAlign.Y);
var layer = AdornerLayer.GetAdornerLayer(this);
layer.Add(new SelectionAlignLine(this, topAlign, new Point(GetLeft(item), GetTop(item))));
}
}
}
///
/// 清除绘制的对齐线
///
public void ClearAlignLine()
{
var arr = AdornerLayer.GetAdornerLayer(this).GetAdorners(this);
if (arr != null)
{
for (int i = arr.Length - 1; i >= 0; i--)
{
AdornerLayer.GetAdornerLayer(this).Remove(arr[i]);
}
}
}
public void ZoomControls(int offsetX, int offsetY)
{
foreach (var item in SelectedItems)
{
if (item.ActualHeight + offsetY > 10)
{
item.Height += offsetY;
}
if (item.ActualWidth + offsetX > 10)
{
item.Width += offsetX;
}
}
}
#endregion
#region 对齐操作
public void AlignLeft()
{
if (SelectedItems == null || SelectedItems.Count == 0)
return;
var leftMin = SelectedItems.Min(x => Canvas.GetLeft(x));
foreach (var item in SelectedItems)
{
SetLeft(item, leftMin);
}
}
public void AlignRight()
{
if (SelectedItems == null || SelectedItems.Count == 0)
return;
var rightMax = SelectedItems.Max(x => GetLeft(x) + x.ActualWidth);
foreach (var item in SelectedItems)
{
var targetLeft = rightMax - item.ActualWidth;
SetLeft(item, targetLeft);
}
}
public void AlignTop()
{
if (SelectedItems == null || SelectedItems.Count == 0)
return;
var topMin = SelectedItems.Min(x => GetTop(x));
foreach (var item in SelectedItems)
{
SetTop(item, topMin);
}
}
public void AlignBottom()
{
if (SelectedItems == null || SelectedItems.Count == 0)
return;
var botMax = SelectedItems.Max(x => GetTop(x) + x.ActualHeight);
foreach (var item in SelectedItems)
{
var targetLeft = botMax - item.ActualHeight;
SetTop(item, targetLeft);
}
}
public void VertialLayout()
{
if (SelectedItems == null || SelectedItems.Count < 3)
return;
var topCtl = SelectedItems.Min(x => GetTop(x) + x.ActualHeight);
var botCtrl = SelectedItems.Max(x => GetTop(x));
var emptyHeight = botCtrl - topCtl;
var orderCtrl = SelectedItems.OrderBy(x => GetTop(x)).ToList();
orderCtrl.RemoveAt(0);
orderCtrl.RemoveAt(orderCtrl.Count - 1);
var useSpace = orderCtrl.Sum(x => x.ActualHeight);
var ableSpaceAvg = (emptyHeight - useSpace) / (SelectedItems.Count - 1);
double nowPostion = topCtl;
foreach (var item in orderCtrl)
{
SetTop(item, nowPostion + ableSpaceAvg);
nowPostion += item.ActualHeight;
}
}
public void HorizontalLayout()
{
if (SelectedItems == null || SelectedItems.Count < 3)
return;
var leftCtl = SelectedItems.Min(x => GetLeft(x) + x.ActualWidth);
var rightCtrl = SelectedItems.Max(x => GetLeft(x));
var emptyHeight = rightCtrl - leftCtl;
var orderCtrl = SelectedItems.OrderBy(x => GetLeft(x)).ToList();
orderCtrl.RemoveAt(0);
orderCtrl.RemoveAt(orderCtrl.Count - 1);
var useSpace = orderCtrl.Sum(x => x.ActualWidth);
var ableSpaceAvg = (emptyHeight - useSpace) / (SelectedItems.Count - 1);
double nowPostion = leftCtl;
foreach (var item in orderCtrl)
{
SetLeft(item, nowPostion + ableSpaceAvg);
nowPostion += item.ActualWidth;
}
}
#endregion
}
SelectionAdorner代码
internal class SelectionAdorner : Adorner
{
public SelectionAdorner(UIElement adornedEIeent) : base(adornedEIeent) { }
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
Rect adornerRect = new Rect(AdornedElement.DesiredSize);
SolidColorBrush renderBrush = Brushes.Transparent;
Pen render = new Pen(new SolidColorBrush(Colors.OrangeRed), 1);
drawingContext.DrawRectangle(renderBrush, render, new Rect(adornerRect.TopLeft.X, adornerRect.TopLeft.Y, adornerRect.Width, adornerRect.Height));
MouseDown += SelectionAdorner_MouseDown;
MouseMove += SelectionAdorner_MouseMove;
MouseUp += SelectionAdorner_MouseUp;
}
private void SelectionAdorner_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
ReleaseMouseCapture();
CanvasPanel.GetParentObject<CanvasPanel>(AdornedElement).ClearAlignLine();
}
Point lastPoint = new Point();
private void SelectionAdorner_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (e.LeftButton == System.Windows.Input.MouseButtonState.Pressed)
{
CaptureMouse();
var nowPoint = e.GetPosition(CanvasPanel.GetParentObject<CanvasPanel>(AdornedElement));
int offsetX = (int)(nowPoint.X - lastPoint.X);
int offsetY = (int)(nowPoint.Y - lastPoint.Y);
CanvasPanel.GetParentObject<CanvasPanel>(AdornedElement).MoveControls(offsetX, offsetY);
lastPoint = nowPoint;
}
else if (e.RightButton == System.Windows.Input.MouseButtonState.Pressed)
{
CaptureMouse();
var nowPoint = e.GetPosition(CanvasPanel.GetParentObject<CanvasPanel>(AdornedElement));
int offsetX = (int)(nowPoint.X - lastPoint.X);
int offsetY = (int)(nowPoint.Y - lastPoint.Y);
CanvasPanel.GetParentObject<CanvasPanel>(AdornedElement).ZoomControls(offsetX, offsetY);
lastPoint = nowPoint;
}
}
private void SelectionAdorner_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
lastPoint = e.GetPosition(CanvasPanel.GetParentObject<CanvasPanel>(AdornedElement));
}
}
SelectionAlignLine代码
public class SelectionAlignLine : Adorner
{
public SelectionAlignLine(UIElement adornedElement, Point start, Point end) : base(adornedElement)
{
startPoint = start;
endPoint = end;
}
Point startPoint = default(Point);
Point endPoint = default(Point);
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
Rect adornerRect = new Rect(AdornedElement.DesiredSize);
Pen render = new Pen(new SolidColorBrush(Colors.Gray), 1);
drawingContext.DrawLine(render, startPoint, endPoint);
}
}