前言

微软基础类库(英语:Microsoft Foundation Classes,简称MFC)是微软公司提供的一个类库(class libraries),以C++类的形式封装了Windows API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量。其中包含大量Windows句柄封装类和很多Windows的内建控件和组件的封装类。


正文

MFC为何物

传统手工业,需要很多人,然后手工去操作,对于厂家而言,人工费高,质量不确定。
而改用机器后,只需要投入材料,一天内的效应会大于人工,而且质量比较平均,当然不是所有的传统手工业都能被机器代替,只是部分。

这也就是普通c/c++代码和api的区别,经过多次封装它自然就变得看起来简单,复用性高。

MFC既然是微软设计的,自然只适合在windows上做应用开发,像xp、win7、win10兼容性肯定没得说,毕竟也是个亲儿子。
不过目前的更新迭代之下,MFC的场景也会比较少,后面还有个跨平台的Qt。
虽然少,但是老公司的项目没有转型之前,大部分还是需要维护的。
可能外包比较多。

MFC的学习方式

  1. (1.c++多态、2.windows消息循环、3.msg loop)
  2. 查文档,不会就查,可以用vs的ide在函数上按f1跳转,也可以记网址

安装:xxxx自己百度,vs的ide模块化其实看得很清楚了,就是吃内存。

vs2022应该是C++ ATL for v143生成工具和c++ MFC for v143生成工具,都是x86和x64。

MFC能做啥

  1. 微软的基础框架
  2. 桌面应用
  3. 上位机
  4. pc端的监控软件
  5. 修改注册表/启动项等

前身Win32

1.窗口程序架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int WinMain(){
//设计窗口外观以及交互响应
RegisterClass(...)
//生产窗口
CreateWinodw(...)
//显示窗口
ShowWindow(...)
//刷新窗口
UpdateWindow(...)
//消息循环
while(GetMessage(...)){
//消息转发
TranslateMessage(...);
//消息分发
DispatchMessage(...);
}
}

2.API和SDK

api全称(Application Program Interface) 应用程序编程接口
sdk全称(Software Development Kit) 也就是软件开发工具包,一般会包括API接口文档、示例文档、帮助文档、使用手册和相关工具。


3.窗口和句柄

窗口就是屏幕上的一片特定区域,可能存在等待接收用户的输入,显示程序的输出。可以包含标题栏、菜单栏、工具栏、空间等
句柄(handle)(资源的编号、二级指针),窗口句柄、文件句柄、数据库连接句柄,本质都是指针
c++窗口类对象和窗口并不是一回事,二者的关系是c++窗口类内部定义了一个窗口句柄变量,保存了这个c++窗口类对象和相关的窗口句柄。当窗口销毁时,与之对应的c++窗口类对象销毁与否要看生命周期结束没。反之c++窗口类对象销毁时,与之相关的窗口肯定被销毁了。


4.消息循环

银行这种,一般都是个人业务比较多。取个号要么机器上操作,要么去柜台。

对于windows系统而言,这种循环好看懂一些。

消息循环会引出一个回调函数
就是说当出现特定事件的时候,都会交给回调函数处理。


5.变量命名约定

前缀 含义 前缀 含义
a 数组array b 布尔值bool
by 无符号字符[字节] c 字符[字节]
cb 字节计数 rgb 保存颜色值的长整型
cx,cy 短整型[计算x,y的长度] dw 无符号长整型
fn 函数 h 句柄
i 整型 m_ 类的数据成员member
n 短整型或整型 np 近指针
p 指针 l 长整型
lp 长指针 s 字符串string
sz 以0结尾的字符串 tm 正文大小
w 无符号整型 x,y 无符号整型[表示x,y的坐标]

反正windows的产品基本都遵循这样的命名规范。


MFC程序开发流程


1.基于对话框的程序

无菜单栏、工具栏,界面很简单,可使用此类型为对话框。

类似于计算器这种


2.基于文档/试图的程序

标准的windows应用界面,包含菜单栏、工具栏、状态栏等。

最直观的就是vs的ide喽


3.MFC与win32开发的区别

  • 定制界面的区别(手写代码和拖放控件)
  • 响应键鼠操作的区别(窗口处理函数和消息映射机制)

win32可以开发纯命令行的程序,也就是windows所说的dos,在命令行里面gcc cmake之类的操作。
所以涉及到界面的开发自然而然的是选择MFC。


4.MFC消息映射机制

1
2
3
4
5
BEGIN_MESSAGE_MAP(CMFCApplication1Dlg, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
END_MESSAGE_MAP()

BEGIN_MESSAGE_MAP即为消息映射表
如果有特殊需求也可以进行自定义消息。虽然不能说自定义就一定能达到需求。


5.构建项目

vs有默认的项目类型可选,是提高效率的首要之选,不是说不可以从空项目起手,而是节约基础配置的时间。

至于共享dll编译出来的exe体积肯定是比使用静态库的exe要小。

默认就共享得了。有需要在使用静态。
然后此处基于对话框开发,所以文档模板属性没啥可选的。

用户界面常用的最小化最大化是可以勾上的,标题名呢也可以在此就设置好。

这些也可以看着来。

最后的生成的类一般不做修改。也别闲着用中文,每个人的环境不同,编码有异,在你这能跑在别人那就可能乱码了。

创建完成之后

其实就能看到大致的模板了。

跟预览的效果是一样的。这种直接套模板的确省去了不少麻烦。

其中拖动控件的精髓在工具箱里,属性里面直接选消息。


画线

  • 知识点
    • 屏幕坐标和客户端坐标
    • 设备上下文
    • 事件
  • 起点和终点
    • 如何捕捉这两点,如左键为起点右键为终点

这次选择基于单个文档,其它倒是不用太在意

在创建完项目后看到很多头文件和源文件的时候,可以通过菜单栏的视图找到类视图

能够主要看清有哪些类。

启动项目后可以看到这样一个模板

要自己一开始就写肯定是不行的,所以这就是项目模板的好处,可以帮你完成很多基础的操作。
至于实现部分,可以通过类视图的类名去得到大意。

至于画线部分,拆分为两个地方,起点就是我们鼠标左键按下确定第一个点,然后随便他怎么拖动到其他地方然后再点击一下,即为线的终点。

至于这些鼠标按下抬起的消息,可以通过类视图右击打开属性,找到消息,能看到有很多。

然后选择这俩

也就是left button up和left button up,按下和松开

至于形参,可以转到定义查看
typedef unsigned int UINT;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class CPoint :
public tagPOINT
{
public:
// Constructors

// create an uninitialized point
CPoint() throw();
// create from two integers
CPoint(
_In_ int initX,
_In_ int initY) throw();
// create from another point
CPoint(_In_ POINT initPt) throw();
// create from a size
CPoint(_In_ SIZE initSize) throw();
// create from an LPARAM: x = LOWORD(dw) y = HIWORD(dw)
CPoint(_In_ LPARAM dwPoint) throw();


// Operations

// translate the point
void Offset(
_In_ int xOffset,
_In_ int yOffset) throw();
void Offset(_In_ POINT point) throw();
void Offset(_In_ SIZE size) throw();
void SetPoint(
_In_ int X,
_In_ int Y) throw();

BOOL operator==(_In_ POINT point) const throw();
BOOL operator!=(_In_ POINT point) const throw();
void operator+=(_In_ SIZE size) throw();
void operator-=(_In_ SIZE size) throw();
void operator+=(_In_ POINT point) throw();
void operator-=(_In_ POINT point) throw();

// Operators returning CPoint values
CPoint operator+(_In_ SIZE size) const throw();
CPoint operator-(_In_ SIZE size) const throw();
CPoint operator-() const throw();
CPoint operator+(_In_ POINT point) const throw();

// Operators returning CSize values
CSize operator-(_In_ POINT point) const throw();

// Operators returning CRect values
CRect operator+(_In_ const RECT* lpRect) const throw();
CRect operator-(_In_ const RECT* lpRect) const throw();
};

一个是改了名的类型,一个则是类。

这个类中有两个字眼比较醒目,x和y。变相的能猜到肯定是记录了xy轴的坐标。
但是按住和松开肯定是会改变xy的,就需要自己定义两个变量去记录起始位置。

1
2
3
protected:
CPoint m_start; //起始位置,用于绘制线条
CPoint m_stop; //终止位置,用于绘制线条
1
2
3
4
5
6
7
8
9
10
11
void CMFCPaintView::OnLButtonDown(UINT nFlags, CPoint point){
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_start = point;
CView::OnLButtonDown(nFlags, point);
}

void CMFCPaintView::OnLButtonUp(UINT nFlags, CPoint point){
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_stop = point;
CView::OnLButtonUp(nFlags, point);
}

在这打两个断点然后跑程序,随便点击一下,就能看到point的xy出现值了

然而坐标其实也有分别,像屏幕传统的都是1920*1080,说的就是x轴长1920,y轴长1080,而且比较有意思的是这个0,0坐标在屏幕的左上角。
屏幕坐标可称为screen point,而程序的坐标可称为client point。

有了这两个的区别,就可能导致获取到的xy坐标是有问题的。
除了基础的xy获取了之后,还要考虑这个信息会不会被其它消息所引用,有引用又得防着被修改了。

xy有了,那么要考虑绘制的问题。
要用到一个类CDC
这里就不展示了,这个类的定义里面东西有点多。

// The device context在注释中说明,这是一个设备的上下文
上下文:在画图中要绘制一些东西的时候,肯定会用到线条的粗细,线条的颜色,画图的大小,画图的大小状态比如最小化最大化和普通状态等许多参数信息,只有了解了所有的参数信息才能绘制出想要的东西。
为什么叫设备上下文:上述举例的上下文是属于窗口的上下文,而有其他的情况,你会把绘制的东西显示在显示器上,而不是单独的窗口。那么这种时候就需要获取到你这个屏幕的参数。

1
2
3
4
5
6
7
8
9
10
void CMFCPaintView::OnLButtonUp(UINT nFlags, CPoint point){
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_stop = point;

//获取设备上下文
CDC *pDC = GetDC();
pDC->MoveTo(m_start);

CView::OnLButtonUp(nFlags, point);
}

在c++中结构体和类基本功能相通,区别在于类有私有成员。所以传递类并没有太大关系。
pDC->MoveTo(m_start);
moveto 就是说移动到我们的这个点上。然后才可以进行绘制线条之类的。

1
2
3
4
5
6
7
8
9
10
11
12
void CMFCPaintView::OnLButtonUp(UINT nFlags, CPoint point){
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_stop = point;

//获取设备上下文
CDC *pDC = GetDC();
pDC->MoveTo(m_start);
pDC->LineTo(m_stop);
ReleaseDC(pDC);

CView::OnLButtonUp(nFlags, point);
}

pDC->LineTo(m_stop);line就是线呗,从按下鼠标左键的点到松开的点直接绘制一条直线。
并且要记住ReleaseDC(pDC);,避免占用导致程序异常。

然后就可以run了

鼠标左键按下直到某个点松开即可绘制出线条。

多画几条也没事,不过有点踩坑点:就是重绘的问题,此处就是当最大化和最小化的时候线条就莫得了。

除此之外,每次都需要滑动才能绘制,可能有点low。

1
2
3
4
protected:
CPoint m_start; //起始位置,用于绘制线条
CPoint m_stop; //终止位置,用于绘制线条
BOOL m_status; //绘制状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CMFCPaintView::OnLButtonDown(UINT nFlags, CPoint point){
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_start = point;
m_status = TRUE;
CView::OnLButtonDown(nFlags, point);
}

