65.9K
CodeProject 正在变化。 阅读更多。
Home

经典 Shell

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (121投票s)

2009年11月29日

MIT

33分钟阅读

viewsIcon

1092837

downloadIcon

10065

适用于 Windows 7 和 Vista 的经典开始菜单和其他 Shell 功能。

最新版本请访问 SourceForge 上的 Classic Shell 项目

引言

ClassicShell

Classic Shell 是一系列在旧版 Windows 中可用但在新版本中已消失的功能。它恢复了 Windows 7 不支持的经典开始菜单,为 Windows 资源管理器添加了一个工具栏,将 Vista 和 Windows 7 中的复制界面替换为 Windows XP 的经典界面,并增加了一些其他小型功能。

经典开始菜单

Classic Start Menu 是原始开始菜单的克隆,您可以在从 Windows 95 到 Vista 的所有 Windows 版本中找到它。它具有各种高级功能

  • 支持拖放以整理您的应用程序。
  • 选项以显示“收藏夹”,展开“控制面板”等。
  • 显示最近使用的文档。要显示的文档数量是可自定义的。
  • 已翻译成 35 种语言,包括阿拉伯语和希伯来语的从右到左支持。
  • 不会禁用 Windows 中的原始开始菜单。您可以通过 Shift+单击开始按钮来访问它。
  • 右键单击菜单中的项目以删除、重命名、排序或执行其他任务。
  • 支持 32 位和 64 位操作系统。
  • 支持皮肤,包括额外的第三方皮肤。
  • 外观和功能均可完全自定义。
  • 支持 Microsoft 的 Active Accessibility
  • 最重要的是——它是免费的!

如果您在旧版 Windows 中使用过开始菜单,您会感到宾至如归

Classic Start Menu

经典资源管理器

Classic Explorer 是 Windows Explorer 的一个插件,它

  • 为 Explorer 添加了一个工具栏,用于一些常用操作(转到父文件夹、剪切、复制、粘贴、删除、属性、电子邮件)。可以手动添加更多按钮。
  • 将 Vista 和 Windows 7 中的复制界面替换为更用户友好、类似于 Windows XP 的版本。
  • 处理 Windows Explorer 文件夹窗格中的 Alt+Enter,并显示所选文件夹的属性。
  • 提供了自定义文件夹窗格的选项,使其外观更像 Windows XP 版本或不淡出展开按钮。
  • 可以在状态栏中显示可用磁盘空间和文件总大小。

Windows Explorer 的工具栏

Vista 中的 Windows Explorer 没有像 Windows XP 那样的工具栏。如果您想转到父文件夹,则必须使用面包屑导航栏。如果您想用鼠标复制或删除文件,则必须右键单击并查找“删除”命令。随着您安装的 Shell 扩展越来越多,右键菜单变得越来越大,找到正确的命令可能需要一段时间。

为了解决这个问题,Classic Explorer 插件添加了一个新的工具栏

Explorer Toolbar

按住 Control 键单击“向上”按钮,将在新的 Explorer 窗口中打开父文件夹。

按住 Shift 键单击“删除”按钮可永久删除文件。

附加的向上按钮

有些人问我是否可以做一个小的向上按钮,并将其放在 Explorer 标题栏的“后退/前进”按钮旁边。如果向上是您从工具栏中需要的唯一按钮,这将节省您的屏幕空间

Up button in the title bar

右键单击按钮以调出 Classic Explorer 设置。

新的复制界面

在 Vista 中,当您复制文件并出现冲突时,会显示以下内容

Copy in Vista

有什么问题?

首先,它会占据半个屏幕的文本,您需要阅读。此外,它并不清楚哪些部分是可点击的。您必须移动鼠标才能发现界面,就像在 Lucas Arts 冒险游戏中一样。最后,键盘可用性非常糟糕。要告诉它“是的,我知道我在做什么,我想覆盖所有文件”,您必须按Alt+D,上,上,上,空格!这比在 Street Fighter 3 中执行 Akuma Kara Demon move 还要难。这些东西有其时间和地点,而复制文件则不是。

Classic Explorer 插件恢复了 Windows XP 中更简单的对话框

Copy in XP

可以立即清楚哪些是可点击的(提示——底部按钮),有简单的键盘导航(按Y代表“是”,A代表复制所有文件),您仍然可以看到哪个文件较新,哪个文件较大。当然,就像在 Windows XP 中一样,按住Shift并单击“否”按钮意味着“全部否”(或者只需按Shift+N)。

如果您单击“更多…”,您将获得 Windows 的原始对话框。从那里,您将看到所有详细信息,并且会有一个额外的选项“复制,但保留两个文件”。

重要提示:仅替换了界面。执行实际复制操作的底层系统不受影响。

文件夹窗格中的 Alt+Enter

Alt+Enter 是 Windows 中用于显示选定内容属性的通用快捷方式。但在 Vista 和 Windows 7 中,它在显示文件夹的左窗格中不起作用。在右侧文件所在的位置,它工作正常。与 Windows XP 相比,这已损坏,因为在 Windows XP 中 Alt+Enter 在两个位置都有效。

为了解决这个问题,Classic Explorer 插件会检测到您按下 Alt+Enter,并显示当前所选文件夹的属性。

状态栏

在 Windows 7 中,Explorer 的状态栏不显示可用磁盘空间和所选文件的大小。Classic Explorer 解决了这个问题

File size in status bar

当没有选择文件时,将显示文件夹中所有文件的总大小。

其他开始菜单实现

在我决定开发自己的开始菜单之前,我尝试寻找替代方案。我找不到免费的、支持程序重新排序、显示最近文档等的东西。不列出“竞争对手”及其(希望是客观的)优缺点,这篇文章将不完整

CSMenuhttp://www.csmenu.com/

CSMenu 是免费的,并提供基本功能——打开程序菜单,单击程序以运行。它缺少键盘导航、拖放、最近文档、自定义等高级功能。此外,本地化也不太正确——例如,“帮助与支持”、“计算器”等未本地化,并且不支持从右到左的语言。

