该应用程序称为一个接口,编组为一个不同的线程

我正在写一个与Excel通信的delphi应用程序。 我注意到的一件事是,如果我在Excel工作簿对象上调用Save方法,它可能会挂起,因为Excel有一个打开的用户对话框。 我正在使用后期绑定。

我想我的应用程序能够注意到,当保存需要几秒钟,然后采取一些行动,如显示一个对话框,告诉这是发生了什么。

我觉得这很容易。 所有我需要做的是创build一个线程,调用保存并让该线程调用Excel的保存例程。 如果时间太长,我可以采取一些行动。

procedure TOfficeConnect.Save; var Thread:TOfficeHangThread; begin // spin off as thread so we can control timeout Thread:=TOfficeSaveThread.Create(m_vExcelWorkbook); if WaitForSingleObject(Thread.Handle, 5 {s} * 1000 {ms/s})=WAIT_TIMEOUT then begin Thread.FreeOnTerminate:=true; raise Exception.Create(_('The Office spreadsheet program seems to be busy.')); end; Thread.Free; end; TOfficeSaveThread = class(TThread) private { Private declarations } m_vExcelWorkbook:variant; protected procedure Execute; override; procedure DoSave; public constructor Create(vExcelWorkbook:variant); end; { TOfficeSaveThread } constructor TOfficeSaveThread.Create(vExcelWorkbook:variant); begin inherited Create(true); m_vExcelWorkbook:=vExcelWorkbook; Resume; end; procedure TOfficeSaveThread.Execute; begin m_vExcelWorkbook.Save; end; 

我明白这个问题发生,因为OLE对象是从另一个线程(绝对)创build的。

我怎样才能解决这个问题? 很可能我需要为这个电话重新“马歇尔”…

有任何想法吗?

而不是从两个线程访问COM对象,只需在辅助线程中显示消息对话框。 VCL不是线程安全的,但Windows是。

 type TOfficeHungThread = class(TThread) private FTerminateEvent: TEvent; protected procedure Execute; override; public constructor Create; destructor Destroy; override; procedure Terminate; override; end; ... constructor TOfficeHungThread.Create; begin inherited Create(True); FTerminateEvent := TSimpleEvent.Create; Resume; end; destructor TOfficeHungThread.Destroy; begin FTerminateEvent.Free; inherited; end; procedure TOfficeHungThread.Execute; begin if FTerminateEvent.WaitFor(5000) = wrTimeout then MessageBox(Application.MainForm.Handle, 'The Office spreadsheet program seems to be busy.', nil, MB_OK); end; procedure TOfficeHungThread.Terminate; begin FTerminateEvent.SetEvent; end; ... procedure TMainForm.Save; var Thread: TOfficeHungThread; begin Thread := TOfficeHungThread.Create; try m_vExcelWorkbook.Save; Thread.Terminate; Thread.WaitFor; finally Thread.Free; end; end; 

这里真正的问题是Office应用程序不适用于multithreading使用。 因为可以有任意数量的客户端应用程序通过COM发出命令,这些命令被串行化为调用并逐一处理。 但有时Office处于不接受新呼叫的状态(例如,当它显示modal dialog时),并且您的呼叫被拒绝(给您“呼叫被呼叫者拒绝” – 错误)。 另请参阅本主题中Geoff Darst的答案。

你需要做的是实现一个IMessageFilter和照顾你的电话被拒绝。 我是这样做的:

 function TIMessageFilterImpl.HandleInComingCall(dwCallType: Integer; htaskCaller: HTASK; dwTickCount: Integer; lpInterfaceInfo: PInterfaceInfo): Integer; begin Result := SERVERCALL_ISHANDLED; end; function TIMessageFilterImpl.MessagePending(htaskCallee: HTASK; dwTickCount, dwPendingType: Integer): Integer; begin Result := PENDINGMSG_WAITDEFPROCESS; end; function ShouldCancel(aTask: HTASK; aWaitTime: Integer): Boolean; var lBusy: tagOLEUIBUSYA; begin FillChar(lBusy, SizeOf(tagOLEUIBUSYA), 0); lBusy.cbStruct := SizeOf(tagOLEUIBUSYA); lBusy.hWndOwner := Application.Handle; if aWaitTime < 20000 then //enable cancel button after 20 seconds lBusy.dwFlags := BZ_NOTRESPONDINGDIALOG; lBusy.task := aTask; Result := OleUIBusy(lBusy) = OLEUI_CANCEL; end; function TIMessageFilterImpl.RetryRejectedCall(htaskCallee: HTASK; dwTickCount, dwRejectType: Integer): Integer; begin if dwRejectType = SERVERCALL_RETRYLATER then begin if dwTickCount > 10000 then //show Busy dialog after 10 seconds begin if ShouldCancel(htaskCallee, dwTickCount) then Result := -1 else Result := 100; end else Result := 100; //value between 0 and 99 means 'try again immediatly', value >= 100 means wait this amount of milliseconds before trying again end else begin Result := -1; //cancel end; end; 

messagefilter必须在发出COM调用的线程上注册。 我的messagefilter实现将等待10秒,然后显示标准的OLEUiBusy对话框。 这个对话框让你select重试被拒绝的呼叫(在你的情况下保存)或切换到阻止应用程序(Excel显示模式对话框)。 阻塞20秒后,取消button将被启用。 点击取消button将导致您的保存呼叫失败。

所以,不要乱搞线程并实现messagefilter,这是处理这些问题的方法。

编辑:上面的修复“调用被拒绝被调用者”的错误,但你有一个保存挂起。 我怀疑Save会popup一个需要注意的popup窗口(你的工作簿已经有一个文件名了吗?)。 如果是popup窗口,请尝试以下操作(不要在单独的线程中):

 { Turn off Messageboxes etc. } m_vExcelWorkbook.Application.DisplayAlerts := False; try { Saves the workbook as a xls file with the name 'c:\test.xls' } m_vExcelWorkbook.SaveAs('c:\test.xls', xlWorkbookNormal); finally { Turn on Messageboxes again } m_vExcelWorkbook.Application.DisplayAlerts := True; end; 

也尝试使用Application.Visible进行debugging:= True; 如果有popup窗口,有一个变化,你会看到他们,采取行动,以防止他们在未来。

尝试使用COINIT_MULTITHREADED调用CoInitializeEx ,因为MSDN指出:

multithreading(也称为自由线程)允许调用由此线程创build的对象的方法在任何线程上运行。

可以通过使用CoMarshalInterThreadInterfaceInStream来将接口从一个线程“编组”到另一个线程,将接口放入一个stream中,将stream移动到另一个线程,然后使用CoGetInterfaceAndReleaseStream从该stream中返回接口。 在Delphi中看一个例子 。

拉斯的回答是我认为正确的。 他的build议的替代方法是使用GIT(全局接口表),它可以用作接口的跨线程存储库。

在这里看到这个线程代码与GIT交互,在​​那里我发布了一个Delphi单元,提供了简单的访问GIT。

它应该只是一个问题,从您的主线程将您的Excel接口注册到GIT中,然后使用GetInterfaceFromGlobal方法从TOfficeHangThread线程中获取单独的接口引用。