Balloon Tooltips

Win32 2 Comments »

In the Windows XP login screen, the password text box will warn you with a balloon tooltip if you accidentally turn Caps Lock on:

Windows XP Caps Lock Warning Message

The balloon tooltip appears to be a Windows tooltip common control with the TTS_BALLOON style.

To replicate this functionality, I decided to write a function called ShowMsgBalloon() which, given a control and the various balloon tooltip parameters, creates and shows the balloon tooltip below the control.

The key insight to making ShowMsgBallon() work as intended was to use the TTF_TRACK option to create a tracking tooltip. This will immediately show the tooltip without requiring the user to position the mouse over the control. The main downside to using TTF_TRACK is that the tooltip will not move with the control if the window is moved; you need to manually move the tooltip using TTM_TRACKPOSITION as required. One could probably make this automatic by subclassing the tooltip’s parent control and handling WM_WINDOWPOSCHANGED messages.

Here is the source code to ShowMsgBalloon(). When you are done with the balloon, call DestroyWindow() on the returned HWND. Note: you may want your application to use comctl32.dll version 6 as it will lead to a nicer visual style, including a close button.

  1. #include <windows.h>
  2. #include <commctrl.h>
  3.  
  4. // Options to ShowMsgBallon() (see dwOpts parameter).  These are the
  5. // standard icon types for balloon tooltips.
  6. #define SMB_ICON_INFO    (1 << 0)
  7. #define SMB_ICON_WARNING (1 << 1)
  8. #define SMB_ICON_ERROR   (1 << 2)
  9.  
  10. // Given the options passed to ShowMsgBalloon(), determine what
  11. // parameter to send to TTM_SETTITLE for the balloon tooltip’s icon.
  12. static DWORD
  13. GetTitleIcon(DWORD dwOpts)
  14. {
  15.     if (dwOpts & SMB_ICON_INFO)
  16.         return TTI_INFO;
  17.     else if (dwOpts & SMB_ICON_WARNING)
  18.         return TTI_WARNING;
  19.     else if (dwOpts & SMB_ICON_ERROR)
  20.         return TTI_ERROR;
  21.     else
  22.         return 0;
  23. }
  24.  
  25. // Create and show a balloon tooltip immediately below the control
  26. // hwndCtrl with the given title, message, and options.
  27. HWND
  28. ShowMsgBalloon(HWND hwndCtrl, LPCTSTR szTitle, LPCTSTR szMsg,
  29.                DWORD dwOpts)
  30. {
  31.     HWND hwndRet = NULL;
  32.     HWND hwndTT = NULL;
  33.     TOOLINFO ti = { 0 };
  34.     RECT rc;
  35.  
  36.     // Even though TTS_CLOSE is always specified, a close button will
  37.     // only be shown if your application has a manifest that requires
  38.     // comctl32.dll version 6.
  39.     hwndTT = CreateWindow
  40.         (
  41.         TOOLTIPS_CLASS,
  42.         TEXT(""),
  43.         WS_POPUP | TTS_NOPREFIX | TTS_BALLOON | TTS_CLOSE,
  44.         CW_USEDEFAULT, CW_USEDEFAULT,
  45.         CW_USEDEFAULT, CW_USEDEFAULT,
  46.         hwndCtrl,
  47.         NULL,
  48.         NULL,
  49.         NULL
  50.         );
  51.     if (hwndTT == NULL)
  52.         goto Cleanup;
  53.  
  54.     // By using TTTOOLINFO_V1_SIZE rather than sizeof(TOOLINFO),
  55.     // we don’t require users to be using comctl32 version 6.
  56.     ti.cbSize = TTTOOLINFO_V1_SIZE;
  57.     ti.uFlags = TTF_TRACK;
  58.     ti.hwnd = hwndCtrl;
  59.     ti.lpszText = const_cast<LPTSTR>(szMsg);
  60.     if (!SendMessage(hwndTT, TTM_ADDTOOL, 0, (LPARAM) &ti))
  61.         goto Cleanup;
  62.     if (!SendMessage(hwndTT, TTM_SETTITLE, GetTitleIcon(dwOpts),
  63.                      (LPARAM) szTitle))
  64.         goto Cleanup;
  65.  
  66.     // Position the tooltip below the control
  67.     if (!GetWindowRect(hwndCtrl, &rc))
  68.         goto Cleanup;
  69.     SendMessage(hwndTT, TTM_TRACKPOSITION, 0,
  70.                 MAKELONG(rc.left + 10, rc.bottom));
  71.  
  72.     // Show the tooltip
  73.     if (!SendMessage(hwndTT, TTM_TRACKACTIVATE, TRUE, (LPARAM) &ti))
  74.         goto Cleanup;
  75.  
  76.     hwndRet = hwndTT;
  77.     hwndTT = NULL;
  78.  
  79. Cleanup:
  80.     if (hwndTT != NULL)
  81.         ::DestroyWindow(hwndTT);
  82.  
  83.     return hwndRet;
  84. }