void CMFCPaintView::OnLButtonUp(UINT nFlags, CPoint point){
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_stop = point;

//获取设备上下文
CDC *pDC = GetDC();
pDC->MoveTo(m_start);
pDC->LineTo(m_stop);
ReleaseDC(pDC);
m_status = FALSE;

CView::OnLButtonUp(nFlags, point);
}

然后新增消息,鼠标移动时。快捷操作就是类视图然后属性里面找到消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CMFCPaintView::OnMouseMove(UINT nFlags, CPoint point){
// TODO: 在此添加消息处理程序代码和/或调用默认值
if (m_status){
InvalidateRect(NULL);

//如果鼠标正在移动就进行操作
CDC *pDC = GetDC();
pDC->MoveTo(m_start);
pDC->LineTo(m_stop);
ReleaseDC(pDC);
}

CView::OnMouseMove(nFlags, point);
}

增加一个状态是为了考虑程序刚启动的时候鼠标可能就在绘制区域了,那么有可能m_start没能获取到值,那么后面的绘制就会出现一些问题。

注:状态肯定要在构造函数里面初始化

1
2
3
4
CMFCPaintView::CMFCPaintView() noexcept{
// TODO: 在此处添加构造代码
m_status = FALSE;
}

InvalidateRect(NULL);的作用
加了这个之后虽然绘制的线条没有刷新了,但是会接住前面的鼠标释放的点。看似连贯但是效果不对。
而没加这个,则是无论画多少条线都只会显示最近一次画的,也就是传统说法被刷新了或者叫重绘。

为了保证安全,再多加一个当前点位的变量。

1
2
3
4
5
6
7
CMFCPaintView::CMFCPaintView() noexcept{
// TODO: 在此处添加构造代码
m_status = FALSE;
m_start = { 0,0 };
m_stop = { 0,0 };
m_cur = { 0,0 };
}

反正构造函数不要浪费,避免不必要的错误,就都从0,0坐标开始初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CMFCPaintView::OnLButtonDown(UINT nFlags, CPoint point){
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_start = point;
m_status = TRUE;
CView::OnLButtonDown(nFlags, point);
}

void CMFCPaintView::OnLButtonUp(UINT nFlags, CPoint point){
// TODO: 在此添加消息处理程序代码和/或调用默认值
m_stop = point;

//获取设备上下文
CDC *pDC = GetDC();
pDC->MoveTo(m_start);
pDC->LineTo(m_stop);
ReleaseDC(pDC);
m_status = FALSE;

CView::OnLButtonUp(nFlags, point);
}

按下和释放不需要怎么改动。

在鼠标移动消息中

1
2
3
4
5
6
7
8
9
void CMFCPaintView::OnMouseMove(UINT nFlags, CPoint point){
// TODO: 在此添加消息处理程序代码和/或调用默认值
if (m_status){
InvalidateRect(NULL);
m_cur = point; //让m_cur = 当前的鼠标坐标。
}

CView::OnMouseMove(nFlags, point);
}

至于这个刷新,它会自动触发函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void CMFCPaintView::OnDraw(CDC* pDC){
CMFCPaintDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;

// TODO: 在此处为本机数据添加绘制代码
if (m_status){
pDC->MoveTo(m_start);
pDC->LineTo(m_cur);
} else{
pDC->MoveTo(m_start);
pDC->LineTo(m_stop);
}
}

下面的绘图代码才是我们需要添加的,前面不用管,前面的就是相当于一个刷新白板的操作。
也就是我们之前提到过的,最大化和最小化的时候,之前画的线条就消失了,其实就是触发了重绘没有保存。

InvalidateRect会调用OnDraw,在我们没有重写OnDraw的时候,默认操作就是重绘白板。

一个m_start和m_stop和m_cur只能完整的记录一条线。如果想要画多条,就要用到列表方式去控制。


画笔

之前的画线,它的粗细和颜色和形状都不能调整。所以可以尝试修改这些。

用画笔自然也要用它封装好的类,CPen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class CPen : public CGdiObject
{
DECLARE_DYNAMIC(CPen)

public:
static CPen* PASCAL FromHandle(HPEN hPen);

// Constructors
CPen();
CPen(int nPenStyle, int nWidth, COLORREF crColor);
CPen(int nPenStyle, int nWidth, const LOGBRUSH* pLogBrush,
int nStyleCount = 0, const DWORD* lpStyle = NULL);
BOOL CreatePen(int nPenStyle, int nWidth, COLORREF crColor);
BOOL CreatePen(int nPenStyle, int nWidth, const LOGBRUSH* pLogBrush,
int nStyleCount = 0, const DWORD* lpStyle = NULL);
BOOL CreatePenIndirect(LPLOGPEN lpLogPen);

// Attributes
operator HPEN() const;
int GetLogPen(LOGPEN* pLogPen);
int GetExtLogPen(EXTLOGPEN* pLogPen);

// Implementation
public:
virtual ~CPen();
#ifdef _DEBUG
virtual void Dump(CDumpContext& dc) const;
#endif
};

这里看到构造的时候有两个带参数了

1
2
3
CPen(int nPenStyle, int nWidth, COLORREF crColor);
CPen(int nPenStyle, int nWidth, const LOGBRUSH* pLogBrush,
int nStyleCount = 0, const DWORD* lpStyle = NULL);

刚学先用前者
style就是样式,那么具体有

  1. PS_SOLID 实线
  2. PS_DASH 虚线
  3. PS_DOT 点线
  4. PS_DOTDASH 点划线

转到定义之后注释其实也很详细的给出了样子

1
2
3
4
5
#define PS_SOLID            0
#define PS_DASH 1 /* ------- */
#define PS_DOT 2 /* ....... */
#define PS_DASHDOT 3 /* _._._._ */
#define PS_DASHDOTDOT 4 /* _.._.._ */

width自然就是线宽了,一般的单位都是像素。传int就完事。
color颜色,表示起来就RGB(RED,GREE,BULR)按照ps那会的情况,数值应该是0-255,暂时没看这里能不能用十六进制的方式塞。

设置好了笔的属性,就该用这支笔去画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CMFCPaintView::OnDraw(CDC* pDC){
CMFCPaintDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;

// TODO: 在此处为本机数据添加绘制代码
CPen pen(PS_DASH, 3, RGB(255, 0, 0));
CPen *pPen = pDC->SelectObject(&pen); //要把这个笔加入设备上下文

if (m_status){
pDC->MoveTo(m_start);
pDC->LineTo(m_cur);
} else{
pDC->MoveTo(m_start);
pDC->LineTo(m_stop);
}
//还原笔的属性是为了保证后面要画的时候不会还是这个类型
pDC->SelectObject(pPen);
}

可以看到粗细和颜色是有变化了,但是样式好像没看出变化。
越来越粗的话肯定是看不出变化的。那么改小一点
CPen pen(PS_DASH, 1, RGB(255, 0, 0));

可以看到了虚线效果。。。嘶,还挺麻烦。只能用一个像素点才能看到效果。


TRACE

这是一个调试的时候用的函数,可以在输出栏里面打印出一些需要的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void CMFCPaintView::OnDraw(CDC* pDC){
CMFCPaintDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;

// TODO: 在此处为本机数据添加绘制代码
CPen pen(PS_DASH, 4, RGB(255, 0, 0));
CPen *pPen = pDC->SelectObject(&pen);

LOGPEN logpen;
pPen->GetLogPen(&logpen);
TRACE("\nstyle:%d width:%d color:%08X\n", logpen.lopnStyle, logpen.lopnWidth, logpen.lopnColor);

if (m_status){
pDC->MoveTo(m_start);
pDC->LineTo(m_cur);

} else{
pDC->MoveTo(m_start);
pDC->LineTo(m_stop);

}
//还原笔的属性是为了保证后面要画的时候不会还是这个类型
pDC->SelectObject(pPen);
}

毕竟有的时候单步调试比较累,如果能隐约猜到,可以尝试打印看看是否有问题。


画刷

用过画图其实应该知道,画笔毕竟画的是点阵练成的线,而画刷画出来的是实心的对象。
一个点绘,一个填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class CBrush : public CGdiObject
{
DECLARE_DYNAMIC(CBrush)

public:
static CBrush* PASCAL FromHandle(HBRUSH hBrush);

// Constructors
CBrush();
CBrush(COLORREF crColor); // CreateSolidBrush
CBrush(int nIndex, COLORREF crColor); // CreateHatchBrush
explicit CBrush(CBitmap* pBitmap); // CreatePatternBrush

BOOL CreateSolidBrush(COLORREF crColor);
BOOL CreateHatchBrush(int nIndex, COLORREF crColor);
BOOL CreateBrushIndirect(const LOGBRUSH* lpLogBrush);
BOOL CreatePatternBrush(CBitmap* pBitmap);
BOOL CreateDIBPatternBrush(HGLOBAL hPackedDIB, UINT nUsage);
BOOL CreateDIBPatternBrush(const void* lpPackedDIB, UINT nUsage);
BOOL CreateSysColorBrush(int nIndex);

// Attributes
operator HBRUSH() const;
int GetLogBrush(LOGBRUSH* pLogBrush);

// Implementation
public:
virtual ~CBrush();
#ifdef _DEBUG
virtual void Dump(CDumpContext& dc) const;
#endif
};

CBrush即为c++中的画刷。而构造函数里面,有个是就放颜色就行的。

