I have a method that writes data in excel, but there are parts that have a lot of text and the method wrap the text into the column but I can´t do it in the rows because they are merged. So i get something like this:
And I need to get it like This:
Is there a way to do it by code? Using Interop Objects. Thanks for any help.
I don't know whether this is correct or not, but I've come up with the following solution. The idea is the following:
remember rows count
calculate total height of all rows in merged area
calculate percentage of every row according to total height
unmerge cells
autofit rows
remember the height of first row (i.e. data row) - a new height
main: apply percentage (on stage 3) to new height
merge cells back (with the help of rows count on stage 1)
As you can see, this method is universal - i.e. it will work on any rows count in merged area and it will honor the previous ratio of each row according to new height. You can download sample workbook with code.
VBA
Sub Test()
Call AutoFitMergedCells(Range("D11"))
End Sub
Sub AutoFitMergedCells(cell As Range)
Dim dOldHeight#, dNewHeight#, dPercent#, arow, addr, rows_count
Dim dicCells As New Dictionary, dicHeights As New Dictionary
'// turn off flickering
Application.ScreenUpdating = False
With cell
'// remember rows count for merging cells back
rows_count = .MergeArea.Count
'// every dictionary entry holds following info:
'// 1) original height of all rows in merged cells
'// 2) percentage of row's height to height of all rows in merged area
For Each arow In .MergeArea.Rows
With arow.Cells(1)
Set dicHeights = New Dictionary
dicHeights("height") = .RowHeight
dicHeights("percent") = 0
dicCells.Add Key:=.Address(0, 0), Item:=dicHeights
End With
Next
'// total height of all rows
For Each addr In dicCells.Keys()
dOldHeight = dOldHeight + dicCells(addr)("height")
Next
'// update the percentage of every row
For Each addr In dicCells.Keys()
dicCells(addr)("percent") = dicCells(addr)("height") / dOldHeight
Next
.UnMerge
.EntireRow.AutoFit
'// remember new height
dNewHeight = .RowHeight
'// this applies percentage of previous row's height to new height
For Each addr In dicCells.Keys()
Range(addr).EntireRow.RowHeight = dicCells(addr)("percent") * dNewHeight
Next
'// merge back
.Resize(rows_count).Merge
End With
Application.ScreenUpdating = True
End Sub
UPDATE
C#
using System.Diagnostics;
using Excel = Microsoft.Office.Interop.Excel;
private void ProcessMergedCells()
{
var xlApp = new Excel.Application { Visible = false, ScreenUpdating = false };
// get Excel process in order to kill it after the work is done
var xlHandle = new IntPtr(xlApp.Hwnd);
var xlProc = Process
.GetProcesses()
.First(p => p.MainWindowHandle == xlHandle);
var book = xlApp.Workbooks.Open(#"C:\AutoFitMergedCells.xlsm");
var sheet = book.Sheets[1] as Excel.Worksheet;
// obtain merged cells any way you like
// here I just populate array with arbitrary cells
var merged_ranges = new Excel.Range[]
{
sheet.Range["D11"],
sheet.Range["D13"]
};
// process merged cells
foreach(var merged_range in merged_ranges)
{
AutoFitMergedCells(merged_range);
}
// quit with saving
book.Close(SaveChanges: true);
xlApp.Quit();
// clean up
GC.Collect();
GC.WaitForFullGCComplete();
// kill Excel for sure
xlProc.Kill();
}
private void AutoFitMergedCells(Excel.Range merged_range)
{
double dOldHeight = 0d, dNewHeight = 0d;
var dicCells = new Dictionary<string, Dictionary<string, double>>();
// remember rows count for merging cells back
int rows_count = merged_range.MergeArea.Count;
// every dictionary entry holds following info:
// 1) original height of all rows in merged cells
// 2) percentage of row's height to height of all rows in merged area
foreach (Excel.Range the_row in merged_range.MergeArea.Rows)
{
// we need only top-left cell
var first_cell = the_row.Cells[1];
var dicHeights = new Dictionary<string, double>
{
["height"] = first_cell.RowHeight,
["percent"] = 0d
};
dicCells[first_cell.Address[0, 0]] = dicHeights;
}
// total height of all rows
foreach (string addr in dicCells.Keys)
dOldHeight += dicCells[addr]["height"];
// update the percentage of every row
foreach (string addr in dicCells.Keys)
dicCells[addr]["percent"] = dicCells[addr]["height"] / dOldHeight;
// unmerge range and autofit
merged_range.UnMerge();
merged_range.EntireRow.AutoFit();
// remember new height
dNewHeight = merged_range.RowHeight;
// this applies percentage of previous row's height to new height
var sheet = merged_range.Parent;
foreach (string addr in dicCells.Keys)
sheet.Range[addr].EntireRow.RowHeight = dicCells[addr]["percent"] * dNewHeight;
// merge back
merged_range.Resize[rows_count].Merge();
}
I don't recall where I saw this, but I once saw a hack to accomplish this. In a nutshell, you take the text in question from the merged cell, paste it into a non-merged cell in the same column and do an autofit on that one cell to see how tall it should be. Then you take that height of the safe cell and manually set it to each of the merged rows, dividing by the total number of rows that were merged.
It's ugly, but it does work. In your example, if it's always two rows, that makes it a lot easier because you can always divide by two and increment the row count by two. If not, you just need to factor that in. Assuming C1 is our "safe cell," it would looks something like this:
Excel.Range testCell = ws.Cells[1, 3];
testCell.WrapText = true;
for (int row = 11; row < ws.UsedRange.Rows.Count; row += 2)
{
testCell.Value2 = ws.Cells[row, 4].Value2;
testCell.Rows.AutoFit();
ws.Range[string.Format("{0}:{1}", row, row + 1)].RowHeight = testCell.RowHeight / 2;
}
Related
I am using a .xltx template file with a logo in the A1 cell and then an empty row with a background colour on A2 (which will act as my column headings row). When the excel document gets generated, I want to set the rowHeight of the logo row to a specific height and set the A2 row to another specific height. However, both rows' height are set exactly the same (the height of the A2 row).
excelWorksheet = excelApplication.ActiveSheet;
//Getting the image row range
var imageRowRange = ((Worksheet)excelWorksheet).Range["A1", "A1"];
var entireImageRowRange = imageRowRange.EntireRow;
//Setting the image rowHeight
imageRowRange.RowHeight = 48.75;
//Getting the second row which acts as the column heading row
var rowRange = ((Worksheet)excelWorksheet).get_Range("2:1");
var entireRowRange = rowRange.EntireRow;
rowRange.WrapText = true;
if (setCustomHeight())
rowRange.RowHeight = 0.75 * int.Parse(getCustomHeight());
else
entireRowRange.AutoFit();
rowRange.VerticalAlignment = XlVAlign.xlVAlignBottom;
rowRange.HorizontalAlignment = XlHAlign.xlHAlignCenter;
How can I set the first row individually from the second row please?
you can change get_Range("2:1"); to get_Range("2:2");, so that you only target the second row.
Range 2:1 means all rows from 2 to 1, range 2:2 means all rows from 2 to 2. You can test this by typing the range in Excel's "Name" box in the top left.
I have encountered the following problem while generating Excel files using ClosedXML library: rows containing merged cells with wrapped text.
I cannot blame the library, since the problem is also happening when trying to manually perform autoheight (double click the rows separator).
at the movement, there is no solution for auto fit row height for WrapText + Merge&Center at Excel. But I have a trick that you can do:
In the same row, when you set value for Merge&Center + Wraptext cell (ex:A1:C1) then set another cell in that row too (ex: G1) which not using Merge&Center but using WrapText (for text-downline of course).
Set width of G1 = width of A1:C1.
Set auto fit row height for G1 (Cause Excel will not allow auto fit like i mentioned above for A1:C1).
Now let's hide the value in G1 by changing the text-color to white.
=> It will auto fit row height for (WrapText + Merge&Center) A1:C1 up on G1.
A solution I found for this problem is the following: copy the data from the merged cells outside the defined print area of the worksheet in a non-merged column, ensuring that the destination column has a width very close to the merged columns width one and then performing autofit.
The code looks like this:
private Dictionary<int, IList<int>> _RowAutofitBufferToMergedColsMapping = new Dictionary<int, IList<int>>();
// tells the exporter what column to use as a buffer
public void RegisterAutofitMapping(int startCol, int stopCol, int bufferCol)
{
var mergedCols = Enumerable.Range(startCol, stopCol - startCol + 1).ToList();
if (_RowAutofitBufferToMergedColsMapping.ContainsKey(bufferCol))
throw new ArgumentException(String.Format("Current worksheet already contains a mapping for buffer column {0}", bufferCol));
_RowAutofitBufferToMergedColsMapping[bufferCol] = mergedCols;
Worksheet.Column(bufferCol).Width = mergedCols.Sum(item => Worksheet.Column(item).Width + ColumnSeparatorWidth);
}
// performs row autofit
public void RowAutofit(int rowNo, int startCol, int stopCol, bool merge = true)
{
// finding mapping to use for autofit
IList<int> vals = Enumerable.Range(startCol, stopCol - startCol + 1).ToList();
String valsStr = String.Join(",", vals);
var mappingKey = _RowAutofitBufferToMergedColsMapping.Keys.FirstOrDefault(key => vals.SequenceEqual(_RowAutofitBufferToMergedColsMapping[key]));
if (mappingKey == 0)
throw new ArgumentException(String.Format("Could not mapping for provided columns - {0}", valsStr));
var range = Worksheet.Worksheet.Range(rowNo, startCol, rowNo, stopCol);
if (merge)
range.Merge();
range.Style.Alignment.SetWrapText();
if (copyStyles)
ClosedXmlExporter.CopyStyles(this, this, rowNo, startCol, rowNo, mappingKey, CopyStyleOptions.CopyAll());
var sourceValue = Worksheet.Cell(rowNo, startCol).Value;
Worksheet.Cell(rowNo, mappingKey)
.SetValue(sourceValue)
.Style.Alignment.SetWrapText(true);
Worksheet.Column(AutofitDummyCol).AdjustToContents(rowNo, rowNo);
}
Inspired by Excel tips.
private void mergeCellAutoFit(Excel.Range range) //range had value
{
range.HorizontalAlignment = 7;//format center across Selection
range.WrapText = true;//set wrap text
double dRowHeight = Convert.ToDouble(range.RowHeight);//get row heigth now
range.MergeCells = true; //merge cell
range.EntireRow.RowHeight = dRowHeight; //perfect row heigth
range.HorizontalAlignment = 2; //align text left
}
I am trying to make the application generate an Excel file from a list of string type and a list of image type. I have the user enter a line then makes them take a screenshot and so on until G is pressed and then it should generate an Excel file with the format as so:
Row 1- Title-0-
Row 2- Header-1-
Row 3- Image-0-
Row 4- Header-2-
Row 5- Image-1-
Row 6- Header-3-
Row 7- Image-2-
Row 8- Header-4-
Row 9- Image-3-
Row 10- Header-5-
Row 11- Image-4-
...and so on until its done all in the collections.
I have created the List and List and I know that they both contain Strings and Images before I hit G as I have looked inspected the collections debug mode.
This is the code I have so far and the excel file looks right except there are no Images to be seen however it is re-sizing the rows to the pictures heights. I have never worked with Images before so think I could be missing something important but not sure what.
The Collections are passed into this method from a calling method
String collection is named "withHeadersList",
Image collection is named "withImgList".
Generate Excel Method:
public static bool GenerateTestPlan(List<String> withHeadersList, List<Image> withImgList, string stringOutputPath)
{
ExcelPackage newExcelPackage = CreateExcelPackage(withHeadersList[0]);
ExcelWorksheet newExcelWorksheet = CreateWorkSheet(newExcelPackage, "Sheet1");
SetCellValue(newExcelWorksheet, 1, 1, withHeadersList[0]); //Title
newExcelWorksheet.Row(1).Style.Font.Size = 35;
newExcelWorksheet.Row(1).Style.Font.Bold = true;
int pRowIndex = 3;
int hRowIndex = 2;
for (int i = 1; i < withHeadersList.Count; i++)
{
SetCellValue(newExcelWorksheet, hRowIndex, 1, withHeadersList[i]);
newExcelWorksheet.Row(hRowIndex).Style.Font.Size = 20;
newExcelWorksheet.Row(pRowIndex).Height = withImgList[i - 1].Height; //Set row height to height of screenshot
var img = newExcelWorksheet.Drawings.AddPicture(withHeadersList[i], withImgList[i - 1]); //Add Images (THINK THIS LAST PARAMETER IS THE PROBLEM)
img.SetPosition(pRowIndex, Pixel2MTU(2), 1, Pixel2MTU(2));
img.SetSize(withImgList[i - 1].Width, withImgList[i - 1].Height);
hRowIndex += 2;
pRowIndex += 2;
}
SaveExcelPackage(newExcelPackage, stringOutputPath);
return true;
}
Excel File here
As you see it's like the images are just not being rendered.
Your issue is most certainly with this line:
img.SetPosition(pRowIndex, Pixel2MTU(2), 1, Pixel2MTU(2));
I'm not sure why you are converting pixels to anything considering SetPosition is looking for the offset in pixels. From the metadata:
// Summary:
// Set the top left corner of a drawing. Note that resizing columns / rows after
// using this function will effect the position of the drawing
//
// Parameters:
// Row:
// Start row
//
// RowOffsetPixels:
// Offset in pixels
//
// Column:
// Start Column
//
// ColumnOffsetPixels:
// Offset in pixels
public void SetPosition(int Row, int RowOffsetPixels, int Column, int ColumnOffsetPixels);
I would recommend just passing through small values, such as 2, for the RowOffestPixels and ColumnOffsetPixels parameters:
img.SetPosition(pRowIndex, 2, 1, 2);
I found a method called Pixel2MTU(int pixels) on codeproject from a quick google search. The method is as follows:
public int Pixel2MTU(int pixels)
{
int mtus = pixels * 9525;
return mtus;
}
If this is the same method you are using, your images might be at the very far bottom right of your excel document.
we use this two methods to adjust column length based on Column content and header resp.
ListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
ListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);
But how to adjust based on both? i.e. adjust to the longest length for header and column content.
lvw.Columns[0].Width = -2
See remarks in MSDN for details:
http://msdn.microsoft.com/en-us/library/system.windows.forms.columnheader.width.aspx
Also note that MSDN says that 'To autosize to the width of the column heading, set the Width property to -2.', but actually it works for column heading AND column contents.
Here is a code to prove that:
lvw.Columns.Add(new String('x', 25)); // short header
lvw.Items.Add(new String('x', 100)); // long content
lvw.Columns[0].Width = -2;
// in result column width will be set to fit content
As answered here, calling both resizing options do the job :
myListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
myListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);
This is what I use to adjust column width to both content and header:
public static void autoResizeColumns(ListView lv)
{
lv.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
ListView.ColumnHeaderCollection cc = lv.Columns;
for (int i = 0; i < cc.Count; i++)
{
int colWidth = TextRenderer.MeasureText(cc[i].Text, lv.Font).Width + 10;
if (colWidth > cc[i].Width)
{
cc[i].Width = colWidth;
}
}
}
Example use:
autoResizeColumns(listView1);
The method isn't that well tested, but at least it works in the context I'm using it in.
It's possible indeed to use MeasureText and then to calculate how much space is left and somehow distribute between all columns. But this is quick-and-dirty approach which I have quickly coded:
/// <summary>
/// Enables autoresizing for specific listview.
/// You can specify how much to scale in columnScaleNumbers array - length of that array
/// should match column count which you have.
/// </summary>
/// <param name="listView">control for which to enable auto-resize</param>
/// <param name="columnScaleNumbers">Percentage or numbers how much each column will be scaled.</param>
private void EnableAutoresize(ListView listView, params int[] columnScaleNumbers)
{
listView.View = View.Details;
for( int i = 0; i < columnScaleNumbers.Length; i++ )
{
if( i >= listView.Columns.Count )
break;
listView.Columns[i].Tag = columnScaleNumbers[i];
}
listView.SizeChanged += lvw_SizeChanged;
DoResize(listView);
}
void lvw_SizeChanged(object sender, EventArgs e)
{
ListView listView = sender as ListView;
DoResize(listView);
}
bool Resizing = false;
void DoResize( ListView listView )
{
// Don't allow overlapping of SizeChanged calls
if (!Resizing)
{
// Set the resizing flag
Resizing = true;
if (listView != null)
{
float totalColumnWidth = 0;
// Get the sum of all column tags
for (int i = 0; i < listView.Columns.Count; i++)
totalColumnWidth += Convert.ToInt32(listView.Columns[i].Tag);
// Calculate the percentage of space each column should
// occupy in reference to the other columns and then set the
// width of the column to that percentage of the visible space.
for (int i = 0; i < listView.Columns.Count; i++)
{
float colPercentage = (Convert.ToInt32(listView.Columns[i].Tag) / totalColumnWidth);
listView.Columns[i].Width = (int)(colPercentage * listView.ClientRectangle.Width);
}
}
}
// Clear the resizing flag
Resizing = false;
}
And depending how many columns you have - you specify each column "percentage" or simply number. For example for 3 columns - call looks like this:
EnableAutoresize(listView1, 6, 3, 1);
This will distribute column sizes as:
6 * 100% / (6 + 3 + 1) = 60% for first column,
30% for next and 10% for remaining.
This is somehow poor man quick implementation. :-)
In my case, I do this through the next steps (for two columns of data):
Creating a ColumnHeader object for each column.
Setting the size by AutoResize based on HeaderSize (on both columns)
Store that value in a Integer variable
Setting the size by AutoResize based on ColumnContent (on both columns)
Updating the value of each Integer variable through the Max criteria between the old value and the new value (for each column).
Setting the column width size for each ColumnHeader object.
In VB.NET:
'Create two header objects as ColumnHeader Class
Dim header1, header2 As ColumnHeader
'Construcción de los objetos header
header1 = New ColumnHeader
header1.Text = "ID"
header1.TextAlign = HorizontalAlignment.Right
header1.Width = 10
header2 = New ColumnHeader
header2.Text = "Combinaciones a Procesar"
header2.TextAlign = HorizontalAlignment.Left
header2.Width = 10
'Add two columns using your news headers objects
ListView.Columns.Add(header1)
ListView.Columns.Add(header2)
'Fill three rows of data, for each column
ListView.Items.Add(New ListViewItem({"A1", "B1"}))
ListView.Items.Add(New ListViewItem({"A2", "B2"}))
ListView.Items.Add(New ListViewItem({"A3", "B3"}))
'Change the size of each column
Dim headsz1, headsz2 As Integer
SelectionInTable.ListView.AutoResizeColumn(0, ColumnHeaderAutoResizeStyle.HeaderSize)
SelectionInTable.ListView.AutoResizeColumn(1, ColumnHeaderAutoResizeStyle.HeaderSize)
headsz1 = header1.Width
headsz2 = header2.Width
SelectionInTable.ListView.AutoResizeColumn(0, ColumnHeaderAutoResizeStyle.ColumnContent)
SelectionInTable.ListView.AutoResizeColumn(1, ColumnHeaderAutoResizeStyle.ColumnContent)
headsz1 = Math.Max(headsz1, header1.Width)
headsz2 = Math.Max(headsz2, header2.Width)
header1.Width = headsz1
header2.Width = headsz2
Here's a C# solution that can be used for any ListView. It assumes your column count and headers won't change for any given list view. Get rid of the listViewHeaderWidths dictionary if you want to recalculate header widths every time (if headers change, or number of columns changes).
private Dictionary<string, int[]> listViewHeaderWidths = new Dictionary<string, int[]>();
private void ResizeListViewColumns(ListView lv)
{
int[] headerWidths = listViewHeaderWidths.ContainsKey(lv.Name) ? listViewHeaderWidths[lv.Name] : null;
lv.BeginUpdate();
if (headerWidths == null)
{
lv.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);
headerWidths = new int[lv.Columns.Count];
for (int i = 0; i < lv.Columns.Count; i++)
{
headerWidths[i] = lv.Columns[i].Width;
}
listViewHeaderWidths.Add(lv.Name, headerWidths);
}
lv.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
for(int j = 0; j < lv.Columns.Count; j++)
{
lv.Columns[j].Width = Math.Max(lv.Columns[j].Width, headerWidths[j]);
}
lv.EndUpdate();
}
Anton Kedrov answer is best one but in my case i have a listview with more than 50 columns and i update its data frequently in this case i notice listview's this.AutoResizeColumns performs much faster work so i m writing this solution also
First Method by setting with to -2
public void AutoUpdateColumnWidth(ListView lv)
{
for (int i = 0; i <= lv.Columns.Count - 1; i++) {
lv.Columns(i).Width = -2;
}
}
Second method i used (less flicker on multiple calls)
public void AutoUpdateColumnWidth(ListView lv)
{
ListViewItem nLstItem = new ListViewItem(lv.Columns(0).Text);
for (int i = 1; i <= lv.Columns.Count - 1; i++) {
nLstItem.SubItems.Add(lv.Columns(i).Text);
}
v.Items.Add(nLstItem);
lv.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
lv.Items.RemoveAt(nLstItem.Index);
}
This is simple (although it took me a while to figure out)...
We know that the width must be at least as great as the column headers, so that we see all of the header text. Beyond that, the width can expand larger to accommodate contents. Hence, we do the following:
Autosize the columns to header.
Iterate through the columns and set the minimum width property for each column to the current column width (which guarantees your columns will never get too small to see the header).
From now on, autosize columns by content.
It is not necessary to track widths separately and reset them as other posters suggest. Setting the minimum width for the column solves the issue until the header text is changed, in which case you set the minimum width to 0, autosize just the modified column, and then set the minimum width to the current width again.
EDIT: My apologies, I forgot that I was not using the standard listview, but instead the 3rd party product BetterListView (a free version is available). The standard listview columns don't appear to support minimum width. I do recommend BetterListView highly as a great alternative (much better feature set and performance).
I'm working with an excel object in c#. I want to auto-fit the columns, but like this: I want the columns' width to be 5 bigger than what the AutoFit method set.
How can I get the width after AutoFit() is used?
How can I make the columns 5 bigger than this width?
If you wish to use the Selection object and have IntelliSense with early binding, you need to cast the Selection object to a Range first:
Excel.Range selectedRange = (Excel.Range)myExcelApp.Selection;
selectedRange.Columns.AutoFit();
foreach (Excel.Range column in selectedRange.Columns)
{
column.ColumnWidth = (double)column.ColumnWidth + 5;
}
-- Mike
Assuming that you are on cell A1 & have long text in it, following code will make the column Autofit and then increase the width by 5 characters.
Selection.Columns.Autofit
Selection.Columns(1).ColumnWidth = Selection.Columns(1).ColumnWidth + 5
Try to loop through your rows to get the text length of it:
var row = 1;
ws.Column(1).AutoFit(ws.Cells[row, 1].Text.Length + 5);
Where ws is your Worksheet:
var pck = new ExcelPackage();
var ws = pck.Workbook.Worksheets.Add("Plan1")
Try Like this,
ExcelWorksheet ws = pck.Workbook.Worksheets.Add("Sheet1");
//Load the datatable into the sheet, starting from cell A1. Print the column names on row 1
ws.Cells["A1"].LoadFromDataTable(data_table, true);
//Set full Sheet Auto Fit
ws.Cells[1, 1, data_table.Rows.Count, data_table.Columns.Count].AutoFitColumns();