Use an Excel .xlsx sheet as calculator (function) in Delphi? - excel

I have a fairly complex Excel sheet made by an external company. It uses several formulas, lookups etc.
From Delphi (version 6 or 7) , is it possible to have this sheet open in the background, allow a user to input a few variables, then pass these on to excel and have the calculated result returned to the delphi program ?
I know how to open excel and read the entire sheet using ADO, but here I need to send data both ways, by reference of range or coordinate.
This could be done as a unit written in Delphi, but the logic is already done in Excel, and it's simpler to write such calculations in Excel.
Simplified example:
procedure do_it;
var
v1: integer;
v2: integer;
begin
v1 := strtoint(inp1.text); // operator provided numbers
v2 := strtoint(inp2.text);
theresult.caption := get_excel_value(v1, v2); // get back result from excel formulas
end;
function get_excel_value(v1, v2: integer) : string;
begin
// pseudocode
myExcel.range('A3').text := v1;
myExcel.range('A4').text := v2;
myExcel.recalculate; // update excel sheet on demand
result := myExcel.range('A10').text;
end;
Is it correct to assume that I need to be looking in the direction of Excel COM or Excel OLE ?
If possible to do - can the excel sheet call and use macros internally (invisible to Delphi) ?

Microsoft Office (Word, Excel and others) are applications which can be fully automated from another application. Everything you can do by hand can also be done programmatically from another application and of course from your Delphi application.
To ease the automation, Delphi is delivered with non-visual components which are "wrapper" around the underlying COM objects and exposes the same properties, methods and events as the COM interface.
Delphi has several sets of components for working with several Office versions. There are components for Office 2000, Office XP and Office 2010. 2010 version is available from Delphi XE2. Of course there are more Office versions, but don’t worry: Microsoft has carefully made his interfaces upward compatible. For example, if you use the Office 2000 components, you can still automate all Office versions from 2000 to the latest. The only restriction is that you cannot easily use new features if you use the old component.
Actually, it is better to use the older components which are capable of doing what you need to do!
This is because the compatibility in Microsoft office API goes upward and not downward.
Since there are several sets of components, you must make sure the correct version is installed.
o see the installed packages, launch Delphi and go to the menu / Component / Install Packages. You see a long list of all installed design time packages with a checkbox telling that the package is available or not.
Locate “Microsoft Office 2000 Sample Automation Server Wrapper Components” (Or Office XP) and check the checkbox in front of the one you plan to use. Click OK and verify that your component palette now include a tab “Servers”.
As an example, we will create a sample application to insert a sentence at the end of a Word document. This is quick and easy!
Create a new VCL forms application, drop a TWordApplication and a TButton on the form. Then add the code below as the button’s OnClick handler.
The quickest way to locate it is to enter WordApplication in the component palette search tool.
TForm1.Button1Click(Sender: TObject);
begin
WordApplication1.Connect;
WordApplication1.Visible := TRUE;
WordApplication1.Selection.EndOf(wdStory, wdMove);
WordApplication1.Selection.Text := 'Delphi Rocks !' + #13;
WordApplication1.Selection.EndOf(wdStory, wdMove);
WordApplication1.Disconnect;
end;

Adding to fpiette's good answer:
Another way to do it is without the components, but instead to directly create an Excel COM object on the fly in the code. Example:
uses
ComObj, Variants;
var
V: Variant;
Square: Double; // could also be of type string
begin
try
// Get a running Excel instance, if there is one
V := GetActiveOleObject('Excel.Application');
except
on EOleSysError do // Probably Excel was not running
begin
// Create a new Excel instance
V := CreateOleObject('Excel.Application');
V.Visible := True; // Optional
end;
end;
//
// Do some work with Excel - a few examples
//
// If no workbook is open, then create one
if VarIsClear(V.ActiveWorkbook) then
begin
V.Workbooks.Add;
end;
V.ActiveSheet.Cells[4, 1].Value := 7; // Put a value into cell A4
V.ActiveSheet.Cells[5, 1].Formula := '=A4 * A4'; // Put a formula into A5
V.Calculate; // Normally not needed, but in case the user has chosen manual calculation
Square := V.ActiveSheet.Cells[5, 1].Value; // Read the value of A5
end;
It should be noted, even if perhaps obvious, that all of this works only if Excel is installed on the computer where your Delphi program is run.

Related

Loading resources at design time?

