/* ************************************************************** *\ ToddB's Super Cool Balloon ToolTip InputLimiter Copyright Microsoft 1998 \* ************************************************************** */ #include "shellprv.h" #include "ids.h" #define IsTextPtr(pszText) ((LPSTR_TEXTCALLBACK != pszText) && !IS_INTRESOURCE(pszText)) #define CHAR_IN_RANGE(ch,l,h) ((ch >= l) && (ch <= h)) #define LIMITINPUTTIMERID 472 // ************************************************************************************************ // CInputLimiter class description // ************************************************************************************************ class CInputLimiter : public tagLIMITINPUT { public: CInputLimiter(); ~CInputLimiter(); BOOL SubclassEditControl(HWND hwnd, const LIMITINPUT *pli); protected: BOOL OnChar(HWND hwnd, WPARAM & wParam, LPARAM lParam); LRESULT OnPaste(HWND hwnd, WPARAM wParam, LPARAM lParam); void ShowToolTip(); void HideToolTip(); void CreateToolTipWindow(); BOOL IsValidChar(TCHAR ch, BOOL bPaste); static LRESULT CALLBACK SubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uID, ULONG_PTR dwRefData); HWND m_hwnd; // the subclassed edit control hwnd HWND m_hwndToolTip; // the tooltip control UINT_PTR m_uTimerID; // the timer id BOOL m_dwCallbacks; // true if any data is callback data. }; CInputLimiter::CInputLimiter() { // our allocation function should have zeroed our memory. Check to make sure: ASSERT(0==m_hwndToolTip); ASSERT(0==m_uTimerID); } CInputLimiter::~CInputLimiter() { // we might have allocated some strings, if we did delete them if (IsTextPtr(pszFilter)) { delete pszFilter; } if (IsTextPtr(pszTitle)) { delete pszTitle; } if (IsTextPtr(pszMessage)) { delete pszMessage; } } BOOL CInputLimiter::SubclassEditControl(HWND hwnd, const LIMITINPUT *pli) { if (!IsWindow(hwnd)) { // must have a valid hwnd TraceMsg(TF_WARNING, "Invalid HWND passed to CInputLimiter::SubclassEditControl"); return FALSE; } m_hwnd = hwnd; // validate all the data passed in the pli structure. Return false if // any of it is out of whack. dwMask = pli->dwMask; if (LIM_FLAGS & dwMask) { dwFlags = pli->dwFlags; if ((LIF_FORCEUPPERCASE|LIF_FORCELOWERCASE) == ((LIF_FORCEUPPERCASE|LIF_FORCELOWERCASE) & dwFlags)) { // cannot use both ForceUpperCase and ForceLowerCase flags TraceMsg(TF_WARNING, "cannot use both ForceUpperCase and ForceLowerCase flags"); return FALSE; } } else { ASSERT(0==dwFlags); } if (LIM_HINST & dwMask) { hinst = pli->hinst; } else { ASSERT(0==hinst); } // keep track of which fields require a valid hwndNotify ASSERT(0==m_dwCallbacks); if (LIM_FILTER & dwMask) { if (LIF_CATEGORYFILTER & dwFlags) { // category filters are not callbacks or int resources even though the data looks like it is. // The don't need any validation. pszFilter = pli->pszFilter; } else if (LPSTR_TEXTCALLBACK == pli->pszFilter) { pszFilter = pli->pszFilter; m_dwCallbacks |= LIM_FILTER; } else if (IS_INTRESOURCE(pli->pszFilter)) { if (!hinst) { // must have valid hinst in order to use int resources TraceMsg(TF_WARNING, "must have valid hinst in order to use int resources for filter"); return FALSE; } // We need to load the target string upfront and store it in a buffer. DWORD cchSize = 64; DWORD cchLoaded; for (;;) { pszFilter = new TCHAR[cchSize]; if (!pszFilter) { // Out of memory TraceMsg(TF_WARNING, "Out of memory in CInputLimiter::SubclassEditControl"); return FALSE; } cchLoaded = LoadString(hinst, PtrToUint(pli->pszFilter), pszFilter, cchSize); if (0 == cchLoaded) { // Could not load filter resource, pszFilter will get deleted in our destructor TraceMsg(TF_WARNING, "Could not load filter resource"); return FALSE; } else if (cchLoaded >= cchSize-1) { // didn't fit in the given buffer, try a larger buffer delete [] pszFilter; cchSize *= 2; } else { // the string loaded successfully break; } } ASSERT(IS_VALID_STRING_PTR(pszFilter,-1)); } else { ASSERT(IS_VALID_STRING_PTR(pli->pszFilter,-1)); pszFilter = new TCHAR[lstrlen(pli->pszFilter)+1]; if (!pszFilter) { // Out of memory TraceMsg(TF_WARNING, "CInputLimiter Out of memory"); return FALSE; } StrCpy(pszFilter, pli->pszFilter); } } else { ASSERT(0==pszFilter); } if (!(LIF_WARNINGOFF & dwFlags) && !((LIM_TITLE|LIM_MESSAGE) & dwMask)) { // if warnings are on then at least one of Title or Message is required. TraceMsg(TF_WARNING, "if warnings are on then at least one of Title or Message is required"); return FALSE; } if (LIM_TITLE & dwMask) { if (LPSTR_TEXTCALLBACK == pli->pszTitle) { pszTitle = pli->pszTitle; m_dwCallbacks |= LIM_TITLE; } else if (IS_INTRESOURCE(pli->pszTitle)) { if (!hinst) { // must have valid hinst in order to use int resources TraceMsg(TF_WARNING, "must have valid hinst in order to use int resources for title"); return FALSE; } // REVIEW: Does the title need to be laoded up fromt or will the ToolTip control do this // for us? pszTitle = pli->pszTitle; } else { ASSERT(IS_VALID_STRING_PTR(pli->pszTitle,-1)); pszTitle = new TCHAR[lstrlen(pli->pszTitle)+1]; StrCpy(pszTitle, pli->pszTitle); } } else { ASSERT(0==pszTitle); } if (LIM_MESSAGE & dwMask) { if (LPSTR_TEXTCALLBACK == pli->pszMessage) { pszMessage = pli->pszMessage; m_dwCallbacks |= LIM_MESSAGE; } else if (IS_INTRESOURCE(pli->pszMessage)) { if (!hinst) { // must have valid hinst in order to use int resources TraceMsg(TF_WARNING, "must have valid hinst in order to use int resources for message"); return FALSE; } // We will let the ToolTip control load this string for us pszMessage = pli->pszMessage; } else { ASSERT(IS_VALID_STRING_PTR(pli->pszMessage,-1)); pszMessage = new TCHAR[lstrlen(pli->pszMessage)+1]; StrCpy(pszMessage, pli->pszMessage); } } else { ASSERT(0==pszMessage); } if (LIM_ICON & dwMask) { hIcon = pli->hIcon; if (I_ICONCALLBACK == hIcon) { m_dwCallbacks |= LIM_ICON; } } if (LIM_NOTIFY & dwMask) { hwndNotify = pli->hwndNotify; } else { hwndNotify = GetParent(m_hwnd); } if (m_dwCallbacks && !IsWindow(hwndNotify)) { // invalid notify window TraceMsg(TF_WARNING, "invalid notify window"); return FALSE; } if (LIM_TIMEOUT & dwMask) { iTimeout = pli->iTimeout; } else { iTimeout = 10000; } if (LIM_TIPWIDTH & dwMask) { cxTipWidth = pli->cxTipWidth; } else { cxTipWidth = 500; } // everything in the *pli structure is valid TraceMsg(TF_GENERAL, "pli structure is valid"); return SetWindowSubclass(hwnd, CInputLimiter::SubclassProc, 0, (LONG_PTR)this); } LRESULT CALLBACK CInputLimiter::SubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uID, ULONG_PTR dwRefData) { CInputLimiter * pthis = (CInputLimiter*)dwRefData; switch (uMsg) { case WM_CHAR: if (!pthis->OnChar(hwnd, wParam, lParam)) { return 0; } break; case WM_KILLFOCUS: pthis->HideToolTip(); break; case WM_TIMER: if (LIMITINPUTTIMERID == wParam) { pthis->HideToolTip(); return 0; } break; case WM_PASTE: // Paste handler handles calling the super wnd proc when needed return pthis->OnPaste(hwnd, wParam, lParam); case WM_NCDESTROY: RemoveWindowSubclass(hwnd, CInputLimiter::SubclassProc, uID); delete pthis; break; default: break; } return DefSubclassProc(hwnd, uMsg, wParam, lParam); } BOOL CInputLimiter::IsValidChar(TCHAR ch, BOOL bPaste) { BOOL bValidChar = FALSE; // start by assuming the character is invalid if (LIF_CATEGORYFILTER & dwFlags) { TraceMsg(TF_GENERAL, "Processing LIF_CATEGORYFILTER: <0x%08x>", (WORD)pszFilter); // pszFilter is actually a bit field with valid character types WORD CharType = 0; #define GETSTRINGTYPEEX_MASK 0x1FF // We only need to call GetStringTypeEx if some of the CT_TYPE1 values are being asked for if (((WORD)pszFilter) & GETSTRINGTYPEEX_MASK) { TraceMsg(TF_GENERAL, "Calling GetStringTypeEx"); // We treat ch as a one character long string. // REVIEW: How are DBCS characters handled? Is this fundamentally flawed for win9x? EVAL(GetStringTypeEx(LOCALE_USER_DEFAULT, CT_CTYPE1, (LPTSTR)&ch, 1, &CharType)); } if (((WORD)pszFilter) & (WORD)CharType) { TraceMsg(TF_GENERAL, "GetStringTypeEx matched a character"); // GetStringTypeEx found the string in one of the selected groups bValidChar = !(LIF_EXCLUDEFILTER & dwFlags); } else { TraceMsg(TF_GENERAL, "Checking the extra types not supported by GetStringTypeEx"); // check for the string in our special groups. We will temporarily use bValidChar // to indicate whether the character was found, not whether it's valid. if (LICF_BINARYDIGIT & PtrToUint(pszFilter)) { if (CHAR_IN_RANGE(ch, TEXT('0'), TEXT('1'))) { bValidChar = TRUE; goto charWasFound; } } if (LICF_OCTALDIGIT & PtrToUint(pszFilter)) { if (CHAR_IN_RANGE(ch, TEXT('0'), TEXT('7'))) { bValidChar = TRUE; goto charWasFound; } } if (LICF_ATOZUPPER & PtrToUint(pszFilter)) { if (CHAR_IN_RANGE(ch, TEXT('A'), TEXT('Z'))) { bValidChar = TRUE; goto charWasFound; } } if (LICF_ATOZLOWER & PtrToUint(pszFilter)) { if (CHAR_IN_RANGE(ch, TEXT('a'), TEXT('z'))) { bValidChar = TRUE; goto charWasFound; } } charWasFound: // right now we have perverted the meaning of bValidChar to indicate if the // character was found or not. We now convert the meaning from "was the // character found" to "is the character valid" by considering LIF_EXCLUDEFILTER. if (LIF_EXCLUDEFILTER & dwFlags) { bValidChar = !bValidChar; } } } else { TraceMsg(TF_GENERAL, "Processing string based filter"); // pszFilter points to a NULL terminated string of characters LPTSTR psz = StrChr(pszFilter, ch); if (LIF_EXCLUDEFILTER & dwFlags) { bValidChar = (NULL == psz); } else { bValidChar = (NULL != psz); } } return bValidChar; } BOOL CInputLimiter::OnChar(HWND hwnd, WPARAM & wParam, LPARAM lParam) { // if the char is a good one return TRUE, this will pass the char on to the // default window proc. For a bad character do a beep and then display the // ballon tooltip pointing at the control. TCHAR ch = (TCHAR)wParam; if (LIM_FILTER & m_dwCallbacks) { // If we have callbacks then we need to update the filter and/or mask text. // Otherwise the filter and/or mask text is already correct. NMLIFILTERINFO lidi = {0}; lidi.hdr.hwndFrom = m_hwnd; lidi.hdr.idFrom = GetWindowLong(m_hwnd, GWL_ID); lidi.hdr.code = LIN_GETFILTERINFO; lidi.li.dwMask = LIM_FILTER & m_dwCallbacks; SendMessage(hwndNotify, WM_NOTIFY, lidi.hdr.idFrom, (LPARAM)&lidi); pszFilter = lidi.li.pszFilter; // REVIEW: we should have a way for the notify hanlder to say "store this // result and stop asking me for the filter to use every time". } if (LIF_FORCEUPPERCASE & dwFlags) { ch = (TCHAR)CharUpper((LPTSTR)ch); } else if (LIF_FORCELOWERCASE & dwFlags) { ch = (TCHAR)CharLower((LPTSTR)ch); } if (IsValidChar(ch, FALSE)) { if (LIF_HIDETIPONVALID & dwFlags) { HideToolTip(); } // We might have upper or lower cased ch, so reflect this in wParam. Since // wParam was passed by reference this will effect the message we forward // on to the original window proc. wParam = (WPARAM)ch; return TRUE; } else { // if we get here then an invalid character was entered if (LIF_NOTIFYONBADCHAR & dwFlags) { NMLIBADCHAR libc = {0}; libc.hdr.hwndFrom = m_hwnd; libc.hdr.idFrom = GetWindowLong(m_hwnd, GWL_ID); libc.hdr.code = LIN_BADCHAR; libc.wParam = wParam; // use the original, non case shifted wParam libc.lParam = lParam; SendMessage(hwndNotify, WM_NOTIFY, libc.hdr.idFrom, (LPARAM)&libc); } if (!(LIF_SILENT & dwFlags)) { MessageBeep(MB_OK); } if (!(LIF_WARNINGOFF & dwFlags)) { ShowToolTip(); } return FALSE; } } LRESULT CInputLimiter::OnPaste(HWND hwnd, WPARAM wParam, LPARAM lParam) { // There are hundreds of lines of code in user to successfully handle a paste into an edit control. // We need to leverage all that code while still disallowing invalid input to result from the paste. // As a result, what we need to do is to get the clip board data, validate that data, place the // valid data back onto the clipboard, call the default window proc to let user do it's thing, and // then restore the clipboard to it's original format. if (OpenClipboard(hwnd)) { HANDLE hdata; UINT iFormat; DWORD cchBad = 0; // count of the number of bad characters // REVIEW: Should this be based on the compile type or the window type? // Compile time check for the correct clipboard format to use: if (sizeof(WCHAR) == sizeof(TCHAR)) { iFormat = CF_UNICODETEXT; } else { iFormat = CF_TEXT; } hdata = GetClipboardData(iFormat); if (hdata) { LPTSTR pszData; pszData = (LPTSTR)GlobalLock(hdata); if (pszData) { // we need to copy the original data because the clipboard owns the hdata // pointer. That data will be invalid after we call SetClipboardData. // We start by calculating the size of the data: DWORD dwSize = (DWORD)GlobalSize(hdata); // Use the prefered GlobalAlloc for clipboard data HANDLE hClone = GlobalAlloc(GPTR, dwSize + sizeof(TCHAR)); HANDLE hNew = GlobalAlloc(GPTR, dwSize + sizeof(TCHAR)); if (hClone && hNew) { LPTSTR pszClone = (LPTSTR)GlobalLock(hClone); LPTSTR pszNew = (LPTSTR)GlobalLock(hNew); if (pszClone && pszNew) { int iNew = 0; // copy the original data as-is memcpy(pszClone, pszData, (size_t)dwSize); // ensure that it's NULL terminated pszClone[(dwSize / sizeof(TCHAR))] = TEXT('\0'); // For a paste, we only call the filter callback once, not once for each // character. Why? Because. if (LIM_FILTER & m_dwCallbacks) { // If we have callbacks then we need to update the filter and/or mask text. // Otherwise the filter and/or mask text is already correct. NMLIFILTERINFO lidi = {0}; lidi.hdr.hwndFrom = m_hwnd; lidi.hdr.idFrom = GetWindowLong(m_hwnd, GWL_ID); lidi.hdr.code = LIN_GETFILTERINFO; lidi.li.dwMask = LIM_FILTER & m_dwCallbacks; SendMessage(hwndNotify, WM_NOTIFY, lidi.hdr.idFrom, (LPARAM)&lidi); pszFilter = lidi.li.pszFilter; // REVIEW: we should have a way for the notify hanlder to say "store this // result and stop asking me for the filter to use every time". } for (LPTSTR psz = pszClone; *psz; psz++) { // we do the Upper/Lower casing one character at a time because we don't want to // alter pszClone. pszClone is used later to restore the ClipBoard. if (LIF_FORCEUPPERCASE & dwFlags) { pszNew[iNew] = (TCHAR)CharUpper((LPTSTR)*psz); // yes, this funky cast is correct. } else if (LIF_FORCELOWERCASE & dwFlags) { pszNew[iNew] = (TCHAR)CharLower((LPTSTR)*psz); // yes, this funky cast is correct. } else { pszNew[iNew] = *psz; } if (IsValidChar(pszNew[iNew], TRUE)) { iNew++; } else { if (LIF_NOTIFYONBADCHAR & dwFlags) { NMLIBADCHAR libc = {0}; libc.hdr.hwndFrom = m_hwnd; libc.hdr.idFrom = GetWindowLong(m_hwnd, GWL_ID); libc.hdr.code = LIN_BADCHAR; libc.wParam = (WPARAM)pszClone[iNew + cchBad]; // use the original, non case shifted chat libc.lParam = lParam; SendMessage(hwndNotify, WM_NOTIFY, libc.hdr.idFrom, (LPARAM)&libc); } cchBad++; if (LIF_PASTECANCEL & dwFlags) { iNew = 0; break; } if (LIF_PASTESTOP & dwFlags) { break; } } } pszNew[iNew] = NULL; // If there are any characters in the paste buffer then we paste the validated string if (*pszNew) { // we always set the new string. Worst case it's identical to the old string GlobalUnlock(hNew); pszNew = NULL; SetClipboardData(iFormat, hNew); hNew = NULL; // call the super proc to do the paste DefSubclassProc(hwnd, WM_PASTE, wParam, lParam); // The above call will have closed the clipboard on us. We try to re-open it. // If this fails it's no big deal, that simply means the SetClipboardData // call below will fail which is good if somebody else managed to open the // clipboard in the mean time. OpenClipboard(hwnd); // and then we set it back to the original value. GlobalUnlock(hClone); pszClone = NULL; if (LIF_KEEPCLIPBOARD & dwFlags) { SetClipboardData(iFormat, hClone); hClone = NULL; } } } if (pszClone) { GlobalUnlock(hClone); } if (pszNew) { GlobalUnlock(hNew); } } if (hClone) { GlobalFree(hClone); } if (hNew) { GlobalFree(hNew); } // at this point we are done with hdata so unlock it GlobalUnlock(hdata); } } CloseClipboard(); if (0 == cchBad) { // the entire paste was valid if (LIF_HIDETIPONVALID & dwFlags) { HideToolTip(); } } else { // if we get here then at least one invalid character was pasted if (!(LIF_SILENT & dwFlags)) { MessageBeep(MB_OK); } if (!(LIF_WARNINGOFF & dwFlags)) { ShowToolTip(); } } } return TRUE; } void CInputLimiter::ShowToolTip() { TraceMsg(TF_GENERAL, "About to show the tooltip"); if (!m_hwndToolTip) { CreateToolTipWindow(); } // Set the tooltip display point RECT rc; GetWindowRect(m_hwnd, &rc); int x, y; x = (rc.left+rc.right)/2; if (LIF_WARNINGABOVE & dwFlags) { y = rc.top; } else if (LIF_WARNINGCENTERED & dwFlags) { y = (rc.top+rc.bottom)/2; } else { y = rc.bottom; } SendMessage(m_hwndToolTip, TTM_TRACKPOSITION, 0, MAKELONG(x,y)); TOOLINFO ti = {0}; ti.cbSize = sizeof(ti); ti.hwnd = m_hwnd; ti.uId = 1; if ((LIM_TITLE|LIM_MESSAGE|LIM_ICON) & m_dwCallbacks) { // If we have callbacks then we need to update the tooltip text. // Otherwise the tooltip text is already correct. NMLIDISPINFO lidi = {0}; lidi.hdr.hwndFrom = m_hwnd; lidi.hdr.idFrom = GetWindowLong(m_hwnd, GWL_ID); lidi.hdr.code = LIN_GETDISPINFO; lidi.li.dwMask = (LIM_TITLE|LIM_MESSAGE|LIM_ICON) & m_dwCallbacks; SendMessage(hwndNotify, WM_NOTIFY, lidi.hdr.idFrom, (LPARAM)&lidi); // REARCHITECT How do we use the icon, bold title, message style tooltips? // Until I learn how I'm just using the message string. ti.lpszText = lidi.li.pszMessage; SendMessage(m_hwndToolTip, TTM_UPDATETIPTEXT, 0, (LPARAM)&ti); if (lidi.li.pszTitle || lidi.li.hIcon) { SendMessage(m_hwndToolTip, TTM_SETTITLE, (WPARAM)lidi.li.hIcon, (LPARAM)lidi.li.pszTitle); } } // Show the tooltip SendMessage(m_hwndToolTip, TTM_TRACKACTIVATE, TRUE, (LPARAM)&ti); // Set a timer to hide the tooltip if (m_uTimerID) { KillTimer(NULL,LIMITINPUTTIMERID); } m_uTimerID = SetTimer(m_hwnd, LIMITINPUTTIMERID, iTimeout, NULL); } // CreateToolTipWindow // // Creates our tooltip control. We share this one tooltip control and use it for all invalid // input messages. The control is hiden when not in use and then shown when needed. // void CInputLimiter::CreateToolTipWindow() { m_hwndToolTip = CreateWindow( TOOLTIPS_CLASS, NULL, WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP | TTS_BALLOON, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, m_hwnd, NULL, GetModuleHandle(NULL), NULL); if (m_hwndToolTip) { SetWindowPos(m_hwndToolTip, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); TOOLINFO ti = {0}; RECT rc = {2,2,2,2}; ti.cbSize = sizeof(ti); ti.uFlags = TTF_TRACK | TTF_TRANSPARENT; ti.hwnd = m_hwnd; ti.uId = 1; ti.hinst = hinst; // REARCHITECT: How do we use the icon, bold title, message style tooltips? // Until I learn how I'm just using the message string. ti.lpszText = pszMessage; // set the version so we can have non buggy mouse event forwarding SendMessage(m_hwndToolTip, CCM_SETVERSION, COMCTL32_VERSION, 0); SendMessage(m_hwndToolTip, TTM_ADDTOOL, 0, (LPARAM)&ti); SendMessage(m_hwndToolTip, TTM_SETMAXTIPWIDTH, 0, cxTipWidth); SendMessage(m_hwndToolTip, TTM_SETMARGIN, 0, (LPARAM)&rc); if (pszTitle || hIcon) { // REARCHITECT: hIcon needs to be an image list index or some such. Get details // on how this really works. SendMessage(m_hwndToolTip, TTM_SETTITLE, (WPARAM)hIcon, (LPARAM)pszTitle); } } else { // failed to create tool tip window, now what should we do? Unsubclass ourselves? TraceMsg(TF_GENERAL, "Failed to create tooltip window"); } } void CInputLimiter::HideToolTip() { // When the timer fires we hide the tooltip window if (m_uTimerID) { KillTimer(m_hwnd,LIMITINPUTTIMERID); m_uTimerID = 0; } if (m_hwndToolTip) { SendMessage(m_hwndToolTip, TTM_TRACKACTIVATE, FALSE, 0); } } // allows caller to pass in already contructed LIMITINPUT structure... HRESULT SHLimitInputEditWithFlags(HWND hwndEdit, LIMITINPUT * pli) { HRESULT hr; CInputLimiter *pInputLimiter = new CInputLimiter; if (pInputLimiter) { if (pInputLimiter->SubclassEditControl(hwndEdit, pli)) { hr = S_OK; } else { hr = E_FAIL; delete pInputLimiter; } } else { hr = E_OUTOFMEMORY; } return hr; } // LimitInput // // Limits the characters that can be entered into an edit box. It intercepts WM_CHAR // messages and only allows certain characters through. Some characters, such as backspace // are always allowed through. // // Args: // hwndEdit Handle to an edit control. Results will be unpredictable if any other window // type is passed in. // // pli Pointer to a LIMITINPUT structure that determines how the input is limited. HRESULT SHLimitInputEditChars(HWND hwndEdit, LPCWSTR pszValidChars, LPCWSTR pszInvalidChars) { LPWSTR pszMessage = NULL; LIMITINPUT li = {0}; li.cbSize = sizeof(li); li.dwMask = LIM_FLAGS | LIM_FILTER | LIM_MESSAGE | LIM_HINST; li.dwFlags = LIF_HIDETIPONVALID; li.hinst = g_hinst; if (pszValidChars) { // ick, li.pszFilter is used as const, but since CInputLimiter is derived from the struct itd be a // pain to define it as such. li.pszFilter = (LPWSTR)pszValidChars; li.dwFlags |= LIF_INCLUDEFILTER; } else { li.pszFilter = (LPWSTR)pszInvalidChars; li.dwFlags |= LIF_EXCLUDEFILTER; } // create the error message. PCWSTR pszChars = pszInvalidChars ? pszInvalidChars : pszValidChars; PWSTR pszSpacedChars = new WCHAR[2 * lstrlen(pszChars) + 1]; if (pszSpacedChars) { // we're mimicing what IDS_INVALIDFN does for the known set of bad chars on the filesystem -- // append each char and separate them by spaces. PWSTR psz = pszSpacedChars; for (int i = 0; i < lstrlen(pszChars); i++) { *psz++ = pszChars[i]; *psz++ = L' '; } *psz = 0; int id = pszInvalidChars ? IDS_CHARSINVALID : IDS_CHARSVALID; pszMessage = ShellConstructMessageString(HINST_THISDLL, MAKEINTRESOURCE(id), pszSpacedChars); delete [] pszSpacedChars; } if (pszMessage) { li.pszMessage = pszMessage; } else { // fall back to the old message li.pszMessage = MAKEINTRESOURCE(IDS_INVALIDFN); } HRESULT hr = SHLimitInputEditWithFlags(hwndEdit, &li); if (pszMessage) { LocalFree(pszMessage); } return hr; } HRESULT SHLimitInputEdit(HWND hwndEdit, IShellFolder *psf) { IItemNameLimits *pinl; HRESULT hr = psf->QueryInterface(IID_PPV_ARG(IItemNameLimits, &pinl)); if (SUCCEEDED(hr)) { LPWSTR pszValidChars; LPWSTR pszInvalidChars; hr = pinl->GetValidCharacters(&pszValidChars, &pszInvalidChars); if (SUCCEEDED(hr)) { hr = SHLimitInputEditChars(hwndEdit, pszValidChars, pszInvalidChars); if (pszValidChars) CoTaskMemFree(pszValidChars); if (pszInvalidChars) CoTaskMemFree(pszInvalidChars); } pinl->Release(); } return hr; } typedef struct tagCBLIMITINPUT { HRESULT hr; IShellFolder *psf; } CBLIMITINPUT; // Limiting the input on a combo box is special cased because you first // have to find the edit box and then LimitInput on that. BOOL CALLBACK FindTheEditBox(HWND hwnd, LPARAM lParam) { // The combo box only has one child, subclass it CBLIMITINPUT *pcbli = (CBLIMITINPUT*)lParam; pcbli->hr = SHLimitInputEdit(hwnd, pcbli->psf); return FALSE; } HRESULT SHLimitInputCombo(HWND hwndComboBox, IShellFolder *psf) { CBLIMITINPUT cbli; cbli.hr = E_FAIL; cbli.psf = psf; EnumChildWindows(hwndComboBox, FindTheEditBox, (LPARAM)&cbli); return cbli.hr; }