《Symbian OS Internal》 -- 窗口服务器(二)


11.7 一个简单的动画DLL
在这部分,我将开发一个人简单的动画DLL,我不会尝试真实得字符识别,但是我会演示所有周边的框架,
包括得到指针事件,绘制墨水到屏幕及发送一个字符事件到程序。我的目的在于解释动态DLL的基本,
特别是它能够怎样的处理事件。

我们原本在Symbian OS v5中为时钟设计了anim DLL,这一次它提供了两个主要特征:
1.准确的事件信息。对相关CTimer的简单使用,例如,提供比真实时间更新更慢的时钟
2.绘制窗口的用户代码能力,当执行在同WSERV同一个线程内时,避免由IPC导致的延迟。

11.7.1 创建一个anim DLL
一个anim DLL有两个部分:anim DLL本身,其实是WSERV的一个插件,以及用来载入和控制anim DLL的客户端代码。
在MMP构建文件中给一个anim DLL标准的“DLL”类型是可能的,但是要正确的设置它,你需要做更多的工作。
最好定义ANI类型
TARGETTYPE ANI
客户端代码调用下面的函数来载入anim DLL:
TInt RAnimDll::Load(const TDesC& &aFileName)
WSERV接着相在anim DLL中调用序数1作为响应。这个函数应该返回CAnimDll的一个子类给服务器:
EXPORT_C CAnimDll* CreateCAnimDllL()
{
return new(ELeave) CHandWritingAnimDll();
}
这个CAnimDll 的子类一般只有一个函数,用来创建一个anim DLL的插件对象:
class ChandWritingAnimDll:public CAnimDll
{
public://从CAnimDLL继承的纯虚函数
 CAnim* CreateInstanceL(TInt aType);
};

每一个anim DLL都可以支持很多不同类别的动画DLL对象,每一个又可以实例化多次。
但是在我的例子中,手写动画DLL只提供一个那种对象,所以这个函数的实现十分简单:
CAnim* CHandWritingAnimDll::CreateInstanceL(TInt)
{
return new(ELeave) CHandWritingAnim();
}

CreateInstanceL()被调用以响应4个重载的RAnim::Construct()客户端函数之一:
TInt Construct(const RWindowBase &aDevice, TInt aType,
const TDesC8 &aParams);
TInt Construct(const RWindowBase &aDevice, TInt aType,
const TDesC8 &aParams, const TIpcArgs& aIpcArgs);
TInt Construct(const RWsSprite &aDevice, TInt aType,
const TDesC8 &aParams);
TInt Construct(const RWsSprite &aDevice, TInt aType,
const TDesC8 &aParams, const TIpcArgs& aIpcArgs);

11.7.2 动态DLL的两种类别
原本,动态DLL仅提供每anim对象绘制一个单个窗口的能力。这一类anim DLL现在被称作“窗口anim”,
要创建这样的一个DLL,你必须从CreateInstanceL()返回一个派生于CWindowAnim的对象。

为提供屏幕上的数字墨水,我们开发一个新类型的anim允许画到一个精灵上。这被称作“精灵anim”,
为创建一个这样的DLL吗、,你必须从CreateInstanceL()返回一个派生于CSpriteAnim的对象。
他们的关系如下图11.2所示:

在anim接口中有两类函数。首先,是WSERV在anim中调用的函数。这被现实在图表中作为MEventHandler,CAnim,
CSpriteAnim和CWindowAnim的成员函数;他们都是纯虚函数,所以anim作者必须为这些函数提供实现,根据类的派生情况。

一个anim可以调用的WSERV函数使用成员数据的形式提供。总共有4个,显示在下面,并从属于括号内:
1.iFunction(MAnimGeneralFunctions)
2.iSpriteFunction(MAnimSpriteFunctions)
3.iWindowFunctions(MAnimWindowFunctions)
4.iGc(CAnimGc)

CAnimGc 提供绘制函数,窗口anim用来绘制他的窗口。

这样我们的例子CHandleWritingAnim是CSpriteAnim的一个子类。

