URLDownloadToFile 下载的URL文件不能通过ShellExecute多次打开问题及解决

本文探讨了使用URLDownloadToFile下载文件并通过ShellExecute打开的问题。当在同一进程中重复操作时会出现SE_ERR_OOM错误,实际与内存无关。解决方法是将下载和打开文件的操作分配到不同进程中。

今天开发遇到一个问题。

URLDownloadToFile  下载的URL文件只能通过ShellExecute打开一次, 

再次下载并改名(不改名也不行,改名是为了说明不是下载问题)调用ShellExecute会返回错误如下:

SE_ERR_OOM  
There was not enough memory to complete the operation

只要在一个进程中调用就会出现这个问题,其实与内存无关。

主要代码如下:

static int s_iIndex = 0;
#define BUFSIZE 512

#include <urlmon.h>
#pragma comment(lib, "urlmon.lib") 

#include <Wininet.h>
#pragma comment(lib, "Wininet.lib") 

char* OpenUrlFileEx( char* _cstrUrl )
{

    DWORD dwRetVal = 0;
    UINT uRetVal = 0;

    char lpPathBuffer[BUFSIZE] = {0};
    DWORD dwBufSize=BUFSIZE;
    // 获取临时文件路径
    dwRetVal = GetTempPath(dwBufSize,     // length of the buffer
        lpPathBuffer); // buffer for path 
    if (dwRetVal > dwBufSize || (dwRetVal == 0))
    {
        printf ("dfb GetTempPath failed with error %d.\n", GetLastError());                
        return NULL;
    }


    char *cFileName = strrchr(_cstrUrl, '\\');

    if (cFileName == NULL)
    {
        cFileName = strrchr(_cstrUrl, '/');
        cFileName = cFileName+1;
    }
    if (cFileName != NULL)
    {

        char cBuf[BUFSIZE] = {0};
        sprintf(cBuf,"%sServerTmp%d_%s",lpPathBuffer,s_iIndex++,cFileName);
  

        if (S_OK == URLDownloadToFile(NULL, _cstrUrl, cBuf, NULL,NULL) )
        {
            return cBuf;
        }
        else
        {
            //失败
            printf("dfb URLDownloadToFile error %d.  \n", 
                GetLastError());
        }
    }
    else
    {
        printf("dfb URLDownloadToFile error can't find file.\n");
    }
    return "";
}


void CtestdoDlg::OnBnClickedButton1()
{
    char *pFile = OpenUrlFileEx("http://10.3.241.71:8088/cloud/111.doc");

    HINSTANCE hInstance = NULL;
    hInstance = ShellExecute(NULL, "open", pFile, NULL, NULL, SW_SHOW);
    if((int)hInstance > 31)
    {
        CloseHandle(hInstance);

    }
    else
    {
        TRACE("ERROR %d",hInstance);            
    }
}
 

