之前用NSIS的时候发现不方便自制UI,找了其他的安装包开发框架,开发手感还是不好,功能残缺不一。我就想 如何使用纯代码的方式制作安装包 呢?经历了众多艰难才终于摸出方法。网上现有的文章都是用NSIS,Windows Installer等等现成框架制作安装包的。所以我另开一个专栏,直接讲如何手撸安装包!
1.新建项目,包含所需的头文件
推荐开发环境:VS2022
#include<Windows.h> //大Win头
#include<ShlObj.h> //COM接口头文件
#include<wrl.h> //用于COM接口,方便我们调用
using namespace Microsoft::WRL;
#include<iostream> //C++标准输入输出
#include<fstream> //C++标准文件
#include<string> //C++标准字符串,另外也用于宽窄字符转换
using namespace std;
#include "zlib/zlib.h" //源于Zlib,是Zlib的源头
#include "zlib/unzip.h" //Zlib第三方库minizip的一个头文件,用于解压zip文件
#pragma comment(lib,"zlib/zlibstat") //Zlib链接库
#include "resource.h" //向项目添加资源时自动生成的资源头文件
Zlib地址:https://wwek.lanzoub.com/irjyk0i1gyhc
这里我们引用了一个第三方库 Zlib,上边已经有我编译好的Zlib库。各位读者直接去链接下载解包,将文件夹直接放进项目文件夹就行了。(这里的Installer是我的项目文件夹)
为什么我们要引用 Zlib 库?一般而言,安装包其实是一个本身包含压缩文件的可执行程序 ,运行时就会将文件解压到安装目录,并通过修改注册表的方式将软件录入到系统里,从而实现程序的安装与卸载。所以我们使用 Zlib 库,实现程序自解压文件的功能 (当然你可以自己造轮子实现)
2.将压缩文件添加到项目
那么我们如何将压缩文件“嵌入”到程序中呢?
我们可以利用 Windows资源文件(.rc文件) 载入
1.将压缩文件放入项目中
2.右键项目->添加->新建项,资源->选择 资源文件(.rc)->添加
3.在资源视图里右键点击RC文件->添加资源->自定义->类型名称填入ZIP (可以自定义,只是个字符串资源),确定后就会发现我们已经将"ZIP"这个字符串加入资源文件里了。
4.按第3步的方法再次添加资源,选择导入->右下角选择 所有文件 ->点击压缩文件->打开,选择你刚刚创建的资源类型 (弹出的列表会有),确定后回到“解决方案资源管理器”即可。
可以看到,新生成了四个文件项:
resource.h:这个头文件是VS自动生成的,定义资源ID这些琐事由VS帮我们做
Resource.rc:定义资源的引用列表
test.zip:使用的压缩文件,本质上还是对该文件的引用。所以在编译前我们是可以随时修改这个压缩文件的!
zip1.bin:将链接资源的二进制文件,编译器通过这个实现文件的“嵌入”
当然,你嫌VS帮你做,结果导致报错改来改去很麻烦,可以手动重写 Resource.rc 和 resource.h 文件,不要忘记将Zip导入项目中就行了,这里不在一一赘述。
3.将资源从内存写入硬盘中
我们光有zip资源是不行的,我们要想一个方法将资源从程序中“取出来”,过程如下:
- FindResource() 通过资源描述符寻找zip资源
- LoadResource() 加载资源到内存
- LockResource() 锁定资源并得到资源在内存的指针
有了这三步,就能取zip文件到硬盘了。
HINSTANCE hins = GetModuleHandle(0); // 获取当前程序实例
HRSRC my_zip = nullptr; // 资源标识句柄
DWORD zip_size = 0; // 资源大小
HGLOBAL zip_data = nullptr; // 资源数据
my_zip = ::FindResource(hins, MAKEINTRESOURCE(IDR_ZIP2), L"ZIP"); //通过资源标识符寻找资源
zip_size = SizeofResource(nullptr, my_zip); //获取资源大小
zip_data = LoadResource(nullptr, my_zip); //加载资源
WCHAR SetupPath[MAX_PATH] = L"D:\\desktop\\myInstaller"; //安装目录
CreateDirectory(SetupPath, nullptr); //创建目录
wstring AddStrEngine(SetupPath);
AddStrEngine = AddStrEngine + wstring(L"\\m_zip.zip");
// 先创建文件
HANDLE zipfile = CreateFile2(AddStrEngine.c_str(), GENERIC_WRITE, 0, CREATE_ALWAYS, nullptr);
// 将数据写入文件
WriteFile(zipfile, (LPCVOID)LockResource(zip_data), zip_size, nullptr, nullptr);
// 关闭文件
CloseHandle(zipfile);
4.解压文件
我们将zip文件取出来,接下来就可以解压了。
解压要连续经历7步:
- unzOpen64()————————————————打开zip
- unzGetGlobalInfo64()———————————— 读取zip全局数据
- unzGetCurrentFileInfo64()—————————— 获取当前解压文件信息
- unzOpenCurrentFile()———————————— 打开文件
- unzReadCurrentFile()———————————— 解压当前文件到内存
- unzCloseCurrentFile()————————————关闭当前文件
- unzClose()—————————————————关闭zip文件
本质上还是依靠循环来读取并解压的。
char zipLocation[MAX_PATH]; //zip位置
unzFile compressZip = nullptr; //zip句柄
unz_global_info64 globalinfo = {}; //zip全局信息
size_t zip_info_filename_length = 0; //当前解压的文件名长度
size_t zip_info_file_size = 0; //当前解压的文件大小
char* zip_info_FileName = nullptr; //当前解压的文件名
char* zip_info_FileData = nullptr; //当前解压的文件数据
unz_file_info64 fileinfo = {}; //解压文件的信息
ofstream unzip_fstream; //解压文件流
string AStrEngine; //用于拼接字符串
sprintf_s(zipLocation, MAX_PATH, "%ws", AddStrEngine.c_str());
compressZip = unzOpen64(zipLocation); //打开zip
unzGetGlobalInfo64(compressZip, &globalinfo); //读取zip全局数据
for (size_t i = 0; i < globalinfo.number_entry; ++i) //逐个解压
{
AStrEngine.clear();
AStrEngine = "D:\\desktop\\myInstaller\\";
fileinfo = {};
zip_info_FileName = new char[MAX_PATH];
unzGetCurrentFileInfo64(compressZip, &fileinfo, zip_info_FileName, MAX_PATH,
nullptr, 0, nullptr, 0);
//获取当前解压文件信息
zip_info_file_size = fileinfo.uncompressed_size;
zip_info_FileData = new char[zip_info_file_size];
zip_info_filename_length = strlen(zip_info_FileName);
//如果是文件夹
if (zip_info_FileName[zip_info_filename_length - 1] == '/')
{
AStrEngine += zip_info_FileName;
CreateDirectoryA(AStrEngine.c_str(), nullptr);
//创建文件夹,解压文件时会解压到相应目录
}
else //如果是文件就解压
{
unzOpenCurrentFile(compressZip); //打开文件
AStrEngine += zip_info_FileName;
unzip_fstream.open(AStrEngine.c_str(), ios::out | ios::binary);
unzReadCurrentFile(compressZip, reinterpret_cast<void*>(zip_info_FileData), zip_info_file_size);
//读取当前文件到内存
unzip_fstream.write(zip_info_FileData, zip_info_file_size); //将文件从内存写入到硬盘里
unzip_fstream.close(); //关闭文件
}
unzCloseCurrentFile(compressZip); //关闭当前文件
delete[] zip_info_FileName;
delete[] zip_info_FileData;
if (i + 1 < globalinfo.number_entry)
{
unzGoToNextFile(compressZip); //解压下一个文件,指针偏移
}
}
unzClose(compressZip); //关闭zip文件
看到代码这么多,估计你此时和我都是一个表情 (😅),其实这部分代码很适合封装成函数,因为其功能对于做一个普通的安装包来说已经够完备了!你只要理解上述7步就可以立刻上手Zlib解压,只是麻烦在内存读取罢了。
5.修改注册表,将程序录入到系统中
读者是不是经常看到这个“烦人”的UAC (用户账户控制) 窗口?然而,这个窗口可以提供基本的保护,起码不受简单恶意程序对系统的篡改 (绕过UAC的除外) 。安装或卸载程序时如果有设置UAC,这个窗口就会弹出,如果安装程序没有获得管理员的许可,获得相应的权限,是不能读取与修改注册表的,即安装失败。 这个时候,我们就需要申请权限。
如何像一般安装包一样,先申请管理员权限再安装?
右键项目->属性->链接器->清单文件->UAC执行级别->将级别设为 RequireAdministrator (管理员)
有了管理员权限,仅差一步就大功告成了:
HKEY hkey = nullptr; //根键句柄
//创建一个叫 MyInstaller 的子键,这个是安装与卸载程序的地方
RegCreateKeyEx(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MyInstaller",
0, (WCHAR*)L"", 0, KEY_READ | KEY_WRITE,
nullptr, &hkey, nullptr);
//打开子键
RegOpenKey(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MyInstaller", &hkey);
//设置在"程序与功能"中显示的应用图标 (启动程序地址)
RegSetValueEx(hkey, L"DisplayIcon", 0, REG_SZ,
reinterpret_cast<const BYTE*>(L"D:\\desktop\\myInstaller\\d3dx12.exe"),
wcslen(L"D:\\desktop\\myInstaller\\d3dx12.exe") * 2);
//设置在"程序与功能"中显示的应用名称
RegSetValueEx(hkey, L"DisplayName", 0, REG_SZ,
reinterpret_cast<const BYTE*>(L"我的程序"),
wcslen(L"我的程序") * 2);
//设置版本号
RegSetValueEx(hkey, L"DisplayVersion", 0, REG_SZ,
reinterpret_cast<const BYTE*>(L"1.0.0.0"),
wcslen(L"1.0.0.0") * 2);
//设置发布者 (所属公司)
RegSetValueEx(hkey, L"Publisher", 0, REG_SZ,
reinterpret_cast<const BYTE*>(L"DGAF"),
wcslen(L"DGAF") * 2);
//设置安装路径
RegSetValueEx(hkey, L"InstallLocation", 0, REG_SZ,
reinterpret_cast<const BYTE*>(L"D:\\desktop\\myInstaller\\"),
wcslen(L"D:\\desktop\\myInstaller\\") * 2);
//设置卸载地址
RegSetValueEx(hkey, L"UninstallString", 0, REG_SZ,
reinterpret_cast<const BYTE*>(L"D:\\desktop\\myInstaller\\Uninstall.exe"),
wcslen(L"D:\\desktop\\myInstaller\\Uninstall.exe") * 2);
//关闭子键
RegCloseKey(hkey);
用到了以下四个函数,读者可以自行网上搜:
- RegCreateKeyEx
- RegOpenKey
- RegSetValueEx
- RegCloseKey
我这边没有用管理员开VS,就会弹出这个窗口,可以直接到编译路径找到.exe,点击运行就行了
效果就是这样,大家可以注意到,我现在还没有给出 Uninstall.exe 的源码。下一篇教程,我会讲如何做安装界面,顺带连卸载程序一并讲。可能有聪明的读者已经想到,原理都是一个样,还是用 修改注册表,删程序 的方式卸载,只不过是卸完擦屁股罢了。
6.添加程序快捷方式到桌面和开始菜单中
不多说,上代码:
WCHAR DesktopPath[MAX_PATH]; //桌面路径
WCHAR Programs_path[MAX_PATH]; //开始菜单路径
::SHGetSpecialFolderPath(nullptr, DesktopPath, CSIDL_DESKTOPDIRECTORY, false); //获取桌面路径
::SHGetSpecialFolderPath(nullptr, Programs_path, CSIDL_PROGRAMS, false); //获取菜单路径
ComPtr<IShellLink> pShortcutLink; //lnk链接接口
ComPtr<IPersistFile> pPersistFile; //lnk文件接口
//创建pShortcutLink
CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShortcutLink));
pShortcutLink->SetPath(L"D:\\desktop\\MyInstaller\\Media Play.exe"); //设置启动程序
pShortcutLink->SetIconLocation(L"D:\\desktop\\MyInstaller\\softmgr.ico", 0); //设置快捷方式图标位置
pShortcutLink->SetWorkingDirectory(L"D:\\desktop\\MyInstaller\\"); //设置快捷方式工作目录
pShortcutLink->SetDescription(L"一个快捷方式"); //设置说明
pShortcutLink.As(&pPersistFile); //将数据传递给pPersistFile接口
//最终创建快捷方式
pPersistFile->Save((wstring(DesktopPath) + wstring(L"\\myInstaller.lnk")).c_str(), false); //添加到桌面
pPersistFile->Save((wstring(Programs_path) + wstring(L"\\myInstaller.lnk")).c_str(), false); //添加到开始菜单
到这里,你的安装包就大功告成了。
完整代码示例
// Media Player Setup.cpp
#include<Windows.h> //大Win头
#include<ShlObj.h> //COM接口头文件
#include<wrl.h> //用于COM接口,方便我们调用
using namespace Microsoft::WRL;
#include<iostream> //C++标准输入输出
#include<fstream> //C++标准文件
#include<string> //C++标准字符串,另外也用于宽窄字符转换
#include<conio.h> //用于接收键盘消息
using namespace std;
#include "zlib/zlib.h" //源于Zlib,是Zlib的源头
#include "zlib/unzip.h" //Zlib第三方库minizip的一个头文件,用于解压zip文件
#pragma comment(lib,"zlib/zlibstat") //Zlib链接库
#include "resource.h" //向项目添加资源时自动生成的资源头文件
int main()
{
::CoInitialize(nullptr); //初始化COM接口
HINSTANCE hins = GetModuleHandle(0); //获取当前程序实例
HRSRC my_zip = nullptr; //资源标识句柄
DWORD zip_size = 0; //资源大小
HGLOBAL zip_data = nullptr; //资源数据
wstring StrPrintfEngineW; //用于拼接字符串的Unicode版本
WCHAR* SetupPathW = new WCHAR[MAX_PATH]; //安装目录的Unicode版本
BROWSEINFO file_open_dialog = {}; //文件夹选择框
LPITEMIDLIST idl = nullptr;
cout << endl;
cout << "您正在安装零秋Player!\n"
<< "按Y确认安装,并选择安装目录\n"
<< "按N取消安装并退出\n";
bool isExit = false; //是否退出循环
while (!isExit)
{
if (_kbhit()) //如果有键盘消息
{
switch (_getch())
{
case 'N':
case 'n':
exit(0);
break;
case 'Y':
case 'y':
file_open_dialog.hwndOwner = nullptr;
file_open_dialog.pszDisplayName = SetupPathW;
file_open_dialog.lpszTitle = L"请选择你的安装目录:";
file_open_dialog.ulFlags = BIF_RETURNFSANCESTORS;
idl = SHBrowseForFolder(&file_open_dialog); //如果没有选择就继续
if (nullptr == idl)
{
continue;
}
SHGetPathFromIDList(idl, SetupPathW);
isExit = true;
break;
}
}
}
//————————————————————————————————————————————————————————————————————————————————————————————————————————————
my_zip = ::FindResource(hins, MAKEINTRESOURCE(IDR_ZIP2), L"ZIP"); //通过资源标识符寻找资源
zip_size = SizeofResource(nullptr, my_zip); //获取资源大小
zip_data = LoadResource(nullptr, my_zip); //加载资源
StrPrintfEngineW = SetupPathW;
StrPrintfEngineW += wstring(L"\\ZAMediaPlayerSetup.zip");
// 先创建文件
HANDLE zipfile = CreateFile2(StrPrintfEngineW.c_str(), GENERIC_WRITE, 0, CREATE_ALWAYS, nullptr);
// 将数据写入文件
WriteFile(zipfile, (LPCVOID)LockResource(zip_data), zip_size, nullptr, nullptr);
// 关闭文件
CloseHandle(zipfile);
//————————————————————————————————————————————————————————————————————————————————————————————————————————————
char SetupPathA[MAX_PATH]; //安装路径的ASCII版本
char zipLocation[MAX_PATH]; //zip位置
unzFile compressZip = nullptr; //zip句柄
unz_global_info64 globalinfo = {}; //zip全局信息
size_t zip_info_filename_length = 0; //当前解压的文件名长度
size_t zip_info_file_size = 0; //当前解压的文件大小
char* zip_info_FileName = nullptr; //当前解压的文件名
char* zip_info_FileData = nullptr; //当前解压的文件数据
unz_file_info64 fileinfo = {}; //解压文件的信息
ofstream unzip_fstream; //解压文件流
string StrPrintfEngineA; //用于拼接字符串的ASCII版本
sprintf_s(SetupPathA, MAX_PATH, "%ws", SetupPathW);
sprintf_s(zipLocation, MAX_PATH, "%ws", StrPrintfEngineW.c_str());
compressZip = unzOpen64(zipLocation); //打开zip
unzGetGlobalInfo64(compressZip, &globalinfo); //读取zip全局数据
for (size_t i = 0; i < globalinfo.number_entry; ++i) //逐个解压
{
StrPrintfEngineA = SetupPathA;
StrPrintfEngineA += "\\";
fileinfo = {};
zip_info_FileName = new char[MAX_PATH];
unzGetCurrentFileInfo64(compressZip, &fileinfo, zip_info_FileName, MAX_PATH,
nullptr, 0, nullptr, 0);
//获取当前解压文件信息
zip_info_file_size = fileinfo.uncompressed_size;
zip_info_FileData = new char[zip_info_file_size];
zip_info_filename_length = strlen(zip_info_FileName);
//如果是文件夹
if (zip_info_FileName[zip_info_filename_length - 1] == '/')
{
StrPrintfEngineA += zip_info_FileName;
CreateDirectoryA(StrPrintfEngineA.c_str(), nullptr);
//创建文件夹,解压文件时会解压到相应目录
}
else //如果是文件就解压
{
unzOpenCurrentFile(compressZip); //打开文件
StrPrintfEngineA += zip_info_FileName;
unzip_fstream.open(StrPrintfEngineA.c_str(), ios::out | ios::binary);
unzReadCurrentFile(compressZip, reinterpret_cast<void*>(zip_info_FileData), zip_info_file_size);
//读取当前文件到内存
unzip_fstream.write(zip_info_FileData, zip_info_file_size); //将文件从内存写入到硬盘里
unzip_fstream.close(); //关闭文件
}
unzCloseCurrentFile(compressZip); //关闭当前文件
delete[] zip_info_FileName;
delete[] zip_info_FileData;
if (i + 1 < globalinfo.number_entry)
{
unzGoToNextFile(compressZip); //解压下一个文件,指针偏移
}
}
unzClose(compressZip); //关闭zip文件
//————————————————————————————————————————————————————————————————————————————————————————————————————————————
HKEY hkey = nullptr; //根键句柄
//创建一个叫 MyInstaller 的子键,这个是安装与卸载程序的地方
RegCreateKeyEx(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\ZA_MediaPlayer",
0, (WCHAR*)L"", 0, KEY_READ | KEY_WRITE,
nullptr, &hkey, nullptr);
//打开子键
RegOpenKey(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\ZA_MediaPlayer", &hkey);
StrPrintfEngineW = SetupPathW;
StrPrintfEngineW += L"\\";
StrPrintfEngineW += L"Media Player.exe";
//设置在"程序与功能"中显示的应用图标 (启动程序地址)
RegSetValueEx(hkey, L"DisplayIcon", 0, REG_SZ,
reinterpret_cast<const BYTE*>(StrPrintfEngineW.c_str()),
StrPrintfEngineW.length() * 2);
//设置在"程序与功能"中显示的应用名称
RegSetValueEx(hkey, L"DisplayName", 0, REG_SZ,
reinterpret_cast<const BYTE*>(L"零秋Player"),
wcslen(L"零秋Player") * 2);
//设置版本号
RegSetValueEx(hkey, L"DisplayVersion", 0, REG_SZ,
reinterpret_cast<const BYTE*>(L"1.0.0.0"),
wcslen(L"1.0.0.0") * 2);
//设置发布者 (所属公司)
RegSetValueEx(hkey, L"Publisher", 0, REG_SZ,
reinterpret_cast<const BYTE*>(L"DGAF"),
wcslen(L"DGAF") * 2);
//设置安装路径
RegSetValueEx(hkey, L"InstallLocation", 0, REG_SZ,
reinterpret_cast<const BYTE*>(SetupPathW),
wcslen(SetupPathW) * 2);
StrPrintfEngineW = SetupPathW;
StrPrintfEngineW += L"\\";
StrPrintfEngineW += L"Uninstall.exe";
//设置卸载地址
RegSetValueEx(hkey, L"UninstallString", 0, REG_SZ,
reinterpret_cast<const BYTE*>(StrPrintfEngineW.c_str()),
StrPrintfEngineW.length() * 2);
//关闭子键
RegCloseKey(hkey);
//————————————————————————————————————————————————————————————————————————————————————————————————————————————
WCHAR DesktopPath[MAX_PATH]; //桌面路径
WCHAR Programs_path[MAX_PATH]; //开始菜单路径
::SHGetSpecialFolderPath(nullptr, DesktopPath, CSIDL_DESKTOPDIRECTORY, false); //获取桌面路径
::SHGetSpecialFolderPath(nullptr, Programs_path, CSIDL_PROGRAMS, false); //获取菜单路径
ComPtr<IShellLink> pShortcutLink; //lnk链接接口
ComPtr<IPersistFile> pPersistFile; //lnk文件接口
CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShortcutLink));
StrPrintfEngineW = SetupPathW;
StrPrintfEngineW += L"\\";
pShortcutLink->SetPath((StrPrintfEngineW + wstring(L"Media Player.exe")).c_str()); //设置启动程序
pShortcutLink->SetIconLocation((StrPrintfEngineW + wstring(L"appicon.ico")).c_str(), 0); //设置快捷方式图标位置
pShortcutLink->SetWorkingDirectory(SetupPathW); //设置快捷方式工作目录
pShortcutLink->SetDescription(L"零秋播放器——快捷方式"); //设置说明
pShortcutLink.As(&pPersistFile); //将数据传递给pPersistFile接口
//最终创建快捷方式
pPersistFile->Save((wstring(DesktopPath) + wstring(L"\\零秋播放器.lnk")).c_str(), false); //添加到桌面
pPersistFile->Save((wstring(Programs_path) + wstring(L"\\零秋播放器.lnk")).c_str(), false); //添加到开始菜单
StrPrintfEngineW = SetupPathW;
StrPrintfEngineW += wstring(L"\\ZAMediaPlayerSetup.zip");
DeleteFile(StrPrintfEngineW.c_str()); //删除多余的安装zip
cout << endl;
cout << "安装已完成!" << endl;
system("pause");
}
文章来源:https://www.toymoban.com/news/detail-442306.html
这里有示例😎项目下载:https://wwek.lanzoub.com/iA0RE0ia3yri
密码:dgaf
下一章,我们将会用 Windows窗口 完善这个安装包。文章来源地址https://www.toymoban.com/news/detail-442306.html
到了这里,关于C++制作安装包【1】—— 控制台实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!