STL objects and module boundaries

STL, Win32 3 Comments »

Let’s say you have the following function:

  1. void AppendChar(std::string& s, char ch)
  2. {
  3.     s += ch;
  4. }

What happens if this function is exported as an ordinal function from a DLL (not an inlined piece of code inside a header) and you call it from an EXE?
Read the rest of this entry »

Runtime Memory Allocation Tracing

Win32 No Comments »

While reading the paper Dynamic Storage Allocation: A Survey and Critical Review, I quickly focused on the claim that one requires actual memory allocation traces, as opposed to simulated data, in order to properly design an optimized memory allocator. After my experience with Win32 LD_PRELOAD, I knew that I could implement a minimally invasive memory trace mechanism for existing Windows binaries. So I did.

My first task was to write a program which would launch the application we wanted to trace and log its calls to the Windows memory allocation functions. As I recommend on my Win32 LD_PRELOAD page, I used Microsoft Research’s Detours, rather than Win32 LD_PRELOAD, for the mechanism to intercept Windows function calls. The only functions I chose to intercept were HeapAlloc and HeapFree; these functions seem to be the workhorses of the Windows memory allocation world.

When my code detects a call to HeapAlloc or HeapFree, it logs basic information about the call to a file and then forwards the call to their real implementations. Reentrancy was an issue; the process of logging could potentially cause another allocation, so I had to guard against that. A more robust application would also consider multithreading scenarios more carefully.

For performance and space considerations, I decided the allocation logger would write log entries as binary records into a file. An allocation log record is 17 bytes long; a free, 13 bytes. However, memory operations are so frequent that a one minute browsing session in Firefox (including a quick visit to Google Reader), generated a 14 megabyte log file. Furthermore, the logger introduced a noticeable, although not drastic, performance hit.

With the Firefox log file in hand, I wrote a few post-processing scripts in Python to calculate some useful data, including total cumulative memory usage and a memory allocation frequency histogram. I then used Gnuplot to graph the results.

Here is the total cumulative memory usage graph for my short Firefox browsing session:

Firefox Total Cumulative Memory Use

Here is the memory allocation frequency histogram. Note the log-log scale; Firefox (like most applications) is so dominated by very small allocations that the graph is useless without it. The graph has a huge spike of allocations with very small object sizes and, somewhat interestingly, a moderate one with sizes just under 10 KB.

Firefox Memory Allocation Frequency

Here is the source code to the memory trace logger.

Custom-Drawn Tooltips

C, Win32 1 Comment »

Like many common controls, the tooltip control supports custom drawing for maximum flexibility. This is a quick tutorial on how to use the tooltip custom draw facility.

Read the rest of this entry »

Win32: Getting LOGFONT from HFONT

Win32 1 Comment »

To convert a HFONT to a LOGFONT, use the GDI function GetObject(), as in:

  1. LOGFONT lf;
  2. int ret = GetObject(hfont, sizeof(lf), &lf);
  3. // Be sure to check the return value of GetObject

The code is trivial but the function took me forever to find.

Microsoft’s XmlLite

Win32, XML No Comments »

Microsoft has created a new, lightweight C++ XML processing library called XmlLite. It includes a streaming XML writing class patterned after .NET’s System.Xml.XmlWriter.

This library makes the IXmlWriter in Implementing IXmlWriter Series obsolete for Windows developers.

Generating and Parsing Localized Numbers In Windows

C++, Win32 2 Comments »

While Windows supports dozens or even hundreds of languages, its localization APIs require quite a bit of getting used to. Below is how I solved some common problems related to formatting and parsing a number for a specific locale.

Formatting a Number for a Locale

The function GetNumberFormat() formats a number for a particular locale. Its simplest usage looks something like:

  1. #define ARRAYSIZE(x) ( sizeof(x) / sizeof(x[0]) )
  2.  
  3. TCHAR buf[80];
  4. int ret = GetNumberFormat
  5.     (
  6.     LOCALE_USER_DEFAULT, // locale
  7.     0,                   // dwFlags
  8.     TEXT("1234567.89"),  // lpValue
  9.     NULL,                // lpFormat
  10.     buf,                 // lpNumberStr
  11.     ARRAYSIZE(buf)       // cchNumber
  12.     );
  13. ASSERT(ret != 0);

