分享
 
 
 

WTL流程分析-初稿

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

WTL流程分析

WTL流程分析

欢迎访问我的个人主页http://www.noasia.net/taowen

一个窗口从创建到销毁,有这么几个主要过程。

在winmain中

注册窗口类

创建窗口

进入消息循环

在wndproc中

处理消息

现在我们就是要挖掘出wtl中在何处处理这些东西,怎么处理的。首先:

winmain在哪里?

winmain在和工程名相同的cpp文件中。名字叫做_twinmain

int WINAPI _tWinMain(HINSTANCE

hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpstrCmdLine, int nCmdShow)

{

HRESULT hRes = ::CoInitialize(NULL);

// If you are running on NT 4.0 or

higher you can use the following call instead to

// make the EXE free threaded. This

means that calls come in on a random RPC thread.

// HRESULT

hRes = ::CoInitializeEx(NULL, COINIT_MULTITHREADED);

ATLASSERT(SUCCEEDED(hRes));

// this resolves ATL window thunking problem when Microsoft Layer for Unicode

(MSLU) is used

::DefWindowProc(NULL,

0, 0, 0L);

AtlInitCommonControls(ICC_COOL_CLASSES

| ICC_BAR_CLASSES); // add flags to support other controls

hRes = _Module.Init(NULL,

hInstance);

ATLASSERT(SUCCEEDED(hRes));

int nRet =

Run(lpstrCmdLine, nCmdShow);

_Module.Term();

::CoUninitialize();

return nRet;

}

从这个函数中,看不出什么,基本上实质上的内容都被分配在别的函数中处理了。这里所说的别的函数就是Run(lpstrCmdLine, nCmdShow);这个函数是我们自己写的,就在这个_twinmain的上面。

Run的作用

int Run(LPTSTR /*lpstrCmdLine*/

= NULL, int nCmdShow = SW_SHOWDEFAULT)

{

CMessageLoop theLoop;

_Module.AddMessageLoop(&theLoop);

CMainFrame wndMain;

if(wndMain.CreateEx()

== NULL)

{

ATLTRACE(_T("Main window creation failed!\n"));

return 0;

}

wndMain.ShowWindow(nCmdShow);

int nRet = theLoop.Run();

_Module.RemoveMessageLoop();

return nRet;

}

从名字MessageLoop和CreateEx就可以猜测到这个Run就是创建窗口并进入消息循环的地方。所以

winmain进行必要的初始化,主要的工作在Run中进行

Run创建窗口并进入消息循环。

窗口的创建

很容易就可以知道这么一段完成了窗口的创建

CMainFrame wndMain;

if(wndMain.CreateEx()

== NULL)

{

ATLTRACE(_T("Main window creation failed!\n"));

return 0;

}

CMainFrame定义在MainFrm.h中。

class CMainFrame : public CFrameWindowImpl<CMainFrame>,

public CUpdateUI<CMainFrame>,

public CMessageFilter, public CIdleHandler

可见这里使用了多继承,这是一个普遍行为。主要继承于CFrameWindowImpl,而且这个是模板,提供的参数就是CMainFrame。后面可以发现,这个参数在基类中用于强制类型转换,算是向下转换。

创建调用的是wndMain.CreateEx(),这个函数在CMainFrame中找不到,自然在其基类中有。这个是CFrameWindowImpl中的CreateEx():

HWND CreateEx(HWND hWndParent = NULL, _U_RECT rect = NULL,

DWORD dwStyle = 0, DWORD dwExStyle = 0, LPVOID lpCreateParam = NULL)

{

TCHAR szWindowName[256];

szWindowName[0] = 0;

::LoadString(_Module.GetResourceInstance(), T::GetWndClassInfo().m_uCommonResourceID,

szWindowName, 256);

HMENU hMenu=::LoadMenu(_Module.GetResourceInstance(), MAKEINTRESOURCE(T::GetWndClassInfo().m_uCommonResourceID));

T* pT = static_cast<T*>(this);

HWND hWnd = pT->Create(hWndParent,

rect, szWindowName, dwStyle, dwExStyle, hMenu, lpCreateParam);

if(hWnd!=NULL)

m_hAccel=::LoadAccelerators(_Module.GetResourceInstance(),MAKEINTRESOURCE(T::GetWndClassInfo().m_uCommonResourceID));

return hWnd;

}

等等,我们在这里发现了一个奇异的行为。

