WPF父容器根据內部控件的拖拽自动扩展大小

流程图系列文章

第二章 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)


总结

逻辑可视化,编程拖拉拽

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WPF中,可以使用数据模板和数据绑定机制来根据结构体属性自动生成控件。具体来说,可以为结构体定义一个数据模板,用于描述结构体的属性应该如何呈现为控件,并将结构体的实例作为数据上下文设置到该数据模板中,这样WPF就会自动根据数据模板生成对应的控件。 假设我们有一个包含三个属性的结构体Person,如下所示: ```csharp public struct Person { public string Name { get; set; } public int Age { get; set; } public bool IsMale { get; set; } } ``` 现在,我们想要根据这个结构体的属性自动生成控件,可以使用以下代码定义一个数据模板: ```xml <DataTemplate DataType="{x:Type local:Person}"> <StackPanel> <TextBlock Text="Name:"/> <TextBox Text="{Binding Path=Name, Mode=TwoWay}"/> <TextBlock Text="Age:"/> <TextBox Text="{Binding Path=Age, Mode=TwoWay}"/> <TextBlock Text="Is Male:"/> <CheckBox IsChecked="{Binding Path=IsMale, Mode=TwoWay}"/> </StackPanel> </DataTemplate> ``` 然后,在代码中,我们需要将Person结构体的实例作为数据上下文设置到该数据模板中,以便WPF能够根据数据模板自动生成控件。假设我们有一个名为person的Person结构体变量,可以使用以下代码将它设置为数据上下文: ```csharp ContentControl contentControl = new ContentControl(); contentControl.Content = person; contentControl.ContentTemplate = FindResource(typeof(Person)) as DataTemplate; ``` 这样,WPF就会根据数据模板自动生成一个包含三个TextBox控件和一个CheckBox控件的StackPanel控件,用于显示和修改Person结构体的属性。当用户在界面中修改Name、Age或IsMale属性时,Person结构体中的相应属性值也会自动更新。反之,当Person结构体中的属性值发生变化时,界面中显示这些属性的控件的值也会自动更新。 需要注意的是,为了实现自动更新,必须将数据绑定的Mode属性设置为TwoWay,这样绑定才能够在控件的值发生变化时更新数据源。另外,如果Person结构体中的属性发生变化时,界面中的控件没有自动更新,可以使用INotifyPropertyChanged接口来通知界面更新控件的值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值