Delphi Controlling Excel - Creating Pivot Tables and Charts - excel

Delphi 10 / Seattle, with Excel 2013. I am writing a plugin (using AddIn Express) for Excel. One of the things that I need to do is create a series of Excel Pivot Tables/Pivot Charts. I have recorded macros within Excel, so I have VBA code to do what I want. My challenge is porting it to Delphi.
My code compiles, and when I step through it, the very last line gives me the error .. Method 'SetSourceData' not supported by Automation Object. FYI - XLApp is a variable point to the Excel Application.
procedure TMyTemplateForm.Pivot_TouchesByQuarter;
var
myPivotCache: OleVariant;
myActive_WB : OleVariant;
MyChart : OleVariant;
ChartSourceRange : OleVariant;
TabDestination : string;
begin
// Add the new Sheet
XLApp.Connect;
myActive_WB := 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'; // CANNOT CONTAIN SPACES.. ?????
// Parameters: SourceType, SourceData, Version
// Doc at: https://msdn.microsoft.com/en-us/library/office/ff839430.aspx
myPivotCache := myActive_WB.PivotCaches.Create(xlDatabase,'Raw Data!R1C1:R1048576C36',xlPivotTableVersion15);
// Parameters: TableDestination, TableName, DefaultVersion
TabDestination := 'Graph1!R3C1';
myPivotCache.CreatePivotTable(TabDestination, 'PivotTable1',xlPivotTableVersion15);
// Select where we want this placed...
sheet_Graph1.Cells.Item[3, 1].Select;
// https://msdn.microsoft.com/en-us/library/office/jj228277.aspx
// Create the chart object
myChart := sheet_Graph1.Shapes.AddChart2(201, xlColumnClustered);
// Define the Range that is the source for the chart. This is the Pivot Table I created just above
ChartSourceRange := sheet_Graph1.Range['Graph1!$A$3:$C$20'];
// Tell the Pivot Chart to use the Chart Range
myChart.SetSourceData(ChartSourceRange);
end;
Why am I getting this error? As a related question, can I point my Chart Source to the PivotTable1 object? Right now, it is hard coded to specific cell locations, but depending on the data, my Pivot table can be bigger than from Row3 to Row20.
If it helps any, the VBA macro code (last 2 lines) is..
ActiveSheet.Shapes.AddChart2(201, xlColumnClustered).Select
ActiveChart.SetSourceData Source:=Range("Sheet1!$A$3:$C$20")

I found the answer. Instead of
// Tell the Pivot Chart to use the Chart Range
myChart.SetSourceData(ChartSourceRange)
the code should be
myChart.chart.SetSourceData(ChartSourceRange);

Related

Delphi - How to create Excel PivotChart

