Inserting images to an Excel sheet in Delphi - excel

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;

Related

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.

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.

Excel 2007 gets CSV file wrong?

I am using Delphi 7 to generate CSV file from data acquired from COM port. Now, this seems to work OK since any other spreadsheet software reads generated file properly (Open Office and Libre Office spreadshhet software, for instance). Excel 2007 does the following. Instead of these two column values:
1.976139e-2<TAB>22.98027
i got these two column values:
1.98E+04<TAB>2.298.027
Note that generated file opened in any text editor (Notepad++ for instance) has proper values.
What could be the problem here ?
Thanks,
Josip
If the setting in Delphi do not fit your needs you could change them before loading the CSV. But you should make sure to reset them afterwards.
var
Excel, WorkBook: Variant;
KeepDecimalSeparator, KeepThousandsSeparator: String;
KeepUseSystem: Boolean;
begin
Excel := CreateOleObject('Excel.Application');
Excel.Visible := true;
try
KeepDecimalSeparator := Excel.DecimalSeparator;
KeepThousandsSeparator := Excel.ThousandsSeparator;
KeepUseSystem := Excel.UseSystemSeparators;
Excel.DecimalSeparator := '.';
Excel.ThousandsSeparator := ',';
Excel.UseSystemSeparators := false;
WorkBook := Excel.WorkBooks.Open('C:\Temp\1.csv');
finally
Excel.DecimalSeparator := KeepDecimalSeparator;
Excel.ThousandsSeparator := KeepThousandsSeparator;
Excel.UseSystemSeparators := KeepUseSystem;
end;
end;
Try to use quotes.
"1.976139e-2"<TAB>"22.98027"

reasons why excel.exe remains loaded after running a delphi client automation program?

I wrote a Delphi program that extracts and consolidates data from several different spreadsheets of a single .XLS file, to a text file for later processing. It is a Delphi 7 console program.
An excerpt of the most relevant pieces of code will show you that, apparently, my program is pretty well behaved or at least as much as it needs to be.
uses ... ActiveX, ComObj ... ;
procedure Fatal(s:string);
...
Halt(1);
var ExcelApp:Variant; (* global var *)
begin (* main program block *)
coInitialize(nil);
ExcelApp:=CreateOleObject('Excel.Application');
try
ExcelApp.Visible:=False;
ExcelApp.WorkBooks.Open(ExcelFileName);
...
XLSSheet := ExcelApp.Worksheets[ExcelSheetName];
...
try
XLSRange := XLSSheet.Range[ExcelRangeName];
except
Fatal('Range "'+ExcelRangeName+'" not found');
end;
if VarIsNull(XLSRange) then Fatal('Range '+ExcelRangeName+' not found');
for row:=XLSRange.Row to XLSRange.Rows[XLSRange.Rows.Count].Row do
for col:=XLSRange.Column to XLSRange.Columns[XLSRange.Columns.Count].Column do
CellValue:=XLSSheet.Cells[Row,Col].Value;
...
if CellValue<>'' then ...
...
ExcelApp.Workbooks.Close;
...
finally
ExcelApp.Quit;
coUninitialize;
end;
end.
Sometimes, when the program exits, the XLS remains locked. Looking at the Task Manager, I see that Excel.exe process that was started when the client program ran, is still running, eventhoug the client program has exited and succesfully unloaded.
Do you happen to know what are the usual suspects for this behaviour? have any idea where to look for always unloading excel upon client execution?
You need to release the ExcelApp variant. It still holds a reference count of 1, and therefore Excel isn't completely closed.
Add this to your code (the marked line):
finally
ExcelApp.Quit;
ExcelApp := Unassigned; // Add this line
coUninitialize;
end;
Here is some simple code to reproduce the problem, and test the solution:
// Add two buttons to a form, and declare a private form field.
// Add OnClick handlers to the two buttons, and use the code provided.
// Run the app, and click Button1. Wait until Excel is shown, and then click
// Button2 to close it. See the comments in the Button2Click event handler.
type
TForm1=class(TForm)
Button1: TButton;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
ExcelApp: Variant;
end;
implementation
uses
ComObj;
procedure TForm1.Button1Click(Sender: TObject);
begin
ExcelApp := CreateOleObject('Excel.Application');
ExcelApp.Visible := True;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
ExcelApp.Visible := False;
ExcelApp.Quit;
// Leave the next line commented, run the app, and click the button.
// After exiting your app NORMALLY, check Task Manager processes, and you'll
// see an instance of Excel.exe still running, even though it's not
// in the Applications tab.
//
// Do an "end process" in Task Manager to remove the orphaned instance
// of Excel.exe left from the above. Uncomment the next line of code
// and repeat the process, again closing your app normally after clicking
// Button2. You'll note that Excel.exe is no longer in
// Task Manager Processes after closing your app.
// ExcelApp := Unassigned;
end;
end.
I have encountered much the same problem in XE2 and my solution was to replace such code samples:
fExcel.ActiveWorkBook.ActiveSheet.Range[
fExcel.ActiveWorkBook.ActiveSheet.Cells[3, 2],
fExcel.ActiveWorkBook.ActiveSheet.Cells[3+i,1+XL_PT_Tip_FieldCount]
].Formula := VarArr;
with:
cl := fExcel.ActiveWorkBook.ActiveSheet.Cells[3, 2];
ch := fExcel.ActiveWorkBook.ActiveSheet.Cells[3+i,1+XL_PT_Tip_FieldCount];
fExcel.ActiveWorkBook.ActiveSheet.Range[cl, ch].Formula := VarArr;
Same happens in this case, where sheet variable is used:
sheetDynamicHb := fExcel.ActiveWorkBook.Sheets['Dynamics Hb'];
cl := sheetDynamicHb.Cells[52, 2];
ch := sheetDynamicHb.Cells[52+i, 2+3];
sheetDynamicHb.Range[cl, ch].Formula := VarArr;
Somehow introducing temp variables (cl,ch: Variant) does the trick. It seems like the nested Excel variable access does something odd. I can not explain why this works like that, but it does work..
I faced the same issue trying to close "zombie" Excel processes (the ones that stay running if I launch them from my app and then forced terminate the app). I tried all suggested actions with no luck. Finally I created a combined killer procedure that robustly does the trick using WinApi if usual COM methods do not help.
procedure KillExcel(var App: Variant);
var
ProcID: DWORD;
hProc: THandle;
hW: HWND;
begin
hW := App.Application.Hwnd;
// close with usual methods
App.DisplayAlerts := False;
App.Workbooks.Close;
App.Quit;
App := Unassigned;
// close with WinApi
if not IsWindow(hW) then Exit; // already closed?
GetWindowThreadProcessId(hW, ProcID);
hProc := OpenProcess(PROCESS_TERMINATE, False, ProcID);
TerminateProcess(hProc, 0);
end;

Resources