Select collection of cells - c#

I need to select a collection of cells in a worksheet. I could find how to select a range, but not when the cells are "isolated".
For example "$D$4", $G$9" ...
My code:
var excelApp = Globals.ThisAddIn.Application;
List<string> unlockedCells = new List<string>();
foreach (_Excel.Range cells in excelApp.ActiveSheet.UsedRange)
{
if (!cells.Locked)
{
unlockedCells.Add(cells.Address);
}
}
unlockedCells.ForEach(_c =>
{
excelApp.Range[_c].Select();
});
The problem here is that every time a new range is selected, the previous selection is lost.
Another approach. It doesn't work, raises exception Exception from HRESULT: 0x800A03EC at Microsoft.Office.Interop.Excel._Application.get_Range(Object Cell1, Object Cell2)
The range I get is the following: "$D$8,$E$8,$D$9,$E$9,$D$10,$E$10,$D$11,$E$11,$D$12,$E$12"
StringBuilder output = new StringBuilder();
...
output.Append(String.Format("{0},", cells.Address));
string rangeDef = output.ToString().Left(output.Length - 1);
excelApp.Range[rangeDef].Select();
How could I achieve it?
Office Version 2016

Although the excelApp itself does have a Range property, the workbook or worksheet is ambiguous. Instead, try to reference the Range of a certain worksheet:
StringBuilder output = new StringBuilder();
output.Append("C4,");
output.Append("D5,");
output.Append("E6,");
string rangeDef = output.ToString().Substring(0, output.Length - 1);
Worksheet worksheet = excelApp.ActiveSheet;
Range range = worksheet.Range[rangeDef];
range.Value = "test";
Worked for me for some minimal testing.

List<string> cells = new List<string>();
cells.Add("$C$4");
cells.Add("$D$5");
cells.Add("$H$10");
for (int idx = 0; idx < cells.Count; idx++)
cells[idx] = String.Format("'{0}'!{1}", (excelApp.ActiveSheet as _Excel.Worksheet).Name, cells[idx]);
string rangeDef = String.Format("={0}", String.Join(";", cells));
var sheet = (excelApp.ActiveSheet as _Excel.Worksheet).get_Range(rangeDef, Type.Missing).Select();
It seems I was missing the worksheet name.

Related

C# Read column from CSV file only if column name exists in list

I have a list of column headers that are read from a file into a List:
// Read column headers from text file
List<string> colHeads = new List<string>();
string[] lines = File.ReadAllLines("C:\\DATA\\MLData\\colHeads.txt");
string[] spl = new string[100];
foreach (string line in lines)
{
spl = line.Split('=');
colHeads.Add(spl[0].Trim());
}
I would like to then read a CSV file but only read the columns that are in the list:
// Read CSV file
string[] lines = File.ReadAllLines("C:\\DATA\\MLData\hist.csv");
string[] spl = new string[500];
foreach (string line in lines)
{
spl = line.Split(',');
var rec = new Record()
{
Name = spl[1],
ident = float.Parse(spl[4], CultureInfo.InvariantCulture.NumberFormat),
location = float.Parse(spl[5], CultureInfo.InvariantCulture.NumberFormat),
...
...
};
}
Example of colHeads.txt....
Name
ident
location
...
Example of hist.csv...
Name,yob,ident,level,location,score1
John B,1981,23,3,GB,54
There are more columns in hist.csv than colHeads.txt so I need a way to read the csv file by column name rather than column number, ignoring the columns that are not in the list. The variables I'm assigning to match the column names exactly.
Assuming that colHeads contains a list of header names, you can try something like this.
location = colHeads.Contains("location") ? float.Parse(spl[5], CultureInfo.InvariantCulture.NumberFormat) : 0
If the set of available columns are always the same, and you just want to change the columns you read, this will probably work fine for basic use.
var colHeads = File.ReadLines("colHeads.txt").ToHashSet();
var lines = File.ReadLines("hist.csv");
var headers = lines.First().Split(',');
var indexes = new Dictionary<string, int>();
var records = new List<Record>();
for (int i = 0; i < headers.Length; i++)
{
indexes[headers[i]] = colHeads.Contains(headers[i]) ? i : -1;
}
int nameIndex = indexes[nameof(Record.Name)];
int identIndex = indexes[nameof(Record.ident)];
int locationIndex = indexes[nameof(Record.location)];
//...
foreach (var line in lines.Skip(1))
{
var values = line.Split(',');
var record = new Record();
records.Add(record);
if (nameIndex >= 0) record.Name = values[nameIndex];
if (identIndex >= 0) record.ident = float.Parse(values[identIndex]);
if (locationIndex >= 0) record.location = values[locationIndex];
//...
}
First we read the header from the hist.csv file and assign indexes.
Next, using these indexes, we set the properties of the record.

