Delphi - Copying Rows in Excel - excel

Delphi Tokyo - I have a Delphi app which is working against an Excel spreadsheet. I have a source sheet... called RawData. I need to copy specific rows (based on a filter) to another worksheet in the same workbook (called ActivitySheet). While I can get this to work... I am getting an Excel error message at the end of the process with the message 'The picture is too large and will be truncated.' This makes me believe that the command I am using is using the Clipboard, so I don't want to do that.
Here is what I am doing now...
var
ActivitySheet : _WorkSheet;
destRange : OleVariant;
begin
oExcel.Worksheets.Add(EmptyParam, EmptyParam, 1, xlWorksheet, LOCALE_USER_DEFAULT);
ActivitySheet:= oExcel.ActiveSheet as _Worksheet;
ActivitySheet.Name := 'Activity';
destRange := ActivitySheet.Range['A1', 'A1'];
// Set RawData Sheet as active
RawDataSheet.Activate(LOCALE_USER_DEFAULT);
Application.ProcessMessages;
// Now add the both filters that we want... (Not real filters, but easier to understand)
RawDataSheet.UsedRange[LOCALE_USER_DEFAULT].AutoFilter(1, '=*Blue*', xlOr, '=*Red*', True );
RawDataSheet.UsedRange[LOCALE_USER_DEFAULT].AutoFilter(2, '=Car', xlOr, '=Truck', True );
ShowMessage(IntToStr(RawDataSheet.UsedRange[LOCALE_USER_DEFAULT].Rows.Count));
RawDataSheet.UsedRange[LOCALE_USER_DEFAULT].Copy(destRange);
...
While this code works, and only copies the rows that are valid based on the current filter, it gives me the error at the 'Save Spreadsheet' stage later in my code. I thought about using a Vararray, but the ShowMessage line shows me the TOTAL rows in RawSheet, not the Filtered rows...so I would have to iterate through all the rows in the sheet (as opposed to just filtered rows) and then determine if the row is valid...
What is the best way to copy a set of filtered rows to a new sheet?

When I first saw this q, I had no idea how to do what you are asking. After a number
of false starts, I came across this answer
Excel Filtering and Copying in VBA
which shows that it can be done very simply, if you know the exact "magic spell" to do it.
Here is the code I wrote. I have used late binding to Excel throughout because it
avoids having to spray references to LOCALE_USER_DEFAULT everywhere.
type
TDefaultForm = class(TForm)
[...]
public
vExcel,
vWB,
vRange,
vSheet,
vActivitySheet,
vRawDataSheet : OleVariant;
end;
[...]
procedure TDefaultForm.CopyFilteredRange;
var
i : Integer;
vRange : OleVariant;
vDestRange : OleVariant;
begin
vRange := vRawDataSheet.Range['A1', 'A100'];
vDestRange := vActivitySheet.Range['A1', 'A1'];
vRange.SpecialCells(xlCellTypeVisible).Copy(vDestRange);
end;
procedure TDefaultForm.TestFilter;
var
vRange : OleVariant;
begin
vExcel := CreateOleObject('Excel.Application');
vExcel.Visible := True;
vWB := vExcel.WorkBooks.Add;
vRawDataSheet := vWB.ActiveSheet;
vActivitySheet := vWB.Sheets[2];
vRange := vRawDataSheet.Range['A1', 'A100'];
vRange.Item[1, 1] := 'Vehicle';
vRange.Item[2, 1] := 'Car';
vRange.Item[3, 1] := 'Truck';
vRange.Item[4, 1] := 'Truck';
vRange.Item[5, 1] := 'Car';
vRange.Item[6, 1] := 'Truck';
vRange.Item[7, 1] := 'Truck';
vRawDataSheet.UsedRange.Select;
vRawDataSheet.UsedRange.AutoFilter(Field := 1, Criteria1 := 'Car');
CopyFilteredRange;
end;
Btw, in case you are not familiar with it, the way of passing the arguments in the call
vRawDataSheet.UsedRange.AutoFilter(Field := 1, Criteria1 := 'Car');
is a special Delphi syntax for late-binding which avoids the rigmarole of passing
all the arguments required for early binding.
Also btw, I got the "Picture too large ..." message, but it seems easy to avoid: just include ClipBrd in the Uses list, and call ClipBoard.Clear after copying the filtered rows. Ordinarily, I don't like messing with the Clipboard because it's a system-wide resource, but as a fast fix here, clearing it may be acceptable.

