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.
To convert a HFONT to a LOGFONT, use the GDI function GetObject(), as in:
-
LOGFONT lf;
-
int ret = GetObject(hfont, sizeof(lf), &lf);
-
// Be sure to check the return value of GetObject
The code is trivial but the function took me forever to find.
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.
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:
-
#define ARRAYSIZE(x) ( sizeof(x) / sizeof(x[0]) )
-
-
TCHAR buf[80];
-
int ret = GetNumberFormat
-
(
-
LOCALE_USER_DEFAULT, // locale
-
0, // dwFlags
-
TEXT("1234567.89"), // lpValue
-
NULL, // lpFormat
-
buf, // lpNumberStr
-
ARRAYSIZE(buf) // cchNumber
-
);
-
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:
-
int GetNumberFormatDbl(LCID locale, DWORD dwFlags, double value,
-
const NUMBERFMT* lpFormat, LPTSTR lpNumberStr,
-
int cchNumber)
-
{
-
// DBL_MAX is 1.7976931348623158e+308 and 317 characters
-
// (including null terminator)
-
TCHAR szBuf[317];
-
HRESULT hr = StringCchPrintf(szBuf, ARRAYSIZE(szBuf),
-
TEXT("%lf"), value);
-
if (hr != S_OK)
-
{
-
SetLastError(ERROR_INVALID_PARAMETER);
-
return 0;
-
}
-
return GetNumberFormat(locale, dwFlags, szBuf, lpFormat,
-
lpNumberStr, cchNumber);
-
}
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:
-
// Convert szStr to a BSTR. Returns NULL on failure. Result must be
-
// freed with SysFreeString.
-
BSTR TstrToBstr(LPCTSTR szStr)
-
{
-
#if defined(UNICODE)
-
return SysAllocString(szStr);
-
#else
-
BSTR bstrRet = NULL;
-
int cch = MultiByteToWideChar(CP_ACP, 0, szStr, -1, NULL, 0);
-
if (cch != 0)
-
{
-
WCHAR* pswz = new WCHAR[cch];
-
cch = MultiByteToWideChar(CP_ACP, 0, szStr, -1, pswz, cch);
-
if (cch != 0)
-
{
-
bstrRet = SysAllocString(pswz);
-
}
-
delete[] pswz;
-
}
-
-
return bstrRet;
-
#endif
-
}
-
-
// Converts the localized number string szNumber to a double using the
-
// given locale. Returns TRUE and sets *pVal on success, FALSE
-
// otherwise.
-
BOOL LocalizedStrToDbl(LCID lcid, LPCTSTR szNumber, double* pVal)
-
{
-
BOOL bSuccess = FALSE;
-
-
// Set out parameter regardless
-
*pVal = 0;
-
-
BSTR bstr = TstrToBstr(szNumber);
-
if (bstr != NULL)
-
{
-
VARIANT var;
-
VariantInit(&var);
-
// bstr will be freed on VariantClear
-
var.bstrVal = bstr;
-
var.vt = VT_BSTR;
-
-
HRESULT hr = VariantChangeTypeEx(&var, &var, lcid, 0, VT_R8);
-
if (hr == S_OK)
-
{
-
*pVal = var.dblVal;
-
bSuccess = TRUE;
-
}
-
-
VariantClear(&var);
-
}
-
-
return bSuccess;
-
}
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:
-
// Converts a grouping string returned by
-
// GetLocaleInfo(LOCALE_SGROUPING) into a UINT understood by NUMBERFMT.
-
UINT GroupingStrToUint(LPCTSTR szGrouping)
-
{
-
LPCTSTR szCurr = szGrouping;
-
UINT ret = 0;
-
-
while (true)
-
{
-
ret *= 10;
-
if (*szCurr == TEXT(‘\\0′))
-
break;
-
-
TCHAR* pch;
-
ret += _tcstol(szCurr, &pch, 10);
-
-
if (_tcscmp(pch, TEXT(";0")) == 0)
-
break;
-
-
szCurr = pch + 1;
-
}
-
-
return ret;
-
}
-
-
// Fills the default NUMBERFMT structure for a given locale.
-
// pFmt->lpDecimalSep and pFmt->lpThousandSep must point to valid
-
// buffers of size cchDecimalSep and cchThousandSep respectively.
-
BOOL GetDefaultNumberFmt(LCID lcid, NUMBERFMT* pFmt, int cchDecimalSep,
-
int cchThousandSep)
-
{
-
TCHAR szBuf[80];
-
-
int ret = ::GetLocaleInfo(lcid, LOCALE_IDIGITS, szBuf,
-
ARRAYSIZE(szBuf));
-
if (ret == 0)
-
return FALSE;
-
pFmt->NumDigits = _tcstol(szBuf, NULL, 10);
-
-
ret = ::GetLocaleInfo(lcid, LOCALE_ILZERO, szBuf, ARRAYSIZE(szBuf));
-
if (ret == 0)
-
return FALSE;
-
pFmt->LeadingZero = _tcstol(szBuf, NULL, 10);
-
-
ret = ::GetLocaleInfo(lcid, LOCALE_SGROUPING, szBuf,
-
ARRAYSIZE(szBuf));
-
if (ret == 0)
-
return FALSE;
-
pFmt->Grouping = GroupingStrToUint(szBuf);
-
-
ret = ::GetLocaleInfo(lcid, LOCALE_SDECIMAL, pFmt->lpDecimalSep,
-
cchDecimalSep);
-
if (ret == 0)
-
return FALSE;
-
-
ret = ::GetLocaleInfo(lcid, LOCALE_STHOUSAND, pFmt->lpThousandSep,
-
cchThousandSep);
-
if (ret == 0)
-
return FALSE;
-
-
ret = ::GetLocaleInfo(lcid, LOCALE_INEGNUMBER, szBuf,
-
ARRAYSIZE(szBuf));
-
if (ret == 0)
-
return FALSE;
-
pFmt->NegativeOrder = _tcstol(szBuf, NULL, 10);
-
-
return TRUE;
-
}
Now that we have these functions, we can use them to better control the output from GetNumberFormat(), as in:
-
// Converts the double value to a localized string for the specified
-
// locale with the given number of fractional digits.
-
BOOL DblToLocalizedStr(LCID lcid, double value, int nDigits,
-
LPTSTR szStr, int cchStr)
-
{
-
// Get locale-default NUMBERFMT
-
TCHAR szDecimalSep[5];
-
TCHAR szThousandSep[5];
-
-
NUMBERFMT fmt;
-
fmt.lpDecimalSep = szDecimalSep;
-
fmt.lpThousandSep = szThousandSep;
-
if (!GetDefaultNumberFmt(lcid, &fmt, ARRAYSIZE(szDecimalSep),
-
ARRAYSIZE(szThousandSep)))
-
return FALSE;
-
-
// Override the NumDigits member of NUMBERFMT
-
fmt.NumDigits = nDigits;
-
-
// Format the number with the custom NUMBERFMT
-
int ret = GetNumberFormatDbl(lcid, 0, value, &fmt, szStr, cchStr);
-
return (ret != 0);
-
}
XmlTextWriter is .NET’s class for writing XML in a forward-only streaming manner. It is highly efficient and is the preferred way to generate XML in .NET in most circumstances. I find XmlTextWriter so useful I wrote a partial C++ implementation of it in Implenting IXmlWriter Series.
Unfortunately, XmlTextWriter isn’t quite as strict as it could be. It will let slip some invalid XML such as duplicate attributes, invalid Unicode characters in the range 0×0 to 0×20, and invalid element and attribute names. You can read about XmlTextWriter’s limitations in the article Customized XML Writer Creation.
If these limitations are an issue for you, I suggest following the instructions in “Customized XML Writer Creation” by writing a custom writer that extends the current XmlTextWriter and adds this functionality. This class can be used directly or passed to any functions which are designed to use XmlTextWriter.
Windows Vista developers beware: Vista does not perform file virtualization on the creation of shell links. Consider the following code:
-
// Creates a shell link (a.k.a. shortcut) located at swzLinkFile that
-
// points to szTargetFile with a description of szDescription.
-
BOOL CreateLink(LPCTSTR szTargetFile, LPCTSTR szDescription,
-
LPCOLESTR swzLinkFile)
-
{
-
BOOL bRet = FALSE;
-
-
IShellLink* psl;
-
HRESULT hr = ::CoCreateInstance(CLSID_ShellLink, NULL,
-
CLSCTX_INPROC_SERVER,
-
IID_IShellLink,
-
(void**) &psl);
-
if (SUCCEEDED(hr))
-
{
-
IPersistFile* ppf;
-
hr = psl->QueryInterface(IID_IPersistFile, (void**) &ppf);
-
if (SUCCEEDED(hr))
-
{
-
hr = psl->SetPath(szTargetFile);
-
if (SUCCEEDED(hr))
-
{
-
hr = psl->SetDescription(szDescription);
-
if (SUCCEEDED(hr))
-
{
-
hr = ppf->Save(swzLinkFile, TRUE);
-
if (SUCCEEDED(hr))
-
{
-
bSuccess = TRUE;
-
}
-
}
-
}
-
ppf->Release();
-
}
-
psl->Release();
-
}
-
return bSuccess;
-
}
-
-
// NOTE: Hardcoding C:\\WINDOWS and C:\\Program Files is a bad practice.
-
// Use something like ::SHGetFolderPath().
-
BOOL bSuccess = CreateLink
-
(
-
_T("C:\\WINDOWS\\SYSTEM32\\SOL.EXE"),
-
_T("Shortcut to SOL.EXE"),
-
L"C:\\Program Files\\sol.lnk")
-
);
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.
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.
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.
Apparently Microsoft has an API for just about everything. Today I read about Microsoft’s Delta Compression Application Programming Interface, an API for creating and applying binary diffs. This API looks ideal for building an application’s incremental online update facility.
The Win32 shell lightweight utility API (shlwapi.dll) is a cornucopia of useful functions. It appears to be Microsoft’s “dumping ground” for functions without a better home. (I believe Microsoft internal DLL ownership also played a part.) Had I known about shlwapi years ago, it would have saved me considerable programming effort.
Particularly useful are SHCreateStreamOnFile, the path family of functions (e.g. PathCombine), and the registry family of functions (e.g. SHRegGetPath).
However, it appears that Microsoft is slowly moving functionality out of shlwapi into other places. For example, many of shlwapi’s string handling functions have better-designed replacements in Microsoft’s safe string library strsafe. Similarly, shlwapi’s SHRegGetValue has been deprecated in favor of advapi32.dll’s RegGetValue.
Be sure to look at all the functionality shlwapi provides — you may find a hidden gem of your own!
Recent Comments