INTERACT FORUM

Please login or register.

Login with username, password and session length
Advanced search  
Pages: [1]   Go Down

Author Topic: VS2008 C++ Interface how-to  (Read 4932 times)

stottle

  • Junior Woodchuck
  • **
  • Posts: 71
VS2008 C++ Interface how-to
« on: August 26, 2011, 06:34:16 pm »

I'm making progress on getting this working in VS2008, based on the instructions from 2004 that are stickied.  Thought I'd try to document the changes I've had to make.  Still a work in progress, so feel free to post feedback.

Unfortunately, since the VS 2008 wizard generates the unique GUIDs for you, and knows how to register everything for COM, you really need to start with a new project.  There isn't an easy way to provide a template.

I've been working with (what I think) is a cool trick.  Nokia's Qt framework adds a lot of UI capability in.  I like the API's a lot.  And I find it a lot easier to use than ATL.  So my process lets ATL do the heavy COM lifting, and I make a separate Qt project using ActiveQt for the actual UI.  The nice thing in this case is that J River really provides only two things, the automation object (which lets you query and manipulate the db/playlists/etc) and the events to tell you the current state.  I can pass both of those into Qt, and not have to worry about ATL within my UI project.

So, without further ado, here are the steps to create a MC Interface plugin:
Step 1: Create a new ATL Project
Step 2: Add a composite control to the project
Step 3(Optional): Add the IMJAutomationEvents interface
Step 4: Update the dll registration to let MC know about the plugin
Step 5: Add code for the interfaces and registration

Step 1: Create a new ATL Project
File->New->Project
In the dialog, select ATL from the Visual C++ tree, then enter a name for the project.  The defaults are fine for the project (if you want in-process, which is all I've tested so far), so you can hit Finish.  For the rest of the description, I'm using JrmcInterfacePlugin as the name of the project.

Step 2: Add a composite control to the project
Go to the Class View tab instead of the Solution View tab.  This will list two projects, JrmcInterfacePlugin and JrmcInterfacePluginPS.  You can ignore the PS project.  Right click on the JrmcInterfacePlugin and select add->Class from the menu.  This will pull up a new dialog.  Select Visual C++->ATL from the tree on the left, then ATL Control from the list of templates on the right.  Then click the Add button to continue.  On the next dialog, enter a short name for the control (the rest is filled in automatically).  I've chosen JrmcPluginCtrl.  Click Next, then select a Composite Control from the next screen, and then Finish.

This gives you an empty control, to fill in later.  But it is the control that needs to implement the Init interface to work with J River, so I'm letting ATL handle that, as well as the event hooks of the next step.

Once the control is added, you will have two new items in the project's tree in Class View.  CJrmcPluginCtrl and IJrmcPluginCtrl.  One is the class itself, the other is the interface exposed to COM.

In order for the plugin to play nice with MC, it needs to implement the Init method.  So right click on the IJrmcPluginCtrl item in the Class View, and select Add->Method.  In the new dialog, select Method Name: Init, check the "in" checkbox, and select "IDispatch *" as the Parameter type.  I also set the Parameter Name: field to "pMJ".  Then hit the Add button to the right, and then Finish from the Bottom.

I'll get to the code modifications in the class in a bit.

Step 3: Add the IMJAutomationEvents interface.
This is optional, you don't need these events (playback status, volume changes, etc) for every plugin.

To set up to receive these events from J River, go to Class View, and right mouse on CJrmcCtrl (Not IJrmcCtrl, which was used in the last step).  Then select Add->Implement Interface....  This will bring up a new dialog.  From the Available type libraries: pulldown, select "Mediacenter <1.0>".  Then, from the interfaces list, select "IMJAutomationEvents" and add it the the implement interfaces list on the right.  Then click finish.

So what have we got now?  We have a ton of automatically generated code and templates, and we've written no code ourselves.  We'll get to that next.
Logged

stottle

  • Junior Woodchuck
  • **
  • Posts: 71
Re: VS2008 C++ Interface how-to
« Reply #1 on: August 26, 2011, 06:34:36 pm »

Step 4: Update the dll registration to let MC know about the plugin
We have two classes in our project.  The JrmcInterfacePlugin, which is the COM server, and the JrmcPluginCtrl, which is the COM Control/UI.

We don't have to do much with the JrmcInterfacePlugin class, as ATL has done the bulk of the work for us.  What we do have to implement is the additional code to tell J River about our plugin.  So now that we are ready to modify some code, lets jump back to solution explorer and open JrmcInterfacePlugin.cpp.

You will already have a function for registering the COM server:
Code: [Select]
// DllRegisterServer - Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
    // registers object, typelib and all interfaces in typelib
    HRESULT hr = _AtlModule.DllRegisterServer();
return hr;
}

