WPF Dynamically Merge and Split Grid Cells - c#

This question might be a bit open ended but I am looking for advice on how to tackle the issue of merging and splitting Grid cells dynamically.
So currently I generate a Grid of X rows and Y columns and each of these cells is then populated with a StackPanel. When clicked a StackPanel has a border added to it and an array holding bool values representing each Grid cell is modified to show that it has been clicked. The grid holds no other controls. This is all done programmatically in c# rather than in the xaml.
What I want to be able to do is to merge and split selected cells in the grid if they are next to each other by hitting a button. I have found no way of easily doing this and I am wondering if anyone can recommend a good approach for this issue.
I am not tied to my current approach and any alternate method suggestions are appreciated.

I just posted one example of how to do this on Github.
The basic idea is to initialize the grid with the maximum number of rows and columns and then create and delete cells while modifying their positions and spans as necessary to fill the grid.
Here are the most relevant snippets:
Initialization: (MainWindow.xaml.cs)
public MainWindow()
{
InitializeComponent();
var numRows = grid.RowDefinitions.Count;
var numCols = grid.ColumnDefinitions.Count;
for (int i = 0; i < numRows; i++)
{
for (int j = 0; j < numCols; j++)
{
var item = new DynamicGridItem(j, i);
item.Merge += HandleMerge;
item.Split += HandleSplit;
grid.Children.Add(item);
Grid.SetRow(item, i);
Grid.SetColumn(item, j);
}
}
}
Here you can see that my cell items are a custom UserControl class with a constructor that takes two integers as X and Y grid coordinates. These coordinates can then be used to simplify grid row and column assignment. They aren't strictly necessary in my example, but could be helpful if storing/loading grid items and working with them in other code modules.
Merge Event:
My example uses a single Merge event with a boolean event argument to indicate whether the merge is to the left or to the right. The actual event handler just figures out whether to offset left or right and passes that information as well as the source item to the HandleMergeOffset method below.
private void HandleMergeOffset(DynamicGridItem item, int offset)
{
var otherItem = FindItemByXOffset(item, offset);
if (otherItem == null)
{
return; // Nothing to do
}
otherItem.Merge -= HandleMerge;
otherItem.Split -= HandleSplit;
Grid.SetColumnSpan(item, Grid.GetColumnSpan(item) + Grid.GetColumnSpan(otherItem));
grid.Children.Remove(otherItem);
if (offset < 0) // Reposition item if merging left
{
Grid.SetColumn(item, otherItem.X);
item.X = otherItem.X;
}
}
You could probably write a better FindItem method than I did. The basic idea is to find the neighbor to remove, expand the current item to include both original column spans, unhook events from the removed item, and of course remove it from the grid.
Split Event:
Since we remove an item on Merge, we create a new item on Split. I decided not to bother with a SplitLeft or SplitRight designation. This code will always create a minimum cell at the left-most index of the original cell and push the remainder one column right.
private void HandleSplit(object sender, EventArgs e)
{
var item = (DynamicGridItem)sender;
var itemColSpan = Grid.GetColumnSpan(item);
if (itemColSpan < 2)
{
return; // Nothing to do
}
var newItem = new DynamicGridItem(item.X, item.Y);
newItem.Merge += HandleMerge;
newItem.Split += HandleSplit;
grid.Children.Add(newItem);
Grid.SetColumn(newItem, newItem.X);
Grid.SetRow(newItem, newItem.Y);
Grid.SetColumn(item, item.X + 1);
Grid.SetColumnSpan(item, itemColSpan - 1);
item.X += 1;
}
Note that this makes no attempt to handle splitting a single cell into half cells (so that we never mutate the original grid or deal with sub-grids). If you need arbitrary dynamic grid behavior, you'll want to look elsewhere.

Related

How to add rows from one datagridview to another empty datagridview by some conditional in c#

