自从3月2日下午4:20又几分钟收到这本经典巨作《window程序设计》我就很兴奋,这本书给我的感觉就想《C++Primer Plus》,或者更好,因为它们两者都兼备了一个共同特点:让你消除基础盲点,解除你从前的疑惑,一切都那么有始有终。
都过了10天了,现在才看到第十章,哎,是有点慢,但是因为没有一味看书,而是基本上把书上所有出现过的程序,以自己理解并独立写出的方式进行着。自己感觉还可以,觉得消除了以前很多觉得很神秘,很难懂的,例如:GetSystemMetrics(虽然现在觉得是那么基本的函数,但以前看到这个函数感觉就是这个函数跟系统牵连很大,很怕!),函数窗口视口,映射模式自由变换,还有子窗口创建等等,好多东西。这也验证了这本圣经的不朽,让你学完这后感觉知识体系好清楚。虽然内容多,但是不太乱。说不太乱只要是因为windows本身的原因,而不是这本书讲述的原因,都知道windows的函数以及消息定义,消息通知这类,如果全看能让你看个几个月的。
铺垫了这么多该进入这篇博客的主题了,那就是自我总结关键要点。
我以章节顺序一一总结自己认为的重点:
第一章:起步
- windows简史。1981年pc机才开始出现,那个时候风靡的操作系统霸主就是MS-DOS。而后1983年微软宣布windows。(而在此之前苹果公司发行过Lisa,提出与字符界面不同的思路。苹果1984年发布了Macintosh成为了图形界面的标准。)1985年发布了window1.0。1987年发布了window2.0,同期与IBM公司推出了OS/2操作系统。1990年推出了window3.0,同时与IBM公司分道扬镳,微软不再参与os/2,而专心自己的Windows。1992年window3.1发布,1993年windowNT发布。紧接着就是我们熟悉的window95,window98。而到现在的windows 2000,windows2002,xp,vista,window7等等。
- 动态链接库的作用。原来我们使用的API全都是以动态链接库的形式连接到我们程序里面的,怨不得程序Debug调试下,输出框里面N多已加载xxx.dll。而我们用的C语言的函数,例如什么strlen(),都是相当于把代码加到你程序里面,程序本身会变大。而导入库的作用就是为了加载dll做好准备信息用的,这是由于导入库本身的觉得,因为它就是记载这相应dll地址,以及dll里面的地址,程序只有在连接阶段通过lib才能把这些信息加到程序里面,然后程序实际运行的时候就根本不需要lib了,因为信息已经被加载到了程序中。这就可以解释为什么以前的时候我们编写Ogre,语音引擎需要dll,还需要lib,还需要头文件(头文件是一种方便调用dll函数的方法,在dll里面有详细讲解,这种通过头文件方式调用dll函数成为“隐式连接”)。
- 我们经常看到的LP,其实是以前window16版本遗留下来的东西,那时候采用段地址寻址,物理地址包括段地址和段内地址。长指针可以用于不同段的寻址,而NP,也就是近指针可以用于同一段地址的段内寻址。不过32位的windows操作系统早就不分这个,LP,NP都是一样的,只是为了兼容以前版本的程序。你会经常看到这样的系统定义:struct POINT {}* PPOINT,*LPPOINT,*NPPOINT;这就可以证明LP,NP,P都是一样的。
- 命名方式:匈牙利命名法。也就是所有变量的第一个或者前几表示类型的字符要小些,以后每个单词或者其简写的第一个字符大写。例如:int iLen;HWND hwnd;HWND hwndDlg;还有一些具有特殊意义的非类型小写,如:int cbWndExtra;其中的cb,c是count的缩写就是数量,个数的意思,b是byte的缩写是字节的意思,那么cbWndExtra就是为窗口预留的字节数,这么一个变量。这个变量的意义这里不用追究以后自然会知道。
- MessageBox原来有很多形式。有只有一个“确定”按钮的,有“确定”和“取消”两个按钮的,有“忽略”,“重试”,“取消”三个按钮的,还有什么四个按钮的。还可以指定消息框出现的图标。有信息提示的,有错误的,有询问的。
第一张说的还真不少!无语了,看来这本书就是这样,让你知道很多,就连总结也很多。看下一章。
第二章:Unicode简介
- 基本上就是一点UNICODE的来历。原来的时候美国字符少,自己够用,后来考虑欧洲就得扩充字符,再后来再往东扩展,电脑用到了亚洲,终于美国人傻眼了,这么多字符了,开始想方设法,第一个方法是双字节字符集(DBCS)。但是这个字符集很怪,有些字符时一个字节,有些字符时两个字节。这就使得编程麻烦,因为要确定指针的位置(处于第一个字符),不能简单的和首字符地址相减得到偏移量,因为有些字符时一个字节的。而后就出现了UNICODE字符集,对了确实是唯一的,因为它统统用两个字节表示一个字符,所以不会有任何歧义了。但是浪费空间。(而后就出现utf-8,utf-16,utf-32等UNICODE解决方案,window里面用utf-16)。还要明确一个概念就是“宽字符”是一种指示用多个字节表示一个字符的方法,所以UNICODE是一种宽字符。而不能用宽字符就是UNICODE。
- 因为字符所用的字节不同,所以所有牵扯到字符的函数都需要新增一个宽字符版本,这就是我们经常看到的什么strlen,对应wcslen(这是个c函数,windows里面有不同的宏定义,以方便实现不同编码的编译)。以后所有的常量字符串统统加上TEXT(""),这样可以用unicode编码方案编译,也可以用ASCII编码方案。
- 接触了个sprintf,fprintf,分别是将格式化到字符串,格式化到文件。还有vsprintf用于辅助实现不定量变量输出的函数。基本不用。c语言也是用了宏定义,window也是用了宏定义,就是为了让程序能够在unicode和ASCII下是用相同函数名,而不用关心是否是宽字符版本什么。因为编译器帮你决定。
看来第二章稍微少点。嘿嘿。明天写下一张吧。
今天是3月13日继续总结。
第三章:窗口与消息
- 这里呢,现在回想一下是那么的基础,但是第一次见到窗口创建的整个过程还是很惊心动魄,很让人害怕,你要知道,对于初学者使用一个10个参数WNDCLASS结构体,和使用11个参数的CreateWindow真的很吓人得。但好在这些都过去了。
- WNDCLASS里面需要注意的基本上没事了,就是系统资源的使用,如GetStockObject,LoadIcon(NULL,IDI_APPLICATION);LoadCursor(NULL,IDC_ARROW);特别是IDI_APPLICATION,IDC_ARROW是MAKEINTRESOURCE的宏定义,以实现将数字ID转换成字符串指针,但本质不用这个字符串指针做事情,而是如果字符指针的高位是0的话,LoadIcon等这些函数就知道第16位是资源ID。
- CreateWindow第一个参数是窗口类名称,而不是窗口类对象。说到窗口类,可能就要说为什么要将这个窗口创建弄成两部分,我认为有两方面原因:1,首先是为了窗口类的多次使用,也就是说很多窗口的一些窗口类变量的值是一样,而且事实也是这样,所以把窗口类单独出来,可以为以后窗口创建提供方便。2,本身两者是各司其职,窗口类主要是一些属性上的东西,和窗口本身有关联,但是还是不是窗口本身,而CreateWindow就是完全侧重窗口本身的外观的东西和本身属性的东西。而且CreateWindow函数本身会发送WM_CREATE消息,说是发送消息感觉像异步,实际上是同步,是在CreateWindow函数里面回调了WndProc,调用完了才回去继续完成CreateWindow。所以记住所有的发送消息的字眼就是同步函数调用。而如果说投递消息的话,那就是发到消息队列,就有异步的意思了。
- 另外CreateWindow里面一个重要点就是,如果作为子窗口创建的话,hMenu这个参数就作为子窗口ID,这是规定。
- CreateWindow完了就是ShowWindow了,就是因为各司其职,CreateWindow创建内存中的窗口基本数据,而ShowWindow是将抽象的窗口显示到屏幕。这个函数发送WM_SIZE消息,我说发送,你懂得,同步回调窗口过程。
- UpdateWindow发送WM_PAINT的,你懂的。但是需要注意的是,WM_PAINT消息是根据无效区域进行重画的。为什么是根据无效区域进行重画的呢,就是因为BeginPaint返回的HDC决定的,因为这个hdc裁剪了那无效区域,所谓裁剪就是只重画裁剪了的区域,不在此区域的不予重画。所以如果你没有无效区域,你发送UpdateWindow是无用的。而且WM_PAINT还有很多需要注意的地方,等在WM_PANT消息那总结吧。
- 窗口过程,我们把我们关心的消息用case语句拦截一下,但是我们因该知道我们拦截的消息是否有系统默认处理,如果有默认处理的话,在case块处理完了break出来,如果知道系统没有默认处理的话,那么就直接return就可以了。
- WM_PAINT消息,应该注意的事这个语句里面一定要写BeginPaint消息,记得自己有以下写WM_TIMER消息,在WM_CREATE消息里面SetTimer了,但是一直收不到WM_TIMER消息,原来是WM_PAINT没写代码,但想先看WM_TIMER效果,但是因为WM_PAINT里面没有BeginPaint,而BeginPaint是清除无效区域的。可能说了你也不明白,关键是WM_PAINT在消息队列里面是怎么被清除的呢?不要以为你处理的WM_PAINT就没了,错了,是清除无效区域是,就会清除消息队列里面的所有WM_PAINT消息。继续说刚才的bug,因为WM_PAINT没有BeginPaint,所以WM_PAINT就一直清除不了,就会一直重复的WM_PAINT处理过程,所以WM_TIMER消息就没有机会处理了,因为WM_TIMER也是队列消息。队列消息就是要经过消息队列的消息。每个程序有一个消息队列。经过消息队列的消息可以说是序列消息了,就是遵守“先来后到”和“优先级”。
- GetMessage函数呢,是从消息队列里面取消息。如果有,就取出来,然后交给TranslateMessage,如果是键盘消息,TranslateMessage就判断这个键盘消息是否产生字符消息,如果产生字符消息,TranslateMessage就往消息队列里面添加字符消息,作用仅此而已(目前为止)。DispatchMessage是将消息派送到相应的窗口过程。
- 对了WinMain函数里面的hInstancePrev永远是0,为什么要用这个东西,就是因为以前的程序用,所以为了兼容性就保留了,而且永远置为0,这样也会让原来的程序逻辑正确。啊,我们居然知道一点兼容性的东西了,以前的时候总是听到书上或者别人说什么windows兼容性很强,自己总无法理解什么叫兼容性。啊,我们也可以懂一点点兼容性了。呵呵。
啊,说是总结,却还是写了很多。不过和书上的比起来,已经很少了,不知道又打了多少错别字。嘿嘿。
今天是3月21日,已经过了8天都没写了,因为上个星期看到很多新东西,而起有些程序比较大,有点累就没写,还好好休息了休息,今天继续努力咯!
第四章:文本输出
- 几种情况下会出现WM_PAINT消息。窗口尺寸变化(这要求窗口的类型CS_HREDRAW,CS_VREDRAW),窗口被遮挡,用InvalidateRect指明无效矩形,一些windows函数如ScrollWindow,ScrollDC使得窗口画面滚动,而导致一些无效区域产生。其他的就不用太在意了。有些windows会保存,有些不会。总之不用太在意,一般不影响你程序。
- 上面说到无效矩形,就是用InvalidateRect函数指定的矩形就是无效矩形,这个函数会发出WM_PAINT消息,用BeginPaint函数得到的窗口句柄只会在无效矩形上面重新绘画(当然是绘画在无效矩形里面的图形了)。或者通过GetUpdateRect来得到无效矩形或者称为“更新矩形”。
- 这一章是第一章与图形相关的章节,说到GDI,就不得不说一个令所有初次接触GDI的新手感到困惑和恐惧的单词了。那就是Device Context。这个到底是什么,什么叫做设备上下文呢?上下文经常出现在语文课本里面,但是设备怎么跟语扯上边了呢?这可能就是计算机的无所不涉及吧。我理解是这样的:首先我要说什么是设备,这里说的设备比较抽象一点,不要一味的去想显示器设备,确实和它有关系,但用另一种观点去看能够有利于我们编程的理解。直接给设备的比较完整的描述比较苦涩。还是这样说吧!可以这么说,如果你知道什么是客户区的话,你就可以更好理解设备。因为我们一般绘画都在客户区,虽然也可以在整个窗口绘画,但是不是正规做法。为什么我们可以单独在客户区绘画,而且绘画就好像是在一个独立的设备上绘画,我提到了设备,这是因为我们在客户区绘画时,如果坐标出了客户区就不会显示出来。如果我们坐标出了客户区而影响到其他窗口的话,我想谁都不愿意。你看客户区这个时候就好像一个单独的显示屏,具有完整的绘画体系。所以我们可以成客户区是一个设备。同样窗口也是设备。屏幕也是设备。是不是感觉这些设备有很多重叠区域,感觉不像。但是如果你得到这些设备环境的句柄,在上面绘画就相当于在一个独立的设备绘画。刚才说到设备环境句柄,那么我们说一下设备环境吧,设备你有点认识了,那设备环境是什么呢?我问你你想在客户区写字是不是直接用TextOut将你想写的字写出来就可以了!是的,但是我要问你,你写的字的字体是什么颜色的,字体什么类型的。如果你用直接画线的话,线条是虚线还是实线。这些你都不用再使用前设置,设备有默认设置。而我认为这就是设备的环境,就是设备当前的一种状态。那么设备环境句柄呢?句柄就是一个值,通过它我们可以引用到我们想要的对象,而windows内核对象都是通过句柄来因为。暂时不要管什么是内核对象,简单的认识就是很重要的windows对象。那么设备环境句柄就可以让我们得到设备环境的很多信息。写的真够多的。
- 列举几种得到设备环境句柄的方法:BeginPaint专用于WM_PAINT消息,因为它就是为WM_PAINT量身定做的。里面包含了很多重画的重要信息,比如说无效矩形。GetDC是得到各种设备句柄,但不包含任何重画信息。CreateDC是创建像屏幕设备环境句柄,打印机句柄等。CreateIC是得到用于获取信息而不能绘画的句柄。
- 文本输出的话,就我看来基本上在显示方面可以分为两个函数,一个经典的TextOut函数,另一个是格式输出的函数DrawText。但要是输出比较工整的话,那么要得到一些具体的字体信息,比如你要使用什么字体在客户区进行输出,这些字体的高度和宽度直接影响你的输出效果,所以一般的字体输出的话,都会在WM_CREATE消息里面得到你欲使用字体的信息。然后以后就可以用了,不过预先说明,我们这里能处理的是等宽字体,也就是SYSTEM_FIXED_FONT,或者在用LOGFONT的时候制定pitch的时候制定FIXED_PITCH,那样也能得到等宽字体。因为非等宽字体的处理确实比较麻烦,在排版和布局上要做很多细致活。
- 在说到字体时,就应该知道计算机对字体处理的一些基本信息,这些基本信息可以用过GetTextMetrics函数得到,将结果保存在TEXTMETRIC类型的变量里面。其中有tmHeight,tmAveCharWidth,tmExternalLeading,tmInternalLeading,tmAscent,tmDescent。tmAscent从字体基准线到字体能达到的最高处(包括普通字符上面的一些音调符号),而tmDescent是基准线到所有字体能达到的最低处。基准线就想我们在信纸上写g一样,我们会把g的弯钩下载横线下方,把g的o卸载横线上方,而横线就是我刚才说的基准线。tmInternalLeading就是普通字符上面的音调符号(如果有的话)。tmExternalLeading是不属于字体,它是行间距。基本上可以这么认为,当然有更美观的排版在tmExternalLeading的基础上进行修正,已达到视觉最好的效果。tmAveCharWidth是平均字符宽度,为什么说平均,那是对非等宽字体而言的,等宽字体的话就是每个字符的宽度。tmHeight就是tmAscent和tmDescent的和。而对我们最重要的就是tmHeight,tmExternalLeading,tmAveCharWidth。因为tmHeight+tmExternalLeading就是一行字体所占的所有高度。tmAveCharWidth就是字体的宽度了。
- 用到字体的话我们就要说一下很重的细节了。字体,笔,画刷都会牵扯到背景颜色和背景模式,不要把这个和窗口背景混了。窗口背景是进行无效区域刷新用。而字体的背景颜色和背景模式是在指定位置输出字体时,将字体写在背景颜色上,在经背景模式变换,再“贴”到你要显示的地方。当然背景颜色和背景模式在默认模式下和系统的其他默认设置是很和谐。可以通过SetBkColor和SetBkMode来改变。具体函数用法可以自己查。
- 滚动条基本上就是基本上就用SetScrollInfo了,像如SetScrollRange,SetScrollPos基本都可以被SetScrollInfo代替。而且SetScrollInfo提供更多便利,比如滚动最后一页的设置,不应该把文件的最后一行放到最后一页顶部,而应该最后一页底部。而放到底部需要自己计算,而SetScrollInfo帮你计算。
- 滚动条中经常使用ScrollWindow,因为效率很高,不会出现屏幕很卡的现象。但是ScrollWindow是为数不多的不用设备句柄的函数。所以因该是它不属于GDI模块。在滚动条中应该判断滚动条是否到顶了,或者到底了,以避免不必要的重画。
哇,好累啊,哎,写书的人你们辛苦了!写完后没怎么检查,谁给核稿啊?
今天3月23日,第五章要开始了,要知道这是多么重要,多么对像我们这样的菜鸟给力的一张。不过内容真的好多又重要!
第五章:绘图基础
- 首先来说的话,我们应该知道windows通过GDI模块,使得我们的绘图完全独立于硬件。使得绘图操作可以被移植,当然尽量是图像输出类型一样的设备,因为矢量设备不能使用很多光栅操作,而使得光栅操作的函数不能用到矢量设备上。
- 我们需要经常用到的,用于得到系统信息的函数应该知道:GetSystemMetrics,GetDeviceCaps,GetTextMetrics;
- 对于图像的一些基本属于应该清楚:分辨率有的时候是指设备总长度(宽或高)所用的像素数。而本书的作者则更倾向于用每度量单位下的像素数为分辨率,而用像素规模表示设备总长度得像素数。用度量尺寸或规模来表示设备总长度的物理大小。还有HORZSIZE和HORZRES不同,一个是度量单位,一个是像素单位。且前者从系统得到的不一定对。作者不建议用物理尺寸去干一些事。
- 如果我们在某次操作中,要改变我们设备环境做一些,但接着又要在回到原来状态,那么SaveDC,RestoreDC是必不可少,且经常用堆栈形式。SaveDC(hdc);RestoreDC(hdc,-1);-1表示回到上一次状态。
- 各种函数打统计:点:SetPixel,GetPixel。线:MoveToEx,LineTo,Polyline,PolylineTo,PolyPolyLine,Arc,PolyBezier,PolyBezierTo。填充类型:Rectangle,Ellipse,Chord,Pie,Polygon,PolyPolygon(画多边形用处极大)
- 画笔,画刷各种样式都应该了解。这些对象也前者到背景颜色和背景模式。
- 另外很重要的ROP操作,也就是Raster operation。光栅操作。通过SetROP2,2代表两元,就是两个对象间的关系,如画笔颜色和目标颜色。通过使用巧妙的SetROP2,可以极大提高我们程序的绘图效率,而不用设置什么无效区域,那样很可能要重画背景。所以ROP应该好好看看!
- 要知道一点基本知识,其实我们背景重画就是用当前画刷(8*8的位图),不断重复粘贴,完成的。而这个当前画刷不是通过SelectObject就可以更换,那是你的窗口类中的数据,要修改用SetClassLong。说到SetClassLong,就要知道其他三个超强函数,GetClassLong,SetWindowLong,GetWindowLong。
- 映射模式,这可是非常非常重要的。被我认为是绘画中的灵魂。因为如果你不懂映射,或者没搞清楚,那么那些建立在映射上的方便操作你是很难理解的。有一种设备坐标映射WM_TEXT,其他的已经规定了的都是度量映射,也就是牵扯到物理单位。这是为了方便我们创建设备无关的一些信息用的。比如你要创建30mm宽,30mm高的,你不用去计算30mm高在你电脑上有多少像素,你只需要换成WM_LOMETRIC即可或者其他的。度量映射下的y正方向和设备坐标映射下的y正方向正好相反。但是最好用的还属WM_ISOTROPIC和WM_ANISOTROPIC,这让我们的映射坐标无比灵活。你可以自己制定1代表多少像素,或者你觉得对你程序好用的单位,如一个字符高度。WM_ISOTROPIC是各向同性,WM_ANISOTROPIC是各向异性。但WM_ANSIOTROPIC并不是必须是宽,高单位必须不同的,只是说明它可以胜任宽,高不同单位的情况。WM_ISOTROPIC让你的宽和高的单位一定是一样,即使你用不同的宽高数值,因为windows会检查你的数值,而想尽办法且最好的转换成宽高相同单位的映射模式。
- 说映射模式,不得不提窗口,视口,原点。不要太在意窗口,视口中文意思。我们更多的去记住它在绘图中的含义更好。窗口,只要想到和窗口有关系,一般就是只的逻辑坐标,并表示当前的逻辑映射模式,说逻辑映射模式因该就是自己这么说吧,不过可以让我理解更清晰一点,因为我理解逻辑映射模式就是你自己设的,或者你没设而当前默认的映射模式,它可能是设备坐标映射。视口就是指的设备坐标了。因为视口不会变。所以修改原点是多用视口修改。说到原点,其实我们要知道windows是怎么做到把你写的逻辑坐标,转换到设备坐标,你就知道为什么要有窗口,视口,原点了。看下面的公式吧:,其中xWindow-xWindowOrg是算出逻辑坐标位移差(点到原点举例),xViewExt/xWinExt是得到一个逻辑坐标有多少个设备坐标长度,那两者相乘就得到了设备坐标位移差,那在加上xViewOrg就得到真正在设备上的坐标了。不要认为麻烦,这是非常有必要的,没有这个公式,那么绘图会变得很单调,而且很复杂。因为设计出这些映射模式就是为了方便缩放和布局排版的。没有他们相信你在打印啊,排版什么地方会很痛苦。通过公式,可以看出原点有窗口原点和视口原点,他们就是窗口中的点和视口中的点对应的参考点。我们可以通过改变其中之一的原点,以达到我们改变我们的映射坐标系原点。将设备原点往正方向(无论宽,还是高的正方向),映射坐标系原点会随着该方向进行相应的位移的偏移。将逻辑坐标系原点往改逻辑坐标系的正方向移动,会使得映射坐标系往相反的方向移动。因为公式里面对逻辑坐标系原点是进行减的,而对设备坐标系原点是进行加的。 而我说的映射坐标系就是你的逻辑坐标系。
- 窗口范围和视口范围,也不要用中文去想。看到上一点的那个公式,你就知道了,其实他们两个和范围没太多关系,只要有关系的是他们的比例。几种常见的设定WM_ISOTROPIC方法,用同一事物去设定窗口范围和视口范围。这样就能得到自己理想中的比例了。比如说,你想自己的逻辑坐标是1英尺3个逻辑单位。那么你应该这么写:这样,因为设置窗口范围和视口范围都是参照1英寸的,所以windows就帮你实现了,你的3逻辑单位对应1英寸,其实是对应到windows计算出来的像素数,但这个像素数正好等于1英寸。
- 另外介绍有用的InvertRect,InvertRgn。这个是反转区域色素的函数。黑变成白。和矩形相关的很多,如OffsetRect,SetRect,InsectRect,InflateRect,UnioRect,PtInRect。
- 区域是一个很重要的GDI对象,不想矩形那样,就几个坐标数据。区域首先是GDI对象,所以是一种资源,所以区域创建后都要删除。我们可以创建各种区域,矩形的,椭圆的,然后由矩形,椭圆以及其他区域进行各种区域操作得到的复杂区域。区域可以用于画图,可以用于画图。区域操作函数,CombineRgn(hDestRgn,hSrcRgn1,hSrcRgn2,iCombine)通过不同方式iCombine两个区域的各种操作。但要注意这个函数中的hDestRgn可是单纯用于承接结果。这个句柄必须是原来就有有效区域才可以,要不然会出错。通过裁剪区域,然后将区域选进设备环境,使得设备重画只在裁剪后的区域内绘图。以至于可以通过区域的屏蔽作用画出很复杂的图形。