MFC程序的生死因果

项目HELLO
理想如果不向实际做点妥协,理想就会归于尘土。

中华民国还得十次革命才得建立,对象导向怎能把一切传统都抛开。

以传统的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 程序的生死因果 (学习笔记)相关推荐

  1. 深入浅出MFC学习笔记(第6章 :MFC程序的生死因果)

    第六章:MFC程序的生死因果 本章主要是从MFC程序代码中,找出一个windows程序原本该有的程序入口点.窗口类注册.窗口产生.消息循环.窗口函数等操作.抽丝剥茧彻底理解一个MFC程序的诞生与结束. ...

  2. 《深入浅出MFC》第六章 MFC程序的生死因果

    SDK程序设计的第一要务是理解最重要的数个API函数的意义和用法,MFC程序设计的第一要务则是理解几个最重要的类,最基本的两个类为CWin App和CFrameWnd. 开发MFC程序需要的函数库:W ...

  3. MFC程序的生死因果

    MFC程序的来龙去脉(causal relations)  MFC的程序如何运行,第一件事情就是找出MFC程序的进入点.MFC程序也是Windows程序.所以它应该也有一个WinMain.但在程序中好 ...

  4. 深入浅出mfc随笔——MFc程序的生死因果

    1:窗口的显示与更新 CMyWinApp theApp___AfxWinInit___pApp->Initapplication____pApp->InitInstance____m_pM ...

  5. Java程序猿的JavaScript学习笔记(12——jQuery-扩展选择器)

    计划按例如以下顺序完毕这篇笔记: Java程序猿的JavaScript学习笔记(1--理念) Java程序猿的JavaScript学习笔记(2--属性复制和继承) Java程序猿的JavaScript ...

  6. Java程序猿的JavaScript学习笔记(汇总文件夹)

    最终完结了,历时半个月. 内容包含: JavaScript面向对象特性分析,JavaScript高手必经之路. jQuery源代码级解析. jQuery EasyUI源代码级解析. Java程序猿的J ...

  7. Java程序猿的JavaScript学习笔记(10—— jQuery-在“类”层面扩展)

    计划按例如以下顺序完毕这篇笔记: Java程序猿的JavaScript学习笔记(1--理念) Java程序猿的JavaScript学习笔记(2--属性复制和继承) Java程序猿的JavaScript ...

  8. 计算机、程序和 Java 概述 学习笔记

    计算机.程序和java概述 学习笔记 1.1什么是计算机 简单来说:计算机就是 ' 存储 ' 和 ' 处理 ' 数据的电子设备. 计算机包括硬件( hardware ) 和软件( software) ...

  9. 面向对象的编程思想写单片机程序——(3)学习笔记 之 程序分层、数据产生流程

    系列文章目录 面向对象的编程思想写单片机程序--(1)学习笔记 之 程序设计 面向对象的编程思想写单片机程序--(2)学习笔记 之 怎么抽象出结构体 面向对象的编程思想写单片机程序--(3)学习笔记 ...

最新文章

  1. Dedecms5.7搜索结果页空白无内容的解决方法
  2. NandFlash详述【转】
  3. [20150205]分析函数ntile.txt
  4. RestTemplate设置通用header
  5. GitHub Research:超过50%的Java记录语句写错了
  6. prompt你到底行不行?
  7. Xshell报错“The remote SSH server rejected X11 forwarding request.”
  8. @scheduled注解配置时间_Spring Boot中使用@Scheduled创建定时任务
  9. java 经典免费教程下载
  10. less+rem迭代适配
  11. 【转载】Python tips: 什么是*args和**kwargs?
  12. Linux服务器配置多台虚拟主机
  13. 计算图的可达矩阵MATLAB程序
  14. springboot整合mongodb
  15. IE浏览器兼容性问题!(按alt+x+b不弹出兼容性窗口)
  16. 利用极小极大搜索和alpha-beta剪枝算法预测五子棋对弈落子
  17. 《软技能——代码之外的生存指南》读书笔记之职业(一)
  18. 云计算机是不是虚拟机,云计算和虚拟机(VMWare)有什么区别?
  19. 数字图像处理|Matlab-数字图像编码实验-有损压缩/压缩算法实验-JPEG编码压缩
  20. 车联网上云最佳实践学习笔记

热门文章

  1. Java中关于数组的初始化方式
  2. 目标检测:二维码检测方案
  3. 计算机网络技术课程答案网课,《计算机网络技术》大学生网课答案.docx
  4. thawte,globalsign,alphassl,rapidssl,geotrust,digicert证书品牌的对照
  5. UserWarning: Glyph 30005 (\N{CJK UNIFIED IDEOGRAPH-7535}) missing from current font解决方式方法
  6. FFMpeg.AutoGen(1)讲解官方example代码:Main函数、 解码
  7. 单片机用c语言编写测量波形频率和占空比,单片机测量方波的频率、占空比及相位差的方法...
  8. 中国传统文化2022秋雨课堂期末测试答案
  9. 常见的继电接触器控制线路总结
  10. 支持DoH的DNS服务器,谷歌公共 DNS 服务器正式支持 DoH 加密