Delphi - Unable to change Function in Excel PivotField - excel

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;

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 - 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 - Changing Chart Title in Excel causes AV - Updated with complete sample

I am using Delphi Seattle to build and display a Chart in Excel (2013). I build my chart from a pivot table. The chart displays everything fine. It has a default title, so I want to change that. This should be a simple property change, but I keep getting AV errors. When I google, the closest thing I can find mentions that I need to select the Chart and/or Chart title before I change it. [UPDATED] Here is a working sample that shows the problem.
procedure TForm1.Button1Click(Sender: TObject);
var
oExcel : ExcelApplication;
RawDataSheet :_Worksheet;
myChart: Shape;
begin
oExcel := CreateOleObject('Excel.Application') as ExcelApplication;
oExcel.Visible[LOCALE_USER_DEFAULT] := True;
// Add a New Workbook, with a single sheet
oExcel.Workbooks.Add(EmptyParam, LOCALE_USER_DEFAULT);
// Get the handle to the active Sheet, and insert some dummy data
RawDataSheet := oExcel.ActiveSheet as _Worksheet;
RawDataSheet.Range['A1', 'B10'].value2 := 10;
// Now add my chart
myChart := RawDataSheet.Shapes.AddChart2(208, xlColumnClustered, 200, 10, 300, 300, True);
// Try to access the Chart Title... This always AVs here.
myChart.Chart.HasTitle[LOCALE_USER_DEFAULT] := True;
// Set Title Text. If Comemnt out above line, this AVs as well
myChart.Chart.ChartTitle[LOCALE_USER_DEFAULT].Caption := 'New Chart Title';
end;
I cannot find any way to change my Title. I can't even find any way to READ the existing title, let alone change it.
The Microsoft Object Model documentation for ChartTitle says that this is the correct property...(https://msdn.microsoft.com/en-us/library/office/ff840521.aspx)
Additional Info: I did generate my own type library, Excel_TLB, and I have that in my USES clause. My complete USES clause is...
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, ComObj, Excel_TLB;
I was able to set a title as below. The difference is to select the chart to be able to refer to it through ActiveChart. Note that I have office 14 which doesn't have AddChart2, so I used AddChart.
uses
comobj, excel_tlb;
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
oExcel : ExcelApplication;
RawDataSheet :_Worksheet;
myChart: Shape;
begin
oExcel := CreateOleObject('Excel.Application') as ExcelApplication;
oExcel.Visible[LOCALE_USER_DEFAULT] := True;
// Add a New Workbook, with a single sheet
oExcel.Workbooks.Add(EmptyParam, LOCALE_USER_DEFAULT);
// Get the handle to the active Sheet, and insert some dummy data
RawDataSheet := oExcel.ActiveSheet as _Worksheet;
RawDataSheet.Range['A1', 'B10'].value2 := 10;
// Now add my chart
// myChart := RawDataSheet.Shapes.AddChart2(208, xlColumnClustered, 200, 10, 300, 300, True);
myChart := RawDataSheet.Shapes.AddChart(xlColumnClustered, 200, 10, 300, 300);
myChart.Select(False);
oExcel.ActiveChart.HasTitle[LOCALE_USER_DEFAULT] := True;
oExcel.ActiveChart.ChartTitle[LOCALE_USER_DEFAULT].Caption := 'New Chart Title';
end;
Also note, I don't know the implication of the parameter that's passed to Select.
Another option that works is to let go of type safety after some point (where it doesn't work anymore) and rely on VBA examples found on MS documentation (which does not match signatures with that of generated type library). This allowed to work on the chart directly.
procedure TForm1.Button1Click(Sender: TObject);
var
oExcel : ExcelApplication;
RawDataSheet :_Worksheet;
myChart: Shape;
V: OleVariant;
begin
oExcel := CreateOleObject('Excel.Application') as ExcelApplication;
oExcel.Visible[LOCALE_USER_DEFAULT] := True;
// Add a New Workbook, with a single sheet
oExcel.Workbooks.Add(EmptyParam, LOCALE_USER_DEFAULT);
// Get the handle to the active Sheet, and insert some dummy data
RawDataSheet := oExcel.ActiveSheet as _Worksheet;
RawDataSheet.Range['A1', 'B10'].value2 := 10;
// Now add my chart
// myChart := RawDataSheet.Shapes.AddChart2(208, xlColumnClustered, 200, 10, 300, 300, True);
myChart := RawDataSheet.Shapes.AddChart(xlColumnClustered, 200, 10, 300, 300);
V := myChart.Chart;
V.HasTitle := True;
V.ChartTitle.Caption := 'Chart Title';
end;

Delphi Controlling Excel - Creating Pivot Tables and Charts

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);

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.

Resources