MFC、ATL窗口消息封装机制对比分析

http://www.brucesky.com/articles/242

新产品在不紧不慢的进行中,这应该是有史以来开发比较“自由”的一个项目。在折腾完一个功能服务器的demo之后,开始折腾起PC客户端。Leader说客户端界面需用ATL来实现。这时候可以满足一下客户端界面开发的兴趣,于是开始学习ATL界面开发,有人说做界面是个累人加无趣的体力活,但对于做界面的新手来说自得其乐,君子懂得善假于物的道理,短期不允许重新造轮子,于是使用了kui做铺垫。现在要讨论的与kui无关,只讨论MFC和ATL对窗口消息封装的实现手法。一切还是从调试源码过程中寻找答案,进入正题。

我们知道,一个标准Windows App的主要逻辑有:1.注册一个窗口类/窗口过程;2.创建该类窗口;3.显示、激活窗口;4.消息循环;5.处理该窗口感兴趣的消息。

我们又知道,Win32 API是面向过程的(虽然可以说Windows是一个OO系统),而我们希望可以利用Win32 API进行快乐的OOP(不需要重复上面的逻辑)编程,于是,我们需要包装API,封装Windows窗口。从上面的逻辑可以看出,要封装窗口主要需解决怎样封装窗口消息处理机制。由于交给Windows调用的标准窗口过程是全局/静态的,此时,将面临两个问题:

1.怎么知道将窗口过程中的消息转发给对应封装好的窗口类实例?(也就是HWND到对应窗口类实例的转换)

2.假设第1个问题解决了,怎样将消息传递给相应窗口类的实例?

重点是解决第1个问题,下面来看MFC和ATL分别是怎么来解决这两个问题。

一、MFC窗口消息封装机制

我们通过一个手工产生(not by wizzard)的最简单的MFC程序(基于CWinApp和CWnd的两个类,以省掉不必要的代码和麻烦)开始调试分析。

01 BOOL CMFCApp::InitInstance()
02 {
03     CMFCWin* pMainWnd = new CMFCWin;
04     if (NULL == pMainWnd)
05         return FALSE;
06  
07     CString strWndClass;
08     strWndClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW,
09         ::LoadCursor(NULL, IDC_ARROW), HBRUSH(COLOR_WINDOW+1), NULL);
10  
11     HMENU hMenu = ::LoadMenu(NULL, MAKEINTRESOURCE(IDR_MAINFRAME));
12     // Bruce:开始创建主窗口
13     if (!pMainWnd->CreateEx(WS_EX_APPWINDOW, strWndClass,
14         _T("MFCBased without Wizzard"), WS_OVERLAPPEDWINDOW,
15         CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
16         NULL, hMenu, NULL))
17     {
18         AfxMessageBox(_T("CreateEx Failed!"));
19         return FALSE;
20     }
21  
22     m_pMainWnd = pMainWnd;
23     pMainWnd->ShowWindow(m_nCmdShow);
24     pMainWnd->UpdateWindow();
25  
26     return TRUE;
27 }

进入pMainWnd->CreateEx:

01 BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,
02     LPCTSTR lpszWindowName, DWORD dwStyle,
03     int x, int y, int nWidth, int nHeight,
04     HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)
05 {
06     // . . .
07     // Bruce:Hook? 好戏来了。
08     AfxHookWindowCreate(this);
09     HWND hWnd = ::AfxCtxCreateWindowEx(cs.dwExStyle, cs.lpszClass,
10             cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
11             cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);
12  
13 }

AfxHookWindowCreate,看上去是装了个Hook,进去看。

01 void AFXAPI AfxHookWindowCreate(CWnd* pWnd)
02 {
03     // Bruce:这里取的什么数据?
04     _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
05     if (pThreadState->m_pWndInit == pWnd)
06         return;
07  
08     if (pThreadState->m_hHookOldCbtFilter == NULL)
09     {
10         // Bruce:果然,这里装了个WH_CBT钩子
11         pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT,
12             _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
13         if (pThreadState->m_hHookOldCbtFilter == NULL)
14             AfxThrowMemoryException();
15     }
16     // . . .
17     pThreadState->m_pWndInit = pWnd;
18 }

先来说_afxThreadState.GetData(),_afxThreadState是一个全局CThreadLocal模板对象,是对TLS的封装,记录了线程相关的私有数据,_afxThreadState后面还会看到。接下来我们看到安装了一个WH_CBT钩子,_AfxCbtFilterHook是hook procedure,用来监视窗口的激活,创建,销毁等消息,也就是在窗口被激活,创建,销毁的时候系统会先调用这个函数。下面来看_AfxCbtFilterHook做了些什么。