I'm using visual studio 2017, C#, Windows Forms to create an index for words in a list of sentences.
I have two datagridview:
dataGridView2: This grid has a single column where each row contains a worded sentence.
dGvTopics: This grid has one column for every word that is repeated in the first sentence (first row) in dataGridView2, the column header text is the word.
Goal: I want to click button to categorize, inserting a row in dGvTopics for each row in dataGridView2 (sentences), place a copy of the sentence as the value for that column if the sentence contains the column header text.
My Code is:
private void btnClassify_Click(object sender, EventArgs e)
{
for (int i = 0; i < dGvTopics.Columns.Count; i++)
{
if (dataGridView2.Rows[i].Cells[0].Value.ToString().Contains(dGvTopics.Columns[i].HeaderText))
{
this.dGvTopics.Rows.Add();
this.dGvTopics.Rows[i].Cells[i].Value = dataGridView2.Rows[i].Cells[0].Value;
}
}
}
We can discuss later why you are doing this at all, there are easier ways :)
You need to understand that there are two dimensions to iterate here, the rows in dataGridView2 and the columns in dGvTopics, this means you will need two looping statements, not just one.
Your current code is looping through the Rows in dataGridView2 but only for the number of columns that are in dGvTopics which is a bit confusing.
PRO TIP: Don't use arbitrary single character variable names that have no meaning. Yes i is ubiquitously used to represent index in code you will find around the web, that doesn't mean it is good practice. i should be reserved for lazy programming where there is a single, single dimension array that you are iterating over, in your example there are 4 different levels of arrays that you accessing, the meaning of i is now ambiguous.
Instead of i, use a meaningful variable name like columnIndex or topicIndex. That way when each line is reviewed in isolation, the code is more self documenting. I would even accept t or c in this code, taking the first initial from the conceptual variable meaning will help spot common errors where the wrong indexer is used for the wrong array.
Yes this make the code wordy and long, but we're not constrained by memory space in the same way as our developer ancestors, this doesn't change the size of the final executable, strive to make your code self-documenting.
If you are programming in a code-memory-constrained environment, like for micro-controllers, or tiny chipsets, then still use meaningful short variables, not arbitrarily selected characters.
Applying the above recommendation highlights this first issue:
for (int columnIndex = 0; columnIndex < dGvTopics.Columns.Count; columnIndex ++)
{
if (dataGridView2.Rows[columnIndex].Cells[0].Value.ToString().Contains(dGvTopics.Columns[columnIndex].HeaderText))
{
this.dGvTopics.Rows.Add();
this.dGvTopics.Rows[columnIndex].Cells[columnIndex].Value = dataGridView2.Rows[columnIndex].Cells[0].Value;
}
}
Now we can see that each iteration is moving down the rows, but across the cells at the same rate, meaning that only the cells in a diagonal formation will even be compared and have a value.
The next issue is that because you are only creating a row when the comparison returns true, this means that the rows in dGvTopics might be less than you are expecting, which means less than the value of i (or columnIndex) which will raise an IndexOutOfRangeException the next successful iteration after any comparison that fails.
You can avoid this problem by iterating over the rows and columns separately and adding one row in dGvTopics for every row in dataGridView2.
We can also make the code clearer by saving a reference to the currentSentence rather than referencing the sentence through the array indexers.
private void btnClassify_Click(object sender, EventArgs e)
{
// remove any existing rows, we will reprocess all records.
this.dGvTopics.Rows.Clear();
// Iterate over the rows in the list of sentences.
for (int rowIndex = 0; rowIndex < dataGridView2.Rows.Count; rowIndex ++)
{
// Create one topic row for every sentence
// row index will always be valid now.
this.dGvTopics.Rows.Add();
// save the sentence value to simplify the comparison code.
string currentSentence = dataGridView2.Rows[rowIndex].Cells[0].Value.ToString();
// iterate over the columns in the topics grid
for (int columnIndex = 0; columnIndex < dGvTopics.Columns.Count; columnIndex ++)
{
if (currentSentence.Contains(dGvTopics.Columns[columnIndex].HeaderText))
{
this.dGvTopics.Rows[rowIndex].Cells[columnIndex].Value = currentSentence;
}
}
}
}
It's not easy to comprehend why you want to do this or how this information will be used. In general for manipulating values in cells we generally recommend that databinding techniques are used instead, that way you do not access rows and cells anymore or but the underlying objects that they represent.
demonstrating this is outside of the scope of this question, but it's an avenue worth researching when you have time.
In solutions like this where there are two grids that represent the same logical component, (in this case each row in each grid represents the same sentence value) the underlying dataobject might be a single list, where one property on the object is the sentence and each topic column is a property on the same object.
Importantly, using databinding means that the next process that needs to use the information that you have displayed or edited in the grids can do so without access to or knowledge about the grids at all... Something to think about ;)
Update
This code may result in many empty cells in the topics grid. We could instead only add rows as they are needed, but to do this will require a lot more effort.
NOTE: Grids render all the cells for each row, In the last couple of rows, there may still be empty cells if at least one of the cells for that row has a value.
private void btnClassify_Click(object sender, EventArgs e)
{
// remove any existing rows, we will reprocess all records.
this.dGvTopics.Rows.Clear();
// Iterate over the rows in the list of sentences.
for (int rowIndex = 0; rowIndex < dataGridView2.Rows.Count; rowIndex ++)
{
// save the sentence value to simplify the comparison code.
string currentSentence = dataGridView2.Rows[rowIndex].Cells[0].Value.ToString();
// iterate over the columns in the topics grid
for (int columnIndex = 0; columnIndex < dGvTopics.Columns.Count; columnIndex ++)
{
if (currentSentence.Contains(dGvTopics.Columns[columnIndex].HeaderText))
{
// first we need to know what row index to add this value into
// that involves another iteration, we could store last index in another structure to make this quicker, but here we will do it from first principals.
bool inserted = false;
for(int lookupRow = 0; lookupRow < this.dGvTopics.Rows.Count; lookupRow ++)
{
// find the first row with a null cell;
if(this.dGvTopics.Rows[columnIndex].Value == null)
{
this.dGvTopics.Rows[lookupRow].Cells[columnIndex].Value = currentSentence;
inserted = true;
break;
}
}
if(!inserted)
{
this.dGvTopics.Rows.Add();
this.dGvTopics.Rows[this.dGvTopics.Rows.Count-1].Cells[columnIndex].Value = currentSentence;
}
}
}
}
}
Many thanks to Mr Chris Schaller,
According to his description, the final code changed as follows after compiling:
private void btnClassify_Click(object sender, EventArgs e)
{
// remove any existing rows, we will reprocess all records.
this.dGvTopics.Rows.Clear();
// Iterate over the rows in the list of sentences.
for (int rowIndex = 0; rowIndex < dataGridView2.Rows.Count; rowIndex++)
{
// save the sentence value to simplify the comparison code.
string currentSentence = dataGridView2.Rows[rowIndex].Cells[0].Value.ToString();
// iterate over the columns in the topics grid
for (int columnIndex = 0; columnIndex < dGvTopics.Columns.Count; columnIndex++)
{
if (currentSentence.Contains(dGvTopics.Columns[columnIndex].HeaderText))
{
// first we need to know what row index to add this value into
// that involves another iteration, we could store last index in another structure to make this quicker, but here we will do it from first principals.
bool inserted = false;
for (int lookupRow = 0; lookupRow < this.dGvTopics.Rows.Count; lookupRow++)
{
// find the first row with a null cell;
if (this.dGvTopics.Rows[lookupRow].Cells[columnIndex].Value == null)
{
this.dGvTopics.Rows[lookupRow].Cells[columnIndex].Value = currentSentence;
inserted = true;
break;
}
}
if (!inserted)
{
this.dGvTopics.Rows.Add();
this.dGvTopics.Rows[this.dGvTopics.Rows.Count - 1].Cells[columnIndex].Value = currentSentence;
}
}
}
}
}

Using DataGridView in Visual Studio C#

I have a string that contains: "# of rows, # of columns, Row'X'Col'X'=Serial#, ...
How do I create a DataGrid table with the number of rows and columns defined, and then place the serial #s into the grid.
Examples:
2,1,R1C1=111,R2C1=112,
2,2,R1C1=211,R1C2=212,R2C1=213,R2C2=214,
thanks
Below is code that does what you are asking; however I must point out some problems with this approach. First, getting the total rows and cols from the first two elements in order to create your table is risky. If that data is wrong, this code will most likely crash or possibly omit data. Example if the input is: 2,2,RXCX=.., RXCX=.., RXCX=.., RXCX=..,RXCX=, RXCX=… This line will only get the first 4 values.
Worse… this will crash… if the input is 2,2,RXCX=.., RXCX=.. Then it will crash when you try to access the 4th element in the splitArray because there isn’t a 4th element. Either way is not good.
My point is to be safe… it would be a better approach to see how much data is actually there before you create the grid. You could get how many items there are with StringArray.Length minus the first two elements. These elements will define the dimensions and allow you to check their validity. This will make sure your loops won’t go out of bounds because the supplied data was wrong. It seems redundant and error prone to supply the dimension values when you can get that info from the data itself.
I still am not 100% sure what you want to accomplish here. It looks like a search of some form. This is what I am picturing…
Looking at your (previous) screen shots it appears to me that after you type into the Serial # text box and click the “Search Txt Files” button it will search for data that came from the input string i.e. “PLX51…” and then have the grid display the “filtered” results that match (or are LIKE) what’s in the Serial # textbox. If this is true, I would ignore the RXCX vales and put the data in a single column. Then wire up an OnKeyPress event for the text box to filter the grid whenever the user types into the Serial # text box.
Otherwise I am lost as to why you would need to create the data in the fashion described. Just because the input has unnecessary data… doesn’t mean you have to use it. Just a thought.
string inputString = "2,2,R1C1=211,R1C2=212,R2C1=213,R2C2=214";
string[] splitArray = inputString.Split(',');
int totalRows = int.Parse(splitArray[0]);
int totalCols = int.Parse(splitArray[1]);
int itemIndex = 2;
// add the columns
for (int i = 0; i < totalCols; i++)
{
dataGridView1.Columns.Add("Col", "Col");
}
// add the rows
dataGridView1.Rows.Add(totalRows);
for (int i = 0; i < totalRows; i++)
{
for (int j = 0; j < totalCols; j++)
{
dataGridView1.Rows[i].Cells[j].Value = splitArray[itemIndex];
itemIndex++;
}
}

