【Chapter 01 开始】
本书介绍了 MS Windows 98 、NT4.0/5.0 下程序写作的方法。
使用本书有三个先决条件:
1> 使用者熟悉Windows 98。
2> 了解C 语言。对C 的标准运行时库(Run-Time Libary, RTL )有所了解。
3> 安装一个适于进行Windows 程序设计的32位C 语言编译器和开发环境。
Windows 环境
1985.11月 发布Windows 1.0。
1987.11月 发布Windows 2.0,Windows 以实际模式执行,只能存取地址在1MB 以下的内存。(CPU:8086、8088)Windows/386 使用Intel 386 微处理器的虚拟8086模式,实现将直接存取硬件的多个MS-DOS程序窗口化和多任务化。
1990.5.2 日 发布Windows 3.0(对Intel 的286、386、486 微处理器保护模式的支持。)能存取高达16MB 的内存。
1992.4 发布Windows 3.1 只能在保护模式下运作。
1993.7 发布Windows NT (支持 Intel 386、486、Pentium) 32位保护模式的Windows版本。
1998.6 发布Windows 98。
Windows 方面
Windows 98 和Windows NT 都是支持32位优先权式多任务及多线程的图形操作系统。
动态链接
Windows 运作机制的核心是一个称作动态链接的概念。
在早期,Windows 的主要部分仅通过三个动态链接库实现。三个主要子系统,它们称作Kernel、User 和 GDI。
Kernel(16位 Krnl386.exe 和 32位 Kernel32.dll )处理所有传统上由操作系统核心处理的事务 - 内存管理、文件I /O 和多任务管理。
User (16位 user.exe 和 32位 user32.dll )指使用者接口,实现所有窗口运作机制。
GDI (16位 GDI.exe 和 32位 GDI32.dll )是一个图形设备接口,允许程序在屏幕和打印机上显示文字和图形。
API 和内存模式
Windows 1.0 支持不到450个函数调用。
Windows 98 已有上千种函数调用。
Windows API 和它的语法的最大变化来自从16位架构向32位架构转化的过程中。
Windows 1.0~ 3.1 使用16位Intel (8086 、8088、286)微处理器上所谓的分段内存模式。从32位386开始的32位Intel 微处理器也支持该模式。在这种模式下,微处理器寄存器的大小为16位,因此C 的int 数据型态也是16位宽。
在分段内存模式下,内存地址由两个部分组成---- 一个16位段(segment)指针和一个16位偏移量(offset)指标。
从程序员的角度看,这非常零乱并带来了long 或 far 指针(包括段地址和偏移量地址)和short 或 near 指标(包括带有假定段地址的偏移量地址)的区别。
从Windows NT 和Windows 95 开始,Windows 支持使用Intel (386 、486、Pentium) 微处理器32 位模式下的32位平坦寻址内存模式。
C 语言的int 数据类型也扩展为32 位的值。
第一个Windows 程序
/*------------------------------------------------------------------
// Subject: HelloWin.c
// Author: StartAoA
// Date: 2013-03-05
// Platform: WS2008 + Win 7 32bit SP1
// Version: 1.0
--------------------------------------------------------------------*/
#include
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow)
{
MessageBox (NULL, TEXT ("Hello, Windows 7!"), TEXT ("HelloMsg"), 0);
return 0 ;
}
头文件
windef.h 基本类型定义
winnt.h 支持Unicode 的类型定义
winbase.h kernel 函数
winuser.h 使用者接口函数
wingdi.h 图形设备接口函数
程序的入口函数
int WINAPI WinMain( HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd);
第三个参数,我们修改为PSTR。此处为LPSTR,这两种数据类型都定义在winnt.h 头文件中,作为指向字符串的指针。LP 前缀代表长指针,这是16位Windows 下的产物。
MessageBox 函数
MessageBox (NULL, TEXT ("Hello, Windows 7!"), TEXT ("HelloMsg"), 0);
第一个参数:窗口句柄;
第二个参数:在消息框主体中显示的字符串;
第三个参数:出现在消息框标题列上的字符串。
MessageBox的第四个参数可以是在WINUSER.H中定义的一组以前缀MB_开始的常数的组合。您可从第一组中选择一个常数指出希望在对话框中显示的按钮:
#define MB_OK 0x00000000L
#define MB_OKCANCEL 0x00000001L
#define MB_ABORTRETRYIGNORE 0x00000002L
#define MB_YESNOCANCEL 0x00000003L
#define MB_YESNO 0x00000004L
#define MB_RETRYCANCEL 0x00000005L
如果在HELLOMSG中将第四个参数设置为0,则仅显示「OK」按钮。可以使用C语言的OR(|)操作符号将上面显示的一个常数与代表内定按钮的常数组合:
#define MB_DEFBUTTON1 0x00000000L
#define MB_DEFBUTTON2 0x00000100L
#define MB_DEFBUTTON3 0x00000200L
#define MB_DEFBUTTON4 0x00000300L
指出消息框中图示的外观:
#define MB_ICONHAND 0x00000010L
#define MB_ICONQUESTION 0x00000020L
#define MB_ICONEXCLAMATION 0x00000030L
#define MB_ICONASTERISK 0x00000040L
这些图示中的某些有替代名称:
#define MB_ICONWARNING MB_ICONEXCLAMATION
#define MB_ICONERROR MB_ICONHAND
#define MB_ICONINFORMATION MB_ICONASTERISK
#define MB_ICONSTOP MB_ICONHAND
===============================================================
扩充内容
C 的标准运行时库(Run-Time Libary, RTL) (摘自网络,修正几处错误的信息)
1. 运行时库介绍
运行时库是程序在运行时所需要的库文件,通常运行时库是以lib或dll形式提供的。C运行时库诞生于20世纪70年代,当时的程序世界还很单纯,应用程序都是单线程的,多任务或多线程机制在此时还属于新观念。所以这个时期的C运行时库都是单线程的。
随着操作系统多线程技术的发展,最初的C运行时库无法满足程序的需求,出现了严重的问题。C运行时库使用了多个全局变量(例如errno)和静态变量,这可能在多线程程序中引起冲突。假设两个线程都同时设置errno,其结果是后设置的errno会将先前的覆盖,用户得不到正确的错误信息。
因此,Visual C++提供了两种版本的C运行时库。一个版本供单线程应用程序调用,另一个版本供多线程应用程序调用。多线程运行时库与单线程运行时库有两个重大差别:
(1)类似errno的全局变量,每个线程单独设置一个;这样从每个线程中可以获取正确的错误信息。
(2)多线程库中的数据结构以同步机制加以保护。这样可以避免访问时候的冲突。
Visual C++提供的多线程运行时库又分为静态链接库和动态链接库两类,而每一类运行时库又可再分为debug版和release版,因此Visual C++共提供了6个运行时库。如下表:
C运行时库 库文件
Single thread(static link) libc.lib
Debug single thread(static link) libcd.lib
MultiThread(static link) libcmt.lib
Debug multiThread(static link) libcmtd.lib
MultiThread(dynamic link) msvert.lib
Debug multiThread(dynamic link) msvertd.lib
2.C运行时库的作用
C运行时库除了给我们提供必要的库函数调用(如memcpy、printf、malloc等)之外,它提供的另一个最重要的功能是为应用程序添加启动函数。
C运行时库启动函数的主要功能为进行程序的初始化,对全局变量进行赋初值,加载用户程序的入口函数。
不采用宽字符集的控制台程序的入口点为mainCRTStartup(void)。下面我们以该函数为例来分析运行时库究竟为我们添加了怎样的入口程序。这个函数在crt0.c中被定义,代码如下:
void mainCRTStartup(void)
{
int mainret;
/*获得WIN32完整的版本信息*/
_osver = GetVersion();
_winminor = (_osver >> 8) & 0x00FF ;
_winmajor = _osver & 0x00FF ;
_winver = (_winmajor << 8) + _winminor;
_osver = (_osver >> 16) & 0x00FFFF ;
_ioinit(); /* initialize lowio */
/* 获得命令行信息 */
_acmdln = (char *) GetCommandLineA();
/* 获得环境信息 */
_aenvptr = (char *) __crtGetEnvironmentStringsA();
_setargv(); /* 设置命令行参数 */
_setenvp(); /* 设置环境参数 */
_cinit(); /* C数据初始化:全局变量初始化,就在这里!*/
__initenv = _environ;
mainret = main( __argc, __argv, _environ ); /*调用main函数*/
exit( mainret );
}
从以上代码可知,运行库在调用用户程序的main或WinMain函数之前,进行了一些初始化工作。初始化完成后,接着才调用了我们编写的main或WinMain函数。只有这样,我们的C语言运行时库和应用程序才能正常地工作起来。
除了crt0.c外,C运行时库中还包含wcrt0.c、 wincrt0.c、wwincrt0.c三个文件用来提供初始化函数。wcrt0.c是crt0.c的宽字符集版,wincrt0.c中包含 windows应用程序的入口函数,而wwincrt0.c则是wincrt0.c的宽字符集版。
3.各种C运行时库的区别
(1)静态链接的单线程库
静态链接的单线程库只能用于单线程的应用程序,C运行时库的目标代码最终被编译在应用程序的二进制文件中。通过/ML编译选项可以设置Visual C++使用静态链接的单线程库。
(2)静态链接的多线程库
静态链接的多线程库的目标代码也最终被编译在应用程序的二进制文件中,但是它可以在多线程程序中使用。通过/MD编译选项可以设置Visual C++使用静态链接的单线程库。
(3)动态链接的运行时库
动态链接的运行时库将所有的C库函数保存在一个单独的动态链接库MSVCRTxx.DLL中,MSVCRTxx.DLL处理了多线程问题。使用/MD编译选项可以设置Visual C++使用动态链接的运行时库。
/MDd、 /MLd 或 /MTd 选项使用 Debug runtime library(调试版本的运行时刻函数库),与/MD、 /ML 或 /MT分别对应。Debug版本的 Runtime Library 包含了调试信息,并采用了一些保护机制以帮助发现错误,加强了对错误的检测,因此在运行性能方面比不上Release版本。
下面看一个未正确使用C运行时库的控制台程序:
include
#include
int main()
{
CFile file;
CString str("I love you");
TRY
{
file.Open("file.dat",CFile::modeWrite | CFile::modeCreate);
}
CATCH( CFileException, e )
{
#ifdef _DEBUG
afxDump << "File could not be opened " << e->m_cause << "\n";
#endif
}
END_CATCH
file.Write(str,str.GetLength());
file.Close();
}
在"rebuild all"的时候发生了link错误:
nafxcwd.lib(thrdcore.obj) : error LNK2001: unresolved external symbol __endthreadex
nafxcwd.lib(thrdcore.obj) : error LNK2001: unresolved external symbol __beginthreadex
main.exe : fatal error LNK1120: 2 unresolved externals
Error executing cl.exe.
发生错误的原因在于Visual C++对控制台程序默认使用单线程的静态链接库,而MFC中的CFile类已暗藏了多线程。我们只需要在Visual C++6.0中依次点选Project->Settings->C/C++菜单和选项,在Project Options里修改编译选项即可。
C 运行时库是微软对标准C库函数的实现,因为当时考虑到许多程序都使用C编写,而这些程序都要使用标准的C库,按照以前的方式每一个程序最终都要拷贝一份标准库的实现到程序中,这样同一时刻内存中可能有许多份标准库的代码(一个程序一份),所以微软出于效率的考虑把标准C库做为动态链接来实现,这样多个程序使用C标准库时内存中就只有一份拷贝了。(对每一个程序来说,它相当于自己拥有一份,对于标准库中的全局变量也做了处理的,不会因为共享同一份代码而出现冲突)。 这也算是对C标准库的一个扩展吧,至于说静态链接的时候仍然把它叫做运行时库那只能说这是个习惯问题而已了。
运行时库和普通的dll一样,如果有程序用到了才会加载,没有程序使用的时候不会驻留内存的。
运行库是程序在运行时所需要的库文件。通常运行库是以DLL形式提供的。 Delphi和C++ Builder的运行库为.bpl文件,实际也一个DLL。运行库中一般包括编程时常用的函数,如字符串操作、文件操作、界面等内容。不同的语言所支持的函数通常是不同的,所以使用的库也是完全不同的,这就是为什么有VB运行库、C运行库、Delphi运行库之分的原因。即使都是C++语言,也可能因为提供的函数不同,而使用不同的库。如VC++使用的运行库和C++ Builder就完全不同。
如果不使用运行库,每个程序中都会包括很多重复的代码,而使用运行库,可以大大缩小编译后的程序的大小。但另一方面,由于使用了运行库,所以在分发程序时就必须带有这些库,比较麻烦。如果在操作系统中找不到相应的运行库程序就无法运行。为了解决这个矛盾,Windows总是会带上它自己开发的软件的最新的运行库。象Windows 2000以后的版本都包括Visual Basic 5.0/6.0的库。Internet Explorer总是带有最新的Visual C++ 6.0的库。Windows XP带有Microsoft .NET 1.0(用于VB.NET和C#)的库。Visual C++、Delphi和C++ Builder允许用户选择所编译得到的程序是否依赖于运行库。而VB、FoxPro、PowerBuilder、LabWindows/CVI和 Matlab就不允许用户进行这种选择,必须依赖于运行库。
【Chapter 02 Unicode 简介】
Unicode 扩展于ASCII 字符集。
在严格的ASCII 中,每个字符用7 位表示,而Unicode 使用全16位字符集。
双字节字符集(DBCS: doulbe-byte character set)
为了容纳中国、日本和韩国的象形文字符号(大约21,000个)并仍保持和ASCII的某种兼容性,DBSC从256 代码开始。
双字符集问题并不是说字符由两个字节代表。问题在于一些字符由一个字节表示。这会引起附加的程序设计问题。
Unicode 解决方案
Unicode 使用宽字符集。Unicode 中的每个字符都是16位宽而不是8位宽。
Unicode 的缺点:Unicode 字符串占用的内存是ASCII字符串的两倍。但压缩文件有助于极大地减少文件所占的磁盘空间。
宽字符和 C
ANSI / ISO 9899-1990 美国国家标准程序设计语言 - C (也称ANSI C)
多字节字符集主要影响C 语言程序执行时期链接库函数。从而导致一些编译问题。
Char 数据类型
宽字符
Unicode 或者宽字符都没有改变char 数据类型在C 中的含义。char 继续表示一个字节的储存空间,sizeof(char) 继续返回1。
C 中的宽字符基于wchar_t 数据类型,它在几个头文件包括wchar.h 中都有定义,如:
typedef unsigned short wchar_t ;
因此,wchar_t 数据类型与无符号短整型类型相同,都是16 位宽。
wchar_t *p = L"Hello!";
staic wchar_t a[ ] = L"Hello!";
sizeof (a) 将返回14,索引数组a 可得到单独的字符。a[1] 的值是宽字符 e ,或者0x0065 。
宽字符链接库函数
宽字符串长度函数为:wcslen(wide-character string length: 宽字符串长度),并在string.h 和 wchar.h 中均有说明。
strlen 函数说明如下:
size_t _cdecl strlen(const char *);
wcslen 函数说明如下:
size_t _cdecl wcslen(const wchar_t *);
若:
wchar_t * pw = L"Hello!";
iLength = wcslen(pw);
函数将返回字符串中的字符数为6。当字符(或字符串)定义为宽字节(或宽字符串),字符串的字符长度不改变,只是位组长度改变了。
C 执行时期链接库函数都有宽字符版,如:wprintf。
维护单一原始码
既能按ASCII 编译又能按Unicode 编译的单一原始码文件。
办法一:使用VS6.0 中的tchar.h 头文件。由于该头文件不是ANSI C 标准的一部分。因此定义的每个函数和宏定义的前面都有一个下划线。
如:_tprintf 和 _tcslen 。
有时候这些名称也称为通用函数名称,因此它们既可以指向函数的Unicode 版也可以指向非Unicode 版。
tchar.h
#ifdef _UNICODE
#define _tcslen wcslen
typedef wchar_t TCHAR;
#define __T(x) L##x
#define _T(x) __T(x)
#define _TEXT(x) __T(x)
#ifndef _UNICODE
#define _tcslen strlen
typedef char TCHAR;
#define _T(x) x
宽字符和 Windows
Windows NT 从底层支援Unicode。这意味着Windows NT 内部使用由16位字符组成的字符串。因为世界上其它许多地方还不使用16位字符串,所以Windows NT 必须经常将字符串在操作系统内部转换。
Windows 头文件类型
windows.h 头文件包括其它头文件,包括windef.h 。
windef.h 头文件本身也包括winnt.h ,winnt.h 处理基本的Unicode 支持。
winnt.h 定义了新的数据类型,称作CHAR 和 WCHAR。
typedef char CHAR;
typedef wchar_t WCHAR; //wc
当定义8位字符时,在Windows 程序中使用数据类型是 CHAR。
当定义16位字符时,在Windows 程序中使用数据类型是 WCHAR。
一个基于WCHAR 数据类型的变量可在前面附加上字母wc 以说明一个宽字符。
winnt.h 头文件定义了8 位字符串指针的六种数据类型和四种可用作const 8 位字符串指针的数据类型。
typedef CHAR *PCHAR, *LPCH, * PCH, *NPSTR, *LPSTR, *PSTR;
typedef CONST CHAR *LPCCH, *PCCH, *LPCSTR, *PCSTR;
前缀N和L 表示near 和 long, 指的是16 位Windows 中两种大小不同的指标。在Win32 中near 和 long 指标没有区别。
winnt.h 定义了六种可作为16位字符串指针的数据类型和四种可作为const 16位字符串指针的数据类型。
typedef WCHAR *PWCHAR, *LPWCH, *PWCH, *NWPSTR, *LPWSTR, *PWSTR;
typedef CONST WCHAR *LPCWCH, *PCWCH, *LPCWSTR, *PCWSTR;
至此,在Windows程序设计中有了CHAR 类型(一个8位的char)和 WCHAR 类型(一个16位的wchar_t)。
#ifdef UNICODE
typedef WCHAR TCHAR, *PTCHAR;
typedef LPWSTR LPTCH, PTCH, PTSTR, LPTSTR;
typdef LPCWSTR LPCTSTR;
#else
typedef char TCHAR, *PTCHAR;
typedef LPSTR LPTCH, PTCH, PTSTR, LPTSTR;
typedef LPCSTR LPCTSTR;
#endif
winnt.h 头文件还定义了一个宏,该宏将L 添加到字符串的第一个引号前。
#ifdef UNICODE
#define __TEXT(quote) L##quote
#else
#define __TEXT(quote) quote
#endif
此外,TEXT 宏可以这样定义:
#define TEXT(quote) __TEXT(quote)
8 位字符变量和字符串,使用CHAR、PCHAR,以及带引号的字符串。
16 位字符变量和字符串,使用WCHAR、PWCHAR,并将L 添加到引号前面。
Windows 函数调用
从windows 1.0 ~ 3.1 的16位Windows 中,MessageBox 函数位于动态链接库USER.EXE。
int WINAPI MessageBox(HWND, LPCSTR, LPCSTR, UINT);
而32 位的windows 除了含有与16位兼容的USER.EXE 外,还含有一个称为USER32.DLL 的动态链接库。
在user32.dll 中,没有32 位MessageBox 函数的进入点。实际上有两个进入点:MessageBoxA(ASSIC 版本),MessageBoxW(宽字符版本)。
winuser. h 头文件中定义MessageBoxA 函数如下:
WINUSERAPI int WINAPI MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
WINUSERAPI int WINAPI MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);
由于很多程序员将继续使用MessageBox,winuser.h 头文件使用了如下技巧。
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif
Windows 的字符串函数
ILength = lstrlen(pString);
pString = lstrcpy(pString1, pString2);
pString = lstrcpyn(pString1, pString2, iCount);
pString = lstrcat(pString1, pString2);
iComp = lstrcmp(pString1, pString2);
iComp = lstrcmpi(pString1, pString2);
在Windows 中使用printf
在windows 程序中使用fprintf, 而不是printf。
但仍然可以使用sprintf 及 sprintf 系列中的其它函数来显示文字。
int printf(const char* szFormat, ...);
int sprintf(char *szBuffer, const char* szFormat,...);
printf("The sum of %i and %i is %i", 5, 3, 5+3);
相当于
char szBuffer[100];
sprintf(szBuffer, "The sum of %i and %i is %i", 5, 3, 5 + 3);
puts(szBuffer);
定义的字符串缓冲区必须足够大以存放结果。Microsoft 专用函数_snprintf 解决这一问题。
vsprintf 是 sprintf 的一个变形。该函数的第三个参数是指向格式化参数数组的指针。该指针指向在堆栈中供函数调用的变量。
va_list、va_start、vs_end 宏(在stdarg.h 中定义)帮助我们处理堆栈指针。
int sprintf(char * szBuffer, const char * szFormat, ...)
{
int iReturn;
va_list pArgs;
va_start(pArgs, szFormat);
iReturn = vsprintf(szBuffer, szFormat, pArgs);
va_end(pArgs);
return iReturn;
}
由于早期windows 程序使用了sprintf 和 vsprintf ,从而导致Microsoft 向Windows API 添加了两个相似的函数。
wsprintf 和 wvsprintf 函数,功能与sprintf 和 vsprintf 相同,但它们不能处理浮点格式。
参数的变数个数 ASCII 宽字符 常规
标准版 sprintf swprintf _stprintf
最大长度版 _snprintf _snwprintf _sntprintf
Windows
版本 wsprintfA wsprintfW wsprintf
参数数组的指针 ASCII 宽字符 常规
标准版 vsprintf wswprintf _vstprintf
最大长度版 _vsnprintf _nsnwprintf _vsntprintf
Windows
版本 wvsprintfA wvsprintfW wvsprintf
格式化消息框
/*------------------------------------------------------------------
// Subject: SceenSize.c
// Author: StartAoA
// Date: 2013-03-06
// Platform: WS2008 + Win 7 32bit SP1
// Version: 1.0
--------------------------------------------------------------------*/
#include
#include
#include
int CDECL MessageBoxPrintf (TCHAR * szCaption, TCHAR * szFormat, ...)
{
TCHAR szBuffer [1024] ;
va_list pArgList ;
// The va_start macro (defined in STDARG.H) is usually equivalent to:
// pArgList = (char *) &szFormat + sizeof (szFormat) ;
va_start (pArgList, szFormat) ;
// The last argument to wvsprintf points to the arguments
_vsntprintf ( szBuffer, sizeof (szBuffer) / sizeof (TCHAR),szFormat, pArgList) ;
// The va_end macro just zeroes out pArgList for no good reason
va_end (pArgList) ;
return MessageBox (NULL, szBuffer, szCaption, 0) ;
}
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
int cxScreen, cyScreen ;
cxScreen = GetSystemMetrics (SM_CXSCREEN) ;
cyScreen = GetSystemMetrics (SM_CYSCREEN) ;
MessageBoxPrintf(TEXT ("ScreenSize"),TEXT ("The screen is %i pixels wide by %i pixels high."),cxScreen, cyScreen) ;
return 0 ;
}
【Chapter 03 窗口和消息】
//***********************************************************************
// Project:HelloWin.c
// Author: StartAoA
// Date: 2013-03-07
// Platform: VS2008 + Win 7 32bit SP1
// Version: 1.0
//***********************************************************************
#include
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("HelloWin") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName= szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"),szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow(
szAppName, // window class name
TEXT ("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL) ; // creation parameters
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc ;
PAINTSTRUCT ps ;
RECT rect ;
switch (message)
{
case WM_CREATE:
PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ;
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
GetClientRect (hwnd, &rect) ;
DrawText (hdc, TEXT ("Hello, Windows 7!"), -1,&rect,DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
需在解决方案中选择 HelloWin-->属性-->配置属性-->连接器-->输入-->附加依赖项:输入winmm.lib 确定
大写字母标识符
前缀 |
类别 |
CS |
窗口类别样式 |
CW |
建立窗口 |
DT |
绘制文字 |
IDI |
图示ID |
IDC |
游标ID |
MB |
消息框 |
SND |
声音 |
WM |
窗口消息 |
WS |
窗口样式 |
【Chapter 04 输出文字】
在前一章,您看到了一个简单的Windows 98程序,它在窗口中央,或者更准确地说,在显示区域中央显示一行文字。正如我们学到的,显示区域是整个应用程序窗口中未被标题列、窗口边框,以及可选的菜单列、工具列、状态列和滚动条占据的部分。简而言之,显示区域是窗口中可以由程序任意书写和传递视觉信息的部分。
对于程序的显示区域,您几乎可以为所欲为,只不过您不能假定窗口大小是某一特定尺寸,或者在程序执行时其大小会保持不变。如果您不熟悉图形窗口环境的程序设计,这些限制可能会使您感到惊讶:不能再假设屏幕上的一行文字一定有80个字符了。您的程序必须与其它Windows程序共享视讯显示器。Windows使用者控制程序窗口在屏幕上显示的方式。尽管可以建立固定大小的窗口(这对于计算器之类的应用是合理的),但在大多数情况下,使用者应该能够改变应用程序窗口的大小。您的程序必须能够接受指定给它的大小,并且合理地利用这一空间。
这有两种可能的情况。一种可能是,程序只有仅能显示「hello」的显示区域;还有另一种可能,即程序在一个大屏幕、高分辨率的系统上执行,其显示区域大得足以显示两整页文字。灵活地处理这两种极端是Windows程序设计的要点之一。
这一章,我们将讲述程序在显示区域显示信息的方式,但比上一章说明的显示方式更加复杂。当程序在显示区域显示文字或图形时,它经常要「绘制」它的显示区域。本章着重讲述绘制的方法。
尽管Windows为显示图形提供了强大的图形设备接口(GDI)函数,但在这一章中,我只介绍简单文字行的显示。我也将忽略Windows能够使用的不同字体外形及字体大小,仅使用Windows的内定系统字体。这看起来似乎是一种限制,其实不然,本章涉及和解决的问题适用于所有Windows程序设计。在混合显示文字和图形时,Windows内定字体的字符大小通常决定了图形的尺寸。
本章表面上是讨论绘图的方法,实际上是讨论与设备无关的程序设计基础。Windows程序只能对显示区域大小甚至字符的大小做很少的假定,相反地,必须使用Windows提供的功能来取得关于程序执行环境的信息。
绘制和更新
在文字模式环境下,程序可以在显示器的任意部分输出,程序输出到屏幕上的内容会停留在原处,不会神秘地消失。因此,程序可以丢掉重新生成屏幕显示时所需的信息。
在Windows中,只能在窗口的显示区域绘制文字和图形,而且不能确保在显示区域内显示的内容会一直保留到程序下一次有意地改写它时还保留在那里。例如,使用者可能会在屏幕上移动另一个程序的窗口,这样就可能覆盖您的应用程序窗口的一部分。Windows不会保存您的窗口中被其它程序覆盖的区域,当程序移开后,Windows会要求您的程序更新显示区域的这个部分。
Windows是一个消息驱动系统。它通过把消息投入应用程序消息队列中或者把消息发送给合适的窗口消息处理程序,将发生的各种事件通知给应用程序。Windows通过发送WM_PAINT消息通知窗口消息处理程序,窗口的部分显示区域需要绘制。
WM_PAINT消息
大多数Windows程序在WinMain中进入消息循环之前的初始化期间都要调用函数UpdateWindow。Windows利用这个机会给窗口消息处理程序发送第一个WM_PAINT消息。这个消息通知窗口消息处理程序:必须绘制显示区域。此后,窗口消息处理程序应在任何时刻都准备好处理其它WM_PAINT消息,必要的话,甚至重新绘制窗口的整个显示区域。在发生下面几种事件之一时,窗口消息处理程序会接收到一个WM_PAINT消息:
在某些情况下,显示区域的一部分被临时覆盖,Windows试图保存一个显示区域,并在以后恢复它,但这不一定能成功。在以下情况下,Windows可能发送WM_PAINT消息:
在某些情况下,Windows总是保存它所覆盖的显示区域,然后恢复它。这些情况是:
处理WM_PAINT消息要求程序写作者改变自己向显示器输出的思维方式。程序应该组织成可以保留绘制显示区域需要的所有信息,并且仅当「响应要求」-即Windows给窗口消息处理程序发送WM_PAINT消息时才进行绘制。如果程序在其它时间需要更新其显示区域,它可以强制Windows产生一个WM_PAINT消息。这看来似乎是在屏幕上显示内容的一种舍近求远的方法。但您的程序结构可以从中受益。
有效矩形和无效矩形
尽管窗口消息处理程序一旦接收到WM_PAINT消息之后,就准备更新整个显示区域,但它经常只需要更新一个较小的区域(最常见的是显示区域中的矩形区域)。显然,当对话框覆盖了部分显示区域时,情况即是如此。在擦除对话框之后,需要重画的只是先前被对话框遮住的矩形区域。
这个区域称为「无效区域」或「更新区域」。正是显示区域内无效区域的存在,才会让Windows将一个WM_PAINT消息放在应用程序的消息队列中。只有在显示区域的某一部分失效时,窗口才会接受WM_PAINT消息。
Windows内部为每个窗口保存一个「绘图信息结构」,这个结构包含了包围无效区域的最小矩形的坐标以及其它信息,这个矩形就叫做「无效矩形」,有时也称为「无效区域」。如果在窗口消息处理程序处理WM_PAINT消息之前显示区域中的另一个区域变为无效,则Windows计算出一个包围两个区域的新的无效区域(以及一个新的无效矩形),并将这种变化后的信息放在绘制信息结构中。Windows不会将多个WM_PAINT消息都放在消息队列中。
窗口消息处理程序可以通过调用InvalidateRect使显示区域内的矩形无效。如果消息队列中已经包含一个WM_PAINT消息,Windows将计算出新的无效矩形。否则,它将一个新的WM_PAINT消息放入消息队列中。在接收到WM_PAINT消息时,窗口消息处理程序可以取得无效矩形的坐标(我们马上就会看到这一点)。通过调用GetUpdateRect,可以在任何时候取得这些坐标。
在处理WM_PAINT消息处理期间,窗口消息处理程序在调用了BeginPaint之后,整个显示区域即变为有效。程序也可以通过调用ValidateRect函数使显示区域内的任意矩形区域变为有效。如果这调用具有令整个无效区域变为有效的效果,则目前队列中的任何WM_PAINT消息都将被删除。
GDI 简介
要在窗口的显示区域绘图,可以使用Windows的图形设备接口(GDI)函数。Windows提供了几个GDI函数,用于将字符串输出到窗口的显示区域内。我们已经在上一章看过DrawText函数,但是目前使用最为普遍的文字输出函数是TextOut。该函数的格式如下:
TextOut (hdc, x, y, psText, iLength) ;
TextOut向窗口的显示区域写入字符串。psText参数是指向字符串的指针,iLength是字符串的长度。x和y参数定义了字符串在显示区域的开始位置(不久会讲述关于它们的详细情况)。hdc参数是「设备内容句柄」,它是GDI的重要部分。实际上,每个GDI函数都需要将这个句柄作为函数的第一个参数。
设备内容
读者可能还记得,句柄只不过是一个数值,Windows以它在内部使用对象。程序写作者从Windows取得句柄,然后在其它函数中使用该句柄。设备内容句柄是GDI函数的窗口「通行证」,有了这种设备内容句柄,程序写作者就能自如地在显示区域上绘图,使图形如自己所愿地变得好看或者难看。
设备内容(简称为「DC」)实际上是GDI内部保存的数据结构。设备内容与特定的显示设备(如视讯显示器或打印机)相关。对于视讯显示器,设备内容总是与显示器上的特定窗口相关。
设备内容中的有些值是图形「属性」,这些属性定义了GDI绘图函数工作的细节。例如,对于TextOut,设备内容的属性确定了文字的颜色、文字的背景色、x坐标和y坐标映像到窗口的显示区域的方式,以及显示文字时Windows使用的字体。
当程序需要绘图时,它必须先取得设备内容句柄。在取得了该句柄后,Windows用内定的属性值填入内部设备内容结构。在后面的章节中您会看到,可以通过调用不同的GDI函数改变这些默认值。利用其它的GDI函数可以取得这些属性的目前值。当然,还有其它的GDI函数能够在窗口的显示区域真正地绘图。
当程序在显示区域绘图完毕后,它必须释放设备内容句柄。句柄被程序释放后就不再有效,且不能再被使用。程序必须在处理单个消息处理期间取得和释放句柄。除了调用CreateDC(函数,在本章暂不讲述)建立的设备内容之外,程序不能在两个消息之间保存其它设备内容句柄。
Windows应用程序一般使用两种方法来取得设备内容句柄,以备在屏幕上绘图。
取得设备内容句柄:方法一
在处理WM_PAINT消息时,使用这种方法。它涉及BeginPaint和EndPaint两个函数,这两个函数需要窗口句柄(作为参数传给窗口消息处理程序)和PAINTSTRUCT结构的变量(在WINUSER.H表头文件中定义)的地址为参数。Windows程序写作者通常把这一结构变量命名为ps并且在窗口消息处理程序中定义它:
PAINTSTRUCT ps ;
在处理WM_PAINT消息时,窗口消息处理程序首先调用BeginPaint。BeginPaint函数一般在准备绘制时导致无效区域的背景被擦除。该函数也填入ps结构的字段。BeginPaint传回的值是设备内容句柄,这一传回值通常被保存在叫做hdc的变量中。它在窗口消息处理程序中的定义如下:
HDC hdc ;
HDC数据类型定义为32位的无正负号整数。然后,程序就可以使用需要设备内容句柄的TextOut等GDI函数。调用EndPaint即可释放设备内容句柄。
一般地,处理WM_PAINT消息的形式如下:
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
使用GDI函数
EndPaint (hwnd, &ps) ;
return 0 ;
在处理WM_PAINT消息时,必须成对地调用BeginPaint和EndPaint。如果窗口消息处理程序不处理WM_PAINT消息,则它必须将WM_PAINT消息传递给Windows中DefWindowProc(内定窗口消息处理程序)。DefWindowProc以下列代码处理WM_PAINT消息:
case WM_PAINT:
BeginPaint (hwnd, &ps) ;
EndPaint (hwnd, &ps) ;
return 0 ;
这两个BeginPaint和EndPaint调用之间中没有任何叙述,仅仅使先前无效区域变为有效。但以下方法是错误的:
case WM_PAINT:
return 0 ; // WRONG !!!
Windows将一个WM_PAINT消息放到消息队列中,是因为显示区域的一部分无效。如果不调用BeginPaint和EndPaint(或者ValidateRect),则Windows不会使该区域变为有效。相反,Windows将发送另一个WM_PAINT消息,且一直发送下去。
绘图信息结构
前面提到过,Windows为每个窗口保存一个「绘图信息结构」,这就是PAINTSTRUCT,定义如下:
typedef struct tagPAINTSTRUCT { HDC hdc ; BOOL fErase ; RECT rcPaint ; BOOL fRestore ; BOOL fIncUpdate ; BYTE rgbReserved[32] ; } PAINTSTRUCT ;
在程序调用BeginPaint时,Windows会适当填入该结构的各个字段值。使用者程序只使用前三个字段,其它字段由Windows内部使用。hdc字段是设备内容句柄。在旧版本的Windows中,BeginPaint的传回值也曾是这个设备内容句柄。在大多数情况下, fErase被标志为FALSE(0),这意味着Windows已经擦除了无效矩形的背景。这最早在BeginPaint 函数中发生(如果要在窗口消息处理程序中自己定义一些背景擦除行为,可以自行处理 WM_ERASEBKGND 消息)。Windows使用WNDCLASS结构的hbrBackground字段指定的画刷来擦除背景,这个WNDCLASS结构是程序在WinMain初始化期间登录窗口类别时使用的。许多Windows程序使用白色画刷。以下叙述设定窗口类别结构字段值:
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
不过,如果程序通过调用Windows函数InvalidateRect使显示区域中的矩形失效,则该函数的最后一个参数会指定是否擦除背景。如果这个参数为FALSE(即0),则Windows将不会擦除背景,并且在调用完BeginPaint后PAINTSTRUCT结构的fErase字段将为TRUE(非零)。
PAINTSTRUCT结构的rcPaint字段是RECT型态的结构。您已经在第三章中看到,RECT结构定义了一个矩形,其四个字段为left、top、right和bottom。PAINTSTRUCT结构的rcPaint字段定义了无效矩形的边界,如图4-1所示。这些值均以像素为单位,并相对于显示区域的左上角。无效矩形是应该重画的区域。
图4-1 无效矩形的边界 |
PAINTSTRUCT中的rcPaint矩形不仅是无效矩形,它还是一个「剪取」矩形。这意味着Windows将绘图操作限制在剪取矩形内(更确切地说,如果无效矩形区域不为矩形,则Windows将绘图操作限制在这个区域内)。
在处理WM_PAINT消息时,为了在更新的矩形外绘图,可以使用如下调用:
InvalidateRect (hwnd, NULL, TRUE) ;
该调用在BeginPaint调用之前进行,它使整个显示区域变为无效,并擦除背景。但是,如果最后一个参数等于FALSE,则不擦除背景,原有的东西将保留在原处。
通常这是Windows程序在无论何时收到WM_PAINT消息而不考虑rcPaint结构的情况下简单地重画整个显示区域最方便的方法。例如,如果在显示区域的显示输出中包括了一个圆,但是只有圆的一部分落到了无效矩形中,它就使仅绘制圆的无效部分变得没有意义。这需要画整个圆。在您使用从BeginPaint传回的设备内容句柄时,Windows不会绘制rcPaint矩形外的任何部分。
在第三章的HELLOWIN程序中,我们并不关心处理WM_PAINT消息时的无效矩形。如果文字显示区域恰巧在无效矩形内,则由DrawText恢复之。否则,在处理DrawText调用的某个时刻,Windows会确定它无须向显示器上输出。不过,这一决定需要时间。关心程序性能和速度的程序写作者希望在处理WM_PAINT期间使用无效矩形范围,以避免不必要的GDI调用。如果绘制时需要存取例如位图这样的磁盘文件,则这就显得尤其重要。
取得设备内容句柄:方法二
虽然最好是在处理WM_PAINT消息处理期间更新整个显示区域,但是您也会发现在处理非WM_PAINT消息处理期间绘制显示区域的某个部分也是非常有用的。或者您需要将设备内容句柄用于其它目的,如取得设备内容的信息。
要得到窗口显示区域的设备内容句柄,可以调用GetDC来取得句柄,在使用完后调用ReleaseDC:
hdc = GetDC (hwnd) ;
使用GDI函数
ReleaseDC (hwnd, hdc) ;
与BeginPaint和EndPaint一样,GetDC和ReleaseDC函数必须成对地使用。如果在处理某消息时调用GetDC,则必须在退出窗口消息处理程序之前调用ReleaseDC。不要在一个消息中调用GetDC却在另一个消息调用ReleaseDC。
与从BeginPaint传回设备内容句柄不同,GetDC传回的设备内容句柄具有一个剪取矩形,它等于整个显示区域。可以在显示区域的某一部分绘图,而不只是在无效矩形上绘图(如果确实存在无效矩形)。与BeginPaint不同,GetDC不会使任何无效区域变为有效。如果需要使整个显示区域有效,可以调用
ValidateRect (hwnd, NULL) ;
一般可以调用GetDC和ReleaseDC来对键盘消息(如在字处理程序中)和鼠标消息(如在画图程序中)作出反应。此时,程序可以立刻根据使用者的键盘或鼠标输入来更新显示区域,而不需要考虑为了窗口的无效区域而使用WM_PAINT消息。不过,一旦确实收到了WM_PAINT消息,程序就必须要收集足够的信息后才能更新显示。
与GetDC相似的函数是GetWindowDC。GetDC传回用于写入窗口显示区域的设备内容句柄,而GetWindowDC传回写入整个窗口的设备内容句柄。例如,您的程序可以使用从GetWindowDC传回的设备内容句柄在窗口的标题列上写入文字。然而,程序同样也应该处理WM_NCPAINT (「非显示区域绘制」)消息。
TextOut:细节
TextOut是用于显示文字的最常用的GDI函数。语法是:
TextOut (hdc, x, y, psText, iLength) ;
以下将详细地讨论这个函数。
第一个参数是设备内容句柄,它既可以是GetDC的传回值,也可以是在处理WM_PAINT消息时BeginPaint的传回值。
设备内容的属性控制了被显示的字符串的特征。例如,设备内容中有一个属性指定文字颜色,内定颜色为黑色;内定设备内容还定义了白色的背景。在程序向显示器输出文字时,Windows使用这个背景色来填入字符周围的矩形空间(称为「字符框」)。
该文字背景色与定义窗口类别时设置的背景并不相同。窗口类别中的背景是一个画刷,它是一种纯色或者非纯色组成的画刷,Windows用它来擦除显示区域,它不是设备内容结构的一部分。在定义窗口类别结构时,大多数Windows应用程序使用WHITE_BRUSH,以便内定设备内容中的内定文字背景颜色与Windows用以擦除显示区域背景的画刷颜色相同。
psText参数是指向字符串的指针,iLength是字符串中字符的个数。如果psText指向Unicode字符串,则字符串中的字节数就是iLength值的两倍。字符串中不能包含任何ASCII控制字符(如回车、换行、制表或退格),Windows会将这些控制字符显示为实心块。Text0ut不识别作为字符串结束标志的内容为零的字节(对于Unicode,是一个短整数型态的0),而需要由nLength参数指明长度。
TextOut中的x和y定义显示区域内字符串的开始位置,x是水平位置,y是垂直位置。字符串中第一个字符的左上角位于坐标点(x,y)。在内定的设备内容中,原点(x和y均为0的点)是显示区域的左上角。如果在TextOut中将x和y设为0,则将从显示区域左上角开始输出字符串。
当您阅读GDI绘图函数(例如TextOut)的文件时,就会发现传递给函数的坐标常常被称为「逻辑坐标」。在第五章会详细地解释这种情况。现在请注意,Windows有许多「坐标映像方式」,它们用来控制GDI函数指定的逻辑坐标转换为显示器的实际像素坐标的方式。映像方式在设备内容中定义,内定映像方式是MM_TEXT(使用WINGDI.H中定义的标识符)。在MM_TEXT映像方式下,逻辑单位与实际单位相同,都是像素;x的值从左向右递增,y的值从上向下递增(参看图4-2)。MM_TEXT坐标系与Windows在PAINTSTRUCT结构中定义无效矩形时使用的坐标系相同,这为我们带来了很多方便(但是,其它映像方式并非如此)。
图4-2 MM_TEXT映像方式下的x坐标和y坐标 |
设备内容也定义了一个剪裁区域。您已经看到,对于从GetDC取得的设备内容句柄,内定剪裁区域是整个显示区域;而对于从BeginPaint取得的设备内容句柄,则为无效区域。Windows不会在剪裁区域之外的任何位置显示字符串。如果一个字符有一部分在剪裁区域外,则Windows将只显示此区域内的那部分。要想将输出写到窗口的显示区域之外不是那么容易的,所以不用担心会无意间出现这种事情。
系统字体
设备内容还定义了在您调用TextOut显示文字时Windows使用的字体。内定字体为「系统字体」,或用Windows表头文件中的标识符,即SYSTEM_FONT。系统字体是Windows用来在标题列、菜单和对话框中显示字符串的内定字体。
在Windows的早期版本中,系统字体是等宽(fixed-pitch)字体,这意味着所有字符均具有同样的宽度,非常类似于打字机。然而,从Windows 3.0开始,系统字体成为一种变宽(variable-pitch)字体,这意味着不同的字符具有不同的大小,比如,「W」要比「i」宽。变宽字体比等宽字体好读,这已经是公认的事实。不过,可以想见,这一转变使很多原来的Windows程序代码不再适用,从而要求程序写作者学习一些使用字体的新技术。
系统字体是一种「点阵字体」,这意味着字符被定义为像素块(在第十七章,将讨论TrueType字体,它是由轮廓定义的)。至于确切的大小,系统字体的字符大小取决于视讯显示器的大小。系统字体设计为至少能在显示器上显示25行80列文字。
字符大小
要用TextOut显示多行文字,就必须确定字体的字符大小,可以根据字符的高度来定位字符的后续行,以及根据字符的宽度来定位字符的后续列。
系统字体的字符高度和平均宽度是多少?这个问题取决于视讯显示器的像素大小。Windows需要的最小显示大小是640×480,但是许多使用者更喜欢800×600或1024×768的显示大小。另外,对于这些较大的显示尺寸,Windows允许使用者选择不同大小的系统字体。
程序可以调用GetSystemMetrics函数以取使用者接口上各类视觉组件大小的信息,调用GetTextMetrics取得字体大小。GetTextMetrics传回设备内容中目前选取的字体信息,因此它需要设备内容句柄。Windows将文字大小的不同值复制到在WINGDI.H中定义的TEXTMETRIC型态的结构中。TEXTMETRIC结构有20个字段,我们只使用前七个:
typedef struct tagTEXTMETRIC{
LONG tmHeight ; // Specifies the height ( ascent + descent ) of characters.
LONG tmAscent ; // Specifies the ascent ( units above the base line ) of characters.
LONG tmDescent ; // Specifies the descent ( units below the base line ) of characters.
LONG tmInternalLeading ; // Specifies the amount of leading ( space ) inside the bounds set by the tmHeight member. Accent marks and other diacritical characters may occur in this area. The designer may set this member to zero.
LONG tmExternalLeading ; // Specifies the amount of extra leading (space ) that the application adds between rows. Since this area is outside the font, it contains no marks and is not altered by text output calls in either OPAQUE or TRANSPARENT mode. The designer may set this member to zero.
LONG tmAveCharWidth ; // Specifies the average width of characters in the font ( generally defined as the width of the letter x ). This values does not include the overhang required for bold or italic characters.
LONG tmMaxCharWidth ; // Specifies the width of the widest character in the font.
LONG tmWeight;
LONG tmOverhang;
LONG tmDigitizedAspecctX
LONG tmDigitizedAspecctY
TCHAR tmFirstChar;
TCHAR tmLastChar;
TCHAR tmDefaultChar;
TCHAR tmBreakChar;
BYTE tmItalic;
BYTE tmUnderlined;
BYTE tmStruckOut;
BYTE tmPitchAndFamily;
BYTE tmCharSet;
}TEXTMETRIC, * PTEXTMETRIC ;
这些字段值的单位取决于选定的设备内容映像方式。在内定设备内容下,映像方式是MM_TEXT,因此值的大小是以像素为单位。
要使用GetTextMetrics函数,需要先定义一个结构变量(通常称为tm):
TEXTMETRIC tm ;
在需要确定文字大小时,先取得设备内容句柄,再调用GetTextMetrics:
hdc = GetDC (hwnd) ;
GetTextMetrics (hdc, &tm) ;
ReleaseDC (hwnd, hdc) ;
此后,您就可以查看文字尺寸结构中的值,并有可能保存其中的一些以备将来使用。
文字大小:细节
TEXTMETRIC结构提供了关于目前设备内容中选用的字体的丰富信息。但是,字体的纵向大小只由5个值确定,其中4个值如图4-3所示。
图4-3 定义字体中纵向字符大小的4个值 |
最重要的值是tmHeight,它是tmAscent和tmDescent的和。这两个值表示了基准在线下字符的最大纵向高度。「间距」(leading)指打印机在两行文字间插入的空间。在TEXTMETRIC结构中,内部的间距包括在tmAscent中(因此也在tmHeight中),并且它经常是重音符号出现的地方。tmInternalLeading字段可被设成0,在这种情况下,加重音的字母会稍稍缩短以便容纳重音符号。
TEXTMETRIC结构还包括一个不包含在tmHeight值中的字段tmExternalLeading。它是字体设计者建议加在横向字符之间的空间大小。在安排文字行之间的空隙时,您可以接受设计者建议的值,也可以拒绝它。在系统字体中tmExternalLeading可以为0,因此我没有在图4-3中显示它。(尽管我不想告诉你们,图4-3确实就是Windows在640×480的显示分辨率中使用的系统字体。)
TEXTMETRICS结构包含有描述字符宽度的两个字段,即tmAveCharWidth(小写字母加权平均宽度)和tmMaxCharWidth(字体中最宽字符的宽度)。对于定宽字体,这两个值是相等的(图4-3中这些值分别为7和14)。
本章的范例程序还需要另一种字符宽度,即大写字母的平均宽度,这可以用tmAveCharWidth乘以150%大致计算出来。
必须认识到,系统字体的大小取决于Windows所执行的视讯显示器的分辨率,在某些情况下,取决于使用者选取的系统字体的大小。Windows提供了一个与设备无关的图形接口,但程序写作者还是有事情要处理的。不要想当然耳地猜测字体大小来写作Windows程序,也不要把值定死,您可以使用GetTextMetrics函数取得这一信息。
格式化文字
Windows启动后,系统字体的大小就不会发生改变,所以在程序执行过程中,程序写作者只需要调用一次GetTexMetrics。最好是在窗口消息处理程序中处理WM_CREATE消息时进行此调用,WM_CREATE消息是窗口消息处理程序接收的第一个消息。在WinMain中调用CreateWindow时,Windows会以一个WM_CREATE消息调用窗口消息处理程序。
假设要编写一个Windows程序,在显示区域显示几行文字,这需要先取得字符宽度和高度。您可以在窗口消息处理程序内定义两个变量来保存平均字符宽度(cxChar)和总的字符高度(cyChar):
static int cxChar, cyChar ;
变量名的前缀c代表「count」,在这里指像素数,与x和y结合,分别指宽和高。这些变量定义为static静态变量,因为它们在窗口消息处理程序中处理其它消息(如WM_PAINT)时也应该是有效的。如果变量在函数外面定义,则不需要定义为static。
下面是取得系统字体的字符宽度和高度的WM_CREATE程序代码:
case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; return 0 ;
注意我在计算cyChar时包括了tmExternalLeading字段,虽然该字段在系统字体中为0,但是因为它使得文字的可读性更好,所以还是应该把它包括进去。沿着窗口向下每隔cyChar像素就会显示一行文字。
您会发现常常需要显示格式化的数字跟简单的字符串。我在第二章讲到过,您不能使惯用的工具(可爱的printf函数)来完成这项工作,但是可以使用sprintf和Windows版的sprintf-wsprintf。这些函数与printf相似,只是把格式化字符串放到字符串中。然后,可以用TextOut将字符串输出到显示器上。非常方便的是,从sprintf和wsprintf传回的值就是字符串的长度。您可以将这个值传递给TextOut作为iLength参数。下面的程序代码显示了wsprintf与TextOut的典型组合:
int iLength ;
TCHAR szBuffer [40] ;
其它行程序
iLength = wsprintf (szBuffer, TEXT ("The sum of %i and %i is %i"), iA, iB, iA + iB) ;
TextOut (hdc, x, y, szBuffer, iLength) ;
对于这样简单的情况,可以将nLength的定义值与TextOut放在同一条叙述中,从而无需定义iLength:
TextOut (hdc, x, y, szBuffer, wsprintf (szBuffer, TEXT ("The sum of %i and %i is %i"), iA, iB, iA + iB)) ;
虽然这样子写起来不好看,但是功能与前者是一样的。
综合使用
现在,我们似乎已经具备了在屏幕上显示多行文字所需要的所有知识。我们知道如何在WM_PAINT消息处理期间取得一个设备内容句柄,如何使用TextOut函数以及如何根据字符大小来安排字距,剩下的就是显示一点有意义的东西了。
在前一章里,我们大概知道从Windows的GetSystemMetrics函数中取得的信息是很有意义的,该函数传回Windows中不同视觉组件的大小信息,如图标、光标、标题列和滚动条等。它们的大小因显示卡和驱动程序的不同而有所不同。GetSystemMetrics是在程序中完成与设备无关图形输出的重要函数。
该函数需要一个参数,叫做「索引」,在Windows表头文件定义了75个整数索引标识符(标识符的数量随着每个版本的Windows的发布而不断地增加,在Windows 1.0的程序写作者文件中仅列出了26个)。GetSystemMetrics传回一个整数,这个整数通常就是参数中指定的图形组件大小。
让我们来编写一个程序,显示一些可以从GetSystemMetrics调用中取得的信息,显示格式为每种视觉组件一行。如果我们建立一个表头文件,在表头文件中定义一个结构数组,此结构包含GetSystemMetrics索引对应的Windows表头文件标识符和调用所传回的每个值对应的字符串,这样处理起来要容易一些。表头文件名为SYSMETS.H,如程序4-1所示。
/*---------------------------------------------------------
SYSMETS.H -- System metrics display structure
-----------------------------------------------------------*/
#define NUMLINES ((int) (sizeof sysmetrics / sizeof sysmetrics [0]))
struct
{
int Index ;
TCHAR * szLabel ;
TCHAR * szDesc ;
} sysmetrics [ ] =
{
SM_CXSCREEN, TEXT ("SM_CXSCREEN"), TEXT ("Screen width in pixels"),
SM_CYSCREEN, TEXT ("SM_CYSCREEN"), TEXT ("Screen height in pixels"),
SM_CXVSCROLL, TEXT ("SM_CXVSCROLL"), TEXT ("Vertical scroll width"),
SM_CYHSCROLL, TEXT ("SM_CYHSCROLL"), TEXT ("Horizontal scroll height"),
SM_CYCAPTION, TEXT ("SM_CYCAPTION"), TEXT ("Caption bar height"),
SM_CXBORDER, TEXT ("SM_CXBORDER"), TEXT ("Window border width"),
SM_CYBORDER, TEXT ("SM_CYBORDER"), TEXT ("Window border height"),
SM_CXFIXEDFRAME, TEXT ("SM_CXFIXEDFRAME"), TEXT ("Dialog window frame width"),
SM_CYFIXEDFRAME, TEXT ("SM_CYFIXEDFRAME"), TEXT ("Dialog window frame height"),
SM_CYVTHUMB, TEXT ("SM_CYVTHUMB"), TEXT ("Vertical scroll thumb height"),
SM_CXHTHUMB, TEXT ("SM_CXHTHUMB"), TEXT ("Horizontal scroll thumb width"),
SM_CXICON, TEXT ("SM_CXICON"), TEXT ("Icon width"),
SM_CYICON, TEXT ("SM_CYICON"), TEXT ("Icon height"),
SM_CXCURSOR, TEXT ("SM_CXCURSOR"), TEXT ("Cursor width"),
SM_CYCURSOR, TEXT ("SM_CYCURSOR"), TEXT ("Cursor height"),
SM_CYMENU, TEXT ("SM_CYMENU"), TEXT ("Menu bar height"),
SM_CXFULLSCREEN, TEXT ("SM_CXFULLSCREEN"), TEXT ("Full screen client area width"),
SM_CYFULLSCREEN, TEXT ("SM_CYFULLSCREEN"), TEXT ("Full screen client area height"),
SM_CYKANJIWINDOW, TEXT ("SM_CYKANJIWINDOW"), TEXT ("Kanji window height"),
SM_MOUSEPRESENT, TEXT ("SM_MOUSEPRESENT"), TEXT ("Mouse present flag"),
SM_CYVSCROLL, TEXT ("SM_CYVSCROLL"), TEXT ("Vertical scroll arrow height"),
SM_CXHSCROLL, TEXT ("SM_CXHSCROLL"), TEXT ("Horizontal scroll arrow width"),
SM_DEBUG, TEXT ("SM_DEBUG"), TEXT ("Debug version flag"),
SM_SWAPBUTTON, TEXT ("SM_SWAPBUTTON"), TEXT ("Mouse buttons swapped flag"),
SM_CXMIN, TEXT ("SM_CXMIN"), TEXT ("Minimum window width"),
SM_CYMIN, TEXT ("SM_CYMIN"), TEXT ("Minimum window height"),
SM_CXSIZE, TEXT ("SM_CXSIZE"), TEXT ("Min/Max/Close button width"),
SM_CYSIZE, TEXT ("SM_CYSIZE"), TEXT ("Min/Max/Close button height"),
SM_CXSIZEFRAME, TEXT ("SM_CXSIZEFRAME"), TEXT ("Window sizing frame width"),
SM_CYSIZEFRAME, TEXT ("SM_CYSIZEFRAME"), TEXT ("Window sizing frame height"),
SM_CXMINTRACK, TEXT ("SM_CXMINTRACK"), TEXT ("Minimum window tracking width"),
SM_CYMINTRACK, TEXT ("SM_CYMINTRACK"), TEXT ("Minimum window tracking height"),
SM_CXDOUBLECLK, TEXT ("SM_CXDOUBLECLK"), TEXT ("Double click x tolerance"),
SM_CYDOUBLECLK, TEXT ("SM_CYDOUBLECLK"), TEXT ("Double click y tolerance"),
SM_CXICONSPACING, TEXT ("SM_CXICONSPACING"), TEXT ("Horizontal icon spacing"),
SM_CYICONSPACING, TEXT ("SM_CYICONSPACING"), TEXT ("Vertical icon spacing"),
SM_MENUDROPALIGNMENT, TEXT ("SM_MENUDROPALIGNMENT"), TEXT ("Left or right menu drop"),
SM_PENWINDOWS, TEXT ("SM_PENWINDOWS"), TEXT ("Pen extensions installed"),
SM_DBCSENABLED, TEXT ("SM_DBCSENABLED"), TEXT ("Double-Byte Char Set enabled"),
SM_CMOUSEBUTTONS, TEXT ("SM_CMOUSEBUTTONS"), TEXT ("Number of mouse buttons"),
SM_SECURE, TEXT ("SM_SECURE"), TEXT ("Security present flag"),
SM_CXEDGE, TEXT ("SM_CXEDGE"), TEXT ("3-D border width"),
SM_CYEDGE, TEXT ("SM_CYEDGE"), TEXT ("3-D border height"),
SM_CXMINSPACING, TEXT ("SM_CXMINSPACING"), TEXT ("Minimized window spacing width"),
SM_CYMINSPACING, TEXT ("SM_CYMINSPACING"), TEXT ("Minimized window spacing height"),
SM_CXSMICON, TEXT ("SM_CXSMICON"), TEXT ("Small icon width"),
SM_CYSMICON, TEXT ("SM_CYSMICON"), TEXT ("Small icon height"),
SM_CYSMCAPTION, TEXT ("SM_CYSMCAPTION"), TEXT ("Small caption height"),
SM_CXSMSIZE, TEXT ("SM_CXSMSIZE"), TEXT ("Small caption button width"),
SM_CYSMSIZE, TEXT ("SM_CYSMSIZE"), TEXT ("Small caption button height"),
SM_CXMENUSIZE, TEXT ("SM_CXMENUSIZE"), TEXT ("Menu bar button width"),
SM_CYMENUSIZE, TEXT ("SM_CYMENUSIZE"), TEXT ("Menu bar button height"),
SM_ARRANGE, TEXT ("SM_ARRANGE"), TEXT ("How minimized windows arranged"),
SM_CXMINIMIZED, TEXT ("SM_CXMINIMIZED"), TEXT ("Minimized window width"),
SM_CYMINIMIZED, TEXT ("SM_CYMINIMIZED"), TEXT ("Minimized window height"),
SM_CXMAXTRACK, TEXT ("SM_CXMAXTRACK"), TEXT ("Maximum draggable width"),
SM_CYMAXTRACK, TEXT ("SM_CYMAXTRACK"), TEXT ("Maximum draggable height"),
SM_CXMAXIMIZED, TEXT ("SM_CXMAXIMIZED"), TEXT ("Width of maximized window"),
SM_CYMAXIMIZED, TEXT ("SM_CYMAXIMIZED"), TEXT ("Height of maximized window"),
SM_NETWORK, TEXT ("SM_NETWORK"), TEXT ("Network present flag"),
SM_CLEANBOOT, TEXT ("SM_CLEANBOOT"), TEXT ("How system was booted"),
SM_CXDRAG, TEXT ("SM_CXDRAG"), TEXT ("Avoid drag x tolerance"),
SM_CYDRAG, TEXT ("SM_CYDRAG"), TEXT ("Avoid drag y tolerance"),
SM_SHOWSOUNDS, TEXT ("SM_SHOWSOUNDS"), TEXT ("Present sounds visually"),
SM_CXMENUCHECK, TEXT ("SM_CXMENUCHECK"), TEXT ("Menu check-mark width"),
SM_CYMENUCHECK, TEXT ("SM_CYMENUCHECK"), TEXT ("Menu check-mark height"),
SM_SLOWMACHINE, TEXT ("SM_SLOWMACHINE"), TEXT ("Slow processor flag"),
SM_MIDEASTENABLED, TEXT ("SM_MIDEASTENABLED"), TEXT ("Hebrew and Arabic enabled flag"),
SM_MOUSEWHEELPRESENT, TEXT ("SM_MOUSEWHEELPRESENT"), TEXT ("Mouse wheel present flag"),
SM_XVIRTUALSCREEN, TEXT ("SM_XVIRTUALSCREEN"), TEXT ("Virtual screen x origin"),
SM_YVIRTUALSCREEN, TEXT ("SM_YVIRTUALSCREEN"), TEXT ("Virtual screen y origin"),
SM_CXVIRTUALSCREEN, TEXT ("SM_CXVIRTUALSCREEN"), TEXT ("Virtual screen width"),
SM_CYVIRTUALSCREEN, TEXT ("SM_CYVIRTUALSCREEN"), TEXT ("Virtual screen height"),
SM_CMONITORS, TEXT ("SM_CMONITORS"), TEXT ("Number of monitors"),
SM_SAMEDISPLAYFORMAT, TEXT ("SM_SAMEDISPLAYFORMAT"), TEXT ("Same color format flag")
} ;
显示信息的程序命名为SYSMETS1。SYSMETS1.C的原始码如程序4-2所示。现在大多数程序代码看起来都很熟悉。WinMain中的程序代码实际上与HELLOWIN中的程序代码相同,并且WndProc中的大部分程序代码都已经讨论过了。
/*------------------------------------------------------------------
SYSMETS1.C -- System Metrics Display Program No. 1
(c) Charles Petzold, 1998
----------------------------------------------------------------*/
#include
#include "sysmets.h"
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[ ] = TEXT ("SysMets1") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 1"),WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT,NULL, NULL, hInstance, NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static int cxChar, cxCaps, cyChar ;
HDC hdc ;
int i ;
PAINTSTRUCT ps ;
TCHAR szBuffer [10] ;
TEXTMETRIC tm ;
switch (message)
{
case WM_CREATE:
hdc = GetDC (hwnd) ; // 获得设备句柄
GetTextMetrics (hdc, &tm) ; // 获取字号
cxChar = tm.tmAveCharWidth ; // 平均字符宽度
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; // 大写字母的平均宽度
cyChar = tm.tmHeight + tm.tmExternalLeading ; // 字符的总高度(包括外部间距)
ReleaseDC (hwnd, hdc) ;
return 0 ;
case WM_PAINT :
hdc = BeginPaint (hwnd, &ps) ;
for (i = 0 ; i < NUMLINES ; i++)
{
TextOut (hdc, 0, cyChar * i, sysmetrics[i].szLabel, lstrlen (sysmetrics[i].szLabel));
TextOut (hdc, 22 * cxCaps, cyChar * i, sysmetrics[i].szDesc, lstrlen (sysmetrics[i].szDesc)) ;
SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;
TextOut (hdc, 22 * cxCaps + 40 * cxChar, cyChar * i, szBuffer, wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex)));
SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
}
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY :
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
图4-4显示了在标准VGA上执行的SYSMETS1。在程序显示区域的前两行可以看到,屏幕宽度是640个像素,屏幕高度是480个像素,这两个值以及程序所显示的其它值可能会因显示器型态的不同而不同。
图4-4 SYSMETS1的显示 |
SYSMETS1.C窗口消息处理程序
SYSMETS1.C程序中的WndProc窗口消息处理程序处理三个消息:WM_CREATE、WM_PAINT和WM_DESTROY。WM_DESTROY消息的处理方法与第三章的HELLOWIN程序相同。
WM_CREATE消息是窗口消息处理程序接收到的第一个消息。在CreateWindow函数建立窗口时,Windows产生这个消息。在处理WM_CREATE消息时,SYSMETS1调用GetDC取得窗口的设备内容,并调用GetTextMetrics取得内定系统字体的文字大小。SYSMETS1将平均字符宽度保存在cxChar中,将字符的总高度(包括外部间距)保存在cyChar中。
SYSMETS1还将大写字母的平均宽度保存在静态变量cxCaps中。对于固定宽度的字体, cxCaps等于cxChar。对于可变宽度字体,cxCaps设定为cxChar乘以150%。对于可变宽度字体,TEXTMETRIC结构中的tmPitchAndFamily字段的低位为1,对于固定宽度字体,该值为0。 SYSMETS1使用这个位从cxChar计算cxCaps:
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
SYSMETS1在处理WM_PAINT消息处理期间完成所有窗口建立工作。通常,窗口消息处理程序先调用BeginPaint取得设备内容句柄,然后用一道for叙述对SYSMETS.H中定义的sysmetrics结构的每一行进行循环。三列文字用三个TextOut函数显示,对于每一列,TextOut的第三个参数都设定为:
cyChar * i
这个参数指示了字符串顶端相对于显示区域顶部的像素位置。
第一条TextOut叙述在第一列显示了大写标识符。TextOut的第二个参数是0,这是说文字从显示区域的左边缘开始。文字的内容来自sysmetrics结构的szLabel字段。我使用Windows函数lstrlen来计算字符串的长度,它是TextOut需要的最后一个参数。
第二条TextOut叙述显示了对系统尺寸值的描述。这些描述存放在sysmetrics结构的szDesc字段中。在这种情况下,TextOut的第二个参数设定为:
22 * cxCaps
第一列显示的最长的大写标识符有20个字符,因此第二列必须在第一列文字开头向右20 * cxCaps处开始。我使用22,以在两列之间加一点多余的空间。
第三条TextOut叙述显示从GetSystemMetrics函数取得的数值。变宽字体使得格式化向右对齐的数值有些棘手。从0到9的数字具有相同的宽度,但是这个宽度比空格宽度大。数值可以比一个数字宽,所以不同的数值应该从不同的横向位置开始。
那么,如果我们指定字符串结束的像素位置,而不是指定字符串的开始位置,以此向右对齐数值,是否会容易一些呢?用SetTextAlign函数就可以做到这一点。在SYSMETS1调用:
SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;
之后,传给后续TextOut函数的坐标将指定字符串的右上角,而不是左上角。
显示列数的TextOut函数的第二个参数设定为:
22 * cxCaps + 40 * cxChar
值40*cxChar包含了第二列的宽度和第三列的宽度。在TextOut函数之后,另一个对SetTextAlign的调用将对齐方式设定回普通方式,以进行下次循环。
/
The TextOut function writes a character string at the specified location, using the currently selected font, background color, and text color.
BOOL TextOut(
HDC hdc, // handle to DC
int nXStart, // x-coordinate of starting position
int nYStart, // y-coordinate of starting position
LPCTSTR lpString, // character string
int cbString // number of characters
);
hdc
Windows 95/98/Me: This value may not exceed 8192.
Return Values
If the function succeeds, the return value is nonzero.
If the function fails, the return value is zero.
Windows NT/2000/XP: To get extended error information, call GetLastError.
/
空间不够
在SYSMETS1程序中存在着一个很难处理的问题:除非您有一个大屏幕跟高分辨率的显示卡,否则就无法看到系统尺度列表的最后几行。如果窗口太窄,甚至根本看不到值。
SYSMETS1不知道这个问题。否则我们就会显示一个消息框说「抱歉!」程序甚至不知道它的显示区域有多大,它从窗口顶部开始输出文字,并依赖Windows 裁剪超出显示区域底部的内容。
显然,这很不理想。为了解决这个问题,我们的第一个任务是确定程序在显示区域内能输出多少内容。
显示区域的大小
如果您使用过现有的Windows应用程序,可能会发现窗口的尺寸变化极大。窗口最大化时(假定窗口只有标题列并且没有菜单),显示区域几乎占据了整个屏幕。这一最大化了的显示区域的尺寸可以通过以SM_CXFULLSCREEN 和 SM_CYFULLSCREEN 为参数调用GetSystemMetrics来获得。窗口的最小尺寸可以很小,有时甚至不存在,更不用说显示区域了。
在最近一章,我们使用GetClientRect函数来取得显示区域的大小。使用这个函数没有什么不好,但是在您每次要使用信息时就去调用它一遍是没有效率的。
确定窗口显示区域大小的更好方法是在窗口消息处理程序中处理 WM_SIZE 消息。在窗口大小改变时,Windows给窗口消息处理程序发送一个WM_SIZE消息。
传给窗口消息处理程序的 lParam 参数 的 低字组中包含显示区域的 宽度,高字组中包含显示区域的高度。要保存这些尺寸,需要在窗口消息处理程序中定义两个静态变量:
static int cxClient, cyClient ;
与cxChar 和 cyChar 相似,这两个变量在窗口消息处理程序内定义为 静态变量,因为在以后处理其它消息时会用到它们。处理 WM_SIZE 的方法如下:
case WM_SIZE:
cxClient = LOWORD (lParam) ; // 低字组中包含显示区域的宽度, lParam 一个32位的消息参数,其值与消息有关
cyClient = HIWORD (lParam) ; // 高字组中包含显示区域的高度
return 0 ;
实际上您会在每个Windows程序中看到类似的程序代码。LOWORD 和 HIWORD 宏在Windows表头文件WINDEF.H中定义。这些宏的定义看起来像这样:
#define LOWORD(l) ((WORD)(l))
#define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))
这两个宏传回 WORD值(16位的无正负号整数,范围从0到0xFFFF)。一般,将这些值保存在32位有号整数中。这就不会牵扯到任何转换问题,并使得这些值在以后需要的任何计算中易于使用。
在许多Windows程序中,WM_SIZE消息必然跟着一个WM_PAINT消息。为什么呢?因为在我们定义窗口类别时指定窗口类别样式为:
CS_HREDRAW | CS_VREDRAW
这种窗口类别样式告诉Windows,如果水平或者垂直大小发生改变, 则强制更新显示区域。
用如下公式计算可以在显示区域内显示的文字的总行数:
cyClient / cyChar
如果显示区域的高度太小以至无法显示一个完整的字符,这个公式的结果可以为0。类似地,在显示区域的水平方向可以显示的小写字符的近似数目为:
cxClient / cxChar
如果在处理WM_CREATE消息处理期间取得cxChar和cyChar,则不用担心在这两个计算公式中会出现被0除的情况。在WinMain调用CreateWindow时,窗口消息处理程序接收一个WM_CREATE消息。在WinMain调用ShowWindow之后接收到第一个WM_CREATE消息,此时cxChar和cyChar已经被赋予正的非零值了。
如果显示区域的大小不足以容纳所有的内容,那么,知道窗口显示区域的大小只是为使用者提供了在显示区域内卷动文字的第一步。如果您对其他有类似需求的Windows应用程序很熟悉,就很可能知道,这种情况下,我们需要使用「滚动条」。
滚动条
滚动条是图形使用者接口中最好的功能之一,它很容易使用,而且提供了很好的视觉回馈效果。您可以使用滚动条显示任何东西--无论是文字、图形、表格、数据库记录、图像或是网页,只要它所需的空间超出了窗口的显示区域所能提供的空间,就可以使用滚动条。
滚动条既有垂直方向的(供上下移动),也有水平方向的(供左右移动)。使用者可以使用鼠标在滚动条两端的箭头上或者在箭头之间的区域中点一下,这时,「卷动方块」在卷动列内的移动位置与所显示的信息在整个文件中的近似相关位置成比例。使用者也可以用鼠标拖动卷动方块到特定的位置。图4-5显示了垂直滚动条的建议用法。
图4-5 垂直滚动条 |
有时,程序写作者对卷动概念很难理解,因为他们的观点与使用者的观点不同:使用者向下卷动是想看到文件较下面的部分;但是,程序实际上是将文件相对于显示窗口向上移动。Windows文件和表头文件标识符是依据使用者的观点:向上卷动意味着朝文件的开头移动;向下卷动意味着朝文件尾部移动。
很容易在应用程序中包含水平或者垂直的滚动条,程序写作者只需要在CreateWindow的第三个参数中包括窗口样式(WS)标识符WS_VSCROLL(垂直卷动)和/或WS_HSCROLL(水平卷动)即可。这些卷动列通常放在窗口的右部和底部,伸展为显示区域的整个长度或宽度。显示区域不包含卷动列所占据的空间。对于特定的显示驱动程序和显示分辨率,垂直卷动列的宽度和水平卷动列的高度是恒定的。如果需要这些值,可以使用GetSystemMetrics调用来取得(如前面的程序那样)。
Windows负责处理对滚动条的所有鼠标操作,但是,窗口滚动条没有自动的键盘接口。如果想用光标键来完成卷动功能,则必须提供这方面的程序代码(我们将在下一章另一个版本的SYSMETS程序中做到这一点)。
滚动条的范围和位置
每个滚动条均有一个相关的「范围」(这是一对整数,分别代表最小值和最大值)和「位置」(它是卷动方块在此范围内的位置)。当卷动方块在卷动列的顶部(或左部)时,卷动方块的位置是范围的最小值;在卷动列的底部(或右部)时,卷动方块的位置是范围的最大值。
在内定情况下,滚动条的范围是从0(顶部或左部)至100(底部或右部),但将范围改变为更方便于程序的数值也是很容易的:
SetScrollRange (hwnd, iBar, iMin, iMax, bRedraw) ;
参数iBar为 SB_VERT或者 SB_HORZ,iMin和iMax分别是范围的最小值和最大值。如果想要Windows根据新范围重画滚动条,则设置bRedraw为TRUE(如果在调用SetScrollRange后,调用了影响滚动条位置的其它函数,则应该将bRedraw设定为FALSE以避免过多的重画)。
卷动方块的位置总是离散的整数值。例如,范围为0至4的滚动条具有5个卷动方块位置,如图4-6所示。
图4-6 具有5个卷动方块位置的卷动列 |
您可以使用SetScrollPos在滚动条范围内设置新的卷动方块位置:
SetScrollPos (hwnd, iBar, iPos, bRedraw) ;
参数iPos是新位置,它必须在iMin至iMax的范围内。Windows提供了类似的函数(GetScrollRange和GetScrollPos)来取得滚动条的目前范围和位置。
在程序内使用滚动条时,程序写作者与Windows共同负责维护滚动条以及更新卷动方块的位置。下面是Windows对滚动条的处理:
以下是程序写作者应该完成的工作:
像生活中的大多数事情一样,在我们看一些程序代码时这些会显得更加有意义。
滚动条消息
在用鼠标单击滚动条或者拖动卷动方块时,Windows给窗口消息处理程序发送WM_VSCROLL(供上下移动)和WM_HSCROLL(供左右移动)消息。在滚动条上的每个鼠标动作都至少产生两个消息,一条在按下鼠标按钮时产生,一条在释放按钮时产生。
和所有的消息一样,WM_VSCROLL 和 WM_HSCROLL 也带有 wParam 和 lParam 消息参数。对于来自作为窗口的一部分而建立的滚动条消息,您可以忽略lParam;
它只用于作为子窗口而建立的滚动条(通常在对话框内)。
wParam消息参数被分为一个低字组和一个高字组。
wParam的低字组是一个数值,它指出了鼠标对滚动条进行的操作。这个数值被看作一个「通知码」。通知码的值由以SB(代表「scroll bar(滚动条)」)开头的标识符定义。
以下是在WINUSER.H中定义的通知码:
#define SB_LINEUP 0
#define SB_LINELEFT 0
#define SB_LINEDOWN 1
#define SB_LINERIGHT 1
#define SB_PAGEUP 2
#define SB_PAGELEFT 2
#define SB_PAGEDOWN 3
#define SB_PAGERIGHT 3
#define SB_THUMBPOSITION 4
#define SB_THUMBTRACK 5
#define SB_TOP 6
#define SB_LEFT 6
#define SB_BOTTOM 7
#define SB_RIGHT 7
#define SB_ENDSCROLL 8
包含 LEFT和 RIGHT 的标识符用于水平滚动条,
包含UP、DOWN、TOP和BOTTOM的标识符用于垂直滚动条。
鼠标在滚动条的不同区域单击所产生的通知码如图4-7所示。
图4-7 用于滚动条消息的wParam值的标识符 |
如果在滚动条的各个部位按住鼠标键,程序就能收到多个滚动条消息。当释放鼠标键后,程序会收到一个带有SB_ENDSCROLL通知码的消息。一般可以忽略这个消息,Windows不会去改变卷动方块的位置,而您可以在程序中调用SetScrollPos来改变卷动方块的位置。
当把鼠标的光标放在卷动方块上并按住鼠标键时,您就可以移动卷动方块。这样就产生了带有SB_THUMBTRACK和SB_THUMBPOSITION通知码的滚动条消息。
在wParam 的低字组是SB_THUMBTRACK 时,
wParam 的高字组是使用者在拖动卷动方块时的目前位置。
该位置位于卷动列范围的最小值和最大值之间。
在wParam的低字组是SB_THUMBPOSITION时,wParam的高字组是使用者释放鼠标键后卷动方块的最终位置。对于其它的卷动列操作,wParam的高字组应该被忽略。
为了给使用者提供回馈,Windows在您用鼠标拖动卷动方块时移动它,同时您的程序会收到SB_THUMBTRACK消息。然而,如果不通过调用SetScrollPos来处理SB_THUMBTRACK或SB_THUMBPOSITION消息,在使用者释放鼠标键后,卷动方块会迅速跳回原来的位置。
程序能够处理SB_THUMBTRACK或SB_THUMBPOSITION消息,但一般不同时处理两者。如果处理SB_THUMBTRACK消息,在使用者拖动卷动方块时您需要移动显示区域的内容。而如果处理SB_THUMBPOSITION消息,则只需在使用者停止拖动卷动方块时移动显示区域的内容。处理SB_THUMBTRACK消息更好一些(但更困难),对于某些型态的数据,您的程序可能很难跟上产生的消息。
WINUSER.H表头文件还包括SB_TOP、SB_BOTTOM、SB_LEFT和SB_RIGHT通知码,指出滚动条已经被移到了它的最小或最大位置。然而,对于作为应用程序窗口一部分而建立的滚动条来说,永远不会接收到这些通知码。
在滚动条范围使用32位的值也是有效的,尽管这不常见。然而,wParam的高字组只有16位的大小,它不能适当地指出SB_THUMBTRACK和SB_THUMBPOSITION操作的位置。在这种情况下,需要使用GetScrollInfo函数(在下面描述)来得到信息。
在SYSMETS中加入卷动功能
前面的说明已经很详尽了,现在,要将那些东西动手做做看了。让我们开始时简单些,从垂直卷动着手,因为我们实在太需要垂直卷动了,而暂时还可以不用水平卷动。SYSMET2如程序4-3所示。这个程序可能是滚动条的最简单的应用。
/*------------------------------------------------------------------ SYSMETS2.C -- System Metrics Display Program No. 2 (c) Charles Petzold, 1998 ------------------------------------------------------------------*/ #include#include "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("SysMets2") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 2"),WS_OVERLAPPEDWINDOW | WS_VSCROLL, CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int cxChar, cxCaps, cyChar, cyClient, iVscrollPos ; HDC hdc ; int i, y ; PAINTSTRUCT ps ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; SetScrollRange (hwnd, SB_VERT, 0, NUMLINES - 1, FALSE) ; // The SetScrollRange function sets the minimun and maximum scroll box positions for the specified scroll bar. // SB_VERT: Sets the range of a window's standard vertical scroll bar. SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ; // The SetScrollPos function sets the position of the scroll box ( thumb ) in the specified scroll bar and, if requested, redraws the // scroll bar to reflect the new position of the scroll box. return 0 ; case WM_SIZE: // 确定窗口显示区域大小的更好方法是在窗口消息处理程序中处理WM_SIZE消息。在窗口大小改变时,Windows给窗口消息处理程序发送一个WM_SIZE消息。 cyClient = HIWORD (lParam) ; return 0 ; case WM_VSCROLL: switch (LOWORD (wParam)) // wParam的低字组是一个数值, 鼠标对滚动条进行的操作 { case SB_LINEUP: // Scrolls one line up. iVscrollPos -= 1 ; break ; case SB_LINEDOWN: // Scrolls one line down. iVscrollPos += 1 ; break ; case SB_PAGEUP: // Scrolls one page down. iVscrollPos -= cyClient / cyChar ; break ; case SB_PAGEDOWN: // Scrolls one page down. iVscrollPos += cyClient / cyChar ; break ; case SB_THUMBPOSITION: iVscrollPos = HIWORD (wParam) ; break ; default : break ; } iVscrollPos = max (0, min (iVscrollPos, NUMLINES - 1)) ; if (iVscrollPos != GetScrollPos (hwnd, SB_VERT)) { SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ; InvalidateRect (hwnd, NULL, TRUE) ; } return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMLINES ; i++) { y = cyChar * (i - iVscrollPos) ; TextOut (hdc, 0, y,sysmetrics[i].szLabel, lstrlen (sysmetrics[i].szLabel)) ; TextOut (hdc, 22 * cxCaps, y,sysmetrics[i].szDesc, lstrlen (sysmetrics[i].szDesc)) ; SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut (hdc, 22 * cxCaps + 40 * cxChar, y, szBuffer, wsprintf (szBuffer, TEXT ("%5d"),GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
新的CreateWindow调用在第三个参数中包含了WS_VSCROLL窗口样式,从而在窗口中加入了垂直滚动条,其窗口样式为:
WS_OVERLAPPEDWINDOW | WS_VSCROLL
WndProc窗口消息处理程序在处理WM_CREATE消息时增加了两条叙述,以设置垂直滚动条的范围和初始位置:
SetScrollRange (hwnd, SB_VERT, 0, NUMLINES - 1, FALSE) ;
SetScrollPos (hwnd, SB_VERT, iVscrollPos, TRUE) ;
sysmetrics结构具有NUMLINES行文字,所以滚动条范围被设定为0至NUMLINES-1。滚动条的每个位置对应于在显示区域顶部显示的一个文字行。如果卷动方块的位置为0,则第一行会被放置在显示区域的顶部。如果位置大于0,其它行就会出现在显示区域的顶部。当位置为NUMLINES-1时,则最后一行文字出现在显示区域的顶部。
为了有助于处理WM_VSCROLL消息,在窗口消息处理程序中定义了一个静态变量iVscrollPos,这一变量是滚动条内卷动方块的目前位置。对于SB_LINEUP和SB_LINEDOWN,只需要将卷动方块调整一个单位的位置。对于SB_PAGEUP和SB_PAGEDOWN,我们想移动一整面的内容,或者移动cyClient /cyChar个单位的位置。对于SB_THUMBPOSITION,新的卷动方块位置是wParam的高字组。SB_ENDSCROLL和SB_THUMBTRACK消息被忽略。
在程序依据收到的WM_VSCROLL消息计算出新的iVscrollPos值后,用min和max宏来调整iVscrollPos,以确保它在最大值与最小值之间。程序然后将iVscrollPos与调用GetScrollPos取得的先前位置相比较,如果卷动位置发生了变化,则使用SetScrollPos来进行更新,并且调用InvalidateRect使整个窗口无效。
InvalidateRect调用产生一个WM_PAINT消息。SYSMETS1在处理WM_PAINT消息时,每一行的y坐标计算公式为:
cyChar * i
在SYSMETS2中,计算公式为:
cyChar * (i - iVscrollPos)
循环仍然显示NUMLINES行文字,但是对于非零值的iVscrollPos是负数。程序实际上在显示区域以外显示这些文字行。当然,Windows不会显示这些行,因此屏幕显得干净和漂亮。
前面说过,我们一开始不想弄得太复杂,这样的程序代码很浪费,效率很低。下面我们对此加以修改,但是先要考虑在WM_VSCROLL 消息之后更新显示区域的方法。
绘图程序的组织
在处理完滚动条消息后,SYSMETS2不更新显示区域,相反,它调用InvalidateRect使显示区域失效。这导致Windows将一个WM_PAINT消息放入消息队列中。
最好能使Windows程序在响应WM_PAINT消息时完成所有的显示区域绘制功能。因为程序必须在一接收到WM_PAINT消息时就更新整个显示区域,如果在程序的其它部分也绘制的话,将很可能使程序代码重复。
首先,您可能对这种拐弯抹角的方式感到厌烦。在Windows的早期,因为这种方式与文字模式的程序设计差别太大,程序写作者感到这种概念很难理解。并且,程序要不断地通过马上绘制画面来响应键盘和鼠标。这样做既方便又有效,但是在很多情况下,这完全不必要。当您掌握了在响应WM_PAINT消息时积累绘制显示区域所需要的全部信息的原则之后,会对这种结果感到满意的。
如同SYSMETS2示范的,程序仍然需要在处理非WM_PAINT消息时更新特定的显示区域,使用InvalidateRect就很方便,您可以用它使显示区域内的特定矩形或者整个显示区域失效。
只将窗口显示区域标记为无效以产生WM_PAINT消息,对于某些应用程序来说也许不是完全令人满意的选择。在调用InvalidateRect之后,Windows将WM_PAINT消息放入消息队列中,最后由窗口消息处理程序处理它。然而,Windows将WM_PAINT消息当成低优先级消息,如果系统有许多其它的动作正在发生,那么也许会让您等待一会儿工夫。这时,当对话框消失时,将会出现一些空白的「洞」,程序仍然等待更新它的窗口。
如果您希望立即更新无效区域,可以在调用InvalidateRect之后调用UpdateWindow:
UpdateWindow (hwnd) ;
如果显示区域的任一部分无效,则UpdateWindow将导致Windows用WM_PAINT消息调用窗口消息处理程序(如果整个显示区域有效,则不调用窗口消息处理程序)。这一WM_PAINT消息不进入消息队列,直接由Windows调用窗口消息处理程序。窗口消息处理程序完成更新后立即退出,Windows将控制传回给程序中UpdateWindow调用之后的叙述。
您可能注意到,UpdateWindow与WinMain中用来产生第一个WM_PAINT消息的函数相同。最初建立窗口时,整个显示区域内容变为无效,UpdateWindow 指示窗口消息处理程序绘制显示区域。
建立更好的滚动
SYSMETS2动作良好,但它只是模仿其它程序中的滚动条,并且效率很低。很快我将示范一个新的版本,改进它的不足。也许最有趣的是这个新版本不使用目前所讨论的四个滚动条函数。相反,它将使用Win32 API中才有的新函数。
滚动条信息函数
滚动条文件(在/Platform SDK/User Interface Services/Controls/Scroll Bars中)指出SetScrollRange、SetScrollPos、GetScrollRange和GetScrollPos函数是「过时的」,但这并不完全正确。这些函数在Windows 1.0中就出现了,在Win32 API中升级以处理32位参数。它们仍然具有良好的功能。而且,它们不与Windows程序设计中新函数相冲突,这就是我在此书中仍使用它们的原因。
Win32 API介绍的两个滚动条函数称作SetScrollInfo和GetScrollInfo。这些函数可以完成以前函数的全部功能,并增加了两个新特性。
第一个功能涉及卷动方块的大小。您可能注意到,卷动方块大小在SYSMETS2程序中是固定的。然而,在您可能使用到的一些Windows应用程序中,卷动方块大小与在窗口中显示的文件大小成比例。显示的大小称作「页面大小」。算法为:
可以使用SetScrollInfo来设置页面大小(从而设置了卷动方块的大小),如将要看到的SYSMETS3程序所示。
GetScrollInfo 函数增加了第二个重要的功能,或者说它改进了目前API的不足。假设您要使用65,536或更大单位的范围,这在16位Windows中是不可能的。当然在Win32中,函数被定义为可接受32位参数,因此是没有问题的。(记住如果使用这样大的范围,卷动方块的实际物理位置数仍然由卷动列的像素大小限制)。然而,当使用SB_THUMBTRACK 或 SB_THUMBPOSITION 通知码得到 WM_VSCROLL 或WM_HSCROLL 消息时,只提供了16位数据来指出卷动方块的目前位置。通过GetScrollInfo函数可以取得真实的32位值。
SetScrollInfo和GetScrollInfo函数的语法是
SetScrollInfo (hwnd, iBar, &si, bRedraw) ;
GetScrollInfo (hwnd, iBar, &si) ;
像在其它滚动条函数中那样,iBar参数是SB_VERT或SB_HORZ,它还可以是用于滚动条控制的SB_CTL。SetScrollInfo的最后一个参数可以是TRUE或FALSE,指出了是否要Windows重新绘制计算了新信息后的滚动条。
两个函数的第三个参数是SCROLLINFO结构,定义为:
typedef struct tagSCROLLINFO
{
UINT cbSize ;// set to sizeof (SCROLLINFO)
UINT fMask ; // values to set or get
int nMin ; // minimum range value
int nMax ; // maximum range value
UINT nPage ; // page size
int nPos ; // current position
int nTrackPos ;// current tracking position
} SCROLLINFO, * PSCROLLINFO ;
在程序中,可以定义如下的SCROLLINFO 结构类型:
SCROLLINFO si ;
在调用SetScrollInfo或GetScrollInfo之前,必须将cbSize字段设定为结构的大小:
si.cbSize = sizeof (si) ;
或
si.cbSize = sizeof (SCROLLINFO) ;
逐渐熟悉Windows后,您就会发现另外几个结构像这个结构一样,第一个字段指出了结构大小。这个字段使将来的Windows版本可以扩充结构并添加新的功能,并且仍然与以前编译的版本兼容。
把fMask字段设定为一个以上以SIF前缀开头的标示,并且可以使用C的位操作OR运算(|)组合这些标示。
SetScrollInfo函数使用SIF_RANGE 标示时,必须把nMin和nMax字段设定为所需的滚动条范围。GetScrollInfo函数使用SIF_RANGE旗标时,应把nMin和nMax字段设定为从函数传回的目前范围。
SIF_POS旗标也一样。当通过SetScrollInfo使用它时,必须把结构的nPos字段设定为所需的位置。可以通过GetScrollInfo使用SIF_POS旗标来取得目前位置。
使用SIF_PAGE旗标能够取得页面大小。用SetScrollInfo函数把nPage设定为所需的页面大小。GetScrollInfo使用SIF_PAGE旗标可以取得目前页面的大小。如果不想得到比例化的滚动条,就不要使用该旗标。
当处理带有 SB_THUMBTRACK 或 SB_THUMBPOSITION 通知码的 WM_VSCROLL 或 WM_HSCROLL 消息时,通过GetScrollInfo只使用 SIF_TRACKPOS 旗标。从函数的传回中,SCROLLINFO 结构的 nTrackPos 字段将指出目前的32位的卷动方块位置。
在SetScrollInfo函数中仅使用SIF_DISABLENOSCROLL旗标。如果指定了此旗标,而且新的滚动条参数使滚动条消失,则该滚动条就不能使用了(下面会有更多的解释)。
SIF_ALL旗标是SIF_RANGE、SIF_POS、SIF_PAGE和SIF_TRACKPOS的组合。在WM_SIZE消息处理期间设置滚动条参数时,这是很方便的(在SetScrollInfo函数中指定SIF_TRACKPOS后,它会被忽略)。这在处理滚动条消息时也是很方便的。
卷动范围
在SYSMETS2中,卷动范围设置最小为0,最大为NUMLINES-1。当滚动条位置是0时,第一行信息显示在显示区域的顶部;当滚动条的位置是NUMLINES-1时,最后一行显示在显示区域的顶部,并且看不见其它行。
可以说SYSMETS2卷动范围太大。事实上只需把信息最后一行显示在显示区域的底部而不是顶部即可。我们可以对SYSMETS2作出一些修改以达到此点。当处理WM_CREATE消息时不设置滚动条范围,而是等到接收到WM_SIZE消息后再做此工作:
iVscrollMax = max (0, NUMLINES - cyClient / cyChar) ;
SetScrollRange (hwnd, SB_VERT, 0, iVscrollMax, TRUE) ;
假定NUMLINES等于75,并假定特定窗口大小是:50(cyChar除以cyClient)。换句话说,我们有75行信息但只有50行可以显示在显示区域中。使用上面的两行程序代码,把范围设置最小为0,最大为25。当滚动条位置等于0时,程序显示0到49行。当滚动条位置等于1时,程序显示1到50行;并且当滚动条位置等于25(最大值)时,程序显示25到74行。很明显需要对程序的其它部分做出修改,但这是可行的。
新滚动条函数的一个好的功能是当使用与滚动条范围一样大的页面时,它已经为您做掉了一大堆杂事。可以像下面的程序代码一样使用SCROLLINFO结构和SetScrollInfo:
si.cbSize = sizeof (SCROLLINFO) ;
si.cbMask = SIF_RANGE | SIF_PAGE ;
si.nMin = 0 ;
si.nMax = NUMLINES - 1 ;
si.nPage = cyClient / cyChar ;
SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ;
这样做之后,Windows会把最大的滚动条位置限制为si.nMax - si.nPage +1而不是si.nMax。像前面那样做出假设:NUMLINES 等于75 (所以si.nMax等于74),si.nPage等于50。这意味着最大的滚动条位置限制为74 - 50 + 1,即25。这正是我们想要的。
当页面大小与滚动条范围一样大时,会发生什么情况呢?在这个例子中,就是nPage等于75或更大的情况。Windows通常隐藏滚动条,因为它并不需要。如果不想隐藏滚动条,可在调用SetScrollInfo时使用SIF_DISABLENOSCROLL,Windows只是让那个滚动条不能被使用,而不隐藏它。
新SYSMETS
SYSMETS3-此章中最后的SYSMETS程序版本-显示在程序4-4中。此版本使用SetScrollInfo和GetScrollInfo函数,添加左右卷动的水平滚动条,并能更有效地重画显示区域。
/*------------------------------------------------------------------ SYSMETS3.C -- System Metrics Display Program No. 3 (c) Charles Petzold, 1998 ----------------------------------------------------------------*/ #include#include "sysmets.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("SysMets3") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("Program requires Windows NT!"),szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Get System Metrics No. 3"),WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth ; HDC hdc ; int i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd ; PAINTSTRUCT ps ; SCROLLINFO si ; TCHAR szBuffer[10] ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; // Save the width of the three columns iMaxWidth = 40 * cxChar + 22 * cxCaps ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; // Set vertical scroll bar range and page size si.cbSize = sizeof (si) ; si.fMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = NUMLINES - 1 ; si.nPage = cyClient / cyChar ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; // Set horizontal scroll bar range and page size si.cbSize = sizeof (si) ; si.fMask = SIF_RANGE | SIF_PAGE ; si.nMin = 0 ; si.nMax = 2 + iMaxWidth / cxChar ; si.nPage = cxClient / cxChar ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; return 0 ; case WM_VSCROLL: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB_VERT, &si) ; // Save the position for comparison later on iVertPos = si.nPos ; switch (LOWORD (wParam)) { case SB_TOP: si.nPos = si.nMin ; break ; case SB_BOTTOM: si.nPos = si.nMax ; break ; case SB_LINEUP: si.nPos - = 1 ; break ; case SB_LINEDOWN: si.nPos += 1 ; break ; case SB_PAGEUP: si.nPos -= si.nPage ; break ; case SB_PAGEDOWN: si.nPos += si.nPage ; break ; case SB_THUMBTRACK: si.nPos = si.nTrackPos ; break ; default: break ; } // Set the position and then retrieve it. Due to adjustments // by Windows it may not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; GetScrollInfo (hwnd, SB_VERT, &si) ; // If the position has changed, scroll the window and update it if (si.nPos != iVertPos) { ScrollWindow (hwnd, 0, cyChar * (iVertPos - si.nPos),NULL, NULL) ; UpdateWindow (hwnd) ; } return 0 ; case WM_HSCROLL: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; // Save the position for comparison later on GetScrollInfo (hwnd, SB_HORZ, &si) ; iHorzPos = si.nPos ; switch (LOWORD (wParam)) { case SB_LINELEFT: si.nPos -= 1 ; break ; case SB_LINERIGHT: si.nPos += 1 ; break ; case SB_PAGELEFT: si.nPos -= si.nPage ; break ; case SB_PAGERIGHT: si.nPos += si.nPage ; break ; case SB_THUMBPOSITION: si.nPos = si.nTrackPos ; break ; default : break ; } // Set the position and then retrieve it. Due to adjustments // by Windows it may not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; GetScrollInfo (hwnd, SB_HORZ, &si) ; // If the position has changed, scroll the window if (si.nPos != iHorzPos) { ScrollWindow (hwnd, cxChar * (iHorzPos - si.nPos), 0,NULL, NULL) ; } return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; // Get vertical scroll bar position si.cbSize = sizeof (si) ; si.fMask = SIF_POS ; GetScrollInfo (hwnd, SB_VERT, &si) ; iVertPos = si.nPos ; // Get horizontal scroll bar position GetScrollInfo (hwnd, SB_HORZ, &si) ; iHorzPos = si.nPos ; // Find painting limits iPaintBeg = max (0, iVertPos + ps.rcPaint.top / cyChar) ; iPaintEnd = min ( NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar) ; for (i = iPaintBeg ; i <= iPaintEnd ; i++) { x = cxChar * (1 - iHorzPos) ; y = cyChar * (i - iVertPos) ; TextOut (hdc, x, y,sysmetrics[i].szLabel,lstrlen (sysmetrics[i].szLabel)) ; TextOut (hdc, x + 22 * cxCaps, y,sysmetrics[i].szDesc,lstrlen (sysmetrics[i].szDesc)) ; SetTextAlign (hdc, TA_RIGHT | TA_TOP) ; TextOut (hdc, x + 22 * cxCaps + 40 * cxChar, y, szBuffer,wsprintf (szBuffer, TEXT ("%5d"), GetSystemMetrics (sysmetrics[i].iIndex))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
这个版本的程序依赖Windows保存滚动条信息并做边界检查。在WM_VSCROLL和WM_HSCROLL处理的开始,它取得所有的滚动条信息,根据通知码调整位置,然后调用SetScrollInfo设置其位置。程序然后调用GetScrollInfo。如果该位置超出了SetScrollInfo调用的范围,则由Windows来纠正该位置并且在GetScrollInfo调用中传回正确的值。
SYSMETS3使用ScrollWindow函数在窗口的显示区域中卷动信息而不是重画它。虽然该函数很复杂(在新版本的Windows中已被更复杂的ScrollWindowEx所替代),SYSMETS3仍以相当简单的方式使用它。函数的第二个参数给出了水平卷动显示区域的数值,第三个参数是垂直卷动显示区域的数值,单位都是像素。
ScrollWindow的最后两个参数设定为NULL,这指出了要卷动整个显示区域。Windows自动把显示区域中未被卷动操作覆盖的矩形设为无效。这会产生WM_PAINT消息。再也不需要InvalidateRect了。注意ScrollWindow不是GDI函数,因为它不需设备内容句柄。它是少数几个非GDI的Windows函数之一,它可以改变窗口的显示区域外观。很特殊但不方便,它是随滚动条函数一起记载在文件中。
WM_HSCROLL处理拦截SB_THUMBPOSITION通知码并忽略SB_THUMBTRACK。因而,如果使用者在水平滚动条上拖动卷动方块,在使用者释放鼠标按钮之前,程序不会水平卷动窗口的内容。
WM_VSCROLL的方法与之不同:程序拦截SB_THUMBTRACK消息并忽略SB_THUMBPOSITION。因而,程序随使用者在垂直滚动条上拖动卷动方块而垂直地滚动内容。这种想法很好,但应注意:一旦使用者发现程序会立即响应拖动的卷动方块,他们就会不断地来回拖动卷动方块。幸运的是现在的PC快得可以胜任这种严酷的测试。但是在较慢的机器上,可以考虑为GetSystemMetrics使用SB_SLOWMACHINE参数来替代这种处理。
加快WM_PAINT处理的一个方法由SYSMETS3展示:WM_PAINT处理程序确定无效区域中的文字行并仅仅重画这些行。当然,程序代码复杂一些,但速度很快。
不用鼠标怎么办
在Windows的早期,有大量的使用者不喜欢使用鼠标,而且,Windows自身也不要求必须有鼠标。虽然,没有鼠标的PC现在步入了单色显示器和点阵打印机的没落之路,但我仍然建议您编写可以使用键盘来产生与鼠标操作相同效果的程序,尤其对于像滚动条这样的基本操作对象更是如此。因为我们的键盘有一组光标移动键,所以可以实现同样的操作。
在下一章,您将学习使用键盘和在SYSMETS3中增加键盘接口的方法。您可能会注意到,SYSMETS3似乎在通知码等于SB_TOP和SB_BOTTOM时处理了WM_VSCROLL消息。前面已经提到过,窗口消息处理程序不从滚动条接收这些消息,所以,目前这是多余的程序代码。当我们在下一章再次回到这个程序时,您将会明白这样做的原因。
Windows 程序设计
March |
2013 |
||||||
Mon |
Tue |
Wed |
Thu |
Fri |
Sat |
Sun |
|
|
|
|
|
1 |
2 |
3 |
|
4 |
5 |
6 |
7 |
8 |
9 |
10 |
|
11 |
12 |
13 |
14 |
15 |
16 |
17 |
|
18 |
19 |
20 |
21 |
22 |
23 |
24 |
|
25 |
26 |
27 |
28 |
29 |
30 |
31 |