11.7.3 一个精灵anim必须提供的函数
所有CSpriteAnim的派生类必须提供该类的所有虚函数,CAnim和MEventHandler,这样我们类的部分定义如下:
class CHandWritingAnim : public CSpriteAnim
{
public:
~CHandWritingAnim();
//pure virtual functions from CSpriteAnim
void ConstructL(TAny* aArgs);
//pure virtual functions from MEventHandler
TBool OfferRawEvent(const TRawEvent& aRawEvent);
//pure virtual functions from CAnim
void Animate(TdateTime* aDateTime);
void Command(TInt aOpcode,TAny* aArgs);
TInt CommandReplyL(TInt aOpcode,TAny* aArgs);
private:
TInt iState;
CFbsBitmapDevice* iBitmapDevice;
CFbsBitmapDevice* iMaskBitmapDevice;
CFbsBitGc* iSpriteGc;
TBool iIsMask;
CPointStore* iPointStore;
};

我将在下面的部分讲到这些函数的目的。

11.7.3.1 构造
第一个要被调用的anim DLL函数是CSpriteAnim::ConstructL。WSERV调用此函数来回应客户对RAnim::Construct的调用。
传入ConstructL的参数aArgs是一个描述符内容的拷贝的指针,这个指针原本在客户端Construct函数中传递。
一般说来,他需要被转换为正确的类型。
void CHandWritingAnim::ConstructL(TAny* )
{
TSpriteMember* spriteMember=iSpriteFunctions->GetSpriteMember(0);
iIsMask=(spriteMember->iBitmap->Handle()
!=spriteMember->iMaskBitmap->Handle());
iBitmapDevice=CFbsBitmapDevice::NewL(spriteMember->iBitmap);
if (iIsMask)
iMaskBitmapDevice=CFbsBitmapDevice::NewL(spriteMember->iMaskBitmap);
iSpriteGc=CFbsBitGc::NewL();
iSpriteGc->Reset();
iState=EHwStateInactive;
iPointStore=new(ELeave) CPointStore();
iPointStore->ConstructL();
iSpriteFunctions->SizeChangedL();
...
}

对GetSpriteMember的调用返回这个anim允许被画到的精灵。一般的,精灵可以产生动画。
为此,一个客户需要给精灵一些成员 - 每一成员包含一帧动画。
在这里,客户只需要为墨水创建1帧的动画,所以我们指定0作为调用参数。然后要检查客户是够提供独立的位图给遮罩及精

灵的内容。
要画到任何的图形对象,比如说位图或者屏幕,需要那个对象的一个设备。
从一个设备,我们能够创建一个图形环境(或GC)并使用它进行绘制。我们现在创建了下面的对象 -
一个为每一位图创建的CFbsBitmapDevice和可以应用到每个位图的单个CFbsBitGc。

状态成员,iState,被初始化表明当前不需要进行手写识别。因为画到屏幕上的形状要被转化为一个字符,
我们创建一个点仓库来记录墨水的详细形状。调用SizeChangedL()是标准的一部分,精灵的一般初始化。
它设置了精灵的备份位图,这个位图的尺寸要与最大帧的尺寸相等。这个函数会继续设置墨水的参数,
比如颜色和线宽 - 这没有显示在之前的代码段中。

11.7.3.2 接收事件
当WSERV调用MEventHandler""OfferRawEvent()函数时事件被接收,参看11.3节,了解WSERV如何处理事件。
记住这个函数在基类中是纯虚的,所以我在我的CHandWritingAnim类中实现了。
默认情况下,WSERV并不会发送事件到anim,所以如果anim想要接收事件,他就必须调用