更新:看来该项目已停止开发,并已被作者放弃。

Classic Windows Start Menu – http://usuarios.lycos.es/coreaffinity/classicwinstartmenu.htm

Classic Windows Start Menu 也是免费的,并且只有基本功能。没有拖放,支持的语言很少,并且在任务栏不在底部时工作不正常。

更新:最新的 beta 版本支持拖放,并且更好地处理了任务栏的不同位置。仍有改进空间(拖放有点错误,缺少 Unicode 支持),但看起来该项目是活跃的,并且正在努力修复现有问题。

Classic Start Menu – http://www.classicstartmenu.com/index.html

Classic Start Menu 虽然不是免费的(20 美元),但具有多种高级功能。您可以使用拖放来重新排列菜单(我发现它有点错误),并且它有两个皮肤可供选择(Aero 和 Classic)。负面影响是几乎没有键盘导航,因为有一个搜索框会窃取所有输入的字符。有一种使用数字键的快捷方式系统,但我无法可靠地使其工作。没有真正的“最近文档”菜单。此外,本地化有点不对,并且对从右到左的支持有点不足。

Vista Start Menu – http://www.vistastartmenu.com/index.html

Vista Start Menu 是由上面 Classic Start Menu 的同一个人开发的另一个版本。它试图做得更花哨,但 UI 对我来说太拥挤了。我快速看了一下,发现键盘快捷方式系统工作得更可靠。有一个免费版本,还有一个 PRO 版本(20 美元),增加了更多自定义功能。如果您喜欢这种 UI,我会给这款打最高分,因为它具有功能。

Seven Classic Start – http://www.sevenclassicstart.com/

Seven Classic Start 可能是中最差的。它是最贵的(25 美元!),只提供基本功能。尽管它被宣传为“具备了所有让原始开始菜单受到如此多用户喜爱的功能”,但没有拖放、展开控制面板、最近文档、本地化或从右到左的支持。

附注:当您尝试卸载试用版时,会有一个提议,如果您同意试用其他软件,就可以免费使用它。

如果您不是程序员,现在可以停止阅读了。只需下载二进制文件并安装它们。

文章的其余部分讨论了各种功能的实现方式。

我会尽量简短,避免重复其他 CodeProject 文章或 MSDN 中已有的信息。

Classic Start Menu 的工作原理

那么,我们如何创建一个开始菜单替换项呢?让我们从头开始。

实现菜单

与原始开始菜单一样,我们的实现使用垂直工具栏控件。这给我们带来了一些比普通菜单优势

  • 工具栏直接支持图像,无需处理所有者绘制菜单
  • 如果项目在屏幕上放不下,工具栏可以放置在分页控件中使其可滚动
  • 您可以右键单击工具栏,但不能右键单击菜单
  • 工具栏提供了一些稍后会派上用场的拖放功能

当然,也有缺点。我们需要自己模拟菜单行为的某些部分。我们需要处理鼠标悬停在项目上时打开子菜单、处理焦点、激活和 Z 顺序问题等。

我还没有解决的工具栏控件有一些问题

  • 工具栏与 Aero 不兼容。当它绘制自身时,会弄乱 Alpha 通道。这使得创建透明菜单成为不可能。
  • 在从右到左模式下使用时,工具栏无法正确处理 WM_PRINTCLIENT 消息。这使得 AnimateWindow 等操作变得困难。
  • 在一个窗口中有多个工具栏来模拟多列菜单会使 JAWS 等屏幕阅读器感到困惑。

由于这些问题,我正在认真考虑在 Classic Shell 的下一个版本中创建自己的控件。

替换标准菜单

我们需要解决的下一个问题是如何显示我们的菜单而不是内置的标准菜单。用户有两种方式激活菜单——按 Win 键,以及单击开始按钮(球体)。在用 Spy++ 侦察一番后,您会注意到一些事情

  • 任务栏是一个类为 Shell_TrayWnd 且没有名称的窗口。
  • 开始按钮是与任务栏在同一线程中的窗口。按钮的文本是本地化的,取决于当前的操作系统语言。
  • 当您单击开始按钮时,它会像任何其他窗口一样接收 WM_LBUTTONDOWN
  • 当您单击开始按钮周围的区域时,任务栏会接收 WM_NCLBUTTONDOWN 消息。
  • 还有一个名为 Program Manager 且类为 Progman 的窗口。它与任务栏在同一个进程中,但位于另一个线程。
  • 当您按 Win 按钮时,Progman 窗口会接收消息 WM_SYSCOMMAND,其中 wParam = SC_TASKLIST

所以任务很简单——为开始按钮和 Progman 窗口的线程安装 WH_GETMESSAGE 挂钩。拦截激活开始菜单的消息,并显示我们的开始菜单。

HWND g_StartButton; // the start button window
HWND g_Taskbar; // the taskbar window
UINT g_StartMenuMsg;
// a private message posted 
// when the Win key is pressed

void ToggleStartMenu(); // function that opens/closes our start menu
STARTMENUAPI LRESULT CALLBACK HookProgMan( int code, 
                     WPARAM wParam, LPARAM lParam )
{
   if (code==HC_ACTION) {
      MSG *msg=(MSG*)lParam;
      if (msg->message==WM_SYSCOMMAND && 
               (msg->wParam&0xFFF0)==SC_TASKLIST) {
         PostMessage(g_StartButton,g_StartMenuMsg,0,0);
         msg->message=WM_NULL;
         // stop the window from processing the message
      }
  }
  return CallNextHookEx(NULL,code,wParam,lParam);
}
 
