六、MFC 程序的生死因果 (学习笔记)
MFC程序的生死因果
中华民国还得十次革命才得建立,对象导向怎能把一切传统都抛开。
以传统的C/SDK 撰写Windows 程序,最大的好处是可以清楚看见整个程序的来龙去脉和消息动向,然而这些重要的动线在MFC 应用程序中却隐晦不明,因为它们被Application Framework包起来了。这一章主要目的除了解释MFC 应用程序的长像,也要从MFC 源代码中检验出一个Windows 程序原本该有的程序进入点(WinMain)、视窗类别注册(RegisterClass)、窗口产生(CreateWindow )、消息循环(Message Loop )、窗口函数(Window Procedure)等等动作,抽丝剥茧彻底了解一个MFC 程序的诞生与结束,以及生命过程。
为什么要安排这一章?了解MFC 内部构造是必要的吗?看电视需要知道映射管的原理吗?开汽车需要知道传动轴与变速箱的原理吗?学习MFC 不就是要一举超越烦琐的Windows API?啊,厂商(不管是哪一家)广告给我们的印象就是,藉由可视化的工具我们可以一步登天,基本上这个论点正确,只是有个但是:你得学会操控Application Framework 。
想象你拥有一部保时捷,风驰电挚风光得很,但是引擎盖打开来全傻了眼。如果你懂汽车内部运作原理,那么至少开车时「脚不要老是含着离合器,以免来令片磨损」这个道理背后的原理你就懂了,「踩煞车时绝不可以同时踩离合器,以免失去引擎煞车力」这个道理背后的原理你也懂了,甚至你的保时捷要保养维修时或也可以不假外力自己来。
不要把自己想象成这场游戏中的后座车主,事实上作为这本技术书籍的读者的你,应该是车厂师傅。
我希望你了解,本书之所以在各个主题中不厌其烦地挖MFC内部动作,解释骨干程序的每一条指令,每一个环节,是为了让你踏实地接受MFC,进而有能力役使MFC。你以为这是一条远路?呵呵,似远实近!
不二法门:熟记MFC 类别的阶层架构
MFC 在1.0版时期的诉求是「一组将SDK API 包装得更好用的类别库」,从2.0版开始更进一步诉求是一个「Application Framework」,拥有重要的Document-View架构;随后又在更新版本上增加了OLE 架构、DAO 架构... 。为了让你有一个最轻松的起点,我把第一个程序简化到最小程度,舍弃Document-View 架构,使你能够尽快掌握C++/MFC 程序的面貌。这个程序并不以AppWizard 制作出来,也不以ClassWizard 管理维护,而是纯手工打造。毕竟Wizards 做出来的程序代码有一大堆批注,某些批注对Wizards 有特殊意义,不能随便删除,却可能会混淆初学者的视听焦点;而且Wizards 所产生的程序骨干已具备Document-View 架构,又有许多奇奇怪怪的宏,初学者暂避为妙。我们目前最想知道的是一个最阳春的MFC 程序以什么面貌呈现,以及它如何开始运作,如何结束生命。
SDK 程序设计的第一要务是了解最重要的数个API 函数的意义和用法,像是RegisterClass、CreateWindow 、GetMessage 、DispatchMessage,以及消息的获得与分配。MFC 程序设计的第一要务则是熟记MFC 的类别阶层架构,并清楚知晓其中几个一定会用到的类别。本书最后面有一张MFC 4.2 架构图,叠床架屋,令人畏惧,我将挑出单单两个类别,组合成一个"Hello MFC" 程序。这两个类别在MFC的地位如图6-1所示。
需要什么函数库?
开始写码之前,我们得先了解程序代码以外的外围环境。第一个必须知道的是,MFC 程序需要什么函数库?SDK 程序联结时期所需的函数库已在第一章显示,MFC 程序一样需要它们:
此外,应用程序还需要联结一个所谓的MFC 函数库,或称为AFX 函数库,它也就是MFC这个application framework 的本体。你可以静态联结之,也可以动态联结之,AppWizard给你选择权。本例使用动态联结方式,所以需要一个对应的MFC import 函数库:
我们如何在联结器(link.exe )中设定选项,把这些函数库都联结起来?稍后在HELLO.MAK中可以一窥全貌。
如果在Visual C++ 整合环境中工作,这些设定不劳你自己动手,整合环境会根据我们圈选的项目自动做出一个合适的makefile。这些makefile 的内容看起来非常诘屈聱牙,事实上我们也不必太在意它,因为那是整合环境的工作。这一章我不打算依赖任何开发工具,一切自己来,你会在稍后看到一个简洁清爽的makefile。
需要什么头文件?
SDK 程序只要包含WINDOWS.H 就好,所有API 的函数声明、消息定义、常数定义、宏定义、都在WINDOWS.H 档中。除非程序另调用了操作系统提供的新模块(如CommDlg、ToolHelp 、DDEML...),才需要再各别包含对应的.H 档。
MFC 程序不这么单纯,下面是它常常需要面对的另外一些.H 档:
■STDAFX.H - 这个文件用来做为Precompiled header file(请看稍后的方块说明),其内只是包含其它的MFC 头文件。应用程序通常会准备自己的STDAFX.H ,例如本章的Hello 程序就在STDAFX.H 中包含AFXWIN.H。
■AFXWIN.H - 每一个Windows MFC 程序都必须包含它,因为它以及它所包含的文件声明了所有的MFC 类别。此档内含AFX.H,后者又包含AFXVER_.H,后者又包含AFXV_W32.H,后者又包含WINDOWS.H(啊呼,终于现身)。
■AFXEXT.H - 凡使用工具栏、状态列之程序必须包含这个文件。
■AFXDLGS.H - 凡使用通用型对话框(Common Dialog)之MFC 程序需包含此档,其内部包含COMMDLG.H。
■AFXCMN.H - 凡使用Windows 95 新增之通用型控制组件(Common Control)之MFC 程序需包含此文件。
■AFXCOLL.H - 凡使用Collections Classes (用以处理数据结构如数组、串行)之程序必须包含此文件。
■AFXDLLX.H - 凡MFC extension DLLs 均需包含此档。
■AFXRES.H - MFC 程序的RC文件必须包含此档。MFC 对于标准资源(例如File、Edit 等菜单项目)的ID 都有默认值,定义于此文件中,例如:
// File commands
#define ID_FILE_NEW 0xE100
#define ID_FILE_OPEN 0xE101
#define ID_FILE_CLOSE 0xE102
#define ID_FILE_SAVE 0xE103
#define ID_FILE_SAVE_AS 0xE104
...
// Edit commands
#define ID_EDIT_COPY 0xE122
#define ID_EDIT_CUT 0xE123
...
这些菜单项目都有预设的说明文字(将出现在状态列中),但说明文字并不会事先定义于此文件,AppWizard 为我们制作骨干程序时才把说明文字加到应用程序的RC文件中。第4章的骨干程序Scribble step0 的RC 档中就有这样的字符串表格:
STRINGTABLE DISCARDABLE
BEGINID_FILE_NEW "Create a new document"ID_FILE_OPEN "Open an existing document"ID_FILE_CLOSE "Close the active document"ID_FILE_SAVE "Save the active document"ID_FILE_SAVE_AS "Save the active document with a new name"...ID_EDIT_COPY "Copy the selection and puts it on the Clipboard"ID_EDIT_CUT "Cut the selection and puts it on the Clipboard"...
END
所有MFC 头文件均置于\MSVC\MFC\INCLUDE 中。这些文件连同Windows SDK 的包含档WINDOWS.H、COMMDLG.H、TOOLHELP.H、DDEML.H... 每每在编译过程中耗费大量的时间,因此你绝对有必要设定Precompiled header 。
简化的MFC 程序架构-以Hello MFC 为例
现在我们正式进入MFC 程序设计。由于Document/View架构复杂,不适合初学者,所以我先把它略去。这里所提的程序观念是一般的MFC Application Framework 的子集合。本章程序名为Hello,执行时会在窗口中从天而降"Hello, MFC" 字样。Hello 是一个非常简单而具代表性的程序,它的代表性在于:
■ 每一个MFC 程序都想从MFC 中衍生出适当的类别来用(不然又何必以MFC 写程序呢),其中两个不可或缺的类别CWinApp 和CFrameWnd 在Hello程序中会表现出来,它们的意义如图6-2。
■ MFC 类别中某些函数一定得被应用程序改写(例如CWinApp :: InitInstance),这在Hello 程序中也看得到。
■ 菜单和对话框,Hello 也都具备。
图6-3 是Hello 源文件的组成。第一次接触MFC 程序,我们常常因为不熟悉MFC 的类别分类、类别命名规则,以至于不能在脑中形成具体印象,于是细部讨论时各种信息及说明彷如过眼云烟。相信我,你必须多看几次,并且用心熟记MFC 命名规则。
图6-3 之后是Hello 程序的源代码。由于MFC 已经把Windows API 都包装起来了,源代码再也不能够「说明一切」。你会发现MFC 程序很有点见林不见树的味道:
■看不到WinMain,因此不知程序从哪里开始执行。
■看不到RegisterClass 和CreateWindow ,那么窗口是如何做出来的呢?
■看不到Message Loop (GetMessage /DispatchMessage ),那么程序如何推动?
■看不到Window Procedure,那么窗口如何运作?
我的目的就在铲除这些困惑。
Hello 程序源代码
■ HELLO.MAK - makefile
■ RESOURCE.H - 所有资源ID 都在这里定义。本例只定义一个IDM_ABOUT。
■ JJHOUR.ICO - 图标文件,用于主窗口和对话框。
■ HELLO.RC - 资源描述档。本例有一份菜单、一个图标、和一个对话框。
■ STDAFX.H - 包含AFXWIN.H。
■ STDAFX.CPP - 包含STDAFX.H ,为的是制造出Precompiled header 。
■ HELLO.H - 声明CMyWinApp 和CMyFrameWn d。
■ HELLO.CPP - 定义CMyWinApp 和CMyFrameWn d。
注意:没有模块定义文件.DEF?是的,如果你不指定模块定义文件,联结器就使用默认值。
MFC 程序的来龙去脉(causal relations)
让我们从第1章的C/SDK 观念出发,看看MFC 程序如何运作。
第一件事情就是找出MFC 程序的进入点。MFC 程序也是Windows 程序,所以它应该也有一个WinMain,但是我们在Hello 程序看不到它的踪影。是的,但先别急,在程序进入点之前,更有一个(而且仅有一个)全域对象(本例名为theApp ),这是所谓的application object,当操作系统将程序加载并激活,这个全域对象获得配置,其构造式会先执行,比WinMain 更早。所以以时间顺序来说,我们先看看这个application object。
我只借用两个类别:CWinApp 和 CFrameWnd
你已经看过了图6-2,作为一个最最粗浅的MFC 程序,Hello 是如此单纯,只有一个视窗。回想第一章Generic 程序的写法,其主体在于WinMain 和WndProc,而这两个部份其实都有相当程度的不变性。好极了,MFC 就把有着相当固定行为之WinMain 内部动作包装在CWinApp 中,把有着相当固定行为之WndProc 内部动作包装在CFrameWnd 中。也就是说:
■ CWinApp 代表程序本体
■ CFrameWnd 代表一个主框窗口(Frame Window)
但虽然我说,WinMain内部动作和WndProc内部动作都有着相当程度的固定行为,它们毕竟需要面对不同应用程序而有某种变化。所以,你必须以这两个类别为基础,衍生自己的类别,并改写其中一部份成员函数。
class CMyWinApp : public CWinApp
{
...
};
class CMyFrameWnd : public CFrameWnd
{
...
};
本章对衍生类别的命名规则是:在基础类别名称的前面加上" My" 。这种规则真正上战场时不见得适用,大型程序可能会自同一个基础类别衍生出许多自己的类别。不过以教学目的而言,这种命名方式使我们从字面就知道类别之间的从属关系,颇为理想(根据我的经验,初学者会被类别的命名搞得头昏脑胀)。
CWinApp-取代 WinMain的地位
CWinApp的衍生对象被称为application object,可以想见,CWinApp本身就代表一个程式本体。一个程序的本体是什么?回想第1章的SDK 程序,与程序本身有关而不与视窗有关的资料或动作有些什么?系统传进来的四个WinMain参数算不算?InitApplication 和InitInstance 算不算?消息循环算不算?都算,是的,以下是MFC 4.x的CWinApp声明(节录自AFXWIN.H):
class CWinApp : public CWinThread
{
// Attributes// Startup args (do not change)HINSTANCE m_hInstance;HINSTANCE m_hPrevInstance;LPTSTR m_lpCmdLine;int m_nCmdShow;// Running args (can be changed in InitInstance)LPCTSTR m_pszAppName; // human readable nameLPCTSTR m_pszRegistryKey; // used for registry entries
public: // set in constructor to override defaultLPCTSTR m_pszExeName; // executable name (no spaces)LPCTSTR m_pszHelpFilePath; // default based on module pathLPCTSTR m_pszProfileName; // default based on app name
public:// hooks for your initialization codevirtual BOOL InitApplication();// overrides for implementationvirtual BOOL InitInstance();virtual int ExitInstance();virtual int Run();virtual BOOL OnIdle(LONG lCount);
...
};
几乎可以说CWinApp 用来取代WinMain 在SDK 程序中的地位。这并不是说MFC 程序没有WinMain(稍后我会解释),而是说传统上SDK 程序的WinMain 所完成的工作现在由CWinApp 的三个函数完成:
virtual BOOL InitApplication();
virtual BOOL InitInstance();
virtual int Run();
WinMain 只是扮演役使它们的角色。
会不会觉得CWinApp 的成员变量中少了点什么东西?是不是应该有个成员变量记录主窗口的handle (或是主窗口对应之C++ 对象)?的确,在MFC 2.5 中的确有m_pMainWnd这么个成员变量(以下节录自MFC 2.5 的AFXWIN.H):
class CWinApp : public CCmdTarget
{
// Attributes// Startup args (do not change)HINSTANCE m_hInstance;HINSTANCE m_hPrevInstance;LPSTR m_lpCmdLine;int m_nCmdShow;// Running args (can be changed in InitInstance)CWnd* m_pMainWnd; // main window (optional)CWnd* m_pActiveWnd; // active main window (may not be m_pMainWnd)const char* m_pszAppName; // human readable name
public: // set in constructor to override defaultconst char* m_pszExeName; // executable name (no spaces)const char* m_pszHelpFilePath; // default based on module pathconst char* m_pszProfileName; // default based on app name
public:// hooks for your initialization codevirtual BOOL InitApplication();virtual BOOL InitInstance();// running and idle processingvirtual int Run();virtual BOOL OnIdle(LONG lCount);// exitingvirtual int ExitInstance();
...
};
但从MFC 4.x 开始,m_pMainWnd 已经被移往CWinThread 中了(它是CWinApp 的父类别)。以下内容节录自MFC 4.x 的AFXWIN.H:
class CWinThread : public CCmdTarget
{
// AttributesCWnd* m_pMainWnd; // main window (usually same AfxGetApp()->m_pMainWnd)CWnd* m_pActiveWnd; // active main window (may not be m_pMainWnd)// only valid while runningHANDLE m_hThread; // this thread's HANDLEDWORD m_nThreadID; // this thread's IDint GetThreadPriority();BOOL SetThreadPriority(int nPriority);
// OperationsDWORD SuspendThread();DWORD ResumeThread();
// Overridables// thread initializationvirtual BOOL InitInstance();// running and idle processingvirtual int Run();virtual BOOL PreTranslateMessage(MSG* pMsg);virtual BOOL PumpMessage(); // low level message pumpvirtual BOOL OnIdle(LONG lCount); // return TRUE if more idle processing
public:// valid after constructionAFX_THREADPROC m_pfnThreadProc;
...
};
熟悉Win32 的朋友,看到CWinThread 类别之中的SuspendThread 和ResumeThread 成员函数,可能会发出会心微笑。
CFrame Wnd-取代 WndProc的地位
CFrameWnd 主要用来掌握一个窗口,几乎你可以说它是用来取代SDK 程序中的窗口函式的地位。传统的SDK 窗口函数写法是:
long FAR PASCAL WndProc(HWND hWnd, UNIT msg, WORD wParam, LONG lParam)
{switch(msg) {case WM_COMMAND :switch(wParam) {case IDM_ABOUT :OnAbout(hWnd, wParam, lParam);break;}break;case WM_PAINT :OnPaint(hWnd, wParam, lParam);break;default :DefWindowProc(hWnd, msg, wParam, lParam);}
}
MFC 程序有新的作法,我们在Hello 程序中也为CMyFrameWnd 准备了两个消息处理例程,声明如下:
class CMyFrameWnd : public CFrameWnd
{
public:CMyFrameWnd();afx_msg void OnPaint();afx_msg void OnAbout();DECLARE_MESSAGE_MAP()
};
OnPaint 处理什么消息?OnAbout 又是处理什么消息?我想你很容易猜到,前者处理WM_PAINT ,后者处理WM_COMMAND 的IDM _ABOUT。这看起来十分俐落,但让人搞不懂来龙去脉。程序中是不是应该有「把消息和处理函数关联在一起」的设定动作?是的,这些设定在HELLO.CPP 才看得到。但让我先着一鞭:DECLARE_MESSAGE_MAP宏与此有关。
这种写法非常奇特,原因是MFC 内建了一个所谓的Message Map 机制,会把消息自动送到「与消息对映之特定函数」去;消息与处理函数之间的对映关系由程序员指定。DECLARE_MESSAGE_MAP 另搭配其它宏,就可以很便利地将消息与其处理函数关联在一起:
BEGIN_MESSAGE_MAP(CMyFrameWnd, CFrameWnd)
ON_WM_PAINT()
ON_COMMAND(IDM_ABOUT, OnAbout)
END_MESSAGE_MAP()
稍后我就来探讨这些神秘的宏。
引爆器-Application object
我们已经看过HELLO.H 声明的两个类别,现在把目光转到HELLO.CPP 身上。这个档案将两个类别实作出来,并产生一个所谓的application object。故事就从这里展开。下面这张图包括右半部的Hello 源代码与左半部的MFC 源代码。从这一节以降,我将以此图解释MFC 程序的激活、运行、与结束。不同小节的图将标示出当时的程序进行状况。
上图的theApp就是Hello 程序的application object,每一个MFC 应用程序都有一个,而且也只有这么一个。当你执行Hello,这个全域对象产生,于是构造式执行起来。我们并没有定义CMyWinApp 构造式;至于其父类别CWinApp 的构造式内容摘要如下(摘录自APPCORE.CPP):
CWinApp::CWinApp(LPCTSTR lpszAppName)
{m_pszAppName = lpszAppName;// initialize CWinThread stateAFX_MODULE_THREAD_STATE* pThreadState = AfxGetModuleThreadState();pThreadState->m_pCurrentWinThread = this;m_hThread = ::GetCurrentThread();m_nThreadID = ::GetCurrentThreadId();// initialize CWinApp stateAFX_MODULE_STATE* pModuleState = AfxGetModuleState();pModuleState->m_pCurrentWinApp = this;// in non-running state until WinMainm_hInstance = NULL;m_pszHelpFilePath = NULL;m_pszProfileName = NULL;m_pszRegistryKey = NULL;m_pszExeName = NULL;m_lpCmdLine = NULL;m_pCmdInfo = NULL;...
}
CWinApp 之中的成员变量将因为theApp 这个全域对象的诞生而获得配置与初值。如果程序中没有theApp 存在,编译联结还是可以顺利通过,但执行时会出现系统错误消息:
隐晦不明的 WinMain
theApp 配置完成后,WinMain 登场。我们并未撰写WinMain 程序代码,这是MFC 早已准备好并由联结器直接加到应用程序代码中的,其源代码列于图6-4。_tWinMain 函数的-t是为了支持Unicode 而准备的一个宏。
// in APPMODUL.CPP
extern "C" int WINAPI
_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPTSTR lpCmdLine, int nCmdShow)
{// call shared/exported WinMainreturn AfxWinMain (hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}
// in WINMAIN.CPP
#0001 /
#0002 // Standard WinMain implementation
#0003 // Can be replaced as long as 'AfxWinInit' is called first
#0004
#0005 int AFXAPI AfxWinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
#0006 LPTSTR lpCmdLine, int nCmdShow)
#0007 {
#0008 ASSERT(hPrevInstance == NULL);
#0009
#0010 int nReturnCode = -1;
#0011 CWinApp* pApp = AfxGetApp();
#0012
#0013 // AFX internal initialization
#0014 if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow))
#0015 goto InitFailure;
#0016
#0017 // App global initializations (rare)
#0018 ASSERT_VALID(pApp);
#0019 if (!pApp->InitApplication())
#0020 goto InitFailure;
#0021 ASSERT_VALID(pApp);
#0022
#0023 // Perform specific initializations
#0024 if (!pApp->InitInstance())
#0025 {
#0026 if (pApp->m_pMainWnd != NULL)
#0027 {
#0028 TRACE0("Warning: Destroying non-NULL m_pMainWnd\n");
#0029 pApp->m_pMainWnd->DestroyWindow();
#0030 }
#0031 nReturnCode = pApp->ExitInstance();
#0032 goto InitFailure;
#0033 }
#0034 ASSERT_VALID(pApp);
#0035
#0036 nReturnCode = pApp->Run();
#0037 ASSERT_VALID(pApp);
#0038
#0039 InitFailure:
#0040
#0041 AfxWinTerm();
#0042 return nReturnCode;
#0043 }
Windows 程序进入点。源代码可从MFC 的WINMAIN.CPP 中获得。
稍加整理去芜存菁,就可以看到这个「程序进入点」主要做些什么事:
int AFXAPI AfxWinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,LPTSTR lpCmdLine, int nCmdShow)
{int nReturnCode = -1;CWinApp* pApp = AfxGetApp();AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow);pApp->InitApplication();pApp->InitInstance();nReturnCode = pApp->Run();AfxWinTerm();return nReturnCode;
}
其中,AfxGetApp 是一个全域函数,定义于AFXWIN1.INL 中:
_AFXWIN_INLINE CWinApp* AFXAPI AfxGetApp(){ return afxCurrentWinApp; }
而afxCurrentWinApp 又定义于AFXWIN.H 中:
#define afxCurrentWinApp AfxGetModuleState()->m_pCurrentWinApp
再根据稍早所述CWinApp :: CWinApp 中的动作,我们于是知道,AfxGetApp 其实就是取得CMyWinApp 对象指针。所以,AfxWinMain 中这样的动作:
CWinApp* pApp = AfxGetApp();
pApp->InitApplication();
pApp->InitInstance();
nReturnCode = pApp->Run();
其实就相当于调用:
CMyWinApp::InitApplication();
CMyWinApp::InitInstance();
CMyWinApp::Run();
因而导至调用:
CWinApp::InitApplication(); //因为CMyWinApp 并没有改写InitApplicationCMyWinApp::InitInstance(); //因为CMyWinApp 改写了InitInstanceCWinApp::Run(); //因为CMyWinApp 并没有改写Run根据第1章SDK 程序设计的经验推测,InitApplication 应该是注册窗口类别的场所?InitInstance 应该是产生窗口并显示窗口的场所?Run 应该是攫取消息并分派消息的场所?有对有错!以下数节我将实际带你看看MFC 的源代码,如此一来就可以了解隐藏在MFC 背后的玄妙了。我的终极目标并不在MFC 源代码(虽然那的确是学习设计一个application framework 的好教材),我只是想拿把刀子把MFC 看似朦胧的内部运作来个大解剖,挑出其经脉;有这种扎实的根基,使用MFC 才能知其然并知其所以然。下面小节分别讨论AfxWinMain 的四个主要动作以及引发的行为。
AfxWinInit-AFX 内部初始化动作
我想你已经清楚看到了,AfxWinInit 是继CWinApp 构造式之后的第一个动作。以下是它的动作摘要(节录自APPINIT.CPP):
BOOL AFXAPI AfxWinInit(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPTSTR lpCmdLine, int nCmdShow)
{ASSERT(hPrevInstance == NULL);// set resource handlesAFX_MODULE_STATE* pState = AfxGetModuleState();pState->m_hCurrentInstanceHandle = hInstance;pState->m_hCurrentResourceHandle = hInstance;// fill in the initial state for the applicationCWinApp* pApp = AfxGetApp();if (pApp != NULL){// Windows specific initialization (not done if no CWinApp)pApp->m_hInstance = hInstance;pApp->m_hPrevInstance = hPrevInstance;pApp->m_lpCmdLine = lpCmdLine;pApp->m_nCmdShow = nCmdShow;pApp->SetCurrentHandles();}// initialize thread specific data (for main thread)if (!afxContextIsDLL)AfxInitThread();return TRUE;
}
其中调用的AfxInitThread 函数的动作摘要如下(节录自THRDCORE.CPP):
void AFXAPI AfxInitThread()
{if (!afxContextIsDLL){// attempt to make the message queue biggerfor (int cMsg = 96; !SetMessageQueue(cMsg) && (cMsg -= 8); );// set message filter proc_AFX_THREAD_STATE* pThreadState = AfxGetThreadState();ASSERT(pThreadState->m_hHookOldMsgFilter == NULL);pThreadState->m_hHookOldMsgFilter = ::SetWindowsHookEx (WH_MSGFILTER,_AfxMsgFilterHook, NULL, ::GetCurrentThreadId());// intialize CTL3D for this thread_AFX_CTL3D_STATE* pCtl3dState = _afxCtl3dState;if (pCtl3dState->m_pfnAutoSubclass != NULL)(*pCtl3dState->m_pfnAutoSubclass)(AfxGetInstanceHandle());// allocate thread local _AFX_CTL3D_THREAD just for automatic termination_AFX_CTL3D_THREAD* pTemp = _afxCtl3dThread;}
}
如果你曾经看过本书前身Visual C++ 对象导向MFC 程序设计,我想你可能对这句话印象深刻:「WinMain 一开始即调用AfxWinInit,注册四个窗口类别」。这是一个已成昨日黄花的事实。MFC 的确会为我们注册四个窗口类别,但不再是在AfxWinInit 中完成。稍后我会把注册动作挖出来,那将是窗口诞生前一刻的行为。
CWinApp::InitApplication
AfxWinInit 之后的动作是pApp-> InitApplication 。稍早我说过了,pApp 指向CMyWinApp对象(也就是本例的theApp ),所以,当程序调用:
pApp->InitApplication();
相当于调用:
CMyWinApp::InitApplication();
但是你要知道,CMyWinApp 继承自CWinApp ,而InitApplication 又是CWinApp 的一个虚拟函数;我们并没有改写它(大部份情况下不需改写它),所以上述动作相当于调用:
CWinApp::InitApplication();
此函数之源代码出现在APPCORE.CPP 中:
BOOL CWinApp::InitApplication()
{if (CDocManager::pStaticDocManager != NULL){if (m_pDocManager == NULL)m_pDocManager = CDocManager::pStaticDocManager;CDocManager::pStaticDocManager = NULL;}if (m_pDocManager != NULL)m_pDocManager->AddDocTemplate(NULL);elseCDocManager::bStaticInit = FALSE;return TRUE;
}
这些动作都是MFC 为了内部管理而做的。
关于Document Template 和CDocManager ,第7章和第8章另有说明。
CMyWinApp::InitInstance
继InitApplication 之后,AfxWinMain 调用pApp-> InitInstance 。稍早我说过了,pApp 指向CMyWinApp 对象(也就是本例的theApp ),所以,当程序调用:
pApp->InitInstance();
相当于调用
CMyWinApp::InitInstance();
但是你要知道,CMyWinApp 继承自CWinApp ,而InitInstance 又是CWinApp 的一个虚拟函数。由于我们改写了它,所以上述动作的的确确就是调用我们自己(CMyWinApp)的这个InitInstance 函数。我们将在该处展开我们的主窗口生命。
CFrameWnd::Create 产生主窗口(并先注册窗口类别)
CMyWinApp :: InitInstance 一开始new 了一个CMyFrameWnd 对象,准备用作主框窗口的C++ 对象。new 会引发构造式:
CMyFrameWnd::CMyFrameWnd
{Create(NULL, "Hello MFC", WS_OVERLAPPEDWINDOW, rectDefault, NULL,"MainMenu");
}
其中Create 是CFrameWnd 的成员函数,它将产生一个窗口。但,使用哪一个窗口类别呢?
根据CFrameWnd:: Create 的规格:
BOOL Create( LPCTSTR lpszClassName,
LPCTSTR lpszWindowName,
DWORD dwStyle = WS_OVERLAPPEDWINDOW,
const RECT& rect = rectDefault,
CWnd* pParentWnd = NULL,
LPCTSTR lpszMenuName = NULL,
DWORD dwExStyle = 0,
CCreateContext* pContext = NULL );
八个参数中的后六个参数都有默认值,只有前两个参数必须指定。
第一个参数lpszClassName 指定WNDCLASS 窗口类别,我们放置NULL 究竟代表什么意思?意思是要以MFC 内建的窗口类别产生一个标准的外框窗口。但,此时此刻Hello 程序中根本不存在任何窗口类别呀!噢,Create函数在产生窗口之前会引发窗口类别的注册动作,稍后再解释。
第二个参数 lpszWindowName 指定窗口标题,本例指定"Hello MFC" 。第三个参数dwStyle 指定窗口风格,预设是WS_OVERLAPPEDWINDOW,也正是最常用的一种,它被定义为(在WINDOWS.H 之中):
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | WS_CAPTION |
WS_SYSMENU | WS_THICKFRAME |
WS_MINIMIZEBOX | WS_MAXIMIZEBOX)
因此如果你不想要窗口右上角的极大极小钮,就得这么做:
Create(NULL,
"Hello MFC",
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
rectDefault,
NULL,
"MainMenu");
如果你希望窗口有垂直滚动条,就得在第三个参数上再加增WS_VSCROLL 风格。除了上述标准的窗口风格,另有所谓的扩充风格,可以在Create 的第七个参数dwExStyle 指定之。扩充风格唯有以:: CreateWindowEx(而非:: CreateWindow )函数才能完成。事实上稍后你就会发现,CFrameWnd:: Create 最终调用的正是:: CreateWindowEx 。Windows 3.1 提供五种窗口扩充风格:
WS_EX_DLGMODALFRAME
WS_EX_NOPARENTNOTIFY
WS_EX_TOPMOST
WS_EX_ACCEPTFILES
WS_EX_TRANSPARENT
Windows 95 有更多选择,包括WS_EX_WINDOWEDGE 和WS_EX_CLIENTEDGE,让窗口更具3D 立体感。Framework 已经自动为我们指定了这两个扩充风格。Create 的第四个参数rect 指定窗口的位置与大小。默认值rectDefault 是CFrameWnd的一个static 成员变量,告诉Windows 以预设方式指定窗口位置与大小,就好象在SDK 程序中以CW_USEDEFAULT 指定给CreateWindow 函数一样。如果你很有主见,希望窗口在特定位置有特定大小,可以这么做:
Create(NULL,
"Hello MFC",
WS_OVERLAPPEDWINDOW,
CRect(40, 60, 240, 460), // 起始位置 (40,60) ,寬 200,高 400)
NULL,
"MainMenu");
第五个参数pParentWnd 指定父窗口。对于一个top-level 窗口而言,此值应为NUL L ,表示没有父窗口(其实是有的,父窗口就是desktop 窗口)。
第六个参数lpszMenuName 指定菜单。本例使用一份在RC 中准备好的菜单MainMenu。
第八个参数pContext 是一个指向CCreateContext 结构的指针,framework利用它,在具备Document/View 架构的程序中初始化外框窗口(第8章的「CDocTemplate管理CDocument / CView / CFrameWnd」一节中将谈到此一主题)。本例不具备
Document/View 架构,所以不必指定pContext 参数,默认值为NULL。前面提过,CFrameWnd:: Create 在产生窗口之前,会先引发窗口类别的注册动作。让我再扮一次MFC 向导,带你寻幽访胜。你会看到MFC 为我们注册的窗口类别名称,及注册动作。
WINFRM.CPP
BOOL CFrameWnd::Create(LPCTSTR lpszClassName,
LPCTSTR lpszWindowName,
DWORD dwStyle,
const RECT& rect,
CWnd* pParentWnd,
LPCTSTR lpszMenuName,
DWORD dwExStyle,
CCreateContext* pContext)
{
HMENU hMenu = NULL;
if (lpszMenuName != NULL)
{
// load in a menu that will get destroyed when window gets destroyed
HINSTANCE hInst = AfxFindResourceHandle(lpszMenuName, RT_MENU);
hMenu = ::LoadMenu(hInst, lpszMenuName);
}
m_strTitle = lpszWindowName; // save title for later
CreateEx(dwExStyle, lpszClassName, lpszWindowName, dwStyle,
rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
pParentWnd->GetSafeHwnd(), hMenu, (LPVOID)pContext);
return TRUE;
}
函数中调用CreateEx 。注意,CWnd 有成员函数CreateEx ,但其衍生类别CFrameWnd 并无,所以这里虽然调用的是CFrameWnd:: CreateEx ,其实乃是从父类别继承下来的CWnd::CreateEx 。
WINCORE.CPP
BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,
LPCTSTR lpszWindowName, DWORD dwStyle,
int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)
{
// allow modification of several common create parameters
CREATESTRUCT cs;
cs.dwExStyle = dwExStyle;
cs.lpszClass = lpszClassName;
cs.lpszName = lpszWindowName;
cs.style = dwStyle;
cs.x = x;
cs.y = y;
cs.cx = nWidth;
cs.cy = nHeight;
cs.hwndParent = hWndParent;
cs.hMenu = nIDorHMenu;
cs.hInstance = AfxGetInstanceHandle();
cs.lpCreateParams = lpParam;
PreCreateWindow(cs);
AfxHookWindowCreate(this); //此动作将在第9章探讨。HWND hWnd = ::CreateWindowEx(cs.dwExStyle, cs.lpszClass,
cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);
...
}
函数中调用的PreCreateWindow 是虚拟函数,CWnd 和CFrameWnd 之中都有定义。由于this 指针所指对象的缘故,这里应该调用的是CFrameWnd:: PreCreateWindow(还记得第2章我说过虚拟函数常见的那种行为模式吗?)
WINFRM.CPP
// CFrameWnd second phase creation
BOOL CFrameWnd::PreCreateWindow(CREATESTRUCT& cs)
{
if (cs.lpszClass == NULL)
{
AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG);
cs.lpszClass = _afxWndFrameOrView; // 各个类中定义了不同的precreatewindow ! 使用不同的串口列别
}
...
}
其中AfxDeferRegisterClass 是一个定义于AFXIMPL.H 中的宏。
AFXIMPL.H
#define AfxDeferRegisterClass(fClass) \
((afxRegisteredClasses & fClass) ? TRUE : AfxEndDeferRegisterClass(fClass))
这个宏表示,如果变量afxRegisteredClasses 的值显示系统已经注册了fClass 这种视窗类别,MFC 就啥也不做;否则就调用AfxEndDeferRegisterClass(fClass),准备注册之。afxRegisteredClasses 定义于AFXWIN.H,是一个旗标变量,用来记录已经注册了哪些视窗类别:
// in AFXWIN.H
#define afxRegisteredClasses AfxGetModuleState()->m_fRegisteredClasses
WINCORE.CPP :
#0001 BOOL AFXAPI AfxEndDeferRegisterClass(short fClass)
#0002 {
#0003 BOOL bResult = FALSE;
#0004
#0005 // common initialization
#0006 WNDCLASS wndcls;
#0007 memset(&wndcls, 0, sizeof(WNDCLASS)); // start with NULL defaults
#0008 wndcls.lpfnWndProc = DefWindowProc;
#0009 wndcls.hInstance = AfxGetInstanceHandle();
#0010 wndcls.hCursor = afxData.hcurArrow;
#0011
#0012 AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
#0013 if (fClass & AFX_WND_REG)
#0014 {
#0015 // Child windows - no brush, no icon, safest default class styles
#0016 wndcls.style = CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW;
#0017 wndcls.lpszClassName = _afxWnd;
#0018 bResult = AfxRegisterClass(&wndcls);
#0019 if (bResult)
#0020 pModuleState->m_fRegisteredClasses |= AFX_WND_REG;
#0021 }
#0022 else if (fClass & AFX_WNDOLECONTROL_REG)
#0023 {
#0024 // OLE Control windows - use parent DC for speed
#0025 wndcls.style |= CS_PARENTDC | CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW;
#0026 wndcls.lpszClassName = _afxWndOleControl;
#0027 bResult = AfxRegisterClass(&wndcls);
#0028 if (bResult)
#0029 pModuleState->m_fRegisteredClasses |= AFX_WNDOLECONTROL_REG;
#0030 }
#0031 else if (fClass & AFX_WNDCONTROLBAR_REG)
#0032 {
#0033 // Control bar windows
#0034 wndcls.style = 0; // control bars don't handle double click
#0035 wndcls.lpszClassName = _afxWndControlBar;
#0036 wndcls.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
#0037 bResult = AfxRegisterClass(&wndcls);
#0038 if (bResult)
#0039 pModuleState->m_fRegisteredClasses |= AFX_WNDCONTROLBAR_REG;
#0040 }
#0041 else if (fClass & AFX_WNDMDIFRAME_REG)
#0042 {
#0043 // MDI Frame window (also used for splitter window)
#0044 wndcls.style = CS_DBLCLKS;
#0045 wndcls.hbrBackground = NULL;
#0046 bResult = RegisterWithIcon(&wndcls,_afxWndMDIFrame,AFX_IDI_STD_MDIFRAME);
#0047 if (bResult)
#0048 pModuleState->m_fRegisteredClasses |= AFX_WNDMDIFRAME_REG;
#0049 }
#0050 else if (fClass & AFX_WNDFRAMEORVIEW_REG)
#0051 {
#0052 // SDI Frame or MDI Child windows or views - normal colors
#0053 wndcls.style = CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW;
#0054 wndcls.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);
#0055 bResult = RegisterWithIcon(&wndcls, _afxWndFrameOrView,
AFX_IDI_STD_FRAME);
#0056 if (bResult)
#0057 pModuleState->m_fRegisteredClasses |= AFX_WNDFRAMEORVIEW_REG;
#0058 }
#0059 else if (fClass & AFX_WNDCOMMCTLS_REG)
#0060 {
#0061 InitCommonControls();
#0062 bResult = TRUE;
#0063 pModuleState->m_fRegisteredClasses |= AFX_WNDCOMMCTLS_REG;
#0064 }
#0065
#0066 return bResult;
#0067 }
出现在上述函数中的六个窗口类别卷标代码,分别定义于AFXIMPL.H 中:
#define AFX_WND_REG (0x0001)
#define AFX_WNDCONTROLBAR_REG (0x0002)
#define AFX_WNDMDIFRAME_REG (0x0004)
#define AFX_WNDFRAMEORVIEW_REG (0x0008)
#define AFX_WNDCOMMCTLS_REG (0x0010)
#define AFX_WNDOLECONTROL_REG (0x0020)
出现在上述函数中的五个窗口类别名称,分别定义于WINCORE.CPP 中:
const TCHAR _afxWnd[] = AFX_WND;
const TCHAR _afxWndControlBar[] = AFX_WNDCONTROLBAR;
const TCHAR _afxWndMDIFrame[] = AFX_WNDMDIFRAME;
const TCHAR _afxWndFrameOrView[] = AFX_WNDFRAMEORVIEW;
const TCHAR _afxWndOleControl[] = AFX_WNDOLECONTROL;
而等号右手边的那些AFX_ 常数又定义于AFXIMPL.H 中:
#ifndef _UNICODE
#define _UNICODE_SUFFIX
#else
#define _UNICODE_SUFFIX _T("u")
#endif
#ifndef _DEBUG
#define _DEBUG_SUFFIX
#else
#define _DEBUG_SUFFIX _T("d")
#endif
#ifdef _AFXDLL
#define _STATIC_SUFFIX
#else
#define _STATIC_SUFFIX _T("s")
#endif
#define AFX_WNDCLASS(s) \
_T("Afx") _T(s) _T("42") _STATIC_SUFFIX _UNICODE_SUFFIX _DEBUG_SUFFIX
#define AFX_WND AFX_WNDCLASS("Wnd")
#define AFX_WNDCONTROLBAR AFX_WNDCLASS("ControlBar")
#define AFX_WNDMDIFRAME AFX_WNDCLASS("MDIFrame")
#define AFX_WNDFRAMEORVIEW AFX_WNDCLASS("FrameOrView")
#define AFX_WNDOLECONTROL AFX_WNDCLASS("OleControl")
所以,如果在Windows 95(non-Unicode)中使用MFC 动态联结版和除错版,五个窗口类别的名称将是:
"AfxWnd42d"
"AfxControlBar42d"
"AfxMDIFrame42d"
"AfxFrameOrView42d"
"AfxOleControl42d"
如果在Windows NT(Unicode 环境)中使用MFC 静态联结版和除错版,五个窗口类别的名称将是:
"AfxWnd42sud"
"AfxControlBar42sud"
"AfxMDIFrame42sud"
"AfxFrameOrView42sud"
"AfxOleControl42sud"
这五个窗口类别的使用时机为何?稍后再来一探究竟。
让我们再回顾AfxEndDeferRegisterClass 的动作。它调用两个函数完成实际的窗口类别注册动作,一个是RegisterWithIcon ,一个是AfxRegisterClass :
static BOOL AFXAPI RegisterWithIcon(WNDCLASS* pWndCls,
LPCTSTR lpszClassName, UINT nIDIcon)
{
pWndCls->lpszClassName = lpszClassName;
HINSTANCE hInst = AfxFindResourceHandle(
MAKEINTRESOURCE(nIDIcon), RT_GROUP_ICON);
if ((pWndCls->hIcon = ::LoadIcon(hInst, MAKEINTRESOURCE(nIDIcon))) == NULL)
{
// use default icon
pWndCls->hIcon = ::LoadIcon(NULL, IDI_APPLICATION);
}
return AfxRegisterClass(pWndCls);
}
BOOL AFXAPI AfxRegisterClass(WNDCLASS* lpWndClass)
{
WNDCLASS wndcls;
if (GetClassInfo(lpWndClass->hInstance,
lpWndClass->lpszClassName, &wndcls))
{
// class already registered
return TRUE;
}
::RegisterClass (lpWndClass);
...
return TRUE;
}
注意,不同类别的PreCreateWindow 成员函数都是在窗口产生之前一刻被调用,准备用来注册窗口类别。如果我们指定的窗口类别是NULL,那么就使用系统预设类别。从CWnd及其各个衍生类别的PreCreateWindow 成员函数可以看出,整个Framework 针对不同功能的窗口使用了哪些窗口类别:
// in WINCORE.CPP
BOOL CWnd::PreCreateWindow(CREATESTRUCT& cs)
{
{
AfxDeferRegisterClass(AFX_WND_REG);
...
cs.lpszClass = _afxWnd; (这表示CWnd 使用的窗口类别是_afxWnd)}
return TRUE;
}
// in WINFRM.CPP
BOOL CFrameWnd::PreCreateWindow(CREATESTRUCT& cs)
{
if (cs.lpszClass == NULL)
{
AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG);
...
cs.lpszClass = _afxWndFrameOrView; (这表示CFrameWnd 使用的窗口类别是_afxWndFrameOrView)
}
...
}
// in WINMDI.CPP
BOOL CMDIFrameWnd::PreCreateWindow(CREATESTRUCT& cs)
{
if (cs.lpszClass == NULL)
{
AfxDeferRegisterClass(AFX_WNDMDIFRAME_REG);
...
cs.lpszClass = _afxWndMDIFrame; (这表示C M D I F r a m e W n d 使用的窗口类别是_afxWndMDIFrame)}
return TRUE;
}
// in WINMDI.CPP
BOOL CMDIChildWnd::PreCreateWindow(CREATESTRUCT& cs)
{
...
return CFrameWnd::PreCreateWindow(cs); (这表示CMDIChildWnd 使用的窗口类别_afxWndFrameOrView)}
// in VIEWCORE.CPP
BOOL CView::PreCreateWindow(CREATESTRUCT & cs)
{
if (cs.lpszClass == NULL)
{
AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG);
...
cs.lpszClass = _afxWndFrameOrView; (这表示CView 使用的窗口类别是_afxWndFrameOrView)}
...
}
奇怪的窗口类别名称 Afx:b:14ae:6:3e8f
当应用程序调用CFrameWnd::Create(或CMDIFrameWnd:: LoadFrame ,第7章)准备产生窗口时,MFC 才会在Create 或LoadFrame 内部所调用的PreCreateWindow 虚拟函式中为你产生适当的窗口类别。你已经在上一节看到了,这些窗口类别的名称分别是(假设在Win95 中使用MFC 4.2 动态联结版和除错版):
"AfxWnd42d"
"AfxControlBar42d"
"AfxMDIFrame42d"
"AfxFrameOrView42d"
"AfxOleControl42d"
然而,当我们以Spy++ (VC++ 所附的一个工具)观察窗口类别的名称,却发现:
窗口显示与更新
CMyFrameWnd::CMyFrameWnd 结束后,窗口已经诞生出来;程序流程又回到CMyWinApp::InitInstance ,于是调用ShowWindow 函数令窗口显示出来,并调用UpdateWindow 函数令Hello 程序送出WM_PAINT 消息。我们很关心这个WM_PAINT 消息如何送到窗口函数的手中。而且,窗口函数又在哪里?MFC 程序是不是也像SDK 程序一样,有一个GetMessage/DispatchMesage 循环?是否每个窗口也都有一个窗口函数,并以某种方式进行消息的判断与处理?两者都是肯定的。我们马上来寻找证据。
六、MFC 程序的生死因果 (学习笔记)相关推荐
- 深入浅出MFC学习笔记(第6章 :MFC程序的生死因果)
第六章:MFC程序的生死因果 本章主要是从MFC程序代码中,找出一个windows程序原本该有的程序入口点.窗口类注册.窗口产生.消息循环.窗口函数等操作.抽丝剥茧彻底理解一个MFC程序的诞生与结束. ...
- 《深入浅出MFC》第六章 MFC程序的生死因果
SDK程序设计的第一要务是理解最重要的数个API函数的意义和用法,MFC程序设计的第一要务则是理解几个最重要的类,最基本的两个类为CWin App和CFrameWnd. 开发MFC程序需要的函数库:W ...
- MFC程序的生死因果
MFC程序的来龙去脉(causal relations) MFC的程序如何运行,第一件事情就是找出MFC程序的进入点.MFC程序也是Windows程序.所以它应该也有一个WinMain.但在程序中好 ...
- 深入浅出mfc随笔——MFc程序的生死因果
1:窗口的显示与更新 CMyWinApp theApp___AfxWinInit___pApp->Initapplication____pApp->InitInstance____m_pM ...
- Java程序猿的JavaScript学习笔记(12——jQuery-扩展选择器)
计划按例如以下顺序完毕这篇笔记: Java程序猿的JavaScript学习笔记(1--理念) Java程序猿的JavaScript学习笔记(2--属性复制和继承) Java程序猿的JavaScript ...
- Java程序猿的JavaScript学习笔记(汇总文件夹)
最终完结了,历时半个月. 内容包含: JavaScript面向对象特性分析,JavaScript高手必经之路. jQuery源代码级解析. jQuery EasyUI源代码级解析. Java程序猿的J ...
- Java程序猿的JavaScript学习笔记(10—— jQuery-在“类”层面扩展)
计划按例如以下顺序完毕这篇笔记: Java程序猿的JavaScript学习笔记(1--理念) Java程序猿的JavaScript学习笔记(2--属性复制和继承) Java程序猿的JavaScript ...
- 计算机、程序和 Java 概述 学习笔记
计算机.程序和java概述 学习笔记 1.1什么是计算机 简单来说:计算机就是 ' 存储 ' 和 ' 处理 ' 数据的电子设备. 计算机包括硬件( hardware ) 和软件( software) ...
- 面向对象的编程思想写单片机程序——(3)学习笔记 之 程序分层、数据产生流程
系列文章目录 面向对象的编程思想写单片机程序--(1)学习笔记 之 程序设计 面向对象的编程思想写单片机程序--(2)学习笔记 之 怎么抽象出结构体 面向对象的编程思想写单片机程序--(3)学习笔记 ...
最新文章
- Dedecms5.7搜索结果页空白无内容的解决方法
- NandFlash详述【转】
- [20150205]分析函数ntile.txt
- RestTemplate设置通用header
- GitHub Research:超过50%的Java记录语句写错了
- prompt你到底行不行?
- Xshell报错“The remote SSH server rejected X11 forwarding request.”
- @scheduled注解配置时间_Spring Boot中使用@Scheduled创建定时任务
- java 经典免费教程下载
- less+rem迭代适配
- 【转载】Python tips: 什么是*args和**kwargs?
- Linux服务器配置多台虚拟主机
- 计算图的可达矩阵MATLAB程序
- springboot整合mongodb
- IE浏览器兼容性问题!(按alt+x+b不弹出兼容性窗口)
- 利用极小极大搜索和alpha-beta剪枝算法预测五子棋对弈落子
- 《软技能——代码之外的生存指南》读书笔记之职业(一)
- 云计算机是不是虚拟机,云计算和虚拟机(VMWare)有什么区别?
- 数字图像处理|Matlab-数字图像编码实验-有损压缩/压缩算法实验-JPEG编码压缩
- 车联网上云最佳实践学习笔记
热门文章
- Java中关于数组的初始化方式
- 目标检测:二维码检测方案
- 计算机网络技术课程答案网课,《计算机网络技术》大学生网课答案.docx
- thawte,globalsign,alphassl,rapidssl,geotrust,digicert证书品牌的对照
- UserWarning: Glyph 30005 (\N{CJK UNIFIED IDEOGRAPH-7535}) missing from current font解决方式方法
- FFMpeg.AutoGen(1)讲解官方example代码:Main函数、 解码
- 单片机用c语言编写测量波形频率和占空比,单片机测量方波的频率、占空比及相位差的方法...
- 中国传统文化2022秋雨课堂期末测试答案
- 常见的继电接触器控制线路总结
- 支持DoH的DNS服务器,谷歌公共 DNS 服务器正式支持 DoH 加密