OLE-Dispatch When Doing MFC/Qt Migration
Most people who have ever ported an application from MFC to Qt know that at some point they have to integrate a QWidget into an MFC part of the user interface. Or the other way around, but that's a different story. Normally you would simply use a QWinWidget for that to do exactly what you want. But what happens if your application created the old MFC window as an OLE Control Extension (OCX)? In that case the easiest solution is to intercept the control name and provide it to your own factory providing the correct widget, which can then be embedded in a QWinWidget. Or you could use plugins to stick to the idea of it being DLLs. All roads lead to Rome. But what if your OLE based application uses the OLE-Dispatch way of initializing the controls? Setting properties, invoking methods, and so on?
I had exactly that problem and came up with a solution which glues OLE's IDispatch interface together with Qt's QMetaObject. That means you can access your QWidget's (or any other QObject) properties via the IDispatch interface provided by my QObjectDispatcher class. They only need to be declared via Qt's Q_PROPERTY macro. The same applies to slots and methods marked with Q_INVOKABLE.
Let's see how this is being used:
// This is the widget we're working with:
QWidget* someWidget = ...;
// Lets' create our dispatcher:
// You have to delete it yourself or let COM do it for you via IUnknown::AddRef/Release
QObjectDispatcher* dispatcher = new QObjectDispatcher(someWidget);
// The code until this point is the only part which is new in your application, as
// far as dispatching the calls/properties is involved. From here, it's exactly the same;
// from the OLE MFC application's perspective we often only have an IUnknown:
IUnknown* pUnknown = dispatcher;
// Now get the interface and start playing with it:
CComQIPtr pDispatch(pUnknown);
// First we have to retrieve the index of the property we change
LPOLESTR name = "windowTitle";
DISPID dispId = -1;
pDispatch->GetIDsOfNames(IID_NULL, &name, 1, LOCALE_SYSTEM_DEFAULT, &dispId);
// Now that we have the index, we change the property
VARIANT varTitle = CComVariant("New window title");
DISPSPARAMS dispParams = { nullptr, nullptr, 0, 0 };
dispParams.rgvarg = &varTitle;
dispParams.cArgs = 1;
pDispatch->Invoke(dispId, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT,
&dispParams, nullptr, nullptr, nullptr);As you can see the API of IDispatch is not fun to work with and there's no reason to use this except of making that kind of stuff work in existing MFC/OLE-applications while porting. I strongly advise against using it in new code.
How does that work? Well, the IDispatch interface works in two steps. First, you have to request the ID of the method/property you want to access via GetIDsOfNames. You can request several IDs at the same time. QObjectDispatcher traverses the methods of the handled QObject via the corresponding QMetaObject. It then returns the index of the method. If there’s no method found, it checks for a property. If it’s found, it returns the index of the property plus the number of methods, to be able to recognize the type afterwards. If neither a method nor a property by that name is found, an error is reported.
Let’s have a short look at how GetIDsOfNames roughly works. That example code cannot be used directly but gives you a rough idea of the implementation:
HRESULT QObjectDispatcher::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, UINT cnames, LCID, DISPID* rgDispId)
{
// GetIdsOfNames supports requesting several at the same time
for (uint i = 0; i < cNames; ++i) {
// note you cannot use QMetaObject::indexOfMethod directly,
// since it expects the fully qualified name
auto indexOfMethod = []() { /* ... */ };
const int methodIndex = indexOfMethod(rgszNames[i]);
if (methodIndex != -1) {
rgDispId[i] = methodIndex;
continue;
}
}
// now do the same for the properties...
//
return S_OK;
}In the second step you can call Invoke to either invoke a method or to access a property. For this you need to pass a struct DISPSPARAMS which contains the arguments. rgvarg is an array to several arguments which are stored in reverse order. QObjectDispatcher will now pick the method or property (depending on the id passed and whether you passed DISPATCH_PROPERTYPUT, DISPATCH_PROPERTYGET, or DISPATCH_METHOD) and call it. For this it has to translate the arguments. For method calling, they need to be translated to QMetaMethodArgument or QMetaMethodReturnArgument for the return value. For properties, we have to go through QVariant to be able to read/write the property via Qt’s Meta Object System.
Let’s also have a look at how Invoke works internally. Even here, this code piece is not complete and won’t work directly:
HRESULT QObjectDispatcher::Invoke(DISPID dispIdMember, REFIID riid, LCID, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO*, UINT*)
{
if (wFlags == DISPATCH_METHOD) {
const QMetaMethod method = metaObject->method(dispIdMember);
// translate return argument
auto returnArgument = translateReturnArgument(method, pVarResult);
// then call the method, translating all the arguments
invokeMethod(method, m_object, returnArgument, translateArgument(0), ...);
return S_OK;
}
//
}So, in your application, which expects a CWnd generated by some factory, you do the following:
- Create a wrapper
CWndwhich you actually return to the caller. ThisCWndwill in some way provide access to anIDispatchinstance like it was doing before your port - Create a
QWinWidgetresiding inside of theCWnd. - Put your ported
QWidgetinto theQWinWidget - Create a
QObjectDispatcherworking on your portedQWidget - Make your
CWndreturn thisQObjectDispatcherasIDispatch
Now your application should threat your ported widget as it was never ported. Of course, you have to provide the same properties and methods as the system expects using Qt's Meta Object System (Q_PROPERTY, Q_INVOKABLE).
There are, of course, some limitations:
- You cannot overload names of methods as OLE doesn't allow that
- Optional parameters are not supported, as
QMetaMethoddoesn't reflect that (or I simply haven't figured out, who knows…) - You have to provide a type-mapping for the argument types. The example project linked below provides only
IUnknown*,int, andQStringas these are the most common ones and show how it works - The example solution is not thread-safe
Find the complete code as a little example project on KDAB's GitHub repository at: https://github.com/KDABLabs/blogs-qt/tree/main/MFC-Migration-OLE-Dispatch
The post OLE-Dispatch When Doing MFC/Qt Migration appeared first on KDAB.