STARTMENUAPI LRESULT CALLBACK HookStartButton( int code, 
                     WPARAM wParam, LPARAM lParam )
{
   if (code==HC_ACTION && !g_bInMenu) {
      MSG *msg=(MSG*)lParam;
      if (msg->message==g_StartMenuMsg && msg->hwnd==g_StartButton) {
         // activated by keyboard
         ToggleStartMenu();
         msg->message=WM_NULL;
      }
      if (msg->message==WM_LBUTTONDOWN && msg->hwnd==g_StartButton) {
         // activated by mouse
         ToggleStartMenu();
         msg->message=WM_NULL;
      }
      if (msg->message==WM_NCLBUTTONDOWN && msg->hwnd==g_Taskbar) {
         // activated by mouse
         ToggleStartMenu();
         msg->message=WM_NULL;
      }
   }
   return CallNextHookEx(NULL,code,wParam,lParam);
}

挂钩可以由 Explorer 自动加载的 Shell 扩展或外部 EXE 安装。我选择了一个外部 EXE,有几个原因。首先,它更简单,因为它不需要注册 Shell 扩展及其所有麻烦。其次,当 EXE 被杀死时,它会自动清理挂钩,Explorer 会恢复到其原始状态。最后,当 Explorer 重新启动时,EXE 会收到 TaskbarCreated 消息,它可以重新安装挂钩。

当然,还有更多细节需要考虑。例如,当鼠标悬停在开始按钮上时,会弹出一个工具提示,文本为“开始”。我们不希望在打开我们的开始菜单时显示此文本。因此,当菜单可见时,暂时禁用工具提示。

下一个要注意的是拖放。当用户拖动程序添加到开始菜单时,他们会悬停在开始按钮上并期望菜单打开。支持此功能的一种方法是替换与开始按钮关联的 IDropTarget 对象。我们从开始按钮窗口的 OleDropTargetInterface 属性中获取旧的拖放目标,设置一个新的拖放目标,并在准备取消 Explorer 挂钩时,恢复原始的拖放目标

CComPtr<IDropTarget> g_pOriginalTarget;

// hook
g_pOriginalTarget=(IDropTarget*)GetProp(
        g_StartButton,L"OleDropTargetInterface");
if (g_pOriginalTarget)
   RevokeDragDrop(g_StartButton);
CStartMenuTarget *pNewTarget=new CStartMenuTarget();
RegisterDragDrop(g_StartButton,pNewTarget);
pNewTarget->Release();
 
// unhook
if (g_pOriginalTarget)
{
   RevokeDragDrop(g_StartButton);
   RegisterDragDrop(g_StartButton,g_pOriginalTarget);
   g_pOriginalTarget=NULL;
}

图标

开始菜单需要为所有 Shell 项以及“运行”、“关机”等命令项显示图标。

对于 Shell 项,我们可以使用 IExtractIcon 接口获取图标。首先调用 IExtractIcon::GetIconLocation,然后将收到的位置传递给 IExtractIcon::Extract。如果 Extract 返回 S_FALSE,您还需要调用 ExtractIconEx 函数。

// Retrieves an icon from a shell folder and child ID
int CIconManager::GetIcon( IShellFolder *pFolder, 
                           PITEMID_CHILD item, bool bLarge )
{
   // get the IExtractIcon object
   CComPtr<IExtractIcon> pExtract;
   HRESULT hr=pFolder->GetUIObjectOf(NULL,1,&item,
                       IID_IExtractIcon,NULL,(void**)&pExtract);
   if (FAILED(hr))
      return 0;
 
   // get the icon location
   wchar_t location[_MAX_PATH];
   int index=0;
   UINT flags=0;
 
   hr=pExtract->GetIconLocation(0,location,_countof(location),&index,&flags);
   if (hr!=S_OK)>      return 0;
 
   // extract the icon
   HICON hIcon;
   hr=pExtract->Extract(location,index,bLarge?&hIcon:NULL,bLarge? 
                        NULL:&hIcon, MAKELONG(LARGE_ICON_SIZE,SMALL_ICON_SIZE));
   if (hr==S_FALSE) {
     // the IExtractIcon object didn't do anything - use ExtractIconEx instead
     if (ExtractIconEx(location,index,bLarge?&hIcon:NULL,bLarge?NULL:&hIcon,1)==1)
         hr=S_OK;
   }
 
   // add to the image list
   index=0;
   if (hr==S_OK) {
      index=ImageList_AddIcon(bLarge?m_LargeIcons:m_SmallIcons,hIcon);
      DestroyIcon(hIcon);
   }
 
   return index;
}

提取图标可能很昂贵,因为包含的 exe 或 DLL 需要先加载到内存中。控制面板是最大的罪魁祸首,因为它包含一个长列表项,每个项都在自己的文件中,并且所有这些项都需要同时加载。

对于命令项,我们可以从 shell32.dll 中提取图标。它已经加载到 Explorer 进程中

Programs

Programs 是 # 326

Settings

Settings 是 # 330

Run

Run 是 # 328

这有点不正常,因为图标资源在不同 Windows 版本之间肯定会发生变化。我已验证在 Vista 和 Windows 7 中,我们需要的图标具有相同的资源 ID。我们来看看下一版 Windows(或下一个 Service Pack)是否会破坏它。访问 Shell 图标的文档方法是使用 SHGetStockIconInfo 函数。不幸的是,它并没有提供我们所需的所有图标。此外,像 SIID_STFINDSIID_STRUN 这样的有趣图标在文档中被标记为“不要使用”。在最新的在线文档中,它们甚至没有列出!因此,目前,使用资源查看器查看 shell32.dll 似乎是唯一可行的解决方案(除非绘制自己的图标)。

多个项目可能共享同一个图标——例如,所有文本文件都使用同一个文本文件图标。为了重用图标,开始菜单有一个全局缓存 CIconManager,它将每个图标与一个键值关联,该键值是图标位置和索引的哈希值。为了提高性能,图标管理器启动一个后台线程,该线程会爬取 Shell 并预缓存图标。因此,当您需要打开控制面板时,所有图标应该都已加载。

程序菜单

程序菜单是两个文件夹的组合——一个用于当前用户,一个由所有用户共享。开始菜单应合并两个文件夹树并将其显示为一棵树。名称相同的项目应合并为一个项目。

比较项目内部名称(通过 GetDisplayNameOf(SHGDN_INFOLDER|SHGDN_FORPARSING) 返回)很重要,但在 UI 中显示正常显示名称 GetDisplayNameOf(SHGDN_INFOLDER|SHGDN_NORMAL)。某些项目可能具有相同的显示名称,但应被视为单独的项目——例如,foo.exefoo.exe.lnk。反之亦然——某些项目可能具有相同的内部名称但不同的显示名称——例如,用户 Startup 文件夹的显示名称将被翻译成当前语言,但通用 Startup 文件夹则不会。尽管它们的显示名称不同,但两个文件夹应合并。

为什么不使用 IShellMenu?

Shell 支持为给定的 Shell 文件夹显示菜单。您创建一个 IShellMenu 实例,为其提供 IShellFolder,它工作得相当好。图标正确显示,拖放工作正常,一切都很顺利,每个人都很高兴。并非如此。

有一些缺点,即使尝试了一周,我也找不到解决方法。首先,我们想显示两个 Shell 文件夹的组合——一个用于用户程序,一个用于公共程序。我创建了一个虚拟 IShellFolder,它将两个文件夹呈现为一个。我不得不使用我自己的私有 PIDL 结构,而 IShellMenu 并不太喜欢。它假设它获得的 PIDL 与 Shell 文件系统兼容。另一个问题是,开始菜单不仅仅是程序菜单。它有最近的文档、系统命令、分隔符等。将这些内容塞进一个 IShellFolder 层级结构被证明是一项不可能完成的任务。

因此,我放弃了 IShellMenu,并决定从头开始实现菜单控件。从好的方面来说,由于我们自己实现了菜单,因此调整外观和感觉的可能性是无限的。

最近文档

Windows 将最近访问的文档的链接存储在 %APPDATA%\Microsoft\Windows\Recent 文件夹中。并非文件夹中的所有链接都指向文档。有些指向最近访问的文件夹。所以,我们需要过滤掉它们。如何区分文件链接和文件夹链接?获取一个 IShellLink 对象,将其转换为 IPersistFile,使用 IPersistFile::Load 加载链接的内容,使用 IShellLink::GetPath 获取目标路径和属性,并检查它是一个文件还是一个文件夹。

此外,可能有很多项目,使用 IShellFolder API 枚举它们可能会非常慢。在我的例子中,大约需要 5-8 秒。我发现使用 FindNextFile API 遍历它们,按时间排序,然后选择前 15 个文档要快得多。

系统命令

除了程序菜单,开始菜单还包含许多执行特定命令的项目。我们应该尝试实现尽可能多的项目。这是当前列表

命令 实现
关机 IShellDispatch::ShutdownWindows()
注销 ExitWindowsEx(EWX_LOGOFF,0)
卸载 IShellDispatch::EjectPC()
Run IShellDispatch::FileRun()
帮助与支持 IShellDispatch::Help()
任务栏属性 IShellDispatch::TrayProperties()
查找文件和文件夹 IShellDispatch::FindFiles()(或执行 Command.SearchFile 设置)
查找打印机 IShellDispatch2::FindPrinter(CComBSTR(L””), CComBSTR(L””), CComBSTR(L””))
查找计算机 IShellDispatch::FindComputer()
查找人员 这个有点棘手。它的实现是Windows Mail的一部分。命令行是 %ProgramFiles%\Windows Mail\wab.exe /find

拖放

这是开始菜单最实用的功能之一。它允许用户重新排列已安装的程序并使其更易于查找。

如何做到?当用户拖动菜单项时,工具栏会发送 TBN_DRAGOUT 通知。我们必须创建一个数据对象和一个拖放源,并运行 SHDoDragDrop 函数。我们可以从 IShellFolder::GetUIObjectOf(IID_IDataObject) 获取数据对象。拖放源没有什么特别之处。

为了启用项目拖放到菜单上,我们使用 TBSTYLE_REGISTERDROP 样式。这会导致工具栏发送 TBN_GETOBJECT 通知,以便从我们这里请求 IDropTarget 接口并在拖放期间使用它。我们的 IDropTarget 必须处理诸如

  • 显示项目可以放置的插入标记(使用 TB_SETINSERTMARK 消息)。
  • 检测鼠标悬停在子菜单上并延迟打开它。
  • 当项目被拖放到同一个菜单时重新排序菜单。
  • 如果项目被拖放到不同的菜单,则移动/复制该项目。为此,我们可以从目标文件夹获取 IDropTarget,并手动调用 IDropTarget::DragOverIDropTarget::Drop 来执行操作。

通常,拖放操作是异步的,并在后台线程中执行。这对我们来说不好,因为我们想立即知道项目何时被移动,以便更新菜单。最简单的解决方案是禁用异步操作(这就是 Windows 自带的开始菜单所做的)

CComQIPtr<IAsyncOperation> pAsync=pDataObj;
if (pAsync)
    pAsync->SetAsyncMode(FALSE);

这不是大问题,因为开始菜单主要是快捷方式,它们复制起来很快。我可以想到两种替代解决方案。理想情况下,我们可以将 IAdviseSink 添加到数据对象中,并在拖放操作完成时收到通知。不幸的是,Shell 数据对象不支持此类通知(IDataObject::DAdvise 返回 OLE_E_ADVISENOTSUPPORTED)。另一种方法是安装一个目录监视器,使用 FindFirstChangeNotification。这会起作用,但对于这么小的优势来说过于复杂了。

上下文菜单

标准开始菜单还有另一个值得支持的功能。用户可以右键单击项目,获取其 Shell 上下文菜单,然后单击命令。正确托管上下文菜单相当棘手;幸运的是,Raymond Chen 在他的博客上对此进行了详细介绍:The Old New Thing: How to host an IContextMenu

