7

I want a table to logically size the columns according to the contents. Is this possible in WPF?

alt text http://img43.imageshack.us/img43/2640/flowdocument.jpg

Here is the code I'm working with:

<Window x:Class="FlowDocument.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <Style TargetType="{x:Type TableCell}">
            <Setter Property="BorderBrush" Value="Gray" />
            <Setter Property="BorderThickness" Value="3" />


        </Style>
        <Style TargetType="{x:Type Paragraph}">
            <Setter Property="Padding" Value="2, 2, 2, 2" />
        </Style>
    </Window.Resources>
    <Grid>
        <FlowDocumentScrollViewer>
            <FlowDocument>
                <Table>
                    <Table.Columns>
                        <TableColumn Background="LightBlue" />
                        <TableColumn Background="Coral" />
                    </Table.Columns>
                    <TableRowGroup>
                        <TableRow>
                            <TableCell>
                                <Paragraph>This is a long piece of text</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>This isn't</Paragraph>
                            </TableCell>
                        </TableRow>
                        <TableRow>
                            <TableCell>
                                <Paragraph>This is a another long piece of text. The column should be wider than the other one!</Paragraph>
                            </TableCell>
                            <TableCell>
                                <Paragraph>Ditto</Paragraph>
                            </TableCell>
                        </TableRow>
                    </TableRowGroup>
                </Table>
            </FlowDocument>
        </FlowDocumentScrollViewer>
    </Grid>
</Window>
Andrew Shepherd
  • 44,254
  • 30
  • 139
  • 205

4 Answers4

5

It is possible by determining the desired width of the widest cell of a column. The widest cell can be determined by looping through all the rows determining the desired width of the cell and remembering the biggest value.

In this example, all columns are optimized. The value of 19 might result from the left and right cell padding plus cell border thickness.

void autoresizeColumns(Table table)
{
    TableColumnCollection columns = table.Columns;
    TableRowCollection rows = table.RowGroups[0].Rows;
    TableCellCollection cells;
    TableRow row;
    TableCell cell;

    int columnCount = columns.Count;
    int rowCount = rows.Count;
    int cellCount = 0;

    double[] columnWidths = new double[columnCount];
    double columnWidth;

    // loop through all rows
    for (int r = 0; r < rowCount; r++)
    {
        row = rows[r];
        cells = row.Cells;
        cellCount = cells.Count;

        // loop through all cells in the row    
        for (int c = 0; c < columnCount && c < cellCount; c++)
        {
            cell = cells[c];
            columnWidth = getDesiredWidth(new TextRange(cell.ContentStart, cell.ContentEnd)) + 19;

            if (columnWidth > columnWidths[c])
            {
                columnWidths[c] = columnWidth;
            }
        }
    }

    // set the columns width to the widest cell
    for (int c = 0; c < columnCount; c++)
    {
        columns[c].Width = new GridLength(columnWidths[c]);
    }
}


double getDesiredWidth(TextRange textRange)
{
    return new FormattedText(
        textRange.Text,
        CultureInfo.CurrentCulture,
        FlowDirection.LeftToRight,
        new Typeface(
            textRange.GetPropertyValue(TextElement.FontFamilyProperty) as FontFamily,
            (FontStyle)textRange.GetPropertyValue(TextElement.FontStyleProperty),
            (FontWeight)textRange.GetPropertyValue(TextElement.FontWeightProperty),
            FontStretches.Normal),
            (double)textRange.GetPropertyValue(TextElement.FontSizeProperty),
        Brushes.Black,
        null,
        TextFormattingMode.Display).Width;
}
zznobody
  • 186
  • 2
  • 3
  • That might be a start to solving the problem. But what happens if the total of desired column widths exceed the available width? – Andrew Shepherd May 11 '14 at 22:21
  • Dear Andrew, in my solution i handle it by checking the available width. If the total exceeds, i am working with a factor that is the quotient of the availabe width and the desired total width of all columns. Unfortunately i had no success with star lengths. So if the available width changes it needs to recalculate all widths in own code. – zznobody May 13 '14 at 19:30
  • Great solution! I had to add a for-next-loop over the `rowGroups` before setting the column widths however. – DrMarbuse Sep 21 '20 at 15:19
4

it's not quite what you're looking for, but you can do something like

<Table.Columns>
    <TableColumn Background="LightBlue" Width="2*"  />
    <TableColumn Background="Coral" Width="*" />
</Table.Columns>
kenwarner
  • 28,650
  • 28
  • 130
  • 173
4

Actually, Microsoft recommends to use a Grid instead of table for this purpose: learn.microsoft.com: Table vs Grid

Alas, Grid does not support simple grid lines out of the box. Microsoft says the Grid.ShowGridLines is only for design purposes and draws rather ugly dashed lines. Micrsoft wants you to draw Gridlines yourself. How lazy is that from Microsoft ?

