C++ Q&A 专栏...
原文出处:MSDN Magazine Feb 2004(C++ Q&A)
原代码下载:CQA0402.exe (127KB)

2001年7月 的专栏文章“怎样创建256色工具条”(编者注:中文译文参见在线杂志第11期)。然而我听说,除非你的IE浏览器为3.0或以后的版本,否则,Windows95
不支持256色工具条,在这种情况下,应用程序将使用一个默认的16位色工具条。我怎样能够判断一个256位色或更高颜色位的工具条是否被支持?
Bayland Park

只要获取窗口或屏幕设备上下文的句柄(HDC),然后调用 GetDeviceCaps (得到设备性能),就可以得到颜色位面(PLANS)和每个象素所含的比特位(BITSPIXEL),两者相乘就可以得到在某个象素中总的比特位数。
CWindowDC dc(NULL);
int nPlanes = dc.GetDeviceCaps(PLANES);
int nBitsPixel = dc.GetDeviceCaps(BITSPIXEL);
int totalBits = nPlanes * nBitsPixel;
通常,对于一个256色系统或者16位、24位或32位的真彩系统来说,其颜色位面是1,每个象素的比特位数是8。GetDeviceCaps
能够得到许多有用的信息,像逻辑英寸象素的个数、屏幕高宽比率、光栅性能和其它一些信息。我写过一个小程序ColorBits
(如 Figure 1 所示)。你
可从本文开始处的链接下载这个程序的源代码。

Figure 1 ColorBits

我见到过一些即可以作为控制台应用程序运行,也可以作为基于Windows的应用程序运行的程序。即:如果你在命令提示符键入程序的名字,它就会作为一个普通的基于
Windows 的应用程序运行;如果你输入一个命令行选项,如:“-batch”,它就会以批处理模式作为一个控制台应用程序运行,同时所有输出被定向到控制台。请问这个功能是怎么做的?
Joe Tadmann

我没法告诉你其它的应用程序是怎么这样做的,因为在 Window 中,总是有多种方法来给操作系统加壳,但是我可以向你展示实现这种功能的一种途径---通过 Visual
Studio.NET 来实现。也许你没有注意到,你可以在命令提示符键入 devenv 来启动 Visual Studio .NET,这时,Windows 启动
其图形用户界面。但是如果你键入 devenv 的同时再加上一个命令行开关,例如,-? 表示帮助,-bulid 表示编译并生成你的工程,它将以控制台模式运行,没有用户交互界面。例如:
devenv -build MyProject.sln
上面这行命令生成解决方案文件 MyProject.sln。
遗憾的是,自从GUI(图形用户界面)出现以后,太多的程序员已经忘了命令行的强大功能,这些强大的命令行功能使用户能以批处理模式从脚本中运行你的应用程序。你可以
确信微软的那帮家伙是不会在 Visual
Studio 中打开某个工程来生成他们的产品的!如果你写了一个确实能实现某些功能的程序,比如,把.wav文件转换成.mp3,或者通过计算预测股票,
这些功能适合用批处理接口来实现。如果你的程序没有人守着就不能成批压缩收藏的音乐乐曲或分析前一天的股票信息,那你的程序有什么好的呢?
好,现在言归正传,你怎么能够实现一个组合了用户界面/控制台的应用呢?现在几乎所有的 Windows 程序员都知道,Windows 把 EXE(可执行程序)分成两
大类:控制台应用和 GUI应用。这种体系结构可以追溯到 Windows 的早期,当时它首先是从 MS_DOS 中发展而来的。如今,你只要给链接
器一个开关:/subsystem:Windows 或者 /subsystem:console,你就能生成你想要的那种应用程序。
所以,如果你要建立一组合应用,首要问题是:它是一个控制台应用还是一个GUI应用?最初,你可能认为要建立一个控制台应用,以后它还能够作为一个 GUI
程序运行。从来没有谁说过一个控制台应用就不能够创建窗口或处理消息,控制台之所以是控制台,那是因为当控制台不存在时, Windows 会为控制台应用创建一个控制台。但是这里有一个问题,如果你在 Windows
下通过在其资源管理器(Explorer)中双击或快捷方式运行某个控制台应用程序,Windows 将会创建一个控制台,你可以通过调用 FreeConsole 来销毁
这个控制台,但是这个控制台窗口会暂时一闪而过,告诉整个世界你其实并不知道你自己做了什么。
然后,要使它为一个GUI应用。那么你如何把它写到控制台呢?有许多文章解释了怎样重新路由printf或cout到控制台,但是,
它们都涉及到创建新控制台窗口的问题,不是使用一个当程序是从命令行被启动时已经存在的窗口。即使有某个使用现有控制台的途径,那你又怎么知道你的应用程序是通过 Windows
Explorer 还是通过命令提示符调用的呢?
有一个新的函数( Windows XP 使用的)正好可以利用:这个函数是 AttachConsole。
它允许你将程序“绑定”到其它进程的控制台窗口,如果你用了专用的进程 ID:ATTACH_PARENT_CONSOLE,AttachConsole
将绑定到启动你的程序的控制台。太好了,但是有两个问题,第一,AttachConsole 只能在 Windows
XP 系统中使用,所以如果你想要你的程序运行在其它版本的 Windows 中,就没那么走运了;第二,AttachConsole
工作并不稳定,你可以写内容到控制台,但是你的程序退出后,命令提示符就乱七八糟了。
简而言之,基于 Windows 的应用程序要么是控制台应用,要么是GUI应用,二者不可兼得。(除非你想写你自己的启动代码,那已非我力所能及)。但是你知道它
是能够做到的,因为我已经告诉你 Visual
Studio 能行,到底怎样做呢?
如果你看一下 Visual Studio 的安装目录,你会发现实际上有两个程序:devenv.exe和 devenv.com。还记得.com 是什么吗?
它可不是 Web,而是可执行程序,在很久很久以前,当你还是小孩子的时候,基于 Windows 的程序有三种内存模式:大内存模式(large)、小内存
模式(small)和巨大内存模式(huge)。其它的模式都被叫做紧凑模式或微小模式,它们产生不同类型的可执行文件,这些可执行文件都以 .com 作为扩展名。(.com文件是一种在加载时不需要固定地址的直接内存映
象,这样使用起来非常的快,但它们必须很小。)
现在内存模式已经没有这么多了,大部分可执行文件都使用PE格式。但是命令解释器仍然能识别 .com 可执行文件,并且你可以将任何.exe 程序重新
更名为 .com 程序,如把 foo.exe 改成 foo.com,它仍然可以通过输入名字执行。所以可以用这个技巧去创建两个程序:foo.com 和 foo.exe。一个是
控制台应用程序,另一个是基于 Windows 应用程序。