当然,还有工作要做,因为我们需要一些自定义行为。我们需要对“重命名”、“删除”和“链接”命令进行特殊处理。对于“重命名”,我们将显示自己的重命名对话框,因为默认实现什么都不做。对于“删除”和“链接”,我们希望在操作完成后刷新菜单。

皮肤

Classic Shell 的 0.9.8 版本支持开始菜单的皮肤。皮肤决定了主菜单的背景图像以及字体大小、颜色、发光等设置。

每个皮肤都是一个资源 DLL,其中包含皮肤描述(文本文件)和用于构建菜单图像的位图。在此处阅读有关皮肤的更多信息:如何为 Classic Start Menu 制作皮肤

Accessibility

由于我们使用的是工具栏而不是菜单,因此像Narrator这样的屏幕阅读器会说“带有 11 个按钮的工具栏”,而不是“带有 11 个项目的菜单”。为了解决这个问题,我们必须创建自己的 IAccessible 实现,并为单独的可访问组件实现 get_accRole 以返回 ROLE_SYSTEM_MENUPOPUPROLE_SYSTEM_MENUITEMROLE_SYSTEM_SEPARATOR

有关完整的实现,请查看 Accessibility.cpp 文件。

Classic Explorer 的工作原理

Classic Explorer 是一个单一的 DLL,它注册为三个不同的 Shell 扩展——一个拖放处理程序、一个浏览器帮助对象和一个桌面栏。拖放处理程序用于复制 UI,浏览器帮助对象挂钩到文件夹树视图,桌面栏向 Explorer 添加一个工具栏。

Classic Copy

Classic Copy 替换了 Explorer 在复制/移动操作期间出现冲突时显示的对话框。它使用拖放处理程序来确保在复制操作进行时加载 DLL。加载后,DLL 会将 WH_CBT 挂钩安装到进程的所有线程中,并等待一个具有特定标题的对话框被创建。

当 Explorer 创建一个标题为“复制文件”或“移动文件”的对话框时,我们会隐藏它,以免它有机会显示,然后显示我们自己的对话框。在用户选择(是、否、取消等)后,我们必须将该选择传达给原始对话框。

原始对话框不是一个普通的窗口,而是任务对话框。这使得它很难以编程方式控制,因为它的按钮不是普通的控件 ID,而是使用直接绘制在对话框表面的无窗口按钮。我发现控制任务对话框的唯一方法是通过 Active Accessibility API。我们定位每个项目的 IAccessible 接口,通过其标签找到重要按钮,并使用 IAccessible::accDoDefaultAction 激活正确的按钮。如果按钮除了标签之外还有其他区分特征,那会容易得多,但我找不到任何。当然,标签取决于当前选择的语言。所以,我们不能只寻找一个名为“Don’t Copy”的按钮。我们必须找到当前语言的文本。文本位于 shell32.dll.mui 文件的字符串表中。例如,“Don’t Copy”是字符串 13606,“Move”是 13610 等等。

所以您有了——隐藏原始对话框,显示我们自己的对话框,然后使用 Accessibility API 根据用户选择来控制原始对话框。就这么简单。

文件夹视图中的 Alt+Enter

这很容易。我们子类化树视图并监听 WM_SYSKEYDOWN 消息,其中 wParam = VK_RETURN。当消息到来时,我们找到选定的项目。存储在树控件项目中的用户数据是相应 Shell 项的 PIDL。我们需要向上遍历树形结构并组合一个完整的 PIDL。最后,我们使用“properties”谓词调用 ShellExecuteEx 来显示属性

HTREEITEM hItem=TreeView_GetSelection(hwndTree);
LPITEMIDLIST pidl=NULL;
while (hItem) {
   TVITEMEX info={TVIF_PARAM,hItem};
   TreeView_GetItem(hwndTree,&info);
   LPITEMIDLIST **pidl1=(LPITEMIDLIST**)info.lParam;
   if (!pidl1 || !*pidl1 || !**pidl1) {
      if (pidl) ILFree(pidl);
      pidl=NULL;
      break;
   }
   LPITEMIDLIST pidl2=pidl?ILCombine(**pidl1,pidl):ILClone(**pidl1);
   if (pidl) ILFree(pidl);
   pidl=pidl2;
   hItem=TreeView_GetParent(hwndTree,hItem);
}
if (pidl) {
   SHELLEXECUTEINFO execute={sizeof(execute),
      SEE_MASK_IDLIST|SEE_MASK_INVOKEIDLIST,NULL,L"properties"};
   execute.lpIDList=pidl;
   execute.nShow=SW_SHOWNORMAL;
   ShellExecuteEx(&execute);
   ILFree(pidl);
   msg->message=WM_NULL;
}

调整文件夹视图的外观

既然我们为 Alt+Enter 功能对文件夹视图进行了子类化,让我们看看是否可以让它看起来像 Windows XP 中的文件夹视图

Different folder views

要获得经典外观,请添加 TVS_HASLINES 样式,删除 TVS_SINGLEEXPANDTVS_TRACKSELECT 样式,并删除 TVS_EX_FADEINOUTEXPANDOSTVS_EX_AUTOHSCROLL 扩展样式。

要获得简洁外观,请添加 TVS_SINGLEEXPANDTVS_TRACKSELECT 样式,删除 TVS_HASLINES 样式,并删除 TVS_EX_FADEINOUTEXPANDOSTVS_EX_AUTOHSCROLL 扩展样式。

或者,如果您只是不希望按钮淡出,只需删除 TVS_EX_FADEINOUTEXPANDOS 扩展样式。

添加工具栏

Classic Explorer Bar 是一个简单的桌面栏,内部有一个工具栏。您可以从此文章中了解如何创建桌面栏:Internet Explorer Toolbar (Deskband) Tutorial。有两个棘手的部分是我自己发现的。

