回 帖 发 新 帖 刷新版面

主题:_____DShow____开发笔记(二)

.5构建一个Filter Graph图
1.6数据流在Filter Graph里的流动(Data Flow)
1 directshow数据流动概述
数据总是存在内存块中的字节集合,每个buffer都被封装在一个叫做media sample的com组件,它引出了IMediaSample接口。这个sample一般都有一个叫做内存分配器(alloctor)的com对象来创建,这个对象具有IMemAllocator接口。每一个pin之间的连接都要指定一个allocator,有时也有几个连接同用一个allocator。
 
   每一个allocator都要创建一个media sample池,并且给每一个sample分配一个内存buffer。每当一个Filter需要一个buffer来填充数据,它就通过allocator的函数IMemAllocator::GetBuffer.来获得一个sample。如果分配器allocator正好有空闲的sample,GetBuffer立即返回一个指向该sample的指针。如果没有空闲的sample,该方法就阻塞,直到有一个sample可用为止。当该函数返回一个sample时,Filter就将数据填充到buffer里,设置好标识,然后就将sample传递给下一个Filter。
当一个renderfilter接收到一个sample时,它就检查该sample的时间戳,直到Fliter Graph的参考时钟表明该数据可用播放,Filter就开始播放该数据。当数据播放完毕,Filter释放sample,直到所有的Filter都释放对该sample的引用,该sample的引用计数为0时,这个sample才返回到sample池。
 
  有时也许数据流的上游对buffer的填充比播放要快,即使这样,render Filter也要按照时间戳播放数据,这样sample池中的sample数量就少,从而填充的速度减慢。
上面描述了在流中只有一个allocator的情景,实际上,在每条数据流中总是有好几个allocator,当一个sample被释放的时候,也许此时有好几个allocator都在等着该sample,这就有新的问题了,也许有的alloctor永远都不能被分配sample,陷入互锁状态。下面的图就演示了这种情形,Decoder有数据需要压缩,因此它在等待Renderer释放sample,但是,Parser也在请求sample,它在等待decoder释放sample。
 
具体参加help
2 传输(Transports)
为了在过滤器图表中传送媒体数据,DirectShow过滤器需要支持一些协议,称之为传输协议(transport)。相连的过滤器必须支持同样的传输协议,否则不能交换媒体数据。
大多数的DirectShow过滤器把媒体数据保存在主存储器中,并通过引脚把数据提交给其它的过滤器,这种传输称为局部存储器传输(local memory transport)。虽然局部存储器传输在DirectShow中最常用,但并不是所有的过滤器都使用它。例如,有些过滤器通过硬件传送媒体数据,引脚只是用来提交控制信息,如IOverlay接口。
DirectShow为局部存储器传输定义了两种机制:推模式(push model)和拉模式(pull model)。在推模式中,源过滤器生成数据并提交给下一级过滤器。下一级过滤器被动的接收数据,完成处理后再传送给再下一级过滤器。在拉模式中,源过滤器与一个分析过滤器相连。分析过滤器向源过滤器请求数据后,源过滤器才传送数据以响应请求。推模式使用的是IMemInputPin接口,拉模式使用IAsyncReader接口,推模式比拉模式要更常用。
3 Samples和Allocators
当一个pin向另一个pin传递数据的时候,它并不是直接将内存块的指针传递下一个pin,实际上,它将传递一个管理内存的com对象的指针给下一个pin。这个com对象称为media sample。暴露了IMediaSample接口。接收pin通过调用IMediaSample的方法来对内存进行操作,比如方法IMediaSample::GetSize, IMediaSample::GetActualDataLength以及IMediaSample::GetPointer。
   Sample一般都是从源filter开始,通过输出pin传递到下一个filter的输入pin,一路传递下去一直到render filter。在拉模式中,输出pin通过调用输入pin上的IMemInputPin::Receive方法传递sample,输入pin或者在Receive函数同步处理数据,或者另外采用一个工作线程异步出来的方式。如果在Recive方法中需要等待资源的话,也可以阻塞。
另外一个com对象,叫做allocator,用来创建和管理sample的。暴露了IMemAllocator接口。当一个filter需要一个空的buffer的时候,就可以调用IMemAllocator::GetBuffer,该方法返回一个指向sample的指针。每一个pin连接都共享一个allocator,当两个pin连接的时候,他们会决定由哪个filter来提供allocator,通过pin还可以设置allocator的属性,例如,buffer的数量和大小。
下面的图表显示了allocator,sample和filter之间的关系。
 
