赞
踩
第二章 WPF父容器根据內部控件的拖拽自动扩展大小
第一章 WPF下实现控件的拖拽功能
我们写运动控制上位机程序的时候,经常是要考虑如何实现一个流程,我们要考虑,完成当前步骤后,下一个步骤要实现什么,判断条件,满足后的步骤是什么,不满足条件的步骤又是什么,或者需要一直等待条件满足才开始下一个步骤,也要考虑超时都不满足条件需要走另一个步骤,根据条件也有可能会跳转回原来的步骤,这些逻辑通常都是很繁琐的(使用if-elseif、switch-case),而且写好后再次修改时,容易漏改忘改等造成逻辑不稳定问题;这里,我们介绍一种全新的实现逻辑的方式:通过流程图的原理去实现这些业务性的繁琐的逻辑,实现逻辑可视化,编程拖拉拽。
前面我们已经实现了第一步,完成了控件的拖拉拽,但还有以下问题点当时是没有优化的,也是为了突出强调"拖拉拽"功能的实现,接下来是完善以下功能的实现:控件只能放进指定的容器内,只能在容器内部移动,容器有默认的大小,超出大小容器要自动扩展;
在这里,我们会有技术点详解、代码展示、效果展示、源码链接!
上文中的目标,使用的是Border源(使用VS的Blend.exe可轻松绘制不同形状),分别是不同形状的,类似于流程图,这里提供以下几个已经绘制好了的控件:
不同形状控件的Style:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:FlowDemo1.Convert" > <local:EnumGetDiaplayNameConvert x:Key="GetDisplayName"/> <Brush x:Key="Brush_FlowControl_Base">#336AA6FF</Brush> <Brush x:Key="Brush_FlowControl_Font">#FF000000</Brush> <Brush x:Key="Brush_FlowControl_Border">#FF000000</Brush> <!-- BaseControlStyle --> <Style x:Key="BaseControlStyle" TargetType="Path"> <Setter Property="StrokeThickness" Value="1"/> <Setter Property="StrokeLineJoin" Value="Round"/> <Setter Property="Stretch" Value="Fill"/> <Setter Property="IsHitTestVisible" Value="False"/> <Setter Property="SnapsToDevicePixels" Value="True"/> <Setter Property="Fill" Value="{StaticResource Brush_FlowControl_Base}"/> <Setter Property="Stroke" Value="{StaticResource Brush_FlowControl_Border}"/> <Setter Property="MinWidth" Value="60"/> <Setter Property="MinHeight" Value="30"/> </Style> <!-- 开始控件:"跑道圆",表示一个流程的开始--> <Style x:Key="StartPath" TargetType="Path" BasedOn="{StaticResource BaseControlStyle}"> <Setter Property="Data" Value="M15,30 A15,15 0 0 1 15,0 H65 A15,15 0 0 1 65,30 Z"/> </Style> <Style x:Key="StartControl" TargetType="Path" BasedOn="{StaticResource StartPath}"> <Setter Property="IsHitTestVisible" Value="true"/> </Style> <!--结束控件:"跑道圆",表示一个流程的结束--> <Style x:Key="FinishPath" TargetType="Path" BasedOn="{StaticResource BaseControlStyle}"> <Setter Property="Data" Value="M15,30 A15,15 0 0 1 15,0 H65 A15,15 0 0 1 65,30 Z"/> </Style> <Style x:Key="FinishControl" TargetType="Path" BasedOn="{StaticResource FinishPath}"> <Setter Property="IsHitTestVisible" Value="true"/> </Style> <!--行为控件:"矩形",在这里写"工序"、"处理流程"--> <Style x:Key="ActionPath" TargetType="Path" BasedOn="{StaticResource BaseControlStyle}"> <Setter Property="Data" Value="M0,0 H80 V30 H0 Z"/> </Style> <Style x:Key="ActionControl" TargetType="Path" BasedOn="{StaticResource ActionPath}"> <Setter Property="IsHitTestVisible" Value="true"/> </Style> <!--条件控件:"菱形",判断条件,根据判断结果走正常分支(Y)、异常分支(N)、超时分支(T)--> <Style x:Key="ConditionPath" TargetType="Path" BasedOn="{StaticResource BaseControlStyle}"> <Setter Property="Data" Value="M40,0 80,30 40,60 0,30 Z"/> </Style> <Style x:Key="ConditionControl" TargetType="Path" BasedOn="{StaticResource ConditionPath}"> <Setter Property="IsHitTestVisible" Value="true"/> </Style> <!--文件控件:"特殊形状",区别于其他形状的特殊形状,用于输出文件或者写入、修改文件,其他的如通讯的处理也是同理--> <Style x:Key="FilePath" TargetType="Path" BasedOn="{StaticResource BaseControlStyle}"> <Setter Property="Data" Value="M0,30 0,0 H80 V30 A60,60 0 0 0 40,30 A60,60 0 0 1 0,30 Z"/> </Style> <Style x:Key="FileControl" TargetType="Path" BasedOn="{StaticResource FilePath}"> <Setter Property="IsHitTestVisible" Value="true"/> </Style> <!--模块控件:"平行四边形",跳转到另外一个流程,全部处理完成后(执行完结束)再跳转回来,继续往下走--> <Style x:Key="ModulePath" TargetType="Path" BasedOn="{StaticResource BaseControlStyle}"> <Setter Property="Data" Value="M10,0 H100 L90,40 H0 Z"/> </Style> <Style x:Key="ModuleControl" TargetType="Path" BasedOn="{StaticResource ModulePath}"> <Setter Property="IsHitTestVisible" Value="true"/> </Style> <!--预设模块控件:"双边矩形",跳转到另外一个流程(已经是实现好了的自定义模块,需要设置输入,不同自定义模块不同的颜色),全部处理完成后(执行完结束)再跳转回来,继续往下走--> <Style x:Key="DefineModulePath" TargetType="Path" BasedOn="{StaticResource BaseControlStyle}"> <Setter Property="Data" Value="M0,0 H100 V40 H0 V0 Z M10,0 V40 M90,0 V40"/> </Style> <Style x:Key="DefineModuleControl" TargetType="Path" BasedOn="{StaticResource DefineModulePath}"> <Setter Property="IsHitTestVisible" Value="true"/> </Style> </ResourceDictionary>
根据名称选择不同的控件形状Style:
<UserControl x:Class="FlowDemo1.FlowControl.BasicControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:FlowDemo1.FlowControl" mc:Ignorable="d" Height="Auto" Width="Auto"> <UserControl.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/FlowDemo1;Component/FlowControl/FlowControlStyleDic.xaml" /> </ResourceDictionary.MergedDictionaries> <ControlTemplate x:Key="ControlStyle" TargetType="Label"> <Grid> <Path x:Name="Path"/> <Label x:Name="Desc" Content="{Binding ControlAttr.ControlName, Converter={StaticResource GetDisplayName}}" FontSize="16" Foreground="Black" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> <ControlTemplate.Triggers> <!--数据触发器解读:当ControlAttr.ControlDesc的值==Value的值“开始”时,触发事件,将名称为Path的控件的Style样式设置为静态样式Start_DragThumb--> <DataTrigger Value="开始" Binding="{Binding ControlAttr.ControlName, Converter={StaticResource GetDisplayName}}"> <Setter TargetName="Path" Property="Style" Value="{StaticResource StartControl}"/> <Setter TargetName="Path" Property="Width" Value="80"/> <Setter TargetName="Path" Property="Height" Value="30"/> </DataTrigger> <DataTrigger Value="结束" Binding="{Binding ControlAttr.ControlName, Converter={StaticResource GetDisplayName}}"> <Setter TargetName="Path" Property="Style" Value="{StaticResource FinishControl}"/> <Setter TargetName="Path" Property="Width" Value="80"/> <Setter TargetName="Path" Property="Height" Value="30"/> </DataTrigger> <DataTrigger Value="行为" Binding="{Binding ControlAttr.ControlName, Converter={StaticResource GetDisplayName}}"> <Setter TargetName="Path" Property="Style" Value="{StaticResource ActionControl}"/> <Setter TargetName="Path" Property="Width" Value="80"/> <Setter TargetName="Path" Property="Height" Value="30"/> </DataTrigger> <DataTrigger Value="条件" Binding="{Binding ControlAttr.ControlName, Converter={StaticResource GetDisplayName}}"> <Setter TargetName="Path" Property="Style" Value="{StaticResource ConditionControl}"/> <Setter TargetName="Path" Property="Width" Value="80"/> <Setter TargetName="Path" Property="Height" Value="60"/> </DataTrigger> <DataTrigger Value="文件" Binding="{Binding ControlAttr.ControlName, Converter={StaticResource GetDisplayName}}"> <Setter TargetName="Path" Property="Style" Value="{StaticResource FileControl}"/> <Setter TargetName="Path" Property="Width" Value="80"/> <Setter TargetName="Path" Property="Height" Value="40"/> </DataTrigger> <DataTrigger Value="模块" Binding="{Binding ControlAttr.ControlName, Converter={StaticResource GetDisplayName}}"> <Setter TargetName="Path" Property="Style" Value="{StaticResource ModuleControl}"/> <Setter TargetName="Path" Property="Width" Value="80"/> <Setter TargetName="Path" Property="Height" Value="40"/> </DataTrigger> <DataTrigger Value="预设模块" Binding="{Binding ControlAttr.ControlName, Converter={StaticResource GetDisplayName}}"> <Setter TargetName="Path" Property="Style" Value="{StaticResource DefineModuleControl}"/> <Setter TargetName="Path" Property="Width" Value="100"/> <Setter TargetName="Path" Property="Height" Value="40"/> </DataTrigger> </ControlTemplate.Triggers> </ControlTemplate> </ResourceDictionary> </UserControl.Resources> <Grid> <Label Content="{Binding ControlAttr.ControlName, Converter={StaticResource GetDisplayName}}" Template="{StaticResource ControlStyle}" HorizontalContentAlignment="Center" VerticalAlignment="Center"/> </Grid> </UserControl>
Xaml
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel x:Name="pan" Background="AliceBlue"/>
<!--Canvas是拖拽的放置对象,需要设置:Drop事件,拖拽完成时触发;AllowDrop,允许拖拽;BackGroud,设置一个颜色,不然无法接收到事件-->
<Grid x:Name="pan1" Grid.Column="1" Height="Auto" Width="Auto" Background="LightGray">
<ScrollViewer x:Name="scroll" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Background="LightGray">
<Canvas x:Name="canvas" Drop="FlowControl_Drop" AllowDrop="True" Background="LightGray" Tag="0"/>
</ScrollViewer>
</Grid>
</Grid>
类的实现
public MainWindow() { InitializeComponent(); pan.Children.Clear(); foreach (EFlowControl value in Enum.GetValues(typeof(EFlowControl))) { BasicControl basicControl = new BasicControl(new FlowControlItem(value)); basicControl.MouseLeftButtonDown += BorderCopy_MouseLeftButtonDown; basicControl.Margin = new Thickness(0, 10, 0, 10); pan.Children.Add(basicControl); } } private void BorderCopy_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (sender is BasicControl item) { //拖动一个窗体内的控件(border)到另一个容器控件内,而保留原来的控件。 DragObject dragObject = new DragObject(); dragObject.ContentObj = item.ControlAttr; CFuncHelper.DoDragDrop(item, dragObject, DragDropEffects.Copy); ; } else { throw new Exception("sender 转换类型Border类型异常"); } } private void FlowControl_Drop(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(typeof(DragObject))) { object obj = e.Data.GetData(typeof(DragObject)); if (obj as DragObject is DragObject item) { BasicControl basicControl = new BasicControl(item.ContentObj as FlowControlItem ?? throw new Exception("FlowControl_Drop113异常")); basicControl.Margin = new Thickness(0); //注册鼠标事件,分别是鼠标按下、鼠标移动、鼠标弹起 basicControl.PreviewMouseDown += BorderMove_PreviewMouseDown; basicControl.PreviewMouseMove += BorderMove_PreviewMouseMove; basicControl.PreviewMouseUp += BorderMove_PreviewMouseUp; canvas.Children.Add(basicControl); //可以获取拖拽完成时,鼠标相对于canvas的位置坐标! Point point = e.GetPosition(canvas); basicControl.ControlAttr.mouseObject.MouseDownPosition = point; //使控件中心点与鼠标焦点对齐 double marginLeft = point.X - basicControl.Width / 2; double marginTop = point.Y - basicControl.Height / 2.0; double marginRight = canvas.ActualWidth - marginLeft; double marginBottom = canvas.ActualHeight - marginTop; basicControl.Margin = new Thickness(marginLeft, marginTop, marginRight, marginBottom); basicControl.ControlAttr.mouseObject.MouseDownMargin = basicControl.Margin; Debug.WriteLine($"Canvas_Drop:{basicControl.Margin.Left},{basicControl.Margin.Right}"); } } }
底层逻辑:当控件一直往下或者往右拖拽时,如果容器固定大小,就没办法去容纳更多的控件,以及无法自由调整控件位置,所以父容器必须要自动扩展大小,当控件拖拽位置到达下边界或者右边界时,父容器要自动变大,并且要有滚动条,滚动条自动跳转合适位置;反之,如果把边界处的控件拖拽回去,父容器可以自动缩小,以便去除多余的滚动条位置,这里是通过便利父容器内子控件的最大位置来实现。
#region C# wpf 实现Canvas内控件拖动 private void BorderMove_PreviewMouseDown(object sender, MouseButtonEventArgs e) { if (sender is BasicControl basicControl) { basicControl.ControlAttr.mouseObject.IsMouseDown = true; basicControl.ControlAttr.mouseObject.MouseDownPosition = e.GetPosition(this); basicControl.ControlAttr.mouseObject.MouseDownMargin = basicControl.Margin; basicControl.CaptureMouse(); } else { throw new Exception("sender 转换类型Border类型异常"); } } private Vector vector = new Vector(); private void BorderMove_PreviewMouseMove(object sender, MouseEventArgs e) { if (sender is BasicControl basicControl && basicControl.ControlAttr.mouseObject.IsMouseDown) { Vector dp = e.GetPosition(this) - basicControl.ControlAttr.mouseObject.MouseDownPosition; //实现移动更加平滑 if ((Math.Abs(vector.X - dp.X) >= 1 && Math.Abs(vector.Y - dp.Y) >= 1) || Math.Abs(vector.X - dp.X) > 1 || Math.Abs(vector.Y - dp.Y) > 1) { vector = dp; //每次鼠标按下的焦点为原点(0,0),往右是X+,往上是Y- double marginLeft = basicControl.ControlAttr.mouseObject.MouseDownMargin.Left + dp.X; double marginTop = basicControl.ControlAttr.mouseObject.MouseDownMargin.Top + dp.Y; //父容器自动往右、往下扩展 marginLeft = marginLeft <= 0 ? 0 : marginLeft; if (marginLeft + basicControl.Width >= canvas.ActualWidth) { double offset = marginLeft + basicControl.Width - canvas.ActualWidth; canvas.Width = canvas.ActualWidth + offset; scroll.ScrollToHorizontalOffset(scroll.HorizontalOffset + offset); } else { canvas.Width = GetMaxWidth(canvas); } marginTop = marginTop <= 0 ? 0 : marginTop; if (marginTop + basicControl.Height >= canvas.ActualHeight) { double offset = marginTop + basicControl.Height - canvas.ActualHeight; canvas.Height = canvas.ActualHeight + offset; scroll.ScrollToVerticalOffset(scroll.VerticalOffset + offset); } else { canvas.Height = GetMaxHeight(canvas); } double marginRight = basicControl.ControlAttr.mouseObject.MouseDownMargin.Left + basicControl.ControlAttr.mouseObject.MouseDownMargin.Right - marginLeft; double marginBottom = basicControl.ControlAttr.mouseObject.MouseDownMargin.Top + basicControl.ControlAttr.mouseObject.MouseDownMargin.Bottom - marginTop; basicControl.Margin = new Thickness(marginLeft, marginTop, marginRight, marginBottom); } } } private void BorderMove_PreviewMouseUp(object sender, MouseButtonEventArgs e) { if (sender is BasicControl basicControl && basicControl.ControlAttr.mouseObject.IsMouseDown) { basicControl.ControlAttr.mouseObject.IsMouseDown = false; basicControl.ControlAttr.mouseObject.MouseDownMargin = basicControl.Margin; basicControl.ReleaseMouseCapture(); } } private double GetMaxWidth(Canvas pan) { Grid grid = (Grid)((ScrollViewer)pan.Parent).Parent; double maxWidth = grid.ActualWidth; foreach (var item in pan.Children) { if (item is BasicControl basicControl) { maxWidth = maxWidth >= (basicControl.Margin.Left + basicControl.Width) ? maxWidth : basicControl.Margin.Left + basicControl.Width; } } return maxWidth; } private double GetMaxHeight(Canvas pan) { Grid grid = (Grid)((ScrollViewer)pan.Parent).Parent; double maxHeight = grid.ActualHeight; foreach (var item in pan.Children) { if (item is BasicControl basicControl) { maxHeight = maxHeight >= (basicControl.Margin.Top + basicControl.Height) ? maxHeight : basicControl.Margin.Top + basicControl.Height; } } return maxHeight; } #endregion
源码下载:基于流程图实现无代码编程业务逻辑(2)
逻辑可视化,编程拖拉拽
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。