上一次,我们可以获取到图片动画帧之间的时间间隔,如果想让动画转起来,就必须有时钟。插入的图片动画数量可能会比较多,因此要想不影响性能,时钟必须很轻量级而且要很高效。
Windows平台上实现时钟的方式五花八门,你可以使用窗口相关的SetTimer来设置一个时钟,也可以自己开辟线程来做等待触发模拟时钟,而Chromium封装的要更加C++对象化一些:依托Windows窗口消息,抽象出延迟任务的概念。这种手法几年前我也曾经考虑过,只是对其中下次最短触发时间计算以及更新的算法和设计都有力不从心,最终得出的是误差很大的精简版:选择固定的最小时间片为最小触发单位,对很小的时间间隔误差很明显。
Windows有Timer Queues用来实现高效的异步时钟,比较奇怪的是这组API用的貌似并不多。我们知道每个进程都有一个默认的线程池,可以在其中执行一些Work Items,时钟队列和等待操作也都会用到这个线程池。timer-queue中的timers创建和销毁都很轻量高效,因此我选择了它。
每个OLE图片对象在设置图片之后,如果发现是多帧的,就需要启动动画,创建时钟:
WaitOrTimerCallback,
callback_parameter_.get(),
image_->GetFrameDelay(current_frame_),
0, WT_EXECUTEDEFAULT));
这里timer_是返回值,返回新建的时钟对象,可以在OLE对象销毁或者回调函数中进行删除,而删除操作会等待回调执行完毕才返回。传递TimerQueue为NULL表示使用系统的队列。Period为0表示只触发一次,触发时间为image_->GetFrameDelay(current_frame_)。由于回调函数WaitOrTimerCallback是在线程池的线程中执行,所以更新操作需要同步到动画图片的创建线程中。callback_parameter_包含有上一节提及的ThreadState对象以及动画OLE对象指针,ThreadState创建的时候会同时创建一个隐藏窗口用于工作者线程向UI线程同步操作:
BOOLEAN TimerOrWaitFired) {
ATLASSERT(TimerOrWaitFired == TRUE);
IMRichPicture::CallbackParameter* parameter =
reinterpret_cast<IMRichPicture::CallbackParameter*>(lpParameter);
ATLASSERT(parameter);
parameter->thread_state->UpdatePictureFrame(parameter->picture);
}
下面是UpdatePictureFrame的实现:
PostMessage(message_window_, kMessageUpdatePictureFrame,
reinterpret_cast<WPARAM>(picture->richedit()),
reinterpret_cast<LPARAM>(picture));
}
这样绕一大圈子,是为了利用Timer Queues的同时保证图片的更新操作是在UI线程中执行,因为图片被插入也是发生在UI线程,即动画控件创建于UI线程,为了避免加锁带来的麻烦以及死锁的可能性,不应该轻易去加锁,尽量利用操作系统提供的基础设施来实现。这里需要注意的是隐藏窗口接收到kMessageUpdatePictureFrame消息时,richedit窗口可能已不存在或者动画控件已经销毁,因此使用指针前,需要判断对象是否还存在:
IMRichEditImpl* richedit = reinterpret_cast<IMRichEditImpl*>(wparam);
IMRichPicture* picture = reinterpret_cast<IMRichPicture*>(lparam);
if (IMThreadState::current()->HasRichEdit(richedit))
richedit->OnUpdatePictureFrame(picture);
return 0;
}