问题
I am performing search within excel workbook
using small c# application
using below method
public void SearchExcelFiles(string FilePath)
{
string ConnStr = @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + FilePath + ";Extended Properties=\"Excel 12.0 Xml;HDR=YES\";";
Microsoft.Office.Interop.Excel.Application oXL = new Microsoft.Office.Interop.Excel.Application();
Microsoft.Office.Interop.Excel.Workbook oWB;
Microsoft.Office.Interop.Excel.Range currentFind = null;
Microsoft.Office.Interop.Excel.Range firstFind = null;
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
if (!GB_Search.Controls.OfType<TextBox>().Any(x => !string.IsNullOrEmpty(x.Text)))
{
MessageBox.Show("Enter text for search");
return;
}
oWB = oXL.Workbooks.Open(FilePath, //---Filename OR FilePath
0, //---object UpdateLinks
true, //---object ReadOnly
Type.Missing, //5//---object Format
"", //---object Password
"", //---object WriteResPassword
false, //---object ReadOnlyRecommend
Excel.XlPlatform.xlWindows, //---object Origin
"", //---object Delimiter
true, //---object Editable
false, //---object Notify
0, //---object Converter
true, //---object AddToMru
false, //---object Local
false); //---object CorruptLoad;
//specifying searching range within each excel sheet
//Excel.Range oRng = oXL.get_Range("A1", "XFD1048576");
Excel.Range xlCell =
xlWSheet.UsedRange.SpecialCells(Excel.XlCellType.xlCellTypeLastCell, Type.Missing);
Excel.Range oRng = xlWSheet.get_Range("A1",
xlWSheet.UsedRange.SpecialCells(Excel.XlCellType.xlCellTypeLastCell, Type.Missing));
//loop to search witin all excel sheets (workbook)
foreach (Excel.Worksheet SheetID in oWB.Worksheets)
{ //loop within all textboxs value to search if it is exist
foreach (TextBox cont in GB_Search.Controls.OfType<TextBox>())
{
if (!string.IsNullOrEmpty(cont.Text))
{
currentFind = oRng.Find(cont.Text,
Type.Missing,
Excel.XlFindLookIn.xlValues,
Excel.XlLookAt.xlPart,
Excel.XlSearchOrder.xlByRows,
Excel.XlSearchDirection.xlNext,
false,
Type.Missing,
Type.Missing);
while (currentFind != null)
{
//Keep track of the first range you find.
if (firstFind == null)
{
firstFind = currentFind;
}
//if current address is same as the starting address stop searching
else if (currentFind.get_Address(true, true, Excel.XlReferenceStyle.xlR1C1, Type.Missing, Type.Missing) == firstFind.get_Address(true, true, Excel.XlReferenceStyle.xlR1C1, Type.Missing, Type.Missing))
{
break;
}
//keep searching for next value
currentFind = oRng.FindNext(currentFind);
MessageBox.Show(currentFind.get_Address(true, true, Excel.XlReferenceStyle.xlR1C1, Type.Missing, Type.Missing)); // for test purpose
string CurrentAddress = currentFind.get_Address(true, true, Excel.XlReferenceStyle.xlR1C1, Type.Missing, Type.Missing);
AddToDataGridView(CurrentAddress, SheetID.Name, ConnStr); //when match found get full Row details and populate it to datagridview
}
//empty ranges before looking for the next textbox values
firstFind = null;
currentFind = null;
}
//MessageBox.Show("Done control..." + cont.Name); //~test Purpose
}
//MessageBox.Show("Done...sheet"); //~test Purpoes
}
//MessageBox.Show("Done...wb"); //~test Purpose
oWB.Close(false, Type.Missing, Type.Missing);
oWB = null;
oXL.Quit();
}
now when match found I am calling this method AddToDataGridView()
which should do the following:
- Get full row details by using where clause (in somehow) the
rowid
shown at attached image - populate this row and add to
datagridview
another way to this are welcomed
public void AddToDataGridView(string CurrentAddress, string SheetName, string ConnStr)
{
string cmdtxt = @"select * from [" + SheetName + "$" + CurrentAddress + "]Where ???? ";
MessageBox.Show(cmdtxt); // test purpose
using (OleDbConnection conn = new OleDbConnection(ConnStr))
{
OleDbDataAdapter DA = new OleDbDataAdapter(cmdtxt, conn);
DA.Fill(dt);
DGV_Data.DataSource = dt;
conn.Close();
}
}
example
----------------------------------------------------------
# | A | B | C | D |
----------------------------------------------------------
1 | A VALUE1 | B VALUE1 | C VALUE1 | D VALUE1 |
----------------------------------------------------------
2 | A VALUE2 | B VALUE2 | C VALUE2 | D VALUE2 |
----------------------------------------------------------
suppose that B VALUE2
is my searching match value... I want to get the rowid which is 2
in that case the query that row select * from [sheet_Name$] where rowid=2
then add it to datagridview
how to do that ?
thanks alot
回答1:
Without fully understanding, what the goal is here. The best I can sum up is to search for a string in an Excel workbook. You want the results of the search to be displayed in a DataGridView
. From your comments, you stated that each worksheet may have a different column structure and you want the “complete” row that matched the search string in at least one of the cells in that row. Therefore, each worksheet search result may/will have a different column structure. I hope I understand this correctly.
If this is the case then I am hoping the code below may help. It is a simple form, that has a button to open/select an Excel workbook to search; a text box to allow the user to type the search string into; a combo box to hold the names of the worksheets in the selected workbook; a search button to initiate the search process; a textLog text box for debugging and finally a DataGridView
to hold the search results. Throw in a few labels and it may look something like below after a search for “John” in the workbook “new293.xlsx”.
The text box on the right is used as a log output for testing if needed. After the search results come back, the user can use the combo box to select the results from each worksheet.
The code does a simple search for a string in a given workbook. When the user types something into the search text box and clicks the search button, the code opens the given workbook, searches for the target string in each worksheet and creates a DataTable
to hold the successfully found rows. A DataSet
is created to hold the “different” DataTable
’s created by each worksheet since each worksheet may have a different column structure.
Below is a more detail description, however, I must comment about some possible Excel issues that may arise.
Currently the code will search for any substring of the target string. Example, if you searched for “a”, the returned results will contain ANY string with an “a” in it... “cat” “bat” etc. You may want to refine how this is done. Excels “Find” method may not be the best option. (more below)
When using Excels UsedRange
property, it should be noted that this may return cells that will “appear” empty. In almost all cases when this happens, there is some formatting in the cell but the cell is empty and contains no data… UsedRange
may include that cell in the range because of the formatting. Please be aware of this and do not be too quick to claim UsedRange
is flawed when it returns these “apparently” empty cells. I have a solution for this in another answer.
Lastly, about Excel and interop… the current code uses the same Excel Find
method as your posted code does. This should be fine if the dataset is not to large, however, if there is a large amount of data (large Excel worksheets with many columns and rows), this may become a performance problem. This is an Excel and interop issue. Calling methods like UsedRange
, Find
and others is VERY EXPENSIVE when used in a loop (which we are). Point being, if the worksheets are large, you may want to consider a different implementation without using interop. I understand there are better (free) third party Excel libraries.
With that said, below is the code to the form above.
Global variables in the form:
A DataSet
ds
to hold the DataTables
; self-explanatory path DefaultDirectory
; an Excel workbook
to search and finally the Excel application itself. Upon load, the Excel app is started and waits for the user to select a workbook.
DataSet ds = new DataSet();
string DefaultDirectory = @"D:\Test\";
Workbook workbook;
Excel.Application excelApp;
public Form3() {
InitializeComponent();
}
private void Form3_Load(object sender, EventArgs e) {
excelApp = new Excel.Application();
}
The open/select workbook button click to select a workbook uses an OpenFileDialog
to allow the user to select the workbook to search. Once selected, the global variable workbook
is open and available to other methods.
private void btnSelectWorkbook_Click(object sender, EventArgs e) {
DGV_Data.DataSource = null;
tbSearch.Text = "";
cbWorksheetNames.Items.Clear();
textLog.Text = "";
lblWorkbookName.Text = "";
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "Excel Files|*.xls;*.xlsx;*.xlsm";
ofd.InitialDirectory = DefaultDirectory;
if (ofd.ShowDialog() == DialogResult.OK) {
string fileToOpen = ofd.FileName;
workbook = excelApp.Workbooks.Open(fileToOpen);
lblWorkbookName.Text = "Workbook: " + fileToOpen;
}
}
After the user selects a workbook to search, and has typed some target text to search for… the user clicks the “Search” button. First, two checks are made to make sure there is some text to search for and also check if there is a workbook open to search in. If there is no text to search for or a workbook is not open, display a message to the user and return without searching.
If there is an open workbook and text to search for, the global DataSet
ds
is initialized, then ds
is filled with each worksheet’s DataTable
by calling the GetRowsFromSearchStringFromAllWorksheets
. . After the DataSet
has been filled (more below) the DataGridView
’s DataSource
is set to the first DataTable
in the DatatSet
; the combo box is filled with the worksheet names and finally some labels are updated.
private void btnSearch_Click(object sender, EventArgs e) {
if (string.IsNullOrEmpty(tbSearch.Text)) {
MessageBox.Show("Enter text for search");
return;
}
if (workbook == null) {
MessageBox.Show("Select a workbook");
return;
}
ds = new DataSet();
try {
ds = GetRowsFromSearchStringFromAllWorksheets(workbook, tbSearch.Text);
DGV_Data.DataSource = ds.Tables[0];
FillComboBoxWithSheetNames();
cbWorksheetNames.SelectedIndex = 0;
gbResults.Text = "Search Results for '" + tbSearch.Text + "'";
tbSearch.Text = "";
}
catch (Exception ex) {
MessageBox.Show("Error: " + ex.Message);
}
}
The GetRowsFromSearchStringFromAllWorksheets
method (which probably needs a better name) is not doing much. It loops through each worksheet in the workbook, creates a DataTable
from the worksheet, fills the data table by calling the FillTableWithSearchResults
method (below), then finally adds the DataTable
to the DataSet
. The GetDTColumnsFromWorksheet
method (below) creates a DataTable
based on what the header rows are in the worksheet. The first row in the worksheet is assumed to be header rows and uses these as column names for the DataTable
. Note: currently, if the search returns with no results, the worksheet DataTable
is STILL added to the DataSet
. I left the debug code to modify if you only want to add worksheets that contained results.
private DataSet GetRowsFromSearchStringFromAllWorksheets(Workbook wb, string searchString) {
DataSet ds = new DataSet();
foreach (Worksheet currentWorksheet in wb.Worksheets) {
System.Data.DataTable currentDT = GetDTColumnsFromWorksheet(currentWorksheet);
//textLog.Text += "Searching in worksheet " + currentWorksheet.Name + Environment.NewLine;
FillTableWithSearchResults(currentWorksheet.UsedRange, searchString, currentDT);
if (currentDT.Rows.Count > 0) {
textLog.Text += "Matches found in worksheet " + currentWorksheet.Name + Environment.NewLine;
}
else {
textLog.Text += "No matches found in worksheet " + currentWorksheet.Name + Environment.NewLine;
}
ds.Tables.Add(currentDT);
}
return ds;
}
The GetDTColumnsFromWorksheet
takes a worksheet and returns a DataTable
. The data table returned will have the same number of columns as there are returned from “UsedRange` from the worksheet. An extra column is added to display where in the worksheet the cell is found. It is in an RXXCXX format in the first column. If a cell in a column in the used range has no value, the string “???XX” will be used. This is for cases where there is an empty column.
private System.Data.DataTable GetDTColumnsFromWorksheet(Worksheet ws) {
// this assumes that row 1 of the worksheet contains a row header
// we will use this to name the `DataTable` columns
// this also assumes there are no "lingering" cells with values
// that may not necessarily belong to the data table
int missingColumnNameCount = 1;
Range usedRange = ws.UsedRange;
int numberOFColumns = usedRange.Columns.Count;
System.Data.DataTable dt = new System.Data.DataTable();
dt.TableName = ws.Name;
string currentColumnHeader = "";
Range row1;
// add an extra column in the front
// this column will show where (RXCX) the found item is in the worksheet
dt.Columns.Add("CXRX", typeof(string));
for (int i = 1; i <= numberOFColumns; i++) {
row1 = usedRange[1, i];
if (row1.Value2 != null) {
currentColumnHeader = row1.Value2.ToString();
}
else {
// if the row has no value a default name and indexer to avoid duplicate column names
currentColumnHeader = "???" + missingColumnNameCount;
missingColumnNameCount++;
}
dt.Columns.Add(currentColumnHeader, typeof(string));
}
return dt;
}
The FillTableWithSearchResults
method takes a range to search through, a string to search for and finally a data table to add the successful searches to. The DataTable
passed in has already been created and the columns have been initialized.
I am not sure if this is the best way to handle Excels Find/FindNext
methods. Therefore, I hope I have this correct. When the Find
is used on a range for the first time, it returns the first found cell that matches what it is searching for. This retuned range is a cell address of the “first” found item. From my understanding, the FindNext
will obviously return the NEXT found item. The issue is that when it finds the LAST item and searches for the next one, it simply starts over at the beginning. Therefore, the stopping condition for the loop would be when the cell address of the NextFind
matches the cell address of the “first” Find
. This will require that we save the “first” Find
’s cell address. Below is one approach to this conundrum.
Create two ranges: one startRange
to hold the starting “first” Find
, another currentRange
to hold the currently “found” range. First, a check is made to make sure there is something to search in. If there is at least one row to search in, the startRange
is set from the “first” Find
. This is the cell address we need to stop at when using FindNext
. If there is at least one item found, then we can search for the next one and enter the FindNext
loop. Simply set currentRange
to the NextFind
add the startRange
from the “first” Find
to the data table then finally enter the FindNext
loop such that it will continue using FindNext
and adding new rows to the data table until the currentRange
cell address Equals
the startingRange
cell address. This indicates that FindNext
has looped back to the beginning and the search is complete. The AddExcelRowToDataTable
adds the found row to the data table (below).
Note: currently this code allows duplicate entries in cases where the searched for string is found in more than one column in the same row. There will be one row entry in the grid for every found search string in the rows columns. Example: if row 5 has the searched for string in columns 4, 6 and 8, there will be a row for R5C4, R5C6 and R5C8. I did not filter this to remove duplicate rows.
private void FillTableWithSearchResults(Range usedRange, string searchString, System.Data.DataTable dt) {
Range currentRange;
if (usedRange.Rows.Count > 0) {
Range startRange = usedRange.Find(searchString);
if (startRange != null) {
currentRange = usedRange.FindNext(startRange);
AddExcelRowToDataTable(usedRange, startRange, dt);
string startAddress = startRange.get_Address(true, true, XlReferenceStyle.xlR1C1);
while (!currentRange.get_Address(true, true, XlReferenceStyle.xlR1C1).Equals(startAddress)) {
AddExcelRowToDataTable(usedRange, currentRange, dt);
currentRange = usedRange.FindNext(currentRange);
}
}
}
}
The AddExcelRowToDataTable
method takes a used range to get the data from, a range row
to add to the third given parameter DataTable
. Again a little hacky, a check is made to make sure that there are not more columns in the used range than there are in the data table. A row Index is obtained to indicate which row in the used range to add to the data table. A DataRow
dr
is created from the given DataTable
dt
to make sure the column schemas are the same. The first column is going to be the column we added earlier to display the RXXCXX location column of the found item. Add the extra column data then loop through the rest of the columns adding the worksheet values to the DataRow
. After the all the column values have been added, the DataRow
is added to the DataTable
.
private void AddExcelRowToDataTable(Range usedRange, Range row, System.Data.DataTable dt) {
if (usedRange.Columns.Count >= dt.Columns.Count - 1) {
int rowIndex = GetRowIndexOfFoundItem(row);
if (rowIndex >= 0) {
DataRow dr = dt.NewRow();
// add the CXRX data
dr[0] = row.get_Address(true, true, XlReferenceStyle.xlR1C1);
for (int i = 1; i <= usedRange.Columns.Count; i++) {
dr[i] = usedRange.Cells[rowIndex, i].Value2;
}
dt.Rows.Add(dr);
}
}
}
The GetRowIndexOfFoundItem
takes a cell range and returns the (int) row index from the cells string RXXCXX address.
private int GetRowIndexOfFoundItem(Range range) {
// hacky ... the string is a format of RXXCX or RXXcXXX or RXXXXCXX.
// we want the XXX after the R... split the string on 'C'
// to get RXX..X, then remove the 'R' and parse the number
string RCaddress = range.get_Address(true, true, XlReferenceStyle.xlR1C1);
string[] split = RCaddress.Split('C');
RCaddress = split[0].Remove(0, 1);
int rowIndex = 0;
if (int.TryParse(RCaddress, out rowIndex)) {
return rowIndex;
}
else {
// not valid number
return -1;
}
}
Method to fill the combo box with worksheet names after the search has finished.
private void FillComboBoxWithSheetNames() {
cbWorksheetNames.Items.Clear();
foreach (System.Data.DataTable dt in ds.Tables) {
cbWorksheetNames.Items.Add(dt.TableName);
}
}
The combo boxes SelectedIndexChnged
event is wired up and uses the combo boxes selected index to determine which DataTable
to display in the grid.
private void cbWorksheetNames_SelectedIndexChanged(object sender, EventArgs e) {
DGV_Data.DataSource = ds.Tables[cbWorksheetNames.SelectedIndex];
}
Finally some resource cleanup.
private void Form3_FormClosing(object sender, FormClosingEventArgs e) {
try {
if (workbook != null) {
workbook.Close();
Marshal.ReleaseComObject(workbook);
}
excelApp.Quit();
Marshal.ReleaseComObject(excelApp);
}
catch (Exception ex) {
MessageBox.Show("Error: " + ex.Message);
}
}
Sorry for the long winded answer, I hope it helps.
回答2:
First, you are only finding the "first" row in each sheet because it is searching for a named range called TB_First_Name.Text
in each sheet - there can only be at most one of those in each sheet.
Secondly, there seems to be a typo in your code - I believe DataGridView gdv_data = new DataGridView();
should be DataGridView DGV_data = new DataGridView();
Finally, seems from other answers (e.g. https://stackoverflow.com/a/46044387/3661120) the best way to populate a datagridview row from excel would be this kind of way:
var rowArray = oRng.Cells.Value2.Cast<object>().ToArray();
try { j = d.CurrentRow.Index; } catch { }
DataGridViewRow r = new DataGridViewRow();
r.CreateCells(d, rowArray);
DGV_Data.Rows.Insert(j, r);
来源:https://stackoverflow.com/questions/48871143/how-to-insert-specific-excel-row-into-datagridview-using-excel-rowindex