01 LRESULT CALLBACK
02 _AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam)
03 {
04     _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
05     if (code != HCBT_CREATEWND) // Bruce:只关心窗口创建消息,其他跳过
06     {
07         // wait for HCBT_CREATEWND just pass others on...
08         return CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code,
09             wParam, lParam);
10     }
11     // . . .
12     LPCREATESTRUCT lpcs = ((LPCBT_CREATEWND)lParam)->lpcs;
13     // . . .
14     HWND hWnd = (HWND)wParam;
15     WNDPROC oldWndProc;
16     if (pWndInit != NULL)
17     {
18         AFX_MANAGE_STATE(pWndInit->m_pModuleState);
19  
20         // Bruce:检查该窗口映射是否存在?后面说明这个函数
21         ASSERT(CWnd::FromHandlePermanent(hWnd) == NULL);
22  
23         // Bruce:添加窗口映射(HWND  CWnd)
24         pWndInit->Attach(hWnd);
25         // allow other subclassing to occur first
26         pWndInit->PreSubclassWindow();
27  
28         WNDPROC *pOldWndProc = pWndInit->GetSuperWndProcAddr();
29         ASSERT(pOldWndProc != NULL);
30  
31         // Bruce:subclass,重新设置窗口过程
32         WNDPROC afxWndProc = AfxGetAfxWndProc();
33         oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC,
34                 (DWORD_PTR)afxWndProc);
35         ASSERT(oldWndProc != NULL);
36         if (oldWndProc != afxWndProc)
37             *pOldWndProc = oldWndProc;
38  
39         pThreadState->m_pWndInit = NULL;
40     }
41     // . . .
42 }

这里可以看到,钩子函数仅监视窗口的创建,通过FromHandlePermanent/Attach完成了HWND到CWnd的映射,并重新设置了窗口过程。这个窗口过程视链接MFC的方式不同要么是AfxWndProcBase或AfxWndProc,即Windows想要的标准窗口过程。

01 CWnd* PASCAL CWnd::FromHandlePermanent(HWND hWnd)
02 {
03     CHandleMap* pMap = afxMapHWND();
04     CWnd* pWnd = NULL;
05     if (pMap != NULL)
06     {
07         // only look in the permanent map - does no allocations
08         pWnd = (CWnd*)pMap->LookupPermanent(hWnd);
09         ASSERT(pWnd == NULL || pWnd->m_hWnd == hWnd);
10     }
11     return pWnd;
12 }
13  
14 BOOL CWnd::Attach(HWND hWndNew)
15 {
16     ASSERT(m_hWnd == NULL);     // only attach once, detach on destroy
17     ASSERT(FromHandlePermanent(hWndNew) == NULL);
18         // must not already be in permanent map
19  
20     if (hWndNew == NULL)
21         return FALSE;
22  
23     CHandleMap* pMap = afxMapHWND(TRUE); // create map if not exist
24     ASSERT(pMap != NULL);
25  
26     pMap->SetPermanent(m_hWnd = hWndNew, this);
27     // . . .
28 }

通过查看FromHandlePermanent/Attach的实现,可以发现HWND到CWnd的映射建立在Map的机制上,另外,查看afxMapHWND()的实现,可以发现,这个映射关系也保存在线程的TLS中,因此有了MFC的一个先天限制,不能把一个MFC对象从某线程手上交给另一线程,也不能够在线程之间传递MFC对象指针(了解更多可以参考《Win32多线程程序设计》MFC多线程一章)。下面再看看窗口过程。

