My company has a large selection of templates which are used to generate customer correspondence. I need to modify the existing processes so that copies of generated files (template + data) are saved for later editing.
My problem is that when I open one of these saved MSWord documents, edit, then close, MSWord is insisting that changes have been made to the template (the one selected in the generation process).
I am not really sure why this is happening, but it may be that the generated document contains a reference to the template upon which it was based, but that because the template is in a remote location, MSWord is attempting to generate a new local file.
If that diagnosis is correct, then I need a method to remove the template reference from the document.
If the diagnosis is incorrect then what is the likely explanation/solution?
I have found that BOTH resultant files contain a reference to the template.
Note: Manual editing in Word has no issue. If I let the letter generate and save to disk from Winword, I can open it and manipulate it quite happily. Somewhere in the automation steps the problem is being created.
Interestingly - I have changed the save format to '.rtf' and the problem remains.
Further - it doesn't matter if I say 'Yes' to saving changes to the template, it continues to prompt me each time I open and close the document (whether I edit or not)
I have discovered that by saving the document as wdFormatXML I can see the reference to the letter template and edit it. If I do that the problem goes away.
I am now attempting to achieve the same result via automation, but with no success;
WordApp.ActiveDocument.Set_AttachedTemplate(tmplt);
Does not work for values of tmplt 'Normal.dot', varNull, 'c:\progra~1\etc\Simple.dotx' and so on. The function call tells me it cannot find the template for the first 2 of those values, or merely hangs.
I am back to my original question - how does one clear the attached template ?
I eventually figured it. My problem was down to late-binding in some way. I found that the following code worked
var
docpath : OleVariant;
fmt : OleVariant;
tmplt : OleVariant;
WordApp : WordApplication;
WordDoc : WordDocument;
begin
docpath := SaveLoggedDocToDisk(GetCurrentFileName());
WordApp := CoWordApplication.Create;
try
fmt := EDITABLE_FORMAT;
tmplt := '';
WordDoc := WordApp.Documents.Open(docpath, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, fmt, EmptyParam, EmptyParam );
WordDoc.Set_AttachedTemplate(tmplt);
The problem I had with earlier versions of this code was that
WordApp.ActiveDocument.SetAttachedTemplate(tmplt);
although it appears equivalent, was not behaving. By creating a variable of type WordDocument the routine sprang into life.
Related
In a Delphi application I am using since years the following code to export xlxs to pdf:
function TExportTool.ExportExcelToPDF(aFileName, aNewFileName: String): Boolean;
// reference : http://embarcadero.newsgroups.archived.at/public.delphi.oleautomation/200811/081103142.html
// unluckily the link above is dead
{- Sheet is counted from 1 and upwards !! }
Var
App,oWB,oSheet : OleVariant;
begin
Result := False;
App:= CreateOleObject('Excel.Application');
Try
App.Visible:= 0;
oWb := App.WorkBooks.Open(ExpandUNCFileName(afilename),1); // Open read only
Try
oSheet := oWB.ActiveSheet;
oSheet.ExportAsFixedFormat(0, //xlTypePDF is constant 0
aNewFileName,
EmptyParam,
EmptyParam,
EmptyParam, // this should be IgnorePrintAreas
EmptyParam,
EmptyParam,
EmptyParam,
EmptyParam
);
Finally
End;
Result := True;
Finally
App.Quit;
App:= UnAssigned;
End;
end;
// IMPROVED WORKING CODE FOLLOWS
function TExportTool.ExportExcelToPDF(aFileName, aNewFileName: String): Boolean;
// reference : http://embarcadero.newsgroups.archived.at/public.delphi.oleautomation/200811/081103142.html
{- Sheet is counted from 1 and upwards !! }
procedure RestoreOriginalPrintArea (oSheet: OleVariant);
// Excel loses print area settings in non-English version of application when file is opened using automation:
// https://stackoverflow.com/questions/71379893/exportasfixedformats-ignoreprintareas-parameter-seems-not-to-have-effect
var
i:Integer;
begin
for i:= 1 to oSheet.Names.Count do
begin
if VarToStr(oSheet.Names.Item(i).Name).EndsWith('!Print_Area') then
begin
oSheet.PageSetup.PrintArea:='Print_area';
Break;
end;
end;
end;
Var
App,oWB,oSheet : OleVariant;
i:Integer;
begin
Result := False;
App:= CreateOleObject('Excel.Application');
Try
App.Visible:= 0;
oWb := App.WorkBooks.Open(ExpandUNCFileName(afilename),1); // Open read only
Try
oSheet := oWB.ActiveSheet;
RestoreOriginalPrintArea(oSheet); // workaround
oSheet.ExportAsFixedFormat(0, //xlTypePDF is constant 0
aNewFileName,
0, // standard quality = 0, Max quality = 1
false, //include doc properties
false, //ignore print area
EmptyParam,
EmptyParam,
EmptyParam,
EmptyParam
);
Finally
End;
Result := True;
Finally
oWB.Close(false); // better to close the WorkBook too
App.Quit;
App:= UnAssigned;
End;
end;
Now i realized that the pdf created with this code behave like when saving to pdf from Excel using the option "Ignore Print areas" (it is one of the options of the export to pdf from Excel feature).
So I decided to "uncheck" that checkbox also from code and I studied the parameters of ExportAsFixedFormat (reference here).
The fifth parameter is IgnorePrintAreas, so I was assuming that passing False to it, the print areas would have been ignored.
I tried several common sense solution, including:
passing only that parameter (passing either True or False )
passing all the first 5 parameters (just in case they are mandatory at runtime)
but no result: the pdf created by my application still "ignores the print areas".
Does anyone has a suggestion or has experience on this specific subject to give me a pointer to fix this issue?
Thanks.
UPDATE
Thanks to the useful accepted answer I appended to the code above the solution for reference, notice two things:
the RestoreOriginalPrintArea procedure that contains the workaround
the call to oWB.Close(false) at the end
Root cause of error:
Excel loses print area settings in non-English version of application when file is opened using automation.
Why this is happening:
When you define print area in a sheet, Excel internally creates a named range. It has two properties defining its name:
Name this property is always of the form WorksheetsName!Print_Area (if the sheet's name contains some special characters it is also enclosed in single quotes).
NameLocal has similar structure, but the second part is translated into the language of the application.
This is what it looks like when you open the file in Excel and inspect these properties in VBA, but when you open the same file using automation (for example using the code in question), then NameLocal is no longer translated. This bug causes the named range to not be recognized correctly as print area. oSheet.PageSetup.PrintArea returns an empty string.
Workaround:
Restore original print area after opening the file using:
oSheet.PageSetup.PrintArea:='Print_Area';
This line of code will throw an exception when there was no print area defined in sheet, so there are two options:
Place the line inside try..except block.
Iterate the Names collection and look for a Name ending with !Print_Area, for example:
var i:Integer;
for i:= 1 to oSheet.Names.Count do
begin
if VarToStr(oSheet.Names.Item(i).Name).EndsWith('!Print_Area') then
begin
oSheet.PageSetup.PrintArea:='Print_area';
Break;
end;
end;
Other important change:
Because the file could have been modified you also need to add:
oWB.Close(false); //do not save changes
before closing the application, otherwise each call to this function would result in another Excel process still running invisible.
I have this simple code to check if a record exists in a table, but it always returns a runtime error :
Arguments are of the wrong type, are out of acceptable range, or are
in conflict with one another.
my code is this :
function TDataModuleMain.BarCodeExists(barCode: string): boolean;
begin
if ADOQuerySql.Active then
ADOQuerySql.Close;
ADOQuerySql.SQL.Clear;
ADOQuerySql.SQL.Text := 'select count(1) from Card where BarCode = (:TestBarcode)';
ADOQuerySql.Parameters.ParamByName('TestBarcode').Value := barCode;
ADOQuerySql.Open; // HERE THE RUNTIME ERROR APPEARS
Result := ADOQuerySql.Fields[0].AsInteger = 1;
ADOQuerySql.Close;
ADOQuerySql.Parameters.Clear;
end;
The field BarCode in table Card is of type nvarchar(100)
In debug I see that the parameter is created, and gets populated with the correct value.
Running the query in sql server management studio also works.
I also found this How to pass string parameters to an TADOQuery? and checked my code with the code in the answer but I don't see any problems here.
Also this AdoQuery Error using parameters did not help me.
It will no doubt be something very simple that I have missed but I just dont see it now.
EDIT : things I tried from suggestions in the comments:
.ParamCheck := True (default)
.Parameters.ParamByName('TestBarcode').DataType := ftString
.Parameters.ParamByName('TestBarcode').DataType := ftWideString
None of these worked however.
What did help was using a non-shared AdoQuery for this, and that one did the job without any errors. I am using that now as the solution but I am still looking at the shared AdoQuery out of curiousity what the exact problem is.
EDIT: the source of the problem is found.
I used the function provided by MartinA to examine both the dynamic created query and the shared AdoQuery and I found one difference.
The shared AdoQuery had the this property filled :
ExecuteOption := [eoExecuteNoRecords]
and the dynamic created query does not.
Since this property is not set in designtime I did not see it.
After clearing the property to [] the shared AdoQuery worked again.
I am going to switch to using non shared AdoQuery for this kind of work as been suggested.
Thanks everyone for your assistance.
The following isn't intended to be a complete answer to your q, but to follow up my comment that "all you have to do is to inspect your form's DFM and compare the properties of your original ADoQuery with the unshared one. The answer should lie in the difference(s)" and your reply that the unshared query is created dynamically.
There is no "voodoo" involved in the difference in behaviour between your two ADOQuerys. It's just a question of capturing what the differences actually are.
So, what you need, to debug the problem yourself, is some code to compare the properties of two components, even if one or both of them is created dynamically. Using the following routine on both components will enable you to do exactly that:
function TForm1.ComponentToString(AComponent : TComponent) : String;
var
SS : TStringStream;
MS : TMemoryStream;
Writer : TWriter;
begin
// Note: There may be a more direct way of doing the following, without
// needing the intermediary TMemoryStream, MS
SS := TStringStream.Create('');
MS := TMemoryStream.Create;
Writer := TWriter.Create(MS, 4096);
try
Writer.Root := Self;
Writer.WriteSignature;
Writer.WriteComponent(AComponent);
Writer.FlushBuffer;
MS.Position := 0;
ObjectBinaryToText(MS, SS);
Result := SS.DataString;
finally
Writer.Free;
MS.Free;
SS.Free;
end;
end;
Over to you ...
I'm using automation to open documents in Word. Sometimes I need to open document in Read mode ON:
var
WordDocument: _Document;
WA: TWordApplication;
begin
WA := TWordApplication.Create( nil );
WA.OnQuit := DocumentClose;
WA.Connect;
WordDocument := Wa.Documents.Open( FileName, EmptyParam, true {ReadOnly}, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam, EmptyParam );
But user can off the Read mode in opened document:
How can I handle this in OnQuit event in procedure DocumentClose ?
In DocumentClose I want to know if document is in read mode or not.
I do not have any solution because I did not have enough experience with it.
So, I need your suggestions, advices about it. Sorry for my English and if I have to add more informations, please let me know. Thanks
UPDATE
I've tried to read protection type, but it's always return the first case. So, when document opening as ReadOnly isn't protected as wdAllowOnlyReading. Some documents can be protected with password, but there is not problem with it.
const
wdAllowOnlyReading: Longword = $00000003;
wdNoProtection: Longword = $ffffffff;
var
ProtectionType: TOleEnum;
begin
ProtectionType := WordDocument.ProtectionType;
case ProtectionType of
wdNoProtection : Showmessage('NoProtection');
wdAllowOnlyReading: Showmessage('ReadOnly');
end;
end;
I'm not sure exactly what you mean by "ReadOnly".
The WordDocument has a ReadOnly boolean property which is read-only in the sense that you can read its value but not set it. This property returns true if when the document was opened, it was already open e.g. on a different workstation, so that the user would get the prompt "This document is locked for editing ..." and asked whether to open the document in read-only mode or whether Word should open a copy instead.
The other sense in which a Word document might be "read only" is if the used has marked it "Final" by clicking the Word button (that leads to the File menu, etc) and gone to Prepare | Mark as Final (in the "Ribbon" versions of MS Word).
To read these properties in code you can do e.g.
if WordDoc.Final then
Caption := 'Final'
else
Caption := 'not Final';
if WordDoc.ReadOnly then
Caption := Caption + ' Read only'
else
Caption := Caption + ' Read/write'
Note: The Final property is not surfaced in Delphi's Word2000.Pas, so to use it you need to go from early binding to late binding, like this:
var vWordDoc : OleVariant;
[...]
vWordDoc := WordDoc;
if vWordDoc.Final then
[...]
Unlike the ReadOnly property, you can toggle the Final property simply by
WordDoc.Final := not WordDoc.Final
But whether you can do this successfully when WordDoc.ReadOnly is True depends on why WordDoc.ReadOnly is True.
If WordDoc.ReadOnly is True because the document was edit-locked when it was opened because it was already open at another workstation, WordDoc.Final is read-only. Otoh, if it's True because you specified ReadOnly in the call to .Open(), then you need to watch out: You can then set Final to False and the user will then be able to edit the document despite its having been opened ReadOnly!
Another complication is that ProtectionType is not directly related to "ReadOnly", as I imagine you've gathered: it can, but doesn't necessarily, prevent editing except to certain regions of a document.
Delphi XE6. I have an application for text snippets, which are stored in a local Database (ABS DB). Some snippets may be straight text, others may include formatting. If the snippets include formatting, the snippets are MS Word format.
The user can view the snippets in two ways, inside my app, or by calling MS-Word, and having the snippet loaded there automatically, - IT IS THE SAME SNIPPET ..
Issue: If the snippet is loaded into a TOLEContainer in my app, it displays fine without a problem... If the snippet gets spawned off into MSWord, I get an error...
"We're sorry. We can't open because we found a problem with its contents." I click on OK, and then get "Word found unreadable content in . Do you want to recover the contents of this document?..." I clik OK, and everything displays fine.
My general processing flow for the "spawn off MS WORD" is...
// FN is a temp file name
FileStream := TFileStream.Create(FN, fmCreate);
BlobStream := dm_text.tEntries.CreateBlobStream(dm_text.tEntries.FieldByName('ANSWER_FMT'), bmRead);
FileStream.CopyFrom(BlobStream, BlobStream.Size);
BlobStream.Free;
FileStream.Free;
// Now open default association, which will be Word
ShellExecute(Handle, 'open', PWideChar(FN), nil, nil, SW_SHOWNORMAL);
This flow is nearly identical for the In place viewing... other than a few commands for the TOleContainer.
OleWord.Enabled := True;
FileStream := TFileStream.Create(FN, fmCreate);
BlobStream := tEntries.CreateBlobStream(tEntries.FieldByName('ANSWER_FMT'), bmRead);
FileStream.CopyFrom(BlobStream, BlobStream.Size);
BlobStream.Free;
FileStream.Free;
OleWord.LoadFromFile(FN);
OleWord.DoVerb(ovInPlaceActivate);
Any ideas why this is happening? This happens on MULTIPLE versions of MSWord.
ADDITIONAL INFO:
Both routines look at the same data, pulled from the exact same row/column in my DB. What I do is create a TEMP file, and then load either via TOleContainer, which loads it fine, or via ShellExecute, which gives an error. However, if I manually load the TEMP file for the OLE Container into MSWord, I get the same error.
So - possibilities...
1). My data is corrupted, i.e. how I save it is wrong...but Word can correct it.
2). I have a setting somehow so that OLEContainer doesn't show the error but Word does.
It is because when using OleContainer.SaveToFile or SaveAsDocument, you are not creating a docx file, but an OleObject containing a docx file. When using OleContainer.SaveToFile with UseOldStreamFormat = True, there is even a Delphi specific header added. Word fortunatly detects this and gives you the option to restore the file.
If you want a valid word-document, then activate the OleContainer (OleContainer.DoVerb(ovPrimary) and then save the document via Word itself (OleContainer.OleObject.SaveAs(MyFileName, wdFormatDocument, EmptyParam, EmptyParam, False).
After that you can store the resulting file in your database.
My D5 application can currently mail merge multiple members data to a Word document using:
wrdapp := CreateOleObjct(word.application);
wrdDoc := wrdApp.Document.Open(TemplateLocation);
wrdMailMerge := wrdDoc.MailMerge;
populateMailMergeDateFile;
wrdMailMerge.execute;
and then
wrdDoc.Saved := False;
wrdDoc.Close(False);
wrdApp.Visible := True;
I would like to offer the option of passing the merged document straight to the printer. However I cannot find the code which allows this to happen
wrdDoc.PrintOut;
wrdDoc.Saved := False;
wrdDoc.Close(False);
Prints out the template document with no merged data.
wrdDoc.Saved := False;
wrdDoc.Close(False);
wrdDoc.PrintOut;
Displays a variant object error.
wrdMailMerge.PrintOut;
Displays an automation errors.
I've tried using True instead of False as well. Can anybody advise me as to how to print the merged document correctly?
many thanks
In my mailmerge code, I set MailMerge.Destination to wdSendToNewDocument before executing the merge, then afterwards I call WordApplication.ActiveDocument.Printout.