WPF 组态软件实现思路(WPF控件可视化布局)

WPF 开源 组态软件实现思路(WPF控件可视化布局)

  • 一、实现控件选中及自由拖动
  • 二、实现控件对齐功能
  • 三、实现对齐辅助线功能
  • 四、实现框选功能

GitHub地址点此

请注意

  • 属性编辑控件基于Devexpress V21.2.3 控件库,如需编译需购买及安装 Devexpress V21.2.3 开发库
  • 脚本编辑基于AvalonEdit开源库 https://github.com/icsharpcode/AvalonEdit
  • 图标控件基于MahApps.Metro.IconPacks开源 https://github.com/MahApps/MahApps.Metro.IconPacks
  • JavaScript脚本运行基于Unvell.ReoScript开源库 https://github.com/unvell/ReoScript

来张图(后续细节完善后的效果)

WPF 组态软件实现思路(WPF控件可视化布局)_第1张图片

本文实现效果
WPF 组态软件实现思路(WPF控件可视化布局)_第2张图片

图标使用了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);
        }
    }

你可能感兴趣的:(自定义控件,WPF技术,wpf,c#,microsoft)