MFC 之 Progress Control 控件的使用
提到 Progress Control 控件,大家可能会觉得在 UI 界面里面装一个进度条控件,一下就会让 UI 界面变得高级了些,所以可能会认为这个控件可能比较难搞。其实恰恰相反,这个控件使用起来特别容易,调用方法也就寥寥几个。
不过本文重点内容并不是讲 Progress Control 的使用,而是会重点介绍一下如何解决 MFC 界面编程中的线程阻塞问题,当然事情得一步步来解决,首先我们来看看 Progress Control 控件的使用。
一、进度条控件的方法
函数名 | 作用 |
---|---|
create() | 创建Progress Control,针对不是通过资源文件上拖拉进度条控件生成的进度条,需要用此函数创建一个 |
SetRange() | 设置进度条的起始值和终止值。(不管拖拽还是创建都要设置此,在初始化中) |
SetPos() | 设置进度条的当前位置(值) |
OffSetPos() | 移动进度条一段距离(动态移动,如果不做延时,默认速度移动) |
SetStep() | 设置步长,设置进度条偏移一次的长度,一般与StepIt搭配使用 |
StepIt() | 按照步长来更新位置 |
GetPos() | 获得进度条当前值 |
二、进度条控件的使用
step 1 添加 Progress Control 控件
我们不需要用 Create() 函数来创建,直接在工具栏中拖拽一个进度条控件到主对话框中对应位置即可。
step 2 对 Progress Control 控件进行初始化
添加控件完成后,接下来我们需要在 OnInitDialog()
函数中对控件进行初始化,添加如下代码:
/* 对进度条控件进行初始化 */
CProgressCtrl* m_proCtrl1 = (CProgressCtrl*)GetDlgItem(IDC_PROGRESS1);
m_proCtrl1->SetRange(0, 100);
其中
IDC_PROGRESS1
为我们控件的资源 ID。
step 3 使用 Progress Control 控件
我们对控件初始化完毕后,我们就可以在需要设置进度条的地方对控件的进度进行设置即可,为了测试效果,我们增加一个按钮组件在主对话框中,使得点击按钮,进度条就会从 0 增加到 100 的效果。我们拖拽一个 Button 组件到主对话框中,右键添加一个变量 m_btn1
,然后双击它,来到处理按钮事件的函数处,填入下面代码:
CProgressCtrl* m_proCtrl = (CProgressCtrl*)GetDlgItem(IDC_PROGRESS1);
for (int i = 0; i <= 100; i++)
{
m_proCtrl->SetPos(i);
Sleep(100);
}
这样,我们可以通过 SetPos 函数对进度条进行设置,实现进度条不断增长的效果,是不是特别简单?但是这个时候我们发现了一个严重的问题,那就是主对话框好像卡死了?只有当进度条走完的时候,主对话框才会响应我们的操作,那有什么办法解决这个问题呢?有两个常用的办法,第一个办法是安装一个定时器,然后把我们设置进度条的操作放在定时器里面,第二个办法是为我们的任务单独开一个线程。
网上的博客在演示 Progress Control 控件的时候大多都是使用定时器的方法,我们在这里就不多做介绍了,有兴趣的可以去搜搜看,主要介绍通过开启子线程来解决主对话框卡死的问题。
三、通过开启子线程来解决主对话框卡死的问题
我们在 MFC 编程中经常使用 AfxBeginThread
函数来开启线程,当我们开启一个子线程来处理一个繁忙的任时,如果我们有同步需求,一定需要等待子线程的返回结果才能进行后面的操作,那么我们就需要用到线程等待函数。
常用的的线程等待函数有 WaitForSingleObjec
、WaitForMultipleObjects
和 MsgWaitForMultipleObjects
,前面两个函数为阻塞函数,如果我们设置参数为一直等待直到返回,那么这个函数就会将主线程一直阻塞下去,导致主对话框卡死,第三个函数是立即返回函数,但是我们的目标是等到线程的返回结果,如果我们在调用函数的时候,线程不返回,就达不到我们想要的目的,因此我们只能设置一个死循环,不断地调用 MsgWaitForMultipleObjects
函数来获取线程的返回结果,但是如果这样操作的话,那和前面两个阻塞函数又有什么区别呢?
其实我们想一下,为什么我们的线程被阻塞会造成主对话框卡死呢?是因为 MFC 对话框程序是基于消息响应机制的,而主线程被阻塞了,所以我们无法对其他的消息进行处理,所以才会造成主对话框卡死,那么我们另辟蹊径,在循环调用 MsgWaitForMultipleObjects
函数的同时也对消息进行分发,是不是就不会造成卡死了呢?确实是这样,我们来试一下,我们在按钮事件中填入下面代码:
CWinThread* pThread = NULL;
void CBasicInterfaceConfigDlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
// 禁用按钮
m_btn1.EnableWindow(FALSE);
// 开一个线程执行任务
pThread = AfxBeginThread(myThread, this);
DWORD dwRet = 999;
do
{
if (dwRet != WAIT_OBJECT_0)
{
MSG msg;
if (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
// 分发消息并派遣
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
dwRet = ::MsgWaitForMultipleObjects(1, &pThread->m_hThread, FALSE, INFINITE, QS_ALLINPUT);
}
} while ((dwRet != WAIT_OBJECT_0) && (dwRet != WAIT_FAILED));
// 恢复按钮
m_btn1.EnableWindow(TRUE);
}
在上面的代码中,当我们点击完 Button 控件后,我们会将该控件禁用,以防用户触发多个更改进度条控件的事件,然后我们开启单独开启一个子线程来对进度条进行设置,再设置一个循环来等待线程返回的同时也对窗口消息进行分发,以免卡死,等到线程返回的时候,我们再恢复按钮点击。
然后我们来编写子线程函数,在这里我们又碰到一个新的问题。我们在主对话框类中添加了一个新的子线程函数 myThread
,但是 AfxBeginThread
函数却显示错误:“没有与参数列表匹配的 重载函数 "AfxBeginThread" 实例” ,这是为什么呢?
其实是传入的 myThread
参数类型不对,我们传入的是类中的一个成员函数,这个问题有两种解决办法,第一种是把线程函数定义在类外,第二种是在类中用 static
修饰,定义为静态函数。为了保持 C++ 的优雅,我们采用第二种方式,必须按照下面方式进行定义(返回值和参数不能改变):
public:
static UINT myThread(LPVOID pParam);
然后我们需要在子线程中获取进度条控件的句柄,并对进度条进行设置:
UINT CBasicInterfaceConfigDlg::myThread(LPVOID pParam)
{
// 获取窗口句柄
HWND hWnd = ::FindWindow(NULL, L"这是一个示例程序");
// 通过窗口句柄得到主对话框句柄
CBasicInterfaceConfigDlg* pWnd = (CBasicInterfaceConfigDlg*)CBasicInterfaceConfigDlg::FromHandle(hWnd);
// 通过主对话框句柄得到进度条控件句柄并进行设置
CProgressCtrl* m_proCtrl = (CProgressCtrl*)pWnd->GetDlgItem(IDC_PROGRESS1);
for (int i = 0; i <= 100; i++)
{
m_proCtrl->SetPos(i);
Sleep(100);
}
return 0;
}
在上面的函数中,我们实现了子线程控制进度条的功能,我们是通过窗口名称来获得窗口句柄的,这里突然让我想起以前用过一个软件可以修改其他界面应用程序的控件状态,大概也是这个原理,感兴趣的朋友可以试试,感觉挺有意思,哈哈哈...
但是在我们自己的程序中通过查找窗口名称来获得窗口句柄的,感觉还是不够优雅,有没有其他更好地方式呢?其实我们这里就是想得到主对话框的窗口句柄,那我们可以在主对话框类中定义一个静态的窗口句柄变量,然后在主对话框调用 OnInitDialog()
函数初始化的时候将其窗口句柄保存,我们就可以实现这个功能。
首先我们在主对话框头文件中定义静态变量:
static CBasicInterfaceConfigDlg* m_BasicInterfaceConfigDlg;
然后我们在主对话框类外对该静态变量进行初始化:
// 注意要写在类外,不要写在类实现函数里面(由于这个指针是静态的,所以我们需要在类外初始化)
CBasicInterfaceConfigDlg* CBasicInterfaceConfigDlg::m_BasicInterfaceConfigDlg = NULL;
接下来我们在主对话框的 OnInitDialog()
函数中将主对话框窗口句柄进行保存:
m_BasicInterfaceConfigDlg = this;
最后我们就可以在 myThread
线程函数中对主对话框中的控件进行修改了:
UINT CBasicInterfaceConfigDlg::myThread(LPVOID pParam)
{
// 得到主对话框句柄
CBasicInterfaceConfigDlg* pWnd = CBasicInterfaceConfigDlg::m_BasicInterfaceConfigDlg;
// 通过主对话框句柄得到进度条控件句柄并进行设置
CProgressCtrl* m_proCtrl = (CProgressCtrl*)pWnd->GetDlgItem(IDC_PROGRESS1);
for (int i = 0; i <= 100; i++)
{
m_proCtrl->SetPos(i);
Sleep(100);
}
return 0;
}
当然上面讲了这么多,我们是出于同步考虑,一定要得到子线程的返回结果我们才能继续处理其他任务,在我们的演示代码中就是等待设置进度条的线程执行完毕返回后,对按钮进行恢复。
然而我们这个示例不需要这么复杂的操作,我们无需等待线程返回,只要将恢复按钮的操作放在子线程函数的最后即可,因此我们也不需要在主线程中循环的调用 MsgWaitForMultipleObjects
函数来等待子线程的返回结果,现在我们对按钮事件的处理函数进行修改:
CWinThread* pThread = NULL;
void CBasicInterfaceConfigDlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
// 禁用按钮
m_btn1.EnableWindow(FALSE);
// 开一个线程执行任务
pThread = AfxBeginThread(myThread, this);
}
对子线程函数进行修改:
UINT CBasicInterfaceConfigDlg::myThread(LPVOID pParam)
{
// 得到主对话框句柄
CBasicInterfaceConfigDlg* pWnd = CBasicInterfaceConfigDlg::m_BasicInterfaceConfigDlg;
// 通过主对话框句柄得到进度条控件句柄并进行设置
CProgressCtrl* m_proCtrl = (CProgressCtrl*)pWnd->GetDlgItem(IDC_PROGRESS1);
for (int i = 0; i <= 100; i++)
{
m_proCtrl->SetPos(i);
Sleep(100);
}
// 恢复按钮
CButton* m_btn = (CButton*)pWnd->GetDlgItem(IDC_BUTTON1);
m_btn->EnableWindow(TRUE);
return 0;
}
在恢复按钮这里,由于我们的子线程函数是一个静态函数,因此我们只能动态的获取按钮句柄进行设置
运行结果如下: