【IT168 专稿】回想当年微软高调发布Windows Vista的时候,突出的兼容性问题成为其在推广时遇到的最大阻力,让Windows Vista“出师未捷身先死”,从而成为继Windows Me之后微软最失败的操作系统。有鉴于此,微软在进行Windows 7的开发的时候,将应用程序兼容性放在了前所未有的高度,提前两年就开始为Windows 7进行各种兼容性测试,同时在Windows 7上提供了Windows XP虚拟模式,最大程度地保证应用程序可以平滑地过渡到Windows 7,从这些我们都可以看出微软的良苦用心。
但是,操作系统的改变,必然会带来一些应用程序兼容性的问题,保持应用程序的良好兼容新,不仅仅是微软自家的事情,作为应用程序的开发者,我们也有不可推卸的责任。你的应用程序是否能够与Windows 7良好兼容?这是摆在每个程序员面前的一个问题。下面我们就以一系列文章,来介绍一下Windows 7有了什么新的变化?这些变化会带来那些应用程序兼容性问题?如何让应用程序与Windows 7保持兼容?
很多程序员在设计和实现应用程序的时候,为了图方便和省事,都有向应用程序所在的目录“Program Files”,Windows目录或者操作系统根目录(尤其是C:\)写入数据文件的习惯。另外,这些人也习惯于用注册表HKLM/Software下的键值来保存一些数据,比如应用程序的配置参数等等。应用程序一直都工作的很好,直到万恶的UAC的出现。在Windows 7中,这些人会发现他们所要创建的文件没有相应的位置被创建,注册表键值没有被修改。他们问“到底怎么回事?我的应用程序运行正常并没有报错,但是所创建的文件怎么就不见了呢?在其他操作系统上都工作的好好的啊?”
都是UAC Virtualization惹的祸
从Windows Vista开始,当然也包括Windows 7,因为UAC机制的引入,操作系统的标准普通用户被限制访问一些核心文件,文件夹和注册表键值。当我们开发的应用程序试图向这些地方写入数据的时候,会被重新定向到其他操作系统认为比较安全的位置。大多数时候,对于普通用户和应用程序开发人员来说这都是透明的,并没有给我们带来什么不便。但是在有些时候,事情并非如此。数据重新定向可能会导致一些奇怪的现象:
——在应用程序中,你写入数据到“Program Files”目录,虽然应用程序执行正确,没有错误值返回,但是在这个目录下你却找不到你刚刚写入的文件。
——你的应用程序修改了注册表键值,但是你在注册表相应的位置却看不到更新。
——当你关闭或者启用UAC后,你的应用程序找不到某些文件了,原来存在的文件凭空消失了。
在Windows Vista之前,我们通常都是以管理员身份来运行应用程序的。这样,应用程序就可以自由地对操作系统相关的目录或者注册表键值进行读写。当我们以普通用户身份运行这些应用程序时,就会出现这样或者那样无法访问的错误。Windows Vista为了减少这种错误,改善对于普通用户的应用程序的兼容性,同时又不失去其安全性,就利用UAC Virtualization(UAC 虚拟化访问)这种机制,将应用程序的写操作(包括文件和注册表操作)重新定向到了一个预先在用户的配置文件中定义的目录。UAC Virtualization分为文件Virtualization和注册表Virtualization。例如,当一个普通用户运行一个应用程序尝试写入数据到 C:\Program Files\Contoso\Settings.ini时,这个写入操作将被重新定向到C:\Users\Username\AppData\Local\VirtualStore\Program Files\Contoso\settings.ini。这就是为什么我们在写入的时候没有任何错误,但是在相应的目录下找不到我们创建的文件。同样的,对注册表HKLM\Software的写入操作也会被重新定向到HKCU\Software\Classes\VirtualStore。下图1展示了整个UAC Virtualization的流程。
图1 UAC Virtualization的流程
这里需要注意的是,UAC Virtualization仅作用于32位的应用程序对系统文件/目录、注册表的读写。64位程序、非交互式程序、模拟程序(Processes that impersonate)、内核调用者、Manifest中含有requestedExecutionLevel属性的可执行文件不包含在Virtualization的作用范围内。
如何获取正确的文件路径
UAC Virtualization (UAC虚拟化访问) 只是为了帮助现有的应用程序与Windows Vista或者Windows 7保持兼容,减少应用程序错误而设计的。为Windows 7而全新设计的应用程序,不应该再向一些敏感的系统目录或者注册表位置写入数据。同时也不应该借助虚拟化技术为一些不正确的应用程序行为提供补救方案,这无异于饮鸩止渴。当更多的应用程序移植到Windows 7之后,微软可能在未来版本的Windows取消对UAC虚拟存储的支持。例如,64位应用程序是禁用虚拟存储的。
在为Windows 7新开发应用程序时,我们应该始终开发与标准用户权限相适应的应用程序,而不要指望总是在管理员权限下运行你所设计的应用程序。同时,更多地在普通用户权限下测试你的应用程序,而不是在管理员权限下测试你的应用程序。
当我们那些在Windows 7之前设计的应用程序遇到UAC Virtualization问题的时候,我们需要从新设计我们的代码,将文件写入到合适的位置。在改善既有代码,使之可以与Windows 7兼容的时候,我们应该确保以下几点:
——在运行的时候,应用程序只会将数据保存到每个用户预先定义的位置或者是%alluserprofile% 中定义的普通用户拥有访问权限的位置。
——确定你要写入数据的“已知文件夹”(Knownfolders)。通常,所有用户共用的公共数据文件应该写入到一个全局的公共的位置,这样所有用户都可以访问到。而其它数据则应该写入每个用户自己的文件夹。
1 公共数据文件包括日志文件,配置文件(通常是INI或者XML文件),应用程序状态文件,比如保存的游戏进程等等。
2 而属于每个用户的文档,则应该保持在文档目录下,或者是用户自己指定的目录。
——当你确定合适的文件保存位置后,不要在代码中明文写出(Hard-code)你选择的路径。为了更好地保持兼容性,我们应该采用下面这些API来获得操作系统“已知文件夹(Knownfolders)”的正确路径。
1 C/C++非托管代码: 使用SHGetKnownFolderPath函数,通过指定“已知文件夹”的KNOWNFOLDERID作为参数来获得正确的文件夹路径。
- FOLDERID_ProgramData –所有用户都可以访问的应用程序数据适合放置在这个目录下。
- FOLDERID_LocalAppData – 每个用户单独访问的应用程序数据适合放置在这个目录下。
- FOLDERID_RoamingAppData – 每个用户单独访问的应用程序数据适合放置在这个目录下。 与上面一个目录不同的是,放置在这个目录下的文件会随着用户迁移,当一个用户在同一个域中的其他计算机登录的时候,这些文件会被复制到当前登录的机器上,就像用户随身携带的公文包一样。
下面这段代码演示了在非托管代码中如何调用SHGetKnownFolderPath函数获得正确的文件保存路径:
#include "shlwapi.h"
//…
#define AppFolderName _T("YourApp")
#define DataFileName _T("SomeFile.txt")
// 构造一个数据文件路径
// dataFilePath指向一个长度为MAX_PATH,类型为TCHAR的字符串数值
// hwndDlg是消息对话框的父窗口句柄
// 当有错误发生的时候用于显示错误提示
// includeFileName用于表示是否在路径后面扩展文件名
BOOL MakeDataFilePath(TCHAR *dataFilePath,
HWND hwndDlg, BOOL includeFileName)
{
// 初始化工作
memset(dataFilePath, 0, MAX_PATH * sizeof(TCHAR));
PWSTR pszPath = NULL;
// SHGetKnownFolderPath函数可以返回一个已知文件见的路径,
// 例如我的文档(My Documents),桌面(Desktop),
// 应用程序文件夹(Program Files)等等。
// 对于数据文件来说,FOLDERID_ProgramFiles并不是一个合适的位置
// 使用FOLDERID_ProgramFiles保存所有用户共享的数据文件
// 使用FOLDERID_LocalAppData保存属于每个用户自己的文件(non-roaming).
// 使用FOLDERID_RoamingAppData保存属于每个用户自己的文件(roaming).
// 对于“随身文件”(Roaming files),
// 当一个用户在一个域中的其他计算机登陆的时候,
// 这些文件会被复制到当前登录的机器上,就像用户随身携带的公文包一样
// 获取文件夹路径
if (FAILED(SHGetKnownFolderPath(FOLDERID_ProgramData,
0, NULL, &pszPath)))
// 错误的做法: if (FAILED(SHGetKnownFolderPath(FOLDERID_ProgramFiles,
// 0, NULL, &pszPath)))
{
// 提示错误
MessageBox(hwndDlg, _T("SHGetKnownFolderPath无法获取文件路径"),
_T("Error"), MB_OK | MB_ICONERROR);
return FALSE;
}
// 复制路径到目标变量
_tcscpy_s(dataFilePath, MAX_PATH, pszPath);
::CoTaskMemFree(pszPath);
//错误的做法: _tcscpy_s(dataFilePath, MAX_PATH, _T("C:\\"));
// 在路径后面扩展应用程序所在文件夹
if (!::PathAppend(dataFilePath, AppFolderName))
{
// 提示错误
MessageBox(hwndDlg, _T("PathAppend无法扩展路径"),
_T("Error"), MB_OK | MB_ICONERROR);
return FALSE;
}
// 是否添加文件名
if (includeFileName)
{
// 在路径后扩展文件名
if (!::PathAppend(dataFilePath, DataFileName))
{
// 提示错误
MessageBox(hwndDlg, _T("PathAppend无法扩展文件名"),
_T("Error"), MB_OK | MB_ICONERROR);
return FALSE;
}
}
return TRUE;
}
2 托管代码: 使用System.Environment.GetFolderPath函数,通过指定我们想要获取的“已知文件夹”为参数,从而获取相应的文件夹的正确路径。
- Environment.SpecialFolder.CommonApplicationData – 所有用户都可以访问的应用程序数据适合放置在这个目录下。
- Environment.SpecialFolder.LocalApplicationData – 每个用户单独访问的应用程序数据适合放置在这个目录下。
- Environment.SpecialFolder.ApplicationData – 每个用户单独访问的应用程序数据适合放置在这个目录下。这是“随身文件夹”。
下面这段代码展示了如何在托管代码中获取正确的文件路径:
{
private const string AppFolderName = "YourApp";
private const string DataFileName = "SomeFile.txt";
private static string _dataFilePath;
/// <summary>
/// 构建路径
/// </summary>
static FileIO()
{
// Environment.GetFolderPath返回一个“已知文件夹”的路径
// Path.Combine可以合并两个路径成一个合法的路径
// …
_dataFilePath = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.ProgramFiles), AppFolderName);
//错误的做法:
//_dataFilePath = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.CommonApplicationData), AppFolderName);
// 扩展文件名
_dataFilePath = Path.Combine(_dataFilePath, DataFileName);
}
public static void Save(string text)
{
// 检查要保存的字符串是否为空
if (String.IsNullOrEmpty(text))
{
MessageBox.Show("字符串为空,无法保持.", "空字符串",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
try
{
// 获取文件保存的路径
string dirPath = Path.GetDirectoryName(_dataFilePath);
// 检查文件夹是否存在
if (!Directory.Exists(dirPath))
Directory.CreateDirectory(dirPath); // 创建文件夹
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "文件夹创建失败",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
try
{
// 保存字符串到文件
StreamWriter sw = new StreamWriter(_dataFilePath);
try
{
sw.Write(text);
}
finally
{
// 关闭文件
sw.Close();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "文件写入失败",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
// …
}
}
如果上面的方法都不适合你,你还可以使用环境变量获取相应的文件夹路径:
- %ALLUSERSPROFILE% – 所有用户都可以访问的应用程序数据适合放置在这个目录下。
- %LOCALAPPDATA% – 每个用户单独访问的应用程序数据适合放置在这个目录下。 - (Windows Vista 或者Windows 7)
- %APPDATA% – 每个用户单独访问的应用程序数据适合放置在这个目录下。这是“随身文件夹”。- (Windows Vista 或者Windows 7)
禁用UAC Virtualization
凡事都没有绝对。如果因为一些特殊的要求(众所周知,客户的要求千奇百怪,无奇不有),我们一定要向“Program Files”目录写入数据,这时该怎么办呢?面对这种极其特殊的情况,我们可以在应用程序的Manifest禁用UAC Virtualization,取消其对数据写操作的重定向。在项目属性中,我们设置启用UAC(Enable User Account Control),并且在UAC Execution Level中设置请求管理员权限。这样,应用程序在启动的时候,就会向用户请求管理员权限,当应用程序获得管理员执行权限后,当然可以向任意目录写入数据,UAC Virtualization也就不会起作用了。
图2 通过Manifest禁用UAC Virtualization
对于64位应用程序,本身是不具备UAC Virtualization机制的,所以根本不存在禁用的问题。当我们在64位应用程序中尝试向“Program Files”等敏感目录写入数据时,就会遇到一个“拒绝访问”的错误:
BOOL IsDirectoryExists(TCHAR *dirName)
{
WIN32_FILE_ATTRIBUTE_DATA dataDirAttrData;
if (!::GetFileAttributesEx(dirName, GetFileExInfoStandard, &dataDirAttrData))
{
DWORD lastError = ::GetLastError();
if (lastError == ERROR_PATH_NOT_FOUND || lastError == ERROR_FILE_NOT_FOUND || lastError == ERROR_NOT_READY)
return FALSE;
}
return (dataDirAttrData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
}
// …
// 获取文件夹路径
//if (FAILED(SHGetKnownFolderPath(FOLDERID_ProgramData,
// 0, NULL, &pszPath)))
// 错误的做法:
if (FAILED(SHGetKnownFolderPath(FOLDERID_ProgramFiles,
0, NULL, &pszPath)))
{
// 提示错误
MessageBox(hwndDlg, _T("SHGetKnownFolderPath无法获取文件路径"),
_T("Error"), MB_OK | MB_ICONERROR);
return FALSE;
}
//…
// 检查文件夹是否存在
if (::IsDirectoryExists(dataFilePath))
{
// 如果文件夹不存在,则创建文件夹
if (!::CreateDirectory(dataFilePath, NULL))
{
DWORD dwErrorCode = ::GetLastError();
LPCWSTR lpBuffer;
// 获取错误信息
FormatMessage ( FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_IGNORE_INSERTS |
FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
dwErrorCode, // 错误代码
LANG_NEUTRAL,
(LPTSTR)&lpBuffer,
0 ,
NULL );
// 显示错误对话框
MessageBox(hwndDlg, lpBuffer, _T("创建文件夹错误"), MB_OK | MB_ICONERROR);
LocalFree((HLOCAL)lpBuffer);
return FALSE;
}
}
当这段代码执行到创建文件夹的时候,会遇到一个“拒绝访问”错误:
图3 创建文件夹的“拒绝访问”错误
为了避免这个错误,同样的,我们可以通过在项目属性中设置,使得Manifest中嵌入UAC相关的信息,在应用程序启动的时候请求管理员权限,就像我们在运行其他大多数需要管理器权限的应用程序一样。当应用程序获得管理员权限后,这个错误就不存在了。但是这里必须要指出,这种做法是不太安全的,能够避免尽量避免。
系列文章索引: