首页 > 代码库 > VC++编程之第一课笔记

VC++编程之第一课笔记

第一课 Windows程序内部运行原理


API

操作系统把它所能够完成的功能以函数的形式提供给应用程序使用,应用程序对这些函数的调用就叫做系统调用。这些函数的集合就是Windows操作系统提供给应用程序编程的接口(Application Programming Interface),简称Windows API

Create Window就是一个API函数,应用程序调用这个函数,操作系统就会按照该函数提供的参数信息产生一个相应的窗口。


MSG(消息结构体)

结构体定义如下:

typedef struct tagMSG {
    HWND        hwnd;
    UINT        message;
    WPARAM      wParam;
    LPARAM      lParam;
    DWORD       time;
    POINT       pt;
#ifdef _MAC
    DWORD       lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

传递的消息,宏以"WM_"开头

hwnd,资源的标识

细分:

  • 图标句柄(HICON)

  • 光标句柄(HCURSOR)

  • 窗口句柄(HWND)

  • 应用程序实例句柄(HINSTANCE)

message

比如说,敲击‘A‘键,它只传递了WM_KEYDOWN的消息,具体参数在下面的参数内:

  • wParam:包含了敲下的键的信息,WM_CHAR(ASCII值)

  • lParam

  • time:消息传递过去的时间

  • pt:消息传递时鼠标所处坐标


WINMAIN函数

Windows程序的入口函数:

int WINAPI WinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    PSTR szCmdLine,
    int iCmdShow
)

hPrevInstance

同一程序执行的实例,指向前一个实例的句柄,现已几乎不用

szCmdLine

类似于C程序main函数的命令行参数argc与argv,sz表示以零结束的字符串

nCmdShow

指定程序运行时的显示状态,是最大化还是最小化亦或是正常

注释:此函数由操作系统调用,因此以上的参数都是由操作系统赋值


窗口

窗口类的结构体:

typedef struct tagWNDCLASSA {
    UINT        style;
    WNDPROC     lpfnWndProc;
    int         cbClsExtra;
    int         cbWndExtra;
    HINSTANCE   hInstance;
    HICON       hIcon;
    HCURSOR     hCursor;
    HBRUSH      hbrBackground;
    LPCSTR      lpszMenuName;
    LPCSTR      lpszClassName;
} WNDCLASSA, *PWNDCLASSA, NEAR *NPWNDCLASSA, FAR *LPWNDCLASSA;

style(窗口类的类型)

CLASS STYLE,宏以"CS_"开头。一般赋值"CS_HREDRAW | CS_VREDRAW",使窗口在水平或垂直坐标发生改变时重画。

lpfnWndProc(窗口过程函数,回调函数)

此为函数指针。当应用程序收到某一窗口的消息时,就调用所指向的函数来处理这条消息。这一调用过程不用应用程序自己来实施,而有操作系统去调用这个函数。但是,回调函数本身的代码必须由我们自己写上去。

cbClsExtra

为整个窗口类申请额外的内存空间,被属于这个窗口类的所有窗口共享。一般设为零。

cbWndExtra

指定一定的内存分配给这个窗口,称为窗口附加内存。通常也设为零。

hInstance

实例号,代表了当前应用程序的实例号。一般将WinMain函数中的hInstance赋给它。

hIcon(图标句柄)

使用LoadIcon函数:

WINUSERAPI HICON WINAPI LoadIconA(
    HINSTANCE hInstance,
    LPCSTR lpIconName
);

使用微软给我们提供的标准图标时,将hInstance设为NULL。微软给我们提供的图标名称是以"IDI_"开头的,比如说IDI_APPLICATION,IDI_QUESTION

hCursor(光标句柄)

与图标类似,也使用LoadCursor函数获取光标,只是名称以"IDC_"开头。

hbrBackground(画刷的句柄)

使用GetStockObject函数,获得的句柄需要强制转换。其所用的宏一般以"_BRUSH"结尾,代表各种颜色的画刷。例子:

wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;

lpszMenuName(菜单的名字)

lpszClassName(窗口类的名字)


窗口创建步骤:

1. 设计一个窗口类

将上面的参数赋值

2. 注册窗口类

使用RegisterClass函数对设计的窗口进行创建,一般都会创建成功。

WINUSERAPI ATOM WINAPI RegisterClassA(
    CONST WNDCLASSA *lpWndClass
);

例子:

RegisterClass(&wndclass);

3. 创建窗口

使用CreateWindow函数进行窗口的创建。创建成功后会返回一个HWND类型的句柄,用以显示及更新窗口。 函数定义如下:

WINUSERAPI HWND WINAPI CreateWindowExA(
    DWORD dwExStyle,
    LPCSTR lpClassName,
    LPCSTR lpWindowName,
    DWORD dwStyle,
    int X,
    int Y,
    int nWidth,
    int nHeight,
    HWND hWndParent ,
    HMENU hMenu,
    HINSTANCE hInstance,
    LPVOID lpParam
);
#define CreateWindowA(lpClassName, lpWindowName, dwStyle, x, y,nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam)CreateWindowExA(0L, lpClassName, lpWindowName, dwStyle, x, y,nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam)

