项目中由于一些原因,需要在单独线程中创建一个包含ProgressCtrl进度条的对话框用于显示操作进度,因此想到利用MFC的UI线程来实现。虽然想法简单,但实现起来还是颇费了一番周折,捣鼓了一阵子,算是找到了一个比较简单的创建UI线程的方法,现记录下来。
所谓UI线程,无非就是一个比普通Work线程多一个消息循环的线程。再延伸一下,想到MFC的对话框工程自动创建的也应该就是一个UI线程而已。因此创建UI线程无需大费周折,直接使用VS2013,新建一个最简单的MFC对话框工程,命名为ProgressCtrlThread,自动生成的主线程代码如下:
主程序头文件、类定义:
// ProgressCtrlThread.h : PROJECT_NAME 应用程序的主头文件
//
#pragma once
#ifndef __AFXWIN_H__
#error "在包含此文件之前包含“stdafx.h”以生成 PCH 文件"
#endif
#include "resource.h" // 主符号
// CProgressCtrlThreadApp:
// 有关此类的实现,请参阅 ProgressCtrlThread.cpp
//
class CProgressCtrlThreadApp : public CWinApp
{
public:
CProgressCtrlThreadApp();
// 重写
public:
virtual BOOL InitInstance();
// 实现
DECLARE_MESSAGE_MAP()
};
extern CProgressCtrlThreadApp theApp;
主程序源文件、类实现:
// ProgressCtrlThread.cpp : 定义应用程序的类行为。
//
#include "stdafx.h"
#include "ProgressCtrlThread.h"
#include "ProgressCtrlThreadDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// CProgressCtrlThreadApp
BEGIN_MESSAGE_MAP(CProgressCtrlThreadApp, CWinApp)
ON_COMMAND(ID_HELP, &CWinApp::OnHelp)
END_MESSAGE_MAP()
// CProgressCtrlThreadApp 构造
CProgressCtrlThreadApp::CProgressCtrlThreadApp()
{
// 支持重新启动管理器
m_dwRestartManagerSupportFlags = AFX_RESTART_MANAGER_SUPPORT_RESTART;
// TODO: 在此处添加构造代码,
// 将所有重要的初始化放置在 InitInstance 中
}
// 唯一的一个 CProgressCtrlThreadApp 对象
CProgressCtrlThreadApp theApp;
// CProgressCtrlThreadApp 初始化
BOOL CProgressCtrlThreadApp::InitInstance()
{
// 如果一个运行在 Windows XP 上的应用程序清单指定要
// 使用 ComCtl32.dll 版本 6 或更高版本来启用可视化方式,
//则需要 InitCommonControlsEx()。 否则,将无法创建窗口。
INITCOMMONCONTROLSEX InitCtrls;
InitCtrls.dwSize = sizeof(InitCtrls);
// 将它设置为包括所有要在应用程序中使用的
// 公共控件类。
InitCtrls.dwICC = ICC_WIN95_CLASSES;
InitCommonControlsEx(&InitCtrls);
CWinApp::InitInstance();
AfxEnableControlContainer();
// 创建 shell 管理器,以防对话框包含
// 任何 shell 树视图控件或 shell 列表视图控件。
CShellManager *pShellManager = new CShellManager;
// 激活“Windows Native”视觉管理器,以便在 MFC 控件中启用主题
CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows));
// 标准初始化
// 如果未使用这些功能并希望减小
// 最终可执行文件的大小,则应移除下列
// 不需要的特定初始化例程
// 更改用于存储设置的注册表项
// TODO: 应适当修改该字符串,
// 例如修改为公司或组织名
SetRegistryKey(_T("应用程序向导生成的本地应用程序"));
CProgressCtrlThreadDlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: 在此放置处理何时用
// “确定”来关闭对话框的代码
}
else if (nResponse == IDCANCEL)
{
// TODO: 在此放置处理何时用
// “取消”来关闭对话框的代码
}
else if (nResponse == -1)
{
TRACE(traceAppMsg, 0, "警告: 对话框创建失败,应用程序将意外终止。\n");
TRACE(traceAppMsg, 0, "警告: 如果您在对话框上使用 MFC 控件,则无法 #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS。\n");
}
// 删除上面创建的 shell 管理器。
if (pShellManager != NULL)
{
delete pShellManager;
}
// 由于对话框已关闭,所以将返回 FALSE 以便退出应用程序,
// 而不是启动应用程序的消息泵。
return FALSE;
}
对话框程序创建完成,下面要做的就是将其修改为UI线程的格式。
主要工作包括:
1. 将自动生成的主线程ProgressCtrlThread.h文件和ProgressCtrlThread.cpp文件直接复制到需要创建UI线程的工程文件夹内,并加入工程
2. h文件中删除无用内容
3. cpp文件中删除UI线程无关内容
4. 将父类从CWinApp修改为CWinThread
5. 线程间通信需要使用消息,在本线程类的MESSAGE_MAP中应使用ON_THREAD_MESSAGE,而不是ON_MESSAGE,并且在主程序中向本UI线程发送消息时应使用PostThreadMessage()函数
6. 在h文件中加入宏DECLARE_DYNCREATE(CProgressCtrlThread),在cpp文件中加入宏IMPLEMENT_DYNCREATE(CProgressCtrlThread, CWinThread)
7. 在工程资源中创建一个名为IDD_PROGRESSCTRLTHREAD_DIALOG对话框,并加入ProgressCtrl控件,然后创建与之绑定的类CProgressCtrlThreadDlg
8. 主程序中开启UI线程,使用CWinThread* pThread = AfxBeginThread(RUNTIME_CLASS(CProgressCtrlThread))即可
修改后的h文件:
// ProgressCtrlThread.h : 进度条对话框UI线程头文件
//
#pragma once
#include "resource.h"
class CProgressCtrlThread : public CWinThread
{
DECLARE_DYNCREATE(CProgressCtrlThread) //声明此线程会被动态创建为UI线程
public:
CProgressCtrlThread();
// 重写
public:
virtual BOOL InitInstance();
// 实现
DECLARE_MESSAGE_MAP()
protected:
//注意是线程间消息传递返回void而不是afx_msg LRESULT
void OnThreadMessages(WPARAM wParam, LPARAM lParam);
};
修改后cpp文件:
// ProgressCtrlThread.h : 进度条对话框UI线程实现文件
//
#include "stdafx.h"
#include "ProgressCtrlThread.h"
#include "ProgressCtrlThreadDlg.h"
IMPLEMENT_DYNCREATE(CProgressCtrlThread, CWinThread)
用于与UI线程(而不是其中的对话框)直接通信的自定义消息
#define WM_THREAD_MESSAGES WM_USER + 99
BEGIN_MESSAGE_MAP(CProgressCtrlThread, CWinThread)
//UI线程的消息应在这里映射!而不是在CDialogEx对话框类中映射
ON_THREAD_MESSAGE(WM_THREAD_MESSAGES, &CProgressCtrlThread::OnThreadMessages)
END_MESSAGE_MAP()
CProgressCtrlThread::CProgressCtrlThread()
{
// TODO: 在此处添加构造代码,
// 将所有重要的初始化放置在 InitInstance 中
}
BOOL CProgressCtrlThread::InitInstance()
{
CWinThread::InitInstance(); //先启用线程的消息循环
CProgressCtrlThreadDlg pctdlg;
m_pMainWnd = &pctdlg;
INT_PTR nResponse = pctdlg.DoModal();
if (nResponse == IDOK)
{
// TODO: 在此放置处理何时用
// “确定”来关闭对话框的代码
}
else if (nResponse == IDCANCEL)
{
// TODO: 在此放置处理何时用
// “取消”来关闭对话框的代码
}
// 由于对话框已关闭,所以将返回 FALSE 以便退出此UI线程,
// 同时将会关闭线程的消息泵。
// 如果返回TRUE,则仅仅退出此函数,而不会关闭线程的消息循环
return FALSE;
}
//本UI线程的自定义消息相应函数,注意线程间消息传递返回void而不是afx_msg LRESULT
void CProgressCtrlThread::OnThreadMessages(WPARAM wParam, LPARAM lParam)
{
//获取进度条对话框指针
CProgressCtrlThreadDlg* pdlg = ((CProgressCtrlThreadDlg*)GetMainWnd());
int flag = (int)wParam;
int c = (int)lParam;
switch (flag)
{
case 1:
pdlg->m_Progress.SetRange(0, c); //设置ProgressCtrl的总范围
pdlg->m_Progress.SetFocus(); //防止默认焦点落在按钮上而误响应回车键
pdlg->m_StaticText.SetWindowText(_T("数据读取中,请稍后..."));
break;
case 2:
c *= 1.2; //显示速度加快20%
pdlg->m_Progress.SetPos(c); //设置进度位置
break;
case 3:
pdlg->m_Progress.SetPos(c); //设置为满进度状态
pdlg->EndDialog(IDCANCEL); //退出此对话框
break;
}
default:
break;
}
return;
}
其中对话框中进度条控件m_Progress的控制是通过UI线程外发送线程消息来实现的,主线程代码片段如下所示,主要是ODBC方式操作CRecordset数据库,将数据库数据提取出来,同时在进度条对话框中实时显示读取进度:
//主线程代码片段
... //非关键代码省略
int iDataCount = 0; //数据库存储的条目总数
while (!m_AwsDatabase.IsEOF())
{
m_AwsDatabase.MoveNext();
}
iDataCount = m_AwsDatabase.GetRecordCount();
m_AwsDatabase.MoveFirst(); //重新指回第一个数据
//创建ProgressCtrl的UI线程,用于显示处理进度条
CWinThread* pThread = AfxBeginThread(RUNTIME_CLASS(CProgressCtrlThread));
//向UI线程发送自定义消息,设置ProgressCtrl进度条的Range
pThread->PostThreadMessage(WM_THREAD_MESSAGES, (WPARAM)1, (LPARAM)iDataCount);
//读取数据库
int iRow = 0;
while (!m_AwsDatabase.IsEOF())
{
... //非关键代码省略
m_AwsDatabase.MoveNext();
iRow++;
//设置UI线程中的进度条
if (iRow % (iDataCount / 20) == 0) //将ProgressCtrl分成20份,即以5%的数量增加
{
//向UI线程发送自定义消息, 设置ProgressCtrl进度条当前位置
pThread->PostThreadMessage(WM_THREAD_MESSAGES, (WPARAM)2, (LPARAM)iRow);
}
}
//向UI线程的主对话框发送自定义消息,直接设置ProgressCtrl进度条为满状态
pThread->PostThreadMessage(WM_THREAD_MESSAGES, (WPARAM)3, (LPARAM)iDataCount);
注意:
1. 使用此方法在UI线程中创建模态对话框,则本UI线程无需手动关闭,只要模态对话框关闭,CProgressCtrlThread::InitInstance()函数便会返回FALSE,从而导致本线程自动退出并关闭消息泵
2. 两个UI线程宏:DECLARE_DYNCREATE(CProgressCtrlThread)与 IMPLEMENT_DYNCREATE(CProgressCtrlThread, CWinThread)一定要添加,否则创建线程会提示内存不足
上述方法实现了UI线程的创建以及对其模态进度条对话框的控制,实际上在消息传递方式上博主也有些疑惑:向UI线程中的对话框发送消息还有一种思路,就是设法获取到对话框的句柄,然后直接使用::PostMessage(),在UI线程的对话框类的MESSAGE_MAP消息映射中实现ON_MESSAGE()宏,如:
//获取UI线程主对话框句柄
HWND hdlg = pThread->GetMainWnd()->GetSafeHwnd();
//向UI线程的主对话框发送自定义消息,设置ProgressCtrl进度条的Range
BOOL success = ::PostMessage(hdlg, WM_PROGRESS_CHANGED, (WPARAM)1, (LPARAM)iDataCount);
但不知何故,这种直接向UI线程的对话框中发送自定义消息的方式总是无法成功,如果哪位高人能够解决博主的疑惑还请留言赐教。
参考文献:
MFC中如何给子线程发送消息
MFC 线程消息传递问题二—两个线程之间进行消息的传递
关于MFC下多线程,在线程中创建非模态对话框以及消息传递
MFC线程中创建非模态对话框
MFC启动和关闭线程