Media sample引用计数
Allocator创建了一个sample池。因此 ,当某个Filter调用GetBuffer函数时,一些sample被使用,其他空闲的sample可以响应。Allocator通过引用计数来跟踪samples。Filter调用Getbuffer返回的sample的引用计数是1。当sample的引用计数为0时,sample就返回内存池,成为空闲的sample,可以再次响应Getbuffer的调用。如果所有的sample都处于繁忙状态,Getbuffer就会阻塞,直到有一个sample空闲。
例如,假设一个输入pin接到一个sample,如果它在Receive方法里同步的处理这个sample,没有增加该sample的引用计数,等到Receive返回后,输出pin就释放这个sample,引用计数为0,sample就返回到内存池中。 如果输入pin的线程还要处理该sample,引用计数增加1,成为2,输出pin返回,释放,计数成1。
当一个输入pin接收一个sample时,它可以将数据复制到另一个sample中,也可以将这个sample传递到下一个Filter。一个sample可以流遍整个filter graph。不过引用计数要保持大于0。 当一个输出pin调用了Release以后,就不应该再次使用该sample,因为也许下游还有filter正在使用该sample。输出pin必须调用GetBuffer获取新的sample。
这种机制减少 了内存分配的,因为buffer可以重用。也防止了数据没有被处理的sample被重新写入。
当一个Filter创建一个allocator的时候,allocator还没有保留任何的内存,如果这个时候有人Getbuffer,就会失败。只有当数据流开始的时候,输出pin调用IMemAllocator::Commit,提交allocator,现在才能分配内存。
当数据流停止的时候,pin就调用IMemAllocator::Decommit,来销毁allocator。在allocator再次committ之前,所有调用GetBuffer方法都会失败。当然,如果有一个GetBuffer阻塞调用在等待sample的时候,遇到Decommit方法,会立即返回一个错误码。
4 Filter的状态
Filter有三种状态,停止,暂停,运行。
 过滤器图表管理器 控制着Filter的所有状态的转换。当应用程序调用IMediaControl::Run, IMediaControl::Pause, or IMediaControl::Stop时, 过滤器图表管理器就调用Filter相应的IMediaFilter方法。停止,运行状态的切换总是要经过暂停,因此,当一个应用程序对一个停止的Graph 调用RUN命令时,过滤器图表管理器  在run之前首先要暂停。
对于大多数的filter来说,running和paused状态是一样的。看下面的Graph
Source > Transform > Renderer

当一个Filter停止时,它拒绝发送给它的任何samples,源filter关闭他们的stream线程,其他filter也关闭他们创建的其他线程,pin  decommit他们的内存分配器。
过滤器图表管理器按照逆流的方向来切换Filter的状态,从Renderer Filter到源filter,这种方式可以防止死锁。最关键的状态切换是暂停和停止之间。
从停止到暂停,当filter暂停时,它就做好了接收sample的准备,源filter是最后一个切换到暂停的。它开始创建streaming线程,发送sample,因为下游的filter的状态都已经切换到暂停了,所以,所有的filter都可以接收sample。只有当所有的flter都接收到sample,过滤器图表管理器才算完成了状态的切换
从暂停到停止。当一个filter停止时,它要释放它拥有的所有的samples。当图表管理器试图停掉上游的一个filter时,这个filter不会阻塞在Getbuffer和receive方法里,它会立即响应stop命令。上游的filter也许在执行stop命令前还会讲少量的sample传递下去,但是下游的filter会拒绝的,因为他们已经停止了。
5拉PULL模式
在IMemInputPin接口中,上游的flter决定了发送什么样的数据,然后将数据推给下游的filter。但在另外的场合,拉模式更适合。下游的filter向上游的filter请求数据,数据依然是从上游到下游,从输出pin到输入pin,但是下游的filter主导着数据的流动。这种类型的连接采用的是IAsyncReader接口
   拉模式的典型应用的文件的回放,例如在一个AVI文件的回放graph中,Async File Source
Filter就担负着从文件中读取数据,然后将数据以字节流的方式发送给下面的filter。
1.7事件通知机制(Event Notification)
1概述
当某个事件发生时,比如数据流结束,产生一个错误等,Filter就给Filter图表管理器发送一个事件通知。Filter图表管理器处理其中的一部分事件,另一部分交给应用程序处理。如果图表管理器没有处理一个filter事件,它就把事件通知放入到一个队列中,图表管理器也可以将自己的事件通知放进队列中。
应用程序可以自己处理队列中的事件,dshow中的事件通知就和windows的消息机制差不多,filter,图表管理器和应用程序通过这种机制就可以互相通信。

