Skip Navigation Links

Tips for Win32, MFC, Visual C++, COM, and JavaScript Developers

The tips on this web site were written by Eric Bergman-Terrell. They cannot be republished without the author's permission.

© Copyright 2004 Eric Bergman-Terrell
All Rights Reserved

These tips apply to Visual C++ version 6 and Windows 95/98, and NT 4 or later (except where noted).

Eric Bergman-Terrell & Levi go to Fall COMDEX '98

  1. General
    1. Determine the directory containing the program
    2. Determine if a screen saver is active
    3. Change the background and text color of individual command buttons
    4. Why does my program, which uses timers, crash when the user shuts down NT?
    5. Setting focus to controls in a CDialog-derivative's OnInitDialog function
  2. Listboxes
    1. Correct horizontal listbox scrolling
    2. CListBox::ItemFromPoint does not work in NT
    3. Why is my fixed-height owner drawn listbox's MeasureItem never called?
  3. Interacting with the Windows Shell
    1. Allow the user to specify a folder
    2. Determine the path of special folders
    3. Create shortcuts (on the Desktop, in the Start Menu, etc.)
  4. Internationalization
    1. Use CString::FormatMessage to allow filled-in items to be reordered
  5. MAPI (Messaging API) and TAPI (Telephony API)
    1. Use MAPI to send e-mail
    2. Use TAPI to place a voice call
  6. JavaScript
    1. Scale an image in a browser window while maintaining the proper aspect ratio
  7. Active Template Library (COM)
    1. Make connection points actually work in VBScript and JavaScript


Determine the directory containing the program

Use the following function to determine the directory path containing the running program:

CString CFileUtils::GetExeDir()
{
	char szPath[1024];
	GetModuleFileName(AfxGetApp()->m_hInstance, szPath, sizeof(szPath));
	
	char *ptr = strrchr(szPath, '\\');
	
	if (ptr)
	{
		ptr[1] = '\0';
	}
	
	ASSERT(strlen(szPath) < sizeof(szPath));
	
	return szPath;
}

Determine if a screen saver is active

You can use the following technique to determine if a screen saver is active:

if (FindWindow("WindowsScreenSaverClass", NULL) != NULL)
{
	AfxMessageBox("Found a Screen Saver");
}

If a screen saver was written using the SCRNSAVE.LIB library it will have the "WindowsScreenSaverClass" window class. See "About Screen Savers" in the Visual C++ on-line help for further information.


Change the background and text color of individual command buttons

One cannot change the background and text color of individual command buttons in Windows 95. One work-around is to simulate the command buttons with static controls. Then when the mouse down event (OnLButtonDown) happens, if the mouse is over a static control, simulate the command message. In other words, post a WM_COMMAND message and specify the control ID of the static control that was clicked with the mouse. To make a static control look like a button, specify the WS_EX_DLGMODALFRAME style bit.

Use the OnCtrlColor response function to set the background and text colors for the static controls.

One drawback to this approach is that static controls don't retain focus when they are pressed with the mouse. An alternate solution is to use owner-drawn buttons. In this case you will be responsible for drawing everything on the button, and focus will be retained properly. Just remember to draw the focus rectangle when necessary.


Why does my program, which uses timers, crash when the user shuts down NT?

When user logs off or shuts down NT, the WM_ENDSESSION message is sent with a flag indicating that the system is going down. But no WM_DESTROY messages are sent, so timers keep running until they (potentially) crash the program.

The following code may solve this problem:

void CMainFrame::OnEndSession(BOOL bEnding) 

// Need to bail if system is going down or user is logging off.  Otherwise timers will
// keep ticking and cause a crash.
{
	if (bEnding)
	{
		ExitProcess(0);
	}
	else
	{
		CFrameWnd::OnEndSession(bEnding);
	}
}

Setting focus to controls in a CDialog-derivative's OnInitDialog function

Usually all that's required to set focus to a particular control when a dialog is first launched is to 1) Set focus to the control in the OnInitDialog function, and 2) return FALSE from the OnInitDialog function. In cases where this doesn't work, use one of the following techniques:

1: Use the CDialog::GotoDlgCtrl function to set focus to the control.

2: PostMessage(WM_NEXTDLGCTL, (WPARAM) GetDlgItem({control ID})->GetSafeHwnd(), (LPARAM) TRUE);


Correct horizontal listbox scrolling

When you create an ordinary (non owner-drawn) listbox and fill it with items, even when you specify the WS_HSCROLL style the horizontal scrollbar will not appear even when some items are wider than the listbox.

The solution is to calculate the widest item, and then call CListBox::SetHorizontalExtent with that value.

The following function does the trick. Make sure you specify the WS_HSCROLL style and call the function after you've filled the listbox:

