Victor's blog

Stay hungry, stay foolish.

0%

WPF单文件打包分享(含嵌入资源及DllImport的C++ Dll)

一般而言,除单项目且无其它引用的WPF应用(控制台、Winform等其它应用也是如此)(对了,还要除掉dotNET 6的单文件发布🤣),在构建后,运行所需的文件不止一个,通常还会带有几个DLL或其它的文件(如各种媒体文件:mp3、mp4、png等),此时分发起来显然不够方便。然后又在某些情况下我们可能不希望制作安装包,此时Costura.Fody就可以派上用场了。

Costura.Fody

据此项目的Github页所述,虽然这个项目使用的是最宽松的MIT开源协议,但其实这居然是个收费项目😂😂😂,这与此前流行的“诚信商店”真是有着异曲同工之妙!

所以…如果我使用了这个项目并且没有贡献至少3美刀就变成失信人了?🤔

对了,等咱有钱了再贡献嘛😁😁

话说回来,Costura.Fody非常强大,大部分情况下只需要安装这个包就行了。

所以全部的操作就是:在nuget安装Costura.Fody到入口项目,非常优雅!

如下图的项目结构,我在SingleFilePackageTest项目中引用了项目SomeClassLibrary,在安装了Costura.Fody后,在生成文件夹单独拷贝SingleFilePackageTest.exe即可运行。

关于项目中其它资源

将对应资源的生成操作改为Resource(注意:不是内容嵌入的资源),

然后我们就可以使用如下代码将其从项目资源中提取出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// <summary>
/// 从“资源”中加载文件的byte数组
/// </summary>
/// <param name="pathInApplication">资源路径</param>
/// <param name="byCurrentAssembly">资源是否在当前项目中</param>
/// <returns>资源文件byte数组</returns>
public static byte[] LoadFileFromResource(string pathInApplication, bool byCurrentAssembly = true)
{
var assembly = byCurrentAssembly ? Assembly.GetCallingAssembly() : Assembly.GetEntryAssembly();
if (pathInApplication[0] == '/')
{
pathInApplication = pathInApplication.Substring(1);
}
var resFilestream = Application.GetResourceStream(new Uri(@"pack://application:,,,/" + assembly.GetName().Name + ";component/" + pathInApplication, UriKind.Absolute));
var ms = new MemoryStream();
resFilestream.Stream.CopyTo(ms);
var bytes = ms.ToArray();
return bytes;
}

//假设项目下有个`Assets`文件夹,里面有个文件`Test.mp3`,那么调用方式如下:
var resourceBytes = LoadFileFromResource("/Assets/Test.mp3", true);
Debug.WriteLine($"文件长度:{resourceBytes.Length}");

接下来将其保存到临时目录就可以想咋玩就咋玩啦!

对于需要通过DllImport调用的DLL

如果项目中包含需要使用DllImport调用的dll时,首先通过上述方式将DLL保存到临时目录,再将临时目录追加到当前进程的PATH环境变量中,然后使用DllImport才可以正确调用到dll。

上代码:

在使用DllImport前先调用下面的代码:

1
2
EmbeddedDllClass.ExtractEmbeddedDlls("MyLib.dll", LoadFileFromResource("/MyLib.dll", true));
EmbeddedDllClass.LoadDll("MyLib.dll");
EmbeddedDllClass.cs
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class EmbeddedDllClass
{
private static string tempFolder = "";
public static void ExtractEmbeddedDlls(string dllName, byte[] resourceBytes)
{
Assembly assem = Assembly.GetExecutingAssembly();
string[] names = assem.GetManifestResourceNames();
AssemblyName an = assem.GetName();

tempFolder = String.Format("{0}.{1}.{2}", an.Name, an.ProcessorArchitecture, an.Version);

string dirName = Path.Combine(Path.GetTempPath(), tempFolder);
if (!Directory.Exists(dirName))
{
Directory.CreateDirectory(dirName);
}

string path = Environment.GetEnvironmentVariable("PATH");
string[] pathPieces = path.Split(';');
bool found = false;
foreach (string pathPiece in pathPieces)
{
if (pathPiece == dirName)
{
found = true;
break;
}
}
if (!found)
{
Environment.SetEnvironmentVariable("PATH", dirName + ";" + path);
}

string dllPath = Path.Combine(dirName, dllName);
bool rewrite = true;
if (File.Exists(dllPath))
{
byte[] existing = File.ReadAllBytes(dllPath);
if (resourceBytes.SequenceEqual(existing))
{
rewrite = false;
}
}
if (rewrite)
{
File.WriteAllBytes(dllPath, resourceBytes);
}
}

[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr LoadLibrary(string lpFileName);

static public void LoadDll(string dllName)
{
if (tempFolder == "")
{
throw new Exception("Please call ExtractEmbeddedDlls before LoadDll");
}
IntPtr h = LoadLibrary(dllName);
if (h == IntPtr.Zero)
{
Exception e = new Win32Exception();
throw new DllNotFoundException("Unable to load library: " + dllName + " from " + tempFolder, e);
}
}
}

本文部分代码来自StackOverflow@Mark Lakata,感谢!