I am trying to automate a series of PivotCharts within Excel. I am using Delphi Rio and Excel 2019. I have an XLSX file, with one sheet, called 'Sheet1'. I have enabled record macro within Excel, and recorded the VBA code. I have ported it to Delphi. Everything compiles, and I get an AV when I go to add my row fields (which becomes Axis fields in the PivotChart). In Excel, to record the macro, I select my sheet with my data, go to Insert/Pivot Chart, checking 'Add Data to Data Model', and then build a simple PivotChart.
The VBA code is
Cells.Select
Workbooks("TestFile.xlsx").Connections.Add2 "WorksheetConnection_Sheet1!$A:$CU" _
, "", "WORKSHEET;S:\Temp\[TestFile.xlsx]Sheet1", "Sheet1!$A:$CU", 7, True, _
False
Sheets.Add
ActiveWorkbook.PivotCaches.Create(SourceType:=xlExternal, SourceData:= _
ActiveWorkbook.Connections("WorksheetConnection_Sheet1!$A:$CU"), Version:=7). _
CreatePivotChart(ChartDestination:="Sheet3").Select
ActiveChart.ChartType = xlColumnClustered
ActiveChart.ChartStyle = 201
With ActiveChart.PivotLayout.PivotTable.CubeFields("[Range].[Fiscal Quarter]")
.Orientation = xlRowField
.Position = 1
End With
With ActiveChart.PivotLayout.PivotTable.CubeFields("[Range].[Territory Name]")
.Orientation = xlColumnField
.Position = 1
End With
ActiveChart.PivotLayout.PivotTable.CubeFields.GetMeasure "[Range].[Pipeline]", _
xlSum, "Sum of Pipeline"
ActiveChart.PivotLayout.PivotTable.AddDataField ActiveChart.PivotLayout. _
PivotTable.CubeFields("[Measures].[Sum of Pipeline]"), "Sum of Pipeline"
The problem I run into (when running my Delphi compiled executable' is when adding the 'Fiscal Quarter' rowfield. It APPEARS that CubeFields is not defined, although I get 'data inaccessible' when I mouse over it. So my first question, in Delphi, is CubeFields created automatically?
My Delphi code is
procedure BuildPivotChart;
var
myChart: _Chart;
myChartShape: Shape;
myPivotCache: PivotCache;
SourceSheet, DestSheet: _Worksheet;
myConnection: WorkbookConnection;
ConnName, ConnString, ConnCommand: String;
begin
// Create the connection since we are adding to DataModel
ConnName := 'WorksheetConnection_Sheet1!$A:$CU';
ConnString := 'WORKSHEET;S:\Temp\[TestFile.xlsx]Sheet1';
ConnCommand := 'Sheet1!$A:$CU';
myConnection := oExcel.ActiveWorkbook.Connections.Add2(ConnName, '', ConnString, ConnCommand, 7, True, False);
// Create a new Sheet, and get a handle to it...
oExcel.Worksheets.Add(EmptyParam, EmptyParam,1, xlWorksheet, LCID );
// Get a handle to the new sheet and set the Sheet Name
DestSheet := oExcel.ActiveSheet as _Worksheet;
DestSheet.Name := 'Graphs';
// Create a Pivot Cache, with a Chart object
myPivotCache := oExcel.ActiveWorkbook.PivotCaches.Create(xlExternal, myConnection, 7);
myChartShape := myPivotCache.CreatePivotChart(DestSheet, xlColumnClustered, 10, 10, 300, 300);
myChartShape.Select(EmptyParam);
myChart := oExcel.ActiveChart;
myChart.ChartStyle := 201;
if oExcel.ActiveChart.PivotLayout.PivotTable.PivotCache.IsConnected = True then
ShowMessage('Connected');
// Now add Row Fields
// ALL THREE OF THE BELOW LINES gives AV
oExcel.ActiveChart.PivotLayout.PivotTable.CubeFields.AddSet('[Range].[Fiscal Quarter]', 'MyName').Orientation := xlRowField;
oExcel.ActiveChart.PivotLayout.PivotTable.CubeFields['[Range].[Fiscal Quarter]'].Orientation := xlRowField;
// Thinking that the CUBE is not defined... check to see cube fields count..
ShowMessage(IntToStr(oExcel.ActiveChart.PivotLayout.PivotTable.CubeFields.Count));
...
When I step through my Delphi code, on the 'myPivotCache.CreatePivotChart' line, I see the chart object show up in Excel, and I see my columns listed in the Pivot selected on the right side of Excel. I have a suspicion that something is wrong with CubeFields, but I can't prove it, or I don't know how to fix it... I DO get my 'Connected' popup.
Any help or ideas appreciated.
This problem occurs when using EXCEL_TLB units.
It can be solved by changing to OLE development.
Use add unit "Excel2010" to "Uses" after interface, as:
interface
uses Excel2010

Delphi - Copying Rows in 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.

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;

OLE Automation Delphi Excel Chart Labels positioning

I am generating an excel Pie Chart from Delphi and all goes well except when I want the labels (with the values) to be sent to the exterior of the Pie
So my generated pie looks like this:
And I want to obtain this:
So my code looks like this:
if not CreateExcel then exit;
VisibleExcel(false);
if AddWorkBook then
begin
SelectSheet(1);
end;
AddChart(ChartName,xl3DPieExploded);//xl3DColumn);
SetSourceData(1{ChartName},2,'A1:B5',xlColumns);
randomize;
VisibleExcel(false);
E.ActiveWorkbook.Sheets.Item[2].Cells[1,1] :='Some text';
E.ActiveWorkbook.Sheets.Item[2].Cells[1,2] :='Chart title';
E.ActiveWorkbook.Sheets.Item[2].Cells[2,1] :='Mijloace de productie';
E.ActiveWorkbook.Sheets.Item[2].Cells[3,1] :='Mediul de munca';
E.ActiveWorkbook.Sheets.Item[2].Cells[4,1] :='Sarcina de munca';
E.ActiveWorkbook.Sheets.Item[2].Cells[5,1] :='Executant';
E.ActiveWorkbook.Sheets.Item[2].Cells[2,2] := FloatToStr(proc1)+'%';
E.ActiveWorkbook.Sheets.Item[2].Cells[3,2] := FloatToStr(proc2)+'%';
E.ActiveWorkbook.Sheets.Item[2].Cells[4,2] := FloatToStr(proc3)+'%';
E.ActiveWorkbook.Sheets.Item[2].Cells[5,2] := FloatToStr(proc4)+'%';
// remove the legend
E.ActiveWorkbook.Charts.Item['Chart1'].Select;
E.ActiveChart.Legend.Select;
E.Selection.Delete;
// apply labels
E.ActiveWorkbook.Charts.Item['Chart1'].Select;
E.ActiveChart.SeriesCollection(1).ApplyDataLabels;
// formatting chart labels.
E.ActiveChart.ApplyDataLabels(xlDataLabelsShowLabelAndPercent, false,true,
true{HasLeaderLines: OleVariant; }, false {ShowSeriesName: OleVariant;},
true {ShowCategoryName: OleVariant; }, true {ShowValue: OleVariant;},
false {ShowPercentage: OleVariant; }, false {ShowBubbleSize: OleVariant;},
'; '{Separator: OleVariant; } );
So it has LeaderLines, this far it works, but now I want to see the labels outside of the pie, distanced.
How can I do this. I recorded a Macro in Excel but I cannot seem to be able to apply it to Delphi. The macro code that I want to use in Delphi is:
ActiveSheet.ChartObjects("Chart 1").Activate
Selection.Position = xlLabelPositionOutsideEnd
The Series object has the DataLabels method which if you omit the optional index parameter can return a DataLabels object which has the Position property, that you might see in your recorded macro. So I believe you can add the following line to the end of your posted code:
E.ActiveChart.SeriesCollection(1).DataLabels.Position := xlLabelPositionOutsideEnd;

Update cell query for Excel ADO from Delphi

I have the following code to open my Excel Worksheet, using TADOConnection and TADOQuery:
conExcel.ConnectionString := 'Provider=Microsoft.JET.OLEDB.4.0;Data Source="D:\temp\file.xls";Extended Properties="Excel 8.0;HDR=No"';
conExcel.Connected := true;
When I run the following code:
queryExcel1.SQL.Text := 'SELECT * FROM [Hoja1$]';
queryExcel1.Open;
while not queryExcel1.eof do
begin
showmessage(queryExcel1.Fields.Fields[0].AsString);
queryExcel1.Next;
end;
I get each line of the file, so it is working ok, but I want to update a cell.
I am using this code:
queryExcel2.SQL.Text := 'UPDATE [Hoja1$] SET F1 = 555555';
queryExcel2.ExecSQL;
Which somehow updates every "A" cell on the worksheet to 555555, but what I really want is to just set the A1 cell to 555555. Any hints on how to include the WHERE section of the query. Thanks.
If you want to use an Adoquery you will have to set ParamCheck to false, since you will have to use : for the range you want to update.
An other option would be to use the connection directly.
In the example below the range B2:B2 is used with F1 which is the default name of Jet for the first column. (B2 is the cell we want to update)
Q.SQL.Text := 'UPDATE [Hoja1$B2:B2] SET [F1] = 555555';
Q.ExecSQL;
// or:
//AdoConnection1.Execute('UPDATE [Hoja1$B2:B2] SET [F1] = 555555');

Resources