Tips for Win32, MFC, Visual C++, COM, and JavaScript Software 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).
|
|
-
General
-
Determine the directory containing the program
-
Determine if a screen saver is active
-
Change the background and text color of individual command buttons
-
Why does my program, which uses timers, crash when the user shuts down NT?
-
Setting focus to controls in a CDialog-derivative's OnInitDialog function
-
Listboxes
-
Correct horizontal listbox scrolling
-
CListBox::ItemFromPoint does not work in NT
-
Why is my fixed-height owner drawn listbox's MeasureItem never called?
-
Interacting with the Windows Shell
-
Allow the user to specify a folder
-
Determine the path of special folders
-
Create shortcuts (on the Desktop, in the Start Menu, etc.)
-
Internationalization
-
Use CString::FormatMessage to allow filled-in items to be reordered
-
MAPI (Messaging API) and TAPI (Telephony API)
-
Use MAPI to send e-mail
-
Use TAPI to place a voice call
-
JavaScript
-
Scale an image in a browser window while maintaining the proper aspect ratio
-
Active Template Library (COM)
-
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:
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:
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:
The telephone calls are automatically logged in the Phone Dialer:
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".