起步都一样,创建了之后添加到设备上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void CMFCPaintView::OnDraw(CDC* pDC)
{
CMFCPaintDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;

// TODO: 在此处为本机数据添加绘制代码
CPen pen(PS_DASH, 4, RGB(255, 0, 0));
CPen *pPen = pDC->SelectObject(&pen);

CBrush brush(RGB(0, 255, 0));
CBrush *pBrush = pDC->SelectObject(&brush);

LOGPEN logpen;
pPen->GetLogPen(&logpen);
TRACE("\nstyle:%d width:%d color:%08X\n", logpen.lopnStyle, logpen.lopnWidth, logpen.lopnColor);

if (m_status){
// pDC->MoveTo(m_start);
// pDC->LineTo(m_cur);

//填充矩形
pDC->FillRect(CRect(m_start, m_cur), &brush);

} else{
// pDC->MoveTo(m_start);
// pDC->LineTo(m_stop);

pDC->FillRect(CRect(m_start, m_stop), &brush);
}
//还原笔的属性是为了保证后面要画的时候不会还是这个类型
pDC->SelectObject(pPen);
pDC->SelectObject(pBrush);
}

其实就是从按下到释放的两个点延伸出去直至闭合形成一个图形。
至于填充的颜色,画刷初始化的时候选择绿色,则默认也为绿色,也就是说pDC->FillRect(CRect(m_start, m_cur), &brush);后面的参数不选择画刷,用NULL,默认的颜色也是画刷的颜色。
也可以创建别的颜色的画刷对象,然后传递,这倒不是啥大问题。

FillRect是填充矩形,那么还有别的几个,下次再整。


光标和文本

我们现在跑的这个mfc程序,虽然中间那个空白区域是一个编辑区域,但是前面的功能都是绘制,与传统本文编辑区域而言,他少了一个光标,还有行号或者是分层的感觉。

至于这个创建光标加在哪里,构造函数肯定不可行,因为窗口绑定有很多相关的东西,不代表你这个地方构造完成了,其它绑定窗口的东西并不一定全部起来了。放在ondraw里面也不合适,那里重绘的话太频繁了这个光标。

所有的windows程序和mfc程序
第一阶段都是构造的时候
第二阶段才到达create阶段,在这个时候才会把构造的对象和窗口句柄之类的绑定
第三阶段要么showWindow或者doModule,就是显示出这些程序的框架,也就是跑起来了
第四阶段大致就是destroy阶段,它这个时候就是去销毁掉窗口
第五阶段就是delete阶段,这个时候才是销毁掉构造的对象

所以构造什么的是肯定行不通了,就要用到Create消息
快捷操作就是类视图,选择CMFCPaintView,然后属性里面找到消息,选择create

1
2
3
4
5
6
7
8
int CMFCPaintView::OnCreate(LPCREATESTRUCT lpCreateStruct) {
if ( CView::OnCreate(lpCreateStruct) == -1 )
return -1;

// TODO: 在此添加您专用的创建代码

return 0;
}

就会得到这样一个模板。这个create消息是create完成之后广播到各个控件,然后调用自己的构造函数,然后去完成一些自定义的操作。

1
2
3
4
5
6
7
8
9
10
int CMFCPaintView::OnCreate(LPCREATESTRUCT lpCreateStruct) {
if ( CView::OnCreate(lpCreateStruct) == -1 )
return -1;

// TODO: 在此添加您专用的创建代码
CreateSolidCaret(3, 20);
ShowCaret();

return 0;
}

在随便指定这个光标大小之后

截图所以不管他闪不闪了,不过因为这个数值是我们指定的,所以当输入的字体万一大于光标或者小于都会看着很奇怪,显然别的程序肯定是有自适应大小的解决方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int CMFCPaintView::OnCreate(LPCREATESTRUCT lpCreateStruct) {
if ( CView::OnCreate(lpCreateStruct) == -1 )
return -1;

// TODO: 在此添加您专用的创建代码
CClientDC dc(this);

TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
CreateSolidCaret(2, tm.tmHeight);
ShowCaret();

return 0;
}

用了点稀奇古怪的感觉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typedef struct tagTEXTMETRICW
{
LONG tmHeight;
LONG tmAscent;
LONG tmDescent;
LONG tmInternalLeading;
LONG tmExternalLeading;
LONG tmAveCharWidth;
LONG tmMaxCharWidth;
LONG tmWeight;
LONG tmOverhang;
LONG tmDigitizedAspectX;
LONG tmDigitizedAspectY;
WCHAR tmFirstChar;
WCHAR tmLastChar;
WCHAR tmDefaultChar;
WCHAR tmBreakChar;
BYTE tmItalic;
BYTE tmUnderlined;
BYTE tmStruckOut;
BYTE tmPitchAndFamily;
BYTE tmCharSet;
} TEXTMETRICW, *PTEXTMETRICW, NEAR *NPTEXTMETRICW, FAR *LPTEXTMETRICW;
#ifdef UNICODE
typedef TEXTMETRICW TEXTMETRIC;

反正传入这个的目的就是为了获取到里面的hight。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CClientDC : public CDC
{
DECLARE_DYNAMIC(CClientDC)

// Constructors
public:
explicit CClientDC(CWnd* pWnd);

// Attributes
protected:
HWND m_hWnd;

// Implementation
public:
virtual ~CClientDC();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
};

显然这个类是继承CDC的一个派生类,那么这个派生类也能拿到基类的设备上下文了。

跑起来之后反正目前看上去差别不大。

光标位置有了,就是输入的时候

1
2
3
4
5
void CMFCPaintView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) {
// TODO: 在此添加消息处理程序代码和/或调用默认值

CView::OnChar(nChar, nRepCnt, nFlags);
}

这玩意也比较有意思。
当我们在输出的地方打印这个uChar的时候

1
2
3
4
5
6
void CMFCPaintView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) {
// TODO: 在此添加消息处理程序代码和/或调用默认值
TRACE("%c\r\n", nChar);

CView::OnChar(nChar, nRepCnt, nFlags);
}

也就是每当键盘按下一个键,他就会接收到。既然它能接收到,那就好办了。

1
2
3
4
5
6
7
8
9
void CMFCPaintView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) {
// TODO: 在此添加消息处理程序代码和/或调用默认值
TRACE("%c\r\n", nChar);
CClientDC dc(this);
m_strText += (TCHAR)nChar;
dc.TextOut(0, 0, m_strText);

CView::OnChar(nChar, nRepCnt, nFlags);
}

写入肯定还是要获取设备上下文的,然后就是TextOut的最后一个参数是Cstring,在这里创建临时变量的话,也不能保证后面别的地方会不会用到,就干脆在类里面新建一个成员保存。

1
2
3
4
5
6
protected:
CPoint m_start; //起始位置,用于绘制线条
CPoint m_cur; //当前点位,用于绘制线条
CPoint m_stop; //终止位置,用于绘制线条
BOOL m_status; //绘制状态
CString m_strText; //用户输出的字符串

这里m_strText += (TCHAR)nChar;转换是因为vs2022反正项目默认都是unicode编码的也就是宽字节,所有的字符占两个字节,而多字节也就是ANSI,在ANSI中英文占用一个字节,所以二者会有区别。这也是有的时候要么改编码环境要么强转。

跑起来试试

可以看到还是有点问题,比如这个光标不移动,还要每次输入闪烁很明显,闪烁肯定是重绘的问题。
所以在重绘函数ondraw里面来一次textout就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void CMFCPaintView::OnDraw(CDC* pDC){
CMFCPaintDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;

// TODO: 在此处为本机数据添加绘制代码
CPen pen(PS_DASH, 4, RGB(255, 0, 0));
CPen *pPen = pDC->SelectObject(&pen);
CBrush brush(RGB(0, 255, 0));
CBrush *pBrush = pDC->SelectObject(&brush);

LOGPEN logpen;
pPen->GetLogPen(&logpen);
TRACE("\nstyle:%d width:%d color:%08X\n", logpen.lopnStyle, logpen.lopnWidth, logpen.lopnColor);

if (m_status){
pDC->FillRect(CRect(m_start, m_cur), &brush);
} else{
pDC->FillRect(CRect(m_start, m_stop), &brush);
}
//还原笔的属性是为了保证后面要画的时候不会还是这个类型
pDC->SelectObject(pPen);
pDC->SelectObject(pBrush);

//重绘字符串
pDC->TextOut(0, 0, m_strText);
}

然后在onchar消息里面刷新

1
2
3
4
5
6
7
8
9
10
void CMFCPaintView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) {
// TODO: 在此添加消息处理程序代码和/或调用默认值
TRACE("%c\r\n", nChar);
CClientDC dc(this);
m_strText += (TCHAR)nChar;
dc.TextOut(0, 0, m_strText);
InvalidateRect(NULL);

CView::OnChar(nChar, nRepCnt, nFlags);
}

闪烁问题就解决了。

另外测试的时候回车键有点问题,因为没有对\n进行处理

其实这些操作都是在考虑一个消息该怎么处理

  1. 要确定响应什么消息,像鼠标按下,就是lbuttondown
  2. 添加消息响应函数,快捷方式从消息中add
  3. 追加消息响应内容,默认是空的,你要给这个消息额外写一些功能

其中比较麻烦的就是确定响应哪个消息,一开始肯定是不知道了只能查了。还有就是响应规则,有些会和头部预先定义的相关,有些则是全都由自己来写。

那么现在还有两个问题,一个多行文本一个光标移动。

换行这个问题其实根本在TextOut上,因为这个方法不具备换行能力。而我们又要考虑重绘的问题,就需要在OnDraw里改动。
最简单的就是用循环,然后if判断输入的是是否为换行,如果是就要让TextOut的y轴变大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void CMFCPaintView::OnDraw(CDC* pDC)
{
CMFCPaintDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;

// TODO: 在此处为本机数据添加绘制代码
CPen pen(PS_DASH, 4, RGB(255, 0, 0));
CPen *pPen = pDC->SelectObject(&pen);
CBrush brush(RGB(0, 255, 0));
CBrush *pBrush = pDC->SelectObject(&brush);

LOGPEN logpen;
pPen->GetLogPen(&logpen);
TRACE("\nstyle:%d width:%d color:%08X\n", logpen.lopnStyle, logpen.lopnWidth, logpen.lopnColor);

if (m_status){
pDC->FillRect(CRect(m_start, m_cur), &brush);
} else{
pDC->FillRect(CRect(m_start, m_stop), &brush);
}
//还原笔的属性是为了保证后面要画的时候不会还是这个属性的笔
pDC->SelectObject(pPen);
pDC->SelectObject(pBrush);

//重绘字符串
CString sub = _T(""); //用来记录要绘制的字符
int y = 0;
for ( int i = 0; i < m_strText.GetLength(); i++ ) {
if (( m_strText.GetAt(i) == '\n' ) || (m_strText.GetAt(i) == '\r')) {
pDC->TextOut(0, y, sub);
sub.Empty();
y += 20;
continue;
}
sub += m_strText.GetAt(i);
}
//sub不为空就直接打印
if ( !sub.IsEmpty() ) pDC->TextOut(0, y, sub);
}