buf now contains the number 1234567.89 formatted for the user’s default locale. For example, for the English-United States locale, buf will contain “1,234,567.89″; for German-Germany, “1.234.567,89″; for Hindi, “12,34,567.89″.

The format of the lpValue parameter is important. From GetNumberFormat()’s MSDN documentation:

lpValue

[in] Pointer to a null-terminated string containing the number string to format. This string can only contain the following characters. All other characters are invalid. The function returns an error if the string indicated by lpValue deviates from these rules.

  • Characters ‘0′ through ‘9′.
  • One decimal point (dot) if the number is a floating-point value.
  • A minus sign in the first character position if the number is a negative value.

Given these constraints, I’ve found the easiest way to convert, say, a double to a string for use as lpValue is to use StringCchPrintf() (or, equivalently, wnsprintf() or _sntprintf()), as in:

  1. int GetNumberFormatDbl(LCID locale, DWORD dwFlags, double value,
  2.                        const NUMBERFMT* lpFormat, LPTSTR lpNumberStr,
  3.                        int cchNumber)
  4. {
  5.     // DBL_MAX is 1.7976931348623158e+308 and 317 characters
  6.     // (including null terminator)
  7.     TCHAR szBuf[317];
  8.     HRESULT hr = StringCchPrintf(szBuf, ARRAYSIZE(szBuf),
  9.                                  TEXT("%lf"), value);
  10.     if (hr != S_OK)
  11.     {
  12.         SetLastError(ERROR_INVALID_PARAMETER);
  13.         return 0;
  14.     }
  15.     return GetNumberFormat(locale, dwFlags, szBuf, lpFormat,
  16.                            lpNumberStr, cchNumber);
  17. }

One caveat: GetNumberFormatDbl() does not deal well with very small numbers (below 1e-5 or so).

Parsing a Localized Number String

I spent a lot of time trying to figure out the best way to parse a localized number string until Michael Kaplan mentioned VariantChangeTypeEx(). Once I had that, the rest was (relatively) easy:

  1. // Convert szStr to a BSTR.  Returns NULL on failure.  Result must be
  2. // freed with SysFreeString.
  3. BSTR TstrToBstr(LPCTSTR szStr)
  4. {
  5. #if defined(UNICODE)
  6.     return SysAllocString(szStr);
  7. #else
  8.     BSTR bstrRet = NULL;
  9.     int cch = MultiByteToWideChar(CP_ACP, 0, szStr, -1, NULL, 0);
  10.     if (cch != 0)
  11.     {
  12.         WCHAR* pswz = new WCHAR[cch];
  13.         cch = MultiByteToWideChar(CP_ACP, 0, szStr, -1, pswz, cch);
  14.         if (cch != 0)
  15.         {
  16.             bstrRet = SysAllocString(pswz);
  17.         }
  18.         delete[] pswz;
  19.     }
  20.  
  21.     return bstrRet;
  22. #endif
  23. }
  24.  
  25. // Converts the localized number string szNumber to a double using the
  26. // given locale.  Returns TRUE and sets *pVal on success, FALSE
  27. // otherwise.
  28. BOOL LocalizedStrToDbl(LCID lcid, LPCTSTR szNumber, double* pVal)
  29. {
  30.     BOOL bSuccess = FALSE;
  31.  
  32.     // Set out parameter regardless
  33.     *pVal = 0;
  34.  
  35.     BSTR bstr = TstrToBstr(szNumber);
  36.     if (bstr != NULL)
  37.     {
  38.         VARIANT var;
  39.         VariantInit(&var);
  40.         // bstr will be freed on VariantClear
  41.         var.bstrVal = bstr;
  42.         var.vt = VT_BSTR;
  43.  
  44.         HRESULT hr = VariantChangeTypeEx(&var, &var, lcid, 0, VT_R8);
  45.         if (hr == S_OK)
  46.         {
  47.             *pVal = var.dblVal;
  48.             bSuccess = TRUE;
  49.         }
  50.  
  51.         VariantClear(&var);
  52.     }
  53.  
  54.     return bSuccess;
  55. }

Using VarR8FromStr() instead of VariantChangeTypeEx() is also an option.

Customizing of the Output of GetNumberFormat()

