将图标嵌入WPF应用程序作为资源

12

我正在尝试将图标嵌入到我的WPF应用程序中,以便我可以使用以下代码从Windows 7 JumpList中提取它并用作图标:

newScene.IconResourcePath = System.Reflection.Assembly.GetEntryAssembly().Location;
newScene.IconResourceIndex = 0;

我已经使用以下方法让它工作:http://dennisdel.com/?p=38

然而,这似乎不是最好的方法,似乎应该有一种更简单的方法将图标资源嵌入到我的应用程序中,同时仍然保留在我的程序的应用程序属性中勾选“图标和清单”选项。

我尝试了许多方法,包括将图标构建操作设置为资源和嵌入式资源,但每次我在资源编辑器中打开我的.exe文件时,图标都不会出现。

有什么建议吗?


仍然无法让它正常工作。 - user99999991
1个回答

31

Visual Studio没有提供从MSBuild任务执行Win32资源编译器的方法,也没有用于创建原始资源的嵌入式功能。因此,您的选择是:

  1. 按照链接文章中所述手动创建“.res”文件,或者
  2. 添加构建任务,以便您可以从.csproj调用Win32资源编译器

首先,我将解释五种不同类型的“资源”,它们可以存在于.exe或.dll文件中,其中包括JumpList所需的“Win32资源”。

然后,我将解释如何构建自定义构建任务,允许您在C#或VB.NET可执行文件中嵌入任意Win32资源。


Win32可执行文件中的五种资源类型

在.exe或.dll文件中,有五种不同类型的“资源”:

  • Win32资源
  • NET Framework“嵌入式资源”
  • ResourceSet中的CLR对象
  • XAML资源
  • WPF资源(ResourceDictionary中的对象)

Win32资源

最初的资源类型是Win32“资源”。这种资源在.rc文件中定义,并且具有编号或名称的资源,每个资源都有一个类型和一组数据。Win32资源编译器rc.exe将.rc文件编译为二进制.res文件,然后可以将其添加到生成的可执行文件中。

使用Win32 FindResourceLoadResource函数可以访问Win32资源。

在C++应用程序中通过将它们添加到.rc文件中,将Win32资源嵌入到应用程序中。.rc文件会被编译成一个.res文件并链接到可执行文件中。也可以使用rc.exe程序在事后添加Win32资源。对于C#和VB.NET应用程序,MSBuild可以将预构建的.res文件添加到由Csc或Vbc编译器创建的可执行文件中,或者可以为您构建默认的.res文件。C#和VB.NET都无法从.rc文件构建非默认的.res文件,也没有MSBuild任务可为您完成此操作。

您可以通过在Visual Studio中使用“文件”->“打开”选项打开.exe或.dll文件本身来查看其中的Win32资源。

典型的C、C++或MFC应用程序将具有许多Win32资源,例如每个对话框将由一个资源指定。

典型的WPF应用程序将仅具有C#或VB.NET编译器构建的三个默认Win32资源:版本资源、RT_MANIFEST和应用程序图标。这些资源的内容是根据代码中的程序集属性和.csproj或.vbproj文件中的<ApplicationIcon>元素构建的。

这是JumpList正在查找的资源类型。

嵌入式资源