换行的效果实现了,这里比较坑的是if (( m_strText.GetAt(i) == '\n' ) || (m_strText.GetAt(i) == '\r'))获取的换行符好像会被\r顶掉,但是\n还是得保留,因为键盘输入的角度都是\r了,但是万一黏贴的文本是个\n就有点搞了。
不过显然是获取数据的时候有点小问题,要是不想在这if,可能就要在onchar消息里面修改掉。

最后就是光标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void CMFCPaintView::OnDraw(CDC* pDC)
{
CMFCPaintDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;

// TODO: 在此处为本机数据添加绘制代码
CPen pen(PS_DASH, 4, RGB(255, 0, 0));
CPen *pPen = pDC->SelectObject(&pen);
CBrush brush(RGB(0, 255, 0));
CBrush *pBrush = pDC->SelectObject(&brush);

LOGPEN logpen;
pPen->GetLogPen(&logpen);
TRACE("\nstyle:%d width:%d color:%08X\n", logpen.lopnStyle, logpen.lopnWidth, logpen.lopnColor);

if (m_status){
pDC->FillRect(CRect(m_start, m_cur), &brush);
} else{
pDC->FillRect(CRect(m_start, m_stop), &brush);
}
//还原笔的属性是为了保证后面要画的时候不会还是这个类型
pDC->SelectObject(pPen);
pDC->SelectObject(pBrush);

//重绘字符串
CString sub = _T(""); //用来记录要绘制的字符
int y = 0;

for ( int i = 0; i < m_strText.GetLength(); i++ ) {
if (( m_strText.GetAt(i) == '\n' ) || (m_strText.GetAt(i) == '\r')) {
pDC->TextOut(0, y, sub);
CSize sz = pDC->GetTextExtent(sub);
sub.Empty();
//y += 20;
y += sz.cy + 2; //+2是为了留点行间距
continue;
}
sub += m_strText.GetAt(i);
}
//循环里面
if ( !sub.IsEmpty() ) pDC->TextOut(0, y, sub);

//移动光标
CPoint cp;
CSize sz = pDC->GetTextExtent(sub);
cp.y = y; //y是局部设置好的
cp.x = sz.cx; //通过捕捉sub,得到x和y
SetCaretPos(cp);
}

光标的y轴还算好计算的,毕竟换行的时候就会根据y改变
x则需要借助CSize sz = pDC->GetTextExtent(sub);这么一个获取设备上下文的文字范围。其中最为关键的就是cx和cy。
所以也修改了换行的时候y的值,但从20这个固定值,只能保证常用字符,有些汉字什么的就不能保证了,所以还是主动获取最好。

要注意,换行前的xy和换行后的xy肯定是不同的,不要想着用一个CSize

效果差不多了。

像玩的深入的还能这样该后面的移动光标
SetCaretPos(CPoint(sz.cx + 2, y));
一句话就该过去了,实际上也是用了父类子类之间的关系,然后就是构造函数。
看了老师的操作雀食不一样,还有一种调用系统api的方式::SetCaretPos(sz.cx + 2, y);,不过毕竟是系统api,不是mfc的直接内容,所以有的时候为了项目维护还是不搞花里胡哨的。

但是仍然有很多不足

  1. 没有删除的功能
  2. 比如左键拖动选中文字,文字的背景颜色会变黑之类的

这些日后再说


菜单和工具栏

菜单栏

资源视图没有的,在vs的菜单栏找到视图,再找其他窗口里面就有了。

然后就是经典设计,注意修改id,默认尾巴跟数字不利于使用和查看。然后描述文字画矩形(&R)后面的是快捷键的一种表达方式,反正具体的应该是mfc写好了。我们只要跟着这样格式写就行。alt+你所输入的字符即为快捷键

跑起来效果肯定就没啥差别

灰色应该是没有事件处理,就是死的按钮。

然后就是给这个菜单绑定事件

类别表选择view。看下面的菜单命令路由

确定之后就会加载一个空的函数了

1
2
3
void CMFCPaintView::OnDrawLine() {
// TODO: 在此添加命令处理程序代码
}

