流程图系列文章
第二章 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)
总结
逻辑可视化,编程拖拉拽