I'm trying to make DataGrid look like:
--------------------------------------
Point name | Distance between points|
--------------------------------------
pt1 |------------------------|
------------| 10.45 |
pt2 |------------------------|
------------| 8.55 |
pt4 |------------------------|
------------| 4.89 |
pt5 |------------------------|
------------| |
The grid is going to be binded to BindingList.
BindingList<PointClass> points;
class PointClass
{
public string Name { get; set;}
public double DistToNextPoint { get; set;}
}
Is it even possible with datagrid? Or should I make a custom control?
The DataGrid is an ItemsControl adapted to represent cells in a row aligned in columns. But the basis is the same - the representation of the source elements (ItemsSource). In the case of the DataGrid, the representation is only row-by-row. Since you need an interline representation, the DataGrid is not suitable for this.
You need either very deep intervention in the DatGrid template, or creating your own CustomControl (possibly derived from ItemsControl).
I'd like to implement something in C# where I could define a multidimensional array and index accordingly or get a specific value by key. Is there a conventional name for this? Or a package for it?
I've implemented in python, but I did a bunch of method overriding to make it work conventionally. There, usage looks like this:
class NamedTicTacToeBoard(VariableMatrix):
def __init__(self):
VariableMatrix.__init__(self)
self.__shape__ = (3, 3)
self.A1 = X
self.A2 = O
self.A3 = X
# and so forth...
board = NamedTicTacToeBoard()
board[0, :]
>> [X, O, X]
board.A1
>> X
Thanks for the help
Edit: I'm not actually making a TicTacToe board, it's for a GNC, so I need to do a bunch of matrix algebra, but also reference the states.
I am looking to obtain the name of a cell given a row/column pair. I need a name to use in a formula. For example, I would like to set cell (3, 4) to a formula that adds the values of cells (4, 4) and (5, 4).
Cell (3, 4) is D5; (4, 4) is E5, and (5, 4) is F5. Therefore, my formula should look like this:
=E5+F5
I can format my formula like this:
const int col = 3;
const int row = 4;
worksheet.Cells[row, col].Formula = string.Format(
"={0}{1}+{2}{3}"
, (char)('A'+col+1) // <<== Broken, do not use
, row+1
, (char)('A'+col+2) // <<== Broken, do not use
, row+1
);
This works fine for columns A..Z, but breaks for columns with names further to the right. I could use a trick described in this Q&A, but the problem looks so basic that it shouldn't require coding on my part.
You do not need to do this the hard way, because Aspose did it for you. Their CellsHelper class has CellIndexToName method, which does exactly what you need:
const int col = 3;
const int row = 4;
worksheet.Cells[row, col].Formula =
$"={CellsHelper.CellIndexToName(row, col+1)}+{CellsHelper.CellIndexToName(row, col+2)}";
The setup
I have a List<Room>() which I get back from a service. The list refreshes every 10 seconds, and rooms get added and removed.
class Room
{
public int ID {get;set;}
}
My job
To display these rooms on the screen, I have a Matrix-like view of variable size.
Sometimes the matrix is 3 x 3 cells, other times it is 4 x 2 or 5 x 1.
I needed a way to "remember" which slot/cell a room has been placed in, so I thought a DataTable would give me that option.
To store the cells I use a DataTable, which has 3 Columns:
"Column" (int)
"Row" (int)
"Room" (Room)
So If I have a 2 x 4 matrix, it would look like this.
Column | Row | Room
-----------------------------
0 | 0 | rooms[0]
-----------------------------
1 | 0 | rooms[1]
-----------------------------
2 | 0 | rooms[2]
-----------------------------
0 | 1 | rooms[3]
-----------------------------
1 | 2 | rooms[4]
And so forth...
Once I have this DataTable I am then able to refresh the screen, knowing that each room will be displayed at the position it was before. This can probably be achieved in a smarter way.
The problem
Now I need to enumerate the List<Room> and fill the matrix/DataTable.
If I have more rooms than cells, then I need to start at position 0,0 again (like adding a new matrix as a layer), until all rooms have been assigned a cell.
The approach so far
I have tried a few for(...) loops that look like:
int totalTiles = area.TileColumns * area.TileRows;
int totalLayers = (int)Math.Ceiling((double)area.Rooms.Count / totalTiles);
for (int i = 0; i < totalLayers; i++)
{
for (int j = 0; j < area.TileRows; j++)
{
for (int k = 0; k < area.TileColumns; k++)
{
// This is going nowhere :-(
}
}
}
In my brain
When I first came across this problem, I immediately thought: "Nothing a simple LINQ query won't fix!". And then I bricked ...
What would be the most efficient / best performing approach to fill this matrix?
Without being able to make assumptions, like will the row/columns change at runtime, I would have to say just make it completely dynamic.
class RoomStorage
{
public Room room {get;set;}
public int layer {get;set;}
public int row {get;set;}
public int col {get;set;}
}
var matrix=new List<RoomStorage>();
Then you can things like:
var newRooms=new List<Room>(); // Get from service
//Remove rooms no longer in use
var matrix=matrix.Where(m=>newRooms.Select(nr=>nr.ID).Contains(m.Room.ID));
//Find rooms we need to add (Optionally use Exclude for faster perf)
var roomsToAdd=newRooms.Where(r=>matrix.Select(m=>m.Room.ID).Contains(r.ID));
var maxLayer=matrix.Max(m=>m.layer);
var rows = ?
var cols = ?
var positions=Enumerable
.Range(0,maxLayer+1)
.SelectMany(layer=>
Enumerable
.Range(0,rows)
.SelectMany(row=>
Enumerable
.Range(0,cols)
.Select(col=>new {layer,row,col})));
Then you can use positions, left joining it to matrix for display purposes, or finding the first empty position.
I have several TableLayoutPanels, each of which displays a category of name/value information in two columns - one with informational labels, and one with data labels.
In each one, I've set the first column to autosize and right-aligned all the labels, which works just fine. However, it works separately to each TableLayoutPanel (obviously), and looks something like this:
TableLayoutPanel 1:
+--------+--------+
| Name 1 | Data 1 |
+--------+--------+
| Name 2 | Data 2 |
+--------+--------+
| Name 3 | Data 3 |
+--------+--------+
TableLayoutPanel 2:
+------------------+--------+
| Long Name 1 | Data 1 |
+------------------+--------+
| Very Long Name 2 | Data 2 |
+------------------+--------+
| Long Name 3 | Data 3 |
+------------------+--------+
I'm looking for a way to consider all of the name labels when autosizing all of the first columns, so it looks like this:
TableLayoutPanel 1:
+------------------+--------+
| Name 1 | Data 1 |
+------------------+--------+
| Name 2 | Data 2 |
+------------------+--------+
| Name 3 | Data 3 |
+------------------+--------+
TableLayoutPanel 2:
+------------------+--------+
| Long Name 1 | Data 1 |
+------------------+--------+
| Very Long Name 2 | Data 2 |
+------------------+--------+
| Long Name 3 | Data 3 |
+------------------+--------+
I can't put all the data into one table, because each table represents a different category of information, and is inside a custom control with a collection of collapsible panels (so you can show or hide each category separately).
I've been trying to achieve this by overriding the container controls OnLayout(), setting all of the TableLayoutPanels' first columns to autosize, getting all of their widths, finding the maximum, and then settings all of their first columns to a fixed size of the greatest width. This works, but looks horrible every time layout occurs as all the columns jump to autosize and then back to fixed size.
I'm assuming I'm going to have to hook ControlAdded and ControlRemoved for each table, and then SizeChanged for each child control, to know when the size of any child control changed, and then manually set the column width somehow, but I'm not sure how to reliably get the correct widths.
I tried a variation of the first method - using GetPreferredSize() on all the controls in the first columns, to attempt to find the largest width, and then setting all first columns to a fixed size, but it seemed to return widths that were slightly to small. Should I be applying some extra spacing?
Does anyone know any way of asking the TableLayoutPanel to perform autosize calculations without it actually applying them visually? Or perhaps, lying to the tables to 'pretend' that there is a control of a certain width, just so it takes it into account? I can't add actual controls, since it will then want to allocate more cells for them. I tried looking at the source with ILSpy, but well, it isn't pretty. Seems most of the work is done by TableLayout class, which is, of course, internal, and I couldn't follow what it was doing.
Thanks for any ideas...
You can use the Graphics.Measurestring to determine the length in pixels without actually drawing it. There are some slight imperfections with it, so you may think about adding or removing some padding. After a test or two, you can get pretty close. That's as proper of a way as I know of, and it doesn't involve the text being in a label.
Also, trying to find a way to get the TableLayoutPanel to calculate sizes without displaying it visually just sounds like you're trying to hack it into doing something it wasn't designed to.
It turned out the width returned by GetPreferredSize() was useful, it was just 'too late'; I was working out the correct size and returning it within code that was called from the TableLayoutPanels' OnLayout() method, and setting the column width there has no effect until the next layout.
I had a solution that used a separate Component implementing IExtenderProvider which could be used to join tables together, but due to the issue above it always lagged behind control changes. Even hooking SizeChanged on all the child controls, the TableLayoutPanel gets the event first, and starts layout.
So I couldn't see any other way but to override the layout process itself. I tried creating a LayoutEngine that performed the necessary calculations, resized the columns, then delegated the actual layout work to the old layout engine, but unfortunately Control.LayoutEngine is read-only, and TableLayoutPanel doesn't even use a backing field, it just returns another object directly, so I couldn't even get around it by using reflection to assign the private backing field.
In the end I had to resort to subclassing the control, to override OnLayout(). Here is the result:
public class SynchronizedTableLayoutPanel : TableLayoutPanel
{
/// <summary>
/// Specifies a key used to group <see cref="SynchronizedTableLayoutPanel"/>s together.
/// </summary>
public String SynchronizationKey
{
get { return _SynchronizationKey; }
set
{
if (!String.IsNullOrEmpty(_SynchronizationKey))
RemoveSyncTarget(this);
_SynchronizationKey = value;
if (!String.IsNullOrEmpty(value))
AddSyncTarget(this);
}
} private String _SynchronizationKey;
#region OnLayout(), Recalculate()
protected override void OnLayout(LayoutEventArgs levent)
{
if (ColumnCount > 0 && !String.IsNullOrEmpty(SynchronizationKey))
{
Recalculate();
ColumnStyles[0] = new ColumnStyle(SizeType.Absolute, GetMaxWidth(SynchronizationKey));
}
base.OnLayout(levent);
}
public void Recalculate()
{
var LargestWidth = Enumerable.Range(0, RowCount)
.Select(i => GetControlFromPosition(0, i))
.Where(c => c != null)
.Select(c => (Int32?)((c.AutoSize ? c.GetPreferredSize(new Size(Width, 0)).Width : c.Width)+ c.Margin.Horizontal))
.Max();
SetMaxWidth(this, LargestWidth.GetValueOrDefault(0));
}
#endregion
#region (Static) Data, cctor, AddSyncTarget(), RemoveSyncTarget(), SetMaxWidth(), GetMaxWidth()
private static readonly Dictionary<SynchronizedTableLayoutPanel, Int32> Data;
static SynchronizedTableLayoutPanel()
{
Data = new Dictionary<SynchronizedTableLayoutPanel, Int32>();
}
private static void AddSyncTarget(SynchronizedTableLayoutPanel table)
{
Data.Add(table, 0);
}
private static void RemoveSyncTarget(SynchronizedTableLayoutPanel table)
{
Data.Remove(table);
}
private static void SetMaxWidth(SynchronizedTableLayoutPanel table, Int32 width)
{
Data[table] = width;
foreach (var pair in Data.ToArray())
if (pair.Key.SynchronizationKey == table.SynchronizationKey && pair.Value != width)
pair.Key.PerformLayout();
}
private static Int32 GetMaxWidth(String key)
{
var MaxWidth = Data
.Where(p => p.Key.SynchronizationKey == key)
.Max(p => (Int32?) p.Value);
return MaxWidth.GetValueOrDefault(0);
}
#endregion
}
This version only cares about the first column, but it could be adapted to synchronise other columns, or rows.
This approach does not flicker or cause jumps with sizing:
public partial class Form1 : Form
{
private readonly Timer _timer = new Timer();
public Form1()
{
InitializeComponent();
_timer.Interval = 500;
_timer.Tick += (o, ea) => UpdateWithRandomSizes();
_timer.Start();
}
private void UpdateWithRandomSizes()
{
var rand = new Random();
label1.Text = new string('A', rand.Next(10));
label2.Text = new string('B', rand.Next(10));
label3.Text = new string('C', rand.Next(10));
label4.Text = new string('D', rand.Next(10));
tableLayoutPanel1.ColumnStyles[0].SizeType = SizeType.AutoSize;
tableLayoutPanel2.ColumnStyles[0].SizeType = SizeType.AutoSize;
var width1 = tableLayoutPanel1.GetColumnWidths()[0];
var width2 = tableLayoutPanel2.GetColumnWidths()[0];
var max = Math.Max(width1, width2);
tableLayoutPanel1.ColumnStyles[0].Width = max;
tableLayoutPanel1.ColumnStyles[0].SizeType = SizeType.Absolute;
tableLayoutPanel2.ColumnStyles[0].Width = max;
tableLayoutPanel2.ColumnStyles[0].SizeType = SizeType.Absolute;
}
}