场景
- 在开发
Windows
和macOS
的界面软件时,Windows
用的是WTL/Win32
技术,而macOS
用的是Cocoa
技术。而两种技术的本地语言一个主打是C++
,另一个却是Object-c
。界面软件的源码随着项目功能增多而增多,这就会给同步Windows
和macOS
的功能造成很大负担。 有大部分的调用底层逻辑,界面数据搜索等代码逻辑会重复写两遍,用C++
和Object-C
写。初步估计这部分代码至少占界面代码的50%
。这无疑会影响产品发布速度,影响软件质量(可能有些逻辑两个平台不能完全一样),增大了开发的工作量,这部分时间无疑是浪费了的。有什么办法可以把这部分代码进行重用?
说明
-
界面代码往往就是获取数据,显示数据,之后点击按钮处理数据。这就需要获取数据,处理数据需要和平台相关的界面代码剥离,显示数据部分依赖平台的框架进行数据绘制。 按照这种逻辑,最好的办法就是数据获取和处理使用
C++
语言处理,处理这些和界面无关的逻辑代码。 当然如果有特殊情况也可以用.mm
文件(这种是Object-C
和C++
混编的文件后缀)来调用Object-C
平台接口处理。比如presenter_mac.mm
和presenter_win.cpp
。 -
界面架构
MVP
架构可以满足这个要求。当然这和Object-c
和C++
可以混编有些关系,如果是Swift
语言,不能直接调用C++
类,需要通过桥接调用Object-C
,再通过Object-C
调用C++
来处理。 使用C++
处理跨平台逻辑,最好使用C++11
以上标准,因为这个标准多了很多有用的库和特性节省MVP
架构的很多代码,如lambda
,thread
,functional
等。 -
看看
MVP
架构的分层,主要是以下三层。很好理解,Presenter作为View
和Model
的通讯层,起到了连接视图和底层模型逻辑处理的作用,也起到了跨平台时处理不同平台界面框架获取数据的本地实现的桥梁。
View <-> Presenter <-> Model
-
这里说的
Presenter
可根据界面语言的实现进行基于本地的实现,比如macOS
下Presenter
层需要处理NSString
作为字符串存储的数据,而在Windows
下需要处理std::wstring
作为字符串处理的数据。这些数据如果传递给Model
,那么需要转换为std::string
的UTF8
编码进行处理。 -
在这个三层模型里,依赖关系需要注意设计, 切
不可以互相依赖
。View
依赖Presenter
接口,View
里有Presenter
的成员变量,Presenter
的实例需要通过方法注入。这样如果View
更换不同的Presenter
也可以通过注入的方法。View
通过成员变量presenter_
调用它的方法。异步处理通过传入std::function
绑定的方法给Presenter
,当Presenter
处理完之后再调用绑定的方法,类似回调函数的处理。
private: shared_ptr<Presenter> presenter_ = nullptr; ... void CView::setPresenter(shared_ptr<Presenter> presenter) { presenter_ = presenter; }
Presenter
依赖Model
接口,注意说的接口是根据实际项目的复杂度来定义虚拟类接口,如果实现只有一个,定义一个普通类就行。Presenter
通过成员变量model_
调用它的方法。异步处理通过传入std::function
绑定的方法给Model
,当Model
处理完之后再调用绑定的方法,类似回调函数的处理。
protected: shared_ptr<Model> model_; ... void Presenter::setModel(shared_ptr<Model> model) { model_ = model; }
例子
- 以下的虚拟
listview
是MVP
架构的实现。
View.h
// View.h : interface of the CView class
//
/
#pragma once
#include <utility>
#include <string>
#include <vector>
#include <memory>
#include <atlmisc.h>
#include <atlctrls.h>
#include <atlctrlx.h>
#include <GdiPlus.h>
using namespace std;
class Presenter;
enum
{
kMyButtonId = WM_USER+1,
kMyButtonId2,
kMyButtonId3,
kMyListViewId
};
class CView : public CWindowImpl<CView>
{
public:
DECLARE_WND_CLASS(NULL)
BOOL PreTranslateMessage(MSG* pMsg);
BEGIN_MSG_MAP_EX(CView)
MSG_WM_CREATE(OnCreate)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
NOTIFY_HANDLER(kMyListViewId,NM_CLICK,OnNMClickListResult)
NOTIFY_HANDLER(kMyListViewId,LVN_GETDISPINFO,OnGetListViewData)
NOTIFY_HANDLER(kMyListViewId,LVN_ODCACHEHINT,OnPrepareListViewData)
NOTIFY_HANDLER(kMyListViewId,LVN_ODFINDITEM,OnFindListViewData)
COMMAND_RANGE_HANDLER_EX(kMyButtonId,kMyButtonId3,OnCommandIDHandlerEX)
REFLECT_NOTIFICATIONS()
END_MSG_MAP()
void setPresenter(shared_ptr<Presenter> presenter);
protected:
// Handler prototypes (uncomment arguments if needed):
// LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
// LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
// LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)
int OnCreate(LPCREATESTRUCT lpCreateStruct);
LRESULT OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
void UpdateLayout();
LRESULT OnNMClickListResult(int idCtrl,LPNMHDR pnmh,BOOL &bHandled);
LRESULT OnGetListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled);
LRESULT OnPrepareListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled);
LRESULT OnFindListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled);
void OnCommandIDHandlerEX(UINT uNotifyCode, int nID, CWindow wndCtl);
void ReloadMockData();
void ReloadListView();
private:
std::wstring GetControlText(HWND hwnd,wchar_t* buf = NULL);
CListViewCtrl listview_;
CFont font_normal_;
CFont font_bold_;
CBrushHandle brush_white_;
CBrushHandle brush_hollow_;
CBrush brush_red_;
CButton buttonReloadMockData_;
CButton buttonReloadListView_;
CButton buttonDeleteListViewOneRow_;
private:
shared_ptr<Presenter> presenter_ = nullptr;
};
View.cpp
// View.cpp : implementation of the CView class
//
/
#include "stdafx.h"
#include "resource.h"
#include <utility>
#include <sstream>
#include <stdint.h>
#include <assert.h>
#include <Strsafe.h>
#include "View.h"
#include <CommCtrl.h>
#include <string>
#include <regex>
#include "Presenter.h"
#include "Photo.h"
using namespace std;
BOOL CView::PreTranslateMessage(MSG* pMsg)
{
return FALSE;
}
LRESULT CView::OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
CPaintDC dc(m_hWnd);
CMemoryDC mdc(dc,dc.m_ps.rcPaint);
CRect rect_client;
GetClientRect(&rect_client);
mdc.FillSolidRect(rect_client,RGB(255,255,255));
//TODO: Add your drawing code here
return 0;
}
static HFONT GetFont(int pixel,bool bold,const wchar_t* font_name)
{
LOGFONT lf;
memset(&lf, 0, sizeof(LOGFONT)); // zero out structure
lf.lfHeight = pixel; // request a 8-pixel-height font
if(bold)
{
lf.lfWeight = FW_BOLD;
}
lstrcpy(lf.lfFaceName, font_name); // request a face name "Arial"
HFONT font = ::CreateFontIndirect(&lf);
return font;
}
std::wstring CView::GetControlText(HWND hwnd,wchar_t* buf)
{
auto length = ::GetWindowTextLength(hwnd);
bool bufNull = false;
if(!buf){
buf = new wchar_t[length+1]();
bufNull = true;
}
::GetWindowText(hwnd,buf,length+1);
std::wstring str(buf);
if(bufNull)
delete []buf;
return str;
}
static std::wstring GetProductBinDir()
{
static wchar_t szbuf[MAX_PATH];
GetModuleFileName(NULL,szbuf,MAX_PATH);
PathRemoveFileSpec(szbuf);
int length = lstrlen(szbuf);
szbuf[length] = L'\\';
szbuf[length+1] = 0;
return std::wstring(szbuf);
}
LRESULT CView::OnGetListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled)
{
NMLVDISPINFO* plvdi = (NMLVDISPINFO*) pnmh;
auto iItem = plvdi->item.iItem;
if (-1 == iItem)
return 0;
auto count = presenter_->getPhotoCount();
if(count <= iItem)
return 0;
auto photo = presenter_->getPhoto(iItem);
if(plvdi->item.mask & LVIF_TEXT){
switch(plvdi->item.iSubItem)
{
case 0:
StringCchCopy(plvdi->item.pszText, plvdi->item.cchTextMax, to_wstring((int64_t)iItem+1).c_str());
break;
case 1:
StringCchCopy(plvdi->item.pszText, plvdi->item.cchTextMax, photo->name.c_str());
break;
case 2:
StringCchCopy(plvdi->item.pszText, plvdi->item.cchTextMax, photo->format.c_str());
break;
case 3:
StringCchCopy(plvdi->item.pszText, plvdi->item.cchTextMax, photo->createDate.c_str());
break;
}
}
return 0;
}
LRESULT CView::OnPrepareListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled)
{
return 0;
}
LRESULT CView::OnFindListViewData(int idCtrl,LPNMHDR pnmh,BOOL &bHandled)
{
return 0;
}
LRESULT CView::OnNMClickListResult(int idCtrl,LPNMHDR pnmh,BOOL &bHandled)
{
return 0;
}
void CView::OnCommandIDHandlerEX(UINT uNotifyCode, int nID, CWindow wndCtl)
{
switch(nID)
{
case kMyButtonId:
{
ReloadMockData();
MessageBox(L"刷新模拟数据完成");
break;
}
case kMyButtonId2:
{
ReloadListView();
MessageBox(L"重新加载表格数据完成");
break;
}
case kMyButtonId3:
{
int iItem = -1;
while((iItem = listview_.GetNextItem(iItem,LVNI_SELECTED)) != -1){
presenter_->removePhoto(iItem);
listview_.DeleteItem(iItem);
iItem--;
}
MessageBox(L"已删除");
break;
}
}
}
void CView::ReloadListView()
{
listview_.SetItemCount(0);
presenter_->clearPhotos();
}
void CView::ReloadMockData()
{
presenter_->loadPhotos([this]() {
listview_.SetItemCount(presenter_->getPhotoCount());
});
}
int CView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
font_normal_ = ::GetFont(16,false,L"Arial");
font_bold_ = ::GetFont(16,true,L"Arial");
brush_hollow_ = AtlGetStockBrush(HOLLOW_BRUSH);
brush_white_ = AtlGetStockBrush(WHITE_BRUSH);
brush_red_.CreateSolidBrush(RGB(255,0,0));
// 1.创建CListViewCtrl
listview_.Create(m_hWnd,0,NULL,WS_CHILD | WS_TABSTOP |WS_VISIBLE
|LVS_ALIGNLEFT|LVS_REPORT|LVS_SHOWSELALWAYS|WS_BORDER|LVS_OWNERDATA,0,kMyListViewId);
listview_.SetExtendedListViewStyle(LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES|LVS_EX_DOUBLEBUFFER);
listview_.SetFont(font_normal_);
auto header = listview_.GetHeader();
header.SetFont(font_bold_);
listview_.SetBkColor(RGB(255,255,255));
listview_.InsertColumn(0,L"No.",LVCFMT_LEFT,40);
listview_.InsertColumn(1,L"Name",LVCFMT_LEFT,100);
listview_.InsertColumn(2,L"Format",LVCFMT_LEFT,100);
listview_.InsertColumn(3,L"Create Date",LVCFMT_LEFT,100);
// 2.创建按钮
buttonReloadMockData_.Create(m_hWnd,0,L"加载新数据",WS_CHILD|WS_VISIBLE,0,kMyButtonId);
buttonReloadMockData_.SetFont(font_normal_);
buttonReloadListView_.Create(m_hWnd,0,L"刷新表格",WS_CHILD|WS_VISIBLE,0,kMyButtonId2);
buttonReloadListView_.SetFont(font_normal_);
buttonDeleteListViewOneRow_.Create(m_hWnd,0,L"删除选中行",WS_CHILD|WS_VISIBLE,0,kMyButtonId3);
buttonDeleteListViewOneRow_.SetFont(font_normal_);
UpdateLayout();
return 0;
}
void CView::UpdateLayout()
{
CRect rect;
GetClientRect(&rect);
CClientDC dc(m_hWnd);
dc.SelectFont(font_normal_);
CSize size_control(500,300);
CRect rect_control = CRect(CPoint(20,20),size_control);
listview_.MoveWindow(rect_control);
CSize size_button;
buttonReloadMockData_.GetIdealSize(&size_button);
rect_control = CRect(CPoint(rect_control.left,rect_control.bottom+10),size_button);
buttonReloadMockData_.MoveWindow(rect_control);
CSize sizeButton2;
buttonReloadListView_.GetIdealSize(&sizeButton2);
rect_control = CRect(CPoint(rect_control.right+10,rect_control.top),sizeButton2);
buttonReloadListView_.MoveWindow(rect_control);
CSize sizeButton3;
buttonDeleteListViewOneRow_.GetIdealSize(&sizeButton3);
rect_control = CRect(CPoint(rect_control.right+10,rect_control.top),sizeButton3);
buttonDeleteListViewOneRow_.MoveWindow(rect_control);
}
void CView::setPresenter(shared_ptr<Presenter> presenter)
{
presenter_ = presenter;
}
Presenter.h
#ifndef PRESENTER_H
#define PRESENTER_H
#include <vector>
#include <memory>
#include <functional>
using namespace std;
class Photo;
class Model;
typedef function<void()> FuncSimple;
class Presenter
{
public:
shared_ptr<Photo> getPhoto(int i);
void clearPhotos();
size_t getPhotoCount();
void removePhoto(int nItem);
void loadPhotos(FuncSimple func);
void setModel(shared_ptr<Model> model);
protected:
vector<shared_ptr<Photo>> photos_;
protected:
shared_ptr<Model> model_;
FuncSimple funcLoadFinish_;
};
#endif // !PRESENTER_H
Presenter.cpp
#include "stdafx.h"
#include "Presenter.h"
#include "Photo.h"
#include "Model.h"
#include "dispatch_queue.h"
shared_ptr<Photo> Presenter::getPhoto(int i)
{
return (i < photos_.size()) ? photos_[i] : nullptr;
}
void Presenter::clearPhotos()
{
photos_.clear();
}
size_t Presenter::getPhotoCount()
{
return photos_.size();
}
void Presenter::removePhoto(int nItem)
{
if (nItem < getPhotoCount()) {
auto ite = photos_.begin() + nItem;
photos_.erase(ite);
}
}
void Presenter::loadPhotos(FuncSimple func)
{
funcLoadFinish_ = func;
model_->loadPhoto([this](vector<shared_ptr<Photo>>* photos) {
// 需要把数据发送到主线程更新,这样才不会出现多线程访问共享数据冲突。
DispatchQueue::DispatchAsync(DispatchQueue::DispatchGetMainQueue(),
new FuncSimple([this, photos]() {
photos_.insert(photos_.end(), photos->begin(), photos->end());
if (funcLoadFinish_)
funcLoadFinish_();
delete photos;
}));
});
}
void Presenter::setModel(shared_ptr<Model> model)
{
model_ = model;
}
Model.h
#ifndef MODEL_H
#define MODEL_H
#include <functional>
#include <memory>
#include <vector>
#include "Photo.h"
using namespace std;
typedef function<void(vector<shared_ptr<Photo>>*)> FuncLoadPhoto;
class Model
{
public:
void loadPhoto(FuncLoadPhoto func);
protected:
FuncLoadPhoto funcLoadPhoto_;
};
#endif
Model.cpp
#include "stdafx.h"
#include "Model.h"
#include <thread>
#include "Photo.h"
using namespace std;
void Model::loadPhoto(FuncLoadPhoto func)
{
funcLoadPhoto_ = func;
// 模拟异步加载数据
thread t1([this]() {
wchar_t buf[MAX_PATH] = { 0 };
int index = 0;
auto photos = new vector<shared_ptr<Photo>>();
for (int i = 0; i < 10000; ++i, ++index) {
auto photo = new Photo();
wsprintf(buf, L"Name-%d", index);
photo->name = buf;
wsprintf(buf, L"Format-%d", index);
photo->format = buf;
wsprintf(buf, L"createDate-%d", index);
photo->createDate = buf;
photos->push_back(move(shared_ptr<Photo>(photo)));
}
if (funcLoadPhoto_)
funcLoadPhoto_(photos);
});
t1.detach();
}
dispatch_queue.h
#ifndef __DISPATCH_QUEUE_H
#define __DISPATCH_QUEUE_H
#include <Windows.h>
#include <WinUser.h>
#include <functional>
enum
{
WMC_DISPATCH_MAIN_QUEUE = WM_USER+1000
};
typedef struct DispatchQueueObject1
{
DWORD threadId;
HWND m_hwnd;
UINT msg;
}DispatchQueueObject;
class DispatchQueue
{
public:
static DWORD GetMainThreadId();
static bool IsCurrentMainThread();
static void DispatchQueueInit(HWND hwnd);
static DispatchQueueObject* DispatchGetMainQueue();
static void FreeDispatchMainQueue(DispatchQueueObject* dqo);
static void DispatchAsync(DispatchQueueObject* queue,std::function<void()>* callback);
};
#endif
dispatch_queue.cpp
#include "stdafx.h"
#include "dispatch_queue.h"
static HWND gMainHwnd = NULL;
static DWORD gMainThreadId = 0;
bool DispatchQueue::IsCurrentMainThread()
{
return GetMainThreadId() == GetCurrentThreadId();
}
DWORD DispatchQueue::GetMainThreadId()
{
return gMainThreadId;
}
void DispatchQueue::DispatchQueueInit(HWND hwnd)
{
gMainHwnd = hwnd;
gMainThreadId = GetCurrentThreadId();
}
void DispatchQueue::DispatchAsync(DispatchQueueObject* queue,std::function<void()>* callback)
{
if(queue->threadId){
::PostThreadMessage(queue->threadId,queue->msg,(WPARAM)callback,0);
}else{
::PostMessage(queue->m_hwnd,queue->msg,(WPARAM)callback,0);
}
FreeDispatchMainQueue(queue);
}
DispatchQueueObject* DispatchQueue::DispatchGetMainQueue()
{
DispatchQueueObject* object = (DispatchQueueObject*)malloc(sizeof(DispatchQueueObject));
memset(object,0,sizeof(DispatchQueueObject));
object->m_hwnd = gMainHwnd;
object->msg = WMC_DISPATCH_MAIN_QUEUE;
return object;
}
void DispatchQueue::FreeDispatchMainQueue(DispatchQueueObject* dqo)
{
free(dqo);
}
MainFrm.h
// MainFrm.h : interface of the CMainFrame class
//
/
#pragma once
#include "View.h"
#include "Presenter.h"
#include "Model.h"
#include "dispatch_queue.h"
class CMainFrame :
public CFrameWindowImpl<CMainFrame>,
public CUpdateUI<CMainFrame>,
public CMessageFilter, public CIdleHandler
{
public:
DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)
CView m_view;
virtual BOOL PreTranslateMessage(MSG* pMsg);
virtual BOOL OnIdle();
BEGIN_UPDATE_UI_MAP(CMainFrame)
END_UPDATE_UI_MAP()
BEGIN_MSG_MAP(CMainFrame)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit)
COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew)
COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)
MESSAGE_HANDLER(WMC_DISPATCH_MAIN_QUEUE, OnDispatchMainQueueEvent)
CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)
CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)
END_MSG_MAP()
// Handler prototypes (uncomment arguments if needed):
// LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
// LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
// LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)
LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/);
LRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled);
LRESULT OnFileExit(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/);
LRESULT OnFileNew(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/);
LRESULT OnAppAbout(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/);
LRESULT OnDispatchMainQueueEvent(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
std::function<void()>* func = (std::function<void()>*)wParam;
(*func)();
delete func;
bHandled = TRUE;
return 0;
}
};
MainFrm.cpp
// MainFrm.cpp : implmentation of the CMainFrame class
//
/
#include "stdafx.h"
#include "resource.h"
#include "aboutdlg.h"
#include "MainFrm.h"
BOOL CMainFrame::PreTranslateMessage(MSG* pMsg)
{
if(CFrameWindowImpl<CMainFrame>::PreTranslateMessage(pMsg))
return TRUE;
return m_view.PreTranslateMessage(pMsg);
}
BOOL CMainFrame::OnIdle()
{
return FALSE;
}
LRESULT CMainFrame::OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
// 注册线程接收异步主线程消息的窗口
DispatchQueue::DispatchQueueInit(m_hWnd);
auto presenter = make_shared<Presenter>();
auto model = make_shared<Model>();
presenter->setModel(model);
m_view.setPresenter(presenter);
m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);
// register object for message filtering and idle updates
CMessageLoop* pLoop = _Module.GetMessageLoop();
ATLASSERT(pLoop != NULL);
pLoop->AddMessageFilter(this);
pLoop->AddIdleHandler(this);
return 0;
}
LRESULT CMainFrame::OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
{
// unregister message filtering and idle updates
CMessageLoop* pLoop = _Module.GetMessageLoop();
ATLASSERT(pLoop != NULL);
pLoop->RemoveMessageFilter(this);
pLoop->RemoveIdleHandler(this);
bHandled = FALSE;
return 1;
}
LRESULT CMainFrame::OnFileExit(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
PostMessage(WM_CLOSE);
return 0;
}
LRESULT CMainFrame::OnFileNew(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
// TODO: add code to initialize document
return 0;
}
LRESULT CMainFrame::OnAppAbout(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
CAboutDlg dlg;
dlg.DoModal();
return 0;
}
项目下载
https://download.csdn.net/download/infoworld/89445554
注意: 关于WTL
的开发学习可以订阅我的课程:
使用WTL进行Windows桌面应用开发-第一部_在线视频教程-CSDN程序员研修院
参考
-
观察者模式在项目中实际使用例子
-
观察者模式在项目中实际使用例子2
-
QQ NT全新重构,探寻24岁QQ大重构背后的思考_跨端开发