IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> MFC中消息映射与命令传递机制初探 -> 正文阅读

[C++知识库]MFC中消息映射与命令传递机制初探

本文主要根据侯捷《深入浅出MFC》整理而成,主要讲述MFC消息映射与传递机制。

一 如何形成消息映射网

1 在源文件加入实现消息映射表格代码

首先你必须在头文件中(.H)声明消息映射表格

class CScribbleDoc : public CDocument
{
 ...
 DECLARE_MESSAGE_MAP()
};

然后在实现文件中(.CPP)实现此表格:

BEGIN_MESSAGE_MAP(CScribbleDoc, CDocument)
 //{{AFX_MSG_MAP(CScribbleDoc)
 ON_COMMAND(ID_EDIT_CLEAR_ALL, OnEditClearAll)
 ON_COMMAND(ID_PEN_THICK_OR_THIN, OnPenThickOrThin)
 ...
 //}}AFX_MSG_MAP
END_MESSAGE_MAP()

这其中出现三个宏。第一个宏BEGIN_MESSAGE_MAP 有两个参数,分别是拥有此消息映射表之类,及其父类。第二个宏是ON_COMMAND,指定消息处理函数名称。第三个宏END_MESSAGE_MAP 作为结尾记号。至于夹在BEGIN_ 和END_ 之中奇奇怪怪的说明符号//}} 和//{{,是ClassWizard 产生的,也是用来给它自己看的。

2 消息映射表格的构建

消息映射的本质其实是一个巨大的数据结构,用来为诸如WM_PAINT 这样的标准消息决定流动路线,使它得以流到父类去;也用来为WM_COMMAND 这个特殊消息决定流动路线,使它能够七拐八弯地流到类继承结构的旁支去。我们看看在头文件和源文件中这些宏的定义是什么。
首先,头文件中的DECLARE_MESSAGE_MAP宏定义为:

#define DECLARE_MESSAGE_MAP() \
protected: \
	static const AFX_MSGMAP* PASCAL GetThisMessageMap(); \
	virtual const AFX_MSGMAP* GetMessageMap() const; \

然后,在实现文件中的宏定义为:

#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
	const AFX_MSGMAP* theClass::GetMessageMap() const \
		{ return GetThisMessageMap(); } \
	const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() \
	{ \
		typedef theClass ThisClass;		\
		typedef baseClass TheBaseClass;		\
		static const AFX_MSGMAP_ENTRY _messageEntries[] =  \
		{


#define END_MESSAGE_MAP() \
			{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
		}; \
		static const AFX_MSGMAP messageMap = \
		{ &TheBaseClass::GetThisMessageMap, &_messageEntries[0] };\
		return &messageMap; \
	}\

其中AFX_MSGMAP结构表示本类和基类的消息映射表地址信息,定义如下:

struct AFX_MSGMAP
{
 const AFX_MSGMAP* pBaseMap;	//基类的消息处理映射表地址
 const AFX_MSGMAP_ENTRY* lpEntries;	//本类的消息映射表地址,是个数组
};

AFX_MSGMAP_ENTRY(代表消息映射表的每个条目)是这样的形式

struct AFX_MSGMAP_ENTRY
{
	UINT nMessage;   // windows message
	UINT nCode;      // control code or WM_NOTIFY code
	UINT nID;        // control ID (or 0 for windows messages)
	UINT nLastID;    // used for entries specifying a range of control id's
	UINT_PTR nSig;       // signature type (action) or pointer to message #
	AFX_PMSG pfn;    // routine to call (or special value)
};

/*
AFX_MSGMAP_ENTRY包括
1. Windows消息号
2. 通知消息代码(notification code,对消息的更多描述,例如EN_CHANGED或CBN_DROPDIOWN等)
3. 控件ID(命令消息为0)
4. 一个签名记号
5. CCmdTarget派生类的成员函数,也就是消息对应的响应函数。
*/

任何一个ON_宏会把这六个项目初始化起来。例如:

#define ON_COMMAND(id, memberFxn) \
 { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSig_vv, (AFX_PMSG)memberFxn },
#define ON_WM_CREATE() \
 { WM_CREATE, 0, 0, 0, AfxSig_is, \
 (AFX_PMSG)(AFX_PMSGW)(int (AFX_MSG_CALL CWnd::*)(LPCREATESTRUCT))OnCreate },
#define ON_WM_DESTROY() \
 { WM_DESTROY, 0, 0, 0, AfxSig_vv, \
 (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))OnDestroy },
#define ON_WM_MOVE() \
 { WM_MOVE, 0, 0, 0, AfxSig_vvii, \
 (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(int, int))OnMove },
#define ON_WM_SIZE() \
 { WM_SIZE, 0, 0, 0, AfxSig_vwii, \
 (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(UINT, int, int))OnSize },
#define ON_WM_ACTIVATE() \
 { WM_ACTIVATE, 0, 0, 0, AfxSig_vwWb, \
 (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(UINT, CWnd*,
BOOL))OnActivate },
#define ON_WM_SETFOCUS() \
 { WM_SETFOCUS, 0, 0, 0, AfxSig_vW, \
 (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(CWnd*))OnSetFocus },
#define ON_WM_PAINT() \
 { WM_PAINT, 0, 0, 0, AfxSig_vv, \
 (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))OnPaint },
#define ON_WM_CLOSE() \
 { WM_CLOSE, 0, 0, 0, AfxSig_vv, \
 (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))OnClose },
...

/*
可以看到,最后一个函数指针进行了三次类型转换,第一次static_cast操作符可以检查函数类型是
否符合要求,第二次(AFX_PMSGW)和第三次(AFX_PMSG) 是为了类型安全,具体说来,如果一个继承
体系中包含多重继承的话,可能在子类强转为祖父类时出现二义性(例如,子类D从两个父类B、C继
承而来,而父类B和C都包含一个祖父类A,此时,将子类D转为A时,编译器会提示二义性错误)。总
之,需要把相应的消息处理函数转为统一的AFX_PMSG形式。
*/

从以上宏的定义可以看出,在定义消息映射表的时候,实际上在类中定义了一个静态的数组,这个数组中每一项是AFX_MSGMAP_ENTRY类型的数据,而AFX_MSGMAP_ENTRY包含了不同消息的消息号、ID值、通知消息代码、签名记号以及消息处理函数;
另外,通过GetMessageMap方法,可以获取AFX_MSGMAP结构,而这个结构体可以获取本类以及基类的消息映射表地址。

至此,我们的消息映射网初步形成,可以想见,我们新建每一个窗口类,都可以有自己的消息映射表格,也可以找到基类的消息映射表格,如此一直上溯,肯定可以找到默认消息处理函数。

二 消息的传递与响应

我们的消息映射网已经搭建好,那么窗口如何根据消息选择相应的处理函数呢?
首先要说,窗口接收到消息后,操作系统通过回调函数,最终调用的是窗口的CWnd::OnWndMsg函数。该函数实现如下:

BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
	LRESULT lResult = 0;
	union MessageMapFunctions mmf;
	mmf.pfn = 0;
	CInternalGlobalLock winMsgLock;
	
	// Windows消息单独处理
	if (message == WM_COMMAND)
	{
		if (OnCommand(wParam, lParam))
		{
			lResult = 1;
			goto LReturnTrue;
		}
		return FALSE;
	}
	
	........

	// 通告消息单独处理
	if (message == WM_NOTIFY)
	{
		NMHDR* pNMHDR = (NMHDR*)lParam;
		if (pNMHDR->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult))
			goto LReturnTrue;
		return FALSE;
	}
	........

	//下面这些就是标准的Windows消息
	const AFX_MSGMAP* pMessageMap; pMessageMap = GetMessageMap();	//找到最底层子类的消息映射表
	UINT iHash; iHash = (LOWORD((DWORD_PTR)pMessageMap) ^ message) & (iHashMax-1);
	winMsgLock.Lock(CRIT_WINMSGCACHE);
	AFX_MSG_CACHE* pMsgCache; pMsgCache = &_afxMsgCache[iHash];
	const AFX_MSGMAP_ENTRY* lpEntry;
	if (........)  	//检查是否在cache之中
	{
	........
	}
	else
	{
		pMsgCache->nMsg = message;
		pMsgCache->pMessageMap = pMessageMap;

		//以下循环请仔细查看,其代码逻辑就是直线上溯,一层一层的找详细映射表,
		//直到找到为止。
		for (/* pMessageMap already init'ed */; pMessageMap->pfnGetBaseMap != NULL;
			pMessageMap = (*pMessageMap->pfnGetBaseMap)())
		{
			if (message < 0xC000)
			{
				// 开始查找符合条件的消息
				if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries,
					message, 0, 0)) != NULL)
				{
					pMsgCache->lpEntry = lpEntry;
					winMsgLock.Unlock();
					goto LDispatch;
				}
			}
			else
			{
				// registered windows message
				lpEntry = pMessageMap->lpEntries;
				while ((lpEntry = AfxFindMessageEntry(lpEntry, 0xC000, 0, 0)) != NULL)
				{
					UINT* pnID = (UINT*)(lpEntry->nSig);
					ASSERT(*pnID >= 0xC000 || *pnID == 0);
						// must be successfully registered
					if (*pnID == message)
					{
						pMsgCache->lpEntry = lpEntry;
						winMsgLock.Unlock();
						goto LDispatchRegistered;
					}
					lpEntry++;      // keep looking past this one
				}
			}
		}

		pMsgCache->lpEntry = NULL;
		winMsgLock.Unlock();
		return FALSE;
	}

LDispatch:
	mmf.pfn = lpEntry->pfn;	//这个神秘的联合体待会详解,你只需知道这个变量代表一个函数指针
	switch (lpEntry->nSig)	//通过AFX_MSGMAP_ENTRY条目中的nSig变量,确定真正的函数形式
	{
	default:
		ASSERT(FALSE);
		break;
	case AfxSig_l_p:
		{
			CPoint point(lParam);
			lResult = (this->*mmf.pfn_l_p)(point);
			break;
		}
	case AfxSig_b_D_v:
		lResult = (this->*mmf.pfn_b_D)(CDC::FromHandle(reinterpret_cast<HDC>(wParam)));
		break;

	case AfxSig_l_D_u:
		lResult = (this->*mmf.pfn_l_D_u)(CDC::FromHandle(reinterpret_cast<HDC>(wParam)), (UINT)lParam);
		break;

	case AfxSig_b_b_v:
		lResult = (this->*mmf.pfn_b_b)(static_cast<BOOL>(wParam));
		break;

	case AfxSig_b_u_v:
		lResult = (this->*mmf.pfn_b_u)(static_cast<UINT>(wParam));
		break;

	case AfxSig_b_h_v:
		lResult = (this->*mmf.pfn_b_h)(reinterpret_cast<HANDLE>(wParam));
		break;
	... ... 
	}
	goto LReturnTrue;

LDispatchRegistered:    // for registered windows messages
	ASSERT(message >= 0xC000);
	ASSERT(sizeof(mmf) == sizeof(mmf.pfn));
	mmf.pfn = lpEntry->pfn;
	lResult = (this->*mmf.pfn_l_w_l)(wParam, lParam);

LReturnTrue:
	if (pResult != NULL)
		*pResult = lResult;
	return TRUE;
}

const AFX_MSGMAP_ENTRY* AFXAPI
AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,
	UINT nMsg, UINT nCode, UINT nID)
{
	... ... 	//以汇编语言处理,加快速度

	// C version of search routine
	while (lpEntry->nSig != AfxSig_end)
	{
		if (lpEntry->nMessage == nMsg && lpEntry->nCode == nCode &&
			nID >= lpEntry->nID && nID <= lpEntry->nLastID)
		{
			return lpEntry;
		}
		lpEntry++;
	}
	return NULL;    // not found
}

通过该函数可以看出,如果是Windows消息的话,采取的是直线上溯的方式,一一比较消息和消息映射表中的条目是否相符。查找函数的实现函数为AfxFindMessageEntry,这个函数其实就是迭代本类中的消息映射表,尝试找到nMessage、nCode、nID都相同的条目。如果在这个子类中没有找到符合的条目,就到这个上一层父类中去找,直到找到为止。
那么,找到会怎么做?怎么调用相关的响应函数?这里的关键是AFX_MSGMAP_ENTRY条目中的nSig变量。
nSig变量可能的值如下:

enum AfxSig
{
 AfxSig_end = 0, // [marks end of message map]
 AfxSig_bD, // BOOL (CDC*)
 AfxSig_bb, // BOOL (BOOL)
 AfxSig_bWww, // BOOL (CWnd*, UINT, UINT)
 AfxSig_hDWw, // HBRUSH (CDC*, CWnd*, UINT)
 AfxSig_hDw, // HBRUSH (CDC*, UINT)
 AfxSig_iwWw, // int (UINT, CWnd*, UINT)
 AfxSig_iww, // int (UINT, UINT)
 AfxSig_iWww, // int (CWnd*, UINT, UINT)
 AfxSig_is, // int (LPTSTR)
 AfxSig_lwl, // LRESULT (WPARAM, LPARAM)
 AfxSig_lwwM, // LRESULT (UINT, UINT, CMenu*)
 AfxSig_vv, // void (void)
 AfxSig_vw, // void (UINT)
 AfxSig_vww, // void (UINT, UINT)
 AfxSig_vvii, // void (int, int) // wParam is ignored
 AfxSig_vwww, // void (UINT, UINT, UINT)
 AfxSig_vwii, // void (UINT, int, int)
 AfxSig_vwl, // void (UINT, LPARAM)
 AfxSig_vbWW, // void (BOOL, CWnd*, CWnd*)
... ...
};