#ifdef UNICODE
#define CreateWindow  CreateWindowW
#else
#define CreateWindow  CreateWindowA
#endif // !UNICODE

其中:

lpClassName(窗口的名字)

使用上面赋值好的即可

lpWindowName(标题)

即窗口左上角的标题名

dwStyle(窗口的类型)

要与上面的窗口类的类型区分开,这个所取的宏值是以"WS_"开头的。一般使用WS_OVERLAPPEDWINDOW

#define WS_OVERLAPPEDWINDOW (
    WS_OVERLAPPED     |     WS_CAPTION        |     WS_SYSMENU        |     WS_THICKFRAME     |     WS_MINIMIZEBOX    |     WS_MAXIMIZEBOX
)

上面的宏定义依次为:可层叠、具有标题栏、具有系统菜单、可调边框、具有最小化按钮、具有最大化按钮。

x,y(窗口左上角的坐标)

这两个值一般使用CW_DEFAULT宏,表示系统给它的缺省值。

nWidth,nHeigth(窗口的宽度和高度)

也可使用缺省的CW_DEFAULT值。

hWndParent(父窗口句柄)
hMenu(菜单句柄)
hInstance(当前实例句柄)

WinMain函数中的hInstance参数赋给它即可

lpParam(附加参数)

作为一个窗口创建时WM_CREATE消息中的lParam参数的指针。在创建多文档接口(MDI)客户窗口的时候,此指针必须指向CLIENTCREATESTRUCT结构体。用不上的时候设置为NULL即可。

4. 显示及更新窗口

使用ShowWindow函数显示窗口,UpdateWindow函数刷新窗口。ShowWindow函数定义如下:

WINUSERAPI BOOL WINAPI ShowWindow(
    HWND hWnd,
    int nCmdShow
);

其中:

HWND(显示的窗口的句柄)

赋值上一步骤中得到的句柄即可

nCmdShow(窗口的显示状态)

使用的宏定义是以"SW_"开头的,正常显示为SW_SHOWNORMAL


消息循环

使用GetMessage函数从消息队列中取消息。函数定义如下:

WINUSERAPI BOOL WINAPI GetMessageA(
    LPMSG lpMsg,
    HWND hWnd ,
    UINT wMsgFilterMin,
    UINT wMsgFilterMax
);

其中:

lpMsg

指向MSG结构体的指针。它有out的标识,说明在进行传参的时候不需要对这个结构体的内部成员进行初始化,只需要定义一个结构体变量并将其地址放在这里就可以了。通过函数调用,会自动填充结构体内部的成员变量。

hWnd

表示获得哪个窗口的消息。设为NULL表示获得调用线程的属于任何窗口的消息。

wMsgFilterMin

设定消息队列中获得的消息的最小值。可以使用WM_KEYFIRST指定第一个键盘的消息,WM_MOUSEFIRST指定第一个鼠标的消息。

wMsgFilterMax

与上一参数相反。可以使用WM_KEYLAST指定最大的键盘的消息。

由于函数返回BOOl值,当从消息队列中取得消息时返回的值为真,当满足为假的条件时,程序就会退出了。因此可以用如下代码得到消息循环:

while (GetMessage (&msg, NULL, 0, 0))
{
    TranslateMessage (&msg);
    DispatchMessage (&msg);
}

其中:

TranslateMessage函数

用户按下按键时会产生WM_KEYDOWNWM_KEYUP消息,同时所按按键的字母会在附加参数wParam中。

这个函数会将这两个消息转换为一个WM_CHAR消息,并投入到消息队列中。整个过程中不会影响原来的消息,只会产生一个新的消息。

DispatchMessage函数

它会将收到的消息传到窗口的回调函数中去处理。它先将其分发给操作系统,然后操作系统会利用回调函数来处理这个消息。


消息处理函数(回调函数)

它返回的是long型。它的参数与消息结构提的前四个参数一样。函数定义:

typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);

其中:

HWND hWnd;
UINT uMsg;
WPARAM wParam;
LPARAM lParam;

MessageBox函数

这个比较熟悉,其返回值是点击的BUTTON的值,其他的就不多介绍了。函数原型:

WINUSERAPI int WINAPI MessageBoxA(
    HWND hWnd ,
    LPCSTR lpText,
    LPCSTR lpCaption,
    UINT uType
);

其中:

hWnd

所属窗口的句柄

lpText

要显示的内容

lpCation

消息框的标题

uType

消息框包含的BUTTON的类型,其定义的宏是以"MB_"开头的,比如MB_OK、MB_OKCANCEL


窗口的重绘

窗口在改变后进行刷新重绘的时候会发送一个WM_PAINT消息,消息处理函数收到这个消息时会调用BeginPaintEndPaint函数对窗口进行重绘。且这两个函数只能在响应WM_PAINT消息时使用。

case WM_PAINT:
    hDC = BeginPaint(hWnd, &ps);
    ···
    EndPaint(hWnd, &ps);

DC(Device Context)设备描述表是一个用来确定任何设备的GDI输出的位置和形象的属性的集合。