void CListBoxUtils::SetHorizontalExtent(CListBox& ListBox)
{
	int nMaxTextWidth = 0;
	
	CDC *pDC = ListBox.GetDC();
	
	if (pDC)
	{
		CFont *pOldFont = pDC->SelectObject(ListBox.GetFont());
		
		CString Text;
		const int nItems = ListBox.GetCount();

		for (int i = 0; i < nItems; i++)
		{
			ListBox.GetText(i, Text);

			Text += "X";  // otherwise item may be clipped.
			
			const int nTextWidth = pDC->GetTextExtent(Text).cx;

			if (nTextWidth > nMaxTextWidth)
			{
				nMaxTextWidth = nTextWidth;
			}
		}

		pDC->SelectObject(pOldFont);

		VERIFY(ListBox.ReleaseDC(pDC) != 0);
	}
	else
	{
		ASSERT(FALSE);
	}

	ListBox.SetHorizontalExtent(nMaxTextWidth);
}

CListBox::ItemFromPoint does not work in NT

Instead of CListBox::ItemFromPoint, use the following:

UINT COutlineListBox::ItemFromPoint2(CPoint pt, BOOL& bOutside) const

// CListBox::ItemFromPoint does not work on NT.

{
	int nFirstIndex, nLastIndex;
	GetFirstAndLastIndex(nFirstIndex, nLastIndex);
	
	bOutside = TRUE;
	
	CRect Rect;
	int nResult = -1;
	
	for (int i = nFirstIndex; nResult == -1 && i <= nLastIndex; i++)
	{
		if (GetItemRect(i, &Rect) != LB_ERR)
		{
			if (Rect.PtInRect(pt))
			{
				nResult  = i;
				bOutside = FALSE;
			}
		}
		else
		{
			ASSERT(FALSE);
		}
	}
	
	return nResult;
}

Why is my fixed-height owner drawn listbox's MeasureItem never called?

The MeasureItem function for a fixed-height owner drawn is only called once. The problem is that it is called before the MFC CListBox object is associated with the Windows listbox control.

The solution is to invoke the listbox's MeasureItem from the OnMeasureItem function of the dialog containing the listbox:

void CExampleDlg::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct)

{
	CDialog::OnMeasureItem(nIDCtl, lpMeasureItemStruct);
	
	if (nIDCtl == IDC_LISTBOX)
		m_ListBox.MeasureItem(lpMeasureItemStruct);
}

Allow the user to specify a folder

Call SelectFolder with a prompt. It returns the directory name if one was selected. Otherwise it returns an empty string:

const CString FolderName = SelectFolder("Please select directory");

const CString SelectFolder(const CString& strMessage)

{
	CString Result;
	
	BROWSEINFO BrowseInfo;
	memset(&BrowseInfo, 0, sizeof(BrowseInfo));
	
	char szBuffer[MAX_PATH];
	
	BrowseInfo.hwndOwner      = m_hWnd;
	BrowseInfo.pszDisplayName = szBuffer;
	BrowseInfo.lpszTitle      = strMessage;
	BrowseInfo.ulFlags        = BIF_RETURNONLYFSDIRS;
	
	// Throw display dialog
	LPITEMIDLIST pList = SHBrowseForFolder(&BrowseInfo);
	ASSERT(strlen(szBuffer) < sizeof(szBuffer));
	
	if (pList != NULL)
	{
		// Convert from MIDLISt to real string path
		SHGetPathFromIDList(pList, szBuffer);
		Result = szBuffer;
		
		// Global pointer to the shell's IMalloc interface.  
		LPMALLOC pMalloc; 
		
		// Get the shell's allocator and free the PIDL returned by
		// SHBrowseForFolder.
		if (SUCCEEDED(SHGetMalloc(&pMalloc))) 
			pMalloc->Free(pList);
	}
	
	return Result;
}

The SHBrowseForFolder and SHGetPathFromIDList APIs do all the work. They are available in Windows 95 and Windows 98 and NT 4.0.

SHBrowseForFolder returns a pointer to an item identifier list. This list specifies the location of the selected folder relative to the root of the name space. Then SHGetPathFromIDList takes the item identifier list and returns the corresponding file system path.

The code that calls SHBrowseForFolder must free the item identifier list (hence the call to pMalloc->Free).


Determine the path of special folders

Use the following code to determine the path of special folders (e.g. the Desktop folder, the Start Menu folder, etc). Read the on-line help topics entitled "CSIDL Values" to determine the nFolder argument value that applies to the folder of interest. For example, call CShellUtils::GetSpecialFolderLocation with an argument of CSIDL_DESKTOP will return the Windows Desktop folder path.

...
#include <objbase.h>
#include <shlguid.h>
...

CString CShellUtils::GetSpecialFolderLocation(int nFolder)
{
	CString Result;
	
	LPITEMIDLIST pidl;
	HRESULT hr = SHGetSpecialFolderLocation(NULL, nFolder, &pidl);
	
	if (SUCCEEDED(hr))
	{
		// Convert the item ID list's binary
		// representation into a file system path
		char szPath[_MAX_PATH];

		if (SHGetPathFromIDList(pidl, szPath))
		{
			Result = szPath;
		}
		else
		{
			ASSERT(FALSE);
		}
	}
	else
	{
		ASSERT(FALSE);
	}
	
	return Result;
}

Create shortcuts (on the Desktop, in the Start Menu, etc.)

Use the following code to create shortcuts. The ExePath argument should be the full path of an executable program file, e.g. "e:\Vault\Vault.exe". The LinkFilename should not be a full path, e.g. "Vault.lnk". The "Description" is free-form text (I don't know how to display this text after the shortcut is created. If you do, please let me know.

Read the on-line help topics entitled "CSIDL Values" to determine the nFolder argument value corresponding to the kind of shortcut that you want to create. For example, use an nFolder argument of CSIDL_DESKTOP to create a Desktop shortcut.

Note: Be sure to call CoInitialize before calling CShellUtils::CreateShortcut, and be sure to call CoUninitialize after calling CoInitialize. You can call CoInitialize(NULL); in your CWinApp-derivative's constructor and call CoUninitialize(); in its destructor.

...
#include <objbase.h>
#include <shlguid.h>
...

void CShellUtils::CreateShortcut(const CString& ExePath, const CString& LinkFilename, const CString& WorkingDirectory, const CString& Description, int nFolder)
{
	// Must have called CoInitalize before this function is called!

    IShellLink* psl; 
	const CString PathLink = GetSpecialFolderLocation(nFolder) + "\\" + LinkFilename;
	
    // Get a pointer to the IShellLink interface. 
    HRESULT hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, 
									IID_IShellLink, (PVOID *) &psl); 
	
    if (SUCCEEDED(hres)) 
	{ 
        IPersistFile* ppf; 
		
        // Set the path to the shortcut target and add the 
        // description. 
        psl->SetPath((LPCSTR) ExePath); 
		psl->SetWorkingDirectory((LPCSTR) WorkingDirectory);
        psl->SetDescription((LPCSTR) Description); 
		
		// Query IShellLink for the IPersistFile interface for saving the 
		// shortcut in persistent storage. 
        hres = psl->QueryInterface(IID_IPersistFile, (PVOID *) &ppf); 
		
        if (SUCCEEDED(hres)) 
		{ 
            WORD wsz[MAX_PATH]; 
			
            // Ensure that the string is ANSI. 
            MultiByteToWideChar(CP_ACP, 0, (LPCSTR) PathLink, -1, wsz, MAX_PATH); 
			
            // Save the link by calling IPersistFile::Save. 
            hres = ppf->Save(wsz, TRUE); 
            ppf->Release(); 
        } 
        psl->Release(); 
    } 
}

Use CString::FormatMessage to allow filled-in items to be reordered

CString::FormatMessage works like CString::Format (or wsprintf), except that the order of items being filled in can be changed by changing the format string in the resource file. For example, when the following code is run:

void CFormatMessageDlg::OnButton1() 
{
	CString Msg;
	Msg.FormatMessage(IDS_STR_1, (LPCSTR) "one",
								 (char)   '2', 
								 (LPCSTR) "three", 
								 (LPCSTR) "four", 
								 (int)    555);
	
	AfxMessageBox(Msg);
}

/* In the resource file: */
IDS_STR_1	"1st: %5!d! 2nd: %4 3rd: %3 4th: %2!c! 5th: %1"

This Message Box is displayed:

FormatMessage

Use MAPI to send e-mail

It's easy to use MAPI (the Windows Messaging API) to programmatically send an e-mail message.

To send an e-mail, just #include <mapi.h> and call MAPISendMail.

For example, when the following code is executed:

...
#include <mapi.h>
...

typedef ULONG (FAR PASCAL *MAPIFUNC) (LHANDLE lhSession, ULONG ulUIParam,
                                      lpMapiMessage lpMessage, FLAGS flFlags,
                                      ULONG ulReserved);

void CEMailDlg::OnSend() 
{
	const HINSTANCE hMAPILib = ::LoadLibrary("MAPI32.DLL");

	if (hMAPILib)
	{
		MAPIFUNC lpMAPISendMail = (MAPIFUNC) GetProcAddress(hMAPILib, "MAPISendMail");

		if (lpMAPISendMail != NULL)
		{
			static MapiMessage Msg;
			memset(&Msg, 0, sizeof(Msg));

			Msg.lpszSubject  = "Put subject text here";
			Msg.lpszNoteText = "Put message text here";

			lpMAPISendMail(NULL, NULL, &Msg, (FLAGS) (MAPI_LOGON_UI | MAPI_DIALOG), 0);
		}
		else
		{
			ASSERT(FALSE);
		}

		VERIFY(::FreeLibrary(hMAPILib));
	}
	else
	{
		ASSERT(FALSE);
	}
}

The user is able to send an e-mail:

MAPI

Note: Even though there is an import library for MAPI, it apparently is not possible to call the MAPISendMail function directly. Hence the necessity of doing the LoadLibrary and GetProcAddress. This is the technique that MFC uses in void CDocument::OnFileSendMail().

For further details, look up MAPISendMail in the on-line help.


Use TAPI to place a voice call

It's easy to use TAPI (the Windows Telephony API) to place a voice call if your phone is connected to your modem's phone jack. When the following code is executed:

typedef LONG (FAR PASCAL *TAPIFUNC) (LPCSTR const lpszDestAddress,
                                     LPCSTR const lpszAppName,
                                     LPCSTR const lpszCalledParty,
                                     LPCSTR const lpszComment);

void CTAPIDemoDlg::OnDialPhone() 
{
	CString PhoneNumber("303-237-5000");
	CString AppName("Program Name");
	CString CalledParty("Fred");
	CString Comment("Comment");
	
	bool bPlacedCall = false;
	
	const HINSTANCE hTapiLib = ::LoadLibrary("TAPI32.DLL");
	
	if (hTapiLib != NULL)
	{
		TAPIFUNC lpFunc = (TAPIFUNC) GetProcAddress(hTapiLib, "tapiRequestMakeCall");
		
		if (lpFunc != NULL)
		{
			const LONG Status = lpFunc((LPCSTR) PhoneNumber, (LPCSTR) AppName, 
									   (LPCSTR) CalledParty, (LPCSTR) Comment);
			
			bPlacedCall = Status == 0;
		}
		
		VERIFY(::FreeLibrary(hTapiLib) != 0);
	}
	
	VERIFY(bPlacedCall);
}

The telephone number is automatically dialed:

TAPI

The telephone calls are automatically logged in the Phone Dialer:

Phone Dialer Logs Calls

Calls are automatically logged

Note: You may be tempted to use the TAPI import library to avoid calling LoadLibrary and GetProcAddress. If you do, your program will not work in Windows 95.


Scale an image in a browser window while maintaining the proper aspect ratio

Use the following technique to scale images:

<HTML>
<HEAD>
<TITLE>Rocket</TITLE>
</HEAD>
<BODY STYLE="display:none"  topmargin="10" leftmargin="10" bottommargin="10" rightmargin = "10" onresize="ShrinkToFit();">
<SCRIPT>
var OriginalWidth = 0, OriginalHeight = 0;

function ShrinkToFit()
{

	if (OriginalWidth == 0 && OriginalHeight == 0)
	{
		document.body.style.display = "block";
		Picture.style.display       = "block";

		OriginalWidth  = Picture.width;
		OriginalHeight = Picture.height;
	}

	var ClientWidth  = document.body.clientWidth  - 20;
	var ClientHeight = document.body.clientHeight - 20;

	var WidthRatio  = OriginalWidth  / ClientWidth;
	var HeightRatio = OriginalHeight / ClientHeight;
	var Ratio = WidthRatio > HeightRatio ? WidthRatio : HeightRatio;

	Picture.width  = OriginalWidth  / Ratio;
	Picture.height = OriginalHeight / Ratio;

}
</SCRIPT>

</BODY>
<P>
<IMG STYLE="display:none;" ID="Picture"onload="ShrinkToFit();" SRC="file://G:\backup\Photo Album\Los Alamos\Abq_5.jpg" ALT="Rocket">
</P>
</BODY>
</HTML>

Make connection points actually work in VBScript and JavaScript

You must implement the IProvideClassInfo or IProvideClassInfo2 interface to react to ATL connection points in VBScript and JavaScript. For example:

/////////////////////////////////////////////////////////////////////////////
// CMenuObj
class ATL_NO_VTABLE CMenuObj : 
...
	public IProvideClassInfo2Impl<&CLSID_MenuObj, &DIID__IMenuObjEvents, &LIBID_MENULib>,
...

BEGIN_COM_MAP(CMenuObj)
...
	COM_INTERFACE_ENTRY(IProvideClassInfo)
	COM_INTERFACE_ENTRY(IProvideClassInfo2)
...
END_COM_MAP()

See also: Microsoft Knowledge Base article: "HOWTO: Enable ActiveX Control Event Handling on a Web Page ID: Q200839".



Home Page   Vault 3   Shareware   C# .NET Tips   Download C# .NET Source Code   Browse C# .NET Source Code   C# .NET Open Source   On-Line Utilities   Blog   Contact