AfxSig枚举类型最后几位代表函数类型,例如 AfxSig_bWww代表函数返回值为bool型,函数参数依次为CWnd*, UINT和 UINT。
程序通过查看AFX_MSGMAP_ENTRY条目中的nSig变量值,就可以知道条目中消息响应函数的函数类型。刚刚我们说过了,消息响应函数表中所存储的都是AFX_PMSG 类型的函数指针,怎么转为正确的函数指针呢?
MFC将所有可能的响应函数形式定义为了一个联合体变量。

union MessageMapFunctions
{
 AFX_PMSG pfn; // generic member function pointer
 // specific type safe variants
 BOOL (AFX_MSG_CALL CWnd::*pfn_bD)(CDC*);
 BOOL (AFX_MSG_CALL CWnd::*pfn_bb)(BOOL);
 BOOL (AFX_MSG_CALL CWnd::*pfn_bWww)(CWnd*, UINT, UINT);
 BOOL (AFX_MSG_CALL CWnd::*pfn_bHELPINFO)(HELPINFO*);
 HBRUSH (AFX_MSG_CALL CWnd::*pfn_hDWw)(CDC*, CWnd*, UINT);
 HBRUSH (AFX_MSG_CALL CWnd::*pfn_hDw)(CDC*, UINT);
 int (AFX_MSG_CALL CWnd::*pfn_iwWw)(UINT, CWnd*, UINT);
 int (AFX_MSG_CALL CWnd::*pfn_iww)(UINT, UINT);
 int (AFX_MSG_CALL CWnd::*pfn_iWww)(CWnd*, UINT, UINT);
 int (AFX_MSG_CALL CWnd::*pfn_is)(LPTSTR);
 LRESULT (AFX_MSG_CALL CWnd::*pfn_lwl)(WPARAM, LPARAM);
 LRESULT (AFX_MSG_CALL CWnd::*pfn_lwwM)(UINT, UINT, CMenu*);
 void (AFX_MSG_CALL CWnd::*pfn_vv)(void);
 void (AFX_MSG_CALL CWnd::*pfn_vw)(UINT);
 void (AFX_MSG_CALL CWnd::*pfn_vww)(UINT, UINT);
 void (AFX_MSG_CALL CWnd::*pfn_vvii)(int, int);
 void (AFX_MSG_CALL CWnd::*pfn_vwww)(UINT, UINT, UINT);
 void (AFX_MSG_CALL CWnd::*pfn_vwii)(UINT, int, int);
 ... ...
};

然后就非常容易了,如果说在消息映射表中知道了相符的条目,就可以知道消息对应的消息响应函数的地址什么(就是函数的名称)。然后,根据AFX_MSGMAP_ENTRY条目中的nSig变量值,选择联合体中合适的成员就好了。以下是一个例子。

static BOOL DispatchCmdMsg(CCmdTarget* pTarget, UINT nID, int nCode,
 AFX_PMSG pfn, void* pExtra, UINT nSig, AFX_CMDHANDLERINFO* pHandlerInfo)
 // return TRUE to stop routing
{
	union MessageMapFunctions mmf;	//定义一个联合体变量,这个变量实际是某个函数指针
	mmf.pfn = pfn;
	BOOL bResult = TRUE; // default is ok
	... ...
	
 	//以下根据AFX_MSGMAP_ENTRY条目中的nSig变量值,调用函数正确的形式和参数。
	//注意:函数的参数实际最初是由消息体中的wParam和lParam传递进来的
	switch (nSig)
	{
	default:    // illegal
		ASSERT(FALSE);
		return 0;
		break;

	case AfxSigCmd_v:
		// normal command or control notification
		ASSERT(CN_COMMAND == 0);        // CN_COMMAND same as BN_CLICKED
		ASSERT(pExtra == NULL);
		(pTarget->*mmf.pfnCmd_v_v)();
		break;

	case AfxSigCmd_b:
		// normal command or control notification
		ASSERT(CN_COMMAND == 0);        // CN_COMMAND same as BN_CLICKED
		ASSERT(pExtra == NULL);
		bResult = (pTarget->*mmf.pfnCmd_b_v)();
		break;

	case AfxSigCmd_RANGE:
		// normal command or control notification in a range
		ASSERT(CN_COMMAND == 0);        // CN_COMMAND same as BN_CLICKED
		ASSERT(pExtra == NULL);
		(pTarget->*mmf.pfnCmd_v_u)(nID);
		break;

	case AfxSigCmd_EX:
		// extended command (passed ID, returns bContinue)
		ASSERT(pExtra == NULL);
		bResult = (pTarget->*mmf.pfnCmd_b_u)(nID);
		break;
	... ...
}

这是MFC动人的一幕,不同的消息,参数不一样,返回值也不一样,而且在定义的时候只是一个指针,可是在调用的时候却有各种各样的方式。用了一个union变量,就将所有不同形态函数统一为一个,这太牛了。
可是为什么要这样做呢?为了降低程序的空间复杂度。如果我们使用C++常用的虚函数来实现多态,需要在类中维护一个虚函数表,从而实现基类指针调用子类方法的效果。由于Windows消息众多,如果在每个类中维护一个虚函数表,大大增加了程序了空间复杂度。

三 最后一个问题,成员解除引用(->*)运算符的用法

还剩下最后一个问题,比如上面函数中的这条语句

	case AfxSigCmd_v:
		(pTarget->*mmf.pfnCmd_v_v)();
		break;

pTarget是一个CCmdTarge指针,mmf.pfnCmd_v_v指针类型如下:

void (AFX_MSG_CALL CCmdTarget::*pfnCmd_v_v)();

可以看出,这分别是一个基类指针,调用了一个基类方法,怎么就最终调用成子类的方法了呢?这里关键是要理解成员解除引用(->*)运算符。基类指针使用成员解除引用运算符调用某个子类的成员函数时,基类指针指向的地址(实际指向子类对象)会被当做this指针压栈,然后程序直接转到这个被调用的函数的地址(实际上这个地址是子类成员函数的地址),然后程序根据this指针获取类中数据成员,而this指针实际指向的是子类对象。最终的效果就好像是基类指针调用一个虚函数。
以下代码模式了这个过程。

#include<iostream>
using namespace std;

class CBase
{
public:
	void BasePrintMsg() {
		cout << "In Base class" << endl;
	}
};

class CDerive :public CBase
{
public:
	CDerive() :m_iDerive(3) {}
	int GetInt(int m) {
		return m_iDerive * m;
	}

	double GetDouble(int m, double d) {
		return m * d;
	}
private:
	int m_iDerive;
};

typedef void (CBase::* pBaseVoidFun)();

union UMapFuns
{
	//请注意,以下三个成员都是基类的成员函数的函数指针
	pBaseVoidFun pfn;
	double (CBase::*pfn_double)(int m, double d);
	int (CBase::*pfn_int)(int m);
};

int main()
{
	UMapFuns uMapFun;	//向MFC学习,定义一个联合体变量,表示函数指针
	uMapFun.pfn = (pBaseVoidFun)&CDerive::GetInt;	//子类方法转为基类方法赋给联合体

	CDerive cDeriveObj;
	CBase* pBase = &cDeriveObj;
	
	//调用成功,关键要理解uMapFun.pfn_int的地址实际上是子类成员函数的地址,子类对象
	//调用解除引用运算符时,不过是将子类对象的地址压栈,让子类成员函数能够根据这个
	//地址找到类的其他成员。
	int i = (cDeriveObj.*(uMapFun.pfn_int))(3);	
	
	//调用成功,用基类指针调用子类函数。关键要理解uMapFun.pfn_int的地址实际上是子类
	//成员函数的地址,基类指针指向子类对象的地址,该地址压栈后,
	//子类成员函数根据这个地址找到的类的数据成员都是正确的。
	int i2 = (pBase->*(uMapFun.pfn_int))(4);	

	uMapFun.pfn = (pBaseVoidFun)&CDerive::GetDouble;
	double d = (pBase->*(uMapFun.pfn_double))(3, 4.0);	//调用成功,用基类指针调用子类函数

	return 0;
}

That is all!

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-02-14 20:56:33  更:2022-02-14 20:59:08 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 6:55:36-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码