Is it possible to load a resource at design time?
I am making a speed button component, and I want to automatically load a new image from the resource whenever the button size changes. It already works properly at run time, but at design time, after I set the resource name property, it does not show any icon.
I can draw a default rectangle in place of the icon if it is not possible, but it would have been nice to display my icons at design time as well.
function TPngSpeedButton.LoadIcon(ResName: String): Boolean;
var hI: HICON;
Ico: TIcon;
ISize: Integer;
Png: TPngImage;
begin
Result:= False;
if ResName = '' then Exit;
ISize:= Height - 7 - Round(Height * 0.15);
Png:= TPngImage.Create; Ico:= TIcon.Create;
try
if LoadIconWithScaleDown(HInstance, PChar(ResName), ISize, ISize, hI) = S_OK then begin
Ico.Handle:= hI;
ConvertToPng(Ico, Png);
SetPngImage(Png);
Result:= True;
end;
finally
Png.Free; Ico.Free;
end;
end;
You can not load icons at design-time from your application resource, since at that time the application executable doesn't even exist as you haven't compiled it yet.
Now, what you might be able to do is create a resource-based dynamic link library (resource DLL) which you compile separately. This way, you would be able to access the DLL resources even at design-time, similar to how the Delphi IDE is already accessing some system resources.
If you don't want to deal with additional DLLs, then put your icons into one or more ImageLists, since images from ImageLists are available both at run-time as well at design-time.

Excel Application.Windows.Count returns 0

I use Windows 10, Delphi Berlin and Microsoft Office 2007. I try to get the number of opened Excel window. When I download Excel file and open it a seperated Excel runs so only one workbook exists in one Excel window.
I imported Microsoft Office 12.0 Object Library and wrote 2 procedures. Button1Click works with tExcelApplication and Button2Click does with CreateOleObject('excel.application'). After I run Excel the former works well but Count is recognized as an error just in the editor and the latter returns 0.
How Can I remove the annoying error message or get the _Excel to work?
type
TForm1 = class(TForm)
ExcelApplication1: TExcelApplication;
Button1: TButton;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Button1.Caption := IntToStr(ExcelApplication1.Windows.Count);
end;
procedure TForm1.Button2Click(Sender: TObject);
var
_Excel: Variant;
begin
_Excel := CreateOleObject('excel.application');
Button2.Caption := _Excel.windows.count;
end;
The message in the IDE is because you are using late bound COM. The method calls are dispatched at runtime and only at runtime do you find out whether or not the method exists. Because of that the compiler can't check the validity of your code. Your code is fine because it executes correctly, but the nature of late bound COM means the IDE thinks your code contains syntax errors. You just have to ignore it when using late bound COM.
You can switch instead to early bound COM and have the compiler be able to check the syntax of your code.
There are pros and cons of both approaches. Late bound can often yield simpler and more concise code. But at the expense of frustration when you only find out your mistakes at runtime.
If the value returned is zero then I guess the obvious conclusion is that there are no windows. The late bound code is creating a new instance of Excel, but the early bound code is attaching to an existing instance. To obtain an existing instance, if there is one, use GetActiveOleObject.

Delphi 7 with..do statement doesen't work with variant variable

I'm working with Microsoft Excel via Delphi 7. It works fine but while formatting rows and ranges I have to write such long strings.
XLApp.Workbooks[1].WorkSheets[NameDoc].Range['A19:L19'].Font.Bold := true;
So I want to get rid of hard work and do it via "with..do" statement like this
with XLApp.Workbooks[1].WorkSheets[NameDoc] do
begin
Range['A19:L19'].Font.Bold := true;
end;
But at compilation stage I see this error
Record, object or class type required
on string - "with..do".
I creating Excel object this way
XLApp: Variant;
XLApp := CreateOleObject('Excel.Application');
I consider that with..do statement doesen't works with variant type variable, but I want to know whether I'm right or not? And if I'm right is there any workaround to make it work?
Variant can be anything or nothing at all - compiler doesn't know it and cannot know: it is so called "dynamically typed value". Since it does not know - it does not know if there would be any members (properties, methods) and if there would - what names would they have.
To get the benefits of strong compile-time typing - including using of with but not only - you have to use interface variables, those that are provided by TExcelApplication component and underlying unit having those values "statically typerd" - thus providing for Delphi compiler to know value types when compiling, in before running. There are plenty of types like iWorsksheet, iRange and others in that unit.
Borland Delphi 7 TExcelApplication.Connect works on office machines but not at client
http://www.delphipages.com/forum/showthread.php?t=157889
http://delphikingdom.ru/asp/viewitem.asp?catalogid=1270
However, since that is about reference-counting and lifetime I'd suggest you go with explicit use of temp variables rather than using with with and implicit invisible variables. Since you cannot control their lifespan and their clearance you might hit the wall in some unexpected place later. I did.
var tmpR: iRange; // assuming we have statically-typed API
// for example - providing we using ExcelXP or Excel2000 unit
tmpR := XLApp.Workbooks[1].WorkSheets[NameDoc];
tmpR.Range['A19:L19'].Font.Bold := true; // instead of with
with tmpR do // also possible but gives little benefit now
begin // when we made a dedicated temp var
Range['A19:L19'].Font.Bold := true;
end;
tmpR := nil; // crucial unless the most short and simplistic functions
// just release hold on Excel's object - let it manage its memory freely,
// by letting Excel know your program no more uses that object.
Also read
https://en.wikipedia.org/wiki/Automatic_Reference_Counting
https://en.wikipedia.org/wiki/Component_Object_Model
Can with be used with a Variant?
No.
You can use with for types whose members are known at compile time. But variants, for which the . operator is evaluated at run time, do not fall into this category. Hence with is not available for variants.
The documentation says, with my emphasis:
A with statement is a shorthand for referencing the fields of a record
or the fields, properties, and methods of an object. The syntax of a
with statement is:
with obj do statement
or:
with obj1, ..., objn do statement
where obj is an expression yielding a reference to a record, object
instance, class instance, interface or class type (metaclass)
instance, and statement is any simple or structured statement.