Modify it to look something like this:
Code: [Select]
STDAPI DllRegisterServer(void)
{
//FIX if not MC 16
static const TCHAR*   REGISTRY_PATH_MJ_PLUGINS_INTERFACE = _T("Software\\JRiver\\Media Center 16\\Plugins\\Interface\\");

TCHAR   cRegistryPath[1024];
HKEY   hk;
DWORD   dwDisp;
//FIX plugin name
wsprintf( cRegistryPath, _T("%s%s"), REGISTRY_PATH_MJ_PLUGINS_INTERFACE, _T("<PLUGIN NAME>") );

if ( RegCreateKeyEx( HKEY_LOCAL_MACHINE, cRegistryPath,
                  0, NULL, REG_OPTION_NON_VOLATILE,
                  KEY_WRITE, NULL, &hk, &dwDisp ) == ERROR_SUCCESS )
{
//FIX all 6 fields
DWORD   nIVersion    = 1;
DWORD   nPluginMode  = 1;
TCHAR*  strCompany   = _T( "My Company" );
TCHAR*  strVersion   = _T( "1.0.0" );
TCHAR*  strURL       = _T( "http://www.<MyWebsite>.com" );
TCHAR*  strCopyright = _T( "Copyright (c) 2011, <ME>" );
LPWSTR  strID;

//FIX CLSID
StringFromCLSID( CLSID_JrmcPluginCtrl, &strID );

RegSetValueExW( hk, _T("IVersion"),   0, REG_DWORD, (LPBYTE)&nIVersion,   sizeof(DWORD) );
RegSetValueExW( hk, _T("PluginMode"), 0, REG_DWORD, (LPBYTE)&nPluginMode, sizeof(DWORD) );

RegSetValueExW( hk, _T("CLSID"),     0, REG_SZ, (LPBYTE)strID,        _tcslen(strID)*sizeof(TCHAR) );
RegSetValueExW( hk, _T("Company"),   0, REG_SZ, (LPBYTE)strCompany,   _tcslen(strCompany)*sizeof(TCHAR) );
RegSetValueExW( hk, _T("Version"),   0, REG_SZ, (LPBYTE)strVersion,   _tcslen(strVersion)*sizeof(TCHAR) );
RegSetValueExW( hk, _T("URL"),       0, REG_SZ, (LPBYTE)strURL,       _tcslen(strURL)*sizeof(TCHAR) );
RegSetValueExW( hk, _T("Copyright"), 0, REG_SZ, (LPBYTE)strCopyright, _tcslen(strCopyright)*sizeof(TCHAR) );
}  
    // registers object, typelib and all interfaces in typelib
    HRESULT hr = _AtlModule.DllRegisterServer();
return hr;
}
Note, there are several places with //FIX comments.  Those should be changed to match yourself/your plugin.  The CLSID will be automatically provided by ATL, but the name will be based on the name of the control you added in Step 2.

At this point, your plugin should compile.  Note, I need to build the Release version, otherwise I get an exception.  If you've opened up Visual Studio (on Vista/Win7) as an administrator, it will register the server when you build.  Otherwise it will fail the registration, and you will need to register manually.  Of course we aren't done yet, but this is a nice sanity check.

