【问题标题】:How to build a grid of controls in XAML?如何在 XAML 中构建控件网格?
【发布时间】:2012-08-20 11:30:25
【问题描述】:

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

                         

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

                           

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

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

问题:在 WPF 中执行此操作的好方法是什么?

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

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

【问题讨论】:

  • 嗯,如果1-label entry包含一个长字符串,2-label entry包含2个短字符串,应该显示什么?
  • @Vlad 所有条目都包含相同数量的标签。最长的标签决定了列的宽度。
  • 哦,我明白了。让我试试 SharedSizeGroup...

标签: wpf xaml


【解决方案1】:

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

(您可以连接到AutoGeneratingColumn,将可写列的宽度设置为*

【讨论】:

  • 好的。我很高兴命令式地添加列、行和实际控件,但不幸的是it's not clear how to add controls in a DataTemplate...
  • @romkyns:在那里添加了一个答案,虽然我不推荐它,但我会尝试在 XAML 中尽可能多地做,而且我认为使用 DataGrid 将是最简单的获取变量列的方法。作为第二好的选择,我可能会实现我自己的面板,根据项目计数(或完全控制)自动添加列。
  • 谢谢 H.B.,你帮我解决了我遇到的每个 WPF 问题 :)
  • @romkyns: 好吧,我几乎只活跃在 WPF 标签中,所以我很可能会回答你的一些 WPF 问题 :)
【解决方案2】:

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

看看这个以获得灵感:

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


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

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

【讨论】:

    【解决方案3】:

    您已经提出了很多要求,以下代码显示了如何使用您想要的控件构建一个网格,并根据需要设置绑定:

        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 时就已经到位。

    【讨论】:

    • 如果您认为我应该在代码中填充网格就足够了 :) 不幸的是,there is a problem with that 我不知道如何解决...
    • 学得很好(对我来说;):我实际上完全错过了“in xaml”部分开始 - 只是为了进一步详细说明,我上面给出的示例与数据略有关联,但是稍微反思一下,也许是一些依赖属性并监视 datacontext 更改事件(或类似事件)免责声明:我对 WPF / c# 比较陌生。
    【解决方案4】:

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

    0) 在开始之前,您需要 GridHelpers。这些确保您可以动态更改行/列。稍微google一下就可以找到:

    How can I dynamically add a RowDefinition to a Grid in an 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 然后再返回。

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

    【讨论】:

      【解决方案5】:

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

      如果您想以 MVVM 方式执行此操作,您或许应该使用ItemsControl,将项目集合设置为其ItemsSource,并为您的集合项目类型定义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>
      

      【讨论】:

      • 只是因为每次我尝试以 WinForms 的方式做事时,它总是丑陋、老套且极其困难(例如 messing aroundselected item in a TreeView)。
      • 谢谢。您能否详细说明在使用ItemsControl/DataTemplate 后如何获得垂直对齐?
      • @romkyns: 再更新一次 :) 如果集合中的所有类型都相同,这将实现垂直对齐。
      • 我不明白发布的代码示例如何解决这个问题。它似乎假设只有一列文本标签。该问题明确指出可能有任意数量,具体取决于显示的特定对象。问题的核心是,如果在运行时之前不知道列数,XAML 会是什么样子?
      • 弗拉德,标签的数量是可变的。视图模型中没有Label。有一组标签。此外,这个模板为每一行生成一个网格,它仍然没有得到我需要的垂直对齐...
      【解决方案6】:

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

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

      我利用AlternationIndex 获取标签的索引并将其分配给列。我想我是从here 学到的。如果一个项目具有 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-06-13
        • 2012-11-07
        • 2015-02-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多