Why doesn't TPageProducer remove quotation marks from strings?

I'm trying to debug behaviour that has only appeared when my large app - working fine in XE3 - is run after compiling with XE4. The issue seems to cause some quoted strings (eg "MyString") to retain their quotes even after having been 'de-quoted' by TPageProducer in Web.HTTPProd. For example, consider the code below which is small extract from this Delphi source unit Web.HTTPApp:
procedure ExtractHeaderFields(Separators, _WhiteSpace: TSysCharSet; Content: PChar;
Strings: TStrings; Decode: Boolean; StripQuotes: Boolean = False);
{$ENDIF NEXTGEN}
var
Head, Tail: PChar;
EOS, InQuote, LeadQuote: Boolean;
QuoteChar: Char;
ExtractedField: string;
{$IFNDEF NEXTGEN}
WhiteSpaceWithCRLF: TSysCharSet;
SeparatorsWithCRLF: TSysCharSet;
{$ENDIF !NEXTGEN}
function DoStripQuotes(const S: string): string;
var
I: Integer;
InStripQuote: Boolean;
StripQuoteChar: Char;
begin
Result := S;
InStripQuote := False;
StripQuoteChar := #0;
if StripQuotes then
begin
for I := Result.Length - 1 downto 0 do
if Result.Chars[I].IsInArray(['''', '"']) then
if InStripQuote and (StripQuoteChar = Result.Chars[I]) then
begin
Result.Remove(I, 1);
InStripQuote := False;
end
else if not InStripQuote then
begin
StripQuoteChar := Result.Chars[I];
InStripQuote := True;
Result.Remove(I, 1);
end
end;
end;
I see this called when I use TPageProducer and I can see my good source string go into the ExtractHeaderFields routine above and then into the 'DoStripQuotes' function. Stepping into DoStripQuotes and watching 'Result' shows that it does not change, even when Result.Remove is called (to strip the quote). When I take this 'DoStripQuotes' routine out to a simple test app, it wont compile, telling me that 'Result.anything' is not allowed. I assume then that Result, although it is defined as 'string' must be another type of string in the context of Web.HTTPProd.
So I get to thinking maybe this is something to do with the 'Immutable strings' that I've heard about. I read this SO question about that and although I get the gist, I could do with more practical advice.
Specifically, I would like answers to the following questions:
What type of 'string' is 'Result' if the notation Result.Length is allowed?
Is there a way in which I can tell the compiler to use 'XE3' compatibility for a unit? (THis might allow me to see where the problem is originiating). Ive ttried {$ZEROBASEDSTRINGS ON} / OFF but this seems to cause even more chaos and I don't know what I'm doing!
Thanks for any help.
LATER EDIT: As noted in the accepted answer below this is a bug in the VCL unit Web.HTTPApp.pas which should read "Result := Result.Remove(I,1)" in two places around line 2645 and not "Result.Remove(I,1)"
What type of 'string' is 'Result' if the notation Result.Length is allowed?
It's just the same old string, aliased to UnicodeString, that you've been using since Delphi 2009. The difference is that this code uses the new record helper (specifically SysUtils.TStringHelper). That's what lets you use . notation on a string variable.
Is there a way in which I can tell the compiler to use 'XE3' compatibility for a unit?
No. The code in question is a library unit and it is designed to be compiled in a particular mode. What's more, you can't readily re-compile it unless you take on compiling the RTL/VCL yourself. Even if there was such a mode, it would not help since the code is simply wrong (see below). No amount of mode switching can fix this particular piece of code.
I get to thinking maybe this is something to do with the Immutable strings that I've heard about.
It's not. None of the Delphi compilers have immutable strings yet. The concept of immutable strings is just something that has been floated as a future change. And if the change is made, expect it to be made in the mobile compilers first.
The problem is in fact just a rather simple bug in the code that you posted which has clearly had no testing at all. The use of Remove is wrong. That method does not modify the string in-place. Instead it returns a new string that has the character removed. The code should read:
Result := Result.Remove(I, 1);
The reason that the developer who coded ExtractHeaderFields has made this mistake is that whoever designed the string helper code named the Remove method incorrectly. Since Remove is a verb you would expect it to operate in-place. A method that does not modify the subject, and returns a new instance, as this method does, should be given a name that is a noun. So this method should be named something like Remnants. It looks to me as though the RTL designers copied the .net naming where the same flaw also exists.
You should submit a QC report, if one does not already exist. I know that XE4 update 1 has just been released. It's plausible that it contains a fix.
Your other options, as I see them, are:
Stick with XE3 until XE4 is sufficiently debugged.
Include a copy of the Web.HTTPApp unit in your project and fix the bugs yourself.

List Index Out of Bounds on screen.forms

I do some reporting on a form with reportbuilder.
On the main form I select some items on a grid and then a generate the reports of the items.
I want to do this in a Tthread but i get an error 'List index out of bounds'.
Here is the call stack:
Classes.TList.Get(1244868)
Classes.TList.Get(???)
Forms.TScreen.GetCustomForms(???)
Forms.TApplication.DoActionIdle
Forms.TApplication.Idle(???)
Forms.TApplication.HandleMessage
Forms.TApplication.Run
Seems some form is either not being added to the Screen.Forms
collection in a timely manner or one is being freed from it during the
loop in DoActionIdle.
Any ideas on how to circumvent this problem?
I work on windows XP and delphi 2010.
I Also I've the problem with a test procedure on my application
TForm3 is just a form with no code.
TDebugThread = class(TThread)
protected
procedure Execute; override;
public
constructor Create();
end;
constructor TDebugThread.Create;
begin
FreeOnTerminate := True;
inherited Create(False);
end;
procedure TDebugThread.Execute;
var
oReport: DeBugReport.TForm3;
begin
inherited;
oReport:= DeBugReport.TForm3.Create(Nil);
try
sleep(1000);
finally
oReport.Free;
end;
end;
....
procedure RunThread();
begin
TDebugThread.Create();
end;
Recapitulation:
I have a list of some Intervention on a form. Each detail and resumation of the intervention can I print on 2/5 reports. Therefore I use reports components (reportbuilder) on another form (not visible). The new feature was to multiselect some interventions on the list and set the reports in a folder in pdf format. That's was simple just on each intervention call the reportform and some parameters to change and save into pdf.
But this take to long. The user must wait until the procedure was ended. No problem I set the procedure in a thread. But there I get the error 'List index out of bounds'. ArgggArggg, I was suspected that the reportform (created, to his job and then destroyed) the problem was but hoped that there was another solution. I was thinking to change the TForm into TDataModule. Can I set all the components of the form into the datamodule. I use the TDbGrid to see some values in design. But in the Tdatamodule I can't set a TDBGrid. Ok, I can live without the TDbGrid. So I transformed the TForm into TDataModule.
But the TDataModule is not the answer. There I get the error 'Graphics.OutOfResource' from a TBitmap. I think that the TBitmap is calling from the TppReport. Now I'm done. I'm changing my code all more than 2 days with no result. I leave the TThread for this time.
Let's take a look at TApplication.DoActionIdle:
procedure TApplication.DoActionIdle;
var
I: Integer;
begin
for I := 0 to Screen.CustomFormCount - 1 do
with Screen.CustomForms[I] do
if HandleAllocated and IsWindowVisible(Handle) and
IsWindowEnabled(Handle) then
UpdateActions;
end;
Let's assume that Screen.CustomFormCount and is implemented correctly and always returns the number of items indexed by Screen.CustomForms. In which case the conclusion is that the body of the loop is deleting a form. That is Screen.CustomFormCount is changing during the execution of the loop.
The only way that can happen is if one of the form's action update handlers results in a form being deleted. So, I can't tell you any more than that, but this analysis should lead you to the root cause of the problem.
And the second part of your question is quite simple. You cannot use VCL components outside the main GUI thread.
In fact it is plausible that destroying the VCL form in your thread is what is leading to Screen.CustomFormCount changing during the execution in the GUI thread of TApplication.DoActionIdle.

Resources