15天学习C语言Windows程序设计.docx
15天学习C语言Windows程序设计 目录 1.C语言Windows程序设计->第一天->第一个Windows程序. - 2 - 2.C语言Windows程序设计->第二天->ASCII与Unicode . - 6 - 3.C语言Windows程序设计->第二天->宽字符和C语言 . - 7 - 4.C语言Windows程序设计->第三天->Windows版printf . - 10 - 5.C语言Windows程序设计->第三天->属于自己的窗口. - 12 - 6.C语言Windows程序设计->第四天->详解我的窗口(上) . - 16 - 7.C语言Windows程序设计->第四天->详解我的窗口(中) . - 20 - 8.C语言Windows程序设计->第四天->详解我的窗口(下) . - 24 - 9.C语言Windows程序设计->第五天->回顾与反思. - 28 - 10.C语言Windows程序设计->第六天->GDI与设备环境. - 29 - 11.C语言Windows程序设计->第七天->TextOut与系统字体. - 32 - 12.C语言Windows程序设计->第八天->滚动条. - 38 - 13.C语言Windows程序设计->第九天->GDI绘图基础. - 57 - 14.C语言Windows程序设计->第十天->响应键盘事件. - 67 - 15.C语言Windows程序设计->第十一天->使用鼠标. - 73 - 16.C语言Windows程序设计->第十二天->使用计时器. - 80 - 17.C语言Windows程序设计->第十三天->按钮类控件. - 88 - 18.C语言Windows程序设计->第十四天->窗口、编辑框样式. - 98 - 19.C语言Windows程序设计->第十五天->文本输入框. - 99 - 20.C语言Windows程序设计-实战:png图片的解析与显示. - 110 - C语言Windows程序设计->第一天->第一个Windows程序 在Windows程序设计(第五版)第一章的起步中, 作者介绍了学习Windows程序设计的一些基本要求: 1. 能够从用户角度熟练的使用Windows; 2. 懂得如何使用C语言; 3. 安装好了Windows的开发环境. 看起来要求并不算高(怎么样?一起来尝试下?)。笔者在这里决定使用Visual C+ 6.0作为开发环境, 虽说在Visual Studio这个大家族中, VC+6早已被长江后浪推前浪, 把VC+6推成了一个将近淘汰的环境, 但是作者的机器实在是有点不够给力, 启动VS2022时相对比较慢。总之, 既然Charles Petzold也假定我会用Visual C+ 6.0, 那么我就用VC+6好了。 ·介绍Windows *Windows的历史: 是的, 你不用惊讶, Windows在这里就是指的微软(Microsoft)的那个操作系统, Windows的历史如果要详细介绍的话, 我觉得可能要单开个随笔分类才行, 所以这里就简 略的介绍下, 不过我还是建议你去搜索引擎查找下关于Windows的历史(如果你认为有必要的话)。 1>. 1985年11月, Windows 1.0正式推出; IBM与Microsoft共同开发, 基于DOS系统,通过DOS来进行文件操作, 当然, 2.0、3.0也都是基于DOS的, 直到Windows 2000的发布,Windows才彻底的摆脱了DOS,成为真正独立的操作系统。 2>. 1987年11月, Windows 2.0推向市场;相对于1.0, 2.0在界面上做了些改动, 采用了重叠窗口。 3>. 1990年 5月, Windows 3.0推向市场;支持Intel 286、386、486微处理器的16位保护模式。 4>. 1993年 7月, Windows NT投放市场;Windows家族中第一个支持Intel 386、486、奔腾微处理器32位模式的版本。 5>. 1995年 8月, Windows 95发布;一个混合的16位/32位Windows系统。 6.> 1998年 6月, Windows 98进入市场;基于Windows 95编写, 对Windows 95的改进。 书的作者就将Windows介绍到这里(没办法, 人家这本书就是1998年写好的), 后来的Windows就更猛了, 横扫桌面, Windows 2000/2022/XP/Vista/2022/7/8., 都懂得。 *Windows的优点: 更加人性化?操作更简单?一定还有其他的优点。 *工作原理的中心思想: "动态链接"概念即为Windows工作原理的中心思想, 通过调用Windows自带的函数来实现在屏幕上显示文本与图形。函数通过动态链接库实现, .dll以及.exe的文件, 在Windows 98中, 这些文件在WindowsSystem子目录下, NT的在WinNTSystem或 WinNTSystem32, NT以上放在WindowsSystem32。 ·关于Windows编程 *Windows API: API, Application Programming Interface, 应用程序编程接口,Windows API 实际上也就是Windows提供的一些函数, 通过对这些函数的调用完成应用程序的开发。 *API文档: 这份文档里介绍了Windows提供的已公开的所有API, 你可以下载份离线的, 或者去MSDN在线图书馆( MSDN Library Online )查阅这份文档.上午的随笔暂时写到这里, 感觉写的有点短, 其实书上写了很多, 仔细品味了好几遍, 越品味越有种找不到重点的感觉, 要是比着书上的句子抄, 这博文岂不是太无味了, 我想, 这些Windows的背景知识对编程影响或许也不是很大, 所以在这里就不啰嗦这些了。下午学习"你的第一个Windows程序"。 *我的第一个Windows程序, Hello,world! 在Charles Petzold的书中, 作者首先回顾了下C语言在控制台下通过标准输入输出函数输出"Hello,world!"的程序, 代码如下: #include int main() printf( "Hello,world!n" ) ; return 0 ; 同样, Charles Petzold也给出了Windows版的"Hello,world!"(其实他给出的是 Hello,windows 98!), 代码如下: #include int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow ) MessageBox( NULL, TEXT("Hello,world!"), TEXT("MessageBox"), 0 ); return 0; 通过Visual C+ 6.0的"文件"->"新建"->"工程", 选择"Win32 Application"创建一个空的项目, 再在这个项目中新建一个"文件", 文件类型为"C+ Source File", 文件以.c 为扩展名, 将上面的代码敲入或者复制粘贴到这个文件内容中, 经过编译运行就可以得到一个对话框了, 赶紧截图留念吧! 在这个对话框中, 有标题栏, 标题栏的内容是"MessageBox", 对话框的内容为"Hello,world!", 还有一个"确定"按钮, 而且, 没有那个黑框框窗口, 一切看起来都是那么美好, 来一起看看这段Windows版的Hello,world!吧! *Windows版的Hello,world!代码注释* *第一行 #include 稍微有点C语音基础的都能明白, 这是要包含"windows.h"这个头文件, 也就说明, 在下面的代码中, 要用到这个头文件, 如果我们将#include这句去掉再进行编译看看会有什么情况: Compiling. HelloWorld.c d:projectlwinchelloworldhelloworld.c(3) : error C2061: syntax error : identifier 'WinMain'd:projectlwinchelloworldhelloworld.c(3) : error C2059: syntax error : ''d:projectlwinchelloworldhelloworld.c(3) : error C2146: syntax error : missing ')' before identifier 'hInstance'd:projectlwinchelloworldhelloworld.c(3) : error C2061: syntax error : identifier 'hInstance'd:projectlwinchelloworldhelloworld.c(3) : error C2059: syntax error : ','d:projectlwinchelloworldhelloworld.c(3) : error C2059: syntax error : ')'执行 cl.exe 时出错. 意料之内的, 报错了, 第一条就是标识符"WinMain"错误, 具体的细节暂时就不深究了, 继续向下看。 *关于windows.h头文件: 在windows.h这个头文件中, 实际上已经包含了若干的其他相关的头文件, 用书上的 话说, windows.h是个非常重要的包含文件, 其中包含的其他比较重要的头文件有: WINDEF.H 基本数据类型定义 WINNT.H 支持Unicode的类型定义 WINBASE.H 内核函数 WINUSER.H 用户界面函数 WINGDI.H 图像设备接口函数 不过我还是好奇windows.h到底包含了那些头文件, 找到VC6的安装目录, 打开Include文件夹, 找到WINDOWS.H并打开, 虽说看不太懂, 但找#include关键词还是无压力的. 除去上面的5个还有: WINRESRC.H EXCPT.H STDARG.H WINNLS.H WINCON.H WINVER.HWINREG.H WINNETWK.H CDERR.H DDE.H DDEML.H DLGS.H LZEXPAND.H MMSYSTEM.H NB30.H RPC.H SHELLAPI.HWINPERF.HWINSOCK2.H MSWSOCK.H WINSOCK.H WINCRYPT.HCOMMDLG.HWINSPOOL.HOLE.H OLE2.H WINWLM.HWINSVC.H MCX.H IMM.H *程序的入口 在Win32控制台程序(Win32 Console Application)中, 应用程序的入口为main()函数, windows程序的程序入口和win32控制台程序的入口类似, 为WinMain()函数. 程序的入口函数在WINBASE.H作出了声明, 声明如下: int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd ); 其中由声明可以看出, WinMain函数的返回值被定义为int型; WINAPI为WinMain函数的调用规则, 在WINDEF.H对"WINAPI"作出了如下宏定义: #define WINAPI _stdcall 说明, WinMain函数的调用规则为"_stdcall"方式, 对于"_stdcall"调用规则, 现在暂时先不去深究, 知道有这么回事就行, 以后会详细了解到的, 现在如果深究"_stdcall"就偏离了这篇博文的主题。 *WinMain函数的参数: 1>. WinMain的第一个参数 HINSTANCE hInstance, 用书上的解释为"实例句柄", 由于第一次接触C语言Windows程序设计, 对这个句柄的概念也不是很了解, 去百科了下, 句柄的解释为"一个句柄是指使用的一个唯一的整数值,即一个四字节长的数值,来标志应用程序中的不同对象和同类对象中的不同的实例,诸如,一个窗口,按钮,图标,滚动条,输出设备,控件或者文件等。"引用自百度百科->句柄。 笔者是这样对句柄进行理解的, 在一个应用程序中, 通常创建了很多的窗口、按钮、标签, 或者使用了一个文件等, 在程序的任何地方, 只要能够获得这个被称为句柄的东西, 就能 够找到该控件或者窗口在内存中的位置, 从而对其进行操作。感觉有点像带参数的main函数, 只是这里的主函数参数为一个句柄。 2>. WinMain函数的第二个参数, 同样是个实例句柄, 但书上又进一步解释说在32位的 Windows程序设计中, WinMain函数的实例句柄概念已不再采用, 因此WinMain的第二个参数通常总是NULL。 笔者的见解: 感觉马上就要晕了, 疑问一: "因此WinMain的第二个参数通常总是NULL", 那么第一个呢?WinMain的第一个参数会不会也可以是NULL呢?疑问二: WinMain函数的参数从何而来?是操作系统么?带着疑问继续向下看。 3>. WinMain的第三个参数是用来运行程序的命令行, PSTR: 用来指向一个字符串的指针类型, szCmdLine, sz:表示以0结尾的字符串; 目的是通过命令行方式运行程序并向主函数中传入参数, 应该就像给main函数传入参数一样; 4>. WinMain的第四个参数是一个int型参数, 用来指明程序(窗口)最初如何被显示, 例如最小化?最大化?全屏? 笔者的见解: 应该很有用, 经常见一些游戏一启动就是全屏的, 但是这个参数也是操作系统传给程序的么?因为从平时运行Windows程序时都是直接双击, 并没有通过命令行给它传入参数, 在编程时应该对程序启动时的显示方式有交代才对, 这样系统再运行时再把这个交代的参数传入给程序告诉程序启动时应该如何显示.(在"笔者的见解"部分的观点均为笔者个人的见解, 如果有误肯定指正, 我会及时更正, 避免误导其他读者。) *WinMain函数函数体的MessageBox函数: MessageBox(), 名如其"人", 不用猜也知道这个就是显示一个对话框的函数, 打开API文档,MSDN Library通过索引找到MessageBox函数, 发现其声明如下: int MessageBox( HWND hWnd, /handle of owner window, 窗口的一个句柄 LPCTSTR lpText, /address of text in message box, 一个文本(字符串)的指针 LPCTSTR lpCaption, /address of title of message box, 标题字符串的指针 UINT uType /style of message box, 对话框的风格 ); 在上面示例中对MessageBox函数的调用如下: MessageBox( NULL, TEXT("Hello,world!"), TEXT("MessageBox"), 0 ); 第一个参数窗口的句柄的实参为NULL, 意思为不属于任何窗口. 第二个参数为对话框的内容。 第三个参数为对话框的标题, 但是这两个参数都使用了一个TEXT()的函数, 书上讲使用TEXT()的目的是将这些字符串打包到TEXT宏代码里面, 笔者尝试了不用这个TEXT()函数而直接像这样: MessageBox( NULL, "Hello,world!", "MessageBox", 0 ); 调用并没有出现警告或者报错信息, 具体使用TEXT()函数的详细原因还不太清楚, 暂时先在这里画个圈。 第四个参数为对话框的风格, 一些以MB_开头的一些常量的组合, 可以使用OR(|)运算进行组合, 这些常量定义在WINUSER.H中, 例如常用的有: 1>.对话框按钮类型: #define MB_OK 0x00000000L /仅有一个"确定"按钮 #define MB_OKCANCEL 0x00000001L /"确定" + "取消" #define MB_ABORTRETRYIGNORE 0x00000002L /"终止" + "重试" + "忽略" #define MB_YESNOCANCEL 0x00000003L /"是" + "否" + "取消" #define MB_YESNO 0x00000004L /"是" + "否" #define MB_RETRYCANCEL 0x00000005L /"重试" + "取消 2>.对话框中的图标类型: #define MB_ICONHAND 0x00000010L /一个红X的错误/停止图标 #define MB_ICONQUESTION 0x00000020L /一个问号的询问图标 #define MB_ICONEXCLAMATION 0x00000030L /一个黄色感叹号的警告图标 #define MB_ICONASTERISK 0x00000040L /一个带有i的信息提示图标 同时, 在这些图标中有的还可以用其他名称代替, 这些别名在WINUSER.H的定义如下: #define MB_ICONWARNING MB_ICONEXCLAMATION /警告 #define MB_ICONERROR MB_ICONHAND /错误 #define MB_ICONINFORMATION MB_ICONASTERISK /信息 #define MB_ICONSTOP MB_ICONHAND /停止 下午的学习暂时就到这里, 在学习的过程中出现了几个疑问, 在这里对疑问进行下总结: 疑问一: 在书中介绍WinMain函数的参数时讲到 "因此WinMain的第二个参数通常总是NULL", 那么第一个呢?WinMain的第一个参数也可以是NULL吗? 疑问二: WinMain函数的参数从何而来?是操作系统么? 疑问三: 使用TEXT()函数的作用是什么呢? C语言Windows程序设计->第二天->ASCII与Unicode 一、ASC 1>. 关于ASC ASCII(American Standard Code for Information Interchange,美国信息互换标准代码) ASCII一共包含128个字符, 包括: 33个控制符号, 1个空格, 32个符号, 10个数字, 26个小写字母和26个大写字母。每个ASCII字符采用7位二进制编码的方式。 ASCII的优点:十分可靠, 普遍扎根在我们的键盘、显示器、系统硬件、打印机、操作系统等, 用途十分广泛。 ASCII的缺点:ASCII, 美国信息互换标准代码, 美国原生, 不能满足其他国家文字的需求, 例如, 中国的汉字?英国的英镑符号()?等, 这些在ASCII都是找不到的。2>. 对ASCII的扩展 由于ASCII不能很好的满足其他国家文字的需求, 所以人们迫切希望能对ASCII进行改进。 . 国际化标准组织的扩展方案 1967年, 国际化标准组织( ISO, International Organization for Standardization )推荐了ASCII的一个变种, 改动内容包括: 从ASCII中, 拿出 0x40(''),、0x5B('')、 0x5C('')、0x5D('')、0x5E('')、0x60(' ' ')、0x7B('')、0x7C('|')、0x7D('')、0x7E('')这10个符号保留给各个国家单独使用。这显然不是解决ASCII国际化的好方法, 首先, 其他国家将这些保留字符重新定义为自己国家需要的字符后, 那么国际上的一致性将不能得到保证, 此外, 10个保留字符远远不能满足美国的东方的一些国家使用的象形文字需求, 比如我们中国的汉字。 . IBM公司的扩展方案 IBM公司采用了使用8位二进制编码方式来表示ASCII, 使用一个字节来储存字符, 这样, 相对于7位的ASCII就可以多出128个额外字符空位来补充ASCII。IBM对ASCII的主要扩展为: 补充了一些重音字符、小写希腊字母、块图字符和线图字符。同时, 还将一些补充的字符分配到ASCII的一些不必要的控制字符上。(注: 在操作系统还是字符模式的年代, 块图字符和线图字符常用来被应用软件装饰自己的程序显示) . 微软公司的扩展方案 1985年11月, Windows 1.0发布, 微软采用了自己定义的一套字符集, 这套字符集被称为"ANSI字符集", 是基于ANSI和ISO标准的一个草案。 在MS-DOS 3.3时代(1987年4月), 微软为了使不同国家的计算机都能正常的显示字符, 微软采用了代码页概念, 不同国家的字符被规定在不同的代码页上, 例如代码页第437页为美国英语, 850页为拉丁语-1。用户只要将代码页设置到自己所在的国家就能正常的进行工作, 但是如果用户尝试着将自己的文档拿到与另外一个使用不同代码页的用户的计算机上 进行修改时, 自己的文档的某些字符将会显示成其他字符, 这还算好, 有解决方案, 应用软件可以通过将代码页信息储存到文件中, 使用时再进行一些代码页的转换。 但在后来, 随着代码页数量的剧增, Windows版本的不断升级, 代码页的混淆问题开始日益凸显, DS-DOS的代码页和Windows的代码页以及其他Windows版本的系统发生了不兼容, 例如MS-DOS代码页第855页西里尔语在Windows中的1251页西里尔语或者Macintosh 的第10007页西里尔语还都不一样。 微软为了解决东方一些国家使用的象形文字问题, 使用了双字节字符集, 这些字符集同样在不同的代码页, 代码页936(简体中文)、949(韩文)、950(繁体中文)以及932(日文)。微软的这个双字节字符集和你象形的可能有所不同, 在这个双字节字符集中, 前128个字符仍然是ASCII(1字节), 较高的128个扩展字符以跟随第二个字节的方式用来表示象形文字(这两个字节被称为前导字节和尾随字节)。 所以在这个代码页中, 有一个字节的字符, 还有2个字节的字符, 这就导致了两个严重的问题: 1>. 在一段字符串中, 字符串的长度不能根据字节的个数确定, 要想确定字符串的长度必须检查每个字节是不是双字节字符的前导字节。 2>. 通过任意指向字符串中的一个指针, 无法知道前一个字符的地址, 通常要回到字符串的开始, 一直解析到指针所在的位置。 二、Unicode 对ASCII扩展的过程中, 没有能够找到一个彻底解决世界上所有书面文字的表示方法, 很显然, 1个字节, 256个字符是无法表示世界上所有的书面文字的, 因此, Unicode诞生了。 Unicode使用16位(2字节)的二进制编码方式来表示字符, 我们知道, 16位最多能够表示65536个字符, 65536个字符对于世界上的所有书面文字以及一些特殊符号来说已经足够用了。在Unicode中, 不同国家使用不同的代码段, 例如, 0x0530 - 0x058F为亚美尼亚语(Armenian) 、0x0600 - 0x06FF为阿拉伯文 (Arabic) 、0x0E00 - 0x0E7F为泰文 (Thai) 、0x2700 - 0x27BF为印刷符号 (Dingbats) 、0x4E00 - 0x9FBF为中文。 Unicode的优点: 只有一个字符集, 避免了二义性, 能够满足跨语言、跨平台进行文本转换、处理的要求。Unicode的缺点: Unicode字符的字符串比ASCII字符串占用的内存大两倍。(笔者认为, 随着计算机性能的不断提高, 内存和外存容量的不断增加, Unicode这一缺点可以慢慢忽略)。 C语言Windows程序设计->第二天->宽字符和C语言 一、回顾C语言中的char数据类型 1>. 在C语言中, 首先我们来声明一个字符型变量: char c ; 我们也可以在声明时对其进行初始化: char c = 'A' ; 这时, 字符型变量c就会被值0x41进行初始化, 0x41也就是ASCII码中的'A'字符; 2>. 我们还可以定义一个指向一个字符型数据的指针, 例如: char *p ; 同样我们再让指针p初始化时指向一个字符串: char *p = "Hello,world!" ; 3>. 我们再声明一个字符数组: char a10 ; 也可以在声明时对其进行初始化: char a10 = "Hello" ; 或者: char a = "Hello" ; 通过对C语言的学习我们可以知道, char型变量为1个字节, 因此, 在char c = 'A'中, 变量c的大小即为1字节; 在32位的操作系统中, 一个指针型的变量需要4字节的存储空间, 因此在第二个示例中, char *p = "Hello,world!" ;所需的存储空间为: 4字节指针变量所需的空间 + 字符串"Hello,world!"的12个字节另外再加上一个字节用来表示字符串结束的0。对于一个字符数组char a10 ;编译器则会自动保留10个字节的储存空间, 对于char a = "Hello" ;这种声明方式, 编译器会根据"Hello"字符串的长度( 5个字符 + 一个结尾0 )来决定初始化时的数组大小。 二、宽字符 通过学习Unicode编码方式,可以知道, 一个Unicode字符占用2个字节的储存空间, 如果我们想用C语言中原有的数据类型表示Unicode的2字节编码类型, char是不行的, char 的储存空间为一个字节, 在32位的环境下, int 占4个字节, 而我们仅仅需要的是2字节, 并且是无符号型数据, 因此, 我们可以使用unsigned short int型数据表示一个2字节的字符, unsigned short int为2字节, 正好符合2字节的要求, 当然, 我们也可以将unsigned short int简写为unsigned short。 在C语言中的宽字符正是基于short型数据的, 这一数据类型在头文件WCHAR.H中的定义为: typedef unsigned short wchar_t ; 所以C语言中的宽字符wchar_t数据类型与一个无符号短整形unsigned short一样, 都是16位宽。 如果我们用宽字符wchar_t数据类型定义一个变量并且初始化, 如下: wchar_t c = 'A' ; 那么宽字符wchar_t变量c的值为0x0041, 学过汇编的朋友应该知道, 如果使用16位的CPU 储存一个字, 将使用两个存储单元, 在这两个存储单元中, 低位字节放在低地址单元中, 高位字节则放在高地址单元中, 所以, 在这里, 处理器依然将从低位内存单元即低位字节开始处理字符, 'A'在内存中的顺序即为 0x41 -> 0x00。 当我们想使用宽字符表示一个字符串, 我们还要通知编译器这个字符串将使用宽字符存储, 我们用大写的字母'L'(表示长整形)来将这一消息告诉编译器, 例如: wchar_t *p = L"Hello" ; 那么这个字符串"Hello"将会使用12个字节来储存, 这12个字节存储单元的内容为:"Hello"这5个字符占10个字节, 另外加上表示结束的0占两个字节。 三、有关宽字符的函数 在C语言的学习中, 字符串处理函数我们经常使用, 比如: unsigned int strlen(char *s); /求字符串的长度 char *strcat( char *dest, char *src ) ; /将src所指字符串连接到dest结尾处 int strcmp( char *s1, char * s2 ) ; /将字符串s1与s2比较 char *strcpy( char *dest, char *src ) ; /将src所指字符串复制到dest结尾处. 这些函数极大的方便了我们对字符串的处理, 遗憾的是, 在宽字符类型的字符串中, 这些函数将不再适用, 例如我们使用strlen求一个宽字符字符串的长度, 代码如下: #include #include int main() wchar_t *p = L"Hello" ; printf( "%dn", strlen(p) ) ; return 0 ; 运行后显示的结果为1, 很显然, 它没有求出正确的长度, 这是因为, 宽字符字符串"Hello"在一段内存中存储的值如下: 48006500 6C 00 6C 00 6F 002100 这是因为当strlen找到该字符串的第一个0时就认为该字符串已经结束了, 所以得到的长 度为1, strlen统计到的这一个字符即为0x48表示的'H'。 幸运的是, 虽然这些字符串处理函数不支持对宽字符的处理, 但是我们可以使用为宽字符 处理准备的函数, C语言中每个字符串处理函数对应的都有其宽字符版本的字符串处理函数, 这些函数定义在STRING.H头文件和WCHAR.H中, 例如strlen函数响应的宽字符版本为wcslen, wcslen函数在STRING.H的声明如下: size_t wcslen(const wchar_t *); size_t为无符号整形unsigned int的别名, STRING.H在头文件的定义如下: typedef unsigned int size_t; 我们尝试使用宽字符处理函数wcslen()求宽字符字符串的长度: #include #include int main()