If you pass NULL as the lpFormat parameter to GetNumberFormat(), you use the locale’s default number formatting information. I often find this to be unacceptable — for example, many times I want to control the number of fractional digits I display. To do this, you need to provide a filled-in NUMBERFMT structure to GetNumberFormat().

I suggest starting with the locale’s default NUMBERFMT and then change only the members you require. Because Windows does not seem to provide a way to retrieve a locale’s default NUMBERFMT, we’ll have to roll our own.

To populate the members of NUMBERFMT we are going to use the function GetLocaleInfo(). The map between NUMBERFMT members and LCTYPEs to pass to GetLocaleInfo() is as follows:

NUMBERFMT Member LCTYPE Constant
NumDigits LOCALE_IDIGITS
LeadingZero LOCALE_ILZERO
Grouping LOCALE_SGROUPING
lpDecimalSep LOCALE_SDECIMAL
lpThousandSep LOCALE_STHOUSAND
NegativeOrder LOCALE_INEGNUMBER

GetLocaleInfo() always returns strings, but many of these strings need to be converted to UINTs. Furthermore, the conversion between the LOCALE_SGROUPING string and the Grouping member is quite tricky; read How to fill in that number grouping member of NUMBERFMT for more information.

We now have enough information to write the function to retrieve a locale-default NUMBERFMT:

  1. // Converts a grouping string returned by
  2. // GetLocaleInfo(LOCALE_SGROUPING) into a UINT understood by NUMBERFMT.
  3. UINT GroupingStrToUint(LPCTSTR szGrouping)
  4. {
  5.     LPCTSTR szCurr = szGrouping;
  6.     UINT ret = 0;
  7.  
  8.     while (true)
  9.     {
  10.         ret *= 10;
  11.         if (*szCurr == TEXT(\\0′))
  12.             break;
  13.  
  14.         TCHAR* pch;
  15.         ret += _tcstol(szCurr, &pch, 10);
  16.  
  17.         if (_tcscmp(pch, TEXT(";0")) == 0)
  18.             break;
  19.  
  20.         szCurr = pch + 1;
  21.     }
  22.  
  23.     return ret;
  24. }
  25.  
  26. // Fills the default NUMBERFMT structure for a given locale.
  27. // pFmt->lpDecimalSep and pFmt->lpThousandSep must point to valid
  28. // buffers of size cchDecimalSep and cchThousandSep respectively.
  29. BOOL GetDefaultNumberFmt(LCID lcid, NUMBERFMT* pFmt, int cchDecimalSep,
  30.                          int cchThousandSep)
  31. {
  32.     TCHAR szBuf[80];
  33.  
  34.     int ret = ::GetLocaleInfo(lcid, LOCALE_IDIGITS, szBuf,
  35.                               ARRAYSIZE(szBuf));
  36.     if (ret == 0)
  37.         return FALSE;
  38.     pFmt->NumDigits = _tcstol(szBuf, NULL, 10);
  39.  
  40.     ret = ::GetLocaleInfo(lcid, LOCALE_ILZERO, szBuf, ARRAYSIZE(szBuf));
  41.     if (ret == 0)
  42.         return FALSE;
  43.     pFmt->LeadingZero = _tcstol(szBuf, NULL, 10);
  44.  
  45.     ret = ::GetLocaleInfo(lcid, LOCALE_SGROUPING, szBuf,
  46.                           ARRAYSIZE(szBuf));
  47.     if (ret == 0)
  48.         return FALSE;
  49.     pFmt->Grouping = GroupingStrToUint(szBuf);
  50.  
  51.     ret = ::GetLocaleInfo(lcid, LOCALE_SDECIMAL, pFmt->lpDecimalSep,
  52.                           cchDecimalSep);
  53.     if (ret == 0)
  54.         return FALSE;
  55.  
  56.     ret = ::GetLocaleInfo(lcid, LOCALE_STHOUSAND, pFmt->lpThousandSep,
  57.                           cchThousandSep);
  58.     if (ret == 0)
  59.         return FALSE;
  60.  
  61.     ret = ::GetLocaleInfo(lcid, LOCALE_INEGNUMBER, szBuf,
  62.                           ARRAYSIZE(szBuf));
  63.     if (ret == 0)
  64.         return FALSE;
  65.     pFmt->NegativeOrder = _tcstol(szBuf, NULL, 10);
  66.  
  67.     return TRUE;
  68. }