01 WNDPROC AFXAPI AfxGetAfxWndProc()
02 {
03 #ifdef _AFXDLL // Bruce:Use MFC in a Shared DLL
04     return AfxGetModuleState()->m_pfnAfxWndProc; // Bruce:也就是AfxWndProcBase
05 #else // Bruce:Use MFC in a Static Library
06     return &AfxWndProc;
07 #endif
08 }
09  
10 LRESULT CALLBACK
11 AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
12 {
13     AFX_MANAGE_STATE(_afxBaseModuleState.GetData());
14     return AfxWndProc(hWnd, nMsg, wParam, lParam);
15 }
16  
17 LRESULT CALLBACK
18 AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
19 {
20     // special message which identifies the window as using AfxWndProc
21     if (nMsg == WM_QUERYAFXWNDPROC)
22         return 1;
23  
24     // Bruce:查找Map,找到HWND对应的CWnd实例
25     CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
26     ASSERT(pWnd != NULL);
27     ASSERT(pWnd==NULL || pWnd->m_hWnd == hWnd);
28     if (pWnd == NULL || pWnd->m_hWnd != hWnd)
29         return ::DefWindowProc(hWnd, nMsg, wParam, lParam);
30     return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam); // Bruce:有了CWnd,可以调用窗口实例对应的窗口过程
31 }
32  
33 LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,
34     WPARAM wParam = 0, LPARAM lParam = 0)
35 {
36     _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
37     MSG oldState = pThreadState->m_lastSentMsg; // save for nesting
38     pThreadState->m_lastSentMsg.hwnd = hWnd;
39     pThreadState->m_lastSentMsg.message = nMsg;
40     pThreadState->m_lastSentMsg.wParam = wParam;
41     pThreadState->m_lastSentMsg.lParam = lParam;
42  
43     #ifdef _DEBUG
44         _AfxTraceMsg(_T("WndProc"), &pThreadState->m_lastSentMsg);
45     #endif
46  
47     // Catch exceptions thrown outside the scope of a callback
48     // in debug builds and warn the user.
49     LRESULT lResult;
50     TRY
51     {
52         // . . .
53         // Bruce:“终于找到属于我的窗口过程了!”,WindowProc是一个virtual function,注意与AfxWndProc的区别
54         lResult = pWnd->WindowProc(nMsg, wParam, lParam);
55     }
56 // . . .
57 }

到此,我们已经完整的看到了MFC封装窗口消息机制的过程,简单总结一下:
通过Hook技术安装一个WH_CBT钩子,在窗口创建时(必须在这个时候,这样才不会有漏网的窗口消息)建立HWND到窗口类实例(如CWnd实例)的映射关系,并保存在创建该窗口的线程的TLS中,然后在需要的时候从TLS中取出数据查找关系Map。MFC就是通过这样的方式解决了开始提出的第1个问题,而第2个问题则通过虚函数来实现。

二、ATL窗口消息封装机制

这里也手工写了一个简单的ATL窗口程序,基于CWindowImpl窗口类。开始调试。

01 int APIENTRY _tWinMain(
02     HINSTANCE hInst,
03     HINSTANCE /*hInstPrev*/,
04     LPTSTR pszCmdLine,
05     int nCmdShow)
06 {
07     _Module.Init(0, hInst);
08  
09     HMENU hMenu = ::LoadMenu(_Module.GetResourceInstance(),
10         MAKEINTRESOURCE(IDR_MENU));
11  
12     CATLWin mainWnd;
13     // Bruce:开始创建主窗口
14     if (!mainWnd.Create(NULL, CWindow::rcDefault,
15         _T("ATLBased without Wizzard"), 0, 0, (UINT)hMenu))
16     {
17         ::MessageBox(NULL, _T("Create Failed"), _T("ATLBased Error"), MB_OK);
18         return -1;
19     }
20  
21     mainWnd.CenterWindow();
22     mainWnd.ShowWindow(nCmdShow);
23     mainWnd.UpdateWindow();
24  
25     MSG msg;
26     while (::GetMessage(&msg, NULL, 0, 0))
27     {
28         ::TranslateMessage(&msg);
29         ::DispatchMessage(&msg);
30     }
31  
32     _Module.Term();
33     return msg.wParam;
34 }

依旧是从创建窗口开始,看看CWindowImpl::Create做了什么。

01 HWND Create(HWND hWndParent, _U_RECT rect = NULL,
02     LPCTSTR szWindowName = NULL,
03     DWORD dwStyle = 0, DWORD dwExStyle = 0,
04     _U_MENUorID MenuOrID = 0U, LPVOID lpCreateParam = NULL)
05 {
06     if (T::GetWndClassInfo().m_lpszOrigName == NULL)
07         T::GetWndClassInfo().m_lpszOrigName = GetWndClassName();
08     ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);
09  
10     dwStyle = T::GetWndStyle(dwStyle);
11     dwExStyle = T::GetWndExStyle(dwExStyle);
12  
13     // . . .
14  
15     return CWindowImplBaseT< TBase, TWinTraits >::Create(hWndParent, rect,
16         szWindowName, dwStyle, dwExStyle, MenuOrID, atom, lpCreateParam);
17 }

函数一开始通过GetWndClassInfo获取默认的WndClass并注册,GetWndClassInfo通过宏DECLARE_WND_CLASS实现,展开可以发现默认的窗口过程是StartWindowProc,继续看CWindowImplBaseT< TBase, TWinTraits >::Create。