MAnimGeneralFunction::GetRawEvents(),传入ETrue。
一旦一个事件传递给了anim DLL,它必须返回一个TBool值告之已消费(ETrue)或未消费(EFalse)事件。
TBool CHandWritingAnim::OfferRawEvent(const TRawEvent &aRawEvent)
{
if (iState==EHwStateDeactive)
return EFalse;
switch (aRawEvent.Type())
{
case TRawEvent::EButton1Down:
return HandlePointerDown(aRawEvent.Pos());
case TRawEvent::EPointerMove:
return HandlePointerMove(aRawEvent.Pos());
case TRawEvent::EButton1Up:
return HandlePointerUp(aRawEvent.Pos());
default:
return EFalse;
}

这个函数干的第一件事就是检查手写是否打开。如果没有,返回EFalse告诉WSERV自己处理事件。
如果事件不是指针事件,同样也会返回EFalse。这是default声明的作用所在。
这个函数接下来会调用其他三个函数来处理指针事件。
我没有给出这些函数的实现,但是注意:如果用户点击和保持一定时长,这要被当做一个指针发送给程序,而不是画数字墨

水。

11.7.3.3 动画
Animante()函数为接受更行时钟的阶段性事件而设计。
void Animate(TDateTime* aDateTime);

WSERV默认不调用这个虚函数。如果一个anim希望被调用,anim需要调下面的函数:
void MAnimGeneralFunction::SetSync(TAnimSync aSyncMode);

这个参数指定多久调用Animate()函数一次,可选值有:
enum TAnimSync
{
ESyncNone,
ESyncFlash,
ESyncSecond,
ESyncMinute,
ESyncDay,
};

显然,ESyncNone表明WSERV从不做动画。其他的3个值告诉WSERV的动画函数指定的时间间隔。

 

第二个值有一点不同,他告诉WSERV每秒做2次动画。但是,这个动画不是平均分布的,每半秒钟 - 他们发生在这一秒和7/12秒以后。
这是为了字符间的分隔符(“12:45:37”中的“:”)闪烁时,其可见时间略长于不可见时间。
(WSERV使用相同间隔的计时器来闪烁光标及精灵,同样其可见时间略长于不可见时间。)

为了实现这些动画,WSERV使用lock计时器(可能也是symbian os中唯一用到lock计时器的地方)。
这个计时器使WSERV更容易的确定系统运行慢。他的API是:
void CTimer::Lock(TTimerLockSpec aLock);
参数指定了一秒的十二分之一,函数第一次被调用,当内核达到这个时间就会给相关的活动对象发出信号。
函数第二次被调用时,只有当请求的十二分之一秒同第一个请求点在同一秒内时,才会激活该活动对象。
如果不在,函数将返回一个错误,显示系统当前忙。
例如,假设一个时钟使用这个计时器来以秒来更新显示。每一秒钟,时钟在当前秒结束时,调用lock计时器请求被通知。
在接下来的第二边界,内核激活该活动对象。如果该活动对象自在那一秒内运行并自己重新入队,一切都很好。
如果系统正忙而恰巧活动对象运行和重新入队,
这一秒过去了,那么这个活动对象将以一个错误完成来告诉时钟需要重设自己,而不是增加。

当WSERV从lock 计时器得到一个错误,他告诉anim应该掠过这个事件而重设自己的动画函数。
(当正常运行时,他传递NULL给此函数。)

虽然手写anim不需要做任何动画,他确实需要一个计时器。
与创建自己的计时器相比,他选择使用CTimer::Lock()函数来接收时间提醒 - 我后面详细说明。

11.7.3.4 客户通信
下面的函数都可以从客户端接收命令:
TInt CAnim::CommnadReplyL(TInt aOpcode,TAny* aArgs) = 0;
void CAnim::Command(TInt aOpcode,TAny* aArgs) = 0;

一个不同点是,第一个可以返回客户的TInt值,他可以将值传回给客户,或者leave,leave代码将发送给客户。
另一个区别就是第一个 函数直接发送给窗口服务器。这样做是因为客户在他的任何其他代码执行之前要知道返回值,
而第二个函数将仅仅被WSERV保存在客户端,推迟发送给服务器端。在客户调用RAnim的CommandReply()和Command()时,
WSERV相应的调用这些函数:
void CHandWritingAnim::Command(TInt aOpcode,TAny* aArgs)
{
THandAnimArgUnion pData;
pData.any=aArgs;
switch (aOpcode)
{
case EHwOpActivate:
Activate();
break;
case EHwOpDeactivate:
Deactivate();
break;
case EHwOpSetDrawData:
SetDrawData(pData.DrawData);
break;
default:
iFunctions->Panic();
}
}

上面的函数显示了客户可以调用手写anim的三个命令。这些函数作用是开关手写识别,改变一些显示设置,
比如线宽和颜色。客户必须传递一大块数据给这个调用;这要使用一个无类型的指针来传递。
为了避免转换,我们使用了一个联合体,当然这不会比类型转换提供更多的类型安全,仅仅为了代码可读性更好。
union THandAnimArgUnion
{
const TAny* Any;
const TBool* Bool;
const THandwritingDrawData* DrawData;
};
CommandReplyL()函数也提供了两个函数供客户调用:
TInt CHandWritingAnim::CommandReplyL(TInt aOpcode, TAny* aArgs)
{
THandAnimArgUnion pData;
pData.any=aArgs;
switch (aOpcode)
{
case EHwOpSpriteMask:
SpriteChangeL(*pData.Bool);
break;
case EHwOpGetLastChar:
return iLastGeneratedCharacter;
default:
iFunctions->Panic();
}
return KErrNone;
}

第一个函数允许客户改变实际画墨水的位图,这个函数没有返回值,但是可能失败。
这种情况他会leave而leave值会返回给客户。第二个函数返回最后一次产生的字符给客户。

11.7.4 手写指针事件和更新精灵
当一个指针按下事件被收到,anim会设置一个计时器。这样做是因为指针事件可能是点击在UI上,而不是开始画数字墨水。
如果用户点击保持直到时间超期,或者点击和释放但不移动笔,这说明是点击在UI上面的。

下面的惯例是用来指针移动事件。他只需要在手写激活且笔按下时才需要处理他们,所以大多数状态只返回EFalse来表示事件没有被消费:
TBool CHandWritingAnim::HandlePointerMove(TPoint aPoint)
{
switch (iState)
{
case EHwStateWaitingMove:
{
cont TInt KMinMovement=5
TPoint moved=aPoint-iCurrentDrawPoint;
if (Abs(moved.iX)< KMinMovement && Abs(moved.iY)< KMinMovement)
return ETrue;
iSpriteFunctions->Activate(ETrue);
DrawPoint();
iState=EHwStateDrawing;
}
case EHwStateDrawing:
break;
default:
return EFalse;
}
DrawLine(aPoint);
UpdateSprite();
return ETrue;
}
如果我们还在等待时间超期(iState == EHwStateWaitingMove),并且指针仍然在原来按下的地方,那么移动事件被忽略 - 但是任然被消费。
否则,anim调用Activate()函数让精灵可见,然后向精灵内画入一个点,并更新状态标明正在绘制。然后调用下面的函数在精灵的位图中绘制一条线:
void CHandWritingAnim::DrawLine(TPoint aEndPoint)
{
iSpriteGc->Activate(iBitmapDevice);
iSpriteGc->SetPenSize(TSize(iDrawData.iLineWidth,
iDrawData.iLineWidth));
iSpriteGc->SetPenColor(iDrawData.iLineColor);
iSpriteGc->MoveTo(iCurrentDrawPoint);
iSpriteGc->DrawLineTo(aEndPoint);
if (iMaskBitmapDevice)
{
iSpriteGc->Activate(iMaskBitmapDevice);
iSpriteGc->SetPenSize(TSize(iDrawData.iMaskLineWidth,
iDrawData.iMaskLineWidth));
//Mask must be drawn in black
iSpriteGc->SetPenColor(KRgbBlack);
iSpriteGc->MoveTo(iCurrentDrawPoint);
iSpriteGc->DrawLineTo(aEndPoint);
}
iCurrentDrawPoint=aEndPoint;
iPointStore->AddPoint(aEndPoint);
}

anim使用相同的图形环境绘制精灵位图和遮罩图 - 他激活要绘制的位图设备上的图形环境。如果没有遮罩图,
动画会使用位图本身作为遮罩图,所以墨水要以黑色来绘制。如果有遮罩图,数字墨水线要同时画到位图和遮罩图上。
在这种情况下,遮罩图要用黑色画,而位图可以用任何其他颜色。WSERV保存线上的结束点,这样下一次划线这一点作为起点。
他同样保存点在BUFFER中,这样以后的字符识别算法能够分析墨水的形状,在位图被更新后,下面的代码将更新屏幕
(参照CHandWritingAnim::HandlePointerMove函数):
void CHandWritingAnim::UpdateSprite()
{
TRect drawTo;
iSpriteGc->RectDrawnTo(drawTo);
iSpriteFunctions->UpdateMember(0,drawTo,EFalse);
}

当任何绘制完成,BITGDI保存绘制像素点的轨迹 - 或者至少包围那些像素点的矩形。
这个矩形并非总是像素完美的,但保持一个好的近似值。这个矩形调用RectDrawnTo()获得,这个函数也会重置矩形。
然后这个函数调用更新成员的函数。这是WSERV提供给所有精灵anim的函数,目的是修正屏幕的这个区域。

更新一个精灵的普通方法是删掉它然后重新绘制到屏幕上 - 但如果这样做,屏幕会闪烁。在Symbian OS v5u中,
我们添加了不闪烁更新精灵的方法 - 这就是刚才代码使用的函数,定义如下:
void MAnimSpriteFunctions::UpdateMember(TInt aMember,const TRect& aRect,TBool aFullUpdate);
第三个参数告诉WSERV是否应该通过删除重绘方式更新精灵,或者仅仅做一个增量更新,将精灵重绘到屏幕。
显然,当绘制数字墨水,墨水只会增加,如果我们用增量更新,我们会得到正确的屏幕内容。
(如果当仅需要一个增量更新时WSERV做了完全更新,是毫无价值的。如果没有遮罩位图,
位图画到屏幕的方式由TSpriteMember类型的iDrawMode成员决定。如果这个成员不为EDrawModePEN,那么WSERV总是做完全更新。)

11.7.5 发送事件
有两种情况anim需要发送一个事件。第一种情况是墨水转换成为一个字符。其次是计时器熄灭,用户在点击以后没有移动笔。
因为按下事件自身被消费,anim需要重新发送他这样客户代码能够处理它。这些在下面的 代码中实现:
void CHandWritingAnim::CharacterFinished()
{
iState=EHwStateInactive;
iLastGeneratedCharacter=iPointStore->GetChar();
TKeyEvent keyEvent;
keyEvent.iCode=iLastGeneratedCharacter;
keyEvent.iScanCode=iLastGeneratedCharacter;
keyEvent.iModifiers=0;
keyEvent.iRepeats=0;
iFunctions->PostKeyEvent(keyEvent);
iPointStore->ClearPoints();
iSpriteFunctions->Activate(EFalse);
ClearSprite();
}
void CHandWritingAnim::SendEatenDownEvent()
{
TRawEvent rawEvent;
rawEvent.Set(TRawEvent::EButton1Down
,iCurrentDrawPoint.iX,iCurrentDrawPoint.iY);
iFunctions->PostRawEvent(rawEvent);
iState=EHwStateInactive;
}

有两个函数允许anim发送事件,他们都出现在前面的代码中,定义在MAnimGeneralFunctions中。
这是其中一个允许anim调用WSERV函数的类:
void PostRawEvent(const TRawEvent& aRawEvent) const;
void PostKeyEvent(const TKeyEvent& aRawEvent) const;
PostRawEvent()用在一个anim中来发送DOWN事件。它允许发送任何原始事件(即,内核发送给WSERV事件集中的一个)到WSERV处理。
下面的做法毫无价值,WSERV首先就把事件传递给了任何正在接收事件的anim - 这样很容易产生死循环!
你可以用PostRawEvent()发送大多数的按键事件,但是你应该会发送一个按键UP事件,一个按键DOWN事件,有时修改键的UP和DOWN事件。
这是为什么第二个函数存在的原因,PostKeyEvent(),允许发送一个EEventKey事件。

11.7.6 客户端代码 -构造
客户端同时创造了anim和精灵。CHandWriting类拥有和管理这两者。我写这个类,允许被包含在其他希望拥有手写anim的其他工程中。
class CHandWriting : public CBase
{
public:
CHandWriting(RWsSession& aSession);
void ConstructL(TSize aScreenSize, RWindowGroup& aGroup,
TBool aUseSeparateMask);
~CHandWriting();
void SetMaskL(TBool aUseSeparateMask);
void ToggleStatus();
private:
void CreateSpriteL(TSize aScreenSize, RWindowGroup& aGroup,
TBool aUseSeparateMask);
void LoadDllL();
void FillInSpriteMember(TSpriteMember& aMember);
private:
RWsSession& iSession;
RAnimDll iAnimDll;
RHandWritingAnim iAnim;
RWsSprite iSprite;
CFbsBitmap *iBitmap;
CFbsBitmap *iMaskBitmap;
TBool iActive;
};

精灵要首先被创建,这样函数才能构造anim。我们使用下面两种方式来做:
void CHandWriting::CreateSpriteL(TSize aScreenSize,
RWindowGroup& aGroup,TBool aUseSeparateMask)
{
TInt color,gray; //Unused variables
TDisplayMode mode=iSession .GetDefModeMaxNumColors(color,gray);
iBitmap=new(ELeave) CFbsBitmap();
User::LeaveIfError(iBitmap->Create(aScreenSize,mode));
TSpriteMember member;
member.iMaskBitmap=iBitmap;
if (aUseSeparateMask)
{
iMaskBitmap=new(ELeave) CFbsBitmap();
User::LeaveIfError(iMaskBitmap->Create(aScreenSize,mode));
member.iMaskBitmap=iMaskBitmap;
}
User::LeaveIfError(iSprite.Construct(aGroup,TPoint(),
ESpriteNoChildClip|ESpriteNoShadows));
FillInSpriteMember(member);
iSprite.AppendMember(member);
}
void CHandWriting::FillInSpriteMember(TSpriteMember& aMember)
{
aMember.iBitmap=iBitmap;
aMember.iInvertMask=ETrue; //Must be inverted
aMember.iDrawMode=CGraphicsContext::EDrawModePEN;
//Ignored when using mask
aMember.iOffset=TPoint(); //Must be 0,0
aMember.iInterval=0;
//Not used as only one TSpriteMember in sprite
}

我们调用iSprite.Constrcut()构造了精灵。所欲的精灵都必须与一个窗口相关联,他们中会被那个窗口的区域截取。
如果你指定一个窗口组,如我之前的代码,精灵就能够显示于整个屏幕。默认的,精灵也会被窗口可见区域截取。
但这里,我的代码指定了ESpriteNoChildClip标签,意味着不做截取。这样手写将总是能显示在整个窗口上。
其他的标志,如ESpriteNoShadow,表示即使精灵窗口之上有一个投影窗口,WSERV也不会对精灵的像素计算阴影。
精灵一旦创建,我为其增加一个成员或一帧。这是以那个函数最后两行来完成。

这个函数的另一个注意点是创建的精灵位图的颜色深度。当精灵画到屏幕,WSERV使用CFbsBitGc的BitBlt()或BitBiltMasked()函数。
这个函数在这样的情况下执行会快许多,即被画的位图或屏幕其颜色深度和要画的位图一样。
对于一般在窗口上显示的精灵来说,最好选择窗口的默认颜色模式。你可以使用GetDefModeMaxNumColors()得到。

创建精灵之后,我们能创建anim了。分为两个阶段 - 首先我们请求WSERV载入anim DLL然后创建精灵动画的实例。
我们用两个函数来实现,第二个被第一个调用:
void CHandWriting::LoadDllL()
{
_LIT(DllName,"HandAnim.DLL");
TInt err=iAnimDll.Load(DllName);
if (err==KErrNone)
err=iAnim.Construct(iSprite);
if (err==KErrNone)
{
iAnim.Activate();
iActive=ETrue;
}
User::LeaveIfError(err);
}
TInt RHandWritingAnim::Construct(const RWsSprite& aDevice)
{
TPtrC8 des(NULL,0);
return RAnim::Construct(aDevice,0,des);
}

为了载入anim DLL ,你必须给出相应的DLL名字。通过使用一个RAnimDll对象实现。然后你需要一个RAnim的派生类,
因为RAnim的接口是受保护的,这就强迫你从这个类派生。
anim构造函数接口有3个参数,窗口的精灵、一个类型 和打包在描述符中的配置数据。
类型允许一个anim DLL有多个anim类型那个,这成为指定创建哪一个的方法。
在我的例子中,手写anim只有一个类型,也没有使用配置数据。

11.7.7 其他客户端代码
RHandWritingAnim包含一些其他函数用来在anim间通信。这里有些例子:
void RHandWritingAnim::SetDrawData(const THandwritingDrawData& aDrawData)
{
TPckgBuf<THandwritingDrawData> param;
param()=aDrawData;
Command(EHwOpSetDrawData,param);
}
TInt RHandWritingAnim::GetLastGeneratedCharacter()
{
return CommandReply(EHwOpGetLastChar);
}

第一个函数告诉手写动画用不同的方式绘制(即,不同的颜色和线宽)。这需要发送数据到anim -
这个包裹到一个描述符TPckBuf类中。这会接下来传入函数CHandWritingAnim::Command()。

第二个函数传递最后产生的字符,没有数据同此请求一起发送,所以不需要一个TPckBuf,但是因为他需要回复,
他使用了RAnim::CommandReply()这个请求随后得到CHandWritingAnim::CommandReplyL()的发送。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值