TRACE("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);
这条内容主要是打印文件路径,然后行号,还有就是函数名
典型的debug调试时候使用。

我们把画矩形绑定事件,不同的就是类列表不同,前面那个在view下,这个在doc下

1
2
3
4
void CMFCPaintDoc::OnDrwaRect() {
// TODO: 在此添加命令处理程序代码
TRACE("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);
}

然后run一个
当我们点击了这个画线和画矩形的时候,输出那里就能看到详细的文件路径、行号和函数名

有意思的是双击输出中的这一行TRACE打印的内容他会直接跳转到这个函数的位置

对于测试来说这个肯定挺好用的。


菜单命令路由

  1. 有view和doc,触发了view,但是没有触发doc
  2. 去掉view类的菜单响应函数,打开doc类的响应函数。触发view类,不触发doc;view > doc
  3. 去掉了doc类的菜单响应函数,打开框架类的响应函数触发doc类,不触发app;doc > 框架
  4. 去掉了app类的菜单响应函数,打开app类的响应函数; 框架 > app

所以响应菜单的命令顺序:view > doc > 框架 > app
在这个mfc程序里面看:CMFCPaintView > CMFCPaintDoc > CMainFrame > CMFCPanitApp

测试的方式可以通过TRACE("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);
在view中都绑定事件,然后在其它doc下也绑定,看看到底先触发的是view还是什么。

经典MVC模式中,M是指业务模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。其中,View的定义比较清晰,就是用户界面。


工具栏

也是通过资源视图去找,找到toolbar,然后叫mainframe

下面哪个256是因为有别的颜色,但是本身都是一个东西,所以做一个东西俩都要弄

绘制完成后,修改id

注意,如果这个id选择的是以前写过内容的id,那么这个工具栏的按钮就会绑定之前的内容
比如我这个又使用DRAW_LINE这是之前测试的

1
2
3
4
void CMFCPaintView::OnDrawLine() {
// TODO: 在此添加命令处理程序代码
TRACE("%s(%d):%s\r\n", __FILE__, __LINE__, __FUNCTION__);
}

内容是这样的,我们run这个程序点击看看是否会触发

我们点击了三次,它也的确触发了三次,说明确实绑定了。

删除这个工具栏的选项,要点击然后拖出去就能删掉了,默认没啥地方有删除选项


mfc结构文档

文档戳链接,如果有误跳转时可修改最后参数,此参数为年份

看层次结构的目的是为了以后当参数转换啥的更方便,像自己在vs里面不断跳转定义也ok,就是稍微麻烦了点。

层次的视觉感观会更直接,比如生物-动物-人-男人,作为派生类,总会有一些与其父类相关的特性。

CObject

  • 支持序列化
    • 可能把一个结构体变成一个字符串,几个字节存放一个数据,最后留俩当长度
    • 也可能是{x:100,y:200}这样结构化,类似于json
    • ….等都是一种序列化的表现
  • 支持运行时提供类的信息
    • static CRuntimeClass *PASCAL _GetBaseClass();
    • static CRuntimeClass *PASCAL GetThisClass();
    • 正常的派生类下来,并不会刻意保留父类叫什么,而mfc做了一些优化
  • 支持动态创建以及支持对象诊断输出
    • virtual void AsserValid() const;
    • virtual void Dump(CDumpContext& dc) const;
    • 当mfc启动时,有些需求不是立马就创建的,只有当触发的时候才会创建

有这么一个超级基类的存在,就提供了无限的可能,但是这种级别的东西不适合个人开发。


mfc框架理论

关键类

CWinApp:MFC应用程序抽象,负责管理Document Template
CFrameWnd:框架窗口、负责创建应用的主窗口,含标题栏、菜单栏、工具栏、状态栏等
CView:负责展示应用数据,View其实是一个没有边框的窗口,客户区
CDocument:负责存储应用数据


关键类之间的关系

CDocTemplate、CDocument、CView、CFrameWnd关系

  • CWinApp 拥有一个对象指针:CDocManager *m_pDocManager
  • CDocManager拥有一个指针链表CPtrList m_templateList,用来维护一系列的DocumentTemplate。应用程序在CMyWinApp::InitInstance中以AddDocTemplate将这些Document Templates加入到有CDocTemplate所维护的链表之中
  • CDocTemplate拥有三个成员变量,分别持有Document、View、Frame的CRuntimeClass指针,另有一个成员变量m_nIDResource,用来表示此Document显示时应该采用的UI对象。这四位数据在CMyWinApp::InitInstance函数构造CDocTemplate时指针,称为构造函数的参数。
  • CDocument有一个成员变量CDocTemplate *m_pDocTemplate,回指其DocumentTemplate;另外有一个成员变量CPtrList m_viewList,表示它可以同时维护一组Views。
  • CFrameWnd有一个成员变量Cview *m_pViewActive,指向当前活动的View
  • CView有一个成员变量CDocument *m_pDocument,指向相关的Document

结构层次化都是为了方便开发和维护。


消息分类

mfc消息的分类大致分为三种:标准消息、命令消息、通告消息。

  1. 标准消息:除WM_COMMAND之外,所有以WM_开头的消息。从CWnd类派生的类都可以接受到这一类消息
  2. 命令消息:来自菜单、加速键或者工具栏按钮的消息。这类消息都以WM_COMMAND呈现。在MFC中,通过菜单项的标识(id)来区分不同的命令消息;在sdk中,通过消息的wParam参数识别。从CCmdTarget(CWnd的父类)派生的类都可以接收到这一类消息
  3. 通告消息:由控件产生的消息,例如按钮的单击,列表框的选择等均会产生此类消息,为的是向其父窗口(通常为对话框)通知时间的发生。这类消息也是以WM_COMMAND形式呈现。从CCmdTarget(CWnd的父类)派生的类都可以接收到这一类消息。

小结:凡是从Cwnd派生的类,即可以接受标准消息,也可以接收命令消息和通告消息。
而对于那些从CCmdTarget派生的类,则只能接受命令消息和通告消息,不能接受标准消息。


对话框

是与用户进行交互的控件,如文件对话框、字体对话框、颜色对话框等,一般用于告示、提醒等。

app这个是之前创建的对话框,从类视图可以看到只有三个类。比文档的结构观感上要简洁不少。

其中标准消息可以直接通过类视图->选择项目的类->属性里面找到消息即可。

对话框其实就是一个窗口,它不仅可以接收消息,而且还可以被移动或者关闭,甚至是在客户区中进行绘图。这些都是有CWnd类派生而来。

其中除了最基础的消息,还有控件

利用拖动控件的方式,可以省去很多麻烦。


创建对话框

同样的,已有的几个是根据项目类型产生的,我们当然可以进行创建
在资源视图中,选这个项目的Dialog,然后右击添加资源

这里不选子类直接选Dialog也是可以的。

通过Dialog创建的它的对话框属性只有一个id能修改,问题不大,改个有意义的就行

资源属性这边能改的就很多。

图形化的做完了,代码实现部分就需要类去控制它

直接右击添加类,类名随便写尽量有意义,然后继承的基类,一般是这头两个。ex说过就是拓展的意思。

这种通过简单的方式创建出模板的好处就是省去了一些小麻烦,也不容易出问题

1
2
3
4
5
6
7
8
void CMFCApplication1Dlg::OnBnClickedOk() {
// TODO: 在此添加控件通知处理程序代码
//MessageBox(_T("你好呀!"));
CBingDialog dlg;
dlg.DoModal();

CDialogEx::OnOK();
}

在主窗口的确认按钮下设置,将我们新建的CBingDialog作为模块化弹出

当dlg弹出之后,原先的窗口是不可改变的状态,只有dlg关闭之后才能操作后面的对话框。

DoModal这就是所谓的模态对话框,有的时候会觉得不太方便吧,但是如果是警示之类的还是挺好的。

有模态化就有非模态化的,非模态化的一个问题其实猜也能猜到,就是因非模态化而产生的对话框不会卡住,如果这个变量优先级不够,很有可能在不知道的地方就被析构释放了,在逻辑上会导致很严重的问题,而且设置起来也较为麻烦。

首先因为没有锁或者说阻塞,所以当按钮按下的适合,这个新建的对话框一闪而过,结束的很快。
所以要在头文件类中建一个全局变量。

1
2
3
4
5
6
7
8
class CMFCApplication1Dlg : public CDialogEx
{
// 构造
public:
CMFCApplication1Dlg(CWnd* pParent = nullptr); // 标准构造函数
CBingDialog dlg; //注意引用头文件不然是未定义的类型
//....后面的省略
}

然后又要在对话框源文件中,找到oninitdialog函数

1
2
// TODO: 在此添加额外的初始化代码
dlg.Create(IDD_DIALOG_NEW, this);

在这完成初始化。

最后在按钮消息中触发显示

1
2
3
void CMFCApplication1Dlg::OnBnClickedOk() {
dlg.ShowWindow(SW_SHOW);
}

可以看到非模态化的对话框设置起来就要挺多步骤了。

非模态化的效果就是不会阻塞,后面的窗口是可以操作的。

或者你可以直接在对话框源文件中定义全局变量CBingDialog dlg;
然后再按钮消息中设置

1
2
3
4
void CMFCApplication1Dlg::OnBnClickedOk() {
dlg.Create(IDD_DIALOG_NEW, this);
dlg.ShowWindow(SW_SHOW);
}

也是可行的,因为这个dlg变量不会再按钮结束后立马被析构掉。

总而言是,模态化对话框的使用场景肯定是基于会修改影响到其他窗口,这个时候肯定要设置称模态化对话框去阻塞,不然改动了还原的部分都没的操作了。非模态像vs的视图吧,这些对话框不会直接影响主窗口的就适用于非模态


按钮

按钮的创建,从图形化角度,直接拖动控件是最直接的,拖动完成后保存,然后双击这个按钮直接会跳转到代码界面,你就可以编辑这个按钮消息能干什么了。

新建一个按钮,修改一下描述文字和id,然后双击开始编辑

1
2
3
4
void CBingDialog::OnBnClickedButtonTest() {
// TODO: 在此添加控件通知处理程序代码
TRACE("%s(%d):%s\n", __FILE__, __LINE__, __FUNCTION__);
}

老样子在日志里输出文件路径,行号,函数名。

这玩意肯定是不会有啥问题了。

至于动态布局,就是比例放大或者缩小的适合这个按钮的位置会自动调整,不设置的话窗口放大或者缩小它的位置都不会改变。

其它的一些行为都有中文描述可以自己试一下。

然后就是通过按钮去创建自定义按钮,本质上就是手动整活了。

1
2
protected:
CButton m_Btn; //自定义按钮

在头文件中肯定要预先定义这个空的按钮。

1
2
3
4
5
6
7
8
void CBingDialog::OnBnClickedButtonTest() {
// TODO: 在此添加控件通知处理程序代码
TRACE("%s(%d):%s\n", __FILE__, __LINE__, __FUNCTION__);

if ( m_Btn.m_hWnd == NULL ) {
m_Btn.Create(_T("动态"), BS_DEFPUSHBUTTON | WS_VISIBLE | WS_CHILD, CRect(100, 100, 200, 150), this, 9999);
}
}

BS_DEFPUSHBUTTON | WS_VISIBLE | WS_CHILD
BS开头就是button style,按钮自带的样式,WS就是 windows style,译为windows系统的样式,具体种类可以转到定义,有不少这样的类型。

注意下最后的id,不要通过变量传递去改变,一定要固定为主,避免冲突或者广播,因为id重了,获取消息的时候就可能一起接收或者发送

跑起来之后,点击test_button之后就会弹出这个动态的按钮,看上去可能有点潦草,毕竟是随便建的。

如果采用全局变量dlg是在源文件的要注意一下
可能存在窗口创建多次报错

1
2
3
4
5
6
void CMFCApplication1Dlg::OnBnClickedOk() {
if ( dlg.m_hWnd == NULL ) {
dlg.Create(IDD_DIALOG_NEW, this);
}
dlg.ShowWindow(SW_SHOW);
}

给他加个验证以防万一

动态按钮的创建,还是随着用户改变为主,一般用的少
不过没启用之前也不占资源倒是一件好事,顶多留个指针。


控件

正常情况下,我们知道访问这个控件,比如按钮,肯定是在当前窗口访问最直接也不需要额外操作,但是别的窗口如果要访问,就是另一回事了。

首先给这个对话框里面添加三个文本框,默认是空的,通过别的地方传递给这个文本框一个初始值。

在自定义的对话框初始化一个文本框之前需要注意,我们自定义的他少一个标准的函数OnInitDialog()作为初始化用

打开类视图->找到这个项目选择CBingDialog->然后属性那边有个重写往下滑找到OnInitDialog()点击后面add即可。

1
2
3
4
5
6
7
8
BOOL CBingDialog::OnInitDialog() {
CDialogEx::OnInitDialog();

// TODO: 在此添加额外的初始化

return TRUE; // return TRUE unless you set the focus to a control
// 异常: OCX 属性页应返回 FALSE
}

访问文本框

通过父类CWnd,我们可以直接在对话框初始化这些文本框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BOOL CBingDialog::OnInitDialog() {
CDialogEx::OnInitDialog();

// TODO: 在此添加额外的初始化
CWnd *pEdit01 = GetDlgItem(IDC_EDIT_ONE);
CWnd *pEdit02 = GetDlgItem(IDC_EDIT_TWO);
CWnd *pEdit03 = GetDlgItem(IDC_EDIT_THREE);

if ( pEdit01 != NULL ) pEdit01->SetWindowText(_T("100"));
if ( pEdit02 != NULL ) pEdit02->SetWindowText(_T("200"));
if ( pEdit03 != NULL ) pEdit03->SetWindowText(_T("300"));

return TRUE; // return TRUE unless you set the focus to a control
// 异常: OCX 属性页应返回 FALSE
}

这个setwindowstext不仅能设置,还能取出值,类型应是Cstring

1
2
CString setText;
pEdit01->SetWindowText(strText);

用父类的CWnd去接收可能有点麻烦,但是至少能判断是否获取成功了
因为还有一种直接的方式

1
2
SetDlgItemText(IDC_EDIT_ONE, _T("100"));
GetDlgItemText(IDC_EDIT_ONE, strText);

这种方式呢,主要是产生错误的时候你也不晓得是前者不存在,还是后者溢出。

还有一种是针对无符号整型的

1
2
3
SetDlgItemInt(IDC_EDIT_THREE, 300);
BOOL isTrans = FALSE;
UINT ret = GetDlgItemINt(IDC_EDIT_THREE, &isTrans);

加这个布尔值的意思是,如果传输成功,这个布尔值就会变成TRUE,那么ret的值自然就是300,到不太在意ret了,如果还是FALSE,则说明传递失败了。


添加变量

先去Diglog页面,选中文本框右击,添加变量打开即可。

选择类别,一种是值一种是控件,这边现用值,名称看着来。
选择类别为值后,变量的类型也需要确定,默认来说字符串Cstring更合适,这边先用int玩,注释就更不用说了。

选择int这种值类型,在其他这里就会有最小值和最大值的分别。最大字符数是给字符串类型用的,至于下面的文件倒不用特意选了,毕竟这个添加变量是在CBingDialog下添加的,默认就在这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#pragma once
#include "afxdialogex.h"


// CBingDialog 对话框

class CBingDialog : public CDialogEx
{
DECLARE_DYNAMIC(CBingDialog)

public:
CBingDialog(CWnd* pParent = nullptr); // 标准构造函数
virtual ~CBingDialog();

// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_DIALOG_NEW };
#endif

protected:
CButton m_Btn; //自定义按钮

protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持

DECLARE_MESSAGE_MAP()
public:
afx_msg void OnBnClickedButtonTest();
virtual BOOL OnInitDialog();
// 文本框1的值
int m_Value1;
};

能看到下面有个注释,然后是我们新增的变量。
在源文件中同样有初始化的地方

1
2
3
4
5
void CBingDialog::DoDataExchange(CDataExchange* pDX) {
CDialogEx::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT_ONE, m_Value1);
DDV_MinMaxInt(pDX, m_Value1, -9999, 9999);
}

别的不说,这个绑定在文本框的id和名称,还有下面的最大值最小值肯定看得出来。

那么肯定会好奇绑定这个值类型的变量有什么用?

给另外两个文本框都添加变量
然后给确定按钮写代码

1
2
3
4
5
6
7
8
void CBingDialog::OnBnClickedOk() {
// TODO: 在此添加控件通知处理程序代码
UpdateData(); //无参数默认为TRUE,此时把界面的值传到变量
m_Value3 = m_Value1 + m_Value2;
UpdateData(FALSE); //为FALASE时,把值传回到界面

//CDialogEx::OnOK();
}

就是按下之后,文本框三的内容是1+2的就对了。
这里随便修改一下文本框2的内容,然后再按下确认,能看到文本框3的内容改变了


添加控件

方法一致,在Dialog视图中右击文本框添加变量,只不过类型改成控件
当三个都添加完成之后编写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CBingDialog::OnBnClickedOk() {
// TODO: 在此添加控件通知处理程序代码

CString str1, str2, str3;
m_Edit1.GetWindowText(str1);
m_Edit2.GetWindowText(str2);
int t = _wtoi(str1) + _wtoi(str2);
TCHAR buf[32] = _T("");
_itow_s(t, buf, 10);
str3 = buf;
m_Edit3.SetWindowText(str3);

//CDialogEx::OnOK();
}

控件创建的时候变量类型是CEdit,那么推测也是跟字符串有关的,所以在做加法之前先完成一个转换。
最后得到的结果也是成功的

虽然肯能不是很实用,但是总归是个小技巧


SendMessage

1
2
SendMessage(WM_GETTEXT);
SendMessage(WM_SETTEXT);

不过这俩在函数内部,其实也不知道发给谁,顶多是传给往上一层的。

1
2
3
4
5
6
7
8
9
10
11
12
13
void CBingDialog::OnBnClickedButtonTest() {
// TODO: 在此添加控件通知处理程序代码
TRACE("%s(%d):%s\n", __FILE__, __LINE__, __FUNCTION__);

if ( m_Btn.m_hWnd == NULL ) {
m_Btn.Create(_T("动态"), BS_DEFPUSHBUTTON | WS_VISIBLE | WS_CHILD, CRect(100, 100, 200, 150), this, 9999);
}

TCHAR buf[20] = _T("");
::SendMessage(m_Edit1.m_hWnd, WM_GETTEXT, 20, (LPARAM)buf);
m_Edit1.SendMessage(WM_SETTEXT, sizeof(buf), (LPARAM)buf);
SendMessage(WM_GETTEXT, 20, (LPARAM)buf);
}

老实说这后面的在干什么我也看不懂了。
不过打断点调试之后,看到buf的值是取了这个窗口的标题

不过按照推理m_Edit1.m_hWnd这个应该是通过控件获取到这个当前窗口句柄了,然后get句柄的Text属性到buf上,之所以能找到这个窗口句柄感觉还是因为::全局作用域的关系,然后后面这个文本框发送消息到buf上这个说法上不太通顺,因为用文本框发送消息和按钮按下后发送消息,buf理论都一样了吧,毕竟是从窗口句柄取值的。

打个?后面碰到了在细究


对话框伸缩

其实窗口是有自带的缩放,但是这里先自定义两个按钮去实现

控件拖动完毕后,修改一下id,然后双击按钮跳到代码编辑部分。

放大还是缩小总归是要知道窗口的大小先

现在头文件里预定一大一小

1
2
3
//窗口大小
CRect m_large;
CRect m_small;

注:CRect有四个成员分别是left,top这二者代表矩形左上角顶点坐标,right,bottom代表矩形右下角的坐标,草图如下:

然后在源文件的OnInitDialog()初始化一下

1
2
3
4
GetWindowRect(m_large);
m_small = m_large;
m_small.right = m_small.left + m_small.Width() / 2;
m_small.bottom = m_small.top + m_small.Height() / 2;

最后给放大缩小实现一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CBingDialog::OnBnClickedBtnLarge() {
// TODO: 在此添加控件通知处理程序代码
CRect curRect; //获取当前窗口尺寸信息
GetWindowRect(curRect);
SetWindowPos(NULL, curRect.left, curRect.top,
m_large.Width(), m_large.Height(),
SWP_NOMOVE | SWP_NOZORDER
);

}

void CBingDialog::OnBnClickedBtnSmall() {
// TODO: 在此添加控件通知处理程序代码
CRect curRect;
GetWindowRect(curRect);
SetWindowPos(NULL, curRect.left, curRect.top,
m_small.Width(), m_small.Height(),
SWP_NOMOVE | SWP_NOZORDER
);
}

SWP_NOZORDER:忽略第一个参数;SWP_NOMOVE:忽略x、y,维持位置不变

curRect都是为了先获取当前窗口尺寸
所以当setwindowpos的时候,xy不需要改变,cx和cy则是用m_large和m_small改变。
不过因为m_large初始化的时候是直接根据当前窗口大小来的,所以一开始点击放大是没有反应的,当缩小了之后在点击放大才会改变回原有尺寸

这里调整了一下俩按钮的位置,因为没有加滑动条,所以缩小了原有位置就够不到了。

不过可以看出上述按钮实现雷同点比较多。
那么就有一个骚操作,就是通过获取按钮标签名去改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void CBingDialog::OnBnClickedBtnLarge() {
// TODO: 在此添加控件通知处理程序代码
CRect curRect; //获取当前窗口尺寸信息
GetWindowRect(curRect);

CWnd *pButton = GetDlgItem(IDC_BTN_LARGE);
CString strTitle;
if ( pButton ) {
pButton->GetWindowText(strTitle);
if ( strTitle == _T("放大") ) {
pButton->SetWindowText(_T("缩小"));

SetWindowPos(NULL, curRect.left, curRect.top,
m_large.Width(), m_large.Height(),
SWP_NOMOVE | SWP_NOZORDER
);
} else {
pButton->SetWindowText(_T("放大"));

SetWindowPos(NULL, curRect.left, curRect.top,
m_small.Width(), m_small.Height(),
SWP_NOMOVE | SWP_NOZORDER
);
}
}

}

先点击放大,发现按钮名变成缩小了,然后再次点击,窗口缩小,按钮名称变成放大,再点击就放大了,按钮名称就变成缩小。

所以另一个按钮就没有存在的必要了,把这个放大按钮的名称改成缩小,这样一来开头的光变名字就可以省去了。

最后做一个安全的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void CBingDialog::OnBnClickedBtnLarge() {
// TODO: 在此添加控件通知处理程序代码
CRect curRect; //获取当前窗口尺寸信息
GetWindowRect(curRect);

CWnd *pButton = GetDlgItem(IDC_BTN_LARGE);
CString strTitle;
if ( pButton ) {
pButton->GetWindowText(strTitle);
if ( strTitle == _T("放大") && (m_large.IsRectEmpty() == FALSE) ) {
pButton->SetWindowText(_T("缩小"));

SetWindowPos(NULL, curRect.left, curRect.top,
m_large.Width(), m_large.Height(),
SWP_NOMOVE | SWP_NOZORDER
);
} else if ( m_large.IsRectEmpty() == FALSE ) {
pButton->SetWindowText(_T("放大"));

SetWindowPos(NULL, curRect.left, curRect.top,
m_small.Width(), m_small.Height(),
SWP_NOMOVE | SWP_NOZORDER
);
}
}

}

IsRectEmpty()主要是为了判断这个窗口如果left、top、right、bottom都一样的话,说明这个窗口就只有一个点,并没有办法完成缩放和放大了。

窗口其实除了大小,就是绘制的位置,有的时候不是在当前窗口之上绘制,就有可能掉下去一层跟后面的窗口平级,至于窗口为什么能叠加,应该是除了xy,还有一个z轴,是3d模型的经典概念。


常用控件

空间交互,首先要创建控件或者说拖个出来,交互,就需要绑定控件或者变量,在消息中有来有回实现一些功能。

例如上述所学到的函数GetDlgItem,他就可以通过控件id获取到对应的控件
CListBox *list = (CListBox*)GetDlgItem(控件id)

绑定控件和变量,在消息中曾使用到UpdateData(TRUE|FALSE),默认不填写为TRUE,也就是将控件内容第一时间同步到变量上,FALSE则是将变量同步回控件


Radio

随便建个mfc项目,选择对话框类型,有的没的取消勾选

然后绘制这样的窗口,其中radio和check按钮,最后一个button

性别直接添加一个变量就可以了,原本想着用bool类型,但是考虑到得有个初始值,其实这个初始化关系到也不是特别大,但此处就换个万一弄弄,用int来表示,那么就需要注意了,你需要修改初始化部分

1
2
3
4
5
CMFCButtonDlg::CMFCButtonDlg(CWnd* pParent /*=nullptr*/)
: CDialogEx(IDD_MFCBUTTON_DIALOG, pParent)
, m_sex(-1){
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

m_sex为我们添加的变量名,括号代表初始化值,默认是0,但这里的思路是-1为未初始化,0为男 1为女这样。

如此一来,结果那个按钮第一步就可以上手了

1
2
3
4
5
6
7
8
9
10
void CMFCButtonDlg::OnBnClickedBtnResult(){
// TODO: 在此添加控件通知处理程序代码
UpdateData();

if( m_sex == -1 ){
MessageBox(_T("请选择性别"), _T("性别缺失"), MB_OK | MB_ICONEXCLAMATION);
return;
}

}

_T只是为了兼容unicode,在你的项目编码是ANSI的时候下次转换能保证字符串不出错,其次还有一个_L,它是不管编译方式都按unicode保存

万国码通用保存2两字节,ANSI英文一字节汉语两字节,再次强调

MB_ICONEXCLAMATION是一个警告图标,不同于SWP_NOZORDER,前者为黄色感叹号,后者为红色x号。

然后打印,肯定就要用CString了。

1
2
3
4
5
6
7
8
9
10
11
12
void CMFCButtonDlg::OnBnClickedBtnResult(){
// TODO: 在此添加控件通知处理程序代码
UpdateData();

if( m_sex == -1 ){
MessageBox(_T("请选择性别"), _T("性别缺失"), MB_OK | MB_ICONEXCLAMATION);
return;
}

CString strMsg = _T("您的性别是:") + (m_sex == 0) ? _T("男\n") : _T("女\n");
MessageBox(strMsg);
}

run的时候会发现

额前面那句好像没有加上,原因应该是Cstring没有重写string+string吧

1
2
3
4
5
6
7
8
9
10
11
12
void CMFCButtonDlg::OnBnClickedBtnResult(){
// TODO: 在此添加控件通知处理程序代码
UpdateData();

if( m_sex == -1 ){
MessageBox(_T("请选择性别"), _T("性别缺失"), MB_OK | MB_ICONEXCLAMATION);
return;
}

CString strMsg = CString(_T("您的性别是:")) + ((m_sex == 0) ? _T("男\n") : _T("女\n"));
MessageBox(strMsg, _T("tips"));
}

修改之后:

对于多选框,类型一般还是bool比较合适,但是默认的添加变量都是单个类型,所以我们在自动的基础上,给他改成数组

1
2
// 爱好
BOOL m_hobby[3];

那么在源文件就要注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CMFCButtonDlg::CMFCButtonDlg(CWnd* pParent /*=nullptr*/)
: CDialogEx(IDD_MFCBUTTON_DIALOG, pParent)
, m_sex(-1) {
memset(m_hobby, 0, sizeof(m_hobby));

m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CMFCButtonDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Radio(pDX, IDC_RAD_MAN, m_sex);
DDX_Check(pDX, IDC_CK_FB, m_hobby[0]);
DDX_Check(pDX, IDC_CK_BKB, m_hobby[1]);
DDX_Check(pDX, IDC_CK_YOGA, m_hobby[2]);
}

手动修改成数组

最后完善一下结果消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CMFCButtonDlg::OnBnClickedBtnResult(){
// TODO: 在此添加控件通知处理程序代码
UpdateData();

if( m_sex == -1 ){
MessageBox(_T("请选择性别"), _T("性别缺失"), MB_OK | MB_ICONEXCLAMATION);
return;
}

CString strMsg = CString(_T("您的性别是:")) + ((m_sex == 0) ? _T("男\n") : _T("女\n"));
strMsg += _T("你的爱好有:");
CString hobby[3]{ _T("足球"),_T("篮球"),_T("瑜伽") };
for( int i = 0; i < 3; i++ ){
if( m_hobby[i] ){
strMsg += hobby[i] + _T(" ");
}
}

MessageBox(strMsg, _T("tips"));
}

就ok了,这两个按钮的应用还算基础的。

当然自己定义数组一个办法,也可以通过控件id,get他的name
至于说这个id该通过什么办法
比如项目头文件里面有个叫Resource.h的,打开之后会看到他宏定义了我们跟控件有关的

1
2
3
4
5
6
7
8
9
#define IDD_MFCBUTTON_DIALOG            102
#define IDR_MAINFRAME 128
#define IDC_BUTTON1 1000
#define IDC_BTN_RESULT 1000
#define IDC_RAD_MAN 1001
#define IDC_RAD_WOMEN 1002
#define IDC_CK_FB 1003
#define IDC_CK_BKB 1004
#define IDC_CK_YOGA 1005

复选框就看后面仨,有了这个其实会容易很多,因为他们是连续的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void CMFCButtonDlg::OnBnClickedBtnResult(){
// TODO: 在此添加控件通知处理程序代码
UpdateData();

if( m_sex == -1 ){
MessageBox(_T("请选择性别"), _T("性别缺失"), MB_OK | MB_ICONEXCLAMATION);
return;
}

CString strMsg = CString(_T("您的性别是:")) + ((m_sex == 0) ? _T("男\n") : _T("女\n"));
strMsg += _T("你的爱好有:");
//CString hobby[3]{ _T("足球"),_T("篮球"),_T("瑜伽") };

UINT nId = IDC_CK_FB;

for( int i = 0; i < 3; i++ ){
if( m_hobby[i] ){
//strMsg += hobby[i] + _T(" ");
CString sName;
GetDlgItemText(nId + i, sName);
strMsg += sName;
}
}

MessageBox(strMsg, _T("tips"));
}

因为从足球开始,后面只需要+1就可以得到,倒是省了定义一个数组。

但是这个是基于你这几个复选框是连续的,id才能跟的上,不然跟数组没啥太大差别


EditControl

工具箱里找

然后拖出来,稍微调整一下大小

对应的属性也有不少,好在2022都做成中文了

这玩意说实在没啥必要演示,自己试几下就行了,但是有个基础应用的地方到时跟上面能联动
就是将我们选完之后的内容打印在editcontrol中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void CMFCButtonDlg::OnBnClickedBtnResult(){
// TODO: 在此添加控件通知处理程序代码
UpdateData();

if( m_sex == -1 ){
MessageBox(_T("请选择性别"), _T("性别缺失"), MB_OK | MB_ICONEXCLAMATION);
return;
}

CString strMsg = CString(_T("您的性别是:")) + ((m_sex == 0) ? _T("男\r\n") : _T("女\r\n"));
strMsg += _T("你的爱好有:");
//CString hobby[3]{ _T("足球"),_T("篮球"),_T("瑜伽") };

UINT nId = IDC_CK_FB;

for( int i = 0; i < 3; i++ ){
if( m_hobby[i] ){
//strMsg += hobby[i] + _T(" ");
CString sName;
GetDlgItemText(nId + i, sName);
strMsg += sName;
}
}

CEdit* edit = (CEdit*)GetDlgItem(IDC_EDIT1);
//edit->GetWindowTextW();
edit->SetWindowText(strMsg); //设置文本

MessageBox(strMsg, _T("tips"));
}

其中要注意editcontrol属性要设置几个地方

  1. 多行 true
  2. 想要返回 true
  3. 就是写入的strMsg想要换行,要在中间加\r\n,单纯的\n好像不起作用

效果就是如下:

点击完成后往edit里面写入,和弹出对话框


ListBox

老样子在dialog界面打开工具箱,找到listbox

然后属性都是中文名了,实在不行点击一下下面还有介绍,再不济就自己修改试试

随便搞个例子试试

先绘制这样的界面,然后就是往里面addsttring,最后根据选中的返回结果这样。

别忘了给listbox添加变量

双击test按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void CMFCButtonDlg::OnBnClickedBtnTest(){
// TODO: 在此添加控件通知处理程序代码
CString strText;

if( m_conmpany.GetSelCount() == 0 ){
MessageBox(_T("没有选中任何公司"));
return;
} else{
int total = m_conmpany.GetSelCount();
int* index = new int[total];
strText += _T("您选中了");
TCHAR buf[32] = _T("");
_itow_s(total, buf, 32, 10);
strText += buf;
strText += _T("个公司\n");
m_conmpany.GetSelItems(total, index);

CString strTmp;
for( int i = 0; i < total; i++ ){
m_conmpany.GetText(index[i], strTmp);
strText += strTmp + _T(" ");
}
delete[] index;
MessageBox(strText);
}
}

m_conmpany为我们给listbox这个控件设置的变量。
首要判断就是是否选中,选中之后在循环接收。

别忘了在oninitdialog里面初始化一下这个listbox。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BOOL CMFCButtonDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();

// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标

// TODO: 在此添加额外的初始化代码
m_conmpany.AddString(_T("山东蓝翔"));
m_conmpany.AddString(_T("深圳电子厂"));
m_conmpany.AddString(_T("义乌商超"));

return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}

效果也不难,就是一些常规操作。


Combox

拖出两个控件
数据在

依旧是用分号阻隔。

样式有三个,除了这个simple特殊一点,因为他不会显示箭头,你选中之后可以通过方向键控制。
或者它的神奇之处。。

你可以在dialog里从下面拉大这个combox,这样他在run的时候就能把在长度之内的列显示出来,虽然有点二。

当然这种测试都是取出值来玩玩

左边样式为simple,添加变量
右边样式为下拉列表,添加变量
然后拖一个按钮,测试用

1
2
3
4
5
6
7
8
9
10
11
12
13
void CMFCButtonDlg::OnBnClickedBtnDroplist(){
// TODO: 在此添加控件通知处理程序代码
int cur = m_simple.GetCurSel();
if( cur == -1 ){
TRACE("%s(%d):当前没有选中任何列\n", __FILE__, __LINE__);
}else{
TRACE("%s(%d):当前选中了第%d列\n", __FILE__, __LINE__, cur);
CString tmp;

m_simple.GetLBText(cur, tmp);
MessageBox(tmp);
}
}

也比较简单

我们选中哪个就messagebox弹出哪个,并且TRACE在日志打印,注意列之类的遵循从0开始计数。

那么还有一个下拉列表,直接套前面那个combox也无伤大雅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void CMFCButtonDlg::OnBnClickedBtnDroplist(){
// TODO: 在此添加控件通知处理程序代码
int cur = m_simple.GetCurSel();
if( cur == -1 ){
TRACE("%s(%d):当前没有选中任何列\n", __FILE__, __LINE__);
}else{
TRACE("%s(%d):当前选中了第%d列\n", __FILE__, __LINE__, cur);
CString tmp;

m_simple.GetLBText(cur, tmp);
MessageBox(tmp);
}

cur = m_droplist.GetCurSel();
if( cur == -1 ){
TRACE("%s(%d):当前没有选中任何列\n", __FILE__, __LINE__);
} else{
TRACE("%s(%d):当前选中了第%d列\n", __FILE__, __LINE__, cur);
CString tmp;

m_droplist.GetLBText(cur, tmp);
MessageBox(tmp);
}
}

能看到日志打印的时候,因为m_simple没有选中过,所以会打印未选中任何列,但是后者m_droplist有选中,就有回执信息。

能get的东西挺多的,用到了翻翻文档就行


Progress

Progress:进度条
一般是要配合定时器去用会好点。

当然这玩意在dialog界面看着有点效果,但实际你自己没写,他就是空的。

初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL CMFCButtonDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();

// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标

// TODO: 在此添加额外的初始化代码
m_conmpany.AddString(_T("山东蓝翔"));
m_conmpany.AddString(_T("深圳电子厂"));
m_conmpany.AddString(_T("义乌商超"));

m_progress.SetRange(0, 1000);

return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}

当然有的时候虽然范围可能是整数,但是步长不一样。

给他加个按钮控制增长。
注意改成竖状的时候需要修改进度条外观属性的垂直设置为True

然后双击按钮

1
2
3
4
5
void CMFCButtonDlg::OnBnClickedBtnPrg(){
// TODO: 在此添加控件通知处理程序代码
int pos = m_progress.GetPos();
m_progress.SetPos(pos + 100);
}

获取初始的时候,然后每次增加100,反正上限1000,10次就到顶了

当然实际用途不会蠢蠢的给用户去点击,肯定是要与计时器绑定。
选中对话框找到消息里面的Timer

在头文件中应该预设一个进度值

1
2
//进度
int m_progress_pos;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BOOL CMFCButtonDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();

// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标

// TODO: 在此添加额外的初始化代码
m_conmpany.AddString(_T("山东蓝翔"));
m_conmpany.AddString(_T("深圳电子厂"));
m_conmpany.AddString(_T("义乌商超"));

m_progress.SetRange(0, 1000);
m_progress_pos = 0;

// 定时器尽量不要低于30ms,不同机子有少许差别,源自mfc的精度不足导致定时器缺陷
SetTimer(99, 500, NULL);
SetTimer(10, 100, NULL);

return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}

