Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
701 views
in Technique[技术] by (71.8m points)

c# - Need a very customized large Winforms grid

I am about to develop a Windows PC application (it can be WinForms or WPF) and my main concern is a UI problem I will have to address.

Basically, I need to have a grid of about 50x50 that I need to get input from the user for. That's 2500 fields. Realistically most will be left blank, about 10% will be filled out by a user. Each field can be blank or have a number from 1 to 4. I want easy input - a drop down box perhaps (since it doesn't make sense to tab through all 2500 fields when with the keyboard, I want the user to fill out the values with the mouse).

I was thinking the drop down boxes or maybe even labels that change value when you click them, but the problem is (from the tests I've done) adding 2500 of ANY type of control will make the interface horribly slow. I tried using a tablelayoutpanel in a WinForms app with the suspend/resumeupdate functions, and also doublebuffering and that helps a bit, but it's still terribly slow. I am reluctant to go the DataGridView route because I need VERY custom headers and I need the UI to auto update some percentages as the user changes values in the fields. But I will not be opposed if that's my only option.

I heard WPF may be better since you can have many controls and each one doesn't take its own windows handle, and there's virtualization (not sure how hard that is to implement).

I'm open to suggestions. I know someone will suggest to break up the grid, which I may end up doing. Either way, I'd like to know the most efficient method for a large grid with many controls in a Windows app as if I were going to develop this without breaking up the grid.

I'm using VS 2013, developing in C#, .NET 4.0.

Thanks!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

As demonstrated by @Kerry's answer, the winforms answer to almost everything is "you can't do that in winforms, therefore you need to create a much poorer substitute UI design that fits into winforms limitations." - that is not what I would expect from any decent UI framework.

This is what I achieved in WPF in 10 minutes with some 20 lines of C# code and 50 lines of XAML:

enter image description here

  • The response time when interacting with this WPF UI is Immediate on my machine (an I5 CPU and a regular video card). Even with no virtualization (since I'm using an UniformGrid which does not virtualize), this is way better than anything you can ever hope to achieve in winforms.
  • I introduced a ComboBox with numbers 1-4 as you requested.
  • Fully customizable (without resorting to any "owner draw" hacks). I even added these Row and Column numbers which of course are all part of the scrollable area.
  • Touch-Ready - this kind of big-scrolling UI is really better suited for a touch device. Something that the winforms paradigm does not even take into account. Otherwise, you could also implement Excel-like keyboard navigation in the grid with the arrow keys to create a better non-touch user experience.
  • With a small effort you could also make the row and column headers fixed while retaining their consistency with the scroll offset of the entire grid.
  • This is actually a ListBox, which means it has the concept of SelectedItem and it also by default exhibits the ListBox-like visual style (you can see the light-blue background and outline on the selected item).
  • The logic is decoupled from the UI by creating a proper ViewModel with a collection of Items and then using ItemsControls to let WPF do it's job of creating the UI. There's not a single line of C# code in this example that manipulates any UI element. It's all done via beautiful DataBinding.

Full source:

<Window x:Class="WpfApplication3.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <DataTemplate x:Key="MarkerTemplate">
            <Border BorderBrush="Gray" BorderThickness="1" Margin="1" Background="Gainsboro">
                <Grid Width="50" Height="30">
                    <TextBlock Text="{Binding}" FontWeight="Bold" VerticalAlignment="Center" HorizontalAlignment="Center"/>
                </Grid>
            </Border>
        </DataTemplate>

        <Style TargetType="ListBoxItem">
            <Setter Property="Padding" Value="0"/>
        </Style>
    </Window.Resources>

    <DockPanel>
        <ListBox ItemsSource="{Binding Items}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderBrush="Gray" BorderThickness="1">
                    <Grid Width="50" Height="30">
                        <TextBlock Text="{Binding Value}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
                        <ComboBox x:Name="ComboBox" SelectedItem="{Binding Value}" 
                                  IsDropDownOpen="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
                                  Visibility="Collapsed">
                            <sys:Int32>1</sys:Int32>
                            <sys:Int32>2</sys:Int32>
                            <sys:Int32>3</sys:Int32>
                            <sys:Int32>4</sys:Int32>
                        </ComboBox>
                    </Grid>
                    </Border>
                    <DataTemplate.Triggers>
                        <DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}" Value="True">
                            <Setter TargetName="ComboBox" Property="Visibility" Value="Visible"/>
                        </DataTrigger>
                    </DataTemplate.Triggers>
                </DataTemplate>
            </ListBox.ItemTemplate>

            <ListBox.Template>
                <ControlTemplate TargetType="ListBox">
                    <ScrollViewer CanContentScroll="True" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
                        <DockPanel>
                            <ItemsControl DockPanel.Dock="Top" ItemsSource="{Binding ColumnMarkers}"
                                ItemTemplate="{StaticResource MarkerTemplate}">
                                <ItemsControl.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <VirtualizingStackPanel Orientation="Horizontal"/>
                                    </ItemsPanelTemplate>
                                </ItemsControl.ItemsPanel>
                            </ItemsControl>

                            <ItemsControl DockPanel.Dock="Left" ItemsSource="{Binding RowMarkers}"
                                          ItemTemplate="{StaticResource MarkerTemplate}">
                                <ItemsControl.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <VirtualizingStackPanel Orientation="Vertical"/>
                                    </ItemsPanelTemplate>
                                </ItemsControl.ItemsPanel>
                            </ItemsControl>

                            <UniformGrid Rows="50" Columns="50" IsItemsHost="True"/>
                        </DockPanel>
                    </ScrollViewer>
                </ControlTemplate>
            </ListBox.Template>
        </ListBox>
    </DockPanel>
</Window>

Code Behind:

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;

namespace WpfApplication3
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            DataContext = new ViewModel();
        }
    }
}