How to Get the Index of the First Item Showing in ListView in C#

Is there any way to get the index of the first item currently showing, when the list is scrollable?
I'm making a CharMap with some extensions and just found that ListView can't contain 64k items (see code below)
for (var i = char.MinValue; i < char.MaxValue; i++)
{
var c = Convert.ToChar(i);
if (!char.IsControl(c))
lv1.Items.Add(""+c);
}
so decided to load chars when scroll is at some appropriate points (ie first/last 15%) but ListView doesn't give absolute position of the scrollbar.
It does feel a little hackish, but maybe it will do the job:
int getFirstVisibleItem(ListView lv)
{
ListViewHitTestInfo HI;
for (int i = 0; i < Math.Min(lv.ClientSize.Width, lv.ClientSize.Height); i += 3)
{
HI = lv.HitTest(i, i);
if (HI.Item != null) return HI.Item.Index;
}
return -1;
}
This does not directly help with your scrolling issue but should find the first visible Item as you have asked. If your Items have extremely weird (ie non-square) shapes you may want to change the travesal code a little..
For your requirement implement ListView with custom scrollbar. So you have more control over your scroll position. You decide when to what action based on scroll position. This might be helpful Code

How to connect Tab Controls

my winForms app has a tab control which consists of two tabs (tab1 & tab2). In tab2 data is fetched in a datagridview fron a database(Product infomations).
In tab1, I've a combobox [sales analyse]which makes a user to select an option.
I now want to get access to tab2 from tab1 on cb selection, displaying me a regional sales information from the data in tab2 datagrid.
Is it possible? I don't really know wher to start
tab1 image
tab2
Expectation:
if the combobox in tab1 is selected, it should then look through the datagridview in tab2 where the (regions) North, East, West ect are and then sum the sale 13, sales 14 .. and display in the textBoxes respectively.
As your controls all sit in one Form their methods can all reference each other without any additional help.
So you can write in the SelectedIndexChanged of the ComboBox cbAnalyse
cbAnalyse_SelectedIndexChanged(object sender, EventArgs e)
{
if (cbAnalyse.SelectedItem.ToStringndex == "Sales Analysis"
{
someTextbox1.Text = ColumnSum(yourDataGridView, someColumn1) + "$";
someTextbox2.Text = ColumnSum(yourDataGridView, someColumn2) + "$";
}
This uses a small helper function, which sums up all values from one column in a DataGridView:
decimal ColumnSum(DataGridView dgv, int columnIndex)
{
decimal sum = 0m;
for (int row = 0; row < DGV.Rows.Count; row++)
if (DGV[columnIndex, row].Value != null) sum += Convert.ToDecimal(DGV[1, row].Value);
return sum;
}
Folks often run into problems when they need to refer to controls that are not sitting in either the same Form but in a 2nd, 3rd etc Form. Or when they are part of a Usercontrol, which is a custome container for holding controls.
In both cases those controls are by default private members of the other Forms or of the UserObject.
In these cases one needs to create some kind of public accessor to them, usually by a Property. And in the case of Forms, one also need to provide a reference to the other forms, often stored when opening them.
In this case, the 2nd Form often also needs a back-refrence to the 1st Form; this is often pass in in the constructor.
But in your case none of these complications matter. All you need is the patience to wire up all those TextBoxes ;-)
Update: As you also seem to have a problem getting the intermediate sums and need to allow for repeating regions in the rows of Tab 2, you also want to use a function that will calculate with a where clause:
decimal ColumnSumWhere(DataGridView dgv, int columnIndex, string region)
{
decimal sum = 0m;
for (int row = 0; row < DGV.Rows.Count; row++)
if (DGV[columnIndex, row].Value != null) &&
(DGV[regionColumn, row].Value.ToString() == region)
sum += Convert.ToDecimal(DGV[1, row].Value);
return sum;
}
If I got it right, whenever you change the value in sales analyze combo, the tab page containing data grid should should be activate.
You can set the selected index of the tab control to the the data grid tab, and it should work
this.tabControl1.SelectedIndex = 1;//Index of data grid tab

How to clear or update the content of an element in a grid in WPF

I have a WPF window with a grid with 2 rows and some columns with text and buttons, then when I click a button the code adds some rows and populates them with random textblocks and images via for loops. For example, the method I call to add a TextBlock with text s in the (i,j) cell of the grid is
public void createTextBlock(int i, int j, string s)
{
TextBlock tb = new TextBlock();
//Properties
tb.Foreground = Brushes.Navy;
tb.FontFamily = new FontFamily("Century Gothic");
tb.FontSize = 16;
tb.FontWeight = FontWeights.UltraBold;
tb.TextWrapping = TextWrapping.Wrap;
tb.VerticalAlignment = VerticalAlignment.Center;
tb.HorizontalAlignment = HorizontalAlignment.Center;
tb.Text = s;
//Add to the Grid
MyGrid.Children.Add(tb);
Grid.SetRow(tb, i);
Grid.SetColumn(tb, j);
}
and the method to add an image is similar. The problem is that when I click again, new textblocks and images are added above the old ones and I don't know how to update the content or clear it before adding another.
It is puzzling because the code (before adding rows) checks if the rows are more than 2, and if this is the case it clears all the exceeding rows:
if (MyGrid.RowDefinitions.Count > 2)
{
MyGrid.RowDefinitions.RemoveRange(2, MyGrid.RowDefinitions.Count-2);
}
but somehow this is not sufficient to clear their content... how can I do it?
EDIT (to clarify):
To add rows I use something like this (a little different because there is a switch call but it does not modify the essence)
public void createGrid(int n)
{
//remove rows if present
if (MyGrid.RowDefinitions.Count > 2)
{
MyGrid.RowDefinitions.RemoveRange(2, MyGrid.RowDefinitions.Count-2);
}
//permutation
int[] permutation = shuffle(deck.Count);
for (int i = 2; i < n + 2; i++)
{
RowDefinition row = new RowDefinition();
MyGrid.RowDefinitions.Add(row);
row.Height = new GridLength(200, GridUnitType.Pixel);
//add image
createImage(i, 0, deck[permutation[i - 2]].ImmPath);
//add textblock in center column with text chosen
//from a jagged array
createTextBlock(i, 1, value[0][i-2]);
//add textblock in right column
createTextBlock(i, 2, deck[permutation[i - 2]].Meaning);
}
So the idea is not to add new rows every time but to update the exsisting ones (or add them if needs be, createGrid can be called with different values for n). So I came up with the idea to wipe out the rows exceeding the first 2 (which contains the title and buttons) every time I call that method and add only the needed ones. This is the reason for the first check and RemoveRange.
Assuming you know the row and column of the control you want to remove you could do this
foreach (UIElement control in MyGrid.Children)
{
if (Grid.GetRow(control) == row && Grid.GetColumn(control) == col)
{
MyGrid.Children.Remove(control);
break;
}
}
The problem is this
It is puzzling because the code (before adding rows) checks if the
rows are more than 2, and if this is the case it clears all the
exceeding rows
If you are including items that you want to show below the current items, you need to increase the number of RowDefinitions instead of maintain the same.

Categories

Resources