初始化设置定时器的时候要注意,因为mfc分层,不同机子处理速度不同,所以定时器精度不要设置太小,以免出现误差。

1
2
3
4
5
6
7
8
9
10
11
12
13
void CMFCButtonDlg::OnTimer(UINT_PTR nIDEvent){
// TODO: 在此添加消息处理程序代码和/或调用默认值
static int count = 0;
if( nIDEvent == 99 ){
m_progress.SetPos(m_progress_pos);
} else if( nIDEvent == 10 ){
TRACE("%s(%d):%s %d\n", __FILE__, __LINE__, __FUNCTION__, GetTickCount());
if( count > 5 ) KillTimer(10);
count++;
}

CDialogEx::OnTimer(nIDEvent);
}

当我们的间隔设置在5,他的误差还在10左右,拉高之后

反正就是突出mfc对于定时精度处理不足的问题

那么接下来让他自己动~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void CMFCButtonDlg::OnTimer(UINT_PTR nIDEvent){
// TODO: 在此添加消息处理程序代码和/或调用默认值
static int count = 0;
if( nIDEvent == 99 ){
m_progress.SetPos(m_progress_pos);
} else if( nIDEvent == 10 ){
TRACE("%s(%d):%s %d\n", __FILE__, __LINE__, __FUNCTION__, GetTickCount());

int low, upper;
m_progress.GetRange(low, upper);
if( m_progress_pos >= upper ){
KillTimer(10);
} else{
m_progress_pos += 10;
}
}

CDialogEx::OnTimer(nIDEvent);
}