Related

Building Pivot Table in Excel via Delphi

I am using Delphi to write a program which builds a Pivot Table. Because I want to use Distinct Count, I have to 'Add to Data Model', which apparently creates an OLAP cube in the background. This means that I cannot use the "normal" Excel_TLB unit, but instead have to use the GetActiveOLE based routines...
I can connect, I can build the data model, I can create the pivot cache and pivot table object, but when I go to add the fields to the Pivot, I get an error at runtime, normally 'Member not Found'.
When I step through my code, after I create the PivotTable object, I can go to excel, and I see the list of possible columns for my pivot table. They are listed under a 'Range' level. (Which is what I would expect when you 'Add to Data Model' for a pivot). When I turn on record Macro, and manually add the field (category) to the filter section, the VBA code looks like this...
With ActiveSheet.PivotTables("PivotTable1").CubeFields("[Range].[Category]")
.Orientation = xlPageField
.Position = 1
End With
Below is my Delphi Code. It is the last commented lines which do NOT work.
procedure Build_Pivot;
var
// Connection Info
SheetName: String;
ConnRange, ConnName, ConnString, ConnCommand: String;
ConnPath, ConnFile : String;
ConnCount : Integer;
XLApp: Variant;
gConnection: Variant;
// Now define variables for the Pivot
myPivotCache: Variant;
myPivotTable: Variant;
StartCellRange: Variant;
fFilter1, fFilter2: Variant;
begin
XLApp := GetActiveOleObject('Excel.Application');
// Create the connection if we are adding to DataModel
// Now parse our gFileName
ConnPath := ExtractFileDir(gFileName); // Global Variable of the FileName
ConnFile := ExtractFileName(gFileName);
SheetName := 'RawData';
ConnRange := '$A:$H';
ConnCount := XLApp.ActiveWorkbook.Connections.Count;
ConnName := 'WorksheetConnection' + IntToStr(ConnCount) + '_' + SheetName + '!' + ConnRange;
ConnString := 'WORKSHEET;' + ConnPath + '[' + ConnFile + ']' + SheetName;
ConnCommand := SheetName + '!' + ConnRange;
gConnection := XLApp.ActiveWorkbook.Connections.Add2(ConnName, '', ConnString, ConnCommand, 7, True, False);
// Create the PivotCache
myPivotCache := XLApp.ActiveWorkbook.PivotCaches.Create(xlExternal, gConnection, 7);
// Create the PivotTable object;
StartCellRange := XLApp.ActiveWorkbook.ActiveSheet.Range['A5', 'A5'];
myPivotTable := myPivotCache.CreatePivotTable(StartCellRange, 'PivotTable1', True, 7);
// I can now see my Pivot Object in Excel
// Now add Filter Fields. I have tried multiple variations, but cannot get anything to work
// fFilter1 := XLApp.ActiveWorkbook.ActiveSheet.PivotTables('PivotTable1').CubeFields('[Range].[Category]');
// fFilter1 := myPivotCache.PivotTable.CubeFields('[Range].[Category]');
// fFilter1 := myPivotTable.CubeFields('[Range].[Category]');
fFilter1.Orientation := xlPageField;
Any help or ideas greatly appreciated.
Found the answer. The parameter needs to be passed as an array, and not within parenthesis. This code works...
fFilter1 := myPivotTable.CubeFields['[Range].[Category]'];
fFilter1.Orientation := xlPageField;

Delphi - Unable to change Function in Excel PivotField