01 template
02 HWND CWindowImplBaseT< TBase, TWinTraits >::Create(
03     HWND hWndParent, _U_RECT rect, LPCTSTR szWindowName,
04     DWORD dwStyle, DWORD dwExStyle, _U_MENUorID MenuOrID,
05     ATOM atom, LPVOID lpCreateParam)
06 {
07     // . . .
08     // Allocate the thunk structure here, where we can fail gracefully.
09     result = m_thunk.Init(NULL,NULL);
10     // . . .
11     _AtlWinModule.AddCreateWndData(&m_thunk.cd, this);
12     // . . .
13  
14     HWND hWnd = ::CreateWindowEx(dwExStyle, MAKEINTATOM(atom),
15         szWindowName,dwStyle, rect.m_lpRect->left, rect.m_lpRect->top,
16         rect.m_lpRect->right - rect.m_lpRect->left,
17         rect.m_lpRect->bottom - rect.m_lpRect->top,
18         hWndParent, MenuOrID.m_hMenu,
19         _AtlBaseModule.GetModuleInstance(), lpCreateParam);
20  
21     ATLASSUME(m_hWnd == hWnd);
22  
23     return hWnd;
24 }

我们看到这里开始使用了thunk成员变量m_thunk,thunk一般可以理解为转换,在这里是一组ASM指令。每个CWindowImpl实例有自己的m_thunk,这里暂不管thunk的技术实现,只需留意窗口this指针和ThreadId被安全的放进一个_AtlCreateWndData全局链表结构中。下面就来看窗口过程StartWindowProc。

01 template
02 LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::
03 StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
04 {
05     // Bruce:通过当前的ThreadId遍历_AtlCreateWndData链表,取得先前保存的窗口this指针
06     CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_AtlWinModule.ExtractCreateWndData();
07     // . . .
08     // Bruce:这里保存HWND以作后用,thunking过程中将被覆盖掉
09     pThis->m_hWnd = hWnd;
10  
11     // Bruce:将静态窗口过程(如果GetWindowProc没被改写,那么就是CWindowImplBaseT::WindowProc)和this指针初始化到thunk中, m_thunk.Init之后,hWnd被替换成this指针
12     pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);
13     // Bruce:这里返回的其实是一个_stdcallthunk结构体的首地址,后面会提及
14     WNDPROC pProc = pThis->m_thunk.GetWNDPROC();
15     // . . .
16     // Bruce:进入静态窗口过程
17     return pProc(hWnd, uMsg, wParam, lParam);
18 }

继续看CWindowImplBaseT::WindowProc。

01 template
02 LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(
03     HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
04 {
05     // Bruce:hWnd变回this(thunk的功劳)
06     CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;
07     // . . .
08  
09     // Bruce:转换过程到这里结束,找到this的virtual成员函数处理窗口消息
10     BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0);
11     // . . .
12 }

到这里,大致看完了ATL为了解决第1个问题进行的转换过程,总结起来就是:通过thunk的作用,对hWnd做缓存后将Windows提供的hWnd替换成this指针,接着thunk把整个调用栈传递给真正的窗口过程(如果GetWindowProc没被改写,也就是CWindowImplBaseT::WindowProc)。
最后再来看有趣的thunk是怎么实现的。

01 class CWndProcThunk
02 {
03 public:
04     _AtlCreateWndData cd;
05     CStdCallThunk thunk;
06  
07     BOOL Init(WNDPROC proc, void* pThis)
08     {
09         return thunk.Init((DWORD_PTR)proc, pThis);
10     }
11     WNDPROC GetWNDPROC()
12     {
13         return (WNDPROC)thunk.GetCodeAddress();
14     }
15 };

不管内存字节对齐方式,这里假设CStdCallThunk为_stdcallthunk,在_M_IX86平台下。

01 #if defined(_M_IX86)
02 PVOID __stdcall __AllocStdCallThunk(VOID);
03 VOID  __stdcall __FreeStdCallThunk(PVOID);
04  
05 #pragma pack(push,1)    // Bruce:1字节内存对齐方式
06 struct _stdcallthunk
07 {
08     DWORD   m_mov;          // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
09     DWORD   m_this;         //
10     BYTE    m_jmp;          // jmp WndProc
11     DWORD   m_relproc;      // relative jmp
12     BOOL Init(DWORD_PTR proc, void* pThis)
13     {
14         m_mov = 0x042444C7;  //C7 44 24 0C
15         m_this = PtrToUlong(pThis);
16         m_jmp = 0xe9;
17         m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk)));
18         // write block from data cache and
19         //  flush from instruction cache
20         FlushInstructionCache(GetCurrentProcess(), this,sizeof(_stdcallthunk));
21         return TRUE;
22     }
23     //some thunks will dynamically allocate the memory for the code
24     void* GetCodeAddress()
25     {
26         return this;
27     }
28     void* operator new(size_t)
29     {
30         return __AllocStdCallThunk();
31     }
32     void operator delete(void* pThunk)
33     {
34         __FreeStdCallThunk(pThunk);
35     }
36 };
37 #pragma pack(pop)

