公司的生产测试工具用了N多年了。N多年的是多少年?不知道是不是现在的一些小同学还不知道VC6.0(还不知道可能不妥,仅仅调侃,无意冒犯,应该说是没用过),生产测试工具当时还是用VC6.0开发的,感觉这和现在去打仗用的还是清朝的“神威无敌大将军”一样。所以,我这个起步VC6.0,但是又半路出家的程序猿再恶补一下当年丢下的东西。
工具的UI和功能其实都很简单,但是要优化的地方也是很多。
今天只说一下涉及到UI线程也就是界面线程部分,因为真正的测试工具的功能比多,这里仅仅用demo的方式记录一下自己恶补的一些东西。
代码下载链接:https://download.csdn.net/download/yangkunhenry/14110419
基本内容
首先,主界面是个对话框,利用MFC很容易创建一个基于对话框的程序,因为这几天刚好要做一个售后工具,所以工具的名字就叫做after sales tool了,听起来比较俗气,见谅。
对话框大概是这个样子,比较简陋,中间的编辑框作用就不说了(是RichEdit2,刚开始犯了错误,因为在app的InitInstance中没有加入AfxInitRichEdit2();,导致程序刚开始运行就报错:“警告: 如果您在对话框上使用 MFC 控件,则无法 #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS。”),不是重点。这里主要是 在鼠标点下Setting的时候产生另外一个Setting 对话框,如下面:
这里的setting对话框原来是在主进程中做的,设置退出之后,主进程就退出了,这样不好。
所以这里将ASL Setting界面用用户线程的方式去做
创建界面线程
界面线程和工作线程是 线程的两个基本类型,最近在CSDN论坛上也问了不少大牛,界面线程就是处理界面相关的,其他啥也不要干。
前面说是在按下确定的时候去创建界面线程。
界面线程的创建函数:AfxBeginThread
MSDN:
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
CWinThread* AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
pThreadClass
从 CWinThread 派生的对象的 RUNTIME_CLASS。
MSDN中两个创建线程的函数,创建界面线程的话我们就用后面这个,第一个参数是CRuntimeClass指针的这个函数。
显然我们要为这个参数做下准备,我们要提供这样一个类型的参数:CRuntimeClass
pThreadClass是 从CWinThread派生的对象的RUNTIME_CLASS
我这里建立了一个CSetting的类,当然基类就是CWinThread了
#pragma once
#include <afxwin.h>
#include "stdafx.h"
#include "CSettingDlg.h"
class CSetting :
public CWinThread
{
DECLARE_DYNCREATE(CSetting)
protected:
CSetting();
~CSetting();
public:
BOOL InitInstance();
virtual int ExitInstance();
CSettingDlg *m_psetting_dlg;
afx_msg void OnThreadInfo(WPARAM wParam, LPARAM lParam);
protected:
DECLARE_MESSAGE_MAP() /* 本身宏中已经有protected:但是为了防止直接在后面追加成员变量,所以这里还是再把protected写上*/
public:
BOOL m_b_dev_id;
};
创建线程类的方法:
- 创建CWinThread的子类,这里的CSetting;
- 在类声明.h中添加DECLARE_DYNCREATE(CSetting) ,DECLARE_DYNCREATE作用其实很简单,就是填加了一个静态成员static const CRuntimeClass classSetting;后面会用到这个成员变量的指针
- 在类的定义中添加IMPLEMENT_DYNCREATE(CSetting, CWinThread),当然这肯定是前面.h中声明部分的具体定义了,这里面具体是什么呢?这里面是关于CRunTimeClass* 是怎么来的的具体过程。这个可以单独开一篇博客去讲解了,有时间我再具体说一下这个,这里打mark.
- 重载CWinThrad的InitInstance和ExitInstance,当然,有些疑问的是,在重载的时候里面怎么去填?那我告诉你InitInstance中要返回TRUE ,为什么呢?看下CWinThread的InitInstance的实现:
/ // CWinThread default implementation BOOL CWinThread::InitInstance() { ASSERT_VALID(this); return FALSE; // by default don't enter run loop }
看注释:by default don't enter run loop ,意思是说如果是返回FALSE,则不会跑CWinThread的run 循环,所以,有些同学在重载这个函数的时候,不是返回TRUE,而是返回CWinThread::InitInstance,那么这样你定义了消息响应函数,那么你去调用PostThreadMessage的时候也不会消息响应,在thrdcore.cpp(在C盘下你可以自己搜下或者单步跟踪下)中可以查下有CWinThread的函数实现,可以看下_AfxThreadEntry这个函数中有这样的代码:
// else -- check for thread with message loop else if (!pThread->InitInstance()) { ASSERT_VALID(pThread); nResult = pThread->ExitInstance(); } else { // will stop after PostQuitMessage called ASSERT_VALID(pThread); nResult = pThread->Run(); }
看到了吧,如果InitInstance返回的是FALSE,那么下面的else肯定跑不进去。
- 添加你要创建的Dlg的指针(如何创建对话框类?打mark)m_psetting_dlg
- 因为我们需要从主进程中发送消息给界面线程,所以需要写消息响应函数afx_msg void OnThreadInfo(WPARAM wParam, LPARAM lParam);
- 当然DECLARE_MESSAGE_MAP也必不可少,平时我们在为简单的对话框添加消息的时候,通过MFC类向导自动添加了消息响应函数之后,我们发现代码中就有这样一句代码,但是我们目前不少窗口类型的类,MFC类向导不支持添加非窗口类型的消息响应函数,所以我们需要自己写,这是类声明中需要写这一句话,另外就是下面要说的。
- 在类定义的时候添加
BEGIN_MESSAGE_MAP(CSetting, CWinThread) ON_THREAD_MESSAGE(WM_THREADINFO, &CSetting::OnThreadInfo) END_MESSAGE_MAP()
- 当然我们需要提前定义对应的WM_THREADINFO,我这里定义的是#define WM_THREADINFO WM_USER+50
- 以上做了之后呢,我们看一个线程类就创建了,那我们就可以利用 m_setting_ui_thread = AfxBeginThread(RUNTIME_CLASS(CSetting)); 去创建一个线程了,m_setting_ui_thread的类型是CWinThread*类型。执行AfxBeginThread这个当然是在我们进程去做的,我们可以在创建线程m_setting_ui_thread之后用发送消息的方式去通知线程创建一个dialog,当然也可以在线程类CSetting的inistance中直接创建dialog对话框,这里采用发送消息的的方式用
m_setting_ui_thread->PostThreadMessage(WM_THREADINFO, 2, 0);
- 这个post message之后,在我们线程类的消息响应函数中
//显示消息处理函数 void CSetting::OnThreadInfo(WPARAM wParam, LPARAM lParam) { if (wParam == 2) {//启动 m_psetting_dlg = new CSettingDlg; m_psetting_dlg->Create(IDD_DLG_SETTING); m_psetting_dlg->UpdateData(FALSE); m_psetting_dlg->ShowWindow(TRUE); } //return 0; }
new一个Dlg类,创建窗口,更新窗口,显示窗口,这样界面窗口就创建完了。
线程退出:
这里我们发现这个Dlg是我们在消息响应函数中new出来的,所以我们也需要在线程退出的时候delete掉这个对象,线程退出也就是线程类退出,线程类退出也就是线程的析构函数调用,所以我们这里是放在析构函数中取做delete操作:
CSetting::~CSetting()
{
if (m_psetting_dlg != NULL) {
delete m_psetting_dlg;
m_psetting_dlg = NULL;
}
}
当然前提是我们线程要退出,线程退出也就是自动退出线程的run,这里虽然我们没有实现线程的run,但是CWinThread中run还是在为我们不断的去做消息循环,一旦run收到了WM_QUIT消息就退出了,CWinThread的run方法:
int CWinThread::Run()
{
ASSERT_VALID(this);
_AFX_THREAD_STATE* pState = AfxGetThreadState();
// for tracking the idle time state
BOOL bIdle = TRUE;
LONG lIdleCount = 0;
// acquire and dispatch messages until a WM_QUIT message is received.
for (;;)
{
// phase1: check to see if we can do idle work
while (bIdle &&
!::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE))
{
// call OnIdle while in bIdle state
if (!OnIdle(lIdleCount++))
bIdle = FALSE; // assume "no idle" state
}
// phase2: pump messages while available
do
{
// pump message, but quit on WM_QUIT
if (!PumpMessage())
return ExitInstance();
// reset "no idle" state after pumping "normal" message
//if (IsIdleMessage(&m_msgCur))
if (IsIdleMessage(&(pState->m_msgCur)))
{
bIdle = TRUE;
lIdleCount = 0;
}
} while (::PeekMessage(&(pState->m_msgCur), NULL, NULL, NULL, PM_NOREMOVE));
}
}
看到了吧:
// pump message, but quit on WM_QUIT
if (!PumpMessage())
return ExitInstance();
注释写的很清楚,pmup message,but quit on WM_QUIT, 同时我们也看到了,如果run退出了,那么会执行ExitInstance动作
因此总结一下,我们要在进程中某一个地方,你需要让线程退出的地方,去发送给线程一个WM_QUIT的消息,
我们就在主进程的对话框中的确定点击的时候去让线程退出:
void CdwaftersalestoolDlg::OnBnClickedOk()
{
// TODO: 在此添加控件通知处理程序代码
/*
发送WM_QUIT线程给UI线程
*/
if (m_setting_ui_thread) {
m_setting_ui_thread->PostThreadMessage(WM_QUIT, 0, 0);
if (WAIT_OBJECT_0 == WaitForSingleObject(m_setting_ui_thread->m_hThread, INFINITE)) {
CDialogEx::OnOK();
}
}else
CDialogEx::OnOK();
}
在调用了PostThreadMessage的之后,我们只是把WM_QUIT消息发给界面线程的消息循环,但是run函数有没有跑到退出的地方,还是另外一回事,总的来说它几乎很快就可以跑到,但是我们还是要用WaitForSingleObject去等待线程变为有信号的,这样才能避免线程的一些操作还没有完全执行,我们就执行其他动作了,比如如果你在创建线程类的时候指定m_bAutoDelete =FALSE;那么你需要手动去delete m_setting_ui_thread;如果还没有等到线程run跑完,没有等到ExitInistance执行完就去delete m_setting_ui_thread了那么就会造成未知的错误。
另外提醒一点,不要再界面线程中调用Afxmessagebox这样的函数,我在调试的时候遇到因为调用了Afxmessagebox导致的程序错误。