2

我正在尝试在 WPF 中构建一个符合规范的 UI。UI 用于编辑项目集合。每个项目都有一个可编辑的字符串属性,以及 UI 需要显示的可变数量的只读字符串。它可能看起来像这样:

                                  在此处输入图像描述

或者,根据数据,可能有不同数量的文本标签列:

                                      在此处输入图像描述

文本列的数量是完全可变的,可以从一到“很多”不等。规范要求调整列的大小以适应最长的条目(它们总是很短),整个东西应该看起来像一个网格。这个网格将包含在一个窗口中,水平拉伸文本框以适应窗口。

重要的是,文本框可以包含多行文本,并且会自动增长以适应文本。如果发生这种情况,需要将下面的行推开。

问题:在 WPF 中这样做的好方法是什么?

来自 WinForms 背景,我正在考虑一个TableLayoutPanel,它直接由我编写的代码填充。但是,我需要在 WPF 中执行此操作。虽然我仍然可以自己获取 aGrid并在代码中填充它,但我真的更喜欢一种更符合 WPF 中的完成方式的方式:即定义一个 ViewModel,填充它,然后完全在 XAML 中描述视图. 但是,我想不出在 XAML 中描述这种视图的方法。

使用 MVVM 和 XAML 最接近的方法是使用ItemsControl每行一个项目,并使用数据模板,该数据模板反过来使用另一个ItemsControl(这次水平堆叠)用于可变数量的标签,然后是文本盒子。不幸的是,这不能像规范要求的那样以网格模式垂直对齐。

4

6 回答 6

1

您可以创建自己的Panel,然后决定您希望布局逻辑如何为放入其中的子级工作。

看看这个以获得灵感:

您可以有一个“ColumnCount”属性,然后在MeassureOverrideand中使用它ArrangeOverride来决定何时包装一个孩子。


或者您可以修改这段代码(我知道它是 Silverlight 代码,但它在 WPF 中应该接近相同)。

您可以添加一个 List/Collection 属性来记录所需大小的不同列宽,然后AutoGrid_LayoutUpdated使用这些宽度来生成ColumnDefinition值,而不是让所有列的宽度都相同(默认值为 1 星“*”)。

于 2012-08-24T22:51:13.040 回答
1

在代码隐藏中执行它实际上不是 WPFish(wpf 方式)。在这里,我为您提供我的解决方案,看起来不错。

0) 在开始之前,你需要 GridHelpers。这些确保您可以动态更改行/列。你可以用一点谷歌找到它:

如何将 RowDefinition 动态添加到 ItemsPanelTemplate 中的网格?

在实际实现某些东西之前,您需要稍微重构一下您的程序。您需要新结构“CustomCollection”,它将具有:

  • RowCount - 有多少行(使用 INotifyPropertyChanged 实现)
  • ColumnCount - 有多少列(使用 INotifyPropertyChanged 实现)
  • ActualItems - 您自己的“行/项目”集合(ObservableCollection)

1) 首先创建一个包含 Grid 的 ItemsControl。确保 Grid RowDefinitions/ColumnDefinitions 是动态的。应用 ItemContainerStyle。

    <ItemsControl 
      ItemsSource="{Binding Collection.ActualItems, 
        Converter={StaticResource presentationConverter}">
      <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <Grid
             local:GridHelpers.RowCount="{Binding Collection.RowCount}"
             local:GridHelpers.StarColumns="{Binding Collection.ColumnCount, 
               Converter={StaticResource subtractOneConverter}"
             local:GridHelpers.ColumnCount="{Binding Collection.ColumnCount}" />
        </ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>
      <ItemsControl.ItemContainerStyle>
         <Style TargetType="{x:Type FrameworkElement}">
           <Setter Property="Grid.Row" Value="{Binding RowIndex}"/>
           <Setter Property="Grid.Column" Value="{Binding ColumnIndex}"/>
         </Style>
     </ItemsControl.ItemContainerStyle>
    </ItemsControl>

