Source code of Windows XP (NT5)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

754 lines
16 KiB

  1. /**********************************************************************/
  2. /** Microsoft Windows/NT **/
  3. /** Copyright(c) Microsoft Corporation, 1997 - 1999 **/
  4. /**********************************************************************/
  5. /*
  6. helper.h
  7. This file defines the following macros helper classes and functions:
  8. Macros to check HRESULT
  9. CDlgHelper -- helper class to Enable/Check dialog Item,
  10. CMangedPage -- helper class for PropertyPage,
  11. It manages ReadOnly, SetModified, and ContextHelp
  12. CHelpDialog -- helper class for dialog with context help support
  13. CStrArray -- an array of pointer to CString
  14. It does NOT duplicate the string upon Add
  15. and It deletes the pointer during destruction
  16. It imports and exports SAFEARRAY
  17. CReadWriteLock -- class for share read or exclusive write lock
  18. CStrBox -- wrapper class for CListBox and CComboBox
  19. CIPAddress -- wrapper for IPAddress
  20. CFramedRoute -- Wrapper for FramedRoute
  21. CStrParse -- parses string for TimeOfDay
  22. FILE HISTORY:
  23. */
  24. // helper functions for dialog and dialog items
  25. #ifndef _DLGHELPER_
  26. #define _DLGHELPER_
  27. #ifdef NOIMP
  28. #undef NOIMP
  29. #endif
  30. #define NOIMP {return E_NOTIMPL;}
  31. #ifdef SAYOK
  32. #undef SAYOK
  33. #endif
  34. #define SAYOK {return S_OK;}
  35. // to reduce the step to set VARIANT
  36. #define V__BOOL(v, v1)\
  37. V_VT(v) = VT_BOOL, V_BOOL(v) = (v1)
  38. #define V__I4(v, v1)\
  39. V_VT(v) = VT_I4, V_I4(v) = (v1)
  40. #define V__I2(v, v1)\
  41. V_VT(v) = VT_I2, V_I2(v) = (v1)
  42. #define V__UI1(v, v1)\
  43. V_VT(v) = VT_UI1, V_UI1(v) = (v1)
  44. #define V__BSTR(v, v1)\
  45. V_VT(v) = VT_BSTR, V_BSTR(v) = (v1)
  46. #define V__ARRAY(v, v1)\
  47. V_VT(v) = VT_ARRAY, V_ARRAY(v) = (v1)
  48. #define REPORT_ERROR(hr) \
  49. TRACE(_T("**** ERROR RETURN <%s @line %d> -> %08lx\n"), \
  50. __FILE__, __LINE__, hr)); \
  51. ReportError(hr, 0, 0);
  52. #ifdef _DEBUG
  53. #define CHECK_HR(hr)\
  54. {if(!CheckADsError(hr, FALSE, __FILE__, __LINE__)){goto L_ERR;}}
  55. #else
  56. #define CHECK_HR(hr)\
  57. if FAILED(hr) goto L_ERR
  58. #endif
  59. #ifdef _DEBUG
  60. #define NOTINCACHE(hr)\
  61. (CheckADsError(hr, TRUE, __FILE__, __LINE__))
  62. #else
  63. #define NOTINCACHE(hr)\
  64. (E_ADS_PROPERTY_NOT_FOUND == (hr))
  65. #endif
  66. BOOL CheckADsError(HRESULT hr, BOOL fIgnoreAttrNotFound, PSTR file, int line);
  67. #ifdef _DEBUG
  68. #define TRACEAfxMessageBox(id) {\
  69. TRACE(_T("AfxMessageBox <%s @line %d> ID: %d\n"), \
  70. __FILE__, __LINE__, id); \
  71. AfxMessageBox(id);}\
  72. #else
  73. #define TRACEAfxMessageBox(id) AfxMessageBox(id)
  74. #endif
  75. // change string Name to CN=Name
  76. void DecorateName(LPWSTR outString, LPCWSTR inString);
  77. // change string Name to CN=Name
  78. HRESULT GetDSRoot(CString& RootString);
  79. // find name from DN for example LDAP://CN=userA,CN=users... returns userA
  80. void FindNameByDN(LPWSTR outString, LPCWSTR inString);
  81. class CDlgHelper
  82. {
  83. public:
  84. static void EnableDlgItem(CDialog* pDialog, int id, bool bEnable = true);
  85. static int GetDlgItemCheck(CDialog* pDialog, int id);
  86. static void SetDlgItemCheck(CDialog* pDialog, int id, int nCheck);
  87. };
  88. struct CMMCNotify
  89. {
  90. CMMCNotify() : m_lNotifyHandle(0), m_lParam(0) {};
  91. LONG_PTR m_lNotifyHandle;
  92. LPARAM m_lParam;
  93. HRESULT MMCNotify()
  94. {
  95. if(m_lNotifyHandle != 0 && m_lParam != 0)
  96. return MMCPropertyChangeNotify(m_lNotifyHandle, m_lParam);
  97. else
  98. return S_FALSE;
  99. };
  100. };
  101. class CManagedPage;
  102. // class CPageManager and CManagedPage together handle the situation when
  103. // the property sheet need to do some processing when OnApply function is called
  104. // on some of the pages
  105. class ATL_NO_VTABLE CPageManager :
  106. public CComObjectRootEx<CComSingleThreadModel>,
  107. public IUnknown
  108. {
  109. BEGIN_COM_MAP(CPageManager)
  110. COM_INTERFACE_ENTRY(IUnknown)
  111. END_COM_MAP()
  112. public:
  113. CPageManager(){ m_bCanCancel = TRUE; m_bModified = FALSE; m_bReadOnly = FALSE; m_dwFlags = 0;};
  114. BOOL GetModified(){ return m_bModified;};
  115. void SetModified(BOOL bModified){ m_bModified = bModified;};
  116. void SetReadOnly(BOOL bReadOnly){ m_bReadOnly = bReadOnly;};
  117. BOOL GetReadOnly(){ return m_bReadOnly;};
  118. HRESULT MMCNotify() { return m_mmcNotify.MMCNotify();};
  119. BOOL IfCanCancel() { return m_bCanCancel;};
  120. void SetCanCancel(BOOL bCanCancel ) { m_bCanCancel = bCanCancel;};
  121. void SetMMCNotify(LONG_PTR lNotifyHandle, LPARAM lParam)
  122. {
  123. // this should be called only once
  124. ASSERT(m_mmcNotify.m_lNotifyHandle == 0);
  125. ASSERT(m_mmcNotify.m_lParam == 0);
  126. ASSERT(lNotifyHandle != 0);
  127. ASSERT(lParam != 0);
  128. m_mmcNotify.m_lNotifyHandle = lNotifyHandle;
  129. m_mmcNotify.m_lParam = lParam;
  130. };
  131. virtual BOOL OnApply();
  132. virtual void OnCancel( ){};
  133. virtual void OnOK( ){};
  134. void AddPage(CManagedPage* pPage);
  135. void AddFlags(DWORD flags) { m_dwFlags |= flags;};
  136. DWORD GetFlags() { return m_dwFlags;};
  137. void ClearFlags() { m_dwFlags = 0;};
  138. protected:
  139. BOOL m_bModified;
  140. BOOL m_bReadOnly;
  141. std::list<CManagedPage*> m_listPages;
  142. DWORD m_dwFlags;
  143. CMMCNotify m_mmcNotify;
  144. BOOL m_bCanCancel;
  145. };
  146. //=============================================================================
  147. // Global Help Table for many Dialog IDs
  148. //
  149. struct CGlobalHelpTable{
  150. UINT nIDD;
  151. const DWORD* pdwTable;
  152. };
  153. //=============================================================================
  154. // Page that handles Context Help, and talk with CPageManager to do
  155. // OnApply together
  156. //
  157. class CManagedPage : public CPropertyPage // talk back to property sheet
  158. {
  159. DECLARE_DYNCREATE(CManagedPage)
  160. // Implementation
  161. protected:
  162. // Generated message map functions
  163. //{{AFX_MSG(CManagedPage)
  164. afx_msg BOOL OnHelpInfo(HELPINFO* pHelpInfo);
  165. afx_msg void OnContextMenu(CWnd* pWnd, CPoint point);
  166. //}}AFX_MSG
  167. DECLARE_MESSAGE_MAP()
  168. protected:
  169. CManagedPage() : CPropertyPage(){
  170. // Need to save the original callback pointer because we are replacing
  171. // it with our own
  172. m_pfnOriginalCallback = m_psp.pfnCallback;
  173. };
  174. public:
  175. CManagedPage(UINT nIDTemplate) : CPropertyPage(nIDTemplate)
  176. {
  177. m_bModified = FALSE;
  178. m_pHelpTable = NULL;
  179. m_nIDD = nIDTemplate;
  180. // Need to save the original callback pointer because we are replacing
  181. // it with our own
  182. m_pfnOriginalCallback = m_psp.pfnCallback;
  183. };
  184. void SetModified( BOOL bModified = TRUE )
  185. {
  186. if(m_spManager.p && !m_spManager->GetReadOnly()) // if NOT readonly
  187. {
  188. m_bModified = bModified;
  189. CPropertyPage::SetModified(bModified);
  190. // only set change
  191. if(bModified) m_spManager->SetModified(TRUE);
  192. }
  193. else
  194. {
  195. m_bModified = bModified;
  196. CPropertyPage::SetModified(bModified);
  197. }
  198. };
  199. BOOL GetModified() { return m_bModified;};
  200. virtual BOOL OnApply()
  201. {
  202. m_bModified = FALSE;
  203. BOOL bRet = TRUE;
  204. if(m_spManager.p && m_spManager->GetModified()) // prevent from entering more than once
  205. {
  206. bRet = m_spManager->OnApply();
  207. }
  208. if(bRet)
  209. return CPropertyPage::OnApply();
  210. else
  211. {
  212. SetModified(); // when user click on OK next time, OnApply Should be called again.
  213. if(m_spManager->IfCanCancel() != TRUE)
  214. CancelToClose(); // change cancel button to close button
  215. return bRet;
  216. }
  217. };
  218. virtual void OnCancel( )
  219. {
  220. if(m_spManager.p) // prevent from entering more than once
  221. m_spManager->OnCancel();
  222. CPropertyPage::OnCancel();
  223. };
  224. virtual void OnOK( )
  225. {
  226. if(m_spManager.p) // prevent from entering more than once
  227. m_spManager->OnOK();
  228. CPropertyPage::OnOK();
  229. };
  230. void SetManager(CPageManager* pManager) { m_spManager = pManager;};
  231. void AddFlags(DWORD flags) { if(m_spManager.p) m_spManager->AddFlags(flags);};
  232. protected:
  233. // set help table: either call SetGHelpTable or call setHelpTable
  234. void SetGlobalHelpTable(CGlobalHelpTable** pGTable)
  235. {
  236. if(pGTable)
  237. {
  238. while((*pGTable)->nIDD && (*pGTable)->nIDD != m_nIDD)
  239. pGTable++;
  240. if((*pGTable)->nIDD)
  241. m_pHelpTable = (*pGTable)->pdwTable;
  242. }
  243. };
  244. void SetHelpTable(DWORD* pTable){m_pHelpTable = pTable;};
  245. #ifdef _DEBUG
  246. virtual void Dump( CDumpContext& dc ) const
  247. {
  248. dc << _T("CManagedPage");
  249. };
  250. #endif
  251. protected:
  252. CComPtr<CPageManager> m_spManager;
  253. BOOL m_bModified;
  254. UINT m_nIDD;
  255. const DWORD* m_pHelpTable;
  256. public:
  257. static UINT CALLBACK PropSheetPageProc( HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp );
  258. void SetSelfDeleteCallback()
  259. {
  260. // tell MMC to hook the proc because we are running on a separate,
  261. // non MFC thread.
  262. m_psp.pfnCallback = PropSheetPageProc;
  263. // We also need to save a self-reference so that the static callback
  264. // function can recover a "this" pointer
  265. m_psp.lParam = (LONG_PTR)this;
  266. };
  267. protected:
  268. LPFNPSPCALLBACK m_pfnOriginalCallback;
  269. };
  270. //=============================================================================
  271. // Dialog that handles Context Help
  272. //
  273. class CHelpDialog : public CDialog // talk back to property sheet
  274. {
  275. DECLARE_DYNCREATE(CHelpDialog)
  276. // Implementation
  277. protected:
  278. // Generated message map functions
  279. //{{AFX_MSG(CHelpDialog)
  280. afx_msg BOOL OnHelpInfo(HELPINFO* pHelpInfo);
  281. afx_msg void OnContextMenu(CWnd* pWnd, CPoint point);
  282. //}}AFX_MSG
  283. DECLARE_MESSAGE_MAP()
  284. protected:
  285. CHelpDialog() : CDialog(){};
  286. public:
  287. CHelpDialog(UINT nIDTemplate, CWnd* pParent) : CDialog(nIDTemplate, pParent)
  288. {
  289. m_pHelpTable = NULL;
  290. m_nIDD = nIDTemplate;
  291. };
  292. protected:
  293. // set help table: either call SetGHelpTable or call setHelpTable
  294. void SetGlobalHelpTable(CGlobalHelpTable** pGTable)
  295. {
  296. if(pGTable)
  297. {
  298. while((*pGTable)->nIDD && (*pGTable)->nIDD != m_nIDD)
  299. pGTable++;
  300. if((*pGTable)->nIDD)
  301. m_pHelpTable = (*pGTable)->pdwTable;
  302. }
  303. };
  304. void SetHelpTable(DWORD* pTable){m_pHelpTable = pTable;};
  305. #ifdef _DEBUG
  306. virtual void Dump( CDumpContext& dc ) const
  307. {
  308. dc << _T("CHelpDialog");
  309. };
  310. #endif
  311. protected:
  312. UINT m_nIDD;
  313. const DWORD* m_pHelpTable;
  314. };
  315. #include <afxtempl.h>
  316. class CStrArray : public CArray< CString*, CString* >
  317. {
  318. public:
  319. CStrArray(SAFEARRAY* pSA = NULL);
  320. CStrArray(const CStrArray& sarray);
  321. CString* AddByRID(UINT rID);
  322. int Find(const CString& Str) const;
  323. int DeleteAll();
  324. virtual ~CStrArray();
  325. operator SAFEARRAY*();
  326. CStrArray& operator = (const CStrArray& sarray);
  327. bool AppendSA(SAFEARRAY* pSA);
  328. };
  329. class CDWArray : public CArray< DWORD, DWORD >
  330. {
  331. public:
  332. CDWArray(){};
  333. CDWArray(const CDWArray& dwarray);
  334. int Find(const DWORD dw) const;
  335. int DeleteAll(){ RemoveAll(); return 0;};
  336. virtual ~CDWArray(){RemoveAll();};
  337. CDWArray& operator = (const CDWArray& dwarray);
  338. };
  339. // a lock to allow multiple read access exclusive or only one write access
  340. class CReadWriteLock // sharable read, exclusive write
  341. {
  342. public:
  343. CReadWriteLock() : m_nRead(0)
  344. {
  345. #ifdef _DEBUG
  346. d_bWrite = false;
  347. #endif
  348. };
  349. void EnterRead()
  350. {
  351. TRACE(_T("Entering Read Lock ..."));
  352. m_csRead.Lock();
  353. if (!m_nRead++)
  354. m_csWrite.Lock();
  355. m_csRead.Unlock();
  356. TRACE(_T("Entered Read Lock\n"));
  357. };
  358. void LeaveRead()
  359. {
  360. TRACE(_T("Leaving Read Lock ..."));
  361. m_csRead.Lock();
  362. ASSERT(m_nRead > 0);
  363. if (!--m_nRead)
  364. m_csWrite.Unlock();
  365. m_csRead.Unlock();
  366. TRACE(_T("Left Read Lock\n"));
  367. };
  368. void EnterWrite()
  369. {
  370. TRACE(_T("Entering Write Lock ..."));
  371. m_csWrite.Lock();
  372. TRACE(_T("Entered Write Lock\n"));
  373. #ifdef _DEBUG
  374. d_bWrite = true;
  375. #endif
  376. };
  377. void LeaveWrite()
  378. {
  379. #ifdef _DEBUG
  380. d_bWrite = false;
  381. #endif
  382. m_csWrite.Unlock();
  383. TRACE(_T("Left Write Lock\n"));
  384. };
  385. public:
  386. #ifdef _DEBUG
  387. bool d_bWrite;
  388. #endif
  389. protected:
  390. CCriticalSection m_csRead;
  391. CCriticalSection m_csWrite;
  392. int m_nRead;
  393. };
  394. // to manage a list box/ combo box
  395. template <class CBox>
  396. class CStrBox
  397. {
  398. public:
  399. CStrBox(CDialog* pDialog, int id, CStrArray& Strings)
  400. : m_Strings(Strings), m_id(id)
  401. {
  402. m_pDialog = pDialog;
  403. m_pBox = NULL;
  404. };
  405. int Fill()
  406. {
  407. m_pBox = (CBox*)m_pDialog->GetDlgItem(m_id);
  408. ASSERT(m_pBox);
  409. m_pBox->ResetContent();
  410. int count = m_Strings.GetSize();
  411. int index;
  412. for(int i = 0; i < count; i++)
  413. {
  414. index = m_pBox->AddString(*m_Strings[(INT_PTR)i]);
  415. m_pBox->SetItemDataPtr(index, m_Strings[(INT_PTR)i]);
  416. }
  417. return count;
  418. };
  419. int DeleteSelected()
  420. {
  421. int index;
  422. ASSERT(m_pBox);
  423. index = m_pBox->GetCurSel();
  424. // if there is any selected
  425. if( index != LB_ERR )
  426. {
  427. CString* pStr;
  428. pStr = (CString*)m_pBox->GetItemDataPtr(index);
  429. // remove the entry from the box
  430. m_pBox->DeleteString(index);
  431. // find the string in the String array
  432. int count = m_Strings.GetSize();
  433. for(int i = 0; i < count; i++)
  434. {
  435. if (m_Strings[(INT_PTR)i] == pStr)
  436. break;
  437. }
  438. ASSERT(i < count);
  439. // remove the string from the string array
  440. m_Strings.RemoveAt(i);
  441. index = i;
  442. delete pStr;
  443. }
  444. return index;
  445. };
  446. int AddString(CString* pStr) // the pStr needs to dynamically allocated
  447. {
  448. int index;
  449. ASSERT(m_pBox && pStr);
  450. index = m_pBox->AddString(*pStr);
  451. m_pBox->SetItemDataPtr(index, pStr);
  452. return m_Strings.Add(pStr);
  453. };
  454. int Select(int arrayindex) // the pStr needs to dynamically allocated
  455. {
  456. if(arrayindex < m_Strings.GetSize())
  457. return m_pBox->SelectString(0, *m_Strings[(INT_PTR)arrayindex]);
  458. else
  459. return CB_ERR;
  460. };
  461. void Enable(BOOL b) // the pStr needs to dynamically allocated
  462. {
  463. ASSERT(m_pBox);
  464. m_pBox->EnableWindow(b);
  465. };
  466. int GetSelected() // it returns the index where the
  467. {
  468. int index;
  469. ASSERT(m_pBox);
  470. index = m_pBox->GetCurSel();
  471. // if there is any selected
  472. if( index != LB_ERR )
  473. {
  474. CString* pStr;
  475. pStr = (CString*)m_pBox->GetItemDataPtr(index);
  476. // find the string in the String array
  477. int count = m_Strings.GetSize();
  478. for(int i = 0; i < count; i++)
  479. {
  480. if (m_Strings[(INT_PTR)i] == pStr)
  481. break;
  482. }
  483. ASSERT(i < count);
  484. index = i;
  485. }
  486. return index;
  487. };
  488. CBox* m_pBox;
  489. protected:
  490. int m_id;
  491. CStrArray& m_Strings;
  492. CDialog* m_pDialog;
  493. };
  494. // class to take care of ip address
  495. class CIPAddress
  496. {
  497. public:
  498. CIPAddress(DWORD address = 0)
  499. {
  500. m_dwAddress = address;
  501. };
  502. // CIPAddress(const CString& strAddress){};
  503. operator DWORD() { return m_dwAddress;};
  504. operator CString()
  505. {
  506. CString str;
  507. WORD hi = HIWORD(m_dwAddress);
  508. WORD lo = LOWORD(m_dwAddress);
  509. str.Format(_T("%-d.%-d.%-d.%d"), HIBYTE(hi), LOBYTE(hi), HIBYTE(lo), LOBYTE(lo));
  510. return str;
  511. };
  512. DWORD m_dwAddress;
  513. };
  514. // format of framedroute: mask dest metric ; mask and dest in dot format
  515. class CFramedRoute
  516. {
  517. public:
  518. void SetRoute(CString* pRoute)
  519. {
  520. m_pStrRoute = pRoute;
  521. m_pStrRoute->TrimLeft();
  522. m_pStrRoute->TrimRight();
  523. m_iFirstSpace = m_pStrRoute->Find(_T(' ')) ;
  524. m_iLastSpace = m_pStrRoute->ReverseFind(_T(' ')) ;
  525. };
  526. CString& GetDest(CString& dest) const
  527. {
  528. int i = m_pStrRoute->Find(_T('/'));
  529. if(i == -1)
  530. i = m_iFirstSpace;
  531. dest = m_pStrRoute->Left(i);
  532. return dest;
  533. };
  534. CString& GetNextStop(CString& nextStop) const
  535. {
  536. nextStop = m_pStrRoute->Mid(m_iFirstSpace + 1, m_iLastSpace - m_iFirstSpace -1 );
  537. return nextStop;
  538. };
  539. CString& GetPrefixLength(CString& prefixLength) const
  540. {
  541. int i = m_pStrRoute->Find(_T('/'));
  542. if( i != -1)
  543. {
  544. prefixLength = m_pStrRoute->Mid(i + 1, m_iFirstSpace - i - 1);
  545. }
  546. else
  547. prefixLength = _T("");
  548. return prefixLength;
  549. };
  550. CString& GetMetric(CString& metric) const
  551. {
  552. metric = m_pStrRoute->Mid(m_iLastSpace + 1);
  553. return metric;
  554. };
  555. protected:
  556. // WARNING: the string is not copied, so user need to make sure the origin is valid
  557. CString* m_pStrRoute;
  558. int m_iFirstSpace;
  559. int m_iLastSpace;
  560. };
  561. class CStrParser
  562. {
  563. public:
  564. CStrParser(LPCTSTR pStr = NULL) : m_pStr(pStr) { }
  565. // get the current string position
  566. LPCTSTR GetStr() const { return m_pStr;};
  567. void SetStr(LPCTSTR pStr) { m_pStr = pStr;};
  568. // find a unsigned interger and return it, -1 == not found
  569. int GetUINT()
  570. {
  571. UINT ret = 0;
  572. while((*m_pStr < _T('0') || *m_pStr > _T('9')) && *m_pStr != _T('\0'))
  573. m_pStr++;
  574. if(*m_pStr == _T('\0')) return -1;
  575. while(*m_pStr >= _T('0') && *m_pStr <= _T('9'))
  576. {
  577. ret = ret * 10 + *m_pStr - _T('0');
  578. m_pStr++;
  579. }
  580. return ret;
  581. };
  582. // find c and skip it
  583. int GotoAfter(TCHAR c)
  584. {
  585. int ret = 0;
  586. // go until find c or end of string
  587. while(*m_pStr != c && *m_pStr != _T('\0'))
  588. m_pStr++, ret++;
  589. // if found
  590. if(*m_pStr == c)
  591. m_pStr++, ret++;
  592. else
  593. ret = -1;
  594. return ret;
  595. };
  596. // skip blank characters space tab
  597. void SkipBlank()
  598. {
  599. while((*m_pStr == _T(' ') || *m_pStr == _T('\t')) && *m_pStr != _T('\0'))
  600. m_pStr++;
  601. };
  602. // check to see if the first character is '0'-'6' for Monday(0) to Sunday(6)
  603. int DayOfWeek() {
  604. SkipBlank();
  605. if(*m_pStr >= _T('0') && *m_pStr <= _T('6'))
  606. return (*m_pStr++ - _T('0'));
  607. else
  608. return -1; // not day of week
  609. };
  610. protected:
  611. LPCTSTR m_pStr;
  612. private:
  613. CString _strTemp;
  614. };
  615. void ReportError(HRESULT hr, int nStr, HWND hWnd);
  616. void ReportError(HRESULT hr, LPCTSTR Str, HWND hWnd);
  617. UINT ReportErrorEx(HRESULT hr, int nStr, HWND hWnd, UINT MB_flags);
  618. UINT ReportErrorEx(HRESULT hr, LPCTSTR Str, HWND hWnd, UINT MB_flags);
  619. // number of characters
  620. void AFXAPI DDV_MinChars(CDataExchange* pDX, CString const& value, int nChars);
  621. #endif