WPF自定义表格控件(动态添加/删除行)

最近在项目开发中遇到一个小问题,我们的设备管理模块中有一项叫做“技术参数”,具体来说就是不同的设备具有不同的属性,而且属性的数量也不同。举个例子,桌子有长、宽、高、材质四个属性,日光灯有安装高度、额定功率两个属性。我们希望根据设备类型能够自主添加/修改/删除属性,另一方面其他模块也会用到此功能,所以考虑做一个自定义控件,将增、删、改操作封装在控件内部,数据对外开放。


环境版本
操作系统Windows 10
编译器Visual Studio 2015 update3

期望目标

期望达到的效果如下图所示:
这里写图片描述

包含两列数据(属性名称和属性值),可以手动添加/删除行,同时支持编辑,对外提供一个数据集合(DataTable,Dictionary或List)。

创建控件并添加依赖项属性

在WPF项目里添加一个UserControl,命名为TableControl。我们希望这个控件的某个属性具有这样的特性:属性值发生变化时,控件的数据呈现立刻跟随变化;任意时刻访问控件的这个属性,都能保证属性值与呈现的数据保持一致。由此我们就需要一个自定义的依赖项属性,另外考虑到通用性,我们决定使用DataTable作为这个依赖项属性的数据类型。(创建自定义依赖项属性快捷键:输入propdp,按两次Tab键)。

    public partial class TableControl : UserControl
    {
        public TableControl()
        {
            InitializeComponent();
        }


        #region 自定义依赖项属性

        public DataTable DataSource
        {
            get { return (DataTable)GetValue(DataSourceProperty); }
            set { SetValue(DataSourceProperty, value); }
        }

        // Using a DependencyProperty as the backing store for DataSource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DataSourceProperty =
            DependencyProperty.Register("DataSource", typeof(DataTable), typeof(TableControl), new PropertyMetadata(new DataTable(), DataSourceChanged));

        private static void DataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            TableControl control = d as TableControl;
            if (e.NewValue != e.OldValue)
            {
                DataTable dt = e.NewValue as DataTable;
            }
        }
        #endregion

    }

创建自定义依赖项属性DataSource后,又为其添加了一个属性改变事件DataSourceChanged,以确保属性值发生变化后能够进行相应的操作。

修改控件布局

WPF自带的DataGrid表格控件本身就支持增删改数据行,所以控件主体仍为DataGrid。要让DataGrid增加行,只需要将其CanUserAddRows属性设为true即可,但使用起来不那么方便,所以另外添加了一个按钮用来添加行。删除和编辑行均可以在DataGrid的模板列里实现,代码如下:

    <Grid>
        <StackPanel>
            <DataGrid HeadersVisibility="None" AutoGenerateColumns="False" CanUserAddRows="False" x:Name="dgData" GridLinesVisibility="Horizontal">

                <DataGrid.Columns>
                    <DataGridTemplateColumn Width="3*">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock  Text="{Binding Path=ParamKey,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"></TextBlock>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                        <DataGridTemplateColumn.CellEditingTemplate>
                            <DataTemplate>
                                <TextBox  Text="{Binding Path=ParamKey,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,TargetNullValue=请输入}"></TextBox>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellEditingTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn Width="10">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock Text=":"></TextBlock>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn Width="3*">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock  Text="{Binding Path=ParamValue,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"></TextBlock>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                        <DataGridTemplateColumn.CellEditingTemplate>
                            <DataTemplate>
                                <TextBox  Text="{Binding Path=ParamValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,TargetNullValue=请输入}"></TextBox>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellEditingTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn Width="*">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Button Click="btnDel_Click">
                                    <Button.Content>
                                        <Border Width="32" Height="32" CornerRadius="16" Background="CornflowerBlue" VerticalAlignment="Center" HorizontalAlignment="Center">
                                            <Path Data="M0 0L22 0" Stroke="WhiteSmoke" StrokeThickness="4" VerticalAlignment="Center" HorizontalAlignment="Center"></Path>
                                        </Border>
                                    </Button.Content>
                                </Button>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>
            <Grid Width="35">
                <Button x:Name="btnAdd" Click="btnAdd_Click">
                    <Button.Content>
                        <Border Width="32" Height="32" CornerRadius="16" Background="CornflowerBlue" VerticalAlignment="Center" HorizontalAlignment="Center">
                            <Path Data="M0 11L22 11M11 0L11 22" Stroke="WhiteSmoke" StrokeThickness="4" VerticalAlignment="Center" HorizontalAlignment="Center"></Path>
                        </Border>
                    </Button.Content>
                </Button>
            </Grid>
        </StackPanel>    
    </Grid>

