上一次,我們可以擷取到圖片動畫幀之間的時間間隔,如果想讓動畫轉起來,就必須有時鐘。插入的圖片動畫數量可能會比較多,因此要想不影響效能,時鐘必須很輕量級而且要很高效。
Windows平台上實現時鐘的方式五花八門,你可以使用視窗相關的SetTimer來設定一個時鐘,也可以自己開闢線程來做等待觸發類比時鐘,而Chromium封裝的要更加C++對象化一些:依託Windows視窗訊息,抽象出延遲任務的概念。這種手法幾年前我也曾經考慮過,只是對其中下次最短觸發時間計算以及更新的演算法和設計都有力不從心,最終得出的是誤差很大的精簡版:選擇固定的最小時間片為最小觸發單位,對很小的時間間隔誤差很明顯。
Windows有Timer Queues用來實現高效的非同步時鐘,比較奇怪的是這組API用的貌似並不多。我們知道每個進程都有一個預設的線程池,可以在其中執行一些Work Items,時鐘隊列和等待操作也都會用到這個線程池。timer-queue中的timers建立和銷毀都很輕量高效,因此我選擇了它。
每個OLE圖片對象在設定圖片之後,如果發現是多幀的,就需要啟動動畫,建立時鐘:
ATLVERIFY(CreateTimerQueueTimer(&timer_, NULL,
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線程同步操作:
VOID CALLBACK IMRichPicture::WaitOrTimerCallback(PVOID lpParameter,
BOOLEAN TimerOrWaitFired) {
ATLASSERT(TimerOrWaitFired == TRUE);
IMRichPicture::CallbackParameter* parameter =
reinterpret_cast<IMRichPicture::CallbackParameter*>(lpParameter);
ATLASSERT(parameter);
parameter->thread_state->UpdatePictureFrame(parameter->picture);
}
下面是UpdatePictureFrame的實現:
void IMThreadState::UpdatePictureFrame(IMRichPicture* picture) const {
PostMessage(message_window_, kMessageUpdatePictureFrame,
reinterpret_cast<WPARAM>(picture->richedit()),
reinterpret_cast<LPARAM>(picture));
}
這樣繞一大圈子,是為了利用Timer Queues的同時保證圖片的更新操作是在UI線程中執行,因為圖片被插入也是發生在UI線程,即動畫控制項建立於UI線程,為了避免加鎖帶來的麻煩以及死結的可能性,不應該輕易去加鎖,盡量利用作業系統提供的基礎設施來實現。這裡需要注意的是隱藏視窗接收到kMessageUpdatePictureFrame訊息時,richedit視窗可能已不存在或者動畫控制項已經銷毀,因此使用指標前,需要判斷對象是否還存在:
case kMessageUpdatePictureFrame: {
IMRichEditImpl* richedit = reinterpret_cast<IMRichEditImpl*>(wparam);
IMRichPicture* picture = reinterpret_cast<IMRichPicture*>(lparam);
if (IMThreadState::current()->HasRichEdit(richedit))
richedit->OnUpdatePictureFrame(picture);
return 0;
}