Step 5: Add code for the interfaces and events
The rest of the work is in the JrmcPluginCtrl files (.h and .cpp).  First off, since we will be using the J River automation pointer (otherwise, why are we writing a plugin?), we need to tell C++ about the classes.  Add this code somewhere near the top of your header file, modified to point to your J River installation directory if it is different:
Code: [Select]
#import "C:\Program Files\J River\Media Center 16\Media Center 16.tlb" no_namespace, named_guids \
rename("DeleteFile","MCDeleteFile") \
rename("MoveFile","MCMoveFile") \
rename("ReportEvent","MCReportEvent")

Next (only if you added the optional event handling) you need to change the event template.  The original code should look like this:
Code: [Select]
public IDispatchImpl<IMJAutomationEvents, &__uuidof(IMJAutomationEvents), &LIBID_MediaCenter, /* wMajor = */ 1>
you need to change it to this:
Code: [Select]
public IDispEventImpl<1,CJrmcPluginCtrl,&DIID_IMJAutomationEvents,&LIBID_MediaCenter, 1, 0>
This will be part of the class declaration for the control in your header file.  Change CJrmcPluginCtrl to match the name you gave the control.

Next, in the COM_MAP section of the header file, you need to change:
Code: [Select]
COM_INTERFACE_ENTRY2(IDispatch, IMJAutomationEvents)to
Code: [Select]
              COM_INTERFACE_ENTRY(IDispatch)and change
Code: [Select]
COM_INTERFACE_ENTRY(IMJAutomationEvents)to
Code: [Select]
//COM_INTERFACE_ENTRY(IMJAutomationEvents)as we will handle adding the event processing in the Init function.

Now find
Code: [Select]
BEGIN_SINK_MAP(CJrmcPliuginCtrl)
//Make sure the Event Handlers have __stdcall calling convention
END_SINK_MAP()
and add
Code: [Select]
SINK_ENTRY_EX(1, DIID_IMJAutomationEvents, 1, MJEvent)
between the BEGIN/END sections

The last change to the header file is to find the line:
Code: [Select]
STDMETHOD(Init)(IDispatch* pMJ);and add
Code: [Select]
STDMETHOD_(void, MJEvent)(LPCTSTR strType, LPCTSTR strParam1, LPCTSTR strParam2);

protected:
IMJAutomationPtr m_pMJ;
CComDispatchDriver spDrv;
after it.  We use these variables later in the .cpp file.  Note that we are using STDMETHOD_, not STDMETHOD (the underscore is intentional).

Now, jump into the JrmcPluginCtrl.cpp file.  Given that I want to let Qt do the UI work, there is actually very little to do in the .cpp file.  We just need to implement to two methods, Init and MJEvent.  Depending on what you want to do, this would be where you would make your custom changes.
Here's what I do for Init:
Code: [Select]
STDMETHODIMP CJrmcPluginCtrl::Init(IDispatch* pMJ)
{
m_pMJ.Attach((IMJAutomation *) pMJ, TRUE);
HRESULT hr2 = DispEventAdvise(m_pMJ, &DIID_IMJAutomationEvents);
CAxWindow wnd;
wnd.Attach(m_hWnd);
HRESULT hr = wnd.CreateControl(OLESTR("{<Qt Control's GUID}"));
if (FAILED(hr))
           AtlThrow(hr);
CComPtr<IDispatch> spDisp;
hr = wnd.QueryControl(&spDisp);
if (FAILED(hr))
           AtlThrow(hr);
spDrv = CComDispatchDriver(spDisp);
CComVariant vIn2(pMJ);
hr = spDrv.Invoke1(L"setAuto",&vIn2);
if (FAILED(hr))
           AtlThrow(hr);
return S_OK;
}
I need to attach the automation object in order to get the events from J River.  I'm sure I could do this in Qt, since I also pass the automation pointer to Qt when I invoke the "setAuto" call.  But since I want to leave ATL out of Qt, this seemed the easier.  Since it is the responsibility of the server (in this case J River) to delete the pointed to object, I don't think using the pointer twice is a problem (once here and then from Qt).  I independently create and register the Qt control, so all I need to get it to show up in J River is add the control's GUID to the CreateControl method.  I don't need to generate any UI from Visual Studio, nor do I need to worry about messages - Qt will handle all of that.  I just need to make sure the Qt control has a setAuto slot to take the m_pMJ pointer (and a slot to take the events, named "eventHandler", see below).  spDrv is one of the variables I added to the class, it is used to pass events to Qt.

