使用 MFC WinInet 封裝類別 (CInternetSession, CStdioFile, CHttpFile) 在工作執行緒 執行下載,避免封鎖 UI 執行緒。主執行緒透過 PostMessage 或控制項通知更新 CProgressCtrl、顯示速度、剩餘時間。支援暫停/繼續/取消、斷點續傳 (HTTP Range 標頭)。
| 類別/函數 | 用途 |
|---|---|
CInternetSession | WinInet 會話管理,建立連線、設定選項 (逾時、Proxy、User-Agent)。 |
CInternetSession::OpenURL() | 開啟 URL 回傳 CStdioFile* (或 CHttpFile*),支援 HTTP/FTP/Gopher。 |
CStdioFile::Read() | 讀取資料區塊 (預設緩衝 4096 bytes)。 |
CFile::Write() | 寫入本地檔案 (CFile::modeCreate | modeWrite | typeBinary)。 |
CStdioFile::SeekToEnd() | 取得遠端檔案總大小 (bytes),用於設定進度條範圍。 |
CProgressCtrl::SetRange32(0, total) | 設定進度條 0~總大小。 |
CProgressCtrl::SetPos(current) | 更新進度。 |
COleDateTime / COleDateTimeSpan | 計算經過時間、下載速度 (KB/s)、預估剩餘時間。 |
// DownloadThread.h
class CDownloadThread : public CWinThread
{
DECLARE_DYNCREATE(CDownloadThread)
protected:
CDownloadThread(); // 動態建立需 protected 建構函式
virtual ~CDownloadThread();
virtual BOOL InitInstance();
virtual int Run(); // 執行緒主函式
virtual void ExitInstance();
public:
// 參數 (主執行緒建立後設定)
CString m_strURL;
CString m_strLocalFile;
HWND m_hWndProgress; // 進度條視窗控制代碼
HWND m_hWndNotify; // 接收進度通知的視窗
UINT m_nMsgUpdate; // 自訂訊息 (WM_USER+1)
BOOL m_bCancel; // 取消旗標
BOOL m_bPause; // 暫停旗標
// 斷點續傳支援
ULONGLONG m_ullDownloaded; // 已下載位元組數
};
// DownloadThread.cpp
IMPLEMENT_DYNCREATE(CDownloadThread, CWinThread)
CDownloadThread::CDownloadThread() : m_bCancel(FALSE), m_bPause(FALSE), m_ullDownloaded(0) {}
BOOL CDownloadThread::InitInstance() { return TRUE; }
int CDownloadThread::Run()
{
CInternetSession session(_T("MyDownloader/1.0"), 1, PRE_CONFIG_INTERNET_ACCESS, NULL, NULL, 0);
session.SetOption(INTERNET_OPTION_CONNECT_TIMEOUT, 10000);
session.SetOption(INTERNET_OPTION_RECEIVE_TIMEOUT, 30000);
CStdioFile* pRemoteFile = NULL;
CFile localFile;
CFileException fe;
ULONGLONG totalSize = 0, current = 0;
BYTE buffer[8192];
UINT nRead;
COleDateTime startTime = COleDateTime::GetCurrentTime();
TRY {
// 斷點續傳:若本地檔存在,設定 Range 標頭
if (m_ullDownloaded > 0 && localFile.Open(m_strLocalFile, CFile::modeRead | CFile::typeBinary, &fe)) {
ULONGLONG existing = localFile.GetLength();
if (existing == m_ullDownloaded) {
CString rangeHeader;
rangeHeader.Format(_T("Range: bytes=%llu-\r\n"), m_ullDownloaded);
session.SetOption(INTERNET_OPTION_HTTP_VERSION, HTTP_VERSION_1_1); // 需 HTTP/1.1
pRemoteFile = session.OpenURL(m_strURL, 1, INTERNET_FLAG_TRANSFER_BINARY | INTERNET_FLAG_RELOAD | INTERNET_FLAG_DONT_CACHE, rangeHeader);
localFile.Close();
localFile.Open(m_strLocalFile, CFile::modeWrite | CFile::modeNoTruncate | CFile::typeBinary, &fe);
localFile.SeekToEnd();
} else {
m_ullDownloaded = 0; // 檔案不一致,重頭下載
}
} else {
pRemoteFile = session.OpenURL(m_strURL, 1, INTERNET_FLAG_TRANSFER_BINARY | INTERNET_FLAG_RELOAD | INTERNET_FLAG_DONT_CACHE);
localFile.Open(m_strLocalFile, CFile::modeCreate | CFile::modeWrite | CFile::typeBinary, &fe);
}
if (!pRemoteFile) AfxThrowInternetException(ERROR_INTERNET_CANNOT_OPEN_FILE);
// 取得總大小 (若支援 Range,需自行計算或從 Content-Range 解析)
// 簡化:假設第一次下載可用 SeekToEnd
if (m_ullDownloaded == 0) {
totalSize = pRemoteFile->SeekToEnd();
pRemoteFile->Seek(0, CFile::begin);
} else {
// 續傳模式下總大小 = 已下載 + 剩餘 (需從 Content-Range 取得)
// 此處略過,實際需解析 HTTP 回應標頭
}
// 通知主執行緒設定進度條範圍
::PostMessage(m_hWndNotify, m_nMsgUpdate, 0, (LPARAM)totalSize); // wParam=0 表設定範圍
current = m_ullDownloaded;
while ((nRead = pRemoteFile->Read(buffer, sizeof(buffer))) > 0) {
// 暫停/取消檢查
while (m_bPause) { Sleep(100); if (m_bCancel) break; }
if (m_bCancel) break;
localFile.Write(buffer, nRead);
current += nRead;
// 更新進度 (每 64KB 或 500ms 通知一次,避免訊息洪流)
static ULONGLONG lastUpdate = 0;
static COleDateTime lastTime = COleDateTime::GetCurrentTime();
COleDateTime now = COleDateTime::GetCurrentTime();
if (current - lastUpdate >= 65536 || (now - lastTime).GetTotalSeconds() >= 0.5) {
::PostMessage(m_hWndNotify, m_nMsgUpdate, 1, (LPARAM)current); // wParam=1 表更新進度
lastUpdate = current;
lastTime = now;
}
}
if (!m_bCancel) {
::PostMessage(m_hWndNotify, m_nMsgUpdate, 2, 0); // wParam=2 表完成
} else {
::PostMessage(m_hWndNotify, m_nMsgUpdate, 3, 0); // wParam=3 表取消
}
}
CATCH(CInternetException, e) {
TCHAR szErr[256];
e->GetErrorMessage(szErr, 255);
::PostMessage(m_hWndNotify, m_nMsgUpdate, 4, (LPARAM)_tcsdup(szErr)); // wParam=4 表錯誤
}
END_CATCH
if (pRemoteFile) { pRemoteFile->Close(); delete pRemoteFile; }
if (localFile.m_hFile != CFile::hFileNull) localFile.Close();
session.Close();
return 0;
}
void CDownloadThread::ExitInstance() { CWinThread::ExitInstance(); }
// MainFrm.h / Dialog.h
#define WM_DOWNLOAD_UPDATE (WM_USER + 1)
afx_msg LRESULT OnDownloadUpdate(WPARAM wParam, LPARAM lParam);
// 訊息對應
ON_MESSAGE(WM_DOWNLOAD_UPDATE, OnDownloadUpdate)
// MainFrm.cpp
LRESULT CMainFrame::OnDownloadUpdate(WPARAM wParam, LPARAM lParam)
{
switch (wParam) {
case 0: // 設定範圍
m_wndProgress.SetRange32(0, (int)lParam);
m_wndProgress.SetPos(0);
break;
case 1: // 更新進度
m_wndProgress.SetPos((int)lParam);
// 計算速度、剩餘時間 (需記錄開始時間、總大小)
UpdateSpeedDisplay(lParam);
break;
case 2: // 完成
MessageBox(_T("下載完成!"));
break;
case 3: // 取消
MessageBox(_T("下載已取消。"));
break;
case 4: // 錯誤
MessageBox((LPCTSTR)lParam, _T("下載錯誤"), MB_ICONERROR);
free((void*)lParam); // _tcsdup 配置的記憶體
break;
}
return 0;
}
void CMainFrame::UpdateSpeedDisplay(ULONGLONG current)
{
static COleDateTime start = COleDateTime::GetCurrentTime();
COleDateTimeSpan elapsed = COleDateTime::GetCurrentTime() - start;
double seconds = elapsed.GetTotalSeconds();
if (seconds < 0.1) return;
double kbReceived = current / 1024.0;
double kbps = kbReceived / seconds;
CString strSpeed;
strSpeed.Format(_T("%.1f KB/s"), kbps);
ULONGLONG total = m_wndProgress.GetRange32(FALSE); // 取最大值
if (total > current && kbps > 0) {
double eta = (total - current) / 1024.0 / kbps;
int mins = (int)eta / 60;
int secs = (int)eta % 60;
CString strETA;
strETA.Format(_T("剩餘 %02d:%02d"), mins, secs);
// 更新狀態列或標籤
}
// 更新速度標籤
}
void CMainFrame::OnDownloadStart()
{
CDownloadThread* pThread = (CDownloadThread*)AfxBeginThread(RUNTIME_CLASS(CDownloadThread), THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED);
pThread->m_strURL = _T("https://example.com/largefile.zip");
pThread->m_strLocalFile = _T("C:\\Downloads\\largefile.zip");
pThread->m_hWndProgress = m_wndProgress.m_hWnd;
pThread->m_hWndNotify = m_hWnd;
pThread->m_nMsgUpdate = WM_DOWNLOAD_UPDATE;
pThread->m_bCancel = FALSE;
pThread->m_bPause = FALSE;
pThread->m_ullDownloaded = 0; // 或讀取現有檔案大小支援續傳
pThread->ResumeThread();
}
void CMainFrame::OnDownloadPause()
{
if (pThread) pThread->m_bPause = !pThread->m_bPause;
// 更新按鈕文字 "暫停" / "繼續"
}
void CMainFrame::OnDownloadCancel()
{
if (pThread) {
pThread->m_bCancel = TRUE;
// 等待執行緒結束 (可選)
// WaitForSingleObject(pThread->m_hThread, 5000);
}
}
MFC WinInet 類別適合維護既有 MFC 專案,新專案建議採用上述現代庫。