Now that we have these functions, we can use them to better control the output from GetNumberFormat(), as in:

  1. // Converts the double value to a localized string for the specified
  2. // locale with the given number of fractional digits.
  3. BOOL DblToLocalizedStr(LCID lcid, double value, int nDigits,
  4.                        LPTSTR szStr, int cchStr)
  5. {
  6.     // Get locale-default NUMBERFMT
  7.     TCHAR szDecimalSep[5];
  8.     TCHAR szThousandSep[5];
  9.  
  10.     NUMBERFMT fmt;
  11.     fmt.lpDecimalSep = szDecimalSep;
  12.     fmt.lpThousandSep = szThousandSep;
  13.     if (!GetDefaultNumberFmt(lcid, &fmt, ARRAYSIZE(szDecimalSep),
  14.                              ARRAYSIZE(szThousandSep)))
  15.         return FALSE;
  16.  
  17.     // Override the NumDigits member of NUMBERFMT
  18.     fmt.NumDigits = nDigits;
  19.  
  20.     // Format the number with the custom NUMBERFMT
  21.     int ret = GetNumberFormatDbl(lcid, 0, value, &fmt, szStr, cchStr);
  22.     return (ret != 0);
  23. }

Vista Does Not Virtualize Creation Of Shell Links

Win32 No Comments »

Windows Vista developers beware: Vista does not perform file virtualization on the creation of shell links. Consider the following code:

  1. // Creates a shell link (a.k.a. shortcut) located at swzLinkFile that
  2. // points to szTargetFile with a description of szDescription.
  3. BOOL CreateLink(LPCTSTR szTargetFile, LPCTSTR szDescription,
  4.                 LPCOLESTR swzLinkFile)
  5. {
  6.     BOOL bRet = FALSE;
  7.  
  8.     IShellLink* psl;
  9.     HRESULT hr = ::CoCreateInstance(CLSID_ShellLink, NULL,
  10.                                     CLSCTX_INPROC_SERVER,
  11.                                     IID_IShellLink,
  12.                                     (void**) &psl);
  13.     if (SUCCEEDED(hr))
  14.     {
  15.         IPersistFile* ppf;
  16.         hr = psl->QueryInterface(IID_IPersistFile, (void**) &ppf);
  17.         if (SUCCEEDED(hr))
  18.         {          
  19.             hr = psl->SetPath(szTargetFile);
  20.             if (SUCCEEDED(hr))
  21.             {
  22.                 hr = psl->SetDescription(szDescription);
  23.                 if (SUCCEEDED(hr))
  24.                 {
  25.                     hr = ppf->Save(swzLinkFile, TRUE);
  26.                     if (SUCCEEDED(hr))
  27.                     {
  28.                         bSuccess = TRUE;
  29.                     }
  30.                 }
  31.             }
  32.             ppf->Release();
  33.         }
  34.         psl->Release();
  35.     }
  36.     return bSuccess;
  37. }
  38.  
  39. // NOTE: Hardcoding C:\\WINDOWS and C:\\Program Files is a bad practice.
  40. // Use something like ::SHGetFolderPath().
  41. BOOL bSuccess = CreateLink
  42.     (
  43.     _T("C:\\WINDOWS\\SYSTEM32\\SOL.EXE"),
  44.     _T("Shortcut to SOL.EXE"),
  45.     L"C:\\Program Files\\sol.lnk")
  46.     );

One might expect that the creation of the file C:\Program Files\sol.lnk would be silently redirected by Vista using file virtualization and CreateLink() would succeed, but it doesn’t — the call to IPersistFile::Save() returns E_ACCESSDENIED.

For more information about developing on Vista, see the document Windows Vista Application Development Requirements for User Account Control Compatibility.

Debugging Crashes in Windows Applications Part 1: The Null Pointer Dereference

Win32 No Comments »

Windows C++ developers remain all too familiar with the standard Windows crash dialog. This post is an attempt to teach developers how to understand the data the crash dialog reports to diagnose difficult issues. A basic understanding of assembly language is assumed; for more background on these topics please read Matt Pietrek’s “Under The Hood” articles in the Microsoft Systems Journal February 1998 and June 1998 issues.

Read the rest of this entry »

MSXML4 Is Going To Be Kill-Bitted

Win32, XML No Comments »

In an effort to EOL MSXML 4 and encourage developers to use MSXML 6, the MSXML team is going to kill-bit MSXML4 in Q4 2007. This means that you will no longer be able to create instances of MSXML 4 from within Internet Explorer.

Be sure to update your applications accordingly. For this, the MSXML team’s post Upgrading to MSXML 6.0 may prove quite useful.

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS Log in