Figure 2 在对话框里显示进程列表
为了示范它的工作原理,我修改了我在2002年7月专栏文章里的程序lp(列举进程)(编者注:中文译文参见在线杂志第14期文章——“如何获取某个进程的主窗口以及创建进程的程序名?”),它既可作为
GUI 程序运行,也可作为控制台程序运行。如果你输入 ListProc 而不用参数,它会在对话框中列出进程,如 Figure 2 所示。如果你键入 ListProc -c,它会以控制台模式运行并列出进程,如 Figure 3 所示。ListProc 有两个主程序文件:ListProc.cpp
是通常的 MFC 应用实现,ListProc-cons.cpp 是控制台应用实现。这两个程序都调用相同的模块—— EnumProc,实际的进程列表正是由它产生的。ListProc-cons
处理命令行并显示控制台信息,没有用命令行参数的程序通过调用 ShellExecute 启动程序的 GUI 版本。
// 将 myself.com 改为 myself.exe 并运行
TCHAR lpExeName[_MAX_FNAME];
GetModuleFileName(NULL, lpExeName, _MAX_FNAME);
LPTSTR ext = lpExeName + _tcslen(lpExeName) - 3;
_tcscpy(ext,_T("exe"));
ShellExecute(NULL, _T("open"),
lpExeName, NULL, NULL, SW_SHOWNORMAL);

Figure 3 在控制台的进程列表
Figure 4 是 ListProc-cons 的全部代码,Visual Studio 的解决方案包含两个工程:ListProc和
ListProc-cons。后者有一个定制编译步骤,是重命名输出文件 ListProc-cons.exe 为ListProc.com (参见
Figure 5)。当你安装程序时,要保证把 .com 和 .exe 都放在了相同的目录下,并且确保你创建的任何快捷方式都指向 .exe
文件。那样,从 Windows 调用会直接运行.exe 文件,而从控制台调用则运行 .com 文件(如果 .com 和 .exe
都存在于用户的路径下, Windows 首选 .com 文件运行)。明白了吗?

Figure 5 将 .exe 文件重命名为 .com

Forms 下创建一个基于 Windows 的应用。我正尝试记下窗口的位置,使它每次打开时都能记起它前一次的位置。在.NET
Framework 中有没有特殊的方法可用?我可以用配置文件吗?
Frank Jacobs

要的答案。配置文件是给管理员用来设置你的应用程序的,而不是给用户保存设置的。为此,你要么用注册表、ini文件,要么自己定制一个数据文件。注册表不是一个好的选择
,因为它难于编辑同时也不容易拷贝。.Net编程明确的目标之一就是 XCOPY 部署,也就是说你可以通过拷贝文件把你的应用直接从 A 处移动到 B 处。所以我建议你使用 INI 文件或者其它数据文件。
如果你要存储复杂的数据结构,像链表、哈希表或你自己私有的数据结构,你最好用序列化(二进制文件或XML文件都可以)来读写你专用的文件,如:MyApp.dat。在 .NET
Framework 中许多公共类已经可序列化了,并且只要你按照下面的格式声明你自己的类,也可能很容易地使你自己的类可序列化:
[Serializable]
public class MyClass {
public int myInt;
public String myStr;
}
对于保存象窗口位置这样简单的数据,INI文件是一个比较好的选择。它简单明了、可读性强、容易拷贝并且可以用任何 ASCII 文本编辑器轻松编辑。可是,在 .NET Framework 中还没有
现成的函数可以直接操作 INI 文件,所以我写了一个类 IniFile 对这些功能进行了封装,同时还写了一个测试程序 SavePos,它使用 IniFile
类来记录主窗口位置。SavePos 是一个典型的 Windows 窗体应用程序。由主框架类创建一个 IniFile 实例,就像下面这样:
using Ini;
public class Form1 : System.Windows.Forms.Form
{
private IniFile iniFile = new IniFile("SavePos.ini",true);
...
}
SavePos.ini 是一个文件名;稍后我将介绍构造函数的第二参数。在窗体被首次被创建,时,还没有显示的时候,Form 构造函数加载窗口位置
数据。如:
// in Form1 class
public Form1()
{
...
iniFile.RestoreWinPos(this,"MainWindow");
}
最后,当窗体被销毁时,Form1 保存窗口位置,代码如下:
// in Form1 class
protected override void Dispose( bool disposing )
{
if( disposing ) {
// save window pos
iniFile.SaveWinPos(this,"MainWindow");
...
}
下面所显示的代码是 INI 文件的结果:
[MainWindow]
X=282
Y=442
Width=417
Height=234
如你所见, IniFile 提供了两个函数,SaveWinPos 和 RestoreWinPos.表示保存/恢复窗口位置。Figure 6
是其实现代码,非常简单。保存/恢复函数调用了更多的原始函数,如 GetIntVal 和SetVal 来读/写键/值对。然后这些函数又用托管服务
(interop services )调用 Win32 API 函数 GetPrivateProfileInt 和 WritePrivateProfileString,
从而完成实际的对 INI 文件的读/写。唯一的技巧是当你恢复窗口位置时,如果你想要你的窗体(Form)关注它的位置(Form.Location)(关于这方面更多信息,
参见 2003年4月的专栏文章)。你必须记住用下面这的一行代码:
form.StartPosition = FormStartPosition.Manual;
机敏的读者会想起我还欠一个关于对 INI 文件构造函数第二个参数的解释。Win32 的Get/WriteProfileXxx 都有一个参数是一个 INI 文件名。如果你传一个相对文件名,
如“foo.ini”,Windows 便在 WINDOWS 目录下进行查找。如果你给一下绝对路径名,它便使用这个绝对路径。你可能要基于每个用户来保存窗口
的位置(Jans 应该得到她最后一次使用程序的窗口位置,而不 Fred 的),你也许想把 INI 文件放到用于你的程序的用户应用数据文件夹里。这就是第二个参数 useAppDataPath
的作用,如果你给它赋值为 true,IniFile 就会在用户应用数据文件夹里找 INI 文件。
IniFile 是怎么做到的呢?这个 Application 类做这这种事情很简单:Application.UserAppDataPath保存着用户数据文件夹的路径名。
这是一个其长无比的路径名,如:\Base\[CompanyName]\[ProductName]\[ProductVersion],其中 Base 就像C:\Documents and Settings\[username]\Application Data。
任何时候其版本管理都是免为其难的,
公共语言运行时添加了一个产品版本号到这个路径中,从你的视角看,它既是一个特性,同时也是一个灾难:如果你在版本号中使用星号(例如:1.0.*)以便让框架每次编译都创建一个新的版本号,很多程序员这么做,那么你会最终会有上百个文件夹——每次重编译或运行时都会产生一个新的文件夹。也许微软的那帮家伙打算从事磁盘生意,你可以从版本号中删除星号,或者是删除路径名中的版本部分,仰或将文件放到别处。和往常一样,你总是可以从本文顶部的链接处得到全部源代码。

在我2003年11月的专栏文章里,我谈到了怎样通过枚举窗口以到弹出菜单的HWND(窗口句柄)以及用特定的类名“#32768”查找窗口。读者 Jim White
提出下列技巧:如果你用NULL作为窗口句柄调用 GetMenuItemRect,Windows 会返回一个弹出式窗口的 HWND---神奇吧!!这也
恰好证明了即使像我这样的所谓专家每天也能学到许多东西。Jim 还指出: GetMenuItemRect 的这个技巧应用在“在 Windows
2000 和 Windows XP上很风光,但是在 Windows 98 上就歇菜了,尽管 MSDN上坚持说可以”。感谢 Jim。
使用 cppqa@microsoft.com 发送你的问题和评论给 Paul

作者简介:
Paul DiLascia 是一个自由作家,顾问和 Web/UI 方面资深的设计师。他是 Windows++: Writing Reusable
Windows Code in C++ (Addison-Wesley, 1992)一书的作者。你可以在
http://www.dilascia.com 网站和 Paul
联系上。
本文出自
February 2004 期刊,可通过当地
报摊获得,或者最好是 订阅