主要也就是获取这个进度的范围,没到头就慢慢网上递增,像复制文件的话,还得在里面计算文件复制到哪了,然后按比例递增效果更明显。

静态的图片看不出效果。

这是演示从空到满的情况,相反的,进度的初始值要改成上限,然后这里改成-=10
理论就是如此,实现另说

哈哈,关于这个进度条,千万不要用多个线程去玩。。不然效果很出奇玩自己了属于是


图片资源

也是拖出一个picture control。

有意思的是命名直接是static,和之前的静态文本框有点相似

拖个静态文本框可以看到有点相同

反正父类总有一个是一样的。

控制这些玩意就老样子添加变量/控件,添加完之后自然没啥效果。。都没把图片塞进去

点击dialog,从消息里面找到

file就是跟文件相关的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void CMFCButtonDlg::OnDropFiles(HDROP hDropInfo){
// TODO: 在此添加消息处理程序代码和/或调用默认值
int count = DragQueryFile(hDropInfo, -1, NULL, 0);

//count > 1 Msg..
TCHAR sPath[MAX_PATH];
char mbsPath[MAX_PATH * 2];
for( int i = 0; i < count; i++ ){
memset(sPath, 0, sizeof(sPath));
memset(mbsPath, 0, sizeof(mbsPath));
DragQueryFile(hDropInfo, i, sPath, MAX_PATH);
size_t total = 0;
wcstombs_s(&total, mbsPath, sizeof(mbsPath), sPath, MAX_PATH);
TRACE("%s(%d):%s %s\n", __FILE__, __LINE__, __FUNCTION__, mbsPath);

if( CString(sPath).Find(_T(".ico")) ){
HICON hicon = (HICON)LoadImage(AfxGetInstanceHandle(), sPath, IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE);
m_pictrue.SetIcon(hicon);
}
}

InvalidateRect(NULL);

CDialogEx::OnDropFiles(hDropInfo);
}

写完之后有一个地方需要注意,因为这个用的图片类型是.ico想直接用mfc那个图片了,所以要修改图片框的类型

将其修改完之后

看到样式发生了变化,有点小

右击打开项目的路径,找到res文件夹,里面就有个mfc的ico

刚开始运行的时候,是看不到图片框的

这里忘了一个事,就是设置对话框可接受文件

不设置为true的话,图片拖动是禁止的。

设置true之后拖动图片到对话框上,发现的确显示了。

并且,日志也输出了这个图片的路径


List Control

长的吧跟列表又有点相似。倒是多了图标

其中有几种可选,默认为icon样式

在list视图下就真的跟列表一样了。

report视图感觉会用的多一点

这里先用report,为这个控件添加变量,然后初始化一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
BOOL CMFCButtonDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();

// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标

// TODO: 在此添加额外的初始化代码
m_conmpany.AddString(_T("山东蓝翔"));
m_conmpany.AddString(_T("深圳电子厂"));
m_conmpany.AddString(_T("义乌商超"));

m_progress.SetRange(0, 1000);
m_progress_pos = 0;

//定时器尽量不要低于30ms,不同机子有少许差别,源自mfc的精度不足导致定时器缺陷
SetTimer(99, 500, NULL);
SetTimer(10, 100, NULL);

//初始化列
m_list.InsertColumn(0, _T("序号"));
m_list.InsertColumn(1, _T("IP"));
m_list.InsertColumn(2, _T("ID"));
m_list.InsertColumn(3, _T("CHECK"));

return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}

设置文字是没啥问题了,但是一开始他都是缩在一团还要拉开太麻烦了。

1
2
3
4
m_list.InsertColumn(0, _T("序号"), LVCFMT_LEFT, 50);
m_list.InsertColumn(1, _T("IP"), LVCFMT_LEFT, 200);
m_list.InsertColumn(2, _T("ID"), LVCFMT_LEFT, 180);
m_list.InsertColumn(3, _T("CHECK"), LVCFMT_LEFT, 200);

修改完之后,其实还要调整一下list control在对话框里面的大小

目前来说调整成这样差不多。再不济,空间有限的情况下,给他上滚动条

除此之外也可以用代码实现改变style,同样在初始化的地方

1
2
3
4
DWORD extStyle = m_list.GetExtendedStyle();
extStyle |= LVS_EX_FULLROWSELECT;
extStyle |= LVS_EX_GRIDLINES;
m_list.SetExtendedStyle(extStyle);

多了点格子,目前还没有数据

1
2
3
4
5
//列增加数据
m_list.InsertItem(0, CString("0"));
m_list.SetItemText(0, 1, _T("192.168.0.1"));
m_list.SetItemText(0, 2, _T("6648964896486480"));
m_list.SetItemText(0, 3, _T("999"));

虽然能设置,但是总归是麻烦了一点。

另外背景颜色。。额没这个本事,用参数调得不得行

1
m_list.SetBkColor(RGB(64, 255, 128));

哈哈哈瞎调的,辣眼睛还是注释了先。

列表比较实用的也可以像多选那样,在初始化的地方给list加个样式

1
extStyle |= LVS_EX_CHECKBOXES;

可以看到多了多选框

那么首先要拖个按钮测试选中之后拉取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void CMFCButtonDlg::OnBnClickedBtnList(){
// TODO: 在此添加控件通知处理程序代码
int lineCount = m_list.GetItemCount();
CHeaderCtrl* pHeader = m_list.GetHeaderCtrl();
int coloumnCount = pHeader->GetItemCount();

for (int i = 0; i < lineCount; i++) {
for (int j = 0; j < coloumnCount; j++) {
CString temp = m_list.GetItemText(i, j);
char Text[MAX_PATH];
memset(Text, 0, sizeof(Text));
size_t total;
wcstombs_s(&total, Text, sizeof(Text), temp, temp.GetLength());
TRACE("%s(%d): %s %s\n", __FILE__, __LINE__, __FUNCTION__, Text);
}
}
}

可以看到日志输出了我们所选的行的数据。

如何把多选框和数据关联
显然就是判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void CMFCButtonDlg::OnBnClickedBtnList(){
// TODO: 在此添加控件通知处理程序代码
int lineCount = m_list.GetItemCount();
CHeaderCtrl* pHeader = m_list.GetHeaderCtrl();
int coloumnCount = pHeader->GetItemCount();

for (int i = 0; i < lineCount; i++) {
if (m_list.GetCheck(i)) {
TRACE("%s(%d): %s %s\n", __FILE__, __LINE__, __FUNCTION__, "选中");
} else {
TRACE("%s(%d): %s %s\n", __FILE__, __LINE__, __FUNCTION__, "未选中");
}

for (int j = 0; j < coloumnCount; j++) {
CString temp = m_list.GetItemText(i, j);
char Text[MAX_PATH];
memset(Text, 0, sizeof(Text));
size_t total;
wcstombs_s(&total, Text, sizeof(Text), temp, temp.GetLength());
TRACE("%s(%d): %s %s\n", __FILE__, __LINE__, __FUNCTION__, Text);
}
}
}

虽然有点简陋。

关于list的style LVS_EX_还有很多不怎么用的,不过一般也是重写
还有些set的方法可以搜一搜看看。


Tree

拖个tree control出来,然后添加个变量
且预览效果跟这样差不多,那我们肯定要自己初始化他

1
2
3
4
5
6
//Tree
HTREEITEM hRoot = m_tree.InsertItem(_T("root"));
HTREEITEM hLeaf1 = m_tree.InsertItem(_T("leaf"), hRoot);
m_tree.InsertItem(_T("sub"), hLeaf1);
HTREEITEM hLeaf2 = m_tree.InsertItem(_T("leaf"), hRoot);
m_tree.InsertItem(_T("sub"), hLeaf2);

根 叶 子叶

这是全部展开的样子,默认只有root,双击之后一个个展开。光秃秃的很潦草。

图标自己画问题不大,右击项目打开所在路径,找到res文件夹,在里面添加个位图

这是画完的样子。
然后导入资源。

搞这种位图呢,主要是应对需要挺多logo之类简单的图片,文件太散找的麻烦,在一张上做分界标记会更好

1
2
//头文件声明
CImageList m_icons;
1
2
3
4
5
6
7
8
9
//源文件初始化
m_icons.Create(IDB_TREE, 32, 3, 0);
m_tree.SetImageList(&m_icons, TVSIL_NORMAL);
//Tree
HTREEITEM hRoot = m_tree.InsertItem(_T("root"), 0, 1);
HTREEITEM hLeaf1 = m_tree.InsertItem(_T("leaf"), 2, 1, hRoot);
m_tree.InsertItem(_T("sub"), 2, 1, hLeaf1);
HTREEITEM hLeaf2 = m_tree.InsertItem(_T("leaf"), 2, 1, hRoot);
m_tree.InsertItem(_T("sub"), 2, 1, hLeaf2);

哈哈图片画少了,他一个节点两个状态可以用两个图片的,选中和未选中两个样,但是我们用的2和1,所以效果就比较糙。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CMFCButtonDlg::OnNMDblclkTree(NMHDR* pNMHDR, LRESULT* pResult) {
// TODO: 在此添加控件通知处理程序代码
UINT nCount = m_tree.GetSelectedCount();
if (nCount > 0) {
HTREEITEM hSelect = m_tree.GetSelectedItem();
CString strText = m_tree.GetItemText(hSelect);
char sText[256] = "";
size_t total;
wcstombs_s(&total, sText, sizeof(sText), strText, strText.GetLength());
TRACE("%s(%d): %s %s\n", __FILE__, __LINE__, __FUNCTION__, sText);
}

*pResult = 0;
}

其实做法有很多,大多例子都是颗糖。


结语

MFC看下来跟去年Qt一个感觉吧,知道拖控件和消息之类的,但是实战太少,经验不足,有的时候很难主动把这些关联起来。

至于Qt后面也要重新捯饬捯饬。