下面是BeginPaint函数的原型:

WINUSERAPI HDC WINAPI BeginPaint(
    HWND hWnd,
    LPPAINTSTRUCT lpPaint
);

其中lpPaint是一个指向PAINTSTRUCT结构的指针,它也是out标识的,即此结构体系统会自动填充的,不用我们去关心。


在窗口中显示文字

1. 获得设备环境句柄(HDC)

先使用GetDC函数获得设备环境句柄(HDC)

2. 再使用TextOut函数在窗口输出文字

函数原型如下:

    WINGDIAPI BOOL  WINAPI TextOutA(
        HDC hdc,
        int nXStart,
        int nYStart,
        LPCSTR lpString,
        int cbString
    );

其中:

hdc

即第一步得到的设备环境句柄

nXStart,nYStart

文本开始的坐标,此坐标相对于窗口左上角(原点)

lpString

输出的字符串

cbString

输出的字符串的位数

3. 使用Release函数释放HDC句柄

例子:

HDC hDC = GetDC(hWnd);
TextOut(hDC, 0, 50, TEXT("title"), TEXT("one words"));
ReleaseDC(hWnd, hDC);

窗口的关闭

示例代码:

case WM_CLOSE:
    if (IDYES == MessageBox(hWnd, TEXT("Quit this program?"), TEXT("QUESTION"), MB_YESNO))
    {
        DestroyWindow(hWnd);
    }
    break;
case WM_DESTROY:
    PostQuitMessage(0);
    break;
default:
    return DefWindowProc(hWnd, uMsg, wParam, lParam);

当窗口即将关闭,即你想要退出程序,点击右上角的"x"时,会发送WM_CLOSE消息,继而调用DestroyWindow函数。

DestroyWindow函数会把窗口销毁,再发送WM_DESTROY、WM_NCDESTROY消息使窗口无效并移除其键盘焦点。这个函数还销毁窗口的菜单,清空线程的消息队列,销毁与窗口过程相关的定时器,解除窗口对剪贴板的拥有权,打断剪贴板器的查看链。

PostQuitMessage函数功能:该函数向系统表明有个线程有终止请求,发送WM_QUIT消息到线程的消息队列中。其参数为退出代码,会作为WM\QUIT消息的附加参数放入wParam中。

而上面的消息循环中的GetMessage函数收到WM_QUIT消息会返回零值,从而终止消息循环。循环终止时,WinMain函数就会退出了。

在上面的额示例代码中我们还看到了DefWindowProc函数,它是缺省的消息处理函数,也是必不可少的。

注意:DestroyWindow函数会将窗口销毁,但是还并没有退出程序。


两种调用约定

1. _stdll调用

_stdcall是Pascal程序的缺省调用方式,参数采用从右到左的压栈方式,被调函数自身在返回前清空堆栈。

WIN32 API都采用_stdcall调用方式,这样的宏定义说明了问题:

#define CALLBACK    __stdcall
#define WINAPI      __stdcall
#define WINAPIV     __cdecl
#define APIENTRY    WINAPI
#define APIPRIVATE  __stdcall
#define PASCAL      __stdcall

按C编译方式,_stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数。

2. _cdecl调用

_cdecl是C/C++的缺省调用方式,参数采用从右到左的压栈方式,传送参数的内存栈由调用者维护。

_cedcl约定的函数只能被C/C++调用,每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。

由于_cdecl调用方式的参数内存栈由调用者维护,所以变长参数的函数能(也只能)使用这种调用约定。

由于Visual C++默认采用_cdecl调用方式,所以VC中中调用DLL时,用户应使用_stdcall调用约定。

按C编译方式,_cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。

3. _fastcall调用

_fastcall调用较快,它通过CPU内部寄存器传递参数。

按C编译方式,_fastcall调用约定在输出函数名前面加“@”符号,后面加“@”符号和参数的字节数,形如@functionname@number。

4. _stdcall与_cdecl的区别

WINDOWS的函数调用时需要用到栈(STACK,一种先入后出的存储结构)。当函数调用完成后,栈需要清除,这里就是问题的关键,如何清除??

如果我们的函数使用了_cdecl,那么栈的清除工作是由调用者,用COM的术语来讲就是客户来完成的。这样带来了一个棘手的问题,不同的编译器产生栈的方式不尽相同,那么调用者能否正常的完成清除工作呢?答案是不能。

如果使用_stdcall,上面的问题就解决了,函数自己解决清除工作。所以,在跨(开发)平台的调用中,我们都使用_stdcall(虽然有时是以WINAPI的样子出现)。

那么为什么还需要_cdecl呢?

当我们遇到这样的函数如fprintf()它的参数是可变的,不定长的,被调用者事先无法知道参数的长度,事后的清除工作也无法正常的进行,因此,这种情况我们只能使用_cdecl

到这里我们有一个结论,如果你的程序中没有涉及可变参数,最好使用_stdcall关键字。


本文出自 “zero4eva” 博客,请务必保留此出处http://zero4eva.blog.51cto.com/7558298/1580296

VC++编程之第一课笔记