10 сентября 2008

Animated GIFs using GDI+

GDI+ как-то поздновато появилась. Именно такой по архитектуре нужно было делать GDI (которая без плюса) изначала. Но, так как изначально GDI сделали как сделали - получили кучу извращений для простой операции - отобразить картинку на экране.

А с GDI+ даже анимированные гифы рисовать - как два пальца. В кратце расскажу как.

Для начала абстрагируемся от GDI+, мало ли потом захотим использовать другую библиотеку. Создадим небольшой интерфейс:
class CImage
{
public:
virtual CSize Size() const = 0;
virtual void Draw(HDC DC, const CRect& DestRect, const CRect& SrcRect) const = 0;
virtual ~CImage() {}

virtual size_t GetFrameCount() const = 0;
virtual void SelectActiveFrame(size_t Frame) = 0;
virtual int /*ms*/ GetFrameDelay(size_t Frame) const = 0;
};
Создадим реализацию этого интерфейса на основе GDI+:
class CGdiPlusImage : public CImage {...}
Уже можно рисовать! Image.Draw(...) и вперед ;) Не забыть только вызвать в нужный момент SelectAcriveFrame() - и все заанимируется сразу.

Один товарисч из Объединенного Королевства предлагает создавать отдельный поток, чтобы там можно было через WaitEvent() дожидаться наступления следующего фрейма. Но зачем нам по отдельному потоку на каждую анимацию? Мы сделаем по-русски - через SetTimer(). Заведем будильник на время, когда нужно будет следующий фрейм отрисовать - и по WM_TIMER отрисуем. Сделаем небольшой helper-класс для этого:
class CAnimation
{
public:
// all time/reriods are in ms
CAnimation(CImage& Image, DWORD Time = timeGetTime());

typedef std::pair<size_t /*current frame*/, DWORD /*period till next frame*/> TInfo;
TInfo CurrentInfo(DWORD Time = timeGetTime());

size_t FrameByTime(DWORD Time = timeGetTime());
DWORD PeriodTillNextFrame(DWORD CurrentTime = timeGetTime());

private:
size_t nFrames_;
DWORD Start_;
typedef std::vector<DWORD> TFrameEnds;
TFrameEnds FrameEnds_;
};
Этот helper выдаст нам:
1) какой фрейм показывать на такой-то момент времени (чтобы в любой момент времени при отрисовке мы знали какой фрейм рисовать)
2) сколько осталось до показа следующего фрейма (чтобы знать на сколько нам будильник заводить).
Выдаст нам это либо по-отдельности, либо сразу (в виде TInfo).

Осталось реализовать получение этого самомго TInfo. Для этого в самом начале посчитаем когда какой фрейм заканчивается:
DWORD LastEnd = 0;
for (size_t nFrame = 0; nFrame < nFrames_; ++nFrame)
{
LastEnd += Image.GetFrameDelay(nFrame);
FrameEnds_[nFrame] = LastEnd;
}
тогда потом легко найдем нужный фрейм простым двоичным поиском:
DWORD FullCyclePeriod = FrameEnds_.back();
DWORD NormalizedTime = (Time - Start_) % FullCyclePeriod;
TFrameEnds::iterator Current(std::upper_bound(FrameEnds_.begin(), FrameEnds_.end(), NormalizedTime));
return TInfo(Current - FrameEnds_.begin(), *Current - NormalizedTime);
И все! Теперь при старте заводим будильник на начало второго фрейма, а при звоне будильника делаем invalidate и заводим будильник на начало следующего фрейма - и т.д. по кругу:
void OnCreate()
{
SetTimer(TimerId, Animation.PeriodTillNextFrame());
}

void OnTimer()
{
CAnimation::TInfo AnimationInfo(Animation.CurrentInfo());
Image.SelectActiveFrame(AnimationInfo.first);
SetTimer(TimerId, AnimationInfo.second);
Invalidate();
return 0;
}

Полный код примера.zip