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:
#ifndef RELEASE_TRACE
#ifdef _USE_RELEASE_TRACE
#define RELEASE_TRACE AtlTrace
#else
#define RELEASE_TRACE 1 ? (void)0 : AtlTrace
#endif
#endif
template <class T>
class CSelfDeleteAtlDlg
{
public:
CSelfDeleteAtlDlg() {m_szDbgClassName = _T("Unknown");}
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);
}
return lResult;
}
protected:
LPCTSTR m_szDbgClassName;
};
__declspec(selectany) UINT WM_ATLDESTROY_MODELESS = 0;
class CProcessWatcherThread
{
public:
CProcessWatcherThread()
{
m_hThread = NULL;
m_WaitHandles.hWatchProcess = NULL;
m_WaitHandles.hEventStopWatch = NULL;
m_pProcessWatcherThread = this;
}
~CProcessWatcherThread()
{
StopWatch();
m_pProcessWatcherThread = NULL;
}
virtual void OnProcessShutdown() = 0;
void StartWatch()
{
if (m_hThread)
return;
m_hOriginalHook = SetWindowsHookEx(WH_GETMESSAGE,
ProcessGetMessageHook, 0, GetCurrentThreadId());
m_WaitHandles.hEventStopWatch =
::CreateEvent(NULL, FALSE, FALSE, NULL);
m_WaitHandles.hWatchProcess =
::CreateEvent(NULL, FALSE, FALSE, NULL);
m_hThread = ::CreateThread(NULL, NULL,
StartThread, this, 0, &m_dwThreadID);
}
void StopWatch()
{
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)
{
RELEASE_TRACE("CProcessWatcherThread: Process "
"terminated.. calling OnProcessShutdown");
OnProcessShutdown();
}
else if (dwWait == WAIT_OBJECT_0 + 1)
{
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.