1. 应用场景
最近在做一个单机版的MFC程序,程序的功能是点击按钮,程序执行运算函数,整个运算过程时间较长,(涉及图片预处理、相位解包裹、点云处理、曲面重建等过程),我需要在edit control控件实时显示函数的执行进度。
2. 最初的解决方法
最初想到的解决办法是:每执行完函数中的一段程序后,在程序之后写上如下代码。(在控制台程序中是用这种思路实现的)
UpdateData(TRUE);
GetStringTime(TimeString);
m_outedit = m_outedit + (CString)"程序开始执行,加载图片" + TimeString + (CString)"\r\n";
UpdateData(FALSE);
其中m_outedit是我对edit control控件关联的变量。
3. 最初的解决方法遇到的问题
- 程序在执行完上述代码后并不会立即更新控件,直到整个函数执行完成才会更新控件,因此该问题是致命的。
- 由于程序的运算过程较为复杂,在程序运算的过程中,在界面上点击其他按钮会出现程序未响应的现象,也就是假死现象。参考文献.
接下来重点解决这两个问题。
4. 激动人心的充电环节
4.1 思路:
每个系统中都有线程(至少都有一个主线程),而线程最重要的作用就是并行处理,提高软件的并发率。针对界面来说,还能提高界面的响应能力。
一般的,为了应用的稳定性,在数据处理等耗时操作会单独在一个线程中运行,而所有与主UI线程有关的控件数据刷新应该到主UI线程中处理。也就是数据处理线程发消息,让界面UI去更新控件。
在MFC中线程分为界面线程和工作者线程,界面实际就是一个线程画出来的东西,这个线程维护一个“消息队列”,“消息队列”也是界面线程和工作者线程的最大区别,MFC中有两类线程,分别称之为工作者线程和用户界面线程。二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环。参考文献.
我的解决思路:我使用主线程(用户界面线程)处理消息(包括按钮的点击以及edit control控件的刷新),新建了一个工作者线程进行复杂的函数运算,在工作者线程中,将执行进度的字符串保存到静态变量中,通过消息投递函数触发主线程对edit control控件进行刷新。
4.2 创建线程的方法
第一种AfxBeginThread();第二种CreateThread();第三种_beginthread()。参考文献.
4.3 三种创建线程的方法具体有什么区别呢?
CreateThread这个函数是windows提供给用户的Win32 API函数,是SDK的标准形式,在使用的过程中要考虑到进程的同步与互斥的关系,进程间的同步互斥等一系列会导致操作系统死锁的因素,用起来比较繁琐一些,初学的人在用到的时候可能会产生不可预料的错误,建议多使用AfxBeginThread,AfxBeginThread是MFC的全局函数,是编译器对原来的CreateThread函数的封装,用与MFC编程(当然,只要修改了项目属性,console和win32项目都能调用)。_beginthread是C的运行库函数。参考文献.
4.4 AfxBeginThread();
接下来主要介绍一下AfxBeginThread()函数。MFC提供了两个重载版的AfxBeginThread,一个用于用户界面线程,另一个用于工作者线程。
由于我创建的是工作者线程,因此详细介绍使用AfxBeginThread()函数创建工作者线程。下图是函数定义:
CWinThread* AFXAPI AfxBeginThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0,
DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);
参数1为线程的入口函数,开启线程所要执行的函数,此函数必须为静态函数;
参树2为传递入线程的参数,注意它的类型为:LPVOID,所以我们可以传递一个结构体入线程,通常把this指针传过去,这样线程函数就可以使用和操作类的成员了;
参数3指定线程优先级,如果为0,则与创建该线程的线程相同,其中THREAD_PRIORITY_NORMAL的宏定义为0;
参数4指定新创建的线程的栈的大小。如果为 0,新创建的线程具有和主线程一样的大小的栈;
参数5是一个创建标识,默认为0,线程在创建后开始线程的执行。如果是CREATE_SUSPENDED,在线程创建后线程挂起,直到调用:ResumeThread。
参数6指向一个 SECURITY_ATTRIBUTES 的结构体,用它来标志新创建线程的安全性,如果为 NULL,那么新创建的线程就具有和主线程一样的安全性。
5. 具体实现过程
5.1 创建工作者线程
在.cpp文件中创建工作者线程,我是在按钮响应函数下创建的,也就是点击按钮创建线程。
CWinThread* mythread = AfxBeginThread(
Calcul,
this,
THREAD_PRIORITY_NORMAL,
0,
0,
NULL
);
其中Calcul是我的运算函数。
在.h件中声明运算函数。
static UINT Calcul(LPVOID lpParam);
在.cpp文件中定义Calcul函数。
UINT CMy3DmeasurementMFC41Dlg::Calcul(LPVOID lpParam)
{
CMy3DmeasurementMFC41Dlg *Dlg = (CMy3DmeasurementMFC41Dlg *)lpParam;
Dlg->GetStringTime(Dlg->TimeString);
OutCstring = OutCstring + (CString)"程序开始执行,加载图片" + Dlg->TimeString + (CString)"\r\n";
::SendMessage(Dlg->m_hWnd, WM_UPDATE_STATIC, 0, 0);
//::SetWindowText(::GetDlgItem(Dlg->m_hWnd, IDC_OUTEDIT), OutCstring);
//Dlg->GetDlgItem(IDC_OUTEDIT)->SetWindowText(OutCstring);
//我的数据处理过程
//
return 0;
}
其中OutCstring这个变量就是我要刷新edit control控件所要用到的。
这里要说明的是:
1.上图中的GetStringTime是我写的函数,函数功能是以CString的格式输出当前时间,后面对这个函数进行详细介绍。
2.我在运算函数中定义了类的指针,通过指针的方式调用类的函数与变量,但是其中的OutCstring并没有使用指针的形式调用。原因是我定义的OutCstring是一个静态变量,方便我在用户界面线程中使用该变量。后面会再详细说一下静态变量。
3.我这里使用return 0来退出线程。
5.2 自定义消息函数的添加
继续我们的实现过程:
我这里通过自定义消息函数SendMessage,来通知主线程(用户界面线程),更新edit control控件,也可以在工作者线程内通过全局函数更新控件内容。这位仁兄介绍了MFC子线程中更新控件内容的两种办法,我试了这两种方法均可行(参考上一张代码图片注释部分),但是我还是建议大家通过发送自定义消息更新控件内容,尽量避免在线程中掺杂对主界面的操作。
首先,在.h件中定义一个消息ID。
#define WM_UPDATE_STATIC (WM_USER + 100)
然后在VS的菜单栏点击项目->类向导->消息->添加自定义消息,在上面的编辑框中填入我们定义的消息ID:WM_UPDATE_STATIC,点击确定;VS会非常贴心地帮分别我们在.h文件中添加自定义消息响应函数的声明,在.cpp文件中添加消息映射和自定义消息响应函数的定义。
.h文件中的自定义消息响应函数的声明
afx_msg LRESULT OnUpdateStatic(WPARAM wParam, LPARAM lParam);
.cpp文件中的消息映射
BEGIN_MESSAGE_MAP(CMy3DmeasurementMFC41Dlg, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_CALCUL, &CMy3DmeasurementMFC41Dlg::OnBnClickedCalcul)
ON_MESSAGE(WM_UPDATE_STATIC, &CMy3DmeasurementMFC41Dlg::OnUpdateStatic)
END_MESSAGE_MAP()
.cpp文件中的自定义消息响应函数的定义
afx_msg LRESULT CMy3DmeasurementMFC41Dlg::OnUpdateStatic(WPARAM wParam, LPARAM lParam)
{
GetDlgItem(IDC_OUTEDIT)->SetWindowText(OutCstring);
return 0;
}
以上,就是我的全部实现过程。
6. 几个用到的知识点
6.1 关于静态变量
我对静态变量的理解:一个类可以实例化多个对象,每一个对象都有各自的存储空间来存储各自内部的变量,但是对于静态变量而言,无论这个类实例化多少个对象,这个静态变量有且仅有一个存储空间,而且类的每一个对象均可访问这个静态变量。
静态变量的定义:
static CString OutCstring;
静态变量的初始化:
CString CMy3DmeasurementMFC41Dlg::OutCstring = (CString)"打开程序"+(CString)"\r\n";
CMy3DmeasurementMFC41Dlg::CMy3DmeasurementMFC41Dlg(CWnd* pParent /*=NULL*/)
第一行代码是初始化,第二行代码是构造函数,是用来表明在哪个位置对静态变量进行初始化。(注意是构造函数前面)
6.2 我的GetStringTime函数
void CMy3DmeasurementMFC41Dlg::GetStringTime(CString &StringTime)
{
StringTime = "";
SYSTEMTIME sys;
CString StringTime_Hour; CString StringTime_Minute; CString StringTime_Second;
GetLocalTime(&sys);
StringTime_Hour.Format(_T("%d"), sys.wHour); StringTime_Minute.Format(_T("%d"), sys.wMinute); StringTime_Second.Format(_T("%d"), sys.wSecond);
StringTime = StringTime_Hour + (CString)":" + StringTime_Minute + (CString)":" + StringTime_Second;
}
我这个GetStringTime函数功能是以CString的格式输出当前时间,其中GetLocalTime()这个函数我觉得还是比较好用的,我这里用到的是时分秒。其次使用Format函数将整型变量转化为CString变量,最后是CString的拼接,注意CString变量可以连续拼接。
6.3 SendMessage与PostMessage
我在这儿用的是SendMessage,也就是我的自定义消息响应函数执行完成后,工作者线程中的函数才会继续执行。
如果使用PostMessage,我的自定义消息响应函数和工作者线程中的函数会同步执行。
6.4 edit control控件和button control控件的设置
- 我在更新edit control控件的变量中应用了(CString)"\r\n",但是在输出的过程中并不会换行,需要对edit control控件的属性进行设置。参考文献.
Auto HScroll 设置为 False
MultiLine 设置为 True
Want Return 设置为 True。 - 点击按钮进行计算之后,由于运算过程较长,程序在运算的过程中用户再次点击可能导致程序崩溃,因此在点击按钮之后,将按钮设置为不可点击,将按钮上的文字设置为“正在运算”。在运算过程执行完之后,再将设置更改过来。
GetDlgItem(IDC_CALCUL)->EnableWindow(FALSE);
GetDlgItem(IDC_CALCUL)->SetWindowText((CString)"正在计算");
后记:
若大家发现错误,敬请批评指正,如有疑问,欢迎留言。