Delphi 10 / Seattle, Excel 2013. I am using Delphi to build an Excel plugin. This plugin will create a Pivot table. My challenge is setting the specific FUNCTION I want (Count vs Average vs Sum, etc). If I do NOT set a function, the default (COUNT) is used, and my code works fine, the pivot table is like it should be. If I try to change the function on a PivotField, then I get an error "Unable to set the Function property of the PivotField class." In searching the error, I have found 2 common causes. (1) The Pivot table has to have a default version of xlPivotTableVersion15, and (2) the PivotField orientation has to be set to xlDataField. I have done both, and when moused-over, the IDE shows the Integer values for both, so I know they are defined.
I have tried getting (as opposed to setting) the value of the PivotField.function. I get a similar error "Unable to get the Function property of the PivotField class". The column I am wanting to Sum is called '#Clicks'. The offending line of code is
myPivotTable.PivotFields('#Clicks').function := xlSum;
If I comment this line out, my routine runs fine, although I get the default COUNT function, instead of the SUM function that I want.
Any ideas appreciated. Note that when the line is commented out, and I run the code, I can then go into Excel, and go into the PivotField listings, and change the function from Count to Sum. Here is my full code.
procedure T_ZTemplateForm.TestPivotTable;
var
myPivotCache: OleVariant;
myPivotTable : OleVariant;
myActive_WorkBook : OleVariant;
TabDestination : string;
f1: OleVariant;
begin
// Add the new Sheet
XLApp.Connect; // XLApp is a global variable, pointing to the Excel instance
myActive_WorkBook := XLApp.ActiveWorkbook;
XLApp.Worksheets.Add(EmptyParam, EmptyParam,1, xlWorksheet, LOCALE_USER_DEFAULT );
// Get a handle to the new sheet and set the Sheet Name
sheet_graph1 := XLApp.ActiveSheet;
sheet_graph1.Name := 'Graph1';
// Create a Pivot Cache
myPivotCache := myActive_WorkBook.PivotCaches.Create(xlDatabase,'Raw Data!R1C1:R1048576C36',xlPivotTableVersion15);
// Create a Pivot table within the PivotCache
TabDestination := 'Graph1!R3C1';
myPivotTable := myPivotCache.CreatePivotTable(TabDestination, 'PivotTable1',xlPivotTableVersion15);
// Now start adding the fields...
f1 := myPivotTable.PivotFields('Fiscal Quarter');
f1.Orientation := xlRowField;
f1.Position := 1;
myPivotTable.PivotFields('#Clicks').orientation := xlDataField;
// myPivotTable.PivotFields('#Clicks').function := xlSum;
end;
Update I can reproduce your problems with Set_Function and GetFunction using Seattle and Excel 2007, so please disregard the original version of my answer.
However, I have found a way to use CreateDataField to create a PivotField with a Function of xlCount, and it is very simple.
Given local variables
var
DataField : OleVariant;
Value : OleVariant;
the following code executes without complaint and correctly
Value := xlCount;
DataField := DestinationSheet.PivotTables('APivotTable').AddDataField(
vPivotField,
'Count',
Value
);
whereas
DataField := DestinationSheet.PivotTables('APivotTable').AddDataField(
vPivotField,
'Count',
xlCount
);
fails with the error message you quoted. So I can only guess that when AddDataField is called with the "raw" value xlCount, the Function_ argument the compiler generates is somehow incorrectly "packaged" whereas when the argument is an OleVariant containing the xlCount value, it is correctly packaged.
I'll leave you to try out the other XlConsolidationFunction values - I've had enough of this problem for now!
Original Answer: Judging by my experiments, you can change the Excel 'function' you want to use without specifying the final parameter (in your case xlSum) of AddDataField. In fact with Seattle and Excel2007, I can't get AddDataField to execute without getting a 'The parameter is incorrect' exception for any value of the final, 'function' parameter.
I have a WorkBook with a table of Company names, dividend payment dates and amounts. The table headers are Company, PaymentDate and Amount.
The code below works for me and allows me to choose the function to be applied to the Amount column, simply by specifying the name of the function as the Caption parameter of AddDataField. I've used late binding mainly for ease of set-up, and so that I can easily omit arguments for parameters I don't want to specify.
Code:
procedure TForm1.TestPivotTable;
var
vXLApp : OleVariant;
PivotCache: OleVariant;
PivotTable : OleVariant;
ActiveWorkBook : OleVariant;
DestinationSheet : OleVariant;
FN : String;
Destination : OleVariant;
PivotField : OleVariant;
DataField : OleVariant;
begin
vXLApp := CreateOleObject('Excel.Application');
vXLApp.Visible := True;
FN := 'D:\aaad7\officeauto\MAPivot.xlsm';
Assert(FileExists(FN));
vXLApp.Workbooks.Open(FN);
ActiveWorkBook := vXLApp.ActiveWorkbook;
PivotCache := ActiveWorkbook.PivotCaches.Create(SourceType := xlDatabase,
SourceData := 'Table1', //'R2C1R30C3',
Version:=xlPivotTableVersion2000);
DestinationSheet := ActiveWorkBook.Sheets['Sheet3'];
Destination := DestinationSheet.Name + '!R3C1';
PivotTable := PivotCache.CreatePivotTable(TableDestination := Destination,
TableName := 'APivotTable'
);
DestinationSheet.Select;
DestinationSheet.Cells[3, 1].Select;
DestinationSheet.PivotTables('APivotTable').PivotFields('Company').Orientation := xlRowField;
DestinationSheet.PivotTables('APivotTable').PivotFields('Company').Position := 1;
DestinationSheet.PivotTables('APivotTable').PivotFields('PayDate').Orientation := xlRowField;
DestinationSheet.PivotTables('APivotTable').PivotFields('PayDate').Position := 2;
DestinationSheet.PivotTables('APivotTable').PivotFields('Amount').Orientation := xlRowField;
DestinationSheet.PivotTables('APivotTable').PivotFields('Amount').Position := 3;
PivotField := DestinationSheet.PivotTables('APivotTable').PivotFields('Amount');
DataField := DestinationSheet.PivotTables('APivotTable').AddDataField(
Field := PivotField,
Caption := 'Sum');
end;