T* pT = static_cast<T*>(this);

这是什么,强制类型转换,而且是基于模板参数的类型转换。嗯,这个就是ATL开发组偶然发明的仿真动态绑定。利用给基类提供派生类作为模板参数,在函数调用的时候强制类型转换以在编译期间决定调用是哪个函数。这样作使得我们可以在派生类中改写基类中的函数,并且免去了虚函数带来的代价。所以说

pT->Create(hWndParent, rect, szWindowName, dwStyle, dwExStyle, hMenu, lpCreateParam);

调用的是派生类的Create函数,虽然派生类并没有改写这个函数,但是你可以这么作并获得灵活性。

下面继续跟踪这个Create的行为,不用寻找了,这个函数就在CreateEx的上面一点。派生类没有改写,调用的就是基类中的版本。

HWND Create(HWND hWndParent = NULL, _U_RECT rect = NULL,

LPCTSTR szWindowName = NULL, DWORD dwStyle =

0, DWORD dwExStyle = 0,

HMENU hMenu = NULL, LPVOID lpCreateParam = NULL)

{

ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);

dwStyle = T::GetWndStyle(dwStyle);

dwExStyle = T::GetWndExStyle(dwExStyle);

if(rect.m_lpRect == NULL)

rect.m_lpRect = &TBase::rcDefault;

return CFrameWindowImplBase< TBase, TWinTraits

>::Create(hWndParent, rect.m_lpRect, szWindowName, dwStyle, dwExStyle,

hMenu, atom, lpCreateParam);

}

红色标记了两个重要的过程,一个注册窗口类,一个创建了窗口。先关注窗口类的注册。

窗口类与注册

T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);

这条代码完成了窗口类的注册。

T是传递给基类的参数,也就是派生类。所以T就是CMainFrame。T::GetWndClassInfo()表示,这里调用的是类的静态函数。那么,这个函数在哪里定义的呢?我们要注意到CMainFrame定义中的这么一行:

DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)

显然,这是一个宏(你看看后面没有分号就知道了)。所以继续搜索这个宏的定义

#define DECLARE_FRAME_WND_CLASS(WndClassName,

uCommonResourceID)

static CFrameWndClassInfo& GetWndClassInfo()

{

static CFrameWndClassInfo

wc =

{

{ sizeof(WNDCLASSEX), 0, StartWindowProc,

0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, WndClassName,

NULL },

NULL, NULL, IDC_ARROW, TRUE, 0, _T(""), uCommonResourceID

};

return wc;

}

^_^,我逮着你了。就是这个宏把一个静态函数搞进来了。这个静态函数就是根据参数产生一个类型CFrameWndClassInfo的静态变量,并返回它。这个静态变量包含了WNDCLASS信息,以及和创建窗口时需要提供的一些信息。是一个所以

Register(&m_pfnSuperWindowProc);

调用的就是CFrameWndClassInfo中的member function。所以,我们要看看CFrameWndClassInfo的定义了:

class CFrameWndClassInfo

{

public:

WNDCLASSEX m_wc;

LPCTSTR m_lpszOrigName;

WNDPROC pWndProc;

LPCTSTR m_lpszCursorID;

BOOL m_bSystemCursor;

ATOM m_atom;

TCHAR m_szAutoName[5

+ sizeof(void*) * 2]; // sizeof(void*) * 2 is the number of digits %p

outputs

UINT m_uCommonResourceID;

ATOM Register(WNDPROC*

pProc)

{

if (m_atom == 0)

{

::EnterCriticalSection(&_Module.m_csWindowCreate);

if(m_atom == 0)

{

HINSTANCE hInst = _Module.GetModuleInstance();

if (m_lpszOrigName != NULL)

{

ATLASSERT(pProc != NULL);

LPCTSTR lpsz = m_wc.lpszClassName;

WNDPROC proc = m_wc.lpfnWndProc;

WNDCLASSEX wc;

wc.cbSize = sizeof(WNDCLASSEX);

// try process local class first

if(!::GetClassInfoEx(_Module.GetModuleInstance(), m_lpszOrigName, &wc))

{

// try global class

if(!::GetClassInfoEx(NULL, m_lpszOrigName, &wc))

{

::LeaveCriticalSection(&_Module.m_csWindowCreate);

return 0;

}

}

memcpy(&m_wc, &wc, sizeof(WNDCLASSEX));

pWndProc = m_wc.lpfnWndProc;

m_wc.lpszClassName = lpsz;

m_wc.lpfnWndProc = proc;

}

else

{

m_wc.hCursor = ::LoadCursor(m_bSystemCursor ? NULL : hInst, m_lpszCursorID);

}

m_wc.hInstance = hInst;

m_wc.style &= ~CS_GLOBALCLASS; //

we don't register global classes

if (m_wc.lpszClassName == NULL)

{

wsprintf(m_szAutoName, _T("ATL:%p"), &m_wc);

m_wc.lpszClassName = m_szAutoName;

}

WNDCLASSEX wcTemp;

memcpy(&wcTemp, &m_wc, sizeof(WNDCLASSEX));

m_atom = (ATOM)::GetClassInfoEx(m_wc.hInstance, m_wc.lpszClassName, &wcTemp);

if (m_atom == 0)

{

if(m_uCommonResourceID != 0) // use it

if not zero

{

m_wc.hIcon = (HICON)::LoadImage(_Module.GetResourceInstance(), MAKEINTRESOURCE(m_uCommonResourceID),

IMAGE_ICON, 32, 32, LR_DEFAULTCOLOR);

m_wc.hIconSm = (HICON)::LoadImage(_Module.GetResourceInstance(), MAKEINTRESOURCE(m_uCommonResourceID),

IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);

}

m_atom = ::RegisterClassEx(&m_wc);

}

}

::LeaveCriticalSection(&_Module.m_csWindowCreate);

}

if (m_lpszOrigName != NULL)

{

ATLASSERT(pProc != NULL);

ATLASSERT(pWndProc != NULL);

*pProc = pWndProc;

}

return m_atom;

}

};

不要管乱七八糟的一大堆,关键部分就是m_atom = ::RegisterClassEx(&m_wc);显而易见,这一句完成了真正的窗口类的注册。并用m_atom标记是否应注册过了。关于窗口类的注册,我们还要留意很关键的一点,那就是wndproc的地址。一路过来,明显的就是StartWindowProc。好的,到此,窗口类的注册已经完成了。下面:

窗口的创建

CFrameWindowImplBase< TBase, TWinTraits

>::Create(hWndParent, rect.m_lpRect, szWindowName, dwStyle, dwExStyle,

hMenu, atom, lpCreateParam);

这是这个函数的定义:

HWND Create(HWND hWndParent, _U_RECT rect, LPCTSTR szWindowName,

DWORD dwStyle, DWORD dwExStyle, _U_MENUorID MenuOrID, ATOM atom, LPVOID lpCreateParam)

{

ATLASSERT(m_hWnd == NULL);

if(atom == 0)

return NULL;

_Module.AddCreateWndData(&m_thunk.cd, this);

if(MenuOrID.m_hMenu == NULL && (dwStyle & WS_CHILD))

MenuOrID.m_hMenu = (HMENU)(UINT_PTR)this;

if(rect.m_lpRect == NULL)

rect.m_lpRect = &TBase::rcDefault;

HWND hWnd=::CreateWindowEx(dwExStyle, (LPCTSTR)(LONG_PTR)MAKELONG(atom,

0), szWindowName,

dwStyle, rect.m_lpRect->left, rect.m_lpRect->top, rect.m_lpRect->right

- rect.m_lpRect->left, rect.m_lpRect->bottom-rect.m_lpRect->top,

hWndParent, MenuOrID.m_hMenu, _Module.GetModuleInstance(),

lpCreateParam);

ATLASSERT(m_hWnd == hWnd);

return hWnd;

}

if(atom == 0)

return NULL;

检查窗口类是否已经正确注册了。然后是CreateWindowEx实质的创建工作。里面的参数窗口类是(LPCTSTR)(LONG_PTR)MAKELONG(atom,

0)。所以这里,窗口类名没有被使用。注册窗口类时返回的atom被用作相应的功能了。这个和mfc的做法很不一样。

到现在为止,窗口类已经注册并创建了一个窗口。我们回到Run中:

int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT)

{

CMessageLoop theLoop;

_Module.AddMessageLoop(&theLoop);

CMainFrame wndMain;

if(wndMain.CreateEx()

== NULL)

{

ATLTRACE(_T("Main window creation failed!\n"));

return 0;

}

wndMain.ShowWindow(nCmdShow);

int nRet = theLoop.Run();

_Module.RemoveMessageLoop();

return nRet;

}

消息循环

AddMessageLoop和RemoveMessageLoop把theLoop挂到模块(程序)对象上或者取下。

现在问题的核心是消息循环的处理。theLoop.Run();我们来看CMessageLoop的Run的定义:

int Run()

{

BOOL bDoIdle = TRUE;

int nIdleCount = 0;

BOOL bRet;

for(;;)

{

while(!::PeekMessage(&m_msg, NULL, 0, 0, PM_NOREMOVE) &&

bDoIdle)

{

if(!OnIdle(nIdleCount++))

bDoIdle = FALSE;

}

bRet = ::GetMessage(&m_msg, NULL, 0, 0);

if(bRet == -1)

{

ATLTRACE2(atlTraceUI, 0, _T("::GetMessage returned -1 (error)\n"));

continue; // error, don't process

}

else if(!bRet)

{

ATLTRACE2(atlTraceUI, 0, _T("CMessageLoop::Run - exiting\n"));

break; //

WM_QUIT, exit message loop

}

if(!PreTranslateMessage(&m_msg))

{

::TranslateMessage(&m_msg);

::DispatchMessage(&m_msg);

}

if(IsIdleMessage(&m_msg))

{

bDoIdle = TRUE;

nIdleCount = 0;

}

}

return (int)m_msg.wParam;

}

很简单,就是用PeekMessage决定当前是否有消息需要处理,然后在把需要处理的消息进行常规的翻译和分发。其中有进行空闲时间处理的机会。

然后消息循环已经开始了,现在要关注是哪里处理消息?

消息的处理

前面都是小菜,很清晰。到这里才遇到了大问题。我们回忆到WNDCLASS中的wndproc记录的是StartWndProc。不论如何,消息一开始进入的就是这个函数。抓住它,就有希望:

template <class TBase, class TWinTraits>

LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits

>::

StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

CWindowImplBaseT<

TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_Module.ExtractCreateWndData();

ATLASSERT(pThis !=

NULL);

pThis->m_hWnd =

hWnd;

pThis->m_thunk.Init(pThis->GetWindowProc(),

pThis);

WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk);

WNDPROC pOldProc =

(WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc);

#ifdef _DEBUG

// check if somebody

has subclassed us already since we discard it

if(pOldProc != StartWindowProc)

ATLTRACE2(atlTraceWindowing, 0, _T("Subclassing through a hook discarded.\n"));

#else

pOldProc;

// avoid unused warning

#endif

return pProc(hWnd,

uMsg, wParam, lParam);

}

首先我们来看SetWindowLong,知道这个是干什么的吗?SetWindowLong改变窗口的一些基本属性。GWL_WNDPROC表示要改变的是wndproc的地址。^_^,知道了为什么要先看这个了吧。这一步就是要把wndproc改为“正确”的地方。也就是pProc。

WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk);

这个就是执行了一次StartWindowProc之后,wndproc将改为的函数。pThis是从_Module中取出来的。取出来的信息是窗口创建的时候记录的,为了清晰,先不管它,反正它是一个CWindowImplBaseT类型的指针。

现在要明白m_thunk是啥子东西。m_thunk定义在CWindowImplBaseT的基类中是类型为CWndProcThunk的变量。我们来看CWndProcThunk:

class CWndProcThunk

{

public:

union

{

_AtlCreateWndData cd;

_WndProcThunk thunk;

};

void Init(WNDPROC proc,

void* pThis)

{

#if defined (_M_IX86)

thunk.m_mov = 0x042444C7; file://C7 44 24 0C

thunk.m_this = (DWORD)pThis;

thunk.m_jmp = 0xe9;

thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk));

#elif defined (_M_ALPHA)

thunk.ldah_at = (0x279f0000 | HIWORD(proc)) + (LOWORD(proc)>>15);

thunk.ldah_a0 = (0x261f0000 | HIWORD(pThis)) + (LOWORD(pThis)>>15);

thunk.lda_at = 0x239c0000 | LOWORD(proc);

thunk.lda_a0 = 0x22100000 | LOWORD(pThis);

thunk.jmp = 0x6bfc0000;

#endif

// write block from data cache and

file:// flush from instruction cache

FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk));

}

};

恐怖吧,居然出现了机器码。基本的思想是通过Init准备好一段机器码,然后把机器码的地址作为函数地址。这段机器码就干两件事情,一个是把wndproc的hwnd参数替换为pThis,另外一个是跳转到相应窗口的真实的wndproc中。

pThis->GetWindowProc()

这条代码返回的就是实际处理消息的地方。现在来看这个函数:

virtual WNDPROC GetWindowProc()

{

return WindowProc;

}

template <class TBase, class TWinTraits>

LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits

>::

WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

CWindowImplBaseT<

TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;

// set a ptr to this

message and save the old value

MSG msg = { pThis->m_hWnd,

uMsg, wParam, lParam, 0, { 0, 0 } };

const MSG* pOldMsg

= pThis->m_pCurrentMsg;

pThis->m_pCurrentMsg

= &msg;

// pass to the message

map to process

LRESULT lRes;

BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd,

uMsg, wParam, lParam, lRes, 0);

// restore saved value

for the current message

ATLASSERT(pThis->m_pCurrentMsg

== &msg);

pThis->m_pCurrentMsg

= pOldMsg;

// do the default processing

if message was not handled

if(!bRet)

{

if(uMsg != WM_NCDESTROY)

lRes = pThis->DefWindowProc(uMsg, wParam, lParam);

else

{

// unsubclass, if needed

LONG pfnWndProc = ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC);

lRes = pThis->DefWindowProc(uMsg, wParam, lParam);

if(pThis->m_pfnSuperWindowProc != ::DefWindowProc && ::GetWindowLong(pThis->m_hWnd,

GWL_WNDPROC) == pfnWndProc)

::SetWindowLong(pThis->m_hWnd, GWL_WNDPROC, (LONG)pThis->m_pfnSuperWindowProc);

// clear out window handle

HWND hWnd = pThis->m_hWnd;

pThis->m_hWnd = NULL;

// clean up after window is destroyed

pThis->OnFinalMessage(hWnd);

}

}

return lRes;

}

可见几经周折,最终还是落到了派生类的ProcessWindowMessage中。这里同样使用了模拟虚函数。还有一个问题是我在CMainFrame中并没有写ProcessWindowMessage啊?但是你写了

BEGIN_MSG_MAP(CMainFrame)

MESSAGE_HANDLER(WM_CREATE, OnCreate)

COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit)

COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew)

COMMAND_ID_HANDLER(ID_FILE_OPEN, OnFileOpen)

COMMAND_ID_HANDLER(ID_VIEW_TOOLBAR, OnViewToolBar)

COMMAND_ID_HANDLER(ID_VIEW_STATUS_BAR, OnViewStatusBar)

COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)

CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)

CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)

END_MSG_MAP()

这些东西其实就是ProcessWindowMessage,他们是宏。

#define BEGIN_MSG_MAP(theClass)

public:

BOOL ProcessWindowMessage(HWND

hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult, DWORD

dwMsgMapID = 0)

{

BOOL bHandled = TRUE;

hWnd;

uMsg;

wParam;

lParam;

lResult;

bHandled;

switch(dwMsgMapID)

{

case 0:

#define MESSAGE_RANGE_HANDLER(msgFirst,

msgLast, func)

if(uMsg >= msgFirst

&& uMsg <= msgLast)

{

bHandled = TRUE;

lResult = func(uMsg, wParam, lParam, bHandled);

if(bHandled)

return TRUE;

}

#define COMMAND_ID_HANDLER(id,

func)

if(uMsg == WM_COMMAND

&& id == LOWORD(wParam))

{

bHandled = TRUE;

lResult = func(HIWORD(wParam), LOWORD(wParam), (HWND)lParam, bHandled);

if(bHandled)

return TRUE;

}

#define CHAIN_MSG_MAP(theChainClass)

{

if(theChainClass::ProcessWindowMessage(hWnd, uMsg, wParam, lParam, lResult))

return TRUE;

}

#define END_MSG_MAP()

break;

default:

ATLTRACE2(atlTraceWindowing, 0, _T("Invalid message map ID (%i)\n"),

dwMsgMapID);

ATLASSERT(FALSE);

break;

}

return FALSE;

}

至此,一切都明白了。其实WTL只是ATL的窗口部分的扩展,这里所分析的东西绝大部分是ATL中的。而且这部分内容在《ATL INTERNAL》中也有比较详细描述了。

欢迎访问我的个人主页http://www.noasia.net/taowen

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有