在WPF网格单元中绘制可变数量的矩形?

时间:2019-03-05 23:08:33

标签: c# .net wpf

我有一个WPF网格,它有3列宽,8行:

<KeyboardAvoidingView style={{ flex: 1 }}>
      <FlatList
         // This keeps the keyboard up and disables the user's ability to hide it.
         keyboardShouldPersistTaps="handled"
         data={this.state.examples}
         keyExtractor={(item, index) => index.toString()}
         renderItem={this._renderItem}
         contentContainerStyle={{ flex: 1}}
      />
</KeyboardAvoidingView>

我正在使用它来绘制类似这样的内容: enter image description here

第一和第三列中的每个单元格都有不同数量的矩形。同样,每个矩形的宽度可能不同,并且在运行时会发生变化。宽度将与数字成比例(在运行时已知并且会不断变化)。

绘制这些矩形的最佳方法是什么?

2 个答案:

答案 0 :(得分:3)

经过大约一个小时的摆弄(GitHub Repo),我想到了以下内容: enter image description here

我正在使用MVVM模式来使UI尽可能容易。现在,它只是填充了一些随机数据。

XAML:

<Window
    x:Class="BuySellOrders.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:BuySellOrders"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Window.DataContext>
        <local:MainWindowVm />
    </Window.DataContext>
    <Grid Margin="15">
        <ItemsControl ItemsSource="{Binding Path=Prices}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid Columns="1" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate DataType="{x:Type local:PriceEntryVm}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <Border
                            Grid.Column="0"
                            Padding="5"
                            HorizontalAlignment="Stretch"
                            BorderBrush="Black"
                            BorderThickness="1">
                            <ItemsControl HorizontalAlignment="Right" ItemsSource="{Binding Path=BuyOrders}">
                                <ItemsControl.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <StackPanel Orientation="Horizontal" />
                                    </ItemsPanelTemplate>
                                </ItemsControl.ItemsPanel>
                                <ItemsControl.ItemTemplate>
                                    <DataTemplate DataType="{x:Type local:OrderVm}">
                                        <Border
                                            Width="{Binding Path=Qty}"
                                            Margin="5"
                                            Background="red"
                                            BorderBrush="Black"
                                            BorderThickness="1" />
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                            </ItemsControl>
                        </Border>
                        <Border
                            Grid.Column="1"
                            BorderBrush="Black"
                            BorderThickness="1">
                            <TextBlock
                                Margin="8"
                                HorizontalAlignment="Center"
                                VerticalAlignment="Center"
                                Text="{Binding Path=Price}" />
                        </Border>
                        <Border
                            Grid.Column="2"
                            Padding="5"
                            BorderBrush="Black"
                            BorderThickness="1">
                            <ItemsControl ItemsSource="{Binding Path=SellOrders}">
                                <ItemsControl.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <StackPanel Orientation="Horizontal" />
                                    </ItemsPanelTemplate>
                                </ItemsControl.ItemsPanel>
                                <ItemsControl.ItemTemplate>
                                    <DataTemplate DataType="{x:Type local:OrderVm}">
                                        <Border
                                            Width="{Binding Path=Qty}"
                                            Margin="5"
                                            Background="red"
                                            BorderBrush="Black"
                                            BorderThickness="1" />
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                            </ItemsControl>
                        </Border>
                    </Grid>

                </DataTemplate>
            </ItemsControl.ItemTemplate>

        </ItemsControl>
    </Grid>
</Window>

视图模型:

class MainWindowVm : ViewModel
{
    public MainWindowVm()
    {
        var rnd = new Random();

        Prices = new ObservableCollection<PriceEntryVm>();

        for (int i = 0; i < 8; i++)
        {
            var entry = new PriceEntryVm();
            Prices.Add(entry);
            entry.BuyOrders.CollectionChanged += OnOrderChanged;
            entry.SellOrders.CollectionChanged += OnOrderChanged;

            entry.Price = (decimal)110.91 + (decimal)i / 100;

            var numBuy = rnd.Next(5);
            for (int orderIndex = 0; orderIndex < numBuy; orderIndex++)
            {
                var order = new OrderVm();
                order.Qty = rnd.Next(70) + 5;
                entry.BuyOrders.Add(order);
            }

            var numSell = rnd.Next(5);
            for (int orderIOndex = 0; orderIOndex < numSell; orderIOndex++)
            {
                var order = new OrderVm();
                order.Qty = rnd.Next(70) + 5;
                entry.SellOrders.Add(order);
            }
        }
    }