Inserting images to an Excel sheet in Delphi

I have a question here, I have an application and I need to add images to it. I have tried:
Sheet.Shapes.AddPicture(G_V.Prog_Dir+'pic.BMP',false,true, 190, 10+(15*rowcount), 100, 100 );
it works just fine, but I don't want to give parameters, I want to insert pictures to specified (and parametric) cells because I need to add picture to the last column of the page; this excel needs to be printed I must mention that. So I tried:
Sheet.Range['E'+inttostr(rowcount),'E'+inttostr(rowcount)].Select;
Sheet.Pictures.Insert(G_V.Prog_Dir+'pic.BMP');
It looks OK at first sight, however I think this code links images to the sheet. For example, I send the created Excel to another computer and these images cannot be seen (I don't recall the exact error) and when I searched it, I found out that receiving computer needs to have images at the exact path. As a solution to this, "Sheet.Shapes.AddPicture" recommended but
as I stated before, I need another solution here.
I didn't see anybody experiencing this kind of problem, I hope someone helps me out.
You can use this code to import picture. You do not need exact path if you use AddPicture function with this parameters. it copies photo and paste it to excel.
Set p = ActiveSheet.Shapes.AddPicture(FileName:=PictureFileName,_
linktofile:=msoFalse, savewithdocument:=msoCTrue, _
left:=TargetCell.Left, Top:=TargetCell.Top, Width:=-1,_
Height:=TargetCell.Height)
The simple solution is to add the picture to the clipboard and then paste it into the sheet.
implementation
uses
VCL.Clipbrd, VCL.Graphics, Excel2000;
procedure TForm1.Test;
var
Bitmap: TBitmap;
Excel: TExcelApplication;
Worksheet: TExcelWorksheet;
begin
Bitmap:= TBitmap.Create;
try
Bitmap.LoadFromFile('c:\test.bmp');
Clipboard.Assign(Bitmap);
Excel:= TExcelApplication.Create(self);
Worksheet:= Excel.Worksheets[0];
Worksheet.Range['a1','a1'].Select;
Worksheet.Paste;
finally
Bitmap.Free;
end;
end;
This destroys the previous contents of the clipboard, which is bad form.
You can save/restore the clipboard data using the following routines (based on: https://www.devexpress.com/Support/Center/Question/Details/Q93874)
procedure TForm1.RestoreClipboardData(SourceDataStream: TMemoryStream);
var
AData: THandle;
ADataPtr: Pointer;
begin
if SourceDataStream.Size = 0 then Exit;
Clipboard.Open;
try
AData := GlobalAlloc(GMEM_MOVEABLE + GMEM_DDESHARE, SourceDataStream.Size);
try
ADataPtr := GlobalLock(AData);
try
SourceDataStream.Position := 0;
SourceDataStream.ReadBuffer(ADataPtr^, SourceDataStream.Size);
SetClipboardData(CF_BITMAP, AData);
finally
GlobalUnlock(AData);
end;
except
GlobalFree(AData);
raise;
end;
finally
Clipboard.Close;
end;
end;
procedure TForm1.SaveClipboardData(DestDataStream: TMemoryStream);
var
AData: THandle;
ADataPtr: Pointer;
begin
if DestDataStream = nil then raise Exception.Create('Dest is nil');
Clipboard.Open;
try
AData := GetClipboardData(CF_BITMAP);
if AData = 0 then Exit;
ADataPtr := GlobalLock(AData);
try
DestDataStream.Size := GlobalSize(AData);
DestDataStream.Position := 0;
DestDataStream.Write(ADataPtr^, DestDataStream.Size);
finally
GlobalUnlock(AData);
end;
finally
Clipboard.Close;
end;
end;

Determine if rows are filtered by user

I have written an Excel plugin. Everything is working fine, up to a point. Assume I am iterating through all rows, doing some work... The user may have enabled a filter, so that the current row is not visible due to that filter. How do I determine what data should not be processed because it is not visible due to the filter?
Answer found. There does not appear to be a row.filtered property. There is a Hidden property, but it is on ranges, not rows.
Solution: Build a range, based on entire row, then check Hidden property.
function IsRowVisible(RowNum: Integer): Boolean;
var
FilterRange: OleVariant;
RangeText: String;
begin
RangeText := 'A' + IntToStr(RowNum);
// aws is my activesheet
FilterRange := aws.Range[RangeText, RangeText].EntireRow;
Result := not(FilterRange.EntireRow.Hidden);
end;
The decision of the previous answer did not working for me, so I found another more simple solution by using aSheet.Rows[RowNum].Hidden property:
function IsRowVisible(aSheet: OLEVariant; aRowNum: Integer): Boolean;
begin
Result := not aSheet.Rows[aRowNum].Hidden;
end;
In this function aSheet - excel worksheet and aRowNum - row number. Same solution can be used for columns.

How to have responsive UI (Form) when performing long-running export task?

Good day people. First off, I'm not an native English speaker I might have some grammar mistakes or such.
I need an advice from people who has done something or an application alike mine, well, the thing is that I'm using a TProgressBar in my delphi form, another component called "TExcelApplication" and a TDBGrid.
When I export the DBGrid's content, the application "freezes", so I basically put that ProgressBar for the user to see how much the process is completed. I've realized that when the TDBGrid is retrieving and exporting each row to the new Excel workbook, you can't move the actual form, so you have to wait until the process is completed to move that form.
So, is it possible to do something (I thought about threads but I'm not sure if they could help) so the user could move the window if he wanted?
Thank you so much for taking your time in reading and giving me an advice. I'm using Delphi XE.
Here's the code I use to export the rows:
with ZQDetalles do
begin
First;
while not EOF do
begin
i := i + 1;
workSheet.Cells.Item[i,2] := DBGridDetalles.Fields[0].AsString;
workSheet.Cells.Item[i,3] := DBGridDetalles.Fields[1].AsString;
workSheet.Cells.Item[i,4] := DBGridDetalles.Fields[2].AsString;
workSheet.Cells.Item[i,5] := DBGridDetalles.Fields[3].AsString;
workSheet.Cells.Item[i,6] := DBGridDetalles.Fields[4].AsString;
workSheet.Cells.Item[i,7] := DBGridDetalles.Fields[5].AsString;
workSheet.Cells.Item[i,8] := DBGridDetalles.Fields[6].AsString;
workSheet.Cells.Item[i,9] := DBGridDetalles.Fields[7].AsString;
Next;
barraProgreso.StepIt;
end;
end;
If you want to see the whole code for the "Export" button, then feel free to see this link: http://pastebin.com/FFWAPdey
Whenever you're doing stuff that takes a significant amount of time in an application with GUI you want to put it in a seperate thread so the user can still operate the form. You can declare a simple thread as such:
TWorkingThread = class(TThread)
protected
procedure Execute; override;
procedure UpdateGui;
procedure TerminateNotify(Sender: TObject);
end;
procedure TWorkingThread.Execute;
begin
// do whatever you want to do
// make sure to use synchronize whenever you want to update gui:
Synchronize(UpdateGui);
end;
procedure TWorkingThread.UpdateGui;
begin
// e.g. updating the progress bar
end;
procedure TWorkingThread.TerminateNotify(Sender: TObject);
begin
// this gets executed when the work is done
// usually you want to give some kind of feedback to the user
end;
// ...
// calling the thread:
procedure TSettingsForm.Button1Click(Sender: TObject);
var WorkingThread: TWorkingThread;
begin
WorkingThread := TWorkingThread.Create(true);
WorkingThread.OnTerminate := TerminateNotify;
WorkingThread.FreeOnTerminate := true;
WorkingThread.Start;
end;
It's pretty straight forward, remember to always use Synchronize when you want to update visual elements from a thread. Usually, you also want to take care that the user can't invoke the thread again while it's still doing work as he's now able to use the GUI.
If the number of rows is small (and you know how many you'll have), you can transfer the data much more quickly (and all at once) using a variant array of variants, something like this:
var
xls, wb, Range: OLEVariant;
arrData: Variant;
RowCount, ColCount, i, j: Integer;
Bookmark: TBookmark;
begin
// Create variant array where we'll copy our data
// Note that getting RowCount can be slow on large datasets; if
// that's the case, it's better to do a separate query first to
// ask for COUNT(*) of rows matching your WHERE clause, and use
// that instead; then run the query that returns the actual rows,
// and use them in the loop itself
RowCount := DataSet1.RecordCount;
ColCount := DataSet1.FieldCount;
arrData := VarArrayCreate([1, RowCount, 1, ColCount], varVariant);
// Disconnect from visual controls
DataSet1.DisableControls;
try
// Save starting row so we can come back to it after
Bookmark := DataSet1.GetBookmark;
try
{fill array}
i := 1;
while not DataSet1.Eof do
begin
for j := 1 to ColCount do
arrData[i, j] := DataSet1.Fields[j-1, i-1].Value;
DataSet1.Next;
Inc(i);
// If we have a lot of rows, we can allow the UI to
// refresh every so often (here every 100 rows)
if (i mod 100) = 0 then
Application.ProcessMessages;
end;
finally
// Reset record pointer to start, and clean up
DataSet1.GotoBookmark;
DataSet1.FreeBookmark;
finally
// Reconnect GUI controls
DataSet1.EnableControls;
end;
// Initialize an instance of Excel - if you have one
// already, of course the next couple of lines aren't
// needed
xls := CreateOLEObject('Excel.Application');
// Create workbook - again, not needed if you have it.
// Just use ActiveWorkbook instead
wb := xls.Workbooks.Add;
// Retrieve the range where data must be placed. Again, your
// own WorkSheet and start of range instead of using 1,1 when
// needed.
Range := wb.WorkSheets[1].Range[wb.WorkSheets[1].Cells[1, 1],
wb.WorkSheets[1].Cells[RowCount, ColCount]];
// Copy data from allocated variant array to Excel in single shot
Range.Value := arrData;
// Show Excel with our data}
xls.Visible := True;
end;
It still takes the same amount of time to loop through the rows and columns of the data, but the time taken to actually transfer that data to Excel is drastically reduced, particularly if there's a good amount of data.

Resources