一个“嵌入式资源”是.NET Framework的资源。包含这些资源的数据结构由CLR进行管理,以更便于托管代码访问的方式进行管理。每个资源都由字符串名称标识,按照惯例以与资源关联的类的命名空间开头。
嵌入式资源只是具有名称的二进制数据块。实际数据类型由调用者知道或从名称推断,类似于文件系统中的文件。例如,名称以“.jpg”结尾的嵌入式资源很可能是JPEG文件。
使用Assembly.GetManifestResourceStream及其子项GetManifestResourceInfo和GetManifestResourceNames访问嵌入式资源。
通过将文件添加到项目并将构建操作设置为“嵌入式资源”,可以将嵌入式资源嵌入.exe和.dll文件中。
您可以在NET Reflector中打开.exe或.dll文件,并查看“Resources”文件夹以查看嵌入式资源。
嵌入式资源通常在WinForms中使用,但在WPF中几乎从不使用。
多个.NET Framework对象(如字符串和图标)可以组合成单个“资源集”数据结构,存储在.exe作为单个.NET Framework嵌入式资源中。例如,WinForms使用它来存储不易包含在生成的代码中的图标和字符串等内容。
可以使用CLR定义的ResourceManager和ResourceSet类单独检索资源集中的对象。
资源集中的对象由.resx文件在源代码中定义。数据可以直接在.resx文件中(如字符串的情况)或由.resx文件引用(如图标的情况)。当构建项目时,每个.resx文件指定的内容会被序列化为二进制形式,并作为单个内嵌资源存储,其扩展名“.resx”替换为“.resources”。
可以通过在NET Reflector中打开.exe或.dll文件,并打开Resources文件夹,单击“.resources”文件并查看右侧窗格中的项来查看资源集中的对象。
许多WinForms时代的功能通常使用.resx文件和ResourceSets以类似于旧的Win32.rc文件的方式存储多个资源,例如字符串。它们还由WinForms本身用于存储无法放入代码后台的表单设置。
WPF应用程序几乎从不使用资源集中的任意对象,尽管WPF本身使用ResourceSets在内部存储已编译的XAML。

WPF XAML 资源是编译后的 XAML 文件,存储在 ResourceSet 内。资源集内的名称为原始文件名并将“.xaml”替换为“.g.baml”。其内容可以是任何有效的 XAML,最常见的类型包括 Window、Page、UserControl、ResourceDictionary 和 Application。

可以使用 Application.LoadComponent() 或在 WPF 上下文中引用原始 XAML 文件名来加载 WPF 资源。此外,任何带有代码依赖项(由x:Class指定)的 WPF 资源都将自动加载并应用于创建该类对象时的每个对象,在其 InitializeComponent 调用期间。

通过将 .xaml 文件添加到项目并将其构建操作设置为“Resource”、“Page”或“ApplicationDefinition”,即可创建 WPF 资源。这会导致编译器将文件编译为 BAML 并将其添加到适当的 ResourceSet 中。

可以使用 NET Reflector 并安装 BamlViewer 插件,打开 .exe 或 .dll 文件以查看其中的 XAML 资源,选择菜单中的“Tools→BAML Viewer”,并使用 BAML Viewer 浏览到 .resources 文件内的特定 .g.baml 文件。

ResourceDictionary 中的 WPF 资源

在 WPF 中,几乎所有所谓的“资源”都是 ResourceDictionary 中的条目。ResourceDictionaries 在 XAML 中描述,可以在其他对象(例如 Windows 和 UserControls)中或仅包含 ResourceDictionary 的单独 XAML 文件中描述。每个 ResourceDictionary 都由“x:Key”标识,可以是任何对象类型。资源本身也可以是任何对象类型。

WPF 资源可以使用 {StaticResource}{DynamicResource} 标记扩展来引用 XAML,也可以使用 FindResource 在代码中加载。

将 WPF 资源添加到 ResourceDictionary 中,可以将其添加到包含 ResourceDictionary 的 XAML 文件中,然后在 <ResourceDictionary> 元素内部赋予它们一个 x:Key 属性。

WPF 资源在 WPF 中广泛使用,包括笔刷、样式、数据、几何图形、模板等。

可以通过浏览上述 XAML 资源并查看 ResourceDictionary 标记内部的资源来查看 .exe 或 .dll 中的 WPF 资源。


在 C# 或 VB.NET 可执行文件中包含 Win32 资源

如何轻松地将任意 Win32 资源嵌入 C# 或 VB.NET .exe 中

从上面的讨论中,您会注意到除了 Win32 资源外,可以轻松地将每种类型的资源添加到 C# 或 VB.NET 应用程序中。为了使此过程更加容易,您可以添加一个额外的构建任务和目标,如下所示:

  1. 构建一个包含单个“Win32ResourceCompiler”构建任务的项目并编译它
  2. 创建一个“.targets”文件,其中包含一个单独的目标,使用此任务自动将.rc文件构建为.res文件
  3. 设置项目使用生成的.res文件

该任务非常简单:

public class Win32ResourceCompiler : ToolTask
{
  public ITaskItem Source { get; set; }
  public ITaskItem Output { get; set; }

  protected override string ToolName { get { return "rc.exe"; } }

  protected override string GenerateCommandLineCommands()
  {
    return @"/r /fo """ + Output.ItemSpec + @""" """ + Source.ItemSpec + @"""";
  }

  protected override string GenerateFullPathToTool()
  {
    // TODO: Return path to rc.exe in your environment
  }
}

.targets文件也非常简单。它将是以下内容:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <UsingTask TaskName="SomeNamespace.Win32ResourceCompiler" AssemblyFile="Something.dll" />

  <PropertyGroup>
    <CoreCompileDependsOn>$(CoreCompileDependsOn);CompileWin32RCFile</CoreCompileDependsOn>
  </PropertyGroup>

  <Target Name="CompileWin32RCFile" Outputs="@(Win32RCFile->'%(filename).res')">
    <Win32ResourceCompiler
      Source="@(Win32RCFile)"
      Output="@(Win32RCFile->'%(filename).res')" />
  </Target>
</Project>

现在,在您的.csproj文件中,添加对您的.targets文件的引用:

<Import Project="Win32ResourceCompiler.targets" />

当然,您需要将.rc文件的文件类型设置为Win32RCFile:

<ItemGroup>
  <Win32RCFile Include="MyWin32Resources.rc" />
</ItemGroup>

使用这个设置,您可以创建一个传统的 Win32 .rc 文件来指定所有的 Win32 资源,包括版本、清单、应用程序图标以及您想要的其他所有图标。每次编译时,所有这些 Win32 资源都将添加到您的 .exe 文件中。

这需要一点时间来设置,但从长远来看比手动编辑 .res 文件更加令人满意和简单。

您可以像这样在您的 .rc 文件中指定多个图标:

1 ICON ApplicationIcon.ico
2 ICON JumpListIcon.ico
3 ICON AnotherIcon.ico

这里是关于在.rc文件中可以使用的所有资源定义语句的文档。

此外请注意,上述的.targets文件是匆忙打出来的,尚未经过测试。有关MSBuild(.csproj和.targets)文件语法的文档可以在这里这里找到,.targets文件的好示例可以在c:\Windows\Microsoft.NET\Framework\v3.5目录中找到)。


非常感谢您提供的详细解释。在您的解释之前,我对资源的使用还一筹莫展。然而,我在编辑.csproj文件并导入我的.targets文件时遇到了问题。我收到以下错误提示:The element <Target> is unrecognized, or not supported in this context.我认为这是因为我不理解如何引用任务,因为我无法根据Microsoft.Common.targets文件找出如何做到这一点。除此之外,我认为我已经掌握了所有内容。 - Scott Scowden
从错误来看,我猜测你的.targets文件省略了<Project>或xmlns定义。我编辑了我的答案,更好地解释了应该如何创建.targets文件,包括完整(但未经测试)的示例代码。看看是否使用我写的内容可以帮助你解决这个错误。 - Ray Burns
好的,我已经尽力自己解决了,但是没有成功。忘了提到我正在使用VS2010/.NET4。我已经到了这个地步,似乎我的.csproj正在调用我的程序集,但无法找到我的ToolTask。每次都必须关闭/重新打开VS2010,因为它似乎无法正确重建(即使我清理)除非我这样做。我还发现,由于MSBuild 4现在存在使用括号的错误,因此使用UsingTask的“AssemblyName”属性会更好,但我无论如何都遇到了相同的错误。 - Scott Scowden
为了诊断确切的问题,我会在包含您的.csproj的目录下,在Assembly.LoadFrom()上设置断点,通过调试器运行MSBuild.exe,并查看程序集是否被加载。如果是这样,请仔细比较Reflector报告的类名与UsingTask中的类名,并确保它实现了ITask接口。如果没有加载,则将UsingTask中的名称与目标元素名称进行比较。并且在测试时一定要使用AssemblyFile而不是AssemblyName。AssemblyName很容易选择错误的文件,而AssemblyFile可以采用绝对路径名。 - Ray Burns
如果失败,请发布您的.targets文件和任务项目的完整代码。您的任务项目应该是一个类库(dll)项目,仅包含单个类Win32ResourceCompiler。还要确保它实现的ITask与MSBuild使用的Microsoft.Build.Framework.dll相同。 - Ray Burns
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接