首先,我希望桌面栏托管在 Windows Explorer 中,而不是 Internet Explorer 中。您可以在 DllMain 中通过检查 exe 名称来做到这一点,如果 exe 名为“iexplore.exe”,则返回 FALSE。为什么我们不能只为“explorer.exe”返回 TRUE?我们的 DLL 可能需要被 regsvr32.exemsiexec.exe 等其他可执行文件加载。因此,最好专门排除 iexplore.exe,而不是提供一个允许主机列表。

extern "C" BOOL WINAPI DllMain( HINSTANCE hInstance, 
           DWORD dwReason, LPVOID lpReserved )
{
   if (dwReason==DLL_PROCESS_ATTACH) {
      wchar_t path[_MAX_PATH];
      GetModuleFileName(NULL,path,_countof(path));
      if (_wcsicmp(PathFindFileName(path),L"iexplore.exe")==0)
         return FALSE;
   }
   return _AtlModule.DllMain(dwReason, lpReserved);
}

其次,Windows 7 中似乎存在一个错误。每个桌面栏都强制要求在 Windows Explorer 中独占一行。Explorer 会为每个乐队强制设置 RBBS_BREAK 样式。为了解决这个问题,解决方案是子类化 rebar 控件并强制为 RBBS_BREAK 样式设置一个特定值。当乐队隐藏时(调用 IDockingWindow::ShowDW 并将 FALSE 传递给它),我们会记住状态,以便在下次打开 Explorer 或显示乐队时能够正确恢复。所有这些都特定于 Windows 7。Vista 不需要任何这种黑客攻击,因为它能正确恢复乐队状态。

既然我们有了工具栏,我们就必须为每个按钮编写代码。向上按钮导航到父文件夹

pBrowser->BrowseObject(NULL, 
  (GetKeyState(VK_CONTROL)<0?SBSP_NEWBROWSER:SBSP_SAMEBROWSER)|
  SBSP_DEFMODE|SBSP_PARENT);

后退前进按钮以类似方式工作。

剪切复制粘贴删除和其他按钮只需向 Explorer 发送 WM_COMMAND 消息。您可以使用 Spy++ 发现命令代码。如果您想操作树视图中的文件夹还是列表视图中的文件,命令代码是不同的

操作 (Operation) 文件夹树命令 文件列表命令
剪切 41025 28696
复制 41026 28697
粘贴 41027 28698
删除 40995 28689
复制到 28702 -----
移动到 28703 -----
撤销 28699 28699
重做 28704 28704
全选 28705 28705
反选 28706 28706
刷新 41504 41504
属性 无命令,请参阅 Alt+Enter 28691

电子邮件按钮使用 SendMail 对象,如这里所述:发送到电子邮件收件人

在标题栏中添加向上按钮

这比预期的要容易得多。Explorer 的标题栏包含一个 rebar 控件。我们只需要创建一个带有一个按钮的小工具栏,并将其添加为 rebar 乐队。我们需要替换工具栏的整个渲染,有两个原因。首先,对于 Aero 模式,需要清除背景以使其透明。其次,我们希望我们的图标占满按钮的整个空间,没有按钮边框。这可以通过在工具栏父级中处理 NM_CUSTOMDRAW 通知来完成。

状态栏

Windows 7 在状态栏中不显示所选文件的总大小。相反,您必须查看“详细信息”窗格,但这仅限于 15 个文件。如果您选择超过 15 个文件,您必须按“更多详细信息”才能获得总大小。一旦您选择了更多文件,大小就会消失。这非常烦人。让我们尝试修复状态栏。

首先,我们使用 IShellBrowser::GetControlWindow(FCW_STATUS) 找到状态栏控件并对其进行子类化。当 Explorer 想要更新文本“已选择 10 个项目”时,它会发送消息 SB_SETTEXT,其中 LOWORD(wParam)=0。我们捕获该消息,并在末尾追加总磁盘空间,使其变为“已选择 10 个项目(磁盘可用空间:358 GB)”。然后,我们计算选定内容的总大小并将其显示在状态栏的第二部分

// recalculate the total size of the selected files and show
// it in part 2 of the status bar
IShellBrowser *pBrowser=((CExplorerBHO*)uIdSubclass)->m_pBrowser;
__int64 size=-1;
CComPtr<IShellView> pView;

if (pBrowser && SUCCEEDED(pBrowser->QueryActiveShellView(&pView)))
{
  CComQIPtr<IFolderView> pView2=pView;
  CComPtr<IPersistFolder2> pFolder;
  LPITEMIDLIST pidl;
  if (pView2 && SUCCEEDED(pView2->GetFolder(IID_IPersistFolder2,(void**)&pFolder))
        && SUCCEEDED(pFolder->GetCurFolder(&pidl)))
  {
    CComQIPtr<IShellFolder2> pFolder2=pFolder;
    UINT type=SVGIO_SELECTION;
    int count;
    if ((dwRefData&SPACE_TOTAL) && 
        (FAILED(pView2->ItemCount(SVGIO_SELECTION,&count))
        || count==0))
      type=SVGIO_ALLVIEW;
    CComPtr<IEnumIDList> pEnum;
    if (SUCCEEDED(pView2->Items(type,IID_IEnumIDList,(void**)&pEnum)) && pEnum)
    {
      PITEMID_CHILD child;
      SHCOLUMNID column={PSGUID_STORAGE,PID_STG_SIZE};
      while (pEnum->Next(1,&child,NULL)==S_OK)
      {
        CComVariant var;
        if (SUCCEEDED(pFolder2->GetDetailsEx(child,&column,&var)) && 
            var.vt==VT_UI8)
        {
          if (size<0)
           size=var.ullVal;
          else
           size+=var.ullVal;
        }
        ILFree(child);
      }
    }
    ILFree(pidl);
  }
}
if (size>=0)
{
  // format the file size as KB, MB, etc
  StrFormatByteSize64(size,buf,_countof(buf));
}
else
  buf[0]=0;
DefSubclassProc(hWnd,SB_SETTEXT,1,(LPARAM)buf);

