Qt/C++案例 记录创建并加载动态链接库修改exe/dll类型文件的版本信息的示例
目录导读
- 简要
- 创建动态库
- rcedit 开源项目介绍
- 拆分 类结构和公共函数
- 定义函数接口
- 部分函数实现示例:
- 使用DependenciesGui 查看WINAPI函数接口
- 加载动态库
- 创建UI界面
- QT 动态加载DLL
- 源码和运行程序
简要
又到年末了,迷迷糊糊感觉啥都没干,又感觉啥都做了,
最近客户又提了个需求,说是要分发软件,修改不同软件版本号,
领导一研究直接发了一个github的开源项目给我参考:
electron / rcedit
是一个通过命令行来修改软件的版本号和其他资源数据的工具源码(命令行工具,用于编辑Windows上的exe文件资源。),
但是总不能直接把这控制台程序直接发给客户用吧,
于是我想着能不能在原有基础上加个界面:
正好熟悉下
用VS2017创建动态链接库封装rcedit 项目中的函数方法,
再通过QT创建界面,
并且动态加载DLL调用WINAPI函数接口,
读取修改EXE/DLL的版本信息。
这篇文章,将所有操作做个总结汇总,毕竟平时创建动态链接库并通过调用函数的情况也比较少。
创建动态库
rcedit 开源项目介绍
rcedit 命令行工具,用于编辑Windows上的exe文件资源。
编译也比较简单,直接下载下来,使用qmake直接编译就通过,
生成项目结构如下图示:
主要只有rescle.cc,rescle.h,main.cc三个文件
创建一个rceditePackage
的动态链接库项目,拆分rescle.h
中的类结构并创建文件,再将对应的代码复制进去,公共部分内容移植到global.h
文件中就行,代码量不多,整体结构也规范,直接复制过去就行。
如下图示:
创建一个rescleapi.h文件,创建RESCLEAPI_API
宏定义,和使用extern "C" {}
使函数名称在生成的时候保持不变,
以及Source.def
文件的创建都是与一般的动态链接库创建操作一般无二。
#ifdef RESCLEAPI_EXPORTS
#define RESCLEAPI_API __declspec(dllexport)
#else
#define RESCLEAPI_API __declspec(dllimport)
#endif
#ifdef __cplusplus
extern "C" {
#endif
RESCLEAPI_API const wchar_t* GetErrorStr();
RESCLEAPI_API bool Get_Version_String(wchar_t* filepath, wchar_t* key, PTCHAR& Val);
RESCLEAPI_API bool Get_Version_String_Batch(std::wstring filepath, std::map<std::wstring, std::wstring>& Mapval);
RESCLEAPI_API bool Set_Version_String_Batch(std::wstring filepath, std::map<std::wstring, std::wstring> Mapval);
RESCLEAPI_API bool Set_Version_String(wchar_t* _filepath, wchar_t* _key, wchar_t* _val);
RESCLEAPI_API bool Set_File_Version(wchar_t* _filepath, wchar_t* _fileversion);
RESCLEAPI_API bool Set_Product_Version(wchar_t* _filepath, wchar_t* _productversion);
RESCLEAPI_API bool Set_Icon(wchar_t* _filepath, wchar_t* _pathicon);
RESCLEAPI_API bool Set_Requested_Execution_Level(wchar_t* _filepath, wchar_t* _level);
RESCLEAPI_API bool Application_Manifest(wchar_t* _filepath, wchar_t* _manifest);
RESCLEAPI_API bool Set_Resource_String(wchar_t* _filepath, wchar_t* _key, wchar_t* _val);
RESCLEAPI_API bool Set_Rcdata(wchar_t* _filepath, wchar_t* _key, wchar_t* _pathToResource);
RESCLEAPI_API bool Get_Resource_String(wchar_t* _filepath, wchar_t* _key, PTCHAR& Val);
#ifdef __cplusplus
}
#endif
需要注意的是添加 UNICODE
宏定义
#ifndef UNICODE
#define UNICODE
#endif
同时为了保证在于Qt数据交互的时候,不会混乱,定义UTF-8编码格式
// 设置 utf-8 编码格式
#if defined(_MSC_VER) && (_MSC_VER >= 1600)
# pragma execution_character_set("utf-8")
#endif
根据 rcedit 中的 main.cc文件的命令行参数调用,封装成WINAPI函数,如:
--get-version-string <key>
获取版本信息对应值
定义为Get_Version_String
函数:
// 获取版本信息 键值 对应值
bool Get_Version_String( wchar_t* filepath, wchar_t* key, PTCHAR& Val)
{
ErrorStr = L"";
ResourceUpdater updater;
if (!updater.Load(filepath))
{
ErrorStr = (std::wstring(L"Unable to load file: ") + std::wstring(filepath)).c_str();
return false;
}
const wchar_t* result = updater.GetVersionString(key);
if (!result)
{
ErrorStr =L"Unable to get version string";
return false;
}
Val = (PTCHAR)result;
return true;
}
--set-version-string <key> <value>
设置软件版本键值对应信息
定义为Set_Version_String
函数:
bool Set_Version_String( wchar_t* _filepath, wchar_t* _key, wchar_t* _val)
{
ResourceUpdater updater;
if (!updater.Load(_filepath))
{
ErrorStr = (std::wstring(L"Unable to load file: ") + std::wstring(_filepath)).c_str();
return false;
}
if (!updater.SetVersionString(_key, _val))
{
ErrorStr = L"Unable to change version string";
return false;
}
if (!updater.Commit())
{
ErrorStr = L"Unable to commit changes";
return false;
}
return true;
}
- 扩展,批量调用
定义Set_Version_String_Batch
函数,批量修改版本信息,
其中文件版本和产品版本信息需要特殊处理。
//! 批量修改版本信息
bool Set_Version_String_Batch( std::wstring filepath, std::map<std::wstring, std::wstring> Mapval)
{
bool IsAllSuccess = true;
ResourceUpdater updater;
if (!updater.Load(filepath.c_str()))
{
ErrorStr = (std::wstring(L"Unable to load file: ") + std::wstring(filepath)).c_str();
return false;
}
for (const auto& keyVal : Mapval)
{
const wchar_t* key = keyVal.first.c_str();
const wchar_t* value = keyVal.second.c_str();
//! 文件版本和产品版本单独处理
if (std::wstring(key) == std::wstring(RU_VS_FILE_VERSION))
{
unsigned short v1, v2, v3, v4;
if (!parse_version_string(value, &v1, &v2, &v3, &v4))
{
ErrorStr = (std::wstring(L"Unable to parse version string for FileVersion:") + std::wstring(value)).c_str();
continue;
}
if (!updater.SetFileVersion(v1, v2, v3, v4))
{
ErrorStr = (std::wstring(L"Unable to change file version:") + std::wstring(value)).c_str();
continue;
}
}
else if (std::wstring(key) == std::wstring(RU_VS_PRODUCT_VERSION))
{
unsigned short v1, v2, v3, v4;
if (!parse_version_string(value, &v1, &v2, &v3, &v4))
{
ErrorStr = (std::wstring(L"Unable to parse version string for ProductVersion:") + std::wstring(value)).c_str();
continue;
}
if (!updater.SetProductVersion(v1, v2, v3, v4))
{
ErrorStr = (std::wstring(L"Unable to change product version:") + std::wstring(value)).c_str();
continue;
}
}
if (!updater.SetVersionString(key, value))
{
ErrorStr = (std::wstring(ErrorStr) + std::wstring(L"Unable to change version string: ") + keyVal.first + std::wstring(L" \r\n ")).c_str();
IsAllSuccess = false;
continue;
}
}
if (!updater.Commit())
{
IsAllSuccess = false;
ErrorStr = L"Unable to commit changes!";
}
return IsAllSuccess;
}
这些代码在main.cc 文件中都有具体实现,这里只是简单整理了一下
右键生成,至此,动态链接库创建完成,
通过 使用Dependencies 下载Dependencies_x86_Release 查看dll中的函数接口,Dependencies能在win10环境下查看DLL调用.
注意:使用x86编译的dll,需要使用Dependencies_x86_Release查看,否则找不到对应函数接口,我甚至因此浪费半天时间不断调整RESCLEAPI_API
的宏定义定义,来判断接口dll中没有显示函数接口的问题。
动态链接库如下图示:
加载动态库
这里加载是通过LoadLibraryExW和GetProcAddress调用函数动态加载的,
如果引入头文件和Lib文件,编译的时候会有各种冲突问题,
为了省事直接动态加载DLL了。
界面只是简单的添加了几个输入框和一个提交按钮,
用Qt来实现界面就比较简单。
如下图示:
自从我发现QFrame控件可以添加阴影后,
那是经常用 QGraphicsDropShadowEffect
来添加一个阴影效果,
因为看起来确实感觉要好看点了,
//! 创建阴影
QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(ui->frame_windows);
shadowEffect->setOffset(0);
shadowEffect->setBlurRadius(10);
shadowEffect->setColor(QColor::fromRgb(0,0,0,60));
ui->frame_windows->setGraphicsEffect(shadowEffect);
界面完成了,接下来就是在Qt环境下动态加载DLL:
首先定义函数类型,声明函数变量:
//! 是否为空的判断
#define isnotnull(_VAL_) (_VAL_!=NULL && _VAL_!=nullptr)
//! 定义函数类型
typedef const wchar_t* (WINAPI *GetErrorStr_W)();
typedef bool (WINAPI *Get_Version_String_W)(wchar_t* filepath, wchar_t* key, PTCHAR& Val);
typedef bool (WINAPI *Get_Version_String_Batch_W)(std::wstring filepath, std::map<std::wstring, std::wstring>& Mapval);
typedef bool (WINAPI *Set_Version_String_Batch_W)(std::wstring filepath, std::map<std::wstring, std::wstring> Mapval);
//! 定义交互的函数接口
class Lib_RescleApi
{
public:
Lib_RescleApi();
GetErrorStr_W pfGetErrorStr=nullptr;
Get_Version_String_W pfGet_Version_String=nullptr;
Get_Version_String_Batch_W pfGet_Version_String_Batch=nullptr;
Set_Version_String_Batch_W pfSet_Version_String_Batch=nullptr;
private:
//! 初始化系统api
void INITWINAPI();
};
//! 定义版本信息对应的键值,
//! 公司名称
#define RU_VS_COMPANY_NAME L"CompanyName"
//! 文件描述
#define RU_VS_FILE_DESCRIPTION L"FileDescription"
//! 文件版本
#define RU_VS_FILE_VERSION L"FileVersion"
//! 内部名称
#define RU_VS_INTERNAL_NAME L"InternalName"
//! 版权信息
#define RU_VS_LEGAL_COPYRIGHT L"LegalCopyright"
//! 商标信息
#define RU_VS_LEGAL_TRADEMARKS L"LegalTrademarks"
//! 原始文件名
#define RU_VS_ORIGINAL_FILENAME L"OriginalFilename"
//! 私有构建信息
#define RU_VS_PRIVATE_BUILD L"PrivateBuild"
//! 产品名称
#define RU_VS_PRODUCT_NAME L"ProductName"
//! 产品版本
#define RU_VS_PRODUCT_VERSION L"ProductVersion"
//! 特殊构建信息
#define RU_VS_SPECIAL_BUILD L"SpecialBuild"
//! 注释
#define RU_VS_COMMENTS L"Comments"
然后初始化类结构时加载rceditPackage.dll
,绑定函数地址指针
Lib_RescleApi::Lib_RescleApi()
{
INITWINAPI();
}
void Lib_RescleApi::INITWINAPI()
{
HMODULE H= NULL;
QString dllpath=QApplication::applicationDirPath()+"/rceditPackage.dll";
const wchar_t* wszLibraryName=utf8_to_wchar(dllpath.toStdString().c_str());
qDebug()<<"DLL: "<<QString::fromWCharArray(wszLibraryName);
if ((H = GetModuleHandleW(wszLibraryName)) != NULL)
goto out;
qDebug()<<"LoadLibraryExW -->";
H = LoadLibraryExW(wszLibraryName, NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
if(H==NULL)
qDebug("Unable to load '%S.dll'", wszLibraryName);
qDebug()<<"Load Function -->";
pfGetErrorStr = (GetErrorStr_W) GetProcAddress(H,"GetErrorStr");
pfGet_Version_String = (Get_Version_String_W) GetProcAddress(H,"Get_Version_String");
pfGet_Version_String_Batch = (Get_Version_String_Batch_W) GetProcAddress(H,"Get_Version_String_Batch");
pfSet_Version_String_Batch = (Set_Version_String_Batch_W) GetProcAddress(H,"Set_Version_String_Batch");
if(pfGetErrorStr==NULL||pfGetErrorStr==nullptr)
qDebug()<<"Not Load GetErrorStr Function!";
if(pfGet_Version_String==NULL||pfGet_Version_String==nullptr)
qDebug()<<"Not Load Get_Version_String Function!";
if(pfGet_Version_String_Batch==NULL||pfGet_Version_String_Batch==nullptr)
qDebug()<<"Not Load Get_Version_String_Batch Function!";
if(pfSet_Version_String_Batch==NULL||pfSet_Version_String_Batch==nullptr)
qDebug()<<"Not Load Set_Version_String_Batch Function!";
out:
// free((LPWSTR)wszLibraryName);
sfree(wszLibraryName);
return;
}
加载后,在界面中简单调用:
QString filepath = QFileDialog::getOpenFileName(this, "选择文件","*","*(*)");
if(filepath!="")
{
ui->lineEdit_Filepath->setText(filepath);
//! 当前文件的有效键值
//! std::map<std::wstring,std::wstring> KayValMap;
KayValMap.clear();
KayValMap.insert(std::make_pair(RU_VS_COMPANY_NAME,L""));
KayValMap.insert(std::make_pair(RU_VS_FILE_DESCRIPTION,L""));
KayValMap.insert(std::make_pair(RU_VS_FILE_VERSION,L""));
KayValMap.insert(std::make_pair(RU_VS_INTERNAL_NAME,L""));
KayValMap.insert(std::make_pair(RU_VS_LEGAL_COPYRIGHT,L""));
KayValMap.insert(std::make_pair(RU_VS_LEGAL_TRADEMARKS,L""));
KayValMap.insert(std::make_pair(RU_VS_ORIGINAL_FILENAME,L""));
KayValMap.insert(std::make_pair(RU_VS_PRIVATE_BUILD,L""));
KayValMap.insert(std::make_pair(RU_VS_PRODUCT_NAME,L""));
KayValMap.insert(std::make_pair(RU_VS_PRODUCT_VERSION,L""));
KayValMap.insert(std::make_pair(RU_VS_SPECIAL_BUILD,L""));
KayValMap.insert(std::make_pair(RU_VS_COMMENTS,L""));
if(isnotnull(Api->pfGet_Version_String_Batch))
{
//! Lib_RescleApi* Api=new Lib_RescleApi();
if(!Api->pfGet_Version_String_Batch(filepath.toStdWString(),KayValMap))
{
qDebug().noquote()<<QString("部分函数[Get_Version_String_Batch]执行失败! \r\n 原因:\r\n %1")
.arg(QString::fromWCharArray(Api->pfGetErrorStr()));
if(KayValMap.size()==0)
{
QMessageBox::warning(this,"提示",QString("函数[Get_Version_String_Batch]执行失败! \r\n 原因:\r\n %1")
.arg(QString::fromWCharArray(Api->pfGetErrorStr())));
}
}
}
else
QMessageBox::warning(this,"提示","未能加载rceditPackage.dll文件中的[Get_Version_String_Batch]函数!");
QList<QLineEdit*> LineEditALL= ui->frame->findChildren<QLineEdit*>();
foreach (QLineEdit* lineEdit, LineEditALL) {
if(isnotnull(lineEdit))
{
if(lineEdit->property("Key").isValid())
{
if(KayValMap.find(lineEdit->property("Key").toString().toStdWString())!=KayValMap.end() && KayValMap.size()>0)
{
lineEdit->setText(QString::fromWCharArray(KayValMap[lineEdit->property("Key").toString().toStdWString()].c_str()));
lineEdit->setEnabled(true);
}
else
{
lineEdit->setText("");
lineEdit->setEnabled(false);
}
}
}
}
}
整体示例技术难度并不高,主要还是费时,也是为了学习Visual Studio生成的动态链接库与Qt开发之间的交互问题,所以整了个示例,平时也是直接移植到Qt静态链接库中调用了。
整个示例源码查看代码仓中的rcedit-qtview项目
或者前往GitCode查看
https://gitcode.com/qq_35554617/rcedit-qtview/overview
exe可执行程序:
https://gitcode.com/qq_35554617/rcedit-qtview/releases/val1.1.1.2