数据处理

在数据处理上主要应用了WPF的双向绑定模式。控件初始化时就为DataGrid进行数据绑定。添加行时,创建一个与当前数据源具有相同结构的DataRow,将其追加到数据源上,重新进行数据绑定。由于采用了双向绑定,在页面上进行修改操作时,数据源也会随之发生变化,这样后台数据源和前台页面展示能始终保持一致,修改后的代码如下:

    public partial class TableControl : UserControl
    {
        private DataTable _dt = new DataTable();
        public TableControl()
        {
            InitializeComponent();
            _dt.Columns.Add(new DataColumn("ParamKey", typeof(string)));
            _dt.Columns.Add(new DataColumn("ParamValue", typeof(string)));
            this.dgData.ItemsSource = null;
            this.dgData.ItemsSource = _dt.DefaultView;
        }


        #region 自定义依赖项属性

        /// <summary>
        /// 数据源
        /// </summary>
        public DataTable DataSource
        {
            get { return ((DataView)this.dgData.ItemsSource).Table; }
            set { SetValue(DataSourceProperty, value); }
        }

        public static readonly DependencyProperty DataSourceProperty =
            DependencyProperty.Register("DataSource", typeof(DataTable), typeof(TableControl), new PropertyMetadata(new DataTable(), DataSourceChanged));


        private static void DataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            TableControl control = d as TableControl;
            if (e.NewValue != e.OldValue)
            {
                DataTable dt = e.NewValue as DataTable;
                control._dt = dt;
                control.dgData.ItemsSource = null;
                control.dgData.ItemsSource = control._dt.DefaultView;
            }
        }

       #endregion

        /// <summary>
        /// 删除行
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnDel_Click(object sender, RoutedEventArgs e)
        {
            ((DataRowView)this.dgData.SelectedItem).Row.Delete();
        }

        /// <summary>
        /// 添加行
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnAdd_Click(object sender, RoutedEventArgs e)
        {
            _dt = ((DataView)this.dgData.ItemsSource).Table;
            DataRow dr = _dt.NewRow();
            _dt.Rows.Add(dr);
            this.dgData.ItemsSource = _dt.DefaultView;
        }

    }

改动主要有四处:
1.中间变量_dt
创建了一个DataTable类型的局部变量_dt作为中间变量,用于数据初始化和数据绑定:

private DataTable _dt = new DataTable();

_dt.Columns.Add(new DataColumn("ParamKey", typeof(string)));
_dt.Columns.Add(new DataColumn("ParamValue", typeof(string)));
this.dgData.ItemsSource = null;
this.dgData.ItemsSource = _dt.DefaultView;

2.DataSource的返回值
由于使用了双向绑定,我们想要的数据就在DataGrid的数据源里,将其返回即可:

get { return ((DataView)this.dgData.ItemsSource).Table; }

3.删除行

((DataRowView)this.dgData.SelectedItem).Row.Delete();

进行删除行操作时,这段代码获取选择的DataRowView对象,查找到相应的DataRow对象,并使用Delete()方法将其标识为即将删除。这时可以看到删除的DataRow对象从列表中消失了,但实际上它仍位于DataTable.Rows集合中。原因是DataView中的默认过滤设置隐藏了所有已删除的记录(只是将其标识为删除,但并未真正删除)。这也是官方推荐使用的方法。
另外一种方式就会导致选择的DataRowView对象被真正删除,代码如下,仅作参考,不建议使用:

_dt.Rows.Remove(((DataRowView)this.dgData.SelectedItem).Row);

4.新增行
获取DataGrid当前的数据源,创建一个具有相同结构的空行,追加到中间变量_dt,然后重新绑定。这样就保证了原有数据不丢失,同时又增加一个空行。

            _dt = ((DataView)this.dgData.ItemsSource).Table;
            DataRow dr = _dt.NewRow();
            _dt.Rows.Add(dr);
            this.dgData.ItemsSource = _dt.DefaultView;

其他

添加样式