当然,每次选择更改时计算大小可能会很昂贵。当您在大型文件夹中通过按住 Shift+向下箭头选择多个文件时,选择会频繁更改。因此,当选择更改时,我们只需启动一个计时器,并在 10 毫秒后进行繁重计算。如果在此期间选择了更改,计时器将重新启动。

注意:Windows 7 有一个烦人的错误。当您打开一个新的 Explorer 窗口时,状态栏通常只有一个部分。如果调整窗口大小,状态栏将重置为其正确的 3 部分状态。为了解决这个问题,代码会将窗口缩小一像素,然后恢复到原始大小。有时,即使这样也不够。如果代码检测到状态栏仍然只有一个部分,它会发送“刷新”命令。这通常会解决问题。

Explorer 中的滚动问题

Windows 7 Explorer 中的另一个错误涉及展开导航窗格中的文件夹。如果您选择一个以前未展开过的文件夹,然后展开它,导航窗格将滚动到顶部,然后滚动到选定的项目。结果是选定的项目出现在底部附近(而不是您期望的顶部附近)。幕后发生的是 Explorer 发送两个 TVM_ENSUREVISIBLE 消息——第一个用于顶部项目,第二个用于选定的项目。我不知道为什么需要发送第一条消息。也许存在某种奇怪的情况,滚动到顶部实际上有所帮助,但我从未见过。

为了解决这个 bug,我们可以让树控件忽略第一条消息

if (uMsg==TVM_ENSUREVISIBLE)
{
  HTREEITEM hItem=(HTREEITEM)lParam;
  if (!TreeView_GetParent(hWnd,hItem) &&
      !(TreeView_GetItemState(hWnd,hItem,TVIS_SELECTED)&TVIS_SELECTED))
    return 0;
}

基本上,它会忽略顶部项目(如果未选中)的 TVM_ENSUREVISIBLE。假设如果 Explorer 真的想聚焦顶部项目,它会先选择它。

抱怨:首先,有 RBBS_BREAK bug,然后是状态栏问题,然后是这个。这里发生了什么?微软在 Windows 7 中雇了一个实习生来完成 Explorer 吗?QA 是否在工作中睡着了?Explorer 是 Windows 最常用的应用程序。它应该比这更受关注!我记得 Windows XP 发布时,Explorer 中唯一的 bug 是它没有正确重绘水平和垂直滚动条之间的角落。啊,美好的时光。现在,我祈祷并等待 Win 7 SP1 :)

本地化

Windows Vista 和 Windows 7 支持 35 种语言(如果您有 Ultimate 版本,可以安装一种以上的语言)。我们希望本地化 UI,使其能无缝集成到操作系统中。

本地化文本是第一项任务。Classic Shell 有两个文件包含本地化数据:ExplorerL10N.ini 包含 Classic Explorer 的文本,StartMenuL10N.ini 包含开始菜单的文本。每种语言都有自己的部分

[ar-SA] - Arabic (Saudi Arabia)
Menu.Programs = البرا&مج
Menu.Favorites = المف&ضلة

[bg-BG] - Bulgarian (Bulgaria)
Menu.Programs = &Програми
Menu.Favorites = Пре&дпочитани

[el-GR] - Greek (Greece)
Menu.Programs = &Προγράμματα
Menu.Favorites = Αγαπ&ημένα

[en-US] - English (United States)
Menu.Programs = &Programs
Menu.Favorites = F&avorites

节的名称([en-US][bg-BG] 等)来自 GetThreadPreferredUILanguages 函数。它返回语言名称列表,按首选项排序。要获取本地化字符串,请调用 FindTranslation 函数

const wchar_t *FindTranslation( const char *name, const wchar_t *def );

如果第一个首选语言中找不到字符串,则使用下一种语言。如果所有语言均未识别,则使用 [default] 部分。而且,如果找不到文本,FindTranslation 将返回 def 参数。

本地化的一部分是支持阿拉伯语和希伯来语等从右到左的语言。我们用这段代码检查语言是否是 RTL

bool IsLanguageRTL( void )
{
    LOCALESIGNATURE localesig;
    LANGID language=GetUserDefaultUILanguage();
    if (GetLocaleInfoW(language,LOCALE_FONTSIGNATURE,(LPWSTR)&localesig,
        (sizeof(localesig)/sizeof(wchar_t))) && 
        (localesig.lsUsb[3]&0x08000000))
      return true;
   return false;
}

对于 RTL 语言,有几点需要考虑

  • 对话框需要镜像。这是通过具有 RTL 样式的单独对话框资源来实现的。
  • 开始菜单需要镜像。因此,我们为菜单容器窗口设置 RTL 样式。工具栏足够智能,可以反转其方向。
  • 由于工具栏已反转,因此所有图标都会被镜像。我们不希望这样,因此我们需要为工具栏的图像列表设置 ILC_MIRROR 标志,将图标镜像回正常状态。
  • 上下文菜单也需要镜像。这是通过将 TPM_LAYOUTRTL 标志传递给 TrackPopupMenu 来实现的。
  • AnimateWindow 在 RTL 布局方面存在一些问题。它使用 WM_PRINTWM_PRINTCLIENT 消息,它们与 RTL 不兼容。不幸的是,我还没有找到修复方法,所以目前,菜单动画在 RTL 语言中是禁用的

安装程序

Classic Shell 的安装程序分为三个项目。ClassicShellSetup32 构建一个 32 位 MSI 文件,ClassicShellSetup64 构建一个 64 位 MSI 文件。64 位包包含 32 位和 64 位版本的 ClassicExplorer.dll,因为您可以在 64 位 Windows 上运行 32 位 Explorer。