Write a List<string> to an Excel.Range

I have the below excel Range:
workSheet.Range[workSheet.Cells[12, 2], workSheet.Cells[5000, 2]]
I have converted this excel range to a List and performed some data manipulations on the list.
object[,] cellValues = (object[,])inputRng.Value2;
List<string> lst = cellValues.Cast<object>().ToList().ConvertAll(x => Convert.ToString(x));
I want to assign the list back to the Excel Range.
Here's some starting code for you that does what you want, it could probably be refined.
Your main "issue" is your conversion from the multidimensional array (object[,] cellValues = (object[,])excelRange.Value;) to the List<string> - if you could somehow keep this as a multidimensional array, then that would help your "setting" code - it depends what your other code is doing.
When you want to "set" your changes back, I've shown 2 ways to do it:
either iterate through the cells (ranges) and set each value
or provide a multi-dimensional array of values in advance and get the range to use them in one shot
Here:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.Office.Interop.Excel;
namespace WindowsFormsApplication10
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
// Create starting spreadsheet
Microsoft.Office.Interop.Excel.Application excelApp;
Microsoft.Office.Interop.Excel.Workbook excelWorkbook;
Microsoft.Office.Interop.Excel.Worksheet excelWorksheet;
excelApp = new Microsoft.Office.Interop.Excel.Application();
excelApp.Visible = true;
excelWorkbook = excelApp.Workbooks.Add();
excelWorksheet = excelWorkbook.Sheets.Add();
excelWorksheet.Activate();
Microsoft.Office.Interop.Excel.Range excelRange = excelWorksheet.Range[excelWorksheet.Cells[12, 2], excelWorksheet.Cells[5000, 2]];
// Turn off updating to make it faster
excelApp.ScreenUpdating = false;
// Set some initial data
int i = 1;
foreach (Microsoft.Office.Interop.Excel.Range cell in excelRange.Cells)
{
cell.Value = i;
i++;
}
// Get the data from those cells as a list of strings
object[,] cellValues = (object[,])excelRange.Value;
List<string> lst = cellValues.Cast<object>().ToList().ConvertAll(x => Convert.ToString(x));
// Modify the strings in some way
for (int l = 0; l < lst.Count; l++)
{
lst[l] = lst[l] + "modified";
}
// Here are some different ways set the "cells" back
// Set the cells back with the changes
//------------------------------------
// Option 1: using a multidimensional array
/*
object[,] cellValuesToWrite = new string[excelRange.Rows.Count, excelRange.Columns.Count];
int z = 0;
foreach (string str in lst)
{
cellValuesToWrite[z,0] = lst[z];
z++;
}
excelRange.Value2 = cellValuesToWrite;
*/
// Option 2: iterating the range of cells and "setting" the value
/*
int z = 0;
foreach (Microsoft.Office.Interop.Excel.Range cell in excelRange.Cells)
{
cell.Value = lst[z];
z++;
}
excelRange.Value2 = lst;
*/
// Turn updating back on
excelApp.ScreenUpdating = true;
}
}
}

Select and merge a range of cells