剩下要做的唯一一件事是:实现将您的 Viewmodel 演示文稿转换为 View 演示文稿的presentationConverter。(阅读:http ://wpftutorial.net/ValueConverters.html )

转换器应该返回一组项目,其中每个“标签”或“文本框”都是一个单独的实体。每个实体都应该有 RowIndex 和 ColumnIndex。

这是实体类:

public class SingleEntity
{
   ..RowIndex property..
   ..ColumnIndex property..
   ..ContentProperty..  <-- This will either hold label string or TextBox binded property.
   ..ContentType..
}

请注意,ContentType 是一个枚举,您将在 ItemsTemplate 中绑定它来决定您是否应该创建 TextBox 或 Label。

这似乎是一个相当冗长的解决方案,但实际上它很好,原因如下:

  • ViewModel 不知道发生了什么。这纯粹是视图问题。
  • 一切都是动态的。一旦您在 ViewModel 中添加/或删除某些内容(假设一切都已正确实现),您的 ItemsControl 将重新触发转换器并再次绑定。如果不是这种情况,您可以设置 ActualItems=null 然后返回。

如果您有任何问题,请告诉我。

于 2012-08-25T12:25:08.947 回答
1

这并不能很好地映射,您可能可以使用 aDataGrid并将其重新模板化,使其看起来像这样。在其他方法中,您可能需要强制添加列等以正确完成布局。

(您可以挂钩AutoGeneratingColumn以将该可写列的宽度设置为*

于 2012-08-24T22:41:15.863 回答
1

您已经提出了很多要求,以下代码显示了如何使用所需大小的控件构建网格,以及设置绑定:

    public void BuildListTemplate(IEnumerable<Class1> myData, int numLabelCols)
    {
        var myGrid = new Grid();

        for (int i = 0; i < myData.Count(); i++)
        {
            myGrid.RowDefinitions.Add(new RowDefinition() { Height= new GridLength(0, GridUnitType.Auto)});
        }
        for (int i = 0; i < numLabelCols; i++)
        {
            myGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Auto) });
        }
        myGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
        for (int i = 0; i < myData.Count(); i++)
        {
            for (int j = 0; j < numLabelCols; j++)
            {
                var tb = new TextBlock();
                tb.SetBinding(TextBlock.TextProperty, new Binding("[" + i + "].labels[" + j + "]"));
                tb.SetValue(Grid.RowProperty, i);
                tb.SetValue(Grid.ColumnProperty, j);
                tb.Margin = new Thickness(0, 0, 20, 0);
                myGrid.Children.Add(tb);
            }
            var edit = new TextBox();
            edit.SetBinding(TextBox.TextProperty, new Binding("[" + i + "].MyEditString"));
            edit.SetValue(Grid.RowProperty, i);
            edit.SetValue(Grid.ColumnProperty, numLabelCols);
            edit.AcceptsReturn = true;
            edit.TextWrapping = TextWrapping.Wrap;
            edit.Margin = new Thickness(0, 0, 20, 6);
            myGrid.Children.Add(edit);
        }
       contentPresenter1.Content = myGrid;
    }

上面的快速解释它所做的就是创建网格,定义网格的行;以及一系列自动调整内容大小的网格列。

然后它只是为每个数据点生成控件,设置绑定路径,并分配各种其他显示属性以及为控件设置正确的行/列。

最后,它将网格放入已在窗口 xaml 中定义的 contentPresenter 中以显示它。

现在您需要做的就是创建一个具有以下属性的类,并将 contentPresenter1 的数据上下文设置为该对象的列表:

public class Class1
{
    public string[] labels { get; set; }
    public string MyEditString { get; set; }
}