回复列表 (共13个回复)

沙发

2 Retrieving Events

  Filter图表管理器暴露了三个接口用来处理事件通知
IMediaEventSink Filter用这个接口来post事件。
IMediaEvent 应用程序利用这个接口来从队列中查询消息
IMediaEventEx 是imediaevent的扩展。
Filter都是通过调用图表管理器的 IMediaEventSink::Notify方法来通知图表管理器某种事件发生。事件通知包括一个事件code,这个code不仅仅代表了事件的类型,还包含两个DWORD类型的参数用来传递一些其他的信息。
关于事件code的内容,在下面的一个专题中列出,这里暂略,使用时可以参考帮助。
 应用程序通过调用图表管理器的IMediaEvent::GetEvent方法来从事件队列中获取事件。如果有事件发生,该函数就返回一个事件码和两个参数,如果没有事件,则一直阻塞直到有事件发生和超过某个时间。调用GetEvent函数后,应用程序必须调用IMediaEvent::FreeEventParams来释放事件码所带参数的资源。例如,某个参数可能是由filter graph分配的内存。
下面的代码演示了如何从事件队列中提取事件
long evCode, param1, param2;
HRESULT hr;
while (hr = pEvent->GetEvent(&evCode, &param1, &param2, 0), SUCCEEDED(hr))
{
    switch(evCode) 
    { 
        // Call application-defined functions for each 
        // type of event that you want to handle.
    } 
    hr = pEvent->FreeEventParams(evCode, param1, param2);
}
为了重载Filter图表管理器对事件的缺省处理,你可以使用某个事件码做参数调用IMediaEvent::CancelDefaultHandling ,这样就可以屏蔽图表管理器对某个事件码的处理了。如果要恢复图表管理器对该事件码的缺省处理,可以调用  IMediaEvent::RestoreDefaultHandling。如果图表管理器对某个事件码没有缺省的处理,调用这两个函数是不起作用的。

3事件是如何发生的