I am trying to select the range A1 to C3 to affect a value but this code is not working:
worksheet.Select["A1:C3"].Value = "toto";
I am able to affect the value to each of the cell with this code (but that's not what I want):
worksheet.Cells["A1:C3"].Value = "toto";
I want to merge all cells from A1 to C3, and that this new cell contains toto value;
You first have to merge the cells like this:
worksheet.Cells["A1:C3"].Merge = true;
then to set the value you would either do this:
worksheet.Cells["A1:C3"].Value = "toto";
or set A1 to the value (since it's merged)
worksheet.Cells["A1"].Value = "toto";
Kelsey's method is more diect but if you want to use the Select methods for some reason:
[TestMethod]
public void MergeCellTest()
{
var existingFile = new FileInfo(#"c:\temp\temp.xlsx");
if (existingFile.Exists)
existingFile.Delete();
using (var package = new ExcelPackage(existingFile))
{
var workbook = package.Workbook;
var worksheet = workbook.Worksheets.Add("newsheet");
worksheet.Select("A1:C3");
worksheet.SelectedRange.Merge = true;
worksheet.SelectedRange.Value = "toto";
package.Save();
}
}

How do you set the value of a cell using Excel Dna?

I've got the following code in my Excel DNA plugin
In my AutoOpen method I put the following code:
ExcelIntegration.RegisterUnhandledExceptionHandler(ex => ex.ToString());
I've got the following function that gets called from my excel sheet.
[ExcelFunction(Category = "Foo", Description = "Sets value of cell")]
public static Foo(String idx)
{
Excel.Application app = (Excel.Application)ExcelDnaUtil.Application;
Excel.Workbook wb = app.Workbooks[1];
Excel.Worksheet ws = GetSheet("Main");
// This gives us the row
Excel.Name idxRange = wb.Names.Item("COL_Main_Index");
var row = (int)app.WorksheetFunction.Match(idx, idxRange.RefersToRange, 0);
// Get the Column
Excel.Name buyOrderRange = wb.Names.Item("COL_Main_BuyOrder");
var col = (int)buyOrderRange.RefersToRange.Cells.Column;
// create the range and update it
Excel.Range r = (Excel.Range)ws.Cells[row, col];
r.Value ="Foo";
}
The issue is that I can't actually set any cell values. When I call the method it causes an error on the last line.
My error handler gives me the followign error:
{System.Runtime.InteropServices.COMException (0x800A03EC)
I've also tried to set the cell value like so:
r = (Excel.Range)ws.Cells[12, 22];
const int nCells = 1;
Object[] args1 = new Object[1];
args1[0] = nCells;
r.GetType().InvokeMember("Value2", BindingFlags.SetProperty, null, r, args1);
With the same result.
Can anyone point to what I might be doing wrong here?
Actually, you can write in any cell if you do this in an async job as a macro.
Simple Example:
using ExcelDna.Integration;
using Excel = Microsoft.Office.Interop.Excel;
[ExcelFunction(Category = "Foo", Description = "Sets value of cell")]
public static Foo(String idx)
{
Excel.Application app = (Excel.Application)ExcelDnaUtil.Application;
Excel.Range range = app.ActiveCell;
object[2,2] dummyData = new object[2, 2] {
{ "foo", "bar" },
{ 2500, 7500 }
};
var reference = new ExcelReference(
range.Row, range.Row + 2 - 1, // from-to-Row
range.Column - 1, range.Column + 2 - 1); // from-to-Column
// Cells are written via this async task
ExcelAsyncUtil.QueueAsMacro(() => { reference.SetValue(dummyData); });
// Value displayed in the current cell.
// It still is a UDF and can be executed multiple times via F2, Return.
return "=Foo()";
}
Writing into a single Cell:
int row = 5;
int column = 6;
var reference = new ExcelReference(row - 1, column - 1);
ExcelAsyncUtil.QueueAsMacro(() => { reference.SetValue("Foobar"); });
// edit:
just fyi, you can also use:
private void WriteArray(object[,] data)
{
Excel.Application app = (Excel.Application)ExcelDnaUtil.Application;
Excel.Worksheet worksheet= (Excel.Worksheet)app.ActiveWorkbook.ActiveSheet;
Excel.Range startCell = app.ActiveCell;
Excel.Range endCell = (Excel.Range)worksheet.Cells[startCell.Row + data.GetLength(0) - 1, startCell.Column + data.GetLength(1) - 1];
var writeRange = worksheet.Range[startCell, endCell];
writeRange.Value2 = data;
}
And then:
object[,] data = ...;
ExcelAsyncUtil.QueueAsMacro(() =>
{
WriteArray();
});
Excel does not allow you to set other worksheet cells from within a user-defined worksheet function. This is to preserve the dependency tree Excel uses to manage the recalculation. This is true whether you are using VBA, the C API or Excel-DNA.
Best is to add a ribbon button, context menu or shortcut key to effect the changes via a macro.
There are some ugly workarounds, but I would not recommend it.
Here is the answer using .NET VB
Dim row As Integer = 7
Dim column As Integer = 7
Dim reference = New ExcelReference(row, column)
ExcelAsyncUtil.QueueAsMacro(Sub()
reference.SetValue("Test")
End Sub)

Bullet points in Word with c# Interop

I have the following code which is supposed to add a bulleted list to a word document that I'm generating automatically. From other answers I believe the code is correct, but the result doesn't produce any bullet points at all, it doesn't seem to apply the indent either.
Any Ideas?
Microsoft.Office.Interop.Word.Paragraph assets;
assets = doc.Content.Paragraphs.Add(Type.Missing);
// Some code to generate the text
foreach (String asset in assetsList)
{
assetText = assetText + asset + "\n";
}
assets.Range.ListFormat.ApplyBulletDefault(Type.Missing);
// Add it to the document
assets.Range.ParagraphFormat.LeftIndent = -1;
assets.Range.Text = assetText;
assets.Range.InsertParagraphAfter();
This happens because you're adding multiple paragraphs to the range after the range (it seems that setting the Text property is equivalent to InsertAfter). You want to InsertBefore the range so that the formatting you set gets applied.
Paragraph assets = doc.Content.Paragraphs.Add();
assets.Range.ListFormat.ApplyBulletDefault();
string[] bulletItems = new string[] { "One", "Two", "Three" };
for (int i = 0; i < bulletItems.Length; i++)
{
string bulletItem = bulletItems[i];
if (i < bulletItems.Length - 1)
bulletItem = bulletItem + "\n";
assets.Range.InsertBefore(bulletItem);
}
Notice that we add an End of Paragraph mark to all items except the last one. You will get an empty bullet if you add one to the last.
This is based on Tergiver's answer. The difference is it inserts the list items in the correct order after the initially created paragraph. For your own use make the starting range equal to the item you want to insert the list after.
Paragraph assets = doc.Content.Paragraphs.Add();
rng = assets.Range;
rng.InsertAfter("\n");
start = rng.End;
end = rng.End;
rng = _oDoc.Range(ref start, ref end);
object listType = 0;
rng.ListFormat.ApplyBulletDefault(ref listType);
string[] bulletItems = new string[] { "One", "Two", "Three" };
for (int i = 0; i < bulletItems.Length; i++)
{
string bulletItem = bulletItems[i];
if (i < RowCount - 1)
bulletItem = bulletItem + "\n";
rng.InsertAfter(bulletItem);
}
Please note I don't really understand what I'm doing with the range here. This solution was arrived at after considerable trial and error. I suspect it may have to do with the fact that I'm reusing the same range and Tergiver's solution is grabbing a new range each time the range is accessed. I particularly don't understand the following lines:
rng.InsertAfter("\n");
start = rng.End;
end = rng.End;
rng = _oDoc.Range(ref start, ref end);
Generally any alterations to the above code and the list gets intermingled with the previous element. If somebody could explain why this works, I'd be grateful.
You can try below code block if you want list-sublist relations:
static void Main(string[] args)
{
try
{
Application app = new Application();
Document doc = app.Documents.Add();
Range range = doc.Range(0, 0);
range.ListFormat.ApplyNumberDefault();
range.Text = "Birinci";
range.InsertParagraphAfter();
ListTemplate listTemplate = range.ListFormat.ListTemplate;
//range.InsertAfter("Birinci");
//range.InsertParagraphAfter();
//range.InsertAfter("İkinci");
//range.InsertParagraphAfter();
//range.InsertAfter("Üçüncü");
//range.InsertParagraphAfter();
Range subRange = doc.Range(range.StoryLength - 1);
subRange.ListFormat.ApplyBulletDefault();
subRange.ListFormat.ListIndent();
subRange.Text = "Alt Birinci";
subRange.InsertParagraphAfter();
ListTemplate sublistTemplate = subRange.ListFormat.ListTemplate;
Range subRange2 = doc.Range(subRange.StoryLength - 1);
subRange2.ListFormat.ApplyListTemplate(sublistTemplate);
subRange2.ListFormat.ListIndent();
subRange2.Text = "Alt İkinci";
subRange2.InsertParagraphAfter();
Range range2 = doc.Range(range.StoryLength - 1);
range2.ListFormat.ApplyListTemplateWithLevel(listTemplate,true);
WdContinue isContinue = range2.ListFormat.CanContinuePreviousList(listTemplate);
range2.Text = "İkinci";
range2.InsertParagraphAfter();
Range range3 = doc.Range(range2.StoryLength - 1);
range3.ListFormat.ApplyListTemplate(listTemplate);
range3.Text = "Üçüncü";
range3.InsertParagraphAfter();
string path = Environment.CurrentDirectory;
int totalExistDocx = Directory.GetFiles(path, "test*.docx").Count();
path = Path.Combine(path, string.Format("test{0}.docx", totalExistDocx + 1));
app.ActiveDocument.SaveAs2(path, WdSaveFormat.wdFormatXMLDocument);
doc.Close();
Process.Start(path);
}
catch (Exception exception)
{
throw;
}
}
Attention this point: If you don't know input length, you must not define the end of range value like this:
static void Main(string[] args)
{
try
{
Application app = new Application();
Document doc = app.Documents.Add();
Range range = doc.Range(0, 0);
range.ListFormat.ApplyNumberDefault();
range.Text = "Birinci";
range.InsertParagraphAfter();
ListTemplate listTemplate = range.ListFormat.ListTemplate;
//range.InsertAfter("Birinci");
//range.InsertParagraphAfter();
//range.InsertAfter("İkinci");
//range.InsertParagraphAfter();
//range.InsertAfter("Üçüncü");
//range.InsertParagraphAfter();
Range subRange = doc.Range(range.StoryLength - 1, range.StoryLength - 1);
subRange.ListFormat.ApplyBulletDefault();
subRange.ListFormat.ListIndent();
subRange.Text = "Alt Birinci";
subRange.InsertParagraphAfter();
ListTemplate sublistTemplate = subRange.ListFormat.ListTemplate;
Range subRange2 = doc.Range(subRange.StoryLength - 1, range.StoryLength - 1);
subRange2.ListFormat.ApplyListTemplate(sublistTemplate);
subRange2.ListFormat.ListIndent();
subRange2.Text = "Alt İkinci";
subRange2.InsertParagraphAfter();
Range range2 = doc.Range(range.StoryLength - 1, range.StoryLength - 1);
range2.ListFormat.ApplyListTemplateWithLevel(listTemplate,true);
WdContinue isContinue = range2.ListFormat.CanContinuePreviousList(listTemplate);
range2.Text = "İkinci";
range2.InsertParagraphAfter();
Range range3 = doc.Range(range2.StoryLength - 1, range.StoryLength - 1);
range3.ListFormat.ApplyListTemplate(listTemplate);
range3.Text = "Üçüncü";
range3.InsertParagraphAfter();
string path = Environment.CurrentDirectory;
int totalExistDocx = Directory.GetFiles(path, "test*.docx").Count();
path = Path.Combine(path, string.Format("test{0}.docx", totalExistDocx + 1));
app.ActiveDocument.SaveAs2(path, WdSaveFormat.wdFormatXMLDocument);
doc.Close();
Process.Start(path);
}
catch (Exception exception)
{
throw;
}
}
You just need to keep track of the start and end positions of the list and then apply the list format.
Application wordApp = new Application() {
Visible = true
};
Document doc = wordApp.Documents.Add();
Range range = doc.Content;
range.Text = "Hello world!";
range.InsertParagraphAfter();
range = doc.Paragraphs.Last.Range;
// start of list
int startOfList = range.Start;
// each \n character adds a new paragraph...
range.Text = "Item 1\nItem 2\nItem 3";
// ...or insert a new paragraph...
range.InsertParagraphAfter();
range = doc.Paragraphs.Last.Range;
range.Text = "Item 4\nItem 5";
// end of list
int endOfList = range.End;
// insert the next paragraph before applying the format, otherwise
// the format will be copied to the suceeding paragraphs.
range.InsertParagraphAfter();
// apply list format
Range listRange = doc.Range(startOfList, endOfList);
listRange.ListFormat.ApplyBulletDefault();
range = doc.Paragraphs.Last.Range;
range.Text = "Bye for now!";
range.InsertParagraphAfter();

Categories