简体   繁体   English

如何在XAML中构建控件网格?

[英]How to build a grid of controls in XAML?

I am trying to build a UI in WPF to a specification. 我正在尝试按照规范在WPF中构建UI。 The UI is for editing a collection of items. UI用于编辑项目集合。 Each item has an editable string property, and also a variable number of read-only strings which the UI needs to display. 每个项目都有一个可编辑的字符串属性,以及UI需要显示的可变数量的只读字符串。 It might look something like this: 它可能看起来像这样:

在此处输入图片说明

or, depending on data, might have a different number of text label columns: 或者,根据数据,文本标签列的数量可能会不同:

在此处输入图片说明

The number of text columns is completely variable and can vary from one to "lots". 文本列的数量是完全可变的,并且可以从一个到“很多”变化。 The specification calls for the columns to be sized to fit the longest entry (they are invariably very short), and the whole thing should look like a grid. 规范要求将列的大小调整为适合最长的条目(它们总是非常短),并且整个对象看起来应该像网格。 This grid will be contained in a window, stretching the text box horizontally to fit the window. 该网格将包含在窗口中,水平拉伸文本框以适合该窗口。

Importantly, the text boxes can contain multi-line text and will grow automatically to fit the text. 重要的是,文本框可以包含多行文本,并且会自动增长以适合文本。 The rows below need to be pushed out of the way if that happens. 如果发生这种情况,需要将下面的行排除在外。

Question : what would be a good way of doing this in WPF? 问题 :在WPF中这样做的好方法是什么?

Coming from a WinForms background, I am thinking of a TableLayoutPanel , which gets populated directly by code I write. 来自WinForms背景,我想到了TableLayoutPanel ,它直接由我编写的代码填充。 However, I need to do this in WPF. 但是,我需要在WPF中执行此操作。 While I could still just get myself a Grid and populate it in code, I would really rather prefer a way that's more in line with how things are done in WPF: namely, define a ViewModel, populate it, and then describe the View entirely in XAML. 虽然我仍然可以自己获得一个Grid并用代码填充它,但我确实更希望使用一种更符合WPF中的工作方式的方法:即定义一个ViewModel,填充它,然后在其中完全描述View。 XAML。 However, I can't think of a way of describing such a view in XAML. 但是,我想不出一种在XAML中描述这种视图的方法。

The closest I can get to this using MVVM and XAML is to use an ItemsControl with one item per row, and use a data template which, in turn, uses another ItemsControl (stacked horizontally this time) for the variable number of labels, followed by the text box. 使用MVVM和XAML,我能得到的最接近的结果是使用每行一个项目的ItemsControl ,并使用数据模板,该数据模板又将另一个ItemsControl (这次是水平堆叠)用于可变数量的标签,然后是文本框。 Unfortunately, this can't be made to align vertically in a grid pattern like the spec requires. 不幸的是,这不能像规范要求那样垂直对齐网格图案。

This does not map all too well, you could probably use a DataGrid and retemplate it to look like this. 这并不能很好地映射,您可以使用DataGrid并对其进行重新模板化 ,使其看起来像这样。 In other approaches you may need to imperatively add columns or the like to get the layout done right. 在其他方法中,您可能需要强制添加列或类似内容以正确完成布局。