ViewModel:

public class ViewModel
{
    public List<string> RowMarkers { get; set; }

    public List<string> ColumnMarkers { get; set; }

    public ObservableCollection<Item> Items { get; set; }

    public ViewModel()
    {
        RowMarkers = Enumerable.Range(1, 50).Select(x => x.ToString()).ToList();
        ColumnMarkers = new[] { " " }.Concat(Enumerable.Range(1, 50).Select(x => x.ToString())).ToList();

        var list = new List<Item>();

        for (int i = 0; i < 50; i++)
        {
            for (int j = 0; j < 50; j++)
            {
                list.Add(new Item());
            }
        }

        Items = new ObservableCollection<Item>(list);
    }
}

Data Item:

public class Item
{
    public int? Value { get; set; }
}
  • You will probably want to add Row and Column properties to the Item class so that you can keep track of what rows/columns actually contain values. Then You could use LINQ like so:

    var values = Items.Where(x => Value != null);
    

    and obtain a list of Items and get item.Row and Item.Column for each of them.

  • Forget winforms. It's completely useless. - at this point, winforms is completely obsolete. Whatever you can achieve with winforms, you can achieve the same thing in WPF with 10% the amount of code and probably with much better results. winforms is not recommended for any new projects, only to maintain legacy applications. It is an ancient technology that is not suited to cater for today's UI needs. That's why Microsoft created WPF to replace it.

  • WPF Rocks. Just copy and paste my code in a File -> New Project -> WPF Application and see the results for yourself.

  • Let me know if you need further help.

Important Note: the WPF default control templates are much lighter weight in Windows 8 than their Windows 7 counterparts (following the Windows 8 philosophy of removing the heavy Aero stuff and practically all transparencies to have a smaller UI footprint).

This means that testing my code on Windows 7 might not yield the expected results in terms of performance. If that happens to be the case, Don't worry. It is fixable. A small amount of additional XAML would have to be introduced (some Styles and ControlTemplates) to replace the Windows 7 defaults by something "faster".


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...