为了处理事件,应用程序需要一种机制来获取正在队列中等待的事件。Filter图表管理器提供了两种方法。
1 窗口通知,图表管理器发送开发者自己定义的窗口消息
2 事件信号 如果队列中有dshow事件,就用事件信号通知应用程序,如果队列为空就重新设置事件信号。
下面的代码演示了如何利用消息通知
#define WM_GRAPHNOTIFY WM_APP + 1   // Private message.
pEvent->SetNotifyWindow((OAHWND)g_hwnd, WM_GRAPHNOTIFY, 0);
然后在窗口消息处理过程中处理该消息如下
LRESULT CALLBACK WindowProc( HWND hwnd, UINT msg, UINT wParam, LONG lParam)
{
    switch (msg)
    {
        case WM_GRAPHNOTIFY:
            HandleEvent();  // Application-defined function.
            break;
        // Handle other Windows messages here too.
    }
    return (DefWindowProc(hwnd, msg, wParam, lParam));
}
由于事件通知和窗口的消息循环都是异步的,因此,当你的应用程序处理消息的时候,队列中或许有N个事件等待处理。因此,在你调用GetEvent的时候,一定要循环调用,直到返回一个错误码,这表明队列是空的。
当你释放IMediaEventEx 指针时,你可以调用SetNotifyWindow来取消事件通知,记住此时要给这个函数传递一个NULL指针。在你的事件处理程序中,在调用GetEvent之前一定要检查IMediaEventEx指针是否为空,这样就可以避免错误。
下面看看采取事件信号的通知方式。
在Filter图表管理器里有一个手动设置的Event内核对象,用来反映事件队列的状态。如果队列中有等待处理的事件,event就处于通知状态,如果队列是空的,IMediaEvent::GetEvent函数调用就会重置该event对象。
应用程序可以调用IMediaEvent::GetEventHandle获得event内核对象的句柄,然后就可以调用WaitForMultipleObjects来等待事件的发生,如果event被通知了,就可以调用IMediaEvent::GetEvent来获得dshow的事件。
下面的代码演示了如何利用event内核对象来获取EC_COMPLETE事件,
HANDLE  hEvent; 
long    evCode, param1, param2;
BOOLEAN bDone = FALSE;
HRESULT hr = S_OK;
hr = pEvent->GetEventHandle((OAEVENT*)&hEvent);
if (FAILED(hr)
{
    /* Insert failure-handling code here. */
}
while(!bDone) 
{
    if (WAIT_OBJECT_0 == WaitForSingleObject(hEvent, 100))
    { 
        while (hr = pEvent->GetEvent(&evCode, &param1, &param2, 0), SUCCEEDED(hr)) 
        {
            printf("Event code: %#04x\n Params: %d, %d\n", evCode, param1, param2);
            pEvent->FreeEventParams(evCode, param1, param2);
           switch (evCode)
          { 
           case EC_COMPLETE:  // Fall through.
           case EC_USERABORT: // Fall through.
          case EC_ERRORABORT:
            CleanUp();
            PostQuitMessage(0);
            return;
         }
    }

事件通知码(Event Notification Codes)
暂略,需要补充

板凳

1.8Directshow中的时钟(Time and Clocks in Dshow)
1参考时钟:
参考时钟是Filter用来同步Filter 图表管理器中的所有的Filter的。
任何一个引出IReferenceClock 接口的对象都可以作为参考时钟。参考时钟可以是Filter提供,例如声卡就可以提供一个硬件的时钟。当然,可靠的时钟就是采用系统的时间。
名义上,参考时钟的精确度在100纳秒,但实际上,没有那么精确。调用IReferenceClock::GetTime可以获取时钟的当前时间。
尽管时钟的精确性还有所变动,但是GetTime方法返回的保证时间是增加的。也就是说,时钟不会倒退回去,比如,对硬件时钟进行了调整,GetTime方法就返回上次的时间。
缺省的参考时钟
 当Graph运行的时候,Filter图表管理器会自动选择一个参考时钟的,选择时钟的法则如下
1 如果应用程序选择了时钟,就采用应用程序选择的时钟
2 如果Graph包含一个活动的源Filter,这个filter有IReferenceClock接口,那么就用这个时钟。
3 如果Graph中不含有任何活动的源Filter,就选用graph中任何暴露IReferenceClock接口的Filter,选择的方法是从Renderers逆流向上,连接的filter优先,没有连接的filter次之。
4如果没有任何filter符合条件,就采用系统参考时钟System Reference Clock
设置参考时钟
如果你想为graph设置新的时钟,应用程序可以调用图表管理器的接口IMediaFilter::SetSyncSource方法来选择一个参考时钟。
 如果你给SetSyncSource传递的参数为NULL,Graph就不设置任何的参考时钟了。如果想恢复缺省的时钟,调用IFilterGraph::SetDefaultSyncSource方法。
当graph的参考时钟改变时,Graph通过the reference clock changes, the Filter Graph Manager notifies each filter by calling its IMediaFilter::SetSyncSource通知所有的Filter。
2 Clock Times
Directshow定义了两个相关的时间,参考时钟和 数据流时间
参考时间是参考时钟的绝对时间
数据流时间和graph开始的时间有关。
 当graph正在运行,流时间就等于从开始时间计数的时间,当graph暂停,流时间就等于它暂停开始的时间,当graph停止时,流时间不确定。
当一个sample具有时间戳t,就意味着这个sample应该在流时间t播放,因此,流时间也叫播放时间。
当应用程序通过IMediaControl::Run来运行graph时,在graph内部也调用了IMediaFilter::Run
3时间戳
时间戳采用的是流时间,它在sample标上开始和结束时间。时间戳也叫播放时间,通过后面的知识你会了解到,并不是所有格式的数据流都采用同一种样式的时间戳。
 当renderer Filter接收到sample,它会根据sample的时间戳进行排序,等到该sample的播放时间到了,就开始播放该sample,从到达到开始播放的时间,可以通过IReferenceClock::AdviseTime获得。如果sample来晚了,或者sample没有时间戳,filter就立即播放sample。
源Filter通过下面的规则来负责给sample设置时间戳
1 文件重播
第一个sample的时间戳为0,随后的时间戳根据sample的大小和播放的速率来确定。这都由分解文件的Filter来计算和确定,例如AVI Splitter
2 视频和音频的捕捉
 所有的sample在捕捉的时候就被打上时间戳了,时间等于流时间
3混和Filter
根据输出数据流的格式,混和filter也许需要时间戳,也许不需要,
可以通过调用IMediaSample::SetTime来给sample设置时间
4活动的源 Live Source
活动的源Filter,就是推模式的源,实时的接收数据。视频捕捉和网络广播就是例子,活动得源无法控制数据流得速率。
下面的Filter通常被认为是活动的源filter
Filter调用IAMFilterMiscFlags::GetMiscFlags方法时返回
AM_FILTER_MISC_FLAGS_IS_SOURCE,并且至少有一个输出pin暴露了IAMPushSource接口
2 Filter暴露IKsPropertySet接口,并且有个捕捉pin(PIN_CATEGORY_CAPTURE)
如果活动的源能够提供参考时钟,那么Graph首先采用。
反应时间(Latency)
过滤器(filter)的反映时间就是Filter处理sample所花费的时间。对于活动的源filter,反应时间由容纳sample的内存大小决定。例如,假设一个filter有一个视频源具有33ms的反应时间,一个音频源有500ms的反应时间,每一个视频祯(video frame)比相应的音频sample要早470ms,除非graph进行补偿,否则视频和音频是不同步的。
活动的源可以通过IAMPushSource接口来进行同步。除非应用程序调用IAMGraphStreams::SyncUsingStreamOffset方法对源进行同步,一般来说Filter图表管理器是不会对源进行同步的。图表管理器对源进行同步时,向各个源filter查询IAMPushSource 接口,如果filter支持IAMPushSource  ,图表管理器就会通过IAMLatency::GetLatency来得到filter期望的反应时间。注:IAMPushSource继承与 IAMLatency。通过组合这些反应值,filter图表管理器Graph最大反应时间,然后调用IAMPushSource::SetStreamOffset给每个源filter设置一个数据流偏移时间,当filter给它产生的sample打时间戳的时候,要加上偏移时间的。
现在,VFW Capture filter 和 Audio Capture filter.都支持IAMPushSource接口。
速率匹配(Rate Matching)
当renderer Filter利用参考时钟安排播放顺序的时候,如果源filter采用另一种时钟,在重放的时候就会发生故障。播放的速度大于源产生的速度,就会产生间隙停顿,或者播放速度小于源的产生速度,就会形成数据的堆积,造成内存出错。源一般来说是无法控制数据的产生速度的,因此,播放速度要随着源的速度改变而改变。
现在,只有在音频播放filter才能进行速率匹配,因为音频中的glitches比视频中的更容易捕捉到。为了匹配音频播放速率,要注意下列事情。
1 如果graph没有使用参考时钟,没法进行速率匹配。
2 上游要有一个活动的源
3 源filter的输出pin要支持IAMPushSource接口,当请求IAMPushSource::GetPushSourceFlags.要返回下面的值
AM_PUSHSOURCECAPS_INTERNAL_RM
AM_PUSHSOURCECAPS_NOT_LIVE
AM_PUSHSOURCECAPS_PRIVATE_CLOCK
4 如果GetPushSourceFlags返回0,播放filter就根据graph时钟和sample的时间戳来自己决定播放速率

3 楼

1.9动态删除或增加Filter(Dynamic Graph Building)
 在进行pin连接的时候,应用程序一般都要讲graph停掉。但是,一些filter支持pin的动态连接。
 
如上图,我们想将Filter 2动态移走。有两个必要条件:(1)Filter 3 (pin D)必须支持IPinConnection接口(这个接口能够保证Filter在非Stopped状态下也能进行Pin的重连);(2)Filter在重连的时候不允许数据的传输,所以要将数据线程阻塞。如果“重连”是由Filter 1发起的(在Filter内部完成),那么Filter 1要同步这个数据发送线程;如果“重连”由应用程序来完成,则要求Filter 1 (pin A)实现IPinFlowControl接口。
动态重连的一般步骤如下:
(1)在Filter 1(pin A)上阻塞数据流线程。
IPinFlowControl::Block可以工作在同步和异步两种模式下。不要在应用程序主线程下使用该Block函数的同步模式,因为这样可能会引起线程死锁。要么另外使用一个worker thread,要么使用Block函数的异步模式。如下:
 // Create an event
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
 if (hEvent != NULL)
 {
    // Block the data flow.
hr = pFlowControl->Block(AM_PIN_FLOW_CONTROL_BLOCK, hEvent);
if (SUCCEEDED(hr))
{
 // Wait for the pin to finish.
DWORD dwRes = WaitForSingleObject(hEvent, dwMilliseconds);
 }
}
(2)重连Pin A和Pin D,必要时插入新的Filter。
这里Pin的重连可以使用IGraphConfig::Reconnect或IGraphConfig::Reconfigure(IGraphConfig接口可以从Filter Graph Manager上获得)。Reconnect比Reconfigure使用起来要简单,它主要作如下几件事:将Filter 2置于Stopped状态,然后将其移走;加入新的Filter;重新连接相关的各个Pin;将新加入的Filter置于Paused或Running状态,以使其与Filter Graph同步。示例如下:
pGraph->AddFilter(pNewFilter, L"New Filter for the Graph");
pConfig->Reconnect(
    pPinA,      // Reconnect this output pin...
    pPinD,      // ... to this input pin.
    pMediaType, // Use this media type.
    pNewFilter, // Connect them through this filter.
    NULL, 
    0);     
实际应用中,如果你觉得Reconnect不够灵活,还可以改用Reconfigure。使用Reconfigure方法,你必须在你的应用程序里实现IGraphConfigCallback接口;在Reconfigure调用之前,还必须依次调用Filter 3(pin D)上的IPinConnection::NotifyEndOfStream和Filter 2(pin B)上的IPin::EndOfStream,以使得还没处理完的数据全部发送下去(这些处理IGraphConfig::Reconnect会自动给我们完成)。
(3)再次启动Filter 1(pin A)上的数据发送线程。
只需调用IPinFlowControl::Block,如下:pFlowControl->Block(0, NULL);  
过滤器链(Filter Chains)
首先要弄明白什么是Filter Chain。见下图:
 
1 Filter Chain是相互连接着的一条Filter链路,并且链路中的每个Filter至多有一个Input pin,至多有一个Output pin;
2 这条Filter链路中的数据流不依赖于链路外的其他Filter。
如上图中,A–B,C–D,F–G–H,F–G,G–H都是Filter Chain,同时Filter链也可以只包括一个filter,因此A,B,C,D,E,F,G也都是独立的链,因为E含有两个输入pin,因此任何含有E的都不是Filter Chain。
Filter Chain通过IFilterChain接口来操作的,该接口可以从Filter Graph Manager上获得。
IFilterChain提供了下面的方法用来操作filter链。
IFilterChain::StartChain      开始一个链条
IFilterChain::StopChain       停止一个链条
IFilterChain::PauseChain      暂停一个链条
IFilterChain::RemoveChain    将一个链条从graph中删除
并没有一个特殊的方法用来添加一个chain,它和正常的添加filter的方法一样,首先用IFilterGraph::AddFilter 在graph中添加一个filter,然后就是IGraphBuilder::Connect, IGraphBuilder::Render等诸如此类的方法。
当Graph在运行的时候,Filter Chain可以在Running和Stopped状态之间切换;当Graph在暂停状态下,Filter Chain可以在Paused和Stopper状态之间切换。以上是Filter Chain仅有的两种状态转换。
Filter Chain的使用规则
当你使用IFilterChain的方法时,你一定要确保graph中的filter都支持这个接口,否则的话你也可能会造成死锁和graph错误。
下面将教给你如何正确使用filter chain
1 在链条的状态改变之前,在链条边界的数据处理必须完成。下面的函数可以完成这些事情:IMemInputPin::Receive, IPin::NewSegment, and IPin::EndOfStream。
Filters in the chain must return from calls to these methods made by filters outside the chain; and filters outside the chain must return from calls made by filters within the chain.
例如:
2 链中的所有的filter必须对链条的状态改变做出反应
3只有当Filter支持动态断开的时候才能删除有个链条。

4 楼


 m_teacher_allInfo.Open();
    if(m_teacher_allInfo.GetRecordCount()<=0)record=1;    
    else
{m_pRecordset->MoveLast();
    m_teacher_allInfo.MoveLast();    
    record=atoi(m_teacher_allInfo.m_column1)+1;

5 楼

HWND hWnd = ::FindWindow("类名",NULL);
          ::SendMessage(hWnd,WM_CLOSE,NULL,NULL);

6 楼

17746801

7 楼

怎么没人关注呀
还有看的没有,真失望,,要不要继续发了,,朋友们!

8 楼

好东西,顶...

9 楼

10 楼

天热,贴冷,只能顶,,,,,

我来回复

您尚未登录,请登录后再回复。点此登录或注册