ClassicShellSetup 构建一个 exe 文件,该文件将两个 MSI 文件合并到一个方便的包中。当然,也可以分发两个单独的 MSI 文件,但 exe 除了方便打包外,还提供了更多优势

  • EXE 可以检查是否在旧版 Windows 上运行并报错。
  • EXE 可以检查操作系统是 32 位还是 64 位。并将运行正确的 MSI。
  • 安装后,我们希望启动开始菜单 exe。由于 MSI 包在提升的环境中运行,而开始菜单需要作为当前用户运行,因此无法从 MSI 包中启动。
  • EXE 可以有一个漂亮的非默认图标。

构建解决方案

包含的解决方案适用于 Visual Studio 2008。它需要分两步构建。

首先,为 Setup|Win32 配置执行完整构建。这将构建 32 位模块并创建 32 位 MSI 文件。其次,为 Setup|x64 配置执行完整构建。这将构建 64 位模块,创建 64 位 MSI 文件,并构建最终包 ClassicShellSetup\Release\ClassicShellSetup.exe。运行 EXE 进行安装。

重要提示:首次构建安装程序项目时,您可能会收到有关添加某些系统文件(如 OLEACC.dllUxTheme.dll)的依赖项的警告。确保排除所有检测到的依赖项,然后再次构建。如果您留下 dwmapi.dll 等依赖项,它们将被安装到目标计算机上。如果它碰巧是不同版本的 Windows(例如 32 位与 64 位,或 Vista 与 Windows 7),Classic Shell 将无法启动!

最后,给开发者的几点建议

如果禁用 ClassicStartMenu.cpp 中的 #define HOOK_EXPLORER 行,则调试开始菜单会更容易。然后,菜单将在自己的进程中运行,并且不会干扰 Explorer 进程。某些功能将被禁用,但对于大多数用途来说已经足够了。

构建 ReleaseDebug 配置的 Classic Explorer 将将其注册为 Shell 扩展。下次打开 Explorer 窗口时,它将被激活。您可以将调试器附加到 explorer.exe 进程并调试 Shell 扩展。

在开发过程中,DLL 由 Explorer 加载且无法重新构建会很不方便。因此,我们需要一种重新启动 Explorer 的方法。您可以通过在任务管理器中杀死它并运行 explorer.exe 来重新启动 Explorer。但是,重新启动 Explorer 的更简单方法是让它崩溃。在 ReleaseDebug 中,如果您按住Shift并单击设置按钮,它将强制 Explorer 崩溃。

如果是在 64 位 Vista 上开发,还有另一种重新启动 Explorer 的解决方法。您可以运行 32 位 Explorer,并在完成后将其关闭。直接从 Visual Studio 运行 C:\Windows\SysWOW64\Explorer.exe。这在 Windows 7 上不起作用。32 位 Explorer 启动 64 位版本,然后立即退出。

有改进空间

当然,总有改进的空间。这是我为未来计划的

  • 开始菜单更好的可访问性。它已经完成了一半,但仍有工作要做。
  • 开始菜单可能会跟踪最近使用的程序并提供更方便的访问。
  • 开始菜单对 Aero 的更好支持,包括菜单项后面和子菜单中的支持。
  • 开始菜单应该使用系统声音来打开子菜单或激活菜单项。

结束

我计划这是本文的最后一次更新。它的主要目的是展示如何做一些棘手的 Explorer 操作——替换开始菜单,添加工具栏,解决 bug 等等。我为未来计划的功能主要是 Classic Shell 内部的改进。它们不涉及与 Explorer 的交互,也不会写成一篇有趣的文章。此外,文章已经很长了。如果有什么新功能值得分享,我认为最好写一篇新文章。

我仍将回答评论区的问题,并在新版本发布时发布更新。

历史

这些只是每个版本中的亮点。有关更改的完整列表,请查看历史页面:Classic Shell 历史记录

  • 版本 1.0.1 正式发布 (2010 年 2 月)
    • 这是一个仅修复 bug 的版本。修复了开始菜单中的一些罕见崩溃。
  • 版本 1.0.0 正式发布 (2010 年 2 月)
    • 在 Explorer 标题栏中添加了向上按钮。
    • 安装程序支持用于日志记录或无人值守安装的命令行选项。
  • 版本 0.9.10 发布候选版 (2010 年 1 月) - 制作您自己的工具栏
    • Explorer 工具栏可以通过新图标和附加按钮进行自定义。
    • Active Accessibility 支持。
  • 版本 0.9.9 发布候选版 (2010 年 1 月) - 制作您自己的开始菜单
    • 开始菜单可以通过新图标和附加菜单项进行自定义。
    • 开始菜单中的皮肤可以有不同的变体。
    • 在 Explorer 中添加了“电子邮件”按钮。
  • 版本 0.9.8 beta (2010 年 1 月) - 开始菜单的皮肤
    • 为经典开始菜单添加了皮肤支持。
    • 将文件夹冲突对话框替换为更简单的版本(类似于文件冲突对话框)。
  • 版本 0.9.7 beta (2009 年 12 月)
    • 在 Windows 7 Explorer 的状态栏中添加了可用磁盘空间和文件大小。
    • 为开始菜单添加了右键拖动。
  • 版本 0.9.6 beta (2009 年 12 月)
    • 在工具栏中添加了“属性”按钮。
    • 添加了 Explorer 文件夹树外观的设置 - XP Classic、XP simple 等。
    • 添加了对开始菜单组策略的支持,例如“隐藏运行”、“隐藏帮助”、“始终显示注销”等。
  • 版本 0.9.5 beta (2009 年 12 月)
    • 添加了删除“文档”菜单的选项,以及使用替代搜索应用程序的选项(由 johnohn 请求)。
    • 向工具栏添加了更多按钮 - 剪切、复制、粘贴、删除。
    • 修复了开始菜单中的崩溃(感谢 AlexG)。
  • 版本 0.9 beta (2009 年 11 月) - 第一个公开版本
    • 经典开始菜单。
    • Vista 复制 UI 的替换项。
    • Explorer 中 Alt+Enter 的修复。
    • Explorer 的工具栏带向上按钮。
© . All rights reserved.