很早就想写这篇日志了,2008年了吗,一个新的开始,总要写点什么做点留念吧!可以一拖再拖,现在都1月5号了!感觉自己再不写的话,就要到2009年才能写这篇日志了。
2008年的第一个天,也就是1月1号,不知道各位是怎么过的,我是基本上就躺在床上什么都不想,傻乎乎的看完了一部《双妻时代》,因为这样,那一天的时间我才能打发掉,那样才能让我觉得不失落。前段时间突然觉得自己毕业后的很多时间都用来去追寻所谓的爱情,结果却TMD什么都没有,无非让自己更暧昧,更知道怎么才不会受伤,更知道怎么才能收放自如。。。。。。等等一切,我发现我开始“变得有点坏”了,不是我想变,是别人把我变成这样的,不要说“你可以不变啊”,我是没有想变,可是TMD我也不知道怎么就这样了。我被人说成了“太暧昧了、嘴巴太甜了。。。”,这两天还被一些人定义成“一个骗子”,可我什么都没有做啊。。。。。。难道让我去接受一个我不喜欢的人,我就不是骗子了? 呵呵!只是有的时候觉得很好笑,以前都TMD人家不要我,原来我也可以“不要”别人的。太搞笑了!
不想那么多了,我知道我自己想要什么,无论别人怎么说我,只是希望以后的她对我好就足够了。这里再扯淡一下:有个人帮我看手上的爱情线,说我要招惹7个女人,晕死!!我现在满打满算,还有好几个要我招惹啊!!
前段时间去参加一个培训后,突然发现自己原来这一年多,都把时间花在前任女朋友身上了,太多的东西要学了,要不然真的要“挂了”,发现自己对技术的了解太少了,自己不知道最近一年都在干些什么,再不学习很快就要完蛋了,追,要追,最近2个月来的学习后觉的自己还是比较喜欢现在这样生活,有学习才有乐趣。不是吗? 希望到了2009年1月1号的时候,自己通过1年多的学习,可以有些进步。
“To Do,To Become,To Own”----用这句话勉励自己!
深入探讨MFC消息循环和消息泵
作者:周焱
首 先,应该清楚MFC的消息循环(::GetMessage,::PeekMessage),消息泵(CWinThread::PumpMessage)和 MFC的消息在窗口之间的路由是两件不同的事情。在MFC的应用程序中(应用程序类基于CWinThread继承),必须要有一个消息循环,他的作用是从 应用程序的消息队列中读取消息,并把它派送出去(::DispatchMessage)。而消息路由是指消息派送出去之后,系统(USER32.DLL) 把消息投递到哪个窗口,以及以后消息在窗口之间的传递是怎样的。
消息分为队列消息(进入线程的消息队列) 和非队列消息(不进入线程的消息队列)。对于队列消息,最常见的是鼠标和键盘触发的消息,例如WM_MOUSERMOVE,WM_CHAR等消息;还有例 如:WM_PAINT、WM_TIMER和WM_QUIT。当鼠标、键盘事件被触发后,相应的鼠标或键盘驱动程序就会把这些事件转换成相应的消息,然后输 送到系统消息队列,由Windows系统负责把消息加入到相应线程的消息队列中,于是就有了消息循环(从消息队列中读取并派送消息)。还有一种是非队列消 息,他绕过系统队列和消息队列,直接将消息发送到窗口过程。例如,当用户激活一个窗口系统发送WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。创建窗口时发送WM_CREATE消息。在后面你将看到,MS这么设计是很有道理的,以及他的整套实现机制。
这里讲述MFC的消息循环,消息泵。先看看程序启动时,怎么进入消息循环的:
_tWinMain ->AfxWinMain ->AfxWinInit ->CWinThread::InitApplication ->CWinThread::InitInstance ->CWinThread::Run
非对话框程序的消息循环的事情都从这CWinThread的一Run开始...
第一部分:非对话框程序的消息循环机制。
//thrdcore.cpp
// main running routine until thread exits
int CWinThread::Run()
{
ASSERT_VALID(this);
// 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(&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))
{
bIdle = TRUE;
lIdleCount = 0;
}
} while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
} //无限循环,退出条件是收到WM_QUIT消息。
ASSERT(FALSE); // not reachable
}
这是一个无限循环,他的退出条件是收到WM_QUIT消息:
if (!PumpMessage())
return ExitInstance();
在PumpMessage中,如果收到WM_QUIT消息,那么返回FALSE,所以ExitInstance()函数执行,跳出循环,返回程序的退出代码。所以,一个程序要退出,只用在代码中调用函数
VOID PostQuitMessage( int nExitCode )。指定退出代码nExitCode就可以退出程序。
下面讨论一下这个函数Run的流程,分两步:
1, 第一个内循环phase1。bIdle代表程序是否空闲。他的意思就是,如果程序是空闲并且消息队列中没有要处理的消息,那么调用虚函数OnIdle进行 空闲处理。在这个处理中将更新UI界面(比如工具栏按钮的enable和disable状态),删除临时对象(比如用FromHandle得到的对象指 针。由于这个原因,在函数之间传递由FromHandle得到的对象指针是不安全的,因为他没有持久性)。OnIdle是可以重载的,你可以重载他并返回 TRUE使消息循环继续处于空闲状态。
NOTE:MS用临时对象是出于效率上的考虑,使内存 有效利用,并能够在空闲时自动撤销资源。关于由句柄转换成对象,可以有若干种方法。一般是先申明一个对象obj,然后使用obj.Attatch来和一个 句柄绑定。这样产生的对象是永久的,你必须用obj.Detach来释放对象。
2,第二个内循环phase2。在这个循环内先启动消息泵(PumpMessage),如果不是WM_QUIT消息,消息泵将消息发送出去(::DispatchMessage)。消息的目的地是消息结构中的hwnd字段所对应的窗口。
//thrdcore.cpp
BOOL CWinThread::PumpMessage()
{
ASSERT_VALID(this);
//如果是WM_QUIT就退出函数(return FALSE),这将导致程序结束.
if (!::GetMessage(&m_msgCur, NULL, NULL, NULL)) {
#ifdef _DEBUG
if (afxTraceFlags & traceAppMsg)
TRACE0("CWinThread::PumpMessage - Received WM_QUIT.\n");
m_nDisablePumpCount++; // application must die
// Note: prevents calling message loop things in 'ExitInstance'
// will never be decremented
#endif
return FALSE;
}
#ifdef _DEBUG
if (m_nDisablePumpCount != 0)
{
TRACE0("Error: CWinThread::PumpMessage called when not permitted.\n");
ASSERT(FALSE);
}
#endif
#ifdef _DEBUG
if (afxTraceFlags & traceAppMsg)
_AfxTraceMsg(_T("PumpMessage"), &m_msgCur);
#endif
// process this message
if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
{
::TranslateMessage(&m_msgCur); //键转换
::DispatchMessage(&m_msgCur); //派送消息
}
return TRUE;
}
在 这一步有一个特别重要的函数大家一定认识:PreTranslateMessage。这个函数在::DispatchMessage发送消息到窗口之前, 进行对消息的预处理。PreTranslateMessage函数是CWinThread的成员函数,大家重载的时候都是在View类或者主窗口类中,那 么,它是怎么进入别的类的呢?代码如下:
//thrdcore.cpp
BOOL CWinThread::PreTranslateMessage(MSG* pMsg)
{
ASSERT_VALID(this);
// 如果是线程消息,那么将会调用线程消息的处理函数
if (pMsg->hwnd == NULL && DispatchThreadMessageEx(pMsg))
return TRUE;
// walk from target to main window
CWnd* pMainWnd = AfxGetMainWnd();
if (CWnd::WalkPreTranslateTree(pMainWnd->GetSafeHwnd(), pMsg))
return TRUE;
// in case of modeless dialogs, last chance route through main
// window's accelerator table
if (pMainWnd != NULL)
{
CWnd* pWnd = CWnd::FromHandle(pMsg->hwnd);
if (pWnd->GetTopLevelParent() != pMainWnd)
return pMainWnd->PreTranslateMessage(pMsg);
}
return FALSE; // no special processing
}
由上面这个函数可以看出:
第一,如果(pMsg->hwnd == NULL),说明这是一个线程消息。调用CWinThread::DispatchThreadMessageEx到消息映射表找到消息入口,然后调用消息处理函数。
NOTE: 一般用PostThreadMessage函数发送线程之间的消息,他和窗口消息不同,需要指定线程id,消息激被系统放入到目标线程的消息队列中;用 ON_THREAD_MESSAGE( message, memberFxn )宏可以映射线程消息和他的处理函数。这个宏必须在应用程序类(从CWinThread继承)中,因为只有应用程序类才处理线程消息。如果你在别的类(比 如视图类)中用这个宏,线程消息的消息处理函数将得不到线程消息。
第二,消息的目标窗口的 PreTranslateMessage函数首先得到消息处理权,如果函数返回FALSE,那么他的父窗口将得到消息的处理权,直到主窗口;如果函数返回 TRUE(表示消息已经被处理了),那么就不需要调用父类的PreTranslateMessage函数。这样,保证了消息的目标窗口以及他的父窗口都可 以有机会调用PreTranslateMessage--在消息发送到窗口之前进行预处理(如果自己处理完然后返回FALSE的话 -_-b),如果你想要消息不传递给父类进行处理的话,返回TRUE就行了。
第三,如果消息的目标窗口和主窗口没有父子关系,那么再调用主 窗口的PreTranslateMessage函数。为什么这样?由第二步知道,一个窗口的父窗口不是主窗口的话,尽管它的 PreTranslateMessage返回FALSE,主窗口也没有机会调用PreTranslateMessage函数。我们知道,加速键的转换一般 在框架窗口的PreTranslateMessage函数中。我找遍了MFC中关于加速键转换的处理,只有CFrameWnd, CMDIFrameWnd,CMDIChildWnd等窗口类有。所以,第三步的意思是,如果消息的目标窗口(他的父窗口不是主窗口,比如一个这样的非模 式对话框)使消息的预处理继续漫游的话(他的PreTranslateMessage返回FALSE),那么给一次机会给主窗口调用 PreTranslateMessage(万一他是某个加速键消息呢?),这样能够保证在有非模式对话框的情况下还能保证主窗口的加速键好使。
我做了一个小例子,在对话框类的PreTranslateMessage中,返回FALSE。在主窗口显示这个非模式对话框,在对话框拥有焦点的时候,仍然能够激活主窗口的快捷键。
总之,整个框架就是让每个消息的目标窗口(包括他的父窗口)都有机会参与消息到来之前的处理。呵呵~
至 此,非对话框的消息循环和消息泵的机制就差不多了。这个机制在一个无限循环中,不断地从消息队列中获取消息,并且保证了程序的线程消息能够得到机会处理, 窗口消息在预处理之后被发送到相应的窗口处理过程。那么,还有一点疑问,为什么要一会儿调用::PeekMessage,一会儿调用:: GetMessage呢,他们有什么区别?
NOTE:一般来说,GetMessage被设计用来高效地从消息队列获取消息。如果队列中没有消息,那么函数GetMessage将导致线程休眠(让出CPU时间)。而PeekMessage是判断消息队列中如果没有消息,它马上返回0,不会导致线程处于睡眠状态。
在 上面的phase1第一个内循环中用到了PeekMessage,它的参数PM_NOREMOVE表示并不从消息队列中移走消息,而是一个检测查询,如果 消息队列中没有消息他立刻返回0,如果这时线程空闲的话将会引起消息循环调用OnIdle处理过程(上面讲到了这个函数的重要性)。如果将:: PeekMessage改成::GetMessage(***),那么如果消息队列中没有消息,线程将休眠,直到线程下一次获得CPU时间并且有消息出现 才可能继续执行,这样,消息循环的空闲时间没有得到应用,OnIdle也将得不到执行。这就是为什么既要用::PeekMessage(查询),又要 用::GetMessage(做实际的工作)的缘故。
第二部分: 对话框程序的消息循环机制
基于对话框的MFC工程和上面的消息循环机制不一样。实际上MFC的对话框工程程序就是模式对话框。他和上面讲到的非对话框程序的不同之处,主要在于应用程序对象的InitInstance()不一样。
//dlg_5Dlg.cpp
BOOL CDlg_5App::InitInstance()
{
AfxEnableControlContainer();
#ifdef _AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif
CDlg_5Dlg dlg; //定义一个对话框对象
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal(); //对话框的消息循环在这里面开始
if (nResponse == IDOK)
{
// TODO: Place code here to handle when the dialog is
// dismissed with OK
}
else if (nResponse == IDCANCEL)
{
// TODO: Place code here to handle when the dialog is
// dismissed with Cancel
}
// Since the dialog has been closed, return FALSE so that we exit the
// application, rather than start the application's message pump.
return FALSE;
}
NOTE: InitInstance函数返回FALSE,由最上面程序启动流程可以看出,CWinThread::Run是不会得到执行的。也就是说,上面第一部分 说的消息循环在对话框中是不能执行的。实际上,对话框也有消息循环,她的消息循环在CDialog::DoModal()虚函数中的一个 RunModalLoop函数中。
这个函数的实现体在CWnd类中:
int CWnd::RunModalLoop(DWORD dwFlags)
{
ASSERT(::IsWindow(m_hWnd)); // window must be created
ASSERT(!(m_nFlags & WF_MODALLOOP)); // window must not already be in modal state
// for tracking the idle time state
BOOL bIdle = TRUE;
LONG lIdleCount = 0;
BOOL bShowIdle = (dwFlags & MLF_SHOWONIDLE) && !(GetStyle() & WS_VISIBLE);
HWND hWndParent = ::GetParent(m_hWnd);
m_nFlags |= (WF_MODALLOOP|WF_CONTINUEMODAL);
MSG* pMsg = &AfxGetThread()->m_msgCur;
// acquire and dispatch messages until the modal state is done
for (;;)
{
ASSERT(ContinueModal());
// phase1: check to see if we can do idle work
while (bIdle &&
!::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE))
{
ASSERT(ContinueModal());
// show the dialog when the message queue goes idle
if (bShowIdle)
{
ShowWindow(SW_SHOWNORMAL);
UpdateWindow();
bShowIdle = FALSE;
}
// call OnIdle while in bIdle state
if (!(dwFlags & MLF_NOIDLEMSG) && hWndParent != NULL && lIdleCount == 0)
{
// send WM_ENTERIDLE to the parent
::SendMessage(hWndParent, WM_ENTERIDLE, MSGF_DIALOGBOX, (LPARAM)m_hWnd);
}
if ((dwFlags & MLF_NOKICKIDLE) ||
!SendMessage(WM_KICKIDLE, MSGF_DIALOGBOX, lIdleCount++))
{
// stop idle processing next time
bIdle = FALSE;
}
}
// phase2: pump messages while available
do
{
ASSERT(ContinueModal());
// pump message, but quit on WM_QUIT
//PumpMessage(消息泵)的实现和上面讲的差不多。都是派送消息到窗口。
if (!AfxGetThread()->PumpMessage())
{
AfxPostQuitMessage(0);
return -1;
}
// show the window when certain special messages rec'd
if (bShowIdle &&
(pMsg->message == 0x118 || pMsg->message == WM_SYSKEYDOWN))
{
ShowWindow(SW_SHOWNORMAL);
UpdateWindow();
bShowIdle = FALSE;
}
if (!ContinueModal())
goto ExitModal;
// reset "no idle" state after pumping "normal" message
if (AfxGetThread()->IsIdleMessage(pMsg))
{
bIdle = TRUE;
lIdleCount = 0;
}
} while (::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE));
} //无限循环
ExitModal:
m_nFlags &= ~(WF_MODALLOOP|WF_CONTINUEMODAL);
return m_nModalResult;
}
先说说怎么退出这个无限循环,在代码中:
if (!ContinueModal())
goto ExitModal;
决定是否退出循环,消息循环函数返回也就是快要结束结束程序了。
BOOL CWnd::ContinueModal()
{
return m_nFlags & WF_CONTINUEMODAL;
}
NOTE: CWnd::ContinueModal()函数检查对话框是否继续模式。返回TRUE,表示现在是模式的;返回FALSE,表示对话框已经不是模式(将要结束)。
如 果要结束对话框,在内部最终会调用函数CWnd::EndModalLoop,它取消m_nFlags的模式标志(消息循环中的 ContinueModal函数将返回FALSE,消息循环将结束,程序将退出);然后激发消息循环读取消息。也就是说,结束模式对话框是一个标志,改变 这个标志就可以了。他的代码是:
//wincore.cpp
void CWnd::EndModalLoop(int nResult)
{
ASSERT(::IsWindow(m_hWnd));
// this result will be returned from CWnd::RunModalLoop
m_nModalResult = nResult;
// make sure a message goes through to exit the modal loop
if (m_nFlags & WF_CONTINUEMODAL)
{
m_nFlags &= ~WF_CONTINUEMODAL;
PostMessage(WM_NULL);
}
}
NOTE: PostMessage(NULL)是有用的。如果消息队列中没有消息的话,可能消息循环中的ContinueModal()不会马上执行,发送一个空消息是激发消息循环马上工作。
下面说一下CWnd::RunModalLoop函数中的消息循环究竟干了些什么事情:
1, 第一个内循环。首先从消息队列中查询消息,如果对话框空闲,而且消息队列中没有消息,他做三件事情,大家应到都能从字面上明白什么意思。最重要的是发送 WM_KICKIDLE消息。为什么呢?第一部分讲到了,非对话框程序用OnIdle来更新用户界面(UI),比如工具栏,状态栏。那么,如果对话框中也 有工具栏和状态栏呢,在哪里更新(网上有很多这样的程序)?可以处理WM_KICKIDLE消息:
LRESULT CDlg_5Dlg::OnKickIdle(WPARAM w,LPARAM l)
{
//调用CWnd::UpdateDialogControls更新用户界面
UpdateDialogControls(this, TRUE);
return 0;
}
NOTE: CWnd::UpdateDialog函数发送CN_UPDATE_COMMAND_UI消息给所有的用户界面对话框控件。
2, 第二个内循环。最重要的还是PumpMessage派送消息到目标窗口。其他的,像第二个if语句,0x118消息好像是WM_SYSTIMER消息(系 统用来通知光标跳动的一个消息)。也就是说,如果消息为WM_SYSTIMER或者WM_SYSKEYDOWN,并且空闲显示标志为真的话,就显示窗口并 通知窗口立刻重绘。
总之,对话框的消息循环机制和非对话框(比如SDI,MDI)还是类似 的,仅仅侧重点不同。模式对话框是模式显示,自然有他的特点。下面部分讨论一下模式对话框和非模式对话框的区别。因为模式对话框有自己的特殊消息循环;而 非模式对话框,共用程序的消息循环,和普通的窗口已经没有什么大的区别了。
第三部分:模式对话框和非模式对话框的区别
这个话题已经有很多人讨论,我说说我所理解的意思。
在MFC 框架中,一个对话框对象DoModal一下就能产生一个模式对话框,Create一下就能产生一个非模式对话框。实际上,无论是模式对话框还是非模式对话 框,在MFC内部都是调用::CreateDialogIndirect(***)函数来创建非模式对话框。只是模式对话框作了更多的工作,包括使父窗口 无效,然后进入自己的消息循环等等。::CreateDialogIndirect(***)函数最终调用CreateWindowEx函数通知系统创建 窗体并返回句柄,他内部没有实现自己的消息循环。
非模式对话框创建之后立即返回,并且和主程序共用一个消息循环。非模式对话框要等对话框结束之后才返回,自己有消息循环。比如下面的代码:
CMyDlg* pdlg = new CMyDlg;
pdlg ->Create(IDD_DIALOG1);
pdlg->ShowWindow(SW_SHOW);
MessageBox("abc");
非模式对话框和消息框MessageBox几乎是同时弹出来。而如果将Create改成DoModal,那么,只能弹出模式对话框,在关闭了对话框之后(模式对话框自己的消息循环结束),消息框才弹出来。
NOTE: 可以在模式对话框中调用GetParent()->EnableWindow(true);这样,主窗口的菜单,工具栏又激活了,能用了。MFC使 用非模式对话框来模拟模式对话框,而在win32 SDK程序中,模式对话框激发他的父窗口Enable操作是没有效果的。
关于消息循环总结:
1, 我们站在一个什么高度看消息循环?消息循环其实没有什么深奥的道理。如果一个邮递员要不断在一个城市中送信,我们要求他做什么?要求他来回跑,但他一次只 能在一个地方出现。如果我们的应用程序只有一个线程的话,我们要他不断地为窗口传递消息,我们怎么做?在一个循环中不断的检测消息,并将他发送到适当的窗 口。窗口可以有很多个,但消息循环只有一个,而且每时每刻最多只有一个地方在执行代码。为什么? 看第二点。
2,因为是单线程的(程序进程 启动的时候,只有而且有一个线程,我们称他为主线程),所以就像邮递员一样,每次只能在某一个地方干活。什么意思呢?举个例子,用:: DiapatchMessage派送消息,在窗口处理过程(WinProc,窗口函数)返回之前,他是阻塞的,不会立即返回,也就是消息循环此时不能再从 消息队列中读取消息,直到::DispatchMessage返回。如果你在窗口函数中执行一个死循环操作,就算你用PostQuitMessage函数 退出,程序也会down掉。
while(1)
{
PostQuitMessage(0); //程序照样down.
}
所 以,当窗口函数处理没有返回的时候,消息循环是不会从消息队列中读取消息的。这也是为什么在模式对话框中要自己用无限循环来继续消息循环,因为这个无限循 环阻塞了原来的消息循环,所以,在这个无限循环中要用GetMessage,PeekMessage,DispatchMessage来从消息队列中读取 消息并派送消息了。要不然程序就不会响应了,这不是我们所希望的。
所以说,消息循环放在程序的什么的地方都基本上是过的去的,比如放在DLL里 面。但是,最好在任何时候,只有一个消息循环在工作(其他的都被阻塞了)。然后,我们要作好的一件事情,就是怎么从消息循环中退出!当然用WM_QUIT 是可以拉~(PostThreadMessage也是个好主意),这个消息循环退出后,可能程序退出,也可能会激活另外一个被阻塞的消息循环,程序继续运 行。这要看你怎么想,怎么去做。最后一个消息循环结束的时候,也许就是程序快结束的时候,因为主线程的执行代码也快要完了(除非BT的再作个死循环)。
NOTE: 让windows系统知道创建一个线程的唯一方法是调用API CreatThread函数(__beginthreadex之类的都要在内部调用他创建新线程)。好像windows核心编程说,在win2000下, 系统用CreateRemoteThread函数来创建线程,CreateThread在内部调用CreateRemoteThread。不过这不是争论 的焦点,至少win98下CreateRemoteThread并不能正常工作,还是CreateThread主持大局。
3,在整个消息循环的机制中,还必须谈到窗口函数的可重入性。什么意思?就是窗口函数(他是个回调函数)的代码什么时候都可以被系统(调用者一般是user32模块)调用。比如在窗口过程中,向自己的窗口SendMessage(***);那么执行过程是怎样的?
我们知道,SendMessage是要等到消息发送并被目标窗口执行完之后才返回的。那么窗口在处理消息,然后又等待刚才发送到本窗口的消息被处理后之后(SendMessage返回)才继续往下执行,程序不就互相死锁了吗?
其 实是不会的。windows设计一套适合SendMessage的算法,他判断如果发送的消息是属于本线程创建的窗口的,那么直接由user32模块调用 窗口函数(可能就有窗口重入),并将消息的处理结果结果返回。这样做体现了窗口重入。上面的例子,我们调用SendMessage(***)发送消息到本 窗口,那么窗口过程再次被调用,处理完消息之后将结果返回,然后SendMessage之后的程序接着执行。对于非队列消息,如果没有窗口重入,不知道会 是什么样子。
NOTE: 由于窗口的可重入性。在win32 SDK程序中应尽量少用全局变量和静态变量,因为在窗口函数执行过程中可能窗口重入,如果重入后将这些变量改了,但你的程序在窗口重入返回之后继续执行, 可能就是使用已经改变的全局或静态变量。在MFC中(所有窗口的窗口函数基本上都是AfxWndProc),按照类的思想进行了组织,一般变量都是类中 的,好管理的多。
4,MFC中窗口类(比如C**View,CFrameWnd等)中的MessageBox函数,以及 AfxMessageBox函数都是阻塞原有的消息循环的。由消息框内部的一个消息循环来从消息队列中读取消息,并派送消息(和模式对话框类似)。实际 上,这些消息函数最终调用的是::MessageBox,它在消息框内部实现了一个消息循环(原有的主程序消息循环被阻塞了)。论坛中碰到过几次关于计时 器和消息框的问题,看下面的代码:
void CTest_recalclayoutView::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
MessageBox("abc");
while(1); //设计一个死循环
CView::OnTimer(nIDEvent);
}
咱 让OnTimer大约5秒钟弹出一个消息框。那么,消息框不断的被弹出来,只要消息框不被关闭,那么程序就不会进入死循环。实际上,每次弹出对话框,都是 最上层的那个消息框掌握着消息循环,其他的消息循环被阻塞了。只要不关闭最上面的消息框,while(1);就得不到执行。如果点了关闭,程序就进入了死 循环,只能用ctrl+alt+del来解决问题了。
5,消息循环在很多地方都有应用。比如应用在线程池中。一个线程的执行周期一般在线程 函数返回之后结束,那么怎么延长线程的生命周期呢?一种方法就是按照消息循环的思想,在线程中加入消息循环,不断地从线程队列读取消息,并处理消息,线程 的生命周期就保持着直到这个消息循环的退出。
NOTE:只要线程有界面元素或者调用GetMessage,或者有线程消息发送过来,系统就会为线程创建一个消息队列。
6, 在单线程程序中,如果要执行一个长时间的复杂操作而且界面要有相应的话,可以考虑用自己的消息泵。比如,可以将一个阻塞等待操作放在一个循环中,并将超时 值设置得比较小,然后每个等待的片段中用消息泵继续消息循环,使界面能够响应用户操作。等等之类,都可以应用消息泵(调用一个类似这样的函数):
BOOL CChildView::PeekAndPump()
{
MSG msg;
while(::PeekMessage(&msg,NULL,0,0,PM_NOREMOVE))
{
if(!AfxGetApp()->PumpMessage())
{
::PostQuitMessage(0);
return false;
}
}
return true;
}
其实,用多线程也能解决复杂运算时的界面问题,但是没有这么方便,而且一般要加入线程通信和同步,考虑的事情更多一点。
综上所述,MFC消息循环就那么回事,主要思想还是和SDK中差不多。这种思想主要的特点表现在迎合MFC整个框架上,为整个框架服务,为应用和功能服务。这是我的理解。呵呵~
1. 你们的项目组使用源代码管理工具了么?
MVM:应该用。VSS、CVS、PVCS、ClearCase、CCC/Harvest、FireFly都可以。我的选择是VSS。
2. 你们的项目组使用缺陷管理系统了么?
MVM:应该用。ClearQuest太复杂,我的推荐是BugZilla。
3. 你们的测试组还在用Word写测试用例么?
MVM:不要用Word写测试用例(Test Case)。应该用一个专门的系统,可以是Test Manager,也可以是自己开发一个ASP.NET的小网站。主要目的是Track和Browse。
4. 你们的项目组有没有建立一个门户网站?
MVM:要有一个门户网站,用来放Contact Info、Baselined Schedule、News等等。推荐Sharepoint Portal Server 2003来实现,15分钟就搞定。买不起SPS 2003可以用WSS (Windows Sharepoint Service)。
5. 你们的项目组用了你能买到最好的工具么?
MVM:应该用尽量好的工具来工作。比如,应该用VS.NET而不是Notepad来写C#。用Notepad写程序多半只是一种炫耀。但也要考虑到经费,所以说是“你能买到最好的”。
6. 你们的程序员工作在安静的环境里么?
MVM:需要安静环境。这点极端重要,而且要保证每个人的空间大于一定面积。
7. 你们的员工每个人都有一部电话么?
MVM:需要每人一部电话。而且电话最好是带留言功能的。当然,上这么一套带留言电话系统开销不小。不过至少每人一部电话要有,千万别搞得经常有人站起来喊:“某某某电话”。《人件》里面就强烈谴责这种做法。
8. 你们每个人都知道出了问题应该找谁么?
MVM:应该知道。任何一个Feature至少都应该有一个Owner,当然,Owner可以继续Dispatch给其他人。
9. 你遇到过有人说“我以为…”么?
MVM:要消灭“我以为”。Never assume anything。
10. 你们的项目组中所有的人都坐在一起么?
MVM:需要。我反对Virtual Team,也反对Dev在美国、Test在中国这种开发方式。能坐在一起就最好坐在一起,好处多得不得了。
11. 你们的进度表是否反映最新开发进展情况?
MVM:应该反映。但是,应该用Baseline的方法来管理进度表:维护一份稳定的Schedule,再维护一份最新更改。Baseline的方法也应该用于其它的Spec。Baseline是变更管理里面的一个重要手段。
12. 你们的工作量是先由每个人自己估算的么?
MVM:应该让每个人自己估算。要从下而上估算工作量,而不是从上往下分派。除非有其他原因,比如政治任务工期固定等。
13. 你们的开发人员从项目一开始就加班么?
MVM:不要这样。不要一开始就搞疲劳战。从项目一开始就加班,只能说明项目进度不合理。当然,一些对日软件外包必须天天加班,那属于剥削的范畴。
14. 你们的项目计划中Buffer Time是加在每个小任务后面的么?
MVM:不要。Buffer Time加在每个小任务后面,很容易轻易的就被消耗掉。Buffer Time要整段的加在一个Milestone或者checkpoint前面。
15. 值得再多花一些时间,从95%做到100%好
MVM:值得,非常值得。尤其当项目后期人困马乏的时候,要坚持。这会给产品带来质的区别。
16. 登记新缺陷时,是否写清了重现步骤?
MVM:要。这属于Dev和Test之间的沟通手段。面对面沟通需要,详细填写Repro Steps也需要。
17. 写新代码前会把已知缺陷解决么?
MVM:要。每个人的缺陷不能超过10个或15个,否则必须先解决老的bug才能继续写新代码。
18. 你们对缺陷的轻重缓急有事先的约定么?
MVM:必须有定义。Severity要分1、2、3,约定好:蓝屏和Data Lost算Sev 1,Function Error算Sev 2,界面上的算Sev 3。但这种约定可以根据产品质量现状适当进行调整。
19. 你们对意见不一的缺陷有三国会议么?
MVM:必须要有。要有一个明确的决策过程。这类似于CCB (Change Control Board)的概念。
20. 所有的缺陷都是由登记的人最后关闭的么?
MVM:Bug应该由Opener关闭。Dev不能私自关闭Bug。
21. 你们的程序员厌恶修改老的代码么?
MVM:厌恶是正常的。解决方法是组织Code Review,单独留出时间来。XP也是一个方法。
22. 你们项目组有Team Morale Activity么?
MVM:每个月都要搞一次,吃饭、唱歌、Outing、打球、开卡丁车等等,一定要有。不要剩这些钱。
23. 你们项目组有自己的Logo么?
MVM:要有自己的Logo。至少应该有自己的Codename。
24. 你们的员工有印有公司Logo的T-Shirt么?
MVM:要有。能增强归属感。当然,T-Shirt要做的好看一些,最好用80支的棉来做。别没穿几次就破破烂烂的。
25. 总经理至少每月参加次项目组会议
MVM:要的。要让team member觉得高层关注这个项目。
26. 你们是给每个Dev开一个分支么?
MVM:反对。Branch的管理以及Merge的工作量太大,而且容易出错。
27. 有人长期不Check-In代码么?
MVM:不可以。对大部分项目来说,最多两三天就应该Check-In。
28. 在Check-In代码时都填写注释了么?
MVM:要写的,至少一两句话,比如“解决了Bug No.225”。如果往高处拔,这也算做“配置审计”的一部分。
29. 有没有设定每天Check-In的最后期限?
MVM:要的,要明确Check-In Deadline。否则会Build Break。
30. 你们能把所有源码一下子编译成安装文件吗?
MVM:要的。这是每日编译(Daily Build)的基础。而且必须要能够做成自动的。
31. 你们的项目组做每日编译么?
MVM:当然要做。有三样东西是软件项目/产品开发必备的:1. bug management; 2. source control; 3. daily build。
32. 你们公司有没有积累一个项目风险列表?
MVM:要。Risk Inventory。否则,下个项目开始的时候,又只能拍脑袋分析Risk了。
33. 设计越简单越好
MVM:越简单越好。设计时候多一句话,将来可能就带来无穷无尽的烦恼。应该从一开始就勇敢的砍。这叫scope management。
34. 尽量利用现有的产品、技术、代码
MVM:千万别什么东西都自己Coding。BizTalk和Sharepoint就是最好的例子,有这两个作为基础,可以把起点提高很多。或者可以尽量多用现成的Control之类的。或者尽量用XML,而不是自己去Parse一个文本文件;尽量用RegExp,而不是自己从头操作字符串,等等等等。这就是“软件复用”的体现。
35. 你们会隔一段时间就停下来夯实代码么?
MVM:要。最好一个月左右一次。传言去年年初Windows组在Stevb的命令下停过一个月增强安全。Btw,“夯”这个字念“hang”,第一声。
36. 你们的项目组每个人都写Daily Report么?
MVM:要写。五分钟就够了,写10句话左右,告诉自己小组的人今天我干了什么。一则为了沟通,二则鞭策自己(要是游手好闲一天,自己都会不好意思写的)。
37. 你们的项目经理会发出Weekly Report么?
MVM:要。也是为了沟通。内容包括目前进度,可能的风险,质量状况,各种工作的进展等。
38. 你们项目组是否至少每周全体开会一次?
MVM:要。一定要开会。程序员讨厌开会,但每个礼拜开会时间加起来至少应该有4小时。包括team meeting, spec review meeting, bug triage meeting。千万别大家闷头写code。
39. 你们项目组的会议、讨论都有记录么?
MVM:会前发meeting request和agenda,会中有人负责主持和记录,会后有人负责发meeting minutes,这都是effective meeting的要点。而且,每个会议都要形成agreements和action items。
40. 其他部门知道你们项目组在干什么么?
MVM:要发一些Newsflash给整个大组织。Show your team’s value。否则,当你坐在电梯里面,其他部门的人问:“你们在干嘛”,你回答“ABC项目”的时候,别人全然不知,那种感觉不太好。
41. 通过Email进行所有正式沟通
MVM:Email的好处是免得抵赖。但也要避免矫枉过正,最好的方法是先用电话和当面说,然后Email来确认。
42. 为项目组建立多个Mailing Group
MVM:如果在AD+Exchange里面,就建Distribution List。比如,我会建ABC Project Core Team,ABC Project Dev Team,ABC Project All Testers,ABC Project Extended Team等等。这样发起Email来方便,而且能让该收到email的人都收到、不该收到不被骚扰。
43. 每个人都知道哪里可以找到全部的文档么?
MVM:应该每个人都知道。这叫做知识管理(Knowledge Management)。最方便的就是把文档放在一个集中的File Share,更好的方法是用Sharepoint。
44. 你做决定、做变化时,告诉大家原因了么?
MVM:要告诉大家原因。Empower team member的手段之一是提供足够的information,这是MSF一开篇的几个原则之一。的确如此,tell me why是人之常情,tell me why了才能有understanding。中国人做事喜欢搞限制,限制信息,似乎能够看到某一份文件的人就是有身份的人。大错特错。权威、权力,不在于是不是能access information/data,而在于是不是掌握资源。
45. Stay agile and expect change
MVM:要这样。需求一定会变的,已经写好的代码一定会被要求修改的。做好心理准备,对change不要抗拒,而是expect change。
46. 你们有没有专职的软件测试人员?
MVM:要有专职测试。如果人手不够,可以peer test,交换了测试。千万别自己测试自己的。
47. 你们的测试有一份总的计划来规定做什么和怎么做么?
MVM:这就是Test Plan。要不要做性能测试?要不要做Usability测试?什么时候开始测试性能?测试通过的标准是什么?用什么手段,自动的还是手动的?这些问题需要用Test Plan来回答。
48. 你是先写Test Case然后再测试的么?
MVM:应该如此。应该先设计再编程、先test case再测试。当然,事情是灵活的。我有时候在做第一遍测试的同时补上test case。至于先test case再开发,我不喜欢,因为不习惯,太麻烦,至于别人推荐,那试试看也无妨。
49. 你是否会为各种输入组合创建测试用例?
MVM:不要,不要搞边界条件组合。当心组合爆炸。有很多test case工具能够自动生成各种边界条件的组合——但要想清楚,你是否有时间去运行那么多test case。
50. 你们的程序员能看到测试用例么?
MVM:要。让Dev看到Test Case吧。我们都是为了同一个目的走到一起来的:提高质量。
51. 你们是否随便抓一些人来做易用性测试?
MVM:要这么做。自己看自己写的程序界面,怎么看都是顺眼的。这叫做审美疲劳——臭的看久了也就不臭了,不方便的永久了也就习惯了。
52. 你对自动测试的期望正确么?
MVM:别期望太高。依我看,除了性能测试以外,还是暂时先忘掉“自动测试”吧,忘掉WinRunner和LoadRunner吧。对于国内的软件测试的现状来说,只能“矫枉必须过正”了。
53. 你们的性能测试是等所有功能都开发完才做的么?
MVM:不能这样。性能测试不能被归到所谓的“系统测试”阶段。早测早改正,早死早升天。
54. 你注意到测试中的杀虫剂效应了么?
MVM:虫子有抗药性,Bug也有。发现的新Bug越来越少是正常的。这时候,最好大家交换一下测试的area,或者用用看其他工具和手法,就又会发现一些新bug了。
55. 你们项目组中有人能说出产品的当前整体质量情况么?
MVM:要有。当老板问起这个产品目前质量如何,Test Lead/Manager应该负责回答。
56. 你们有单元测试么?
MVM:单元测试要有的。不过没有单元测试也不是不可以,我做过没有单元测试的项目,也做成功了——可能是侥幸,可能是大家都是熟手的关系。还是那句话,软件工程是非常实践、非常工程、非常灵活的一套方法,某些方法在某些情况下会比另一些方法好,反之亦然。
57. 你们的程序员是写完代码就扔过墙的么?
MVM:大忌。写好一块程序以后,即便不做单元测试,也应该自己先跑一跑。虽然有了专门的测试人员,做开发的人也不可以一点测试都不做。微软还有Test Release Document的说法,程序太烂的话,测试有权踢回去。
58. 你们的程序中所有的函数都有输入检查么?
MVM:不要。虽然说做输入检查是write secure code的要点,但不要做太多的输入检查,有些内部函数之间的参数传递就不必检查输入了,省点功夫。同样的道理,未必要给所有的函数都写注释。写一部分主要的就够了。
59. 产品有统一的错误处理机制和报错界面么?
MVM:要有。最好能有统一的error message,然后每个error message都带一个error number。这样,用户可以自己根据error number到user manual里面去看看错误的具体描述和可能原因,就像SQL Server的错误那样。同样,ASP.NET也要有统一的Exception处理。可以参考有关的Application Block。
60. 你们有统一的代码书写规范么?
MVM:要有。Code Convention很多,搞一份来发给大家就可以了。当然,要是有FxCop这种工具来检查代码就更好了。
61. 你们的每个人都了解项目的商业意义么?
MVM:要。这是Vision的意思。别把项目只当成工作。有时候要想着自己是在为中国某某行业的信息化作先驱者,或者时不时的告诉team member,这个项目能够为某某某国家部门每年节省多少多少百万的纳税人的钱,这样就有动力了。平凡的事情也是可以有个崇高的目标的。
62. 产品各部分的界面和操作习惯一致么?
MVM:要这样。要让用户觉得整个程序好像是一个人写出来的那样。
63. 有可以作为宣传亮点的Cool Feature么?
MVM:要。这是增强团队凝聚力、信心的。而且,“一俊遮百丑”,有亮点就可以掩盖一些问题。这样,对于客户来说,会感觉产品从质量角度来说还是acceptable的。或者说,cool feature或者说亮点可以作为质量问题的一个事后弥补措施。
64. 尽可能缩短产品的启动时间
MVM:要这样。软件启动时间(Start-Up time)是客户对性能好坏的第一印象。
65. 不要过于注重内在品质而忽视了第一眼的外在印象
MVM:程序员容易犯这个错误:太看重性能、稳定性、存储效率,但忽视了外在感受。而高层经理、客户正相反。这两方面要兼顾,协调这些是PM的工作。
66. 你们根据详细产品功能说明书做开发么?
MVM:要这样。要有设计才能开发,这是必须的。设计文档,应该说清楚这个产品会怎么运行,应该采取一些讲故事的方法。设计的时候千万别钻细节,别钻到数据库、代码等具体实现里面去,那些是后面的事情,一步步来不能着急。
67. 开始开发和测试之前每个人都仔细审阅功能设计么?
MVM:要做。Function Spec review是用来统一思想的。而且,review过以后形成了一致意见,将来再也没有人可以说“你看,当初我就是反对这么设计的,现在吃苦头了吧”
68. 所有人都始终想着The Whole Image么?
MVM:要这样。项目里面每个人虽然都只是在制造一片叶子,但每个人都应该知道自己在制造的那片叶子所在的树是怎么样子的。我反对软件蓝领,反对过分的把软件制造看成流水线、车间。参见第61条。
69. Dev工作的划分是单纯纵向或横向的么?
MVM:不能单纯的根据功能模块分,或者单纯根据表现层、中间层、数据库层分。我推荐这么做:首先根据功能模块分,然后每个“层”都有一个Owner来Review所有人的设计和代码,保证consistency。
70. 你们的程序员写程序设计说明文档么?
MVM:要。不过我听说微软的程序员1999年以前也不写。所以说,写不写也不是绝对的,偷懒有时候也是可以的。参见第56条。
71. 你在招人面试时让他写一段程序么?
MVM:要的。我最喜欢让人做字符串和链表一类的题目。这种题目有很多循环、判断、指针、递归等,既不偏向过于考算法,也不偏向过于考特定的API。
72. 你们有没有技术交流讲座?
MVM:要的。每一两个礼拜搞一次内部的Tech Talk或者Chalk Talk吧。让组员之间分享技术心得,这笔花钱送到外面去培训划算。
73. 你们的程序员都能专注于一件事情么?
MVM:要让程序员专注一件事。例如说,一个部门有两个项目和10个人,一种方法是让10个人同时参加两个项目,每个项目上每个人都花50%时间;另一种方法是5个人去项目A,5个人去项目B,每个人都100%在某一个项目上。我一定选后面一种。这个道理很多人都懂,但很多领导实践起来就把属下当成可以任意拆分的资源了。
74. 你们的程序员会夸大完成某项工作所需要的时间么?
MVM:会的,这是常见的,尤其会在项目后期夸大做某个change所需要的时间,以次来抵制change。解决的方法是坐下来慢慢磨,磨掉程序员的逆反心理,一起分析,并把估算时间的颗粒度变小。
75. 尽量不要用Virtual Heads
MVM:最好不要用Virtual Heads。Virtual heads意味着resource is not secure,shared resource会降低resource的工作效率,容易增加出错的机会,会让一心二用的人没有太多时间去review spec、review design。一个dedicated的人,要强过两个只能投入50%时间和精力的人。我是吃过亏的:7个part time的tester,发现的Bug和干的活,加起来还不如两个full-time的。参见第73条。73条是针对程序员的,75条是针对Resource Manager的。
创建型模式
1、FACTORY —追MM少不了请吃饭了,麦当劳的鸡翅和肯德基的鸡翅都是MM爱吃的东西,虽然口味有所不同,但不管你带MM去麦当劳或肯德基,只管向服务员说“来四个鸡翅”就行了。麦当劳和肯德基就是生产鸡翅的Factory
工厂模式:客户类和工厂类分开。消费者任何时候需要某种产品,只需向工厂请求即可。消费者无须修改就可以接纳新产品。缺点是当产品修改时,工厂类也要做相应的修改。如:如何创建及如何向客户端提供。
2、BUILDER —MM最爱听的就是“我爱你”这句话了,见到不同地方的MM,要能够用她们的方言跟她说这句话哦,我有一个多种语言翻译机,上面每种语言都有一个按键,见到MM我只要按对应的键,它就能够用相应的语言说出“我爱你”这句话了,国外的MM也可以轻松搞掂,这就是我的“我爱你”builder。(这一定比美军在伊拉克用的翻译机好卖)
建造模式:将产品的内部表象和产品的生成过程分割开来,从而使一个建造过程生成具有不同的内部表象的产品对象。建造模式使得产品内部表象可以独立的变化,客户不必知道产品内部组成的细节。建造模式可以强制实行一种分步骤进行的建造过程。
3、FACTORY METHOD —请MM去麦当劳吃汉堡,不同的MM有不同的口味,要每个都记住是一件烦人的事情,我一般采用Factory Method模式,带着MM到服务员那儿,说“要一个汉堡”,具体要什么样的汉堡呢,让MM直接跟服务员说就行了。
工厂方法模式:核心工厂类不再负责所有产品的创建,而是将具体创建的工作交给子类去做,成为一个抽象工厂角色,仅负责给出具体工厂类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。
4、PROTOTYPE —跟MM用QQ聊天,一定要说些深情的话语了,我搜集了好多肉麻的情话,需要时只要copy出来放到QQ里面就行了,这就是我的情话prototype了。(100块钱一份,你要不要)
原始模型模式:通过给出一个原型对象来指明所要创建的对象的类型,然后用复制这个原型对象的方法创建出更多同类型的对象。原始模型模式允许动态的增加或减少产品类,产品类不需要非得有任何事先确定的等级结构,原始模型模式适用于任何的等级结构。缺点是每一个类都必须配备一个克隆方法。
5、SINGLETON —俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公Sigleton,她们只要说道“老公”,都是指的同一个人,那就是我(刚才做了个梦啦,哪有这么好的事)
单例模式:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例单例模式。单例模式只应在有真正的“单一实例”的需求时才可使用。
结构型模式
6、ADAPTER —在朋友聚会上碰到了一个美女Sarah,从香港来的,可我不会说粤语,她不会说普通话,只好求助于我的朋友kent了,他作为我和Sarah之间的Adapter,让我和Sarah可以相互交谈了(也不知道他会不会耍我)
适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。
7、BRIDGE —早上碰到MM,要说早上好,晚上碰到MM,要说晚上好;碰到MM穿了件新衣服,要说你的衣服好漂亮哦,碰到MM新做的发型,要说你的头发好漂亮哦。不要问我“早上碰到MM新做了个发型怎么说”这种问题,自己用BRIDGE组合一下不就行了
桥梁模式:将抽象化与实现化脱耦,使得二者可以独立的变化,也就是说将他们之间的强关联变成弱关联,也就是指在一个软件系统的抽象化和实现化之间使用组合/聚合关系而不是继承关系,从而使两者可以独立的变化。
8、COMPOSITE —Mary今天过生日。“我过生日,你要送我一件礼物。”“嗯,好吧,去商店,你自己挑。”“这件T恤挺漂亮,买,这条裙子好看,买,这个包也不错,买。”“喂,买了三件了呀,我只答应送一件礼物的哦。”“什么呀,T恤加裙子加包包,正好配成一套呀,小姐,麻烦你包起来。”“……”,MM都会用Composite模式了,你会了没有?
合成模式:合成模式将对象组织到树结构中,可以用来描述整体与部分的关系。合成模式就是一个处理对象的树结构的模式。合成模式把部分与整体的关系用树结构表示出来。合成模式使得客户端把一个个单独的成分对象和由他们复合而成的合成对象同等看待。
9、DECORATOR —Mary过完轮到Sarly过生日,还是不要叫她自己挑了,不然这个月伙食费肯定玩完,拿出我去年在华山顶上照的照片,在背面写上“最好的的礼物,就是爱你的Fita”,再到街上礼品店买了个像框(卖礼品的MM也很漂亮哦),再找隔壁搞美术设计的Mike设计了一个漂亮的盒子装起来……,我们都是Decorator,最终都在修饰我这个人呀,怎么样,看懂了吗?
装饰模式:装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案,提供比继承更多的灵活性。动态给一个对象增加功能,这些功能可以再动态的撤消。增加由一些基本功能的排列组合而产生的非常大量的功能。
10、FAÇADE —我有一个专业的Nikon相机,我就喜欢自己手动调光圈、快门,这样照出来的照片才专业,但MM可不懂这些,教了半天也不会。幸好相机有Facade设计模式,把相机调整到自动档,只要对准目标按快门就行了,一切由相机自动调整,这样MM也可以用这个相机给我拍张照片了。
门面模式:外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。每一个子系统只有一个门面类,而且此门面类只有一个实例,也就是说它是一个单例模式。但整个系统可以有多个门面类。
11、FLYWEIGHT —每天跟MM发短信,手指都累死了,最近买了个新手机,可以把一些常用的句子存在手机里,要用的时候,直接拿出来,在前面加上MM的名字就可以发送了,再不用一个字一个字敲了。共享的句子就是Flyweight,MM的名字就是提取出来的外部特征,根据上下文情况使用。
享元模式:FLYWEIGHT在拳击比赛中指最轻量级。享元模式以共享的方式高效的支持大量的细粒度对象。享元模式能做到共享的关键是区分内蕴状态和外蕴状态。内蕴状态存储在享元内部,不会随环境的改变而有所不同。外蕴状态是随环境的改变而改变的。外蕴状态不能影响内蕴状态,它们是相互独立的。将可以共享的状态和不可以共享的状态从常规类中区分开来,将不可以共享的状态从类里剔除出去。客户端不可以直接创建被共享的对象,而应当使用一个工厂对象负责创建被共享的对象。享元模式大幅度的降低内存中对象的数量。
12、PROXY —跟MM在网上聊天,一开头总是“hi,你好”,“你从哪儿来呀?”“你多大了?”“身高多少呀?”这些话,真烦人,写个程序做为我的Proxy吧,凡是接收到这些话都设置好了自动的回答,接收到其他的话时再通知我回答,怎么样,酷吧。
代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。客户端分辨不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象,而仅仅持有一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的其他角色代为创建并传入。
行为模式
13、CHAIN OF RESPONSIBLEITY —晚上去上英语课,为了好开溜坐到了最后一排,哇,前面坐了好几个漂亮的MM哎,找张纸条,写上“Hi,可以做我的女朋友吗?如果不愿意请向前传”,纸条就一个接一个的传上去了,糟糕,传到第一排的MM把纸条传给老师了,听说是个老处女呀,快跑!
责任链模式:在责任链模式中,很多对象由每一个对象对其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的情况下动态的重新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象所接受。
14、COMMAND —俺有一个MM家里管得特别严,没法见面,只好借助于她弟弟在我们俩之间传送信息,她对我有什么指示,就写一张纸条让她弟弟带给我。这不,她弟弟又传送过来一个COMMAND,为了感谢他,我请他吃了碗杂酱面,哪知道他说:“我同时给我姐姐三个男朋友送COMMAND,就数你最小气,才请我吃面。”,
命令模式:命令模式把一个请求或者操作封装到一个对象中。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。命令模式允许请求的一方和发送的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否执行,何时被执行以及是怎么被执行的。系统支持命令的撤消。
15、INTERPRETER —俺有一个《泡MM真经》,上面有各种泡MM的攻略,比如说去吃西餐的步骤、去看电影的方法等等,跟MM约会时,只要做一个Interpreter,照着上面的脚本执行就可以了。
解释器模式:给定一个语言后,解释器模式可以定义出其文法的一种表示,并同时提供一个解释器。客户端可以使用这个解释器来解释这个语言中的句子。解释器模式将描述怎样在有了一个简单的文法后,使用模式设计解释这些语句。在解释器模式里面提到的语言是指任何解释器对象能够解释的任何组合。在解释器模式中需要定义一个代表文法的命令类的等级结构,也就是一系列的组合规则。每一个命令对象都有一个解释方法,代表对命令对象的解释。命令对象的等级结构中的对象的任何排列组合都是一个语言。
16、ITERATOR —我爱上了Mary,不顾一切的向她求婚。
Mary:“想要我跟你结婚,得答应我的条件”
我:“什么条件我都答应,你说吧”
Mary:“我看上了那个一克拉的钻石”
我:“我买,我买,还有吗?”
Mary:“我看上了湖边的那栋别墅”
我:“我买,我买,还有吗?”
Mary:“你的小弟弟必须要有50cm长”
我脑袋嗡的一声,坐在椅子上,一咬牙:“我剪,我剪,还有吗?”
……
迭代子模式:迭代子模式可以顺序访问一个聚集中的元素而不必暴露聚集的内部表象。多个对象聚在一起形成的总体称之为聚集,聚集对象是能够包容一组对象的容器对象。迭代子模式将迭代逻辑封装到一个独立的子对象中,从而与聚集本身隔开。迭代子模式简化了聚集的界面。每一个聚集对象都可以有一个或一个以上的迭代子对象,每一个迭代子的迭代状态可以是彼此独立的。迭代算法可以独立于聚集角色变化。
17、MEDIATOR —四个MM打麻将,相互之间谁应该给谁多少钱算不清楚了,幸亏当时我在旁边,按照各自的筹码数算钱,赚了钱的从我这里拿,赔了钱的也付给我,一切就OK啦,俺得到了四个MM的电话。
调停者模式:调停者模式包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用。从而使他们可以松散偶合。当某些对象之间的作用发生改变时,不会立即影响其他的一些对象之间的作用。保证这些作用可以彼此独立的变化。调停者模式将多对多的相互作用转化为一对多的相互作用。调停者模式将对象的行为和协作抽象化,把对象在小尺度的行为上与其他对象的相互作用分开处理。
18、MEMENTO —同时跟几个MM聊天时,一定要记清楚刚才跟MM说了些什么话,不然MM发现了会不高兴的哦,幸亏我有个备忘录,刚才与哪个MM说了什么话我都拷贝一份放到备忘录里面保存,这样可以随时察看以前的记录啦。
备忘录模式:备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下,将一个对象的状态捉住,并外部化,存储起来,从而可以在将来合适的时候把这个对象还原到存储起来的状态。
19、OBSERVER —想知道咱们公司最新MM情报吗?加入公司的MM情报邮件组就行了,tom负责搜集情报,他发现的新情报不用一个一个通知我们,直接发布给邮件组,我们作为订阅者(观察者)就可以及时收到情报啦
观察者模式:观察者模式定义了一种一队多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使他们能够自动更新自己。
20、STATE —跟MM交往时,一定要注意她的状态哦,在不同的状态时她的行为会有不同,比如你约她今天晚上去看电影,对你没兴趣的MM就会说“有事情啦”,对你不讨厌但还没喜欢上的MM就会说“好啊,不过可以带上我同事么?”,已经喜欢上你的MM就会说“几点钟?看完电影再去泡吧怎么样?”,当然你看电影过程中表现良好的话,也可以把MM的状态从不讨厌不喜欢变成喜欢哦。
状态模式:状态模式允许一个对象在其内部状态改变的时候改变行为。这个对象看上去象是改变了它的类一样。状态模式把所研究的对象的行为包装在不同的状态对象里,每一个状态对象都属于一个抽象状态类的一个子类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变。状态模式需要对每一个系统可能取得的状态创立一个状态类的子类。当系统的状态变化时,系统便改变所选的子类。
21、STRATEGY —跟不同类型的MM约会,要用不同的策略,有的请电影比较好,有的则去吃小吃效果不错,有的去海边浪漫最合适,单目的都是为了得到MM的芳心,我的追MM锦囊中有好多Strategy哦。
策略模式:策略模式针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。策略模式把行为和环境分开。环境类负责维持和查询行为类,各种算法在具体的策略类中提供。由于算法和环境独立开来,算法的增减,修改都不会影响到环境和客户端。
22、TEMPLATE METHOD ——看过《如何说服女生上床》这部经典文章吗?女生从认识到上床的不变的步骤分为巧遇、打破僵局、展开追求、接吻、前戏、动手、爱抚、进去八大步骤(Template method),但每个步骤针对不同的情况,都有不一样的做法,这就要看你随机应变啦(具体实现);
模板方法模式:模板方法模式准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架,而将逻辑的细节留给具体的子类去实现。
23、VISITOR —情人节到了,要给每个MM送一束鲜花和一张卡片,可是每个MM送的花都要针对她个人的特点,每张卡片也要根据个人的特点来挑,我一个人哪搞得清楚,还是找花店老板和礼品店老板做一下Visitor,让花店老板根据MM的特点选一束花,让礼品店老板也根据每个人特点选一张卡,这样就轻松多了;
访问者模式:访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由的演化。访问者模式使得增加新的操作变的很容易,就是增加一个新的访问者类。访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。当使用访问者模式时,要将尽可能多的对象浏览逻辑放在访问者类中,而不是放到它的子类中。访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。
正则表达式是使用一套特殊符号模式做为表达格式的字符串,主要用处是描述和解析文本。许多程序员(甚至一些不错的高手)都无视(也不用)正则表达式,我认为这是一个耻辱,因为在解决很多问题的时候,正则表达式常常让我们有得心应手的感觉。一旦你掌握了,就会发现它能解决无数真实世界的问题。
正则表达式的工作方式就象Windows或者*nix系统里面的文件名替代符 - 你可以使用特定的*或者?来指定一系列文件。但是使用正则表达式的特殊字符或者metacharacters(元字符)来表示这类事情会更准确。
正则表达式把大多数字符当作直接字符,就好像正则表达式 mike,将只会匹配按顺序的字符序列m - i - k - e。与此同时正则表达式使用一个采用元字符的扩展集合,可以表示非常复杂的文字匹配。
认识元字符: ^[](){}.*?\|+$ 以及在某些时候出现的 -
我知道它们看上去很恐怖,但是一旦你了解它们就会知道它们是很可爱的符号。
行定位点: ‘^’ 和 ‘$’
‘^’ (读成:caret) 和 ‘$’ (读成:dollar) 这两个元字符分别代表一行文字的开始和结束。就象我前面举的例子,正则表达式mike会匹配字符序列m - i - k – e,可是它会匹配一行中的所有位置 (比如,它会匹配 “I’m mike”或者 “carmike”)。 ‘^’字符被用来限定匹配行的开始,因此^mike 将只会寻找以mike开始的行。同样,表达式mike$将只会寻找m - i - k - e在一行末尾的(当然还是会匹配 ‘carmike’)。
如果我们联合使用这两个行定位点字符,我们可以搜索在多行文字中寻找包含的特殊字符串序列。比如:表达式 ^mike$ 将只会匹配占有单独一行的单词mike,一个字不多一个字不少。同样,表达式 ^$ 对于发现空行(一行开始就是本行结束的那种)很有用。
字符分类: ‘[]’
一对方括号被称为一个字符分类, 你可以用来匹配任何一个或多个字符。假设你想匹配单词 ‘gray’,同时也想找一下被拼写成 ‘grey’的单词。 使用一个字符分类将允许你匹配这两者 -- 正则表达式 gr[ea]y 被解读成 “匹配这样的字符串 - 一个g, 跟着是r, 跟着或者是一个e或者是一个a, 跟着一个y”。
如果你用 [^ ... ] 代替 [ ... ], 这个分类将匹配后面列出来字符以外的任何字符。首字符 ^ 表示“否定"列表 - 不同于你列出所有希望包含的字符,你是去列出所有不想包含的字符。 注意在这里使用的^ (caret) 字符,它在字符分类方式之外使用表示另外的意思 - 用来匹配文字行的开始(见文章前面部分)。
字符分类中的元字符: ‘-’
在一个字符分类中,字符分类中的元字符 ‘-’ (dash) 用来指出一个字符范围。考虑字符分类 [01234567890abcdefABCDEF],采用’-’的话我们可以这样写[0-9a-fA-F],方便了不少吧。有一点大家要注意的,这个’-’符号只有用一个字符分类中才被认为是元字符,在其他位置,它只是简单的匹配普通的’-’字符,没有任何其他意义。
但是且慢,我看到有人举手质疑。假如在一个字符分类里面,’-’字符做为第一个字符出现的时候,会把它认为成什么呢?比如[-A-F],问题很好,注意:这是一个例外,如果在字符分类中,’-’字符是第一个出现的字符,那我们把它当作普通字符而不是元字符处理(因为实际上它不可能表示一个字符范围,范围需要有开始和结束字符),这个时候它只会匹配一个普通的’-’字符。引申开来,我们再说一个例外:S’?’和’.’在大多数情况下都是正则表达式的元字符,但是有个例外是在字符分类中,当它们在字符分类中的时候(比如在:[-0-9.?],它们只是代表一个普通字符,唯一的特殊字符(元字符)是0和9中间的’-’)。
用一个句点: ‘.’匹配任何字符
‘.’ 元字符(一般读成a dot 或者point)是一种匹配任何字符的写法。在你想在一个字符串的指定位置匹配一个任意字符的时候,它显得非常可爱。再强调一遍,在字符分类中,’.’就不是一个元字符了。到现在为止,你开始看出一些门道来了吧?哪些是元字符哪些不是元字符在字符分类里面和外面是不一样的。
选择性元字符: ‘|’
‘|’ 元字符(读成pipe)的意思是“or”。它允许你把多个表达式合成到一个表达式,然后匹配里面任何单个表达式的结果。这些子表达式被称为备选项。
例如:Mike 和 Michael 是两个独立的正则表达式,但是Mike|Michael 这样来写的话,这个正则表达式匹配任意一个单词。
圆括号在这里可以被用来限制备选的范围。我们可以使用圆括号来达到和上面这个正则表达式同样的目的,同时缩短它长度,正则表达式Mi(ke|chael) 同样匹配Mike或者Michael。当然,在实际程序中我还是会用第一种写法,虽然长了一点,可是更容易理解,因此也更容易维护。
匹配可选项: ‘?’
‘?’ 元字符(读成:question mark)意味着可选。它放在正则表达式的某个位置的一个字符后面,这个字符允许在匹配结果中出现,也可以不出现。当然,我们可以肯定的是:这个’?’字符只能跟在一个普通字符而不是元字符后面。
如果我想匹配英式或者美式拼法的单词‘flavor’ ,我会用正则表达式flavou?r,它被解读成:“匹配一个字符串:f,跟着一个l,跟着一个a,跟着一个v,跟着一个o,跟着一个可选的u,跟着一个r”。
数量符号: ‘+’ and ‘*’
象’?’字符一样,‘+’ (读成plus)和‘*’(读成star)元字符影响前导字符(就是在这个符号前面的字符)可以在匹配字符串中出现的数量 (使用前面说的‘?’的话,相当于前导字符可以出现0次或一次)。元字符‘+’ 匹配前面出现的项目一次或更多次,而‘*’ 则表示匹配任何次,包括0次。
如果我想通过在一场足球比赛中解说员说’goal’的声音次数来统计比分的话,我应该用正则表达式go+al, 它可以匹配‘goal’,也可以匹配一些激情主播的‘gooooooooooooooooal’ (但肯定不会是 ‘gal’)。
前面的三个元字符:’?’、’+’、’*’一般又叫做计量符。因为它们影响前面项目的数量。
数量范围: ‘{}’
‘{最小, 最大}’ 这个元字符序列允许你指定特定项目可以被匹配的最少和最大次数。例如go{1,5}al 可以用来限制我们上面的例子,只匹配1到5次o。同样的{0,1} 其实就等同于一个’?’元字符。
转义字符: ‘\’
‘\’ 元字符(读成:backslash)被用来转换指定的元字符的含义,以便于你可以把它们当成普通字符来匹配。例如,你打算匹配字符’?’或者’\’,你就可以在它们前面加上一个’\’字符,这样它们就被转换成普通字符的含义,就好像这样写:‘\?’ or ‘\\’.
如果在一个非元字符前面使用’\’的话,那么根据你使用正则表达式的语言不同,会有不同的含义,必须参阅相应的手册。比较普遍采用的是perl兼容的正则表达式(PCREs),你可以在这里查看the perldoc page for perl regular expressions. PCREs用得非常普遍,在PHP、 Ruby和ECMAScript/Javascript还有很多语言中都可以使用。
用圆括号匹配: ‘()’
大部分正则表达式工具允许你用圆括号设定一个特定的表达式子集。比如,我们可以用一个正则表达式http://([^/]+)去匹配一个URL的域名部分。下面让我们把这个正则表达式分解开,看看它是如何工作的。
这个表达式的起始部分非常直白:它必须匹配“h - t - t - p - : - / - /”这样的字符序列。这个初始序列之后就是圆括号了,它被用来捕捉符合它们包围的子表达式的字符。在现在的例子中,子表达式是‘[^/]+’,用上面学到的知识,我们知道它实际上是匹配除了‘/’字符以外的任何字符一次到多次。对于一个像是 http://immike.net/blog/Some-blog-post的URL,‘immike.net’ 将会被这个圆括号里面的表达式所匹配。
首先要在在文件首定义菜单项:
#define ID_MENU_EDIT 5001
#define ID_MENU_DELETE 5002
然后添加对话框的WM_CONTEXTMENU消息函数,函数内容为:
CMenu menuPopup;
if(menuPopup.CreatePopupMenu())
{
menuPopup.AppendMenu(MF_STRING,ID_MENU_EDIT,"修改(&E)");
menuPopup.AppendMenu(MF_STRING,ID_MENU_DELETE,"删除(&D)");
menuPopup.TrackPopupMenu(TPM_LEFTALIGN,point.x,point.y,this);
}
然后定义菜单相应函数,
1,在头文件中添加函数定义语句:
// Generated message map functions
//{{AFX_MSG(CAdo2Dlg)
virtual BOOL OnInitDialog();
afx_msg void onInfoEdit(); // 这个是编辑菜单的响应函数
afx_msg void onInfoDelete(); //这个是删除菜单的响应函数
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
afx_msg void OnButton1();
afx_msg void OnButton2();
afx_msg void OnRdblclkList1(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnDblclkList1(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnContextMenu(CWnd* pWnd, CPoint point);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
2,在cpp文件中添加函数体:
void CAdo2Dlg::OnInfoEdit()
{
AfxMessageBox("edit");
}
void CAdo2Dlg::OnInfoDelete()
{
AfxMessageBox("delete");
}
3,然后在cpp文件中添加影射:
BEGIN_MESSAGE_MAP(CAdo2Dlg, CDialog)
//{{AFX_MSG_MAP(CAdo2Dlg)
ON_COMMAND(ID_MENU_EDIT, OnInfoEdit)
ON_COMMAND(ID_MENU_DELETE, OnInfoDelete)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
所有的工作完成了!
一,MFC扩展DLL
创建:
1,新建一个MFC扩展DLL ,名字为dll5,添加头文件,名为dll5
2,头文件中加入:
extern __declspec(dllexport) CString concatA(CString x,CString y);
3,在cpp文件中加入:
extern __declspec(dllexport) CString concatA(CString x,CString y)
{
return x + y;
}
4,在cpp文件中加入:
#include "dll5.h"
5,编译,生成dll
使用:
1,新建一个单文档应用程序,名为Usedll5
2,将刚才生成的dll5.lib文件和dll5.h文件拷贝到当前应用程序路径下,
将dll5.dll 文件拷贝到 当前应用程序下的debug下
3,在当前应用程序中用到该dll5的导出方法(concatA)的文件(或类)上添加如下语句:
#include "dll5.h"
假设将其加到 Usedll5View.cpp中。
4,在Usedll5View类中建立消息映射入口,在消息函数中添加如下语句:
CString a=concatA("中国北车集团","长春轨道客车股份有限公司");
MessageBox(a);
5,在 工程/设置/连接/对象库/模块 中加入:dll5.lib
6,编译执行该应用程序,并触发该消息,则输出:
中国北车集团长春轨道客车股份有限公司
之后只要定义不更改,函数体无论怎么更改。我们只要将编译好的dll拷贝过来即可。如果定义有了修改,则需要将h文件和lib 文件拷贝过来,并需要重新编译。
二,动态链接库使用共享MFC DLL
创建:
1,新建一个 DLL(选 动态链接库使用共享MFC DLL)
2,头文件中加入:
_declspec(dllexport) CString WINAPI concatA(CString x,CString y);
3,在cpp文件末尾加入:
_declspec(dllexport) CString WINAPI concatA(CString x,CString y)
{
return x + y;
}
4,编译,生成dll
使用:
1,新建一个单文档应用程序,名为Usedll8
2,将刚才生成的dll8.lib文件拷贝到当前应用程序路径下,
将dll8.dll 文件拷贝到 c:\winnt\system32下
3,在当前应用程序中用到该dll5的导出方法(concatA)的 类的头文件上添加如下语句:
extern CString WINAPI concatA(CString x,CString y);
假设将其加到 Usedll8View.h中。
4,在Usedll8View类中建立消息映射入口,在消息函数中添加如下语句:
CString a=concatA("中国北车集团","长春轨道客车股份有限公司");
MessageBox(a);
5,在 工程/设置/连接/对象库/模块 中加入:dll8.lib
6,编译执行该应用程序,并触发该消息,则输出:
中国北车集团长春轨道客车股份有限公司
现在有三种比较常用微软Source Control 工具:VSS 6.0d,VSS 2005,TFS(Team Foundation Server),做个简单的比较。
整体功能上,前两者属于小型的管理工具,一般在15个人以下的开发团队中使用比较合适,适合小型的公司或者团队使用。Team Foundation Server 是 Visual Studio Team System 产品线发布的最后一个组件。它为团队项目开发提供了高度集成化的工作平台,可以在大规模的团队开发中使用,不仅可以提供源代码管理功能,还可以提供其他的功能.
1. VSS 6.0d是比较早期的工具,最大支持2GB的数据空间,但是不能支持远程访问。
2. VSS 2005是可以支持最大4GB的数据空间,可以提供远程访问,支持多人签出。
3.TFS是企业级的,支持50个人以上的团队,但是要求必须在Windows 2003 sp1,Sql Server 2005的基础之上,强烈建议干净的安装环境。
声明: 1.本人男 2.下面的事情是真实的事情,前天下午四点多发生 -------------------------------------------------------------------- 前天下午, 我接到移动的10086打电话回访, 我都"喂"好几声了,移动的那位MM竟然说“你是李小姐吗。。。”, 当时我差点没晕倒,崩溃掉... 估计那位MM 也超汗! |