<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>MFC</title><link>http://blog.vckbase.com/iwaswzq/category/1014.html</link><description>MFC</description><managingEditor>九月鹰飞</managingEditor><dc:language>af</dc:language><generator>.Text Version 0.958.2004.214</generator><item><dc:creator>九月鹰飞</dc:creator><title>一个数独小游戏。[附送源代码]</title><link>http://blog.vckbase.com/iwaswzq/archive/2006/12/31/23788.html</link><pubDate>Sun, 31 Dec 2006 03:10:00 GMT</pubDate><guid>http://blog.vckbase.com/iwaswzq/archive/2006/12/31/23788.html</guid><wfw:comment>http://blog.vckbase.com/iwaswzq/comments/23788.html</wfw:comment><comments>http://blog.vckbase.com/iwaswzq/archive/2006/12/31/23788.html#Feedback</comments><slash:comments>5</slash:comments><wfw:commentRss>http://blog.vckbase.com/iwaswzq/comments/commentRss/23788.html</wfw:commentRss><trackback:ping>http://blog.vckbase.com/iwaswzq/services/trackbacks/23788.html</trackback:ping><description>&amp;lt;PRE&amp;gt;&lt;BR&gt;画面简单，不过已经能玩了。喜欢数独的朋友可以拿来消磨时间。喜欢编程的朋友可以拿去进一步改进。不过记得保留我的名字。有些功能感觉没有必要，不过懒得再折腾这个破玩意了。&lt;BR&gt;&lt;BR&gt;下载地址：&lt;BR&gt;&lt;A href="http://blog.vckbase.com/Files/iwaswzq/ShuDu.rar"&gt;http://blog.vckbase.com/Files/iwaswzq/ShuDu.rar&lt;/A&gt;&lt;BR&gt;&lt;BR&gt;&amp;lt;/PRE&amp;gt;&lt;img src ="http://blog.vckbase.com/iwaswzq/aggbug/23788.html" width = "1" height = "1" /&gt;</description></item><item><dc:creator>九月鹰飞</dc:creator><title>Windows 语音编程初步</title><link>http://blog.vckbase.com/iwaswzq/archive/2006/09/12/22382.html</link><pubDate>Tue, 12 Sep 2006 14:49:00 GMT</pubDate><guid>http://blog.vckbase.com/iwaswzq/archive/2006/09/12/22382.html</guid><wfw:comment>http://blog.vckbase.com/iwaswzq/comments/22382.html</wfw:comment><comments>http://blog.vckbase.com/iwaswzq/archive/2006/09/12/22382.html#Feedback</comments><slash:comments>25</slash:comments><wfw:commentRss>http://blog.vckbase.com/iwaswzq/comments/commentRss/22382.html</wfw:commentRss><trackback:ping>http://blog.vckbase.com/iwaswzq/services/trackbacks/22382.html</trackback:ping><description>&lt;P&gt;&amp;lt;PRE&amp;gt;&lt;BR&gt;Windows 语音编程初步&lt;/P&gt;
&lt;P&gt;一、SAPI简介&lt;/P&gt;
&lt;P&gt;软件中的语音技术包括两方面的内容，一个是语音识别(speech recognition) 和语音合成(speech synthesis)。这两个技术都需要语音引擎的支持。微软推出的应用编程接口API，虽然现在不是业界标准，但是应用比较广泛。&lt;/P&gt;
&lt;P&gt;SAPI全称 The Microsoft Speech API.相关的SR和SS引擎位于Speech SDK开发包中。这个语音引擎支持多种语言的识别和朗读，包括英文、中文、日文等。&lt;/P&gt;
&lt;P&gt;SAPI包括以下组件对象（接口）：&lt;/P&gt;
&lt;P&gt;（1）Voice Commands API。对应用程序进行控制，一般用于语音识别系统中。识别某个命令后，会调用相关接口是应用程序完成对应的功能。如果程序想实现语音控制，必须使用此组对象。&lt;BR&gt;（2）Voice Dictation API。听写输入，即语音识别接口。&lt;BR&gt;（3）Voice Text API。完成从文字到语音的转换，即语音合成。&lt;BR&gt;（4）Voice Telephone API。语音识别和语音合成综合运用到电话系统之上，利用此接口可以建立一个电话应答系统，甚至可以通过电话控制计算机。&lt;BR&gt;（5）Audio Objects API。封装了计算机发音系统。&lt;/P&gt;
&lt;P&gt;SAPI是架构在COM基础上的，微软还提供了ActiveX控件，所以不仅可用于一般的windows程序，还可以用于网页、VBA甚至EXCEL的图表中。如果对COM感到陌生，还可以使用微软的C++ WRAPPERS，它用C++类封装了语音SDK COM对象。&lt;/P&gt;
&lt;P&gt;二、安装SAPI SDK。&lt;/P&gt;
&lt;P&gt;首先从这个站点下载开发包：&lt;A href="http://www.microsoft.com/speech/download/sdk51"&gt;http://www.microsoft.com/speech/download/sdk51&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;Microsoft Speech SDK 5.1添加了Automation支持。所以可以在VB,ECMAScript等支持Automation的语言中使用。&lt;/P&gt;
&lt;P&gt;版本说明：&lt;BR&gt;Version: 5.1 &lt;BR&gt;发布日期: 8/8/2001 &lt;BR&gt;语音: English &lt;BR&gt;下载尺寸: 2.0 MB - 288.8 MB&lt;/P&gt;
&lt;P&gt;这个SDK开发包还包括了可以随便发布的英文和中文的语音合成引擎(TTS)，和英文、中文、日文的语音识别引擎(SR)。&lt;/P&gt;
&lt;P&gt;系统要求98以上版本。编译开发包中的例子程序需要vc6以上环境。&lt;/P&gt;
&lt;P&gt;******下载说明******：&lt;BR&gt;（1）如果要下载例子程序，说明文档，SAPI以及用于开发的美国英语语音引擎，需要下载SpeechSDK51.exe，大约68M。&lt;BR&gt;（2）如果想要使用简体中文和日文的语音引擎，需要下载SpeechSDK51LangPack.exe。大约82M。&lt;BR&gt;（3）如果想要和自己的软件一起发布语音引擎，需要下载SpeechSDK51MSM.exe，大约132M。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; （在这个地址，我未能成功下载）。&lt;BR&gt;（4）如果要获取XP下的 Mike 和 Mary 语音，下载Sp5TTIntXP.exe。大约3.5M。&lt;BR&gt;（5）如果要获取开发包的文档说明，请下载sapi.chm。大约2.3M。这个在sdk51里面已经包含。&lt;/P&gt;
&lt;P&gt;下载完毕后，首先安装SpeechSDK51.exe，然后安装中文语言补丁包SpeechSDK51LangPack，然后展开&lt;BR&gt;msttss22l，自动将所需dll安装到系统目录。&lt;/P&gt;
&lt;P&gt;三、配置vc环境&lt;/P&gt;
&lt;P&gt;在vc6.0的环境下编译语音工程，首先要配置编译环境。假设sdk安装在d:\Microsoft Speech SDK 5.1\路径下，打开工程设置对话框，在c/c++栏中选择Preprocessor分类，然后在"附加包含路径"中输入&lt;BR&gt;d:\Microsoft Speech SDK 5.1\include &lt;BR&gt;告诉vc编译程序所需的SAPI头文件的位置。&lt;BR&gt;然后切换到LINK栏，在Input分类下的附加库路径中输入：&lt;BR&gt;d:\Microsoft Speech SDK 5.1\lib\i386&lt;BR&gt;使vc在链接的时候能够找到sapi.lib。&lt;/P&gt;
&lt;P&gt;四、语音合成的应用。即使用SAPI实现TTS(Text to Speech)。&lt;/P&gt;
&lt;P&gt;1、首先要初始化语音接口，一般有两种方式：&lt;BR&gt;&amp;nbsp;&amp;nbsp; ISpVoice* pVoice;&lt;BR&gt;&amp;nbsp;&amp;nbsp; ::CoInitialize(NULL);&lt;BR&gt;&amp;nbsp;&amp;nbsp; HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; (void **)&amp;amp;pVoice);&lt;BR&gt;&amp;nbsp;&amp;nbsp; 然后就可以使用这个指针调用SAPI函数了，例如&lt;BR&gt;&amp;nbsp;&amp;nbsp; pVoice-&amp;gt;SetVolume(50);//设置音量&lt;BR&gt;&amp;nbsp;&amp;nbsp; pVoice-&amp;gt;Speak(str.AllocSysString(),SPF_ASYNC,NULL);&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; 另外也可以使用如下方式：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CComPtr&amp;lt;ISpVoice&amp;gt;&amp;nbsp;&amp;nbsp; m_cpVoice;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; HRESULT&amp;nbsp; hr = m_cpVoice.CoCreateInstance( CLSID_SpVoice );&lt;BR&gt;&amp;nbsp;&amp;nbsp; 在下面的例子中都用这个m_cpVoice变量。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; CLSID_SpVoice的定义位于SPAI.H中。&lt;/P&gt;
&lt;P&gt;2、获取/设置输出频率。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; SAPI朗读文字的时候，可以采用多种频率方式输出声音，比如：&lt;BR&gt;&amp;nbsp;&amp;nbsp; 8kHz 8Bit Mono、8kHz 8Bit Stereo、44kHz 16Bit Mono、44kHz 16Bit Stereo等。在音调上有所差别。具体可以参考sapi.h。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; 可以使用如下代码获取当前的配置：&lt;BR&gt;&amp;nbsp;&amp;nbsp; CComPtr&amp;lt;ISpStreamFormat&amp;gt; cpStream;&lt;BR&gt;&amp;nbsp;&amp;nbsp; HRESULT hrOutputStream = m_cpVoice-&amp;gt;GetOutputStream(&amp;amp;cpStream);&lt;BR&gt;&amp;nbsp;&amp;nbsp; if (hrOutputStream == S_OK)&lt;BR&gt;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; CSpStreamFormat Fmt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; hr = Fmt.AssignFormat(cpStream);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if (SUCCEEDED(hr))&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SPSTREAMFORMAT eFmt = Fmt.ComputeFormatEnum();&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SPSTREAMFORMAT 是一个ENUM类型，定义位于SPAI.H中。每一个值对应了不同的频率设置。例如 SPSF_8kHz8BitStereo&amp;nbsp; = 5&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 通过如下代码设置当前朗读频率：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CComPtr&amp;lt;ISpAudio&amp;gt;&amp;nbsp;&amp;nbsp; m_cpOutAudio; //声音输出接口&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SpCreateDefaultObjectFromCategoryId( SPCAT_AUDIOOUT, &amp;amp;m_cpOutAudio ); //创建接口&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SPSTREAMFORMAT eFmt = 21; //SPSF_22kHz 8Bit Stereo&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CSpStreamFormat Fmt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Fmt.AssignFormat(eFmt);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if ( m_cpOutAudio )&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;hr = m_cpOutAudio-&amp;gt;SetFormat( Fmt.FormatId(), Fmt.WaveFormatExPtr() );&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; else&amp;nbsp; hr = E_FAIL;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if( SUCCEEDED( hr ) )&lt;BR&gt;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; m_cpVoice-&amp;gt;SetOutput( m_cpOutAudio, FALSE );&lt;BR&gt;&amp;nbsp;&amp;nbsp; }&lt;/P&gt;
&lt;P&gt;3、获取/设置播放所用语音。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; 引擎中所用的语音数据文件一般保存在SpeechEngines下的spd或者vce文件中。安装sdk后，在注册表中保存了可用的语音，比如英文的男/女，简体中文的男音等。位置是：&lt;BR&gt;&amp;nbsp;&amp;nbsp; HKEY_LOCAL_MACHINE\Software\Microsoft\Speech\Voices\Tokens&lt;BR&gt;如果安装在中文操作系统下，则缺省所用的朗读语音是简体中文。SAPI的缺点是不能支持中英文混读，在朗读中文的时候，遇到英文，只能逐个字母读出。所以需要程序自己进行语音切换。&lt;/P&gt;
&lt;P&gt;(1) 可以采用如下的函数把当前SDK支持的语音填充在一个组合框中：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; // SAPI5 helper function in sphelper.h &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; HWND hWndCombo = GetDlgItem( hWnd, IDC_COMBO_VOICES ); //组合框句柄&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; HRESULT hr = SpInitTokenComboBox( hWndCombo , SPCAT_VOICES );&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这个函数是通过IEnumSpObjectTokens接口枚举当前可用的语音接口，把接口的说明文字添加到组合框中，并且把接口的指针作为LPARAM&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 保存在组合框中。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 一定要记住最后程序退出的时候，释放组合框中保存的接口：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SpDestroyTokenComboBox( hWndCombo );&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 这个函数的原理就是逐个取得combo里面每一项的LPARAM数据，转换成IUnknown接口指针，然后调用Release函数。&lt;BR&gt;(2) 当组合框选择变化的时候，可以用下面的函数获取用户选择的语音：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; ISpObjectToken* pToken = SpGetCurSelComboBoxToken( hWndCombo );&lt;/P&gt;
&lt;P&gt;(3) 用下面的函数获取当前正在使用的语音：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CComPtr&amp;lt;ISpObjectToken&amp;gt; pOldToken;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; HRESULT hr = m_cpVoice-&amp;gt;GetVoice( &amp;amp;pOldToken );&lt;BR&gt;(4) 当用户选择的语音和当前正在使用的不一致的时候，用下面的函数修改：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if (pOldToken != pToken)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 首先结束当前的朗读，这个不是必须的。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; HRESULT hr = m_cpVoice-&amp;gt;Speak( NULL, SPF_PURGEBEFORESPEAK, 0);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if (SUCCEEDED (hr) )&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; hr = m_cpVoice-&amp;gt;SetVoice( pToken );&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;(5) 也可以直接使用函数SpGetTokenFromId获取指定voice的Token指针，例如：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WCHAR pszTokenId[] = L"HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Speech\\Voices\\Tokens\\MSSimplifiedChineseVoice";&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; SpGetTokenFromId(pszTokenID , &amp;amp;pChineseToken);&lt;/P&gt;
&lt;P&gt;4、开始/暂停/恢复/结束当前的朗读&lt;BR&gt;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp; 要朗读的文字必须位于宽字符串中，假设位于szWTextString中，则：&lt;BR&gt;&amp;nbsp;&amp;nbsp; 开始朗读的代码：&lt;BR&gt;&amp;nbsp;&amp;nbsp; hr = m_cpVoice-&amp;gt;Speak( szWTextString, SPF_ASYNC | SPF_IS_NOT_XML, 0 );&lt;BR&gt;&amp;nbsp;&amp;nbsp; 如果要解读一个XML文本，用：&lt;BR&gt;&amp;nbsp;&amp;nbsp; hr = m_cpVoice-&amp;gt;Speak( szWTextString, SPF_ASYNC | SPF_IS_XML, 0 );&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; 暂停的代码：&amp;nbsp;&amp;nbsp; m_cpVoice-&amp;gt;Pause();&lt;BR&gt;&amp;nbsp;&amp;nbsp; 恢复的代码：&amp;nbsp;&amp;nbsp; m_cpVoice-&amp;gt;Resume();&lt;BR&gt;&amp;nbsp;&amp;nbsp; 结束的代码：（上面的例子中已经给出了）&lt;BR&gt;&amp;nbsp;&amp;nbsp; hr = m_cpVoice-&amp;gt;Speak( NULL, SPF_PURGEBEFORESPEAK, 0);&lt;/P&gt;
&lt;P&gt;5、跳过部分朗读的文字&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; 在朗读的过程中，可以跳过部分文字继续后面的朗读，代码如下：&lt;BR&gt;&amp;nbsp;&amp;nbsp; ULONG ulGarbage = 0;&lt;BR&gt;&amp;nbsp;&amp;nbsp; WCHAR szGarbage[] = L"Sentence";&lt;BR&gt;&amp;nbsp;&amp;nbsp; hr = m_cpVoice-&amp;gt;Skip( szGarbage, SkipNum, &amp;amp;ulGarbage );&lt;BR&gt;&amp;nbsp;&amp;nbsp; SkipNum是设置要跳过的句子数量，值可以是正/负。&lt;BR&gt;&amp;nbsp;&amp;nbsp; 根据sdk的说明，目前SAPI仅仅支持SENTENCE这个类型。SAPI是通过标点符号来区分句子的。&lt;/P&gt;
&lt;P&gt;6、播放WAV文件。SAPI可以播放WAV文件，这是通过ISpStream接口实现的：&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; CComPtr&amp;lt;ISpStream&amp;gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; cpWavStream;&lt;BR&gt;&amp;nbsp;&amp;nbsp; WCHAR&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; szwWavFileName[NORM_SIZE] = L"";;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; USES_CONVERSION;&lt;BR&gt;&amp;nbsp;&amp;nbsp; wcscpy( szwWavFileName, T2W( szAFileName ) );//从ANSI将WAV文件的名字转换成宽字符串&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; //使用sphelper.h 提供的这个函数打开 wav 文件，并得到一个 IStream 指针&lt;BR&gt;&amp;nbsp;&amp;nbsp; hr = SPBindToFile( szwWavFileName, SPFM_OPEN_READONLY, &amp;amp;cpWavStream );&lt;BR&gt;&amp;nbsp;&amp;nbsp; if( SUCCEEDED( hr ) )&lt;BR&gt;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; m_cpVoice-&amp;gt;SpeakStream( cpWavStream, SPF_ASYNC, NULL );//播放WAV文件&lt;BR&gt;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;7、将朗读的结果保存到wav文件&lt;BR&gt;&amp;nbsp;&amp;nbsp; TCHAR szFileName[256];//假设这里面保存着目标文件的路径&lt;BR&gt;&amp;nbsp;&amp;nbsp; USES_CONVERSION;&lt;BR&gt;&amp;nbsp;&amp;nbsp; WCHAR m_szWFileName[MAX_FILE_PATH];&lt;BR&gt;&amp;nbsp;&amp;nbsp; wcscpy( m_szWFileName, T2W(szFileName) );//转换成宽字符串&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; //创建一个输出流，绑定到wav文件&lt;BR&gt;&amp;nbsp;&amp;nbsp; CSpStreamFormat OriginalFmt;&lt;BR&gt;&amp;nbsp;&amp;nbsp; CComPtr&amp;lt;ISpStream&amp;gt;&amp;nbsp; cpWavStream;&lt;BR&gt;&amp;nbsp;&amp;nbsp; CComPtr&amp;lt;ISpStreamFormat&amp;gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; cpOldStream;&lt;BR&gt;&amp;nbsp;&amp;nbsp; HRESULT hr = m_cpVoice-&amp;gt;GetOutputStream( &amp;amp;cpOldStream );&lt;BR&gt;&amp;nbsp;&amp;nbsp; if (hr == S_OK) hr = OriginalFmt.AssignFormat(cpOldStream);&lt;BR&gt;&amp;nbsp;&amp;nbsp; else&amp;nbsp; hr = E_FAIL;&lt;BR&gt;&amp;nbsp;&amp;nbsp; // 使用sphelper.h中提供的函数创建 wav 文件&lt;BR&gt;&amp;nbsp;&amp;nbsp; if (SUCCEEDED(hr))&lt;BR&gt;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; hr = SPBindToFile( m_szWFileName, SPFM_CREATE_ALWAYS, &amp;amp;cpWavStream, &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;amp;OriginalFmt.FormatId(), OriginalFmt.WaveFormatExPtr() ); &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;&amp;nbsp;&amp;nbsp; if( SUCCEEDED( hr ) )&lt;BR&gt;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; //设置声音的输出到 wav 文件，而不是 speakers&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; m_cpVoice-&amp;gt;SetOutput(cpWavStream, TRUE);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; //开始朗读&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; m_cpVoice-&amp;gt;Speak( szWTextString, SPF_ASYNC | SPF_IS_NOT_XML, 0 );&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; //等待朗读结束&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; m_cpVoice-&amp;gt;WaitUntilDone( INFINITE );&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; cpWavStream.Release();&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; //把输出重新定位到原来的流&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; m_cpVoice-&amp;gt;SetOutput( cpOldStream, FALSE );&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;BR&gt;8、设置朗读音量和速度&lt;BR&gt;&amp;nbsp;&amp;nbsp; m_cpVoice-&amp;gt;SetVolume((USHORT)hpos); //设置音量，范围是 0 - 100&lt;BR&gt;&amp;nbsp;&amp;nbsp; m_cpVoice-&amp;gt;SetRate(hpos);&amp;nbsp;&amp;nbsp;//设置速度，范围是 -10 - 10&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; hpos的值一般位于&lt;/P&gt;
&lt;P&gt;9、设置SAPI通知消息。SAPI在朗读的过程中，会给指定窗口发送消息，窗口收到消息后，可以主动获取SAPI的事件，&lt;BR&gt;&amp;nbsp;&amp;nbsp; 根据事件的不同，用户可以得到当前SAPI的一些信息，比如正在朗读的单词的位置，当前的朗读口型值（用于显&lt;BR&gt;&amp;nbsp;&amp;nbsp; 示动画口型，中文语音的情况下并不提供这个事件）等等。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; 要获取SAPI的通知，首先要注册一个消息：&lt;BR&gt;&amp;nbsp;&amp;nbsp; m_cpVoice-&amp;gt;SetNotifyWindowMessage( hWnd, WM_TTSAPPCUSTOMEVENT, 0, 0 );&lt;BR&gt;&amp;nbsp;&amp;nbsp; 这个代码一般是在主窗口初始化的时候调用，hWnd是主窗口（或者接收消息的窗口）句柄。WM_TTSAPPCUSTOMEVENT&lt;BR&gt;&amp;nbsp;&amp;nbsp; 是用户自定义消息。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; 在窗口响应WM_TTSAPPCUSTOMEVENT消息的函数中，通过如下代码获取sapi的通知事件：&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CSpEvent&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; event;&amp;nbsp; // 使用这个类，比用 SPEVENT结构更方便&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; while( event.GetFrom(m_cpVoice) == S_OK )&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; switch( event.eEventId )&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 。。。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp; eEventID有很多种，比如SPEI_START_INPUT_STREAM表示开始朗读，SPEI_END_INPUT_STREAM表示朗读结束等。&lt;BR&gt;&amp;nbsp;&amp;nbsp; 可以根据需要进行判断使用。&lt;/P&gt;
&lt;P&gt;四、结束语&lt;BR&gt;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp; SAPI的功能很多，比如语音识别、使用语法分析等，由于条件和精力有限，我未能一一尝试，感兴趣的朋友可以自己安装一个研究一下。&lt;BR&gt;&amp;nbsp;&amp;nbsp; 另外提供一个简单例子程序的下载，位置是：&lt;BR&gt;&amp;nbsp;&amp;nbsp; &lt;A href="ftp://vckbase:vckbase@210.192.111.117/user/iwaswzq/Universe.rar"&gt;ftp://vckbase:vckbase@210.192.111.117/user/iwaswzq/Universe.rar&lt;/A&gt;&lt;BR&gt;&amp;nbsp;&amp;nbsp; 再次感谢砸玻璃。&lt;BR&gt;&amp;nbsp; &lt;BR&gt;============================================&lt;BR&gt;iwaswzq 初稿于 2006/7/26 ， 修改于 2006/9/7。&lt;BR&gt;&amp;lt;/PRE&amp;gt;&lt;/P&gt;&lt;img src ="http://blog.vckbase.com/iwaswzq/aggbug/22382.html" width = "1" height = "1" /&gt;</description></item><item><dc:creator>九月鹰飞</dc:creator><title>谈谈父窗口和所有者窗口</title><link>http://blog.vckbase.com/iwaswzq/archive/2006/09/12/22380.html</link><pubDate>Tue, 12 Sep 2006 14:43:00 GMT</pubDate><guid>http://blog.vckbase.com/iwaswzq/archive/2006/09/12/22380.html</guid><wfw:comment>http://blog.vckbase.com/iwaswzq/comments/22380.html</wfw:comment><comments>http://blog.vckbase.com/iwaswzq/archive/2006/09/12/22380.html#Feedback</comments><slash:comments>14</slash:comments><wfw:commentRss>http://blog.vckbase.com/iwaswzq/comments/commentRss/22380.html</wfw:commentRss><trackback:ping>http://blog.vckbase.com/iwaswzq/services/trackbacks/22380.html</trackback:ping><description>&lt;P&gt;&amp;lt;PRE&amp;gt;&lt;BR&gt;一、概念和区别&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在windows系统中，每个窗口对象都对应有一个数据结构，形成一个list链表。系统的窗口管理器通过这个list来获取窗口信息和管理每个窗口。这个数据结构中有四个数据用来构建list，即child、sibling、parent、owner四个域。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 所以我们可以看到，窗口之间的关系有两种：owner-owned 关系和 parent-child关系。前者称之为拥有/被拥有关系，后者称之为父/子关系。在这篇文字中，我把owner窗口称之所有者窗口。换句话说，一个窗口在有一个父窗口（parent)的同时，还可能被不同的窗口拥有（owner)，也可以有自己的子窗口(child)。在MFC 的CWnd类中，所有者窗口保存在m_hWndOwner成员变量中，父窗口则保存在m_hParent中，但是这两个值并不一定和窗口对象数据结构中的值相对应。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 窗口之间的关系，决定了窗口的外在表现。比如显示、销毁等。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果一个窗口数据的owner域非NULL，则它和该窗口建立了owner-owned 关系，拥有关系决定了：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; （1）被拥有的窗口永远显示在拥有它的那个窗口的前面；&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; （2）当所有者窗口最小化的时候，它所拥有的窗口都会被隐藏；&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; （3）当所有者窗口被销毁的时候，它所拥有的窗口都会被销毁。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 需要注意的是，隐藏所有者窗口并不会影响它所拥有的窗口的可见状态。比如：如果窗口 A 拥有窗口B,窗口B拥有窗口C,则当窗口A最小化的时候，窗口B被隐藏，但是窗口 C还是可见。&lt;/P&gt;
&lt;P&gt;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 如果一个窗口的parent域非NULL，则它和该窗口之间就建立了parent-child关系。父子决定了：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; （1）窗口在屏幕上面的显示位置。父窗口提供了用来定位子窗口的坐标系统，一个子窗口只能显示在它的父窗口的客户区中，之外的部分将被裁减。这个裁减法则决定了如果父窗口不可见，则子窗口肯定不可见。如果父窗口移动到了屏幕之外，子窗口也一样。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; （2）当父窗口被隐藏时，它的所有子窗口也被隐藏。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; （3）父窗口被销毁的时候，它所拥有的子窗口都会被销毁。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 注意！最小化父窗口不会影响子窗口的可见状态，子窗口会随着父窗口被最小化，但是它的WS_VISIBLE属性不会变。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; Windows系统为什么要使用两种关系呢？这是为了更加灵活的管理窗口。举个例子：组合框（combobox)的下拉列表框（list box）可以超出组合框的父窗口的客户区，这样有利于显示，因此系统创建该list box的时候，是作为控制台窗口（desktop window）的子窗口，它的父窗口hWndParent是NULL，这样，list box的显示区域是限制在整个屏幕内，但是该list box的所有者却是组合框的第一个非子窗口祖先（比如对话框），当它的所有者窗口销毁后，该 list box自动销毁。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 另外，窗口之间消息的传递也和窗口关系有关，通常，一个窗口会把自己的通知消息发送给它的父窗口，但不全是这样，比如，CToolBar发送通知消息给它的所有者窗口而不是父窗口。这样以来，就可以允许工具条作为一个窗口（比如一个 OLE 容器程序窗口）的子窗口的同时，能够给另一个窗口（比如in-place框架窗口）发送消息。至于某类窗口到底是把消息发送给谁，是父窗口还是所有者窗口，microsoft并没有明示。还有，在现场（in-place）编辑的情况下，当一个 server 窗口激活或者失效的时候，框架窗口所拥有的子窗口自动隐藏或者显示，这也是通过直接调用SetOwner函数实现的。&lt;/P&gt;
&lt;P&gt;&amp;nbsp; &lt;BR&gt;二、窗口类型的说明和限制&lt;/P&gt;
&lt;P&gt;（1）控制台窗口（desktop window）。这是系统最早创建的窗口。可以认为它是所有 WS_OVERLAPPED 类型窗口的所有者和父窗口。Kyle Marsh在他的文章&amp;#8220;Win32 Window Hierarchy and Styles&amp;#8221;中指出，当系统初始化的时候，它首先创建控制台窗口，大小覆盖整个屏幕。所有其它窗口都在这个控制台窗口上面显示。窗口管理器所用的窗口list中第一个就是这个控制台。它的下一层窗口叫做顶级窗口（top-level），顶级窗口是指所有非child、没有父窗口，或者父窗口是desktop的窗口，它们没有WS_CHILD属性。&lt;/P&gt;
&lt;P&gt;（2）WS_OVERLAPPED类型的窗口可以显示在屏幕的任何地方。它们的所有者窗口是控制台。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Overlapped 类型的窗口属于顶级窗口，一般作为应用程序的主窗口。不论是否给出了WS_CAPTION、WS_BORDER属性，这类窗口创建后都有标题栏和边框。Overlapped窗口可以拥有其它顶级窗口或者被其它顶级窗口所拥有。所有overlapped窗口都有WS_CLIPSIBLINGS属性。系统可以自动设置 overlapped窗口的大小和初始位置。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 当系统 shuts down的时候，它将销毁所有overlapped类型的窗口。&lt;/P&gt;
&lt;P&gt;（3）WS_POPUP类型的窗口可以显示在屏幕任何地方，它们一般没有父窗口，但是如果明确调用SetParent，这类窗口也可以有父窗口。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WS_POPUP类型的窗口的所有者是在CreateWindow函数中通过设置hWndParent参数给定的，如果hWndParent不是子窗口，则该窗口就成为这个新的弹出式窗口的owner，否则，系统从hWndParent的父窗口向上找，直到找到第一个非子窗口，把它作为该弹出窗口的owner。当owner窗口销毁的时候，系统自动销毁这个弹出窗口。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Pop-up类型的窗口也属于顶级窗口，它和 overlapped 窗口的主要区别是弹出式窗口不需要有标题栏，也不必有边框。弹出式可以拥有其它顶级窗口或者被拥有。所有弹出式窗口也都有 WS_CLIPSIBLINGS属性。&lt;/P&gt;
&lt;P&gt;（4）所有者窗口（owner)只能是 overlapped 或者 pop-up 类型的窗口，子窗口不能是所有者窗口，也就是说子窗口不能拥有其它窗口。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; overlapped 或者 pop-up 类型的窗口在拥有其它窗口的同时，也可以被拥有。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 在使用CreateWindowEx创建 WS_OVERLAPPED 或者 WS_POPUP类型的窗口时，可以在 hwndParent 参数中给出它的所有者窗口的句柄。如果 hwndParent 给出的是一个child 类型的窗口句柄，则系统自动将新创建窗口的所有权交给该子窗口的顶级父窗口。在这种情况下，参数hwndParent被保存在新建窗口的parent域中，而它的所有者窗口句柄则保存在owner域中。&lt;/P&gt;
&lt;P&gt;（5）缺省情况下，对话框和消息框属于 owned 窗口，除非在创建它们的时候明确给出了WS_CHILD属性，（比如对话框中嵌入对话框的情形）&lt;BR&gt;否则由系统负责给它们指定owner窗口。需要注意的是，一旦创建了owned类型的窗口，就无法再改变其所有关系，因为WIN32没有没有提供改变窗口所有者的方法。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 而且在Win32中，由于有多线程的存在，所以要注意保证父子窗口或者owner/owned 窗口要同属于一个线程。&lt;/P&gt;
&lt;P&gt;（6）对于 WS_CHILD类型的窗口，它的父窗口就是它的所有者窗口。一个子窗口的父窗口也是在CreateWindow函数中用hWndParent参数指定的。子窗口只能在父窗口的客户区中显示，并随父窗口一起销毁。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 子窗口必须有一个父窗口，这是它和overlapped 以及 pop-up 窗口之间的主要区别。父窗口可以是顶级窗口，也可以是其它子窗口。&lt;/P&gt;
&lt;P&gt;&lt;BR&gt;三、几个相关函数的说明&lt;/P&gt;
&lt;P&gt;（1）获取/设置所有者窗口&lt;BR&gt;&amp;nbsp;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; win32 API提供了函数GetWindow函数（GW_OWNER 标志）来获取一个窗口的所有者窗口句柄。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; GetWindow(hWnd, GW_OWNER)永远返回窗口的所有者(owner)。对于子窗口，函数返回 NULL，因为它们的父窗口就相当于所有者（注意，是&amp;#8220;相当于&amp;#8221;）。因为Windows系统没有维护子窗口的所有者信息。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; MFC中则是通过如下函数得到所有者窗口指针：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; _AFXWIN_INLINE CWnd* CWnd::GetOwner() const&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { return m_hWndOwner != NULL ? CWnd::FromHandle(m_hWndOwner) : GetParent(); }&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 从上述代码我们可以看出，它返回的值和GetWindow返回的有所区别，如果当前窗口没有owner，那么将返回它的父窗口指针。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 但是Windows没有提供改变窗口所有者的方法。MFC中则提供了改变所有者的方法：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; _AFXWIN_INLINE void CWnd::SetOwner(CWnd* pOwnerWnd)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { m_hWndOwner = pOwnerWnd != NULL ? pOwnerWnd-&amp;gt;m_hWnd : NULL; }&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 另外，mfc还提供了CWnd::GetSafeOwner( CWnd* pParent, HWND* pWndTop );函数，可以用来得到参数pParent的第一个非child属性的父窗口指针。如果这个参数是NULL，则返回当前线程的主窗口(通过AfxGetMainWnd得到)。框架经常使用这个函数查找对话框或者属性页的所有者窗口。&lt;/P&gt;
&lt;P&gt;（2）获取/设置父窗口&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WIN32 API给出了函数GetParent和SetParent。而mfc也是完全封装了这两个函数：&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; _AFXWIN_INLINE CWnd* CWnd::SetParent(CWnd* pWndNewParent)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { ASSERT(::IsWindow(m_hWnd)); return CWnd::FromHandle(::SetParent(m_hWnd,&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;pWndNewParent-&amp;gt;GetSafeHwnd())); }&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; _AFXWIN_INLINE CWnd* CWnd::GetParent() const&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; { ASSERT(::IsWindow(m_hWnd)); return CWnd::FromHandle(::GetParent(m_hWnd)); }&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 对于SetParent，msdn里面说明了父子窗口必须是同一个进程的。但是由于窗口句柄是系统全局唯一的，不属于同一个进程的情况下，也可以成功调用，但是后果未知。&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; GetParent的返回值比较复杂，对于overlapped类型的窗口，它返回0，对于WS_CHILD类型，它返回其父窗口，对于WS_POPUP类型，它返回其所有者窗口，如果想得到创建它时所传递进去的那个hwndParent参数，应该用GetWindowWord(GWW_HWNDPARENT)函数。&lt;/P&gt;
&lt;P&gt;（3）GetWindowWord(hWnd, GWW_HWNDPARENT)返回一个窗口的父窗口，如果没有，则返回其所有者。&lt;/P&gt;
&lt;P&gt;（4）上面谈到，当一个owner窗口被最小化后，系统自动隐藏它所拥有的窗口。当owner窗口被恢复的时候，系统自动显示它所拥有的窗口。在这两种情况下，系统都会发送（send）WM_SHOWWINDOW消息给被拥有的窗口。某些时候，我们可能需要隐藏 owned窗口，但并不想最小化其所有者窗口，这时候，可以通过ShowOwnedPopups函数来实现，该函数设置或者删除当前窗口所拥有的窗口的WS_VISIBLE属性，然后发送WM_SHOWWINDOW消息更新窗口显示。&lt;/P&gt;
&lt;P&gt;&lt;BR&gt;==========================&lt;BR&gt;iwaswzq 初稿于2005/5/19 ，修改于2006/9/12&lt;BR&gt;&amp;lt;/PRE&amp;gt;&lt;/P&gt;&lt;img src ="http://blog.vckbase.com/iwaswzq/aggbug/22380.html" width = "1" height = "1" /&gt;</description></item><item><dc:creator>九月鹰飞</dc:creator><title>虚拟列表控件</title><link>http://blog.vckbase.com/iwaswzq/archive/2006/07/07/21113.html</link><pubDate>Fri, 07 Jul 2006 03:00:00 GMT</pubDate><guid>http://blog.vckbase.com/iwaswzq/archive/2006/07/07/21113.html</guid><wfw:comment>http://blog.vckbase.com/iwaswzq/comments/21113.html</wfw:comment><comments>http://blog.vckbase.com/iwaswzq/archive/2006/07/07/21113.html#Feedback</comments><slash:comments>9</slash:comments><wfw:commentRss>http://blog.vckbase.com/iwaswzq/comments/commentRss/21113.html</wfw:commentRss><trackback:ping>http://blog.vckbase.com/iwaswzq/services/trackbacks/21113.html</trackback:ping><description>&lt;P&gt;&amp;lt;PRE&amp;gt;&lt;BR&gt;一、什么是虚拟列表控件&lt;/P&gt;
&lt;P&gt;虚拟列表控件是指带有LVS_OWNERDATA风格的列表控件。。&lt;/P&gt;
&lt;P&gt;二、为什么使用虚拟列表控件&lt;/P&gt;
&lt;P&gt;我们知道，通常使用列表控件CListCtrl，需要调用InsertItem把要显示的数据插入列表中，之后我们就不必关心数据在哪里了，这是因为控件自己开辟了内存空间来保存这些数据。现在假设我们要显示一个数据库，里面的信息量很大，有几十万条记录。通常有两种方法解决这个问题：1是仅仅在ListCtrl中插入少量的数据，比如100个，然后通过[上一页][下一页]两个按钮进行控制，某一时刻显示的只是从xxx到xxx+100之间的记录。2是把所有数据全部插入到ListCtrl中，然后让用户通过滚动来查看数据。无疑，很多用户喜欢采用第二种方式，特别是对于已经排序的数据，用户只需用键盘输入某行的开头字符，就可以快速定位到某一行。但是，如果这样做，InsertItem插入数据的过程将是很漫长的，而且用户会看到ListCtrl刷新速度也很慢，而且所有数据都位于内存中消耗了大量的内存，当数据多达上万以后几乎是不能忍受的。&lt;/P&gt;
&lt;P&gt;为此，mfc特别提供了虚拟列表的支持。一个虚拟列表看起来和普通的ListCtrl一样，但是不用通过InsertItem来插入数据，它仅仅知道自己应该显示多少数据。但是它如何知道要显示什么数据呢？秘密就在于当列表控件需要显示某个数据的时候，它向父窗口要。假设这个列表控件包含100个元素，第10到20个元素（行）是可见的。当列表控件重画的时候 ，它首先请求父窗口给它第10个元素的数据，父窗口收到请求以后，把数据信息填充到列表提供的一个结构中，列表就可以用来显示了，显示第10个数据后，列表会继续请求下一个数据。&lt;/P&gt;
&lt;P&gt;在虚拟的样式下，ListCtrl可以支持多达DWORD个数据项。(缺省的listctrl控件最多支持int个数据项)。但是，虚拟列表的最大优点不在于此，而是它仅仅需要在内存中保持极少量的数据，从而加快了显示的速度。所以，在使用列表控件显示一个很大的数据库的情况下，采用虚拟列表最好不过了。&lt;/P&gt;
&lt;P&gt;不仅CListCtrl提供虚拟列表的功能， MFC的CListView类也有同样的功能。&lt;/P&gt;
&lt;P&gt;三、虚拟列表控件的消息&lt;/P&gt;
&lt;P&gt;&lt;BR&gt;虚拟列表总共发送三个消息给父窗口：当它需要数据的时候，它发送LVN_GETDISPINFO消息。这是最重要的消息。当用户试图查找某个元素的时候，它发送LVN_ODFINDITEM消息；还有一个消息是LVN_ODCACHEHINT，用来缓冲数据，基本上很少用到这个消息。&lt;/P&gt;
&lt;P&gt;虚拟列表控件使用起来非常简单。它总共只有三个相关的消息，如果你直接使用CListCtrl，应该在对话框中响应这三个消息。如果你使用CListCtrl派生类，可以在派生类中响应这三个消息的反射消息。这三个消息分别是：&lt;/P&gt;
&lt;P&gt;&amp;nbsp; （1）LVN_GETDISPINFO 控件请求某个数据&lt;BR&gt;&amp;nbsp; （2）LVN_ODFINDITEM&amp;nbsp; 查找某个数据&lt;BR&gt;&amp;nbsp; （3）LVN_ODCACHEHINT 缓冲某一部分数据&lt;/P&gt;
&lt;P&gt;我们必须响应的消息是（1），多数情况下要响应(2)，极少数的情况下需要响应（3）&lt;/P&gt;
&lt;P&gt;四、如何使用虚拟列表控件&lt;/P&gt;
&lt;P&gt;1、首先要创建控件，创建一个虚拟列表和创建一个正常的 CListCtrl差不多。先在资源编辑器里面添加一个list control资源。然后选中"Owner data"属性，然后给它捆绑一个CListCtrl变量。添加列，添加imagelist等都和使用正常的listctrl一样。&lt;/P&gt;
&lt;P&gt;2、给虚拟列表添加元素。假设 m_list 是这个列表的控制变量。通常的情况下这样添加数据：&lt;/P&gt;
&lt;P&gt;m_list.InsertItem(0, _T("Hello world"));&lt;/P&gt;
&lt;P&gt;但是对于虚拟列表，不能这么做。只需告诉列表有多少个数据:&lt;/P&gt;
&lt;P&gt;//总共显示100行&lt;BR&gt;m_list.SetItemCount(100);&lt;/P&gt;
&lt;P&gt;3、处理它的通知消息。&lt;/P&gt;
&lt;P&gt;五、如何响应虚拟列表的消息&lt;/P&gt;
&lt;P&gt;1、处理 LVN_GETDISPINFO 通知消息&lt;/P&gt;
&lt;P&gt;当虚拟列表控件需要某个数据的时候，它给父窗口发送一个 LVN_GETDISPINFO通知消息，表示请求某个数据。因此列表的所有者窗口（或者它自己）必须处理这个消息。例如派生类的情况 (CMyListCtrl是一个虚拟列表类对象):&lt;/P&gt;
&lt;P&gt;//这里处理的是反射消息&lt;BR&gt;BEGIN_MESSAGE_MAP(CMyListCtrl, CListCtrl)&lt;BR&gt;&amp;nbsp;&amp;nbsp; //{{AFX_MSG_MAP(CMyListCtrl)&lt;BR&gt;&amp;nbsp;&amp;nbsp; ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnGetdispinfo)&lt;BR&gt;&amp;nbsp;&amp;nbsp; //}}AFX_MSG_MAP&lt;BR&gt;END_MESSAGE_MAP()&lt;/P&gt;
&lt;P&gt;在LVN_GETDISPINFO的处理函数中，必须首先检查列表请求的是什么数据，可能的值包括: &lt;/P&gt;
&lt;P&gt;（1）LVIF_TEXT&amp;nbsp;&amp;nbsp; 必须填充 pszText&lt;BR&gt;（2）LVIF_IMAGE&amp;nbsp; 必须填充 iImage &lt;BR&gt;（3）LVIF_INDENT 必须填充 iIndent&lt;BR&gt;（4）LVIF_PARAM&amp;nbsp; 必须填充 lParam &lt;BR&gt;（5）LVIF_STATE&amp;nbsp; 必须填充 state &lt;/P&gt;
&lt;P&gt;根据它的请求，填充所需的数据即可。&lt;/P&gt;
&lt;P&gt;//================= 例子代码=====================================&lt;/P&gt;
&lt;P&gt;下面的给出一个例子，填充的是列表所需的某个数据项的文字以及图像信息：&lt;/P&gt;
&lt;P&gt;LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;&lt;BR&gt;LV_ITEM* pItem= &amp;amp;(pDispInfo)-&amp;gt;item;&lt;/P&gt;
&lt;P&gt;int iItemIndx= pItem-&amp;gt;iItem;&lt;/P&gt;
&lt;P&gt;if (pItem-&amp;gt;mask &amp;amp; LVIF_TEXT) //字符串缓冲区有效&lt;BR&gt;{&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; switch(pItem-&amp;gt;iSubItem){&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; case 0: //填充数据项的名字&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; lstrcpy(pItem-&amp;gt;pszText,m_Items[iItemIndx].m_strItemText);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; break;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; case 1: //填充子项1&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; lstrcpy(pItem-&amp;gt;pszText,m_Items[iItemIndx].m_strSubItem1Text);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; break;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; case 2: //填充子项2&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; lstrcpy(pItem-&amp;gt;pszText,m_Items[iItemIndx].m_strSubItem2Text);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; break;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;}&lt;BR&gt;/*注意，多数情况下要使用lstrcpyn ，因为最多复制字符的个数由pItem-&amp;gt;cchTextMax给出：&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; lstrcpyn(pItem-&amp;gt;pszText, text, pItem-&amp;gt;cchTextMax);&lt;BR&gt;*/&lt;/P&gt;
&lt;P&gt;if (pItem-&amp;gt;mask &amp;amp; LVIF_IMAGE) //是否请求图像&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; pItem-&amp;gt;iImage= m_Items[iItemIndx].m_iImageIndex;&lt;/P&gt;
&lt;P&gt;甚至连某行数据是否选中（当有checkbox的情况下）的信息也需要由用户自己来维护，例如：&lt;BR&gt;//是否显示该行的选择信息?&lt;BR&gt;if(IsCheckBoxesVisible()) //自定义函数&lt;BR&gt;{&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; pItem-&amp;gt;mask |= LVIF_STATE;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; pItem-&amp;gt;stateMask = LVIS_STATEIMAGEMASK;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if(m_database[itemid].m_checked)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; pItem-&amp;gt;state = INDEXTOSTATEIMAGEMASK(2);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; else&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; pItem-&amp;gt;state = INDEXTOSTATEIMAGEMASK(1);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;}&lt;/P&gt;
&lt;P&gt;&lt;BR&gt;2、处理 LVN_ODFINDITEM 消息&lt;/P&gt;
&lt;P&gt;在资源管理器里面，定位到某个文件夹，会显示很多文件，如果按下键盘的&amp;#8216;A&amp;#8217;，则资源管理器会自动找到名字以 'A'打头的文件夹或者文件, 并选择该文件。继续按 A，如果还有其它名字以'A'打头的文件，则下一个文件被选中。如果输入 "AB"，则 'AB'打头的文件被选中。这就是列表控件的自动查找功能。&lt;/P&gt;
&lt;P&gt;当虚拟列表收到一个LVM_FINDITEM消息，它也会发送这个消息通知父窗口查找目标元素。要搜索的信息通过 LVFINDINFO 结构给出。它是 NMLVFINDITEM 结构的一个成员。当找到要搜索的数据后，应该把该数据的索引（行号）返回，如果没有找到，则返回-1。&lt;/P&gt;
&lt;P&gt;以对话框为例，响应函数大致如下:&lt;/P&gt;
&lt;P&gt;//================= 例子代码=====================================&lt;BR&gt;void CVirtualListDlg::OnOdfinditemList(NMHDR* pNMHDR, LRESULT* pResult) &lt;BR&gt;{&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; // pNMHDR 里面是要查找的元素的信息&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; // 要选中的目标元素的行号最后要保存在 pResult 中， 这是关键！&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; NMLVFINDITEM* pFindInfo = (NMLVFINDITEM*)pNMHDR;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; /* pFindInfo-&amp;gt;iStart 是查找的起始位置，一直到最后，然后从头开始，如果没有找到合适的，最终停留在iStart*/&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; *pResult = -1;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; //是否按照文字查找？&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if( (pFindInfo-&amp;gt;lvfi.flags &amp;amp; LVFI_STRING) == 0 )&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; //这是我们要找的字符串&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; CString searchstr = pFindInfo-&amp;gt;lvfi.psz;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; int startPos = pFindInfo-&amp;gt;iStart;//保存起始位置&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; //判断是否最后一行&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if(startPos &amp;gt;= m_list.GetItemCount())&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; startPos = 0;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; int currentPos=startPos;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; //开始查找&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; do&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if( _tcsnicmp(m_database[currentPos].m_name, &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; searchstr, searchstr.GetLength()) == 0)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; //选中这个元素，停止查找&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; *pResult = currentPos;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; break;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; currentPos++;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; //从头开始&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if(currentPos &amp;gt;= m_list.GetItemCount())&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; currentPos = 0;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }while(currentPos != startPos);&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;}&lt;/P&gt;
&lt;P&gt;显然，如果数据很多，必须实现一个快速查找的方法。&lt;/P&gt;
&lt;P&gt;关于pFindInfo-&amp;gt;lvfi里面的信息的详细说明可以参考 MSDN。&lt;/P&gt;
&lt;P&gt;3、处理 LVN_ODCACHEHINT 消息。&lt;/P&gt;
&lt;P&gt;假如我们从数据库或者其它地方读取数据的速度比较慢，则可以利用这个消息，批量读取一些数据，然后根据请求，逐个提供给虚拟列表。LVN_ODCACHEHINT消息的用途就是给程序一个缓冲数据的机会。以提高程序的性能。&lt;/P&gt;
&lt;P&gt;//================= 例子代码=====================================&lt;BR&gt;使用 ClassWizard 重载 OnChildNotify 函数，检查是否 LVN_ODCACHEHINT 消息，然后准备缓冲数据：&lt;/P&gt;
&lt;P&gt;NMLVCACHEHINT* pcachehint=NULL;&lt;/P&gt;
&lt;P&gt;NMHDR* phdr = (NMHDR*)lParam;&lt;/P&gt;
&lt;P&gt;if(phdr-&amp;gt;code == LVN_ODCACHEHINT)&lt;BR&gt;{&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; pcachehint= (NMLVCACHEHINT*) phdr;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; //自定义函数，准备指定范围的数据到缓冲区&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; PrepCache(pcachehint-&amp;gt;iFrom, pcachehint-&amp;gt;iTo);&lt;BR&gt;}&lt;BR&gt;else ...&lt;/P&gt;
&lt;P&gt;注意，如果消息不是 LVN_ODCACHEHINT，则要传递给基类进行处理。&lt;/P&gt;
&lt;P&gt;五、如何修改ListCtrl显示的数据。&lt;/P&gt;
&lt;P&gt;由于是程序自己维护数据，所以只需修改数据库中的数据，然后调用CListCtrl::RedrawItems函数进行重画即可。&lt;/P&gt;
&lt;P&gt;六、数据的选择状态和选择框&lt;/P&gt;
&lt;P&gt;CListCtrl可以显示checkbox选择框。有些情况下是很有用的。对于正常的listctrl，用户可以用鼠标来修改某个元素的选择状态，但是对于虚拟列表就不行了。必须自己处理一些消息，然后自己保存数据的选中状态:&lt;/P&gt;
&lt;P&gt;void CVirtualListDlg::ToggleCheckBox(int item)&lt;BR&gt;{&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; m_database[item].m_checked = !m_database[item].m_checked;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; m_list.RedrawItems(item, item);&lt;BR&gt;}&lt;/P&gt;
&lt;P&gt;处理 LVN_KEYDOWN消息，添加对空格键 的响应，用于切换选择状态：&lt;/P&gt;
&lt;P&gt;void CVirtualListDlg::OnKeydownList(NMHDR* pNMHDR, LRESULT* pResult) &lt;BR&gt;{&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; LV_KEYDOWN* pLVKeyDown = (LV_KEYDOWN*)pNMHDR;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if( pLVKeyDown-&amp;gt;wVKey == VK_SPACE )&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; int item = m_list.GetSelectionMark();&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if(item != -1)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ToggleCheckBox(item);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; *pResult = 0;&lt;BR&gt;}&lt;/P&gt;
&lt;P&gt;然后处理 NM_CLICK 消息:&lt;/P&gt;
&lt;P&gt;void CVirtualListDlg::OnClickList(NMHDR* pNMHDR, LRESULT* pResult) &lt;BR&gt;{&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; NMLISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; LVHITTESTINFO hitinfo;&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; hitinfo.pt = pNMListView-&amp;gt;ptAction;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; int item = m_list.HitTest(&amp;amp;hitinfo); &lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; if(item != -1)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; //看看鼠标是否单击在 check box上面了?&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; if( (hitinfo.flags &amp;amp; LVHT_ONITEMSTATEICON) != 0)&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; ToggleCheckBox(item);&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;BR&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; *pResult = 0;&lt;BR&gt;}&lt;/P&gt;
&lt;P&gt;七、备注：&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 1、虚拟列表无法进行排序。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 2、虚表的一个优点是容易保持和数据库的同步。修改数据库中的数据，然后重画list十分容易而且高效。&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; 3、虚表的另一个优点是可以根据需要产生数据。比如在某一列加上行号。&lt;/P&gt;
&lt;P&gt;iwaswzq 2006/6/21 0:16&lt;BR&gt;--------------------------------------------------------------------------------&lt;BR&gt;&amp;lt;/PRE&amp;gt;&lt;/P&gt;&lt;img src ="http://blog.vckbase.com/iwaswzq/aggbug/21113.html" width = "1" height = "1" /&gt;</description></item></channel></rss>