(You can hook into AutoGeneratingColumn to set the width of that one writeable column to * ) (您可以挂钩到AutoGeneratingColumn以将那一个可写列的宽度设置为*

You can create your own Panel and then decide on how you want the layout logic to work for the children that are put inside it. 您可以创建自己的Panel ,然后决定希望布局逻辑如何作用于放置在其中的子Panel

Look at this for inspiration: 看一下这个灵感:

You could have a "ColumnCount" property, and then use that within the MeassureOverride and ArrangeOverride to decide when to wrap a child. 您可以具有“ ColumnCount”属性,然后在MeassureOverrideArrangeOverride使用该属性来决定何时包装子项。


Or you could modify this bit of code (I know it's Silverlight code, but it should be close to the same in WPF). 或者,您可以修改这段代码(我知道这是Silverlight代码,但是应该与WPF中的代码差不多)。

Instead of having the same width for all columns (the default is 1-star "*"), you could add a List/Collection property that records the different column widths sized you want, then in the AutoGrid_LayoutUpdated use those widths to make the ColumnDefinition values. 而不是所有列的宽度相同(默认为1星级的“*”),你可以添加,它记录了不同的列宽大小,你想有一个列表/集合属性,然后在AutoGrid_LayoutUpdated使用这些宽度,使ColumnDefinition值。

You've asked for quite a bit, the following code shows how to build a grid with the controls you want that sizes as needed, along with setting up the bindings: 您已经要求了很多,以下代码显示了如何使用所需大小的控件来构建网格,以及如何设置绑定:

    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;
    }

A Quick Explanation of the above All it is doing is creating the grid, defines rows for the grid; 上面的所有操作的简短说明是创建网格,定义网格的行; and a series of columns for the grid that auto size for the content. 网格的一系列列会自动调整内容的大小。

Then it simply generates controls for each data point, sets the binding path, and assigns various other display attributes along with setting the correct row/column for the control. 然后,它只为每个数据点生成控件,设置绑定路径,并分配其他各种显示属性,以及为控件设置正确的行/列。

Finally it puts the grid in a contentPresenter that has been defined in the window xaml in order to show it. 最后,它将网格放在xaml窗口中定义的contentPresenter中,以便显示它。

Now all you need do is create a class with the following properties and set the data context of the contentPresenter1 to a list of that object: 现在,您需要做的就是创建一个具有以下属性的类,并将contentPresenter1的数据上下文设置为该对象的列表:

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

just for completeness here is the window xaml and constructor to show hooking it all up: 为了完整起见,这里是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;
    }

You can of course use other methods such as a listview and build a gridview for it (I'd do this if you have large numbers of rows), or some other such control, but given your specific layout requirements probably you are going to want this method with a grid. 您当然可以使用其他方法,例如列表视图并为其构建网格视图(如果您有大量行,则可以执行此操作),或其他类似的控件,但是鉴于您的特定布局要求,您可能会想要这种方法带有网格。

EDIT : Just spotted that you're looking for a way of doing in xaml - tbh all I can say is that I don't think that with the features you're wanting that it is too viable. 编辑 :刚发现您正在寻找一种在xaml中做某事的方法-tbh我只能说我不认为您想要的功能过于可行。 If you didn't need to keep things aligned to dynamically sized content on seperate rows it would be more viable... But I will also say, don't fear code behind, it has it's place when creating the ui. 如果您不需要使内容与单独行上的动态大小的内容保持一致,那将是更可行的……但是我也要说,不要担心代码背后,它在创建ui时就已经存在。

Doing it in the code-behind is really not a WPFish(wpf way). 在背后的代码中执行此操作实际上不是WPFish(wpf方式)。 Here I offer you my solution, which looks nice imo. 在这里,我向您提供我的解决方案,它看起来不错。

0) Before starting, you need GridHelpers. 0)开始之前,您需要GridHelpers。 Those make sure you can have dynamically changing rows/columns. 这些确保您可以动态更改行/列。 You can find it with a little bit of google: 您可以在Google上找到它:

How can I dynamically add a RowDefinition to a Grid in an ItemsPanelTemplate? 如何在ItemsPanelTemplate中向网格动态添加RowDefinition?

Before actually implementing something, you need to restructure your program a little. 在实际实现某些功能之前,您需要稍微重组一下程序。 You need new structure "CustomCollection", which will have: 您需要新的结构“ CustomCollection”,它将具有:

  • RowCount - how many rows are there(implement using INotifyPropertyChanged) RowCount-有多少行(使用INotifyPropertyChanged实现)
  • ColumnCount - how many columns are there(implement using INotifyPropertyChanged) ColumnCount-有多少列(使用INotifyPropertyChanged实现)
  • ActualItems - Your own collection of "rows/items"(ObservableCollection) ActualItems-您自己的“行/项目”(ObservableCollection)的集合