三、其他实现

除了MFC、ATL中使用的手法,还有另外一种简单的封装方式。且看代码,主要是窗口过程。

01 LRESULT CALLBACK XWindow::WndProc(HWND hWnd, UINT uMsg,
02     WPARAM wParam, LPARAM lParam)
03 {
04     XWindow* pThis = NULL;
05     if (WM_NCCREATE == uMsg)
06     {
07         assert(!::IsBadReadPtr((void*)lParam, sizeof(CREATESTRUCT)));
08         LPCREATESTRUCT lpcs = reinterpret_cast(lParam);
09         pThis = static_cast(lpcs->lpCreateParams);
10         pThis->m_hWnd = hWnd;
11  
12         assert(!::IsBadReadPtr(pThis, sizeof(XWindow)));
13         ::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast(pThis));
14     }
15     else
16         pThis = reinterpret_cast(::GetWindowLongPtr(hWnd, GWLP_USERDATA));
17  
18     if (pThis)
19         return pThis->MsgProc(hWnd, uMsg, wParam, lParam);
20     else
21         return DefWindowProc(hWnd, uMsg, wParam, lParam);
22 }

这种方式通过在收到 WM_NCCREATE消息时,将this指针保存在lParam参数中,lParam事实上是一个LPCREATESTRUCT结构指针。成功保存后,WM_NCCREATE之后的消息就可以取出this指针,调用对应的消息处理函数。一些UI库使用的是这种实现方式。

到此,分析结束,由于MFC、ATL的设计目标不同(MFC在于简单易用,ATL则是短小精悍),MFC、ATL采用了不同的实现手法,且各有优劣。MFC采用全局映射表的方式,由于需要查找,损耗一定的时间,并随着窗口数的增多而增长。ATL在效率上占优势,但增加了一些复杂性。

本文调试源码:brucesky_window_bind_comp


  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WTL 具有两面性,确实是这样的。它没有MFC的界面(GUI)类库那样功能强大,但是能够生成很小的可执行文件。如果你象我一样使用MFC进行界面编程,你会觉得MFC提供的界面控件封装使用起来非常舒服,更不用说MFC内置的消息处理机制。当然,如果你也象我一样不希望自己的程序仅仅因为使用了MFC的框架就增加几百K的大小的话,WTL就是你的选择。当然,我们还要克服一些障碍: ATL样式的模板类初看起来有点怪异 没有类向导的支持,所以要手工处理所有的消息映射。 MSDN没有正式的文档支持,你需要到处去收集有关的文档,甚至是查看WTL的源代码。 买不到参考书籍 没有微软的官方支持 ATL/WTL的窗口MFC窗口有很大的不同,你所了解的有关MFC的知识并不全部适用与WTL。 从另一方面讲,WTL也有它自身的优势: 不需要学习或掌握复杂的文档/视图框架。 具有MFC的基本的界面特色,比如DDX/DDV和命令状态的自动更新功能(译者加:比如菜单的Check标记和Enable标记)。 增强了一些MFC的特性(比如更加易用的分隔窗口)。 可生成比静态链接的MFC程序更小的可执行文件(译者加:WTL的所有源代码都是静态链接到你的程序中的)。 你可以修正自己使用的WTL中的错误(BUG)而不会影响其他的应用程序(相比之下,如果你修正了有BUG的MFC/CRT动态库就可能会引起其它应用程序的崩溃。 如果你仍然需要使用MFCMFC窗口ATL/WTL的窗口可以“和平共处”。(例如我工作中的一个原型就使用了了MFC的CFrameWnd,并在其内包含了WTL的CSplitterWindow,在CSplitterWindow中又使用了MFC的CDialogs -- 我并不是为了炫耀什么,只是修改了MFC的代码使之能够使用WTL的分割窗口,它比MFC的分割窗口好的多)。 在这一系列文章中,我将首先介绍ATL窗口类,毕竟WTL是构建与ATL之上的一系列附加类,所以需要很好的了解ATL窗口类。介绍完ATL之后我将介绍WTL的特性以并展示它是如何使界面编程变得轻而易举。
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值