    private void OnOrderChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            foreach (var item in e.NewItems)
            {
                var order = item as OrderVm;
                if (order.Qty > LargestOrder)
                {
                    LargestOrder = order.Qty;
                }
            }
        }
    }

    private int _largestOrder;
    public int LargestOrder
    {
        get { return _largestOrder; }
        private set { SetValue(ref _largestOrder, value); }
    }


    public ObservableCollection<PriceEntryVm> Prices { get; }
}

public class PriceEntryVm: ViewModel
{
    public PriceEntryVm()
    {
        BuyOrders = new OrderList(this);
        SellOrders = new OrderList(this);
    }

    private Decimal _price;
    public Decimal Price
    {
        get {return _price;}
        set {SetValue(ref _price, value);}
    }

    public OrderList BuyOrders { get; }
    public OrderList SellOrders { get; }
}

public class OrderList : ObservableCollection<OrderVm>
{
    public OrderList(PriceEntryVm priceEntry)
    {
        PriceEntry = priceEntry;
    }

    public PriceEntryVm PriceEntry { get; }

}

public class OrderVm : ViewModel
{
    private int _qty;
    public int Qty
    {
        get { return _qty; }
        set { SetValue(ref _qty, value); }
    }

}

我不得不对事物的命名做一些假设,但是希望您应该对正在发生的事情有基本的了解。

它由PriceEntry的列表构成,每个列表包含一个Price以及一个BuyOrdersSellOrders属性。

BuyOrdersSellOrders只是具有Quantity属性的订单的列表。

XAML将价格条目列表绑定到包含3列网格的模板。该网格的第一和第三列绑定到每个订单列表的另一组项目控件。每个订单的模板只是一个边界,边界Width绑定到订单的Quantity

所有绑定意味着仅更新属性或向价格条目的购买或出售列表添加订单将自动传播到UI。添加或删除PriceEntry也会自动调整用户界面。

我尚未实现您的自动缩放功能,但是基本思路是在ValueConverter绑定上使用Quantity,以使其自动调整为最大顺序。

需要特别注意的是,它使用this nuget package提供了某些MVVM样板代码,但是只要它能提供INotifyPropertyChanged的支持,您就可以使用任何您想使用的代码。


这是一个额外的屏幕截图,显示了MVVM基于计时器更新UI的动态性质。

enter image description here

这只需要几行代码即可随机选择一行,然后随机选择该行上的订单,然后从数量中添加或减去少量随机量。

_updateTimer = new DispatcherTimer();
_updateTimer.Tick += OnUpdate;
_updateTimer.Interval = TimeSpan.FromSeconds(0.01);
_updateTimer.Start();

private void OnUpdate(object sender, EventArgs e)
{
    var entryIndex = _rnd.Next(Prices.Count);
    var entry = Prices[entryIndex];

    OrderList list;
    list = _rnd.Next(2) == 1 ?
               entry.BuyOrders :
               entry.SellOrders;

    if (list.Any())
    {
        var order = list[_rnd.Next(list.Count)];
        order.Qty += _rnd.Next(0, 8) - 4;
    }
}

答案 1 :(得分:2)

那么,就去......

这正是您要使用数据绑定的东西。如果愿意,您可以尝试手动执行操作,但是如果这样做,您的代码很快就会变得非常混乱。 WPF可以让您以老式的方式进行操作(即类似于WinForms等),但这确实是在促进遗留代码的移植。我不会在MVVM上介绍太多细节(网络上的大量信息),但是您可以通过使用NuGet开始向项目添加MVVMLightLibs或其他一些MVVM框架,然后为主窗口分配一个视图,从而开始使用。通过执行以下操作进行建模:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MainViewModel();
    }
}

现在是该视图模型本身的时候了,它是您希望视图显示的数据结构的模型:

public class MainViewModel : ViewModelBase
{
    public ObservableCollection<PriceLevel> PriceLevels { get; } = new ObservableCollection<PriceLevel>
    {
        new PriceLevel(110.98, new int[]{ }, new int[]{ }),
        new PriceLevel(110.97, new int[]{ }, new int[]{ }),
        new PriceLevel(110.96, new int[]{ }, new int[]{ }),
        new PriceLevel(110.95, new int[]{ }, new int[]{ 5 }),
        new PriceLevel(110.94, new int[]{ }, new int[]{ 3, 8 }),
        new PriceLevel(110.93, new int[]{ 8, 3, 5, }, new int[]{ }),
        new PriceLevel(110.92, new int[]{ 3 }, new int[]{ }),
        new PriceLevel(110.91, new int[]{ }, new int[]{ }),
    };
}