Here's the method that does that:
Code: [Select]
STDMETHODIMP_(void) CJrmcPliuginCtrl::MJEvent(LPCTSTR strType, LPCTSTR strParam1, LPCTSTR strParam2)
{
CComVariant vParams[3] = {strParam2, strParam1, strType };
HRESULT hr = spDrv.InvokeN(L"eventHandler",vParams,3);
if (FAILED(hr))
         AtlThrow(hr);
}

That's it.  Everything else is handled by the Qt control.  That will be the subject of a future post.

Of course, you could implement the control in ATL if you want.
Logged

stottle

  • Junior Woodchuck
  • **
  • Posts: 71
Re: VS2008 C++ Interface how-to
« Reply #2 on: August 27, 2011, 10:14:00 am »

Now for the Qt control side of this.

Obviously you need Qt for this.  I've installed the latest Qt SDK.

That gives you a lot of non-COM stuff, plus a few activeqt examples.  The example I will run through here will be to modify a Qt application example and convert it to run as a COM object within J River.  How about asteroids?

If you installed the SDK in the standard location, you will find asteroids in the C:\QtSDK\Examples\4.7\graphicsview\portedasteroids directory.  Since we are modifying the code, I suggest copying the whole directory for portedasteroids to a new location before you make changes. 

From the new location, open the portedasteroids.pro file to edit the project in QtCreator.  I chose Desktop, disabled shadow builds and selected debug/release builds for VS2008 (turning off mingw builds). 

You will find more documentation on the Qt help pages, particularly here, but I walk you through the specific changes to get asteroids to build, get registered and play within J River.

First off, you need to modify the .pro file to set the project up as a COM control instead of a standard application.  Within the .pro file, change
Code: [Select]
TEMPLATE = app
INCLUDEPATH += .
to
Code: [Select]
TEMPLATE = lib
CONFIG += qaxserver dll

DEF_FILE = qaxserver.def
RC_FILE = qaxserver.rc
INCLUDEPATH += .
This requires two new files, qaxserver.rc and qaxserver.def.  These can be found in the Qt SDK directory, or you can just paste the following text into your editor and save as qaxserver.rc:
Code: [Select]
1 TYPELIB "qaxserver.rc"and qaxserver.def:
Code: [Select]
; mfc_test.def : Declares the module parameters.

EXPORTS
        DllCanUnloadNow      PRIVATE
        DllGetClassObject    PRIVATE
        DllRegisterServer    PRIVATE
        DllUnregisterServer  PRIVATE
        DumpIDL              PRIVATE

Now what we started with was a full application, not a QWidget.  We want a specific QWidget to use as our control (which can include layouts and other widgets).  But we need to give our Plugin generated above the GUID of a single control, which means one object.  Looking at the main.cpp file from the project, you can see that the primary UI is the class KAstTopLevel.

The  code for KAstTopLevel is in toplevel.h and toplevel.cpp, so those are the files we need to edit.  In toplevel.h, we need to add in QAxBindable and the J River COM (the later isn't used in this example).
Code: [Select]
#include <QAxBindable>

#import "C:\Program Files\J River\Media Center 16\Media Center 16.tlb" no_namespace, named_guids \
rename("DeleteFile","MCDeleteFile") \
rename("MoveFile","MCMoveFile") \
rename("ReportEvent","MCReportEvent")
Next, make sure the class derives from QAxBindable by changing the class declaration to:
Code: [Select]
class KAstTopLevel : public QMainWindow, public QAxBindableI also add the following line after the Q_OBJECT macro.  This keeps a bunch of QWidget properties from being exported to COM.
Code: [Select]
Q_CLASSINFO("ToSuperClass", "KAstTopLevel")In order to support the COM Server/Composite control we generated in ATL, we need to implement a couple of functions.  Declare them in the header file as:
Code: [Select]
public slots:
    void setAuto(IDispatch *obj);
    void eventHandler(const QString &type,
                      const QString &param1,
                      const QString &param2);
Also make sure you add
Code: [Select]
IMJAutomationPtr m_pMJ;as a private member of the class, assuming you want to use it at some point down the road.

Now we jump to toplevel.cpp.  The first thing we need to do here is add an include for QAxFactory.  This defines macros that let activeqt generate all of the binding code for us.
Code: [Select]
#include <QAxFactory>Then we define the two methods we need for J River:
Code: [Select]
void KAstTopLevel::setAuto( IDispatch *obj )
{
    m_pMJ.Attach((IMJAutomation *) obj, TRUE);
}

void KAstTopLevel::eventHandler(const QString &type,
                            const QString &param1,
                            const QString &param2)
{
    return;
}
Since asteroids doesn't need anything from J River, these are just stubbed out.

Finally, we need to put in the activeqt macros for COM.  This will require 5 new GUIDs to be generated.  I just used Tools->Create GUID from within VS2008, and pasted the (registry format) values into this macro.  Replace the GUIDs below with the ones you generated.
Code: [Select]
QAXFACTORY_DEFAULT(KAstTopLevel,
           "{DCA6FA8F-27B7-4fa6-8B3C-C66A4295C104}",
           "{2640B974-DCA8-47b9-96D9-47847974A9A5}",
           "{2EB6A58C-B176-4095-82F8-965F4871DF04}",
           "{D5CB52F1-A5A0-46eb-8848-8A20125C8B2B}",
           "{D3798C7A-46A9-467b-80C3-CD586842F188}")

Compile your dll.  You may need to open an Administrator command prompt and navigate to this directory, then run the following command line to register the COM control:
Code: [Select]
> regsvr32 portedasteroids.dll
Finally, the only change you need to make to your ATL plugin, is to add your new control's GUID to it.  The GUID we care about is the first one of the QAXFACTORY_DEFAULT macro (in the example above it is "{DCA6FA8F-27B7-4fa6-8B3C-C66A4295C104}").  If you use this value in your ATL plugins Init method (the CreateControl function), your new control will be displayed when you go to your plugin in J River.

Here's a screenshot


Uploaded with ImageShack.us

One nice thing for testing is that the ATL code can be left almost completely alone.  Generate and register any Qt controls you want as described above, and all you have to do is change one line (the CreateControls line in Init) of the ATL code, rebuild, and you will have your new UI.

Enjoy!
Logged

Mr ChriZ

  • Citizen of the Universe
  • *****
  • Posts: 4375
  • :-D
Re: VS2008 C++ Interface how-to
« Reply #3 on: August 29, 2011, 05:53:38 pm »

Good Post!  :)

danrien

  • Galactic Citizen
  • ****
  • Posts: 368
  • Chillin
Re: VS2008 C++ Interface how-to
« Reply #4 on: December 16, 2011, 12:46:48 pm »

This is pretty cool!  I'll have to try this out soon, so I can start doing my damage in unmanaged code instead .... :D
Logged
http://davidvedvick.info

"Always be yourself. Unless you can be Batman. Always be Batman." - Anonymous

mmote

  • Recent member
  • *
  • Posts: 40
Re: VS2008 C++ Interface how-to
« Reply #5 on: December 20, 2011, 08:03:36 am »

Note, I need to build the Release version, otherwise I get an exception.
You can get rid of that by adding
Code: [Select]
_ATL_NO_DEBUG_CRT to your preprocessor definitions and
Code: [Select]
#define ATLASSERT(x) to your stdafx.h
Logged
Pages: [1]   Go Up