You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

创建WinForms控件后PowerShell中WebClient异步下载事件失效原因

PowerShell中WinForms控件导致WebClient异步事件失效的问题及解决

问题描述

我需要在Windows窗体上展示文件下载进度,原本无WinForms控件时,使用WebClient异步下载并通过Register-ObjectEvent订阅事件的代码可以正常运行:

Add-Type -assembly System.Windows.Forms

$isDownloaded = $False
$webMain = New-Object System.Net.WebClient

Register-ObjectEvent -InputObject $webMain -EventName 'DownloadFileCompleted' -SourceIdentifier WebMainDownloadFileCompleted -Action {    
    $Global:isDownloaded = $True
}

Register-ObjectEvent -InputObject $webMain -EventName 'DownloadProgressChanged' -SourceIdentifier WebMainDownloadProgressChanged -Action {
    $Global:Data = $event
}

function DownloadFile($Link, $Path, $Name) {

    write-host "begin"

    $Global:webMain.DownloadFileAsync($Link, $Path)
    
    While (!$isDownloaded) {
        $percent = $Global:Data.SourceArgs.ProgressPercentage
        If ($percent -ne $null) {
            write-host $percent
        }
        Wait-Event -Timeout 1
    }

    write-host "end"
}

DownloadFile 'https://www.7-zip.org/a/7z2301-x64.exe' 'D:\7Zip.exe' '7Zip'

但只要创建WinForms控件(比如$Form1 = New-Object System.Windows.Forms.Form或按钮),DownloadProgressChanged和DownloadFileCompleted事件就完全不触发——文件实际已下载,但Register-ObjectEvent订阅的事件逻辑不再执行。需要解决这个问题并实现带进度条的下载窗体。

原因分析

  • 当创建WinForms控件时,PowerShell会自动初始化WinForms的UI消息循环,这个循环会接管当前线程的执行流程。
  • WebClient的异步事件会被调度到当前的同步上下文(即UI线程),但PowerShell的Register-ObjectEvent依赖自身的运行空间事件队列,UI线程的消息循环不会处理这个队列,导致事件的Action代码块无法执行。
  • 本质是WinForms的消息循环与PowerShell的事件处理机制在同一线程上冲突,异步事件的回调无法触发PowerShell订阅的事件逻辑。

解决方案

放弃使用PowerShell的Register-ObjectEvent,直接将WebClient的事件绑定到WinForms的事件处理方法,让事件回调在UI线程中直接执行,同时通过窗体的消息循环驱动整个流程。

示例代码如下:

Add-Type -AssemblyName System.Windows.Forms

# 创建主窗体
$form = New-Object System.Windows.Forms.Form
$form.Text = "文件下载"
$form.Size = New-Object System.Drawing.Size(400, 150)
$form.StartPosition = "CenterScreen"

# 创建进度条
$progressBar = New-Object System.Windows.Forms.ProgressBar
$progressBar.Location = New-Object System.Drawing.Point(20, 20)
$progressBar.Size = New-Object System.Drawing.Size(340, 25)
$progressBar.Maximum = 100
$form.Controls.Add($progressBar)

# 创建状态标签
$statusLabel = New-Object System.Windows.Forms.Label
$statusLabel.Location = New-Object System.Drawing.Point(20, 60)
$statusLabel.Size = New-Object System.Drawing.Size(340, 20)
$statusLabel.Text = "准备下载..."
$form.Controls.Add($statusLabel)

# 创建WebClient实例
$webClient = New-Object System.Net.WebClient

# 绑定下载进度变更事件
$webClient.add_DownloadProgressChanged({
    param($sender, $e)
    $progressBar.Value = $e.ProgressPercentage
    $statusLabel.Text = "已下载 $($e.ProgressPercentage)% ($($e.BytesReceived)/$($e.TotalBytesToReceive) 字节)"
})

# 绑定下载完成事件
$webClient.add_DownloadFileCompleted({
    param($sender, $e)
    if ($e.Error -ne $null) {
        $statusLabel.Text = "下载失败: $($e.Error.Message)"
    }
    else {
        $statusLabel.Text = "下载完成!"
    }
    # 允许关闭窗体
    $form.Enabled = $true
})

# 启动下载
$downloadLink = "https://www.7-zip.org/a/7z2301-x64.exe"
$savePath = "D:\7Zip.exe"
$statusLabel.Text = "开始下载..."
$form.Enabled = $false # 下载过程中禁用窗体
$webClient.DownloadFileAsync([Uri]$downloadLink, $savePath)

# 启动窗体消息循环
$form.ShowDialog() | Out-Null

# 清理资源
$webClient.Dispose()
$form.Dispose()

代码说明

  1. 直接使用.add_DownloadProgressChanged()和.add_DownloadFileCompleted()方法绑定事件,事件处理逻辑直接操作WinForms控件,无需依赖PowerShell全局变量或事件队列。
  2. 窗体通过ShowDialog()启动消息循环,这个循环会处理所有UI事件和WebClient的异步事件回调。
  3. 下载过程中禁用窗体防止误操作,下载完成后恢复状态。

内容的提问来源于stack exchange,提问作者Dron

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 21:15:54