简单的添加了样式,主要是确保行列对齐,看起来不那么丑

    <UserControl.Resources>
        <Style x:Key="ElementStyle" TargetType="FrameworkElement">
            <Setter Property="VerticalAlignment" Value="Center"></Setter>
            <Setter Property="HorizontalAlignment" Value="Center"></Setter>
        </Style>
        <Style x:Key="TextBoxStyle" TargetType="TextBox" BasedOn="{StaticResource ElementStyle}">
            <Setter Property="BorderThickness" Value="1"></Setter>
            <Setter Property="BorderBrush">
                <Setter.Value>
                    <SolidColorBrush Color="#d6c79b"></SolidColorBrush>
                </Setter.Value>
            </Setter>
            <Setter Property="MinHeight" Value="24"></Setter>
            <Setter Property="MinWidth" Value="100"></Setter>
            <Setter Property="Margin" Value="5,2,5,2"></Setter>
            <Setter Property="TextAlignment" Value="Center"></Setter>
        </Style>
        <Style x:Key="TextBlockStyle" TargetType="TextBlock" BasedOn="{StaticResource ElementStyle}">
            <Setter Property="MinHeight" Value="24"></Setter>
            <Setter Property="MinWidth" Value="100"></Setter>
            <Setter Property="TextAlignment" Value="Center"></Setter>
        </Style>
        <Style x:Key="ButtonStyle" TargetType="Button">
            <Setter Property="BorderThickness" Value="0"></Setter>
            <Setter Property="Background" Value="Transparent"></Setter>
        </Style>
        <Style TargetType="DataGrid">
            <Setter Property="Background" Value="{x:Null}"></Setter>
            <Setter Property="BorderThickness" Value="0"></Setter>
            <Setter Property="HorizontalGridLinesBrush">
                <Setter.Value>
                    <SolidColorBrush Color="#d6c79b"></SolidColorBrush>
                </Setter.Value>
            </Setter>
            <Setter Property="MinRowHeight" Value="32"></Setter>
            <Setter Property="FontSize" Value="14"></Setter>
        </Style>
    </UserControl.Resources>

添加是否编辑状态的依赖项属性

为控件又添加了一个用于控制编辑状态的自定义依赖项属性IsEdit。非编辑状态下(IsEdit=false),隐藏新增和删除按钮,表格只读。编辑状态下(IsEdit=true),显示新增和删除按钮,表格可编辑。默认值为可编辑。

    public bool IsEdit
    {
        get { return (bool)GetValue(IsEditProperty); }
        set { SetValue(IsEditProperty, value); }
    }

    // Using a DependencyProperty as the backing store for IsEdit.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsEditProperty =
        DependencyProperty.Register("IsEdit", typeof(bool), typeof(TableControl), new PropertyMetadata(true, IsEditChanged));

    private static void IsEditChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        TableControl control = d as TableControl;
        bool isEdit = Convert.ToBoolean(e.NewValue);
        if (!isEdit)
        {
            int len = control.dgData.Columns.Count;
            control.dgData.Columns[len - 1].Visibility = Visibility.Collapsed;
            control.dgData.IsReadOnly = true;
            control.btnAdd.Visibility = Visibility.Collapsed;
        }
    }

完整代码下载点这里

总体上实现了预期的效果,但还有一些地方需要继续改进,欢迎广大朋友批评指导!

  • 7
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
WPF(Windows Presentation Foundation)提供了丰富的控件库,允许开发者定制自己的表格,并灵活地管理表格中的内容和布局信息。下面将介绍如何使用WPF创建自定义表格。 在WPF中,表格主要由Grid控件完成,Grid控件WPF中最重要的容器之一,它允许将控件按照和列的方式布局,用于实现自定义表格。 首先,需要在XAML中定义Grid控件,并设置相关属性。Grid中有两个重要属性:RowDefinitions和ColumnDefinitions,它们表示Grid的和列定义。例如: ``` <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> </Grid> ``` 上面的代码定义了一个Grid控件,它有两和三列。接下来可以在Grid中添加控件,定义它们所在的列位置。例如: ``` <Grid> ... <TextBlock Text="Name:" Grid.Row="0" Grid.Column="0" /> <TextBox Grid.Row="0" Grid.Column="1" /> <TextBlock Text="Age:" Grid.Row="1" Grid.Column="0" /> <TextBox Grid.Row="1" Grid.Column="1" /> </Grid> ``` 上面的代码添加了四个控件,分别是两个TextBlock和两个TextBox。这些控件都分别指定了它们在Grid中的和列位置。 通过以上步骤,就可以实现一个简单的自定义表格WPF提供了很多属性可以定义控件的外观和布局信息,开发者可以结合自己的需求灵活地配置表格中的内容和样式。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值