1) Start by creating an ItemsControl that holds Grid. 1)首先创建一个保存网格的ItemsControl。 Make sure Grid RowDefinitions/ColumnDefinitions are dynamic. 确保网格行定义/列定义是动态的。 Apply ItemContainerStyle. 应用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>

The only thing left to do: implement presentationConverter which converts your Viewmodel presentation to View presentation. 剩下要做的唯一事情是:实现presentationConverter,它将您的Viewmodel演示文稿转换为View演示文稿。 (Read: http://wpftutorial.net/ValueConverters.html ) (阅读: http : //wpftutorial.net/ValueConverters.html

The converter should give back a collection of items where each "label" or "textbox" is a seperate entity. 转换器应返回项目集合,其中每个“标签”或“文本框”都是一个单独的实体。 Each entity should have RowIndex and ColumnIndex. 每个实体应具有RowIndex和ColumnIndex。

Here is entity class: 这是实体类:

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

Note that ContentType is an enum which you will bind against in ItemsTemplate to decide if you should create TextBox or Label. 请注意,ContentType是一个枚举,您将在ItemsTemplate中绑定该枚举,以确定是否应创建TextBox或Label。

This might seem like a quite lengthy solution, but it actually is nice for few reasons: 这似乎是一个很长的解决方案,但实际上出于以下几个原因是不错的:

  • The ViewModel does not have any idea what is going on. ViewModel不知道发生了什么。 This is purely View problem. 这纯粹是视图问题。
  • Everything is dynamic. 一切都是动态的。 As soon you add/or remove something in ViewModel(assuming everything is properly implemented), your ItemsControl will retrigger the Converter and bind again. 只要您在ViewModel中添加/或删除某些内容(假设一切都已正确实现),ItemsControl就会重新触发Converter并再次绑定。 If this is not the case, you can set ActualItems=null and then back. 如果不是这种情况,则可以将ActualItems = null设置为空,然后再返回。

If you have any questions, let me know. 如果您有任何疑问,请告诉我。

Well, the simple yet not not very advanced way would be to fill the UI dynamically in the code-behind. 好吧,一种简单但不是很高级的方法是在背后的代码中动态填充UI。 This seems to be the easiest solution, and it more or less matches your winforms experience. 这似乎是最简单的解决方案,它或多或少与您的Winforms体验相匹配。

If you want to do it in a MVVM way, you should perhaps use ItemsControl , set the collection of items as its ItemsSource , and define a DataTemplate for your collection item type. 如果要以MVVM方式进行操作,则可能应使用ItemsControl ,将项目集合设置为其ItemsSource ,然后为您的集合项目类型定义一个DataTemplate

I would have the DataTemplate with something like that: 我将有这样的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>

You're probably way past this issue, but I had a similar issue recently and I got it to work surprisingly well in xaml, so I thought I'd share my solution. 您可能已经解决了这个问题,但是最近我遇到了一个类似的问题,并且在xaml中它可以正常工作,因此我想分享一下我的解决方案。

The major downside is that you have to be willing to put an upper-bound on what "lots" of labels means. 主要的缺点是您必须愿意对“很多”标签的含义进行上限。 If lots can mean 100s, this won't work. 如果抽签可以表示100,那将不起作用。 If lots will definitely be less than the number of times you're willing to type Ctrl+V, you might be able to get this to work. 如果手数肯定少于您愿意输入Ctrl + V的次数,则可以使它起作用。 You also have to be willing to put all the labels into a single ObservableCollection property in your view model. 您还必须愿意将所有标签放入视图模型中的单个ObservableCollection属性。 It sounded to me in your question that you already tried that out anyway though. 在您的问题中,对我来说,您已经尝试了一下。

I takes advantage of AlternationIndex to get the index of the label and assign it to a column. 我利用AlternationIndex来获取标签的索引并将其分配给列。 Think I learned that from here . 想想我是从这里学到的。 If an item has < x labels the extra columns won't get in the way. 如果项目具有<x标签,则多余的列将不会妨碍您。 If an item has > x labels, the labels will start stacking on top of each other. 如果项目具有> 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>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM