Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Use your own ATL-Objects in Dev-Studio Macros

0.00/5 (No votes)
17 Feb 2002 1  
Shows how to develop ATL-Objects to extend VB-Script. Uses ISharedPropertyGroupManager to keep data in memory. Supervises MSDEV-shutdown.

Sample Image - MakroExtensions.gif

Introduction

I'm a lazy (in the manner of trying to avoid too much unnecessary work) developer and I try to automate as much as possible (Using macros or other tools). Due to a bug somewhere in MSDEV, my MSDEV crashes if I'm running a macro which tries to open a non-existing file. (Documents.Open 'SomeFile'). Because I couldn't find a way to determine if a file is present or not, I wrote this little application. This project only runs under Win2000 or NT4.0 + option pack, because I'm using ISharedProperty (Can be replaced or removed).

Overview

This article shows a little bit of these techniques:

  • Interfaces needed for running in a macro
  • Deleting modeless ATL-dialog
  • Hooking into Dev-Studio to determine process shutdown
  • Storing data in memory from a DLL which is dynamically loaded/unloaded using ISharedProperty
  • Using CDialogResize for resizing controls

Implementation

The first point can be explained very easily, because there is nothing special about it. Just create an ATL-Object and additionally implement these interfaces:

  • IObjectSafety - Must be implemented, otherwise the Object can't be created.
  • ISupportErrorInfo - Helpful to display user-friendly error messages.

That's it. Now you can add your own interfaces, but always keep in mind that VB-Script only knows the VARIANT data type. For this in and out params must be of type VARIANT. The function:

const VARIANT* GetBSTRvt(VARIANT& vt) const;

shows how to resolve a VARIANT. The VARIANT can contain a value (e.g. bstrVal, long, etc.) or can keep a reference to another VARIANT or (in my opinion the worst type) can simply contain a IDispatch. In this case, the function behaves like VB does and simply calls the first method available for this object. (Hopefully this will return the expected type.)

For the second and third techniques listed above, I wrote a template class to be used within any other project. And here they are:

// RELEASE_TRACE 

#ifndef RELEASE_TRACE
    #ifdef _USE_RELEASE_TRACE
        #define RELEASE_TRACE AtlTrace
    #else
        #define RELEASE_TRACE     1 ? (void)0 : AtlTrace
    #endif
#endif



// Template Class to derive from, for deleting modeless dialogs 

// Behaves like delete this in MFC-Dialogs in PostNcDestroy 

template <class T>
class CSelfDeleteAtlDlg
{
public:
    CSelfDeleteAtlDlg() {m_szDbgClassName = _T("Unknown");}
    // Call this method to destroy a modeless ATL-Dialog

    BOOL DestroyWindowAndDeleteThis()
    {
        static_cast<T*>(this)->m_thunk.Init( DestroyWindowProc, 
                                          static_cast<T*>(this));
        if (0 == WM_ATLDESTROY_MODELESS) 
        {
            WM_ATLDESTROY_MODELESS = 
                RegisterWindowMessage( _T("DestroyModelessAtlWindow"));
        }
        static_cast<T*>(this)->PostMessage( WM_ATLDESTROY_MODELESS);
        return TRUE;
    }
private:
    static LRESULT CALLBACK DestroyWindowProc( HWND hWnd, 
                   UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
        LRESULT lResult = 0L;
        try
        {
            T* pThis = reinterpret_cast<T*>( hWnd);
            if (WM_ATLDESTROY_MODELESS == uMsg) 
            {
                ::DestroyWindow( pThis->m_hWnd);
                RELEASE_TRACE("Deleting Pointer(%s)  %08x", 
                            pThis->m_szDbgClassName, pThis);
                delete pThis;
            }
            else 
            {
                lResult = pThis->DialogProc( hWnd, uMsg, wParam, lParam);
            }
        }
        catch(...) 
        {
            ATLASSERT(false);    // Hopefully never happenes

        }
        return lResult;
    }
protected:
    LPCTSTR m_szDbgClassName;
};
__declspec(selectany) UINT  WM_ATLDESTROY_MODELESS = 0;

// This class installs a message hook to filter the WM_QUIT

// message. A derived class will be informed to do some clean up stuff

class CProcessWatcherThread
{
public:
    CProcessWatcherThread()
    {
        m_hThread                        = NULL;
        m_WaitHandles.hWatchProcess        = NULL;
        m_WaitHandles.hEventStopWatch    = NULL;
        m_pProcessWatcherThread            = this;
    }
    ~CProcessWatcherThread()
    {
        StopWatch();
        m_pProcessWatcherThread = NULL;
    }
    // This method is called if the process is shutting down

    virtual void OnProcessShutdown() = 0;
    void StartWatch()
    {
        if (m_hThread)    // allready running

            return;

        // Hook into applications main message loop

        m_hOriginalHook = SetWindowsHookEx(WH_GETMESSAGE, 
            ProcessGetMessageHook, 0, GetCurrentThreadId());
        
        // Create two events, one that

        // indicates normal exit (hEventStopWatch)

        // and another which indicated

        // Process is going down (hWatchProcess)

        m_WaitHandles.hEventStopWatch  = 
            ::CreateEvent(NULL, FALSE, FALSE, NULL);
        m_WaitHandles.hWatchProcess  = 
            ::CreateEvent(NULL, FALSE, FALSE, NULL);

        // Create a thread which is supervising the events

        m_hThread = ::CreateThread(NULL, NULL, 
                StartThread, this, 0, &m_dwThreadID);
    }
    void StopWatch()
    {
        // Signal normal exit and unhook

        StopWatch(m_WaitHandles.hEventStopWatch);
    }
private:
    void StopWatch(HANDLE hStop, bool bUnhook = true)
    {
        if (!m_WaitHandles.hEventStopWatch) return;
        ::SetEvent(hStop);
        ::WaitForSingleObject(m_hThread, 2000);

        CloseHandle(m_WaitHandles.hEventStopWatch);
        CloseHandle(m_hThread);
        CloseHandle(m_WaitHandles.hWatchProcess);
        m_WaitHandles.hEventStopWatch    = NULL;
        m_WaitHandles.hWatchProcess        = NULL;
        m_hThread                        = NULL;

        if (bUnhook)
        {
            UnhookWindowsHookEx(m_hOriginalHook);
            m_hOriginalHook = NULL;
        }
    }
    static DWORD WINAPI StartThread(LPVOID lpParameter)
    {
        CProcessWatcherThread* m_pProcessWatcherThread = 
                        (CProcessWatcherThread*) lpParameter;
        m_pProcessWatcherThread->Watch();
        return 0;
    };
    void Watch()
    {
        DWORD  dwWait = 
          ::WaitForMultipleObjects( sizeof(m_WaitHandles) / sizeof(HANDLE), 
          &m_WaitHandles.hWatchProcess , FALSE, INFINITE);
        if (dwWait == WAIT_OBJECT_0) // Process is terminated !!

        {        
            RELEASE_TRACE("CProcessWatcherThread: Process " 
                             "terminated.. calling OnProcessShutdown");
            OnProcessShutdown();
        }
        else if (dwWait == WAIT_OBJECT_0 + 1) // thread is terminated !!

        {        
            RELEASE_TRACE("CProcessWatcherThread:  Normal exit");
        }
        else 
        {        
                RELEASE_TRACE("CProcessWatcherThread: oops");
        }
    }
    
    static LRESULT CALLBACK ProcessGetMessageHook
            (int code, WPARAM wParam, LPARAM lParam)
        {
            MSG* pMsg = (MSG*) (lParam);
            HHOOK hookCall  = m_hOriginalHook;
            if (pMsg->message == WM_QUIT)
            {
                m_pProcessWatcherThread->StopWatch
                  (m_pProcessWatcherThread->m_WaitHandles.hWatchProcess, 
                  false);

                LRESULT lres =  CallNextHookEx(hookCall, 
                                 code, wParam, lParam);
                UnhookWindowsHookEx(m_hOriginalHook);
                m_hOriginalHook = NULL;
                return lres;
            }
            return CallNextHookEx(hookCall, code, wParam, lParam);
        }    

    DWORD                            m_dwThreadID;
    HANDLE                            m_hThread;
    static    HHOOK                    m_hOriginalHook;
    static    CProcessWatcherThread*    m_pProcessWatcherThread;

#pragma pack( push, 1 )
    struct
    {
        HANDLE    hWatchProcess;
        HANDLE    hEventStopWatch;
    } m_WaitHandles;
#pragma pack( pop )
};

__declspec(selectany) HHOOK CProcessWatcherThread::m_hOriginalHook = NULL;
__declspec(selectany) CProcessWatcherThread* 
  CProcessWatcherThread::m_pProcessWatcherThread = NULL;

The macro RELEASE_TRACE can be useful to debug the application. With a tool like "DbgView" from SysInternals, you can read the strings send to the debug-port without starting the debugger. The only way (I know) to debug a macro running a COM-Object is to attach a new debugger to the running MSDEV.exe process. (You can do this using the task-manager). This costs a lot of time and stopping the debugging causes the MSDEV.exe to close.

CSelfDeleteAtlDlg: Derive your ATL-Dialog from CSelfDeleteAtlDlg and call DestroyWindowAndDeleteThis() in OnOk() or OnCancel() or any other method which should close and delete the dialog.

CProcessWatcherThread: Derive your class from CProcessWatcherThread and call StartWatch() from a thread belonging to the process you want to supervise. If the process is shutting down (must be a process having a GUI), the method OnProcessShutdown() is called. You can do your clean-up work here. If you call StopWatch(), the process isn't supervised anymore.

The sample code contains the following macro-extensions:

[id(1)         ] HRESULT IsFilePresent([in] VARIANT bsFileName, 
                            [out, retval] VARIANT* pIsPresent);
[propget, id(2)] HRESULT TextClipboard([out, retval] VARIANT *pVal);
[propput, id(2)] HRESULT TextClipboard([in] VARIANT newVal);
[id(3)         ] HRESULT Append2TextClipboard([in] VARIANT vtAppend, 
                            [optional] VARIANT vtInsertCRLF);
[id(4)         ] HRESULT Copy2Clipboard([in] VARIANT vtCopyText);

IsFilePresent returns VARIANT_TRUE if a file is present, otherwise VARIANT_FALSE.

Copy2Clipboard just copies text to the Windows-Clipboard.

TextClipboard and Append2TextClipboard show a simple text-only-clipboard to be used within a dev-studio instance. Text can be copied or appended to this "cliptext". The text stored in the "cliptext" and the window-handle to the cliptext-window are stored using the ISharedPropertyGroupManager. Our DLL may be reloaded several times, so we can't store the data using a simple pointer. The ISharedPropertyGroupManager lives as long as our process does, so it is handy to it for storing "named" data.

I entered some macros and assigned keystrokes like shift+ctrl+C to the CopyEx macro. Below you can see how to invoke the functions

Sub CopyEx()
    Set ExtFuncs = CreateObject("MakroExtensions.ExtFuncs")
    ExtFuncs.TextClipboard = ActiveDocument.Selection

End Sub
Sub PasteEx()
    Set ExtFuncs = CreateObject("MakroExtensions.ExtFuncs")
    ActiveDocument.Selection = ExtFuncs.TextClipboard
End Sub
Sub AppendCopy()
    Set ExtFuncs = CreateObject("MakroExtensions.ExtFuncs")
    ExtFuncs.Append2TextClipboard ActiveDocument.Selection
End Sub
Sub CopyFullFileName()
    Set ExtFuncs = CreateObject("MakroExtensions.ExtFuncs")
    ExtFuncs.Copy2Clipboard ActiveDocument.FullName
End Sub

Maybe this framework helps you to develop some more macro-extensions. For further information, have a look at the source-code, maybe I added a comment here and there.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here