Calling a computationally intensive routine from VBA without stalling Excel GUI
I have a set of numerically intensive routines (each takes up to 1 minute to complete) bundled in a COM object, implementing IDispatch
cleanly.
I can therefore use them from an Excel worksheet, those routines will be called by VBA macros triggered by buttons.
Now, when one of these routines is called, the Excel user interface is frozen, which is quite uncomfortable for the end users of the sheet.
I'd like to find any mechanism to alleviate this problem.
This could be for instance launching the computation in another thread launched on the COM side, returning immediately, the spawned thread calling back a VBA procedure when results are computed.
Or something simpler, since I only need one computation to be performed at a time.
Now, there may be a lot of issues with calling VBA routines from other threads. I must confess that I am not that experienced with COM, that I only treat as a black box between my code and Excel (I use ATL).
So,
Is it possible to call back VBA routines from another thread ?
Is there a better way to do what I want to achieve ?
UPDATE
After weighing the options and reading a lot of stuff on the internet, I开发者_如何学Python will do cooperative multithreading: in the COM object, instead of having one routine, I shall have three:
class CMyObject : ...
{
...
STDMETHOD(ComputationLaunch)(...); // Spawn a thread and return immediately
STDMETHOD(ComputationQuery)(DOUBLE* progress, BOOL* finished);
STDMETHOD(ComputationResult)(VARIANT* out);
private:
bool finished, progress;
boost::mutex finished_lock, progress_lock;
ResultObject result; // This will be marshaled to out
// when calling ComputationResult
};
And in the VBA:
Private computeActive as Boolean ' Poor man's lock
Public Sub Compute()
OnError GoTo ErrHandler:
If computeActive Then Exit Sub
computeActive = True
Dim o as MyObject
call o.ComputationLaunch
Dim finished as Boolean, progress as Double
While Not o.ComputationQuery(progress)
DoEvents
' Good place also to update a progress display
End While
Dim result as Variant
result = o.ComputationResult
' Do Something with result (eg. display it somewhere)
computeActive = False
Exit Sub
ErrHandler:
computeActive = False
Call HandleErrors
End Sub
Indeed, by doing a depth-first-search on the internet for COM Add-Ins, I realized that VBA macros run in the same event loop as Excel's GUI, that you have the DoEvents
facility, and that it is not safe (or at least very tricky) to call back VBA procedures from other threads. This would require eg. tricking the Accesibility facilities to obtain a synchronized handle to an Excel.Application
object, and call the OnTime
method to set up an asynchronous event handler. Not worth the trouble.
If you want to do this well you need to give up on VBA and write a COM add-in.
Posting my comment as an answer...
You could implement an event in your COM object and have it call back when done. See http://www.dailydoseofexcel.com/archives/2006/10/09/async-xmlhttp-calls/ for an example of how to run a COM object asynchronously.
My dirty hack is: create a new instance of Excel, run the code there.
Another option is to schedule the run for later, have the user say when. (In the example below, I've just hard-coded 5 seconds.) This will still freeze the user interface, but at a scheduled, later time.
Sub ScheduleIt()
Application.OnTime Now + TimeValue("00:00:05"), "DoStuff"
End Sub
Sub DoStuff()
Dim d As Double
Dim i As Long
d = 1.23E+302
For i = 1 To 10000000#
' This loop takes a long time (several seconds).
d = Sqr(d)
Next i
MsgBox "done!"
End Sub
精彩评论