最后解决办法是将下载与打开放在两个进程中解决了这个问题。
<think>我们需要实现一个WPF程序,在启动时检查系统是否安装了.NET Desktop Runtime。如果没有安装,则自动下载并安装它。 步骤: 1. 检查是否安装.NET Desktop Runtime 2. 如果没有安装,下载对应的安装程序 3. 运行安装程序,并等待安装完成 4. 然后启动我们的WPF应用程序 然而,需要注意的是,我们的应用程序本身依赖于.NET Desktop Runtime,所以如果没有安装,我们的应用程序将无法运行。 因此,通常的做法是使用一个独立的引导程序(Bootstrapper)来执行检查。这个引导程序可以是原生的(如C++)或者使用其他不依赖.NET的环境(如批处理文件)来编写。 但是,如果我们希望用.NET来编写引导程序,那么它必须能够在没有安装.NET的环境下运行。这通常是不可能的,除非我们使用一个独立的可执行文件(例如,使用自包含部署的引导程序,但这个引导程序本身不依赖运行时,或者使用其他技术)。 因此,更常见的做法: 方案1:使用安装包(如MSI安装包)来检查依赖并安装。我们可以使用第三方工具(如WiX Toolset)创建安装包,在安装包中设置.NET运行时为依赖项。 方案2:编写一个简单的引导程序(使用非托管代码,如C++)来检查并安装运行时,然后启动WPF应用。 方案3:使用批处理脚本或PowerShell脚本作为启动入口。 但是,用户要求的是在WPF程序启动时检查,这意味着我们可能希望将检查逻辑放在WPF应用程序中。然而,如果运行时不存在,WPF程序根本无法启动。 因此,我们需要将检查逻辑放在WPF应用程序启动之前。我们可以创建一个简单的启动器(Launcher)应用程序,这个启动器可以是控制台应用程序(使用自包含部署,这样它不需要在目标机器上安装运行时),由它来检查目标机器是否安装了所需的.NET运行时,如果没有,则下载并安装,然后启动WPF应用程序。 具体步骤: 1. 创建一个新的控制台项目(例如Launcher),将其发布为自包含(self-contained)应用程序(包含运行时),这样它可以在没有安装.NET的机器上运行。 2. 在Launcher程序中,检查是否安装了所需的.NET Desktop Runtime版本。 3. 如果没有安装,则下载安装程序并运行。 4. 安装完成后,启动WPF应用程序。 但是,自包含的Launcher可能会比较大,因为它包含了整个运行时。另一种做法是使用一个非常小的非托管程序(如C++写的)作为启动器。 这里,我们考虑使用C++编写一个简单的启动器,因为它不需要任何运行时,体积小。 使用C++编写启动器检查.NET运行时的存在并安装: 步骤: 1. 检查注册表:.NET Desktop Runtime的版本信息通常记录在Windows注册表中。我们可以检查注册键:`HKEY_LOCAL_MACHINE\SOFTWARE\dotnet\Setup\InstalledVersions\{Architecture}\sharedhost\Version` 或者检查共享框架的路径。但是更直接的是检查共享框架的安装情况。 对于.NET 5及更高版本,我们可以检查注册表路径:`HKEY_LOCAL_MACHINE\SOFTWARE\dotnet\Setup\InstalledVersions\{Architecture}\sharedfx\Microsoft.WindowsDesktop.App` 其中{Architecture}可以是x86、x64或arm64。 2. 如果没有找到所需的版本,则下载运行时安装程序(例如,从Microsoft官方下载链接)。 3. 运行安装程序(静默安装,使用参数`/install /quiet /norestart`等)。 4. 等待安装完成,然后启动WPF应用程序。 注意:需要管理员权限来安装运行时。 因此,启动器可能需要以管理员权限运行。 下面是一个简单的C++启动器示例代码(仅提供思路,可能需要根据实际情况调整): 注意:此代码未经验证,可能需要根据实际需求修改。 我们可以使用Windows API来检查注册表,并使用ShellExecute来运行安装程序。 但是,由于用户要求的是在WPF程序启动时检查,而WPF程序本身无法在没有运行时的情况下启动,所以我们必须使用上述的启动器方法。 另一种替代方案:将WPF应用程序本身发布为自包含(self-contained),这样就不需要用户安装运行时。但这样应用程序体积会比较大。 考虑到用户的需求是检查并安装,我们采用启动器方案。 接下来,我们将描述如何使用C++编写一个简单的启动器: 步骤1:创建C++控制台项目(在Visual Studio中)。 步骤2:编写代码。 示例代码: ```cpp #include <Windows.h> #include <string> #include <winreg.h> #include <shellapi.h> #include <shlwapi.h> #include <iostream> // 检查是否安装了指定版本的.NET Desktop Runtime // 参数:versionMajor, versionMinor, versionPatch 是我们需要的最低版本 bool IsDotNetDesktopRuntimeInstalled(int versionMajor, int versionMinor, int versionPatch) { HKEY hKey; // 注意:这里检查64位系统下的64位运行时,如果是32位系统,则检查Wow6432Node下的路径 // 我们假设目标机器是64位系统,且需要安装x64运行时 LPCWSTR subKey = L"SOFTWARE\\dotnet\\Setup\\InstalledVersions\\x64\\sharedfx\\Microsoft.WindowsDesktop.App"; if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, subKey, 0, KEY_READ, &hKey) != ERROR_SUCCESS) { // 如果打开失败,可能是没有安装 return false; } // 枚举所有的值,因为可能有多个版本安装 WCHAR valueName[1024]; DWORD valueNameSize, valueType; BYTE data[1024]; DWORD dataSize; DWORD index = 0; bool found = false; while (ERROR_SUCCESS == RegEnumValue(hKey, index, valueName, &(valueNameSize = 1024), NULL, &valueType, data, &(dataSize = 1024))) { // 值名称是版本号,例如"5.0.0" // 我们只需要检查版本是否大于等于我们需要的版本 // 这里简单起见,我们只检查一个特定的版本,或者我们可以解析版本字符串进行比较 // 实际上,我们应该解析版本字符串并比较 std::wstring versionStr(valueName); // 将版本字符串解析为主版本、次版本、修订版本 int major, minor, patch; if (swscanf_s(versionStr.c_str(), L"%d.%d.%d", &major, &minor, &patch) == 3) { if (major > versionMajor || (major == versionMajor && minor > versionMinor) || (major == versionMajor && minor == versionMinor && patch >= versionPatch)) { found = true; break; } } index++; } RegCloseKey(hKey); return found; } // 下载文件 bool DownloadFile(const std::wstring& url, const std::wstring& savePath) { // 使用URLDownloadToFile函数 HRESULT hr = URLDownloadToFile(NULL, url.c_str(), savePath.c_str(), 0, NULL); return hr == S_OK; } int main() { // 检查是否安装了.NET 5.0.0或更高版本的Desktop Runtime if (!IsDotNetDesktopRuntimeInstalled(5, 0, 0)) { // 下载运行时安装程序 std::wstring downloadUrl = L"https://download.visualstudio.microsoft.com/download/pr/12345678-90ab-cdef-1234-567890abcdef/windowsdesktop-runtime-5.0.0-win-x64.exe"; // 替换为实际的下载链接 std::wstring savePath = L"windowsdesktop-runtime-installer.exe"; if (DownloadFile(downloadUrl, savePath)) { // 运行安装程序,需要管理员权限 SHELLEXECUTEINFO sei = { sizeof(sei) }; sei.lpVerb = L"runas"; // 请求管理员权限 sei.lpFile = savePath.c_str(); sei.lpParameters = L"/install /quiet /norestart"; // 静默安装,不重启 sei.hwnd = NULL; sei.nShow = SW_HIDE; if (!ShellExecuteEx(&sei)) { DWORD err = GetLastError(); // 处理错误 return 1; } // 等待安装完成 WaitForSingleObject(sei.hProcess, INFINITE); CloseHandle(sei.hProcess); // 安装完成后,删除安装程序 DeleteFile(savePath.c_str()); } else { // 下载失败 return 1; } } // 启动WPF应用程序 // 假设我们的WPF应用程序名为MyWpfApp.exe SHELLEXECUTEINFO sei = { sizeof(sei) }; sei.lpFile = L"MyWpfApp.exe"; sei.hwnd = NULL; sei.nShow = SW_SHOW; if (!ShellExecuteEx(&sei)) { DWORD err = GetLastError(); return 1; } return 0; } ``` 注意:上述代码是一个示例,实际使用时需要: 1. 替换下载链接为实际需要的.NET Desktop Runtime版本的正确链接。 2. 处理不同的架构(x86, x64, arm64)。 3. 错误处理可能需要更完善。 4. 版本比较可能需要更健壮(例如,处理预览版等)。 5. 在安装运行时后,可能需要重新启动系统,但这里使用/norestart参数避免重启,但某些情况下可能还是需要重启。如果运行时安装后要求重启,则我们的应用程序可能无法立即启动。因此,可能需要提示用户重启,或者由启动器在安装后重启计算机,然后再次启动应用程序。 然而,根据Microsoft的文档,静默安装运行时通常不需要重启,除非有系统更新冲突等情况。 因此,我们可以先尝试不重启,如果应用程序启动失败,再提示用户重启。 另外,我们还可以使用一个更简单的方法:使用PowerShell脚本作为启动器。 使用PowerShell脚本的好处是,Windows系统自带PowerShell,我们不需要额外部署。但是,PowerShell的执行策略可能会阻止脚本运行。 这里给出一个PowerShell脚本的示例: ```powershell # 检查是否安装了.NET 5.0 Desktop Runtime $requiredVersion = [Version]"5.0.0" $installed = $false # 检查64位系统 $regPath = "HKLM:\SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App" if (Test-Path $regPath) { $versions = Get-ItemProperty -Path $regPath | Select-Object -ExpandProperty Property foreach ($version in $versions) { $ver = [Version]$version if ($ver -ge $requiredVersion) { $installed = $true break } } } if (-not $installed) { # 下载安装程序 $url = "https://download.visualstudio.microsoft.com/download/pr/12345678-90ab-cdef-1234-567890abcdef/windowsdesktop-runtime-5.0.0-win-x64.exe" $output = "$env:TEMP\windowsdesktop-runtime-installer.exe" Invoke-WebRequest -Uri $url -OutFile $output # 运行安装程序(需要管理员权限) Start-Process -FilePath $output -ArgumentList "/install /quiet /norestart" -Verb RunAs -Wait # 删除安装程序 Remove-Item $output } # 启动应用程序 Start-Process -FilePath "MyWpfApp.exe" ``` 这个脚本需要保存为.ps1文件。但是,默认情况下,PowerShell不允许执行脚本。我们可以通过设置执行策略来允许,或者在命令行中使用`powershell -ExecutionPolicy Bypass -File script.ps1`来运行。 因此,我们可以创建一个批处理文件(.bat)来调用这个PowerShell脚本: ```batch @echo off PowerShell -ExecutionPolicy Bypass -Command "& '%~dpn0.ps1'" ``` 这样,我们只需要提供两个文件:launcher.bat和launcher.ps1。 但是,这种方法需要用户允许执行脚本,并且可能因为安全策略而受限。 综合考虑,使用C++编写的启动器可能更可靠,但开发相对复杂。而PowerShell脚本更简单,但可能受限于执行策略。 因此,我们可以选择使用C++启动器,并将它作为应用程序的主要入口。 在WPF项目中,我们可以这样组织: 1. 编译C++启动器(Launcher.exe)。 2. 将Launcher.exe和WPF应用程序(MyWpfApp.exe)放在同一个目录。 3. 用户运行Launcher.exe,由它检查运行时并启动MyWpfApp.exe。 这样,我们就不需要修改WPF应用程序的代码。 总结:我们推荐使用C++启动器,因为它不受PowerShell执行策略的限制,并且更接近原生,不需要额外依赖。 但是,请注意:如果用户没有安装C++运行库,那么使用C++编写的启动器可能需要静态链接C++运行库(/MT),这样就不会依赖VC++运行时。 在Visual Studio中设置: 项目属性 -> 配置属性 -> C/C++ -> 代码生成 -> 运行库:选择“多线程(/MT)”。 这样,编译出的可执行文件就可以在没有安装VC++运行库的机器上运行。 因此,我们按照这个思路来实现。 最后,我们需要在WPF应用程序的开发过程中,将启动器项目作为前置项目,并确保在发布时包含启动器可执行文件。 注意:下载安装程序时,需要确保下载链接是有效的,并且是最新的。我们可以从Microsoft官方网站获取最新的下载链接。 另外,我们也可以将运行时安装程序与我们的应用程序一起打包,这样就不需要下载,但是这样会增加安装包的大小。如果选择打包,则不需要下载步骤,直接运行安装程序即可。 因此,我们可以将运行时安装程序(例如,windowdesktop-runtime-5.0.0-win-x64.exe)放在应用程序目录中,然后由启动器直接运行它。 这样,启动器的代码可以简化为: ```cpp // ... 省略注册表检查代码 ... // 如果未安装,则运行同目录下的安装程序 std::wstring installerPath = L".\\windowsdesktop-runtime-5.0.0-win-x64.exe"; // 检查安装程序是否存在 if (PathFileExists(installerPath.c_str())) { // 运行安装程序 SHELLEXECUTEINFO sei = { sizeof(sei) }; sei.lpVerb = L"runas"; sei.lpFile = installerPath.c_str(); sei.lpParameters = L"/install /quiet /norestart"; sei.hwnd = NULL; sei.nShow = SW_HIDE; if (ShellExecuteEx(&sei)) { WaitForSingleObject(sei.hProcess, INFINITE); CloseHandle(sei.hProcess); } } else { // 安装程序不存在,无法安装 // 可以提示用户手动安装 return 1; } ``` 这样,我们就不需要下载,但需要确保安装程序存在。 因此,在发布时,我们需要将运行时安装程序与启动器和WPF应用程序一起打包。 这种方法的好处是安装过程不需要网络连接,但增加了分发文件的大小。 用户可以根据需要选择下载方式还是打包方式。 以上就是实现方法。 注意:在实际应用中,我们可能还需要考虑安装程序的版本更新,以及处理不同架构(x86/x64)的问题。我们可以根据目标平台选择对应的运行时安装程序。 因此,启动器可能需要检查操作系统的位数(32位或64位)并选择相应的安装程序。 对于32位操作系统,我们使用x86的运行时;对于64位操作系统,通常使用x64的运行时。但WPF应用程序的生成平台也要与之匹配。 在启动器中,我们可以使用以下代码来检测操作系统位数: ```cpp BOOL is64bit; IsWow64Process(GetCurrentProcess(), &is64bit); // 如果运行在64位系统上,32位进程会返回TRUE,64位进程返回FALSE // 或者直接使用: SYSTEM_INFO systemInfo; GetNativeSystemInfo(&systemInfo); if (systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64) { // 64位系统 } else if (systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_INTEL) { // 32位系统 } ``` 然后根据系统架构选择注册表路径和安装程序。 综上所述,这是一个相对复杂的任务,需要根据具体情况调整。 在WPF应用程序中,我们无法直接实现启动时检查(因为运行时不存在就无法启动),因此必须使用启动器。 希望这个回答能帮助您。 §§相关问题§§ 1. 如何为WPF应用程序创建安装包并自动安装.NET运行时? 2. 如何检测系统中安装的.NET运行时版本? 3. 如何在没有管理员权限的情况下安装.NET运行时? </think> ### WPF程序启动时检查并安装.NET Desktop Runtime的实现方法 要实现WPF程序启动时自动检查并安装.NET Desktop Runtime,核心思路是使用一个**独立的引导程序(Bootstrapper)**。由于WPF程序本身依赖.NET运行时,无法在未安装运行时的环境中执行,因此需要一个不依赖.NET的引导程序(如C++或批处理)进行前置检查。以下是详细实现方案: --- ### 方案1:C++引导程序(推荐) 创建一个原生C++引导程序(不依赖.NET),负责检查运行时并安装: ```cpp #include <Windows.h> #include <string> #include <winreg.h> #include <shlwapi.h> // 检查.NET Desktop Runtime是否安装 bool IsRuntimeInstalled(const wchar_t* version) { HKEY hKey; wchar_t regPath[256]; swprintf_s(regPath, L"SOFTWARE\\dotnet\\Setup\\InstalledVersions\\x64\\sharedfx\\Microsoft.WindowsDesktop.App"); if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, regPath, 0, KEY_READ, &hKey) != ERROR_SUCCESS) return false; DWORD valueType; wchar_t versionBuf[32]; DWORD bufSize = sizeof(versionBuf); bool installed = (RegQueryValueEx(hKey, version, NULL, &valueType, (LPBYTE)versionBuf, &bufSize) == ERROR_SUCCESS); RegCloseKey(hKey); return installed; } // 静默安装运行时 void InstallRuntime(const wchar_t* installerPath) { SHELLEXECUTEINFO sei = { sizeof(sei) }; sei.lpVerb = L"runas"; // 请求管理员权限 sei.lpFile = installerPath; sei.lpParameters = L"/install /quiet /norestart"; sei.nShow = SW_HIDE; ShellExecuteEx(&sei); WaitForSingleObject(sei.hProcess, INFINITE); CloseHandle(sei.hProcess); } int main() { const wchar_t* REQUIRED_VERSION = L"6.0.0"; // 所需运行时版本 const wchar_t* INSTALLER_PATH = L".\\windowsdesktop-runtime-6.0.0-win-x64.exe"; // 安装程序路径 if (!IsRuntimeInstalled(REQUIRED_VERSION)) { if (PathFileExists(INSTALLER_PATH)) { InstallRuntime(INSTALLER_PATH); } else { // 处理安装程序不存在的情况 MessageBox(NULL, L"请手动安装.NET Desktop Runtime", L"错误", MB_ICONERROR); return 1; } } // 启动WPF应用 ShellExecute(NULL, L"open", L"YourWpfApp.exe", NULL, NULL, SW_SHOW); return 0; } ``` **关键步骤**: 1. 编译C++程序时使用**静态链接**(`/MT`选项),避免依赖VC++运行时 2. 将运行时安装程序(如`windowsdesktop-runtime-6.0.x-win-x64.exe`)与引导程序一起分发 3. 引导程序检查注册表路径: `HKEY_LOCAL_MACHINE\SOFTWARE\dotnet\Setup\InstalledVersions\{架构}\sharedfx\Microsoft.WindowsDesktop.App` 4. 使用管理员权限静默安装:`/install /quiet /norestart`参数[^4] --- ### 方案2:PowerShell脚本(无需编译) 通过PowerShell脚本实现检查与安装: ```powershell # bootstrap.ps1 $requiredVersion = "6.0.0" $installerPath = ".\windowsdesktop-runtime-6.0.0-win-x64.exe" # 检查运行时版本 $isInstalled = Get-ChildItem 'HKLM:\SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App' | Get-ItemProperty | Select-Object -ExpandProperty PSObject.Properties | Where-Object { $_.Value -eq $requiredVersion } | Measure-Object | Select-Object -ExpandProperty Count -gt 0 if (-not $isInstalled) { if (Test-Path $installerPath) { Start-Process $installerPath -ArgumentList "/install /quiet /norestart" -Verb RunAs -Wait } else { Write-Host ".NET安装程序缺失" exit 1 } } Start-Process "YourWpfApp.exe" ``` **调用方式**(创建批处理文件): ```batch @echo off PowerShell -ExecutionPolicy Bypass -File "bootstrap.ps1" ``` --- ### 方案3:WiX安装包集成 通过WiX Toolset创建MSI安装包,自动处理依赖: ```xml <!-- 在WiX项目中添加 --> <PackageGroup Id="NetRuntime"> <ExePackage Id="NetDesktopRuntime" DisplayName=".NET Desktop Runtime" SourceFile="windowsdesktop-runtime-6.0.0-win-x64.exe" InstallCommand="/install /quiet /norestart" DetectCondition="NetDesktopRuntimeVersion >= 6.0.0" /> </PackageGroup> ``` **优势**:专业安装流程,自动回滚机制,符合Windows安装规范[^2]。 --- ### 注意事项 1. **管理员权限**:运行时安装需要管理员权限,引导程序需请求提升权限 2. **版本匹配**:确保安装的运行时版本与WPF项目的目标框架一致 3. **离线部署**:建议将安装程序与应用捆绑分发,避免网络依赖 4. **静默安装**:使用`/quiet`参数避免用户交互,`/norestart`阻止不必要重启 5. **架构处理**:区分x86/x64系统,动态选择注册表路径和安装程序[^1] > ⚠️ **重要**:WPF主程序无法直接实现此功能,必须通过外部引导程序。C++方案兼容性最佳,PowerShell方案适用于简单场景,WiX方案适合专业分发[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值