Here is some sample code how this can be done:

simple grid with lines

<FlowDocumentScrollViewer>
  <FlowDocument>
    <BlockUIContainer>
      <Grid HorizontalAlignment="Left" RenderOptions.EdgeMode="Aliased" UseLayoutRounding="True">
        <Grid.ColumnDefinitions>
          <ColumnDefinition/>
          <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition/>
          <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.Resources>
          <Style TargetType="Border">
            <Setter Property="BorderBrush" Value="Black"/>
            <Setter Property="Padding" Value="2,0,3,0"/>
          </Style>

        </Grid.Resources>
        <Border Grid.Row="0" Grid.Column="0" BorderThickness="1,1,1,1">
          <TextBlock Text="AAA"/>
        </Border>
        <Border Grid.Row="0" Grid.Column="1" BorderThickness="0,1,1,1">
          <TextBlock Text="BBB"/>
        </Border>
        <Border Grid.Row="1" Grid.Column="0" BorderThickness="1,0,1,1">
          <TextBlock Text="CCC"/>
        </Border>
        <Border Grid.Row="1" Grid.Column="2" BorderThickness="0,0,1,1">
          <TextBlock Text="QgQ"/>
        </Border>
      </Grid>
    </BlockUIContainer>
  </FlowDocument>
</FlowDocumentScrollViewer>

The main idea is to set each TextBox inside a Border and to decide for each Border which side needs a borderline.

To get precise 1 pixel line, one must set Grid.RenderOptions.EdgeMode="Aliased" and Grid.UseLayoutRounding="True".

Peter Huber
  • 3,052
  • 2
  • 30
  • 42
0

You can write a simplified "AutoFit" function like below, that roughly compacts the width of columns within a given extent.

If you use Double, not Integer, and try to precisely calculate them, it may take much longer to converge wastefully.

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Documents;
using System.Windows.Controls;
using System.Text.RegularExpressions;
using System.Linq;
using System.Collections.Generic;



namespace Example {

    //----------------------------------------------------------------------------------------------------
    public class MyTable : Table {

        ...

        //------------------------------------------------------------------------------------------------
        public MyTable() {

            ...

            AutoFit();

            ...

        }
        //------------------------------------------------------------------------------------------------

        ...

        //------------------------------------------------------------------------------------------------
        private void AutoFit() {

            int extent = 10;
            int[] lengths = new int[this.Columns.Count];

            // collect content lengths of each column of the first 5 rows
            foreach(TableRow row in this.RowGroups[0].Rows.Take(5)) {
                for(int i = 0; i < row.Cells.Count; i++) {
                    TableCell cell = row.Cells[i];
                    TextRange t = new TextRange(cell.ContentStart, cell.ContentEnd);
                    int length = new List<string>(Regex.Split(t.Text, @"\r\n|[\r\n]")).Select(s => s.Length).Max();
                    lengths[i] = Math.Max(lengths[i], length);
                }
            }

            // keep content lengths with column index before sort
            List<ColumnSize> contentSizes = lengths.Select((length, index) => {
                return new ColumnSize() {Index = index, Size = length};
            }).OrderBy(e => e.Size).ToList();

            // assign compacted ratio to columns by recursion
            int[] compactedSizes = Compact(new int[contentSizes.Count], contentSizes, extent);

            // set width to columns
            for(int i = 0; i < compactedSizes.Length; i++) {
                this.Columns[i].Width = new GridLength(compactedSizes[i], GridUnitType.Star);
            }

        }
        //------------------------------------------------------------------------------------------------
        private int[] Compact(int[] sizes, List<ColumnSize> contentSizes, int extent) {

            int min = contentSizes.Min(e => e.Size);
            int max = extent - contentSizes.Count + 1;

            contentSizes = contentSizes.Select(e => {
                int size = (int)Math.Floor((double)e.Size / (double)min);
                e.Size = size > max ? max : size;
                return e;
            }).OrderBy(e => e.Size).ToList();

            if(contentSizes.Sum(e => e.Size) > extent) {
                if(sizes.All(v => v == 0)) {
                    sizes = sizes.Select(v => 1).ToArray();
                } else {
                    contentSizes.TakeWhile(e => e.Size == 1).ToList().ForEach(e => sizes[e.Index] += 1);
                }
                sizes = Compact(sizes, contentSizes.SkipWhile(e => e.Size <= 1).ToList(), extent);
            } else {
                contentSizes.ForEach(e => sizes[e.Index] = e.Size);
            }

            return sizes;

        }
        //------------------------------------------------------------------------------------------------
        private class ColumnSize {

            public int Index { get; set; }
            public int Size { get; set; }

        }
        //------------------------------------------------------------------------------------------------

        ...

    }
    //----------------------------------------------------------------------------------------------------

}
Masaki Ohashi
  • 421
  • 4
  • 4