public class PriceLevel
{
    public double Price { get; }
    public ObservableCollection<int> BuyOrders { get; }
    public ObservableCollection<int> SellOrders { get; }

    public PriceLevel(double price, IEnumerable<int> buyOrders, IEnumerable<int> sellOrders)
    {
        this.Price = price;
        this.BuyOrders = new ObservableCollection<int>(buyOrders);
        this.SellOrders = new ObservableCollection<int>(sellOrders);
    }
}

如果您还不知道,ObservableCollection与列表非常相似,但是它会发出更改通知,因此,当您使视图显示其中的数据时,只要列表更改,GUI就会自动更新。该MainViewModel类包含类型为ObservableCollection的{​​{1}},每个PriceLevel包含价格和买卖订单列表。这意味着您将能够添加和删除价格点,以及添加和删除价格点中的订单,并且前端将反映这些更改。

接下来是前端本身:

PriceLevel

看起来有点全开,但如果将其细分为几部分,则实际上非常简单。此操作与您尝试执行的操作之间的主要区别在于,我使用的是<Window.Resources> <!-- Style to display order list as horizontal list of red rectangles --> <Style x:Key="OrderListStyle" TargetType="{x:Type ItemsControl}"> <!-- Set ItemsPanel to a horizontal StackPanel --> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal" /> </ItemsPanelTemplate> </Setter.Value> </Setter> <!-- Display each item in the order list as a red rectangle and scale x by 8*size --> <Setter Property="ItemTemplate"> <Setter.Value> <DataTemplate> <Border BorderBrush="Black" BorderThickness="1" Margin="5" > <Rectangle Width="{Binding}" Height="20" Fill="Red"> <Rectangle.LayoutTransform> <ScaleTransform ScaleX="8" ScaleY="1" /> </Rectangle.LayoutTransform> </Rectangle> </Border> </DataTemplate> </Setter.Value> </Setter> </Style> <!-- Style to make Price cells vertically aligned --> <Style TargetType="{x:Type DataGridCell}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type DataGridCell}"> <Grid Background="{TemplateBinding Background}"> <ContentPresenter VerticalAlignment="Center" /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> <!-- This style centers the column's header text --> <Style TargetType="DataGridColumnHeader"> <Setter Property="HorizontalContentAlignment" Value="Center" /> </Style> </Window.Resources> <!-- This datagrid displays the main list of PriceLevels --> <DataGrid ItemsSource="{Binding PriceLevels}" AutoGenerateColumns="False" IsReadOnly="True" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False" CanUserSortColumns="False" RowHeight="30"> <DataGrid.Columns> <!-- The buy orders column --> <DataGridTemplateColumn Header="Buy orders" Width="*"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <ItemsControl ItemsSource="{Binding BuyOrders}" Style="{StaticResource OrderListStyle}" HorizontalAlignment="Right" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <!-- The price column --> <DataGridTextColumn Header="Price" Width="Auto" Binding="{Binding Price}" /> <!-- The sell orders column --> <DataGridTemplateColumn Header="Sell Orders" Width="*"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <ItemsControl ItemsSource="{Binding SellOrders}" Style="{StaticResource OrderListStyle}" HorizontalAlignment="Left" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid> 。这基本上是一个DataGrid控件,已添加了额外的功能以使其对绑定到的数据进行动态响应。它还有很多我们不需要的额外内容(编辑,列大小调整/重新排序等),因此我已将所有功能都关闭了。 Grid绑定到视图模型中的DataGrid,因此它将显示一个垂直列表,其中列出了每个列表。然后,我明确声明了您要使用的3列。中间一个很简单,只是文本,所以PriceLevels会做的。另外两个是矩形的水平阵列,因此我使用了DataGridTextColumn,它使我可以精确地自定义它们的外观。此自定义操作大部分是在XAML顶部的DataGridTemplateColumn中完成的,它将OrderListStyle设置为水平ItemsPanel,并将StackPanel设置为矩形。根据在订单列表中显示的整数值,其中还有一些XAML可以按常数缩放矩形。

结果如下:

enter image description here

我知道XAML可能看起来有些完整,但是请记住,现在XAML已完全绑定到该视图模型,并且它将自动更新以响应更改。一开始的这些额外工作导致MUCH清洁程序更新代码更加易于测试和调试。

希望这就是您的追求,如果您有任何问题让我知道,我们可以聊天。

更新:如果您想查看动态更新,然后将其添加到主视图模型的构造函数中,它只会随机添加和删除订单:

ItemTemplate