只是为了完整起见,这里是窗口 xaml 和构造函数,以显示将其全部连接起来:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<ContentPresenter Name="contentPresenter1"></ContentPresenter>
</Window>

    public MainWindow()
    {
        InitializeComponent();

        var data = new List<Class1>();

        data.Add(new Class1() { labels = new string[] {"the first", "the second", "the third"}, MyEditString = "starting text"});
        data.Add(new Class1() { labels = new string[] { "col a", "col b" }, MyEditString = "<Nothing>" });

        BuildListTemplate(data, 3);
        DataContext = data;
    }

您当然可以使用其他方法,例如列表视图并为其构建网格视图(如果您有大量行,我会这样做)或其他一些此类控件,但考虑到您的特定布局要求,您可能会想要这个方法用网格。

编辑:刚刚发现您正在寻找一种在 xaml 中做事的方式 - 我只能说我不认为您想要的功能太可行。如果您不需要将内容与单独的行上的动态大小的内容对齐,那将更可行......但我还要说,不要害怕后面的代码,它在创建 ui 时就有它的位置。

于 2012-08-24T23:54:00.370 回答
0

好吧,简单但不是很高级的方法是在代码隐藏中动态填充 UI。这似乎是最简单的解决方案,它或多或少符合您的 winforms 体验。

如果您想以 MVVM 方式执行此操作,您或许应该使用ItemsControl,将项目集合设置为其ItemsSource,并为您的集合项目类型定义 a DataTemplate

我会有DataTemplate类似的东西:

<Window x:Class="SharedSG.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:SharedSG"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <DataTemplate DataType="{x:Type app:LabelVM}">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition SharedSizeGroup="G1"/>
                    <ColumnDefinition SharedSizeGroup="G2"/>
                    <ColumnDefinition MinWidth="40" Width="*"/>
                </Grid.ColumnDefinitions>
                <Label Content="{Binding L1}" Grid.Column="0"/>
                <Label Content="{Binding L2}" Grid.Column="1"/>
                <TextBox Grid.Column="2"/>
            </Grid>
        </DataTemplate>
    </Window.Resources>
    <Grid Grid.IsSharedSizeScope="True">
        <ItemsControl ItemsSource="{Binding}"/>
    </Grid>
</Window>
于 2012-08-24T22:07:45.213 回答
0

你可能已经解决了这个问题,但我最近遇到了一个类似的问题,我让它在 xaml 中运行得非常好,所以我想我会分享我的解决方案。

主要的缺点是您必须愿意为“大量”标签的含义设置上限。如果手数可能意味着 100 个,那么这是行不通的。如果批次肯定少于您愿意键入 Ctrl+V 的次数,那么您也许可以让它工作。您还必须愿意将所有标签放入视图模型中的单个 ObservableCollection 属性中。在你的问题中,我觉得你已经尝试过了。

我利用AlternationIndex获取标签的索引并将其分配给列。想我是从这里学到的。如果一个项目有 < x 标签,则额外的列不会妨碍。如果一个项目有 > x 个标签,标签将开始堆叠在一起。

<!-- Increase AlternationCount and RowDefinitions if this template breaks -->
<ItemsControl ItemsSource="{Binding Labels}" IsTabStop="False" AlternationCount="5">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                 <TextBlock Text="{Binding}"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="{x:Type ContentPresenter}">
                <Setter Property="Grid.Column" 
                        Value="{Binding RelativeSource={RelativeSource Self}, 
                                        Path=(ItemsControl.AlternationIndex)}"/>
            </Style>
        </ItemsControl.ItemContainerStyle>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Grid IsItemsHost="True">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition SharedSizeGroup="A"/>
                        <ColumnDefinition SharedSizeGroup="B"/>
                        <ColumnDefinition SharedSizeGroup="C"/>
                        <ColumnDefinition SharedSizeGroup="D"/>
                        <ColumnDefinition SharedSizeGroup="E"/>
                    </Grid.ColumnDefinitions>
                </Grid>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>
于 2012-12-20T18:52:39.223 回答