本教程重操作,轻理论,为操作减负。需了解详细原理的朋友可以自行看各种书籍。
  直接上菜。

MFC:Microsoft Foundation Class ,微软基础类库。

对话框

对话框的创建和显示

  1. 新建MFC AppWizard(exe)工程,单文档类型。工程名:Mybole。编译运行。
    新建
    新建
  2. 点击帮助-关于Mybole。这是MFC自动创建的。

    关于
    关于
  3. 创建自己的对话框。点击Insert-Resource。选择Dialog,点击New。VC++自动将其标识设置为IDD_DIALOG1,并自动添加到ResourceView-Dialog项中。Dialog项下还有一个对话框资源标识:IDD_ABOUTBOX,即上一步中的“关于”对话框。

    Insert Resource对话框
    Insert Resource对话框

新建的对话框资源

  1. 选中对话框本身,右键点击属性。将Caption设置为“测试”。
  2. 选择View-ClassWizard,点击create a new class,OK。出现下图,并输入下图选项。
    New Class
    New Class
  3. 在随后出现的MFC ClassWizard对话框上点击OK。
    列表
    列表
    注意:看看左侧类列表中是否添加好了CTestDlg,否则会影响后续操作。

接下来,我们希望在程序中显示这个对话窗口。

  1. 点击右侧菜单Menu,选中IDR_MAINFRAME。点击帮助旁边的虚线框。

    Menu
    Menu
  2. 对虚线框右键属性,修改为下图。

    属性
    属性
  3. 关闭属性。点击View-ClassWizard(中文是建立类向导),选择CMyboleView,用COMMAND命令消息响应函数。如图。

    COMMAND
    COMMAND

模态对话框的创建

  需要调用CDialog类的成员函数:DoModal,它能创建并显示一个模态对话框,其返回值将作为CDialog类的另一个成员函数:EndDialog的参数,后者功能是关闭模态对话框。

  在FileView中选择MyboleView.cpp,编写程序。
  记得在开头添加头文件 #include “testdlg.h” (头文件大小写问题,linux区分,windows不区分)

编程
编程

  显示模态对话框的具体实现代码:
1
2
3
4
5
6
void CMyboleView::OnDialog() 
{
// TODO: Add your command handler code here
CTestDlg dlg;
dlg.DoModal();
}

编译运行,点击对话框。会发现若不确认该窗口,将无法点击其他窗口。

模态对话框1
模态对话框1

模态对话框2
模态对话框2

非模态对话框的创建

将上面的模态对话框代码注释掉。

改为:

1
2
3
4
5
6
7
8
9
10
void CMyboleView::OnDialog() 
{
// TODO: Add your command handler code here
//CTestDlg dlg;
//dlg.DoModal();

CTestDlg *pDlg = new CTestDlg;
pDlg->Create(IDD_DIALOG1,this);
pDlg->ShowWindow(SW_SHOW);
}

注意:需要把之前运行的对话框关掉才能编译成功。

然而,当它生命周期结束时,所保存的内存地址就丢失了,那么程序中也就无法再引用到它所指向的那块内存。于是,我们这样解决该问题。

MFC ClassWizard
MFC ClassWizard

注意:Message里双击添加函数或者点击add Class…

void CTestDlg::PostNcDestroy()
{
// TODO: Add your specialized code here and/or call the base class
delete this;
CDialog::PostNcDestroy();
}

区别:点击确定,对话框都会消失。但是,模态对话框窗口对象被销毁了。对非模态对话框来说,只是隐藏起来了,并未被销毁。
因此,若要销毁对话框,若有一个ID为IDOK的按钮,就必须重写基类的OnOK这个虚函数,并在重写的函数中调用DestroyWindow函数,完成销毁。并不要再调用基类的OnOK函数。
同样地,若有一个ID为IDCANCEL的按钮,也必须重写基类的OnCancel虚函数,并在重写的函数中调用DestroyWindow函数,完成销毁。并不要再调用基类的OnCancel函数。

动态创建按钮

注释掉非模态对话框代码,还原模态对话框代码。

点击ResourceView-IDD_DIALOG1,打开资源,用鼠标拖出控件面板上的Button按钮控件,对按钮右键,选择属性,设置如下。

按钮
按钮

接下来,我们实现当单击Add按钮时,在对话框中动态创建一个按钮这一功能。

  1. 为CTestDlg类添加一个私有的CButton成员变量。
      点击ClassView标签页右键,如图点击。
    ClassView
    ClassView

  填入信息。

添加成员变量
添加成员变量

  1. 添加Add按钮单击消息的响应函数。
      按钮点右键,选ClassWizard(建立类向导),如图。
    建立类向导
    建立类向导

  单击Edit Code,即可定位到该函数定义处。
  添加一下代码:

1
2
3
4
5
6
void CTestDlg::OnBtnAdd() 
{
// TODO: Add your control notification handler code here
m_btn.Create("New",BS_DEFPUSHBUTTON | WS_VISIBLE | WS_CHILD,
CRect(0,0,100,100),this,123);
}

  为避免多次点击Add出现非法操作,我们需要进行如下步骤。

  1. 为CTestDlg类增加一个私有的BOOL类型成员变量。
    变量类型:BOOL
    变量名称:m_bIsCreated
    Access: private

  2. 在TestDlg.cpp中找到构造函数,将m_bIsCreated初始为FALSE。如图所示。

    这里写图片描述
    这里写图片描述

  或者改为如下亦可。
Static BOOL bIsCreated = FALSE;

  1. 回到Add,双击它,进入代码部分,改之。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void CTestDlg::OnBtnAdd() 
    {
    // TODO: Add your control notification handler code here
    if(m_bIsCreated==FALSE)
    {
    m_btn.Create("New",BS_DEFPUSHBUTTON | WS_VISIBLE | WS_CHILD,
    CRect(0,0,100,100),this,123);
    m_bIsCreated = TRUE;
    }
    else
    {
    m_btn.DestroyWindow();
    m_bIsCreated = FALSE;
    }

    }

  或者以下亦能实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CTestDlg::OnBtnAdd() 
{
// TODO: Add your control notification handler code here
if(!m_btn.m_hWnd)
{
m_btn.Create("New",BS_DEFPUSHBUTTON | WS_VISIBLE | WS_CHILD,
CRect(0,0,100,100),this,123);
}
else
{
m_btn.DestroyWindow();
}

}

  效果:
  这里写图片描述

  点击Add出现New窗口,再点击就销毁。

控件的访问

控件的调整

用Layout-Align,Layout-Make Same Size,Layout-Space Evenly里的选项进行调整。

这里写图片描述
这里写图片描述

静态文本控件

  查看三个静态文本框,它们ID相同。我们可以更改第一个静态文本框ID为IDC_NUMBER1,再打开ClassWizard,可以在ObjectIDs看到新ID。

这里写图片描述
这里写图片描述

  对BN_CLICKED进行Add Function,并Edit Code:

  此时运行程序点击第一个静态文本框并没有反应。这是因为:静态文本控件在默认状态下是不发送通告消息的

  为了该控件能向父窗口发送鼠标事件,我们对该文本框右键-属性,切换到styles选项卡,勾上Notify。

这里写图片描述
这里写图片描述

  现在可以显示了:
  点击就改变。

这里写图片描述
这里写图片描述

  总结:为了使一个静态文本控件能够响应鼠标单击消息,那么需要进行两个特殊的步骤:第一步,改变它的ID;第二步,在它的属性对话框中选中Notify选项。

编辑框控件

利用上面的对话框实现这样的功能:在前两个编辑框中分别输入一个数字,然后单击Add按钮,对前两个编辑框中的数字求和,并将结果显示在第三个编辑框中。

第一种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CTestDlg::OnBtnAdd() 
{
int num1, num2, num3;
char ch1[10], ch2[10], ch3[10];
GetDlgItem(IDC_EDIT1)->GetWindowText(ch1,10);
GetDlgItem(IDC_EDIT2)->GetWindowText(ch2,10);

num1 = atoi(ch1);
num2 = atoi(ch2);
num3 = num1 + num2;

itoa(num3,ch3,10);
GetDlgItem(IDC_EDIT3)->SetWindowText(ch3);
}

C语言转换函数:atoi 将一个由数字组成的字符串转换为相应的数值
itoa 数值转换为文本
itoa函数的第三个参数表示转换的进制,数字10表示十进制。

效果:

这里写图片描述
这里写图片描述

第二种方式

  代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void CTestDlg::OnBtnAdd() 
{
int num1, num2, num3;
char ch1[10], ch2[10], ch3[10];
//GetDlgItem(IDC_EDIT1)->GetWindowText(ch1,10);
//GetDlgItem(IDC_EDIT2)->GetWindowText(ch2,10);

GetDlgItemText(IDC_EDIT1,ch1,10);
GetDlgItemText(IDC_EDIT2,ch2,10);

num1 = atoi(ch1);
num2 = atoi(ch2);
num3 = num1 + num2;

itoa(num3,ch3,10);
//GetDlgItem(IDC_EDIT3)->SetWindowText(ch3);
SetDlgItemText(IDC_EDIT3,ch3);
}

  GetDlgItemText 将返回对话框中指定ID的控件上的文本,相当于将上面的GetDlgItem和GetWindowText这两个函数功能组合起来了。
与之对应的是SetDlgItemText,用来设置对话框中指定ID的控件上的文本。

第三种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void CTestDlg::OnBtnAdd() 
{
int num1, num2, num3;
//char ch1[10], ch2[10], ch3[10];
//GetDlgItem(IDC_EDIT1)->GetWindowText(ch1,10);
//GetDlgItem(IDC_EDIT2)->GetWindowText(ch2,10);

//GetDlgItemText(IDC_EDIT1,ch1,10);
//GetDlgItemText(IDC_EDIT2,ch2,10);

num1 = GetDlgItemInt(IDC_EDIT1);
num2 = GetDlgItemInt(IDC_EDIT2);

//num1 = atoi(ch1);
//num2 = atoi(ch2);
num3 = num1 + num2;

//itoa(num3,ch3,10);
//GetDlgItem(IDC_EDIT3)->SetWindowText(ch3);
//SetDlgItemText(IDC_EDIT3,ch3);
SetDlgItemInt(IDC_EDIT3,num3);
}

第四种方式

  将这三个编辑框分别与对话框类的三个成员变量相关联,然后通过这些成员变量来检索和设置编辑框的文本,这是最简单的访问控件的方式。
打开ClassWizard对话框,切换到Member Variables选项卡,如图。

这里写图片描述
这里写图片描述

  首先为IDC_EDIT1编辑框添加一个关联的成员变量,方法是在Control IDs列表中选中IDC_EDIT1,再单击Add Variable按钮,如图。
这里写图片描述
这里写图片描述

这里写图片描述
这里写图片描述

  同样地,为IDC_EDIT2和IDC_EDIT3分别添加好成员变量。
  接着修改代码:

1
2
3
4
5
6
void CTestDlg::OnBtnAdd() 
{
UpdateData();
m_num3 = m_num1 + m_num2;
UpdateData(FALSE);
}

  对编辑框控件中输入的数值设定一个范围:
  打开ClassWizard-Member Variable,选中IDC_EDIT1,下方输入0和100。同样为IDC_EDIT2也设置好。

这里写图片描述
这里写图片描述

第五种方式

  将编辑框控件再与一个变量相关联,代表控件本身。为IDC_EDIT1增加一个控件类型的变量:m_edit1,类别为Control。同样地,也为IDC_EDIT2和IDC_EDIT3添加。

这里写图片描述
这里写图片描述

  修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void CTestDlg::OnBtnAdd() 
{
int num1, num2, num3;
char ch1[10], ch2[10], ch3[10];

m_edit1.GetWindowText(ch1,10);
m_edit2.GetWindowText(ch2,10);

//num1 = GetDlgItemInt(IDC_EDIT1);
//num2 = GetDlgItemInt(IDC_EDIT2);

num1 = atoi(ch1);
num2 = atoi(ch2);
num3 = num1 + num2;

itoa(num3,ch3,10);
m_edit3.SetWindowText(ch3);
}

第六种方式

  修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void CTestDlg::OnBtnAdd() 
{
int num1, num2, num3;
char ch1[10], ch2[10], ch3[10];

::SendMessage(GetDlgItem(IDC_EDIT1)->m_hWnd, WM_GETTEXT, 10, (LPARAM)ch1);
::SendMessage(m_edit2.m_hWnd, WM_GETTEXT, 10, (LPARAM)ch2);

num1 = atoi(ch1);
num2 = atoi(ch2);
num3 = num1 + num2;

itoa(num3,ch3,10);
m_edit3.SendMessage(WM_SETTEXT, 0, (LPARAM)ch3);
}

第七种方式

  修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void CTestDlg::OnBtnAdd() 
{
int num1, num2, num3;
char ch1[10], ch2[10], ch3[10];

SendDlgItemMessage(IDC_EDIT1, WM_GETTEXT, 10, (LPARAM)ch1);
SendDlgItemMessage(IDC_EDIT2, WM_GETTEXT, 10, (LPARAM)ch2);

num1 = atoi(ch1);
num2 = atoi(ch2);
num3 = num1 + num2;

itoa(num3,ch3,10);
SendDlgItemMessage(IDC_EDIT3, WM_SETTEXT, 0, (LPARAM)ch3);
}

  获得编辑框复选的内容:
  在上述代码最后添加:
SendDlgItemMessage(IDC_EDIT3, EM_SETSEL, 0, -1); //0,-1表示全选若1,3表示选中1-3位复选
m_edit3.SetFocus();

  效果:

这里写图片描述
这里写图片描述

总结

1 GetDlgItem()->Get(Set)WindowTest()
2 GetDlgItemText()/SetDlgItemText()
3 GetDlgItemInt()/SetDlgItemInt()
4 将控件和整型变量相关联
5 将控件和控件变量相关联
6 SendMessage()
7 SendDlgItemMessage()
  最常用是1、4、5。在利用MFC编程时,6、7用得少。

对话框伸缩功能的实现

  对话框上再添加一个按钮,Caption设置为“收缩<<”点击ClassWizard,添加一个命令相应函数(BN_CLICKED)。具体实现代码为:

1
2
3
4
5
6
7
8
9
10
11
12
void CTestDlg::OnButton1() 
{
CString str;
if(GetDlgItemText(IDC_BUTTON1,str), str == "收缩<<")
{
SetDlgItemText(IDC_BUTTON1, "拓展>>");
}
else
{
SetDlgItemText(IDC_BUTTON1, "收缩<<");
}
}

  拖动一个图像控件来划分对话框中要动态切除的部分。

这里写图片描述
这里写图片描述

  修改该控件ID为IDC_SEPATATOR,styles选项卡勾上Sunken选项。
  修改代码:

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
void CTestDlg::OnButton1() 
{
CString str;
if(GetDlgItemText(IDC_BUTTON1,str), str == "收缩<<")
{
SetDlgItemText(IDC_BUTTON1, "拓展>>");
}
else
{
SetDlgItemText(IDC_BUTTON1, "收缩<<");
}
static CRect rectLarge;
static CRect rectSmall;

CRect rect1(10,10,10,10);
CRect rect2(0,0,0,0);

if(rectLarge.IsRectNull())
{
CRect rectSeparator;
GetWindowRect(&rectLarge);
GetDlgItem(IDC_SEPARATOR)->GetWindowRect(&rectSeparator);

rectSmall.left=rectLarge.left;
rectSmall.top=rectLarge.top;
rectSmall.right=rectLarge.right;
rectSmall.bottom=rectSeparator.bottom;
}
if(str == "收缩<<")
{
SetWindowPos(NULL, 0, 0, rectSmall.Width(), rectSmall.Height(), SWP_NOMOVE | SWP_NOZORDER);
}
else
{
SetWindowPos(NULL, 0, 0, rectLarge.Width(), rectLarge.Height(), SWP_NOMOVE | SWP_NOZORDER);
}
}

  效果:

这里写图片描述
这里写图片描述

  点击“收缩<<”:

这里写图片描述
这里写图片描述

  若希望隐藏分隔条,则设置属性去掉“Visible”前的勾。

输入焦点的传递

  为了屏蔽掉默认的回车键关闭对话框这一功能,应该在对话框子类(此处是CTestDlg类)中重写OK按钮的消息响应函数。
  首先点击OK按钮,添加鼠标单击消息响应函数。注释掉原有函数。

法一

  在ClassView选项卡的CTestDlg类添加WM_INITDIALOG消息的响应函数。对类右键,选择Add Windows Message Handler,在弹出的框左侧选择WM_INITDIALOG,直接单击Add and Edit,跳转。
  修改代码为:

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
void CTestDlg::OnOK() 
{
// TODO: Add extra validation here

//CDialog::OnOK();
}


WNDPROC prevProc;
LRESULT CALLBACK NewEditProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
if(uMsg == WM_CHAR && wParam == 0x0d)
{
::SetFocus(GetNextWindow(hwnd,GW_HWNDNEXT));
return 1;
}
else
{
return prevProc(hwnd,uMsg,wParam,lParam);
}
}

BOOL CTestDlg::OnInitDialog()
{
CDialog::OnInitDialog();
prevProc=(WNDPROC)SetWindowLong(GetDlgItem(IDC_EDIT1)->m_hWnd,
GWL_WNDPROC, (LONG)NewEditProc);
return TRUE;
}

  查看第一个编辑框的属性,打开styles选项卡,勾上MultiLine(多行)。即可实现焦点的传递。

法二

  只需要改变一行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
WNDPROC prevProc;
LRESULT CALLBACK NewEditProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
if(uMsg == WM_CHAR && wParam == 0x0d)
{
//::SetFocus(GetNextWindow(hwnd,GW_HWNDNEXT));
SetFocus(::GetWindow(hwnd,GW_HWNDNEXT));
return 1;
}
else
{
return prevProc(hwnd,uMsg,wParam,lParam);
}
}

法三

  编辑框属性有一个WS_TABSTOP,如果勾选了,则在对话框中按下Tab键后,输入焦点可以转移到此控件上。

  修改一行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WNDPROC prevProc;
LRESULT CALLBACK NewEditProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
if(uMsg == WM_CHAR && wParam == 0x0d)
{
SetFocus(::GetNextDlgTabItem(::GetParent(hwnd),hwnd,FALSE));
//::SetFocus(GetNextWindow(hwnd,GW_HWNDNEXT));
//SetFocus(::GetWindow(hwnd,GW_HWNDNEXT));
return 1;
}
else
{
return prevProc(hwnd,uMsg,wParam,lParam);
}
}

  三种方法的缺点:只修改了第一个编辑框的窗口过程,因此从第二到第三个编辑框的焦点转移无法实现,除非继续修改第二个编辑窗口。

  再介绍一种方法解决这个问题。

法四

  在MFC中,默认情况下,当在对话框窗口中按下回车键时,会调用对话框的默认按钮的响应函数,我们可以在此默认按钮的响应函数中把焦点依次向下传递。

  首先取消第一个编辑框的MultiLine。
  接着修改OnOK函数为:

1
2
3
4
5
6
7
8
9
void CTestDlg::OnOK() 
{
// TODO: Add extra validation here
//GetDlgItem(IDC_EDIT1)->GetNextWindow()->SetFocus();
//GetFocus()->GetNextWindow()->SetFocus();
//GetFocus()->GetWindow(GW_HWNDNEXT)->SetFocus();
GetNextDlgTabItem(GetFocus())->SetFocus();
//CDialog::OnOK();
}

  注释掉的部分是各种失败的尝试,各有各的bug。现在程序是正常的。

**注意:然而该屏蔽回车键的方法并非是常规做法,应该在PreTranslateMessage中进行拦截。(return TRUE即拦截)**

  具体做法:
  现在Testdlg.h中添加:

1
2
3
4
5
6
7
8
9
class CTestDlg : public CDialog
{

protected:
virtual BOOL PreTranslateMessage(MSG* pMsg);

public:
virtual void OnOK();
……

  接着:

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
CTestDlg::PreTranslateMessage(MSG* pMsg)
{
//屏蔽ESC关闭窗体
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_ESCAPE)
{
return TRUE;
}
//屏蔽回车关闭窗体,但会导致回车在窗体上失效.
/*
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN && pMsg->wParam)
{
return TRUE;
}
*/
else
{
return CDialog::PreTranslateMessage(pMsg);
}
}

void CTestDlg::OnOK()
{
// TODO: Add extra validation here

//CDialog::OnOK();
}

  点击Layout-Tab order,这些序号就是各控件的Tab顺序。顺序可改变,依次点击希望的顺序控件即可。

  调用顺序:当用户按下回车键时,Windows将查看对话框中是否存在指定的默认按钮,如果有,就调用该默认按钮单击消息的响应函数。如果没有,就会调用虚拟的OnOK函数,即使对话框没有包含默认的OK按钮(这个默认OK按钮的ID是IDOK)。

文件和注册表操作

C语言对文件操作的支持

  新建单文档类型的MFC应用程序,工程名为File,并为主菜单添加一个子菜单,名称为“文件操作”,然后为其添加两个菜单项,并分别为它们添加相应的命令响应函数(通过COMMAND),让CFileView类接收这些菜单项的命令响应。

这里写图片描述
这里写图片描述

这里写图片描述
这里写图片描述

文件的打开和写入

  代码:

1
2
3
4
5
void CFileView::OnFileWrite() 
{
FILE *pFile = fopen("1.txt","w");
fwrite("http://www.sunxin.org", 1, strlen("http://www.sunxin.org"), pFile);
}

  编译后可看到文件夹中生成了1.txt,打开有一行网址。

文件的关闭

  增加一行代码:

1
2
3
4
5
6
void CFileView::OnFileWrite() 
{
FILE *pFile = fopen("1.txt","w");
fwrite("http://www.sunxin.org", 1, strlen("http://www.sunxin.org"), pFile);
fclose(pFile);
}

文件指针定位

  代码:

1
2
3
4
5
6
7
void CFileView::OnFileWrite() 
{
FILE *pFile = fopen("1.txt","w");
fwrite("http://www.sunxin.org", 1, strlen("http://www.sunxin.org"), pFile);
fwrite("欢迎访问", 1, strlen("欢迎访问"), pFile);
fclose(pFile);
}

  显示:http://www.sunxin.org欢迎访问

  将文件指针移动到文件的开始位置处:
  代码:

1
2
3
4
5
6
7
8
9
void CFileView::OnFileWrite() 
{
FILE *pFile = fopen("1.txt","w");
fwrite("http://www.sunxin.org", 1, strlen("http://www.sunxin.org"), pFile);
fseek(pFile, 0, SEEK_SET);
fwrite("ftp:", 1, strlen("ftp:"),pFile);
//fwrite("欢迎访问", 1, strlen("欢迎访问"), pFile);
fclose(pFile);
}

  显示:ftp:://www.sunxin.org

文件的读取

  在OnFileRead函数中写入代码:

1
2
3
4
5
6
7
8
9
void CFileView::OnFileRead() 
{
FILE *pFile = fopen("1.txt","r");
char ch[100];
fread(ch, 1, 100, pFile);
fclose(pFile);
MessageBox(ch);

}

  编译运行:
  
这里写图片描述

  原因:C语言以“\0”结束。

  解决方法:
  法一:
  修改代码:

1
2
3
4
5
6
7
8
void CFileView::OnFileWrite() 
{
FILE *pFile = fopen("1.txt","w");
char buf[22] = "http://www.sunxin.org";
buf[21] = '\0';
fwrite(buf, 1, 22, pFile);
fclose(pFile);
}

  先点击写入文件,再点击读取文件,就可以看到正确的内容。
  缺点:增加了文件大小。

法二:

1
2
3
4
5
6
7
8
9
10
void CFileView::OnFileRead() 
{
FILE *pFile = fopen("1.txt","r");
char ch[100];
memset(ch, 0, 100);
fread(ch, 1, 100, pFile);
fclose(pFile);
MessageBox(ch);

}

法三:
  读取文件时,不知道文件大小时的做法。

1
2
3
4
5
6
7
8
9
10
11
12
13
void CFileView::OnFileRead() 
{
FILE *pFile = fopen("1.txt","r");
char *pBuf;
fseek(pFile, 0, SEEK_END);
int len=ftell(pFile);
pBuf = new char[len+1];
rewind(pFile);
fread(pBuf, 1, len, pFile);
pBuf[len] = 0;
fclose(pFile);
MessageBox(pBuf);
}

二进制文件和文本文件

  代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CFileView::OnFileWrite() 
{
FILE *pFile = fopen("2.txt", "w");
char ch[3];
ch[0] = 'a';
ch[1] = 10;
ch[2] = 'b';
fwrite(ch, 1, 3, pFile);
fclose(pFile);
}

void CFileView::OnFileRead()
{
FILE *pFile = fopen("2.txt","r");
char ch[100];
fread(ch, 1, 3, pFile);
ch[3] = 0;
fclose(pFile);
MessageBox(ch);
}

  效果:
  
这里写图片描述

  文本方式:10实际上是换行符的ASCII码。

  以文本方式和二进制方式读取文件是有明显的区别的。

文本方式和二进制方式

  二进制方式:换行是由两个字符组成的,即ASCII码10(回车符)和13(换行符)。
  写入和读取文件时要保持一致。如果采用文本方式写入,应采用文本方式读取;如果采用二进制方式写入数据,在读取时也应采用二进制方式。

  面试题:给你一个整数,如:98341,将这个整数保存到文件中,要求在以记事本程序打开该文件时,显示的是:98341。
  法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CFileView::OnFileWrite() 
{
FILE *pFile = fopen("3.txt", "w");
char ch[5];
ch[0] = 9 + 48;
ch[1] = 8 + 48;
ch[2] = 3 + 48;
ch[3] = 4 + 48;
ch[4] = 1 + 48;

fwrite(ch, 1, 5, pFile);
fclose(pFile);

}

  或

1
2
3
4
5
6
7
8
9
10
11
void CFileView::OnFileWrite() 
{
FILE *pFile = fopen("3.txt", "w");
int i = 98341;
char ch[5];
itoa(i, ch, 10);

fwrite(ch, 1, 5, pFile);
fclose(pFile);

}

  面试题:给定一个字符串,其中既有数字字符,又有26个英文字母中的几个字符,让你判断一下哪些是数字字符。

  对这种问题,实际上就是判断各字符的ASCII码,对于数字字符来说,它们的ASCII码大于等于48,小于等于57。

C++对文件操作的支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void CFileView::OnFileWrite() 
{
ofstream ofs("4.txt");
ofs.write("http://www.sunxin.org",strlen("http://www.sunxin.org"));
ofs.close;

}

void CFileView::OnFileRead()
{
ifstream ifs("4.txt");
char ch[100];
memset(ch, 0, 100);
ifs.read(ch,100);
ifs.close();
MessageBox(ch);
}

Win32 API 对文件操作的支持

文件的创建、打开和写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void CFileView::OnFileWrite() 
{
//定义一个句柄变量
HANDLE hFile;
//创建文件
hFile = CreateFile("5.txt", GENERIC_WRITE, 0, NULL, CREATE_NEW,
FILE_ATTRIBUTE_NORMAL, NULL);
//接收实际写入的字节数
DWORD dwWrites;
//写入数据
WriteFile(hFile,"http://www.sunxin.org",strlen("http://www.sunxin.org"),
&dwWrites, NULL);
//关闭文件句柄
CloseHandle(hFile);
}

文件的读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void CFileView::OnFileRead() 
{
HANDLE hFile;
//打开文件
hFile = CreateFile("5.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
//接收实际收到的数据
char ch[100];
//接收实际读取到的字节数
DWORD dwReads;
//读取数据
ReadFile(hFile, ch, 100, &dwReads, NULL);
//设置字符串结束字符
ch[dwReads] = 0;
//关闭打开的文件对象的句柄
CloseHandle(hFile);
//显示读取到的数据
MessageBox(ch);
}

菜单

菜单命令响应函数

  新建一个单文档的MFC AppWizard(exe)工程,工程名为Menu。Build运行。

这里写图片描述
  左上角点击按钮,可以让属性框始终显示,不会因为点击对话框以外的地方就消失。
  去掉Pop-up弹出前的勾,将ID改为ID_TEST。给Test添加响应函数在CMainFrame中,在函数中加入 MessageBox(“MainFrame Clicked”);
  效果:

这里写图片描述
这里写图片描述

菜单命令的路由

程序类对菜单命令的响应顺序

  响应Test
  菜单项命令的顺序依次是:视类、文档类、框架类,最后才是应用程序类。

Windows消息的分类

  凡是从CWnd派生的类,它们既可以接收标准消息,也可以接收命令消息和通告消息。而对于那些从CCmdTarget派生的类,则只能接收命令消息和通告消息,不能接收标准消息。
本例中的文档类(CMenuDoc)和应用程序类(CWinApp),因为它们都派生于CCmdTarget类,所以它们可以接收菜单命令消息。但它们不是从CWnd类派生的,所以不能接收标准消息。

菜单命令的路由

  菜单命令消息路由的具体过程:当点击某个菜单项时,最先接收到这个菜单命令消息的是框架类。框架类将把接收到的这个消息交给它的子窗口,即视类,由视类首先进行处理。视类首先根据命令消息映射机制查找自身是否对此消息进行了响应,如果响应了,就调用相应响应函数对这个消息进行处理,消息路由过程结束;如果视类没有对此命令消息做出响应,就交由文档类,文档类同样查找自身是否对这个菜单命令进行了响应,如果响应了,就由文档类的命令消息响应函数进行处理,路由过程结束。如果文档类也未做出响应,就把这个命令消息交还给视类,后者又把该消息交还给框架类。框架类查看自己是否对这个命令消息进行了响应,如果它也没有做出响应,就把这个菜单命令消息交给应用程序类,由后者来进行处理。

基本菜单操作

标记菜单

  运行刚才创建的Menu程序,点击查看,前面都有一个对号,这种类型就是标记菜单。
在CMainFrame类的OnCreate的return语句之前添加这句代码 GetMenu()->GetSubMenu(0)->CheckMenuItem(0, MF_BYPOSITION | MF_CHECKED); 或者GetMenu()->GetSubMenu(0)->CheckMenuItem(ID_FILE_NEW, MF_BYCOMMAND | MF_CHECKED);
  Build并运行,可发现新建左边已添加一个复选标记。

默认菜单项

  在刚才的代码下,添加 GetMenu()->GetSubMenu(0)->SetDefaultItem(1, TRUE); 或者GetMenu()->GetSubMenu(0)->SetDefaultItem(ID_FILE_OPEN, FALSE); 编译运行,会发现“打开”变成了粗体。

  注意:“打印”的索引是5,不是4。计算菜单项索引时,一定要把分割栏菜单项计算在内。并且,一个子菜单只能有一个默认菜单项。

图形标记菜单

  Insert-Resource-Bitmap,创建一个位图资源。如图。

这里写图片描述
这里写图片描述

  为CMainFrame类添加一个CBitmap类型的成员变量:m_bitmap。

  接着添加代码:
CString str;
str.Format(“x=%d”,y=%d”, GetSystemMetrics(SM_CXMENUCHECK),GetSystemMetrics(SM_CYMENUCHECK));
MessageBox(str);
m_bitmap.LoadBitmap(IDB_BITMAP1);
GetMenu()->GetSubMenu(0)->SetMenuItemBitmaps(0, MF_BYPOSITION, &m_bitmap, &m_bitmap);

禁用菜单项

  通常把MF_GRAYED和MF_DISABLED这两个标志放在一起使用。不过这么做并不是必需的。
  删除之前的代码,写入 GetMenu()->GetSubMenu(0)->EnableMenuItem(1, MF_BYPOSITION | MF_DISABLED | MF_GRAYED);
  打开“文件”子菜单,发现“打开”菜单栏变灰,点击不起作用。

移除和装载菜单

  再添加一行代码: SetMenu(NULL); 此时菜单栏被移除了。
  再添加几行代码:
CMenu menu;
menu.LoadMenu(IDR_MAINFRAME);
SetMenu(&menu);
menu.Detach();
  此时菜单栏又装载了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CMenu menu;
menu.CreateMenu();
GetMenu()->AppendMenu(MF_POPUP, (UINT)menu.m_hMenu, "Test1");
menu.AppendMenu(MF_STRING, 111, "Hello");
menu.AppendMenu(MF_STRING, 112, "Bye");
menu.AppendMenu(MF_STRING, 113, "Mybole");


menu.Detach();

CMenu menu1;
menu1.CreateMenu();
GetMenu()->InsertMenu(2, MF_POPUP | MF_BYPOSITION, (UINT)menu1. m_hMenu,"Test");


menu1.Detach();

GetMenu()->GetSubMenu(2)->AppendMenu(MF_STRING, 118, "Welcome");
GetMenu()->GetSubMenu(0)->AppendMenu(MF_STRING, 114, "Welcome");
GetMenu()->GetSubMenu(0)->InsertMenu(ID_FILE_OPEN, MF_BYCOMMAND | MF_STRING, 115, "VC编程");

MFC菜单命令更新机制

这里写图片描述

  MFC命令更新机制:当要显示菜单时,操作系统发出WM_INITMENUPOPOP消息,然后由程序窗口的基类如CFrameWnd接管,它会创建一个CCmdUI对象,并与程序的第一个菜单项相关联,调用该对象的一个成员函数DoUpdate()。这个函数发出CN_UPDATE_COMMAND_UI消息,这条消息带有一个指向CCmdUI对象的指针。这时,系统会判断是否存在一个ON_UPDATE_COMMAND_UI宏去捕获这个菜单项消息。如果找到这样一个宏,就调用相应的消息响应函数进行处理,在这个函数中,可以利用传递过来的CCmdUI对象去调用相应的函数,使该菜单项可以使用,或禁用该菜单项。当更新完第一个菜单项后,同一个CCmdUI对象就设置为与第二个菜单项相关联,依此顺序进行,直到完成所有菜单项的处理。

  添加代码:

1
2
3
4
5
void CMainFrame::OnUpdateEditCut(CCmdUI* pCmdUI) 
{
// TODO: Add your command update UI handler code here
pCmdUI->Enable();
}

  编辑-剪切 可用了。
  如果要把工具栏上的一个工具按钮与菜单栏中的某个菜单项相关联,只要将它们的ID设置为同一个标识就可以了。

  如果希望禁用文件-新建,为ID_FILE_NEW添加UPDATE_COMMAND_UI消息响应函数。
  代码如下:

1
2
3
4
5
6

void CMainFrame::OnUpdateFileNew(CCmdUI* pCmdUI)
{
// TODO: Add your command update UI handler code here
pCmdUI->Enable(FALSE);
}

  或者

1
2
3
4
5
void CMainFrame::OnUpdateFileNew(CCmdUI* pCmdUI) 
{
if (2 == pCmdUI->m_nIndex)
pCmdUI->Enable();
}

快捷菜单

  1. 新增一个新的菜单资源。点开,顶级菜单设置任意的文本,如abc。添加两个菜单项:
  显示 IDM_SHOW
  退出 IDM_EXIT
  2. 给CMenuView类添加WM_RBUTTONDOWN消息响应函数。

1
2
3
4
5
6
7
8
9
10
void CMenu2View::OnRButtonDown(UINT nFlags, CPoint point) 
{
CMenu menu;
menu.LoadMenu(IDR_MENU1);
CMenu* pPopup = menu.GetSubMenu(0);
ClientToScreen(&point);
pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);

CView::OnRButtonDown(nFlags, point);
}

  效果:
  

  3.对“显示”右键ClassWizard,可以取消创建新类的询问。分别为CMainFrame类和CMenuView类添加一个响应。
  代码:

1
2
3
4
void CMenu2View::OnShow() 
{
MessageBox("View show");
}

1
2
3
4
void CMainFrame::OnShow() 
{
MessageBox("Main show");
}

  结果是显示“View show”。说明只有视类才能对快捷菜单项命令做出响应。若想让CMainView类对此快捷菜单项进行响应的话,修改代码:

1
2
3
4
5
6
7
8
9
10
11
void CMenu2View::OnRButtonDown(UINT nFlags, CPoint point) 
{
CMenu menu;
menu.LoadMenu(IDR_MENU1);
CMenu* pPopup = menu.GetSubMenu(0);
ClientToScreen(&point);
//pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, GetParent());

CView::OnRButtonDown(nFlags, point);
}

  同时删去视类的显示。

动态菜单操作

添加菜单项目

  在CMainFrame类的OnCreate函数中添加代码:

1
2
3
4
CMenu menu;
menu.CreateMenu();
GetMenu()->AppendMenu(MF_POPUP, (UINT)menu.m_hMenu, "Test");
menu.Detach();

插入菜单项目

1
2
3
4
5
6
CMenu menu;
menu.CreateMenu();
/*GetMenu()->AppendMenu(MF_POPUP, (UINT)menu.m_hMenu, "Test");
menu.Detach();*/
GetMenu()->InsertMenu(2, MF_POPUP | MF_BYPOSITION, (UINT)menu. m_hMenu,"Test");
menu.Detach();

  如果要在新插入的子菜单中添加菜单项的话,同样可以使用AppendMenu函数来实现。

1
2
3
4
5
6
7
8
9
10
CMenu menu;
menu.CreateMenu();
/*GetMenu()->AppendMenu(MF_POPUP, (UINT)menu.m_hMenu, "Test");
menu.Detach();*/
GetMenu()->InsertMenu(2, MF_POPUP | MF_BYPOSITION, (UINT)menu. m_hMenu,"Test");

menu.AppendMenu(MF_STRING, 111, "Hello");
menu.AppendMenu(MF_STRING, 112, "Bye");
menu.AppendMenu(MF_STRING, 113, "Mybole");
menu.Detach();

  111、112、113是随便赋予的ID号。
  
这里写图片描述

  若要在“文件”子菜单下添加一个菜单项Welcome,再添加一行代码: GetMenu()->GetSubMenu(0)->AppendMenu(MF_STRING, 114, “Welcome”);
  若要在“文件”中的“新建”和“打开”插入一个菜单项VC编程,再添加一行代码:
GetMenu()->GetSubMenu(0)->InsertMenu(ID_FILE_OPEN, MF_BYCOMMAND | MF_STRING, 115, “VC编程”);

删除菜单

  删除“编辑”:在CMainFrame类的OnCreate函数最后(return之前)添加:
  GetMenu()->DeleteMenu(1, MF_BYPOSITION);
  删除“文件”下的“打开”:
  GetMenu()->GetSubMenu(0)->DeleteMenu(2, MF_BYPOSITION);

动态添加的菜单项的命令响应

  Resource.h中添加新ID

1
#define IDM_HELLO	111

将menu.AppendMenu(MF_STRING, 111, “Hello”); 改为 menu.AppendMenu(MF_STRING, IDM_HELLO, “Hello”);

  三部曲:
  1. 点开MainFrm.h,增加为

1
2
3
4
5
6
//{{AFX_MSG(CMainFrame)
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnShow();
//}}AFX_MSG
afx_msg void OnHello();
DECLARE_MESSAGE_MAP()

  2. 点开MainFrm.cpp,增加为

1
2
3
4
5
6
7
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
//{{AFX_MSG_MAP(CMainFrame)
ON_WM_CREATE()
ON_COMMAND(IDM_SHOW, OnShow)
ON_COMMAND(IDM_HELLO, OnHello)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()

  3. CMainFrame类中添加

1
2
3
4
void CMainFrame::OnHello()
{
MessageBox("Hello");
}

电话本示例程序

  删除之前写入CMainFrame类的OnCreate函数,留下原始函数。

动态添加子菜单的实现

  利用ClassWizard添加WM_CHAR消息。在Menu2View.h中添加:

1
2
3
private:
int m_nIndex;
CMenu m_menu;

在Menu2View.cpp里,添加:

1
2
3
4
5
CMenu2View::CMenu2View()
{
// TODO: add construction code here
m_nIndex = -1;
}

显示输入的字符

添加菜单项及其命令响应函数

  在资源编辑器中打开程序的菜单,在“帮助”后添加一个新菜单abc,添加4个菜单项。名称为1,ID为IDM_PHONE1,以此类推。用ClassWizard为CMenu2View类分别加上这四个菜单项的命令响应函数。
  修改CMenu2View类的头文件,如下:

1
2
3
4
5
6
7
8
9
10
protected:
//{{AFX_MSG(CMenu2View)
afx_msg void OnRButtonDown(UINT nFlags, CPoint point);
afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
afx_msg void OnPhone1();
afx_msg void OnPhone2();
afx_msg void OnPhone3();
afx_msg void OnPhone4();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()

CMenu2View.cpp中,

1
2
3
4
5
6
7
8
9
10
11
12
	//{{AFX_MSG_MAP(CMenu2View)
ON_WM_CHAR()
//}}AFX_MSG_MAP
ON_COMMAND(IDM_PHONE1, OnPhone1)
ON_COMMAND(IDM_PHONE2, OnPhone2)
ON_COMMAND(IDM_PHONE3, OnPhone3)
ON_COMMAND(IDM_PHONE4, OnPhone4)
// Standard printing commands
ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview)
END_MESSAGE_MAP()

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 CMenu2View::OnPhone1() 
{
CClientDC dc(this);
dc.TextOut(0, 0, m_strArray.GetAt(0));

}

void CMenu2View::OnPhone2()
{
CClientDC dc(this);
dc.TextOut(0, 0, m_strArray.GetAt(1));

}

void CMenu2View::OnPhone3()
{
CClientDC dc(this);
dc.TextOut(0, 0, m_strArray.GetAt(2));

}

void CMenu2View::OnPhone4()
{
CClientDC dc(this);
dc.TextOut(0, 0, m_strArray.GetAt(3));

}

框架类窗口截获菜单命令消息

  右键单击CMainFrame,选择Add Virtual Functions-OnCommand,单击Add Handler,再点击Edit Existing。

这里写图片描述
这里写图片描述

  代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL CMainFrame::OnCommand(WPARAM wParam, LPARAM lParam) 
{
int MenuCmdID = LOWORD(wParam);
CMenu2View *pView = (CMenu2View *)GetActiveView();
if (MenuCmdID >= IDM_PHONE1 && MenuCmdID < IDM_PHONE1 + pView->m_strArray.GetSize())
{
//MessageBox("Test");
CClientDC dc(pView);
dc.TextOut(0, 0, pView->m_strArray.GetAt(MenuCmdID - IDM_PHONE1));
return TRUE;
}
return CFrameWnd::OnCommand(wParam, lParam);
}

  将MainFrm.cpp里添加#include “Menu2View.h” 。
  将Menu2View.cpp中的#include “Menu2Doc.h”剪切到Menu2View.h文件的前部(#endif // _MSC_VER > 1000下面)。

  最终代码:

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 CMenu2View::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CClientDC dc(this);
if (0x0d == nChar)
{
if (0 == ++m_nIndex)
{
m_menu.CreatePopupMenu();
GetParent()->GetMenu()->AppendMenu(MF_POPUP, (UINT)m_menu.m_hMenu, "PhoneBook");
GetParent()->DrawMenuBar();
}
m_menu.AppendMenu(MF_STRING, IDM_PHONE1 + m_nIndex, m_strLine.Left(m_strLine.Find(' ')));
m_strArray.Add(m_strLine);
m_strLine.Empty();
Invalidate();
}
else
{
m_strLine += nChar;
dc.TextOut(0, 0, m_strLine);
}
CView::OnChar(nChar, nRepCnt, nFlags);
}

  效果:

这里写图片描述
这里写图片描述

简单绘图

MFC消息映射机制

  与消息有关的三处信息:1.头文件XXXX.h中 2.源文件XXXX.cpp中 3.源文件XXXX.cpp的响应函数中

绘制线条

  对CDrawView右键点击Add Member Variable,变量名称:m_ptOrigin,类型:CPoint,访问权限设置:Private。
  代码:

1
2
3
4
5
void CDrawView::OnLButtonDown(UINT nFlags, CPoint point) 
{
m_ptOrigin = point;
CView::OnLButtonDown(nFlags, point);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) 
{
//首先获得窗口的设备描述表
HDC hdc;
hdc = ::GetDC(m_hWnd);
//移动到线条的起点
MoveToEx(hdc, m_ptOrigin.x, m_ptOrigin.y, NULL);
//画线
LineTo(hdc, point.x, point.y);
//释放设备描述表
::ReleaseDC(m_hWnd, hdc);

CView::OnLButtonUp(nFlags, point);
}
这里写图片描述
这里写图片描述

利用MFC的CDC类实现画线功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) 
{
/*//首先获得窗口的设备描述表
HDC hdc;
hdc = ::GetDC(m_hWnd);
//移动到线条的起点
MoveToEx(hdc, m_ptOrigin.x, m_ptOrigin.y, NULL);
//画线
LineTo(hdc, point.x, point.y);
//释放设备描述表
::ReleaseDC(m_hWnd, hdc);*/

CDC* pDC = GetDC();
pDC->MoveTo(m_ptOrigin);
pDC->LineTo(point);
ReleaseDC(pDC);

CView::OnLButtonUp(nFlags, point);
}

利用MFC的CWindowDC类实现画线功能

1
2
3
4
5
6
7
8
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) 
{
CWindowDC dc(GetParent());
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);

CView::OnLButtonUp(nFlags, point);
}

这里写图片描述

在桌面窗口中画线

1
2
3
4
5
6
7
8
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) 
{
CWindowDC dc(GetDesktopWindow());
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);

CView::OnLButtonUp(nFlags, point);
}

这里写图片描述

  注意:在桌面上画图需要权限(一般写代码时需要避免软件以外的操作)。

绘制彩色线条

  在程序中,当构造一个GDI对象后,该对象并不会立即生效,必须选入设备描述表,它才会在以后的绘制操作中生效。
一般情况下,在完成绘图操作之后,都要利用SelectObject函数把之前的GDI对象选入设备描述表,以便使其恢复到先前的状态。

1
2
3
4
5
6
7
8
9
10
11
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) 
{
CPen pen(PS_SOLID, 1, RGB(255, 0, 0));
CClientDC dc(this);
CPen* pOldPen = dc.SelectObject(&pen);
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
dc.SelectObject(pOldPen);

CView::OnLButtonUp(nFlags, point);
}

  运行的效果是红色线条。

  改为 CPen pen(PS_DASH, 1, RGB(255, 0, 0)); 是虚线。(其中第二个参数需小于等于10)
CPen pen(PS_DOT, 1, RGB(255, 0, 0)); 是点线。

使用画刷绘图

简单画刷

1
2
3
4
5
6
7
8
9
10
11
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) 
{
//创建一个红色画刷
CBrush brush(RGB(255, 0, 0));
//创建并获得设备描述表
CClientDC dc(this);
//利用红色画刷填充鼠标拖拽过程中形成的矩形区域
dc.FillRect(CRect(m_ptOrigin, point),&brush);

CView::OnLButtonUp(nFlags, point);
}

这里写图片描述

位图画刷

  Insert-Resource-Bitmap-New,在这里发挥灵魂画手的天赋吧!
  代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) 
{
//创建位图对象
CBitmap bitmap;
//加载位图资源
bitmap.LoadBitmap(IDB_BITMAP1);
//创建位图画刷
CBrush brush(&bitmap);
//创建并获得设备描述表
CClientDC dc(this);
//利用位图画刷填充鼠标拖拽过程中形成的矩形区域
dc.FillRect(CRect(m_ptOrigin, point),&brush);

CView::OnLButtonUp(nFlags, point);
}

这里写图片描述
  我画的是不是很滑稽(手动滑稽)

透明画刷

  先进行一种尝试:

1
2
3
4
5
6
7
8
9
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) 
{
//创建并获得设备描述表
CClientDC dc(this);
//绘制一个矩形
dc.Rectangle(CRect(m_ptOrigin,point));

CView::OnLButtonUp(nFlags, point);
}

这里写图片描述
  如果希望矩形内部是透明的,能够看到被遮挡的图形,就要创建一个透明画刷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void CDrawView::OnLButtonUp(UINT nFlags, CPoint point) 
{
//创建并获得设备描述表
CClientDC dc(this);
//创建一个空画刷
CBrush *pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
//将空画刷选入设备描述表
CBrush *pOldBrush = dc.SelectObject(pBrush);
//绘制一个矩形
dc.Rectangle(CRect(m_ptOrigin, point));
//恢复先前的画刷
dc.SelectObject(pOldBrush);

CView::OnLButtonUp(nFlags, point);
}

这里写图片描述
这里写图片描述

绘制连续线条

  首先为视类增加鼠标移动消息(WM_MOUSEMOVE)的响应函数(默认OnMouseMove),并为视类添加一个BOOL型的私有成员变量m_bDraw。
在视类头文件定义:

1
2
 Private:
 BOOL m_bDraw;

在视类的构造函数中:

1
m_bDraw = FALSE;

在OnLButtonDown中:

1
m_bDraw = TRUE;

在OnLButtonUp中:

1
m_bDraw = FALSE;

1
2
3
4
5
6
7
8
9
10
11
12
13
void CDrawView::OnMouseMove(UINT nFlags, CPoint point) 
{
CClientDC dc(this);
if(m_bDraw == TRUE)
{
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
//修改线段的起点
m_ptOrigin = point;
}

CView::OnMouseMove(nFlags, point);
}
这里写图片描述
这里写图片描述

给线条换色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void CDrawView::OnMouseMove(UINT nFlags, CPoint point) 
{
CClientDC dc(this);
//创建一个红色的、宽度为1的实线画笔
CPen pen(PS_SOLID, 1, RGB(255, 0, 0));
//把创建的画笔选入设备描述表
CPen *pOldPen = dc.SelectObject(&pen);
if (m_bDraw == TRUE)
{
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
//修改线段的起点
m_ptOrigin = point;
}
//恢复设备描述表
dc.SelectObject(pOldPen);

CView::OnMouseMove(nFlags, point);
}

绘制扇形效果的线条

  去掉上述代码中的 m_ptOrigin = point;

  效果:
  这里写图片描述

  绘制一个带边线的扇形:
  为CDrawView类增加一个CPoint类型的私有成员变量m_ptOld,用来保存鼠标上一个移动点。

  在OnLButton中:

1
m_ptOld = point;

  在OnMouseMove中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void CDrawView::OnMouseMove(UINT nFlags, CPoint point) 
{
CClientDC dc(this);
//创建一个红色的、宽度为1的实线画笔
CPen pen(PS_SOLID, 1, RGB(255, 0, 0));
//把创建的画笔选入设备描述表
CPen *pOldPen = dc.SelectObject(&pen);
if (m_bDraw == TRUE)
{
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
dc.LineTo(m_ptOld);
//修改线段的起点
//m_ptOrigin = point;
m_ptOld = point;
}
//恢复设备描述表
dc.SelectObject(pOldPen);

CView::OnMouseMove(nFlags, point);
}

  最好将OnLButtonUp里原来写的代码删除或注释之。
  效果:

这里写图片描述
这里写图片描述

  MFC提供一个设置绘图模式的函数SetROP2,带有一个参数R2_BLACK、R2_WHITE、R2_MERGENOTPEN等。
  例如,在CClientDC dc(this); 下方添加代码: dc.SetROP2(R2_MERGENOTPEN); 编译运行后看不到绘制的线条,这就是设置了R2_MERGENOTPEN这种绘图模式。
使用R2_BLACK,将会发现绘制的线条颜色始终都是黑色的。

文本编程

插入符

创建文本插入符

  创建一个单文档类型的MFC AppWizard(exe)工程,取名为Text。
为CTextView类添加WM_CREATE消息的响应函数OnCreate,在此函数中创建一个宽度为20、高度为100的插入符。代码如下。

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

CreateSolidCaret(20,100);
ShowCaret();
return 0;
}

这里写图片描述
这里写图片描述

  让插入符适应于当前字体的大小:

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

//创建设备描述表
CClientDC dc(this);
//定义文本信息结构体变量
TEXTMETRIC tm;
//获得设备描述表中的文本信息
dc.GetTextMetrics(&tm);
//根据字体大小,创建何时的插入符(除以8是经验值)
CreateSolidCaret(tm.tmAveCharWidth/8, tm.tmHeight);
//显示插入符
ShowCaret();

return 0;
}

  运行结果就比较符合常规了。

创建图形插入符

  新建一个位图资源,画一个图形。
在TextView.h中添加

1
2
private:
CBitmap bitmap;

代码:

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


bitmap.LoadBitmap(IDB_BITMAP1);
CreateCaret(&bitmap);
ShowCaret();

return 0;
}

这里写图片描述
这里写图片描述

窗口重绘

OnDraw函数

  实现在程序窗口中输出一串文字的功能。

1
2
3
4
5
6
7
8
void CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);

CString str("VC++ 深入编程");
pDC->TextOut(50, 50, str);
}

这里写图片描述

添加字符串资源

  点击Resource View-String Table选项,在此字符串表最底部的空行上双击,即可弹出添加新字符串资源的对话框。ID:IDS_STRINGVC,Caption:“VC++编程 文本编程”。代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
void CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);

//CString str("VC++ 深入编程");
CString str;
str = "VC++ 深入编程";
pDC->TextOut(50, 50, str);

str.LoadString(IDS_STRINGVC);
pDC->TextOut(0, 200, str);
}

这里写图片描述
这里写图片描述

路径

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 CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);

//CString str("VC++ 深入编程");
CString str;
str = "VC++ 深入编程";
pDC->TextOut(50, 50, str);

CSize sz = pDC->GetTextExtent(str);

str.LoadString(IDS_STRINGVC);
pDC->TextOut(0, 200, str);

pDC->BeginPath();
pDC->Rectangle(50, 50, 50+sz.cx, 50+sz.cy);
pDC->EndPath();

for(int i=0; i<300; i+=10)
{
pDC->MoveTo(0, i);
pDC->LineTo(300, i);
pDC->MoveTo(i,0);
pDC->LineTo(i,300);
}

这里写图片描述

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
void CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);

//CString str("VC++ 深入编程");
CString str;
str = "VC++ 深入编程";
pDC->TextOut(50, 50, str);

CSize sz = pDC->GetTextExtent(str);

str.LoadString(IDS_STRINGVC);
pDC->TextOut(0, 200, str);

pDC->BeginPath();
pDC->Rectangle(50, 50, 50+sz.cx, 50+sz.cy);
pDC->EndPath();
pDC->SelectClipPath(RGN_DIFF);

for(int i=0; i<300; i+=10)
{
pDC->MoveTo(0, i);
pDC->LineTo(300, i);
pDC->MoveTo(i,0);
pDC->LineTo(i,300);
}

}

这里写图片描述
  这正是RGN_DIFF模式的效果。
  如果是RGN_AND,效果是新的裁剪区域是当前裁剪区域和当前路径层的交集。

  路径层的作用:实现特殊效果。如,希望整幅图形中某一部分与其他部分有所区别,就可以把这部分的图形设置到一个路径层中,然后利用SelectClipPath函数设置一种模式,让路径层和裁剪区域进行互操作以达到一种特殊效果。

字符输入

  当用户在键盘上按下某个字符按键后,要把该字符输出到程序窗口上。
首先让CTextView捕获WM_CHAR消息,接着为该类定义一个CString类型的成员变量:m_strLine,并在CTextView类的构造函数中将这个变量初始化:m_strLine = “”;

1
2
3
4
5
6
void CTextView::OnLButtonDown(UINT nFlags, CPoint point) 
{
SetCaretPos(point);

CView::OnLButtonDown(nFlags, point);
}

这里写图片描述
  为CTextView类再增加一个CPoint类型的成员变量,取名m_ptOrigin,权限为私有。在CTextView类的构造函数中设置其初值为0。

1
2
3
4
5
6
7
8
void CTextView::OnLButtonDown(UINT nFlags, CPoint point) 
{
SetCaretPos(point);
m_strLine.Empty();
m_ptOrigin = point;

CView::OnLButtonDown(nFlags, point);
}

  注意:回车字符的ASCII码十六进制是0x0d,退格键的ASCII码十六进制值是0x08。

  最终代码:

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
void CTextView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
CClientDC dc(this);
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
if (0x0d == nChar)
{
m_strLine.Empty();
m_ptOrigin.y += tm.tmHeight;
}
else if(0x08 == nChar)
{
COLORREF clr = dc.SetTextColor(dc.GetBkColor());
dc.TextOut(m_ptOrigin.x, m_ptOrigin.y, m_strLine);
m_strLine = m_strLine.Left(m_strLine.GetLength() - 1);
dc.SetTextColor(clr);
}
else
{
m_strLine += nChar;
}
CSize sz = dc.GetTextExtent(m_strLine);
CPoint pt;
pt.x = m_ptOrigin.x + sz.cx;
pt.y = m_ptOrigin.y;
SetCaretPos(pt);

dc.TextOut(m_ptOrigin.x, m_ptOrigin.y, m_strLine);


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

这里写图片描述

设置字体

1
2
3
4
5
6
7
8
9
10
11
void CTextView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
CClientDC dc(this);
CFont font;
font.CreatePointFont(300, "华文行楷", NULL);
CFont *pOldFont = dc.SelectObject(&font);
……
dc.SelectObject(pOldFont);

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

这里写图片描述

字幕变色功能的实现

  在这个Text例子中,我们在视类的OnCreate 函数中设置定时器,设置一个时间间隔为100ms,标识为1的定时器。

1
2
3
4
5
6
7
8
int CTextView::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
……

SetTimer(1, 100, NULL);

return 0;
}

  给CTextView类添加WM_TIMER消息的响应函数。

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
void CTextView::OnTimer(UINT nIDEvent) 
{
m_nWidth += 5;

CClientDC dc(this);
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
CRect rect;
rect.left =0;
rect.top = 200;
rect.right = m_nWidth;
rect.bottom = rect.top + tm.tmHeight;

dc.SetTextColor(RGB(255, 0, 0));
CString str;
str.LoadString(IDS_STRINGVC);
dc.DrawText(str, rect, DT_LEFT);

rect.top = 150;
rect.bottom = rect.top + tm.tmHeight;
dc.DrawText(str, rect, DT_RIGHT);

CSize sz = dc.GetTextExtent(str);
if (m_nWidth > sz.cx)
{
m_nWidth = 0;
dc.SetTextColor(RGB(0, 255, 0));
dc.TextOut(0, 200, str);
}

CView::OnTimer(nIDEvent);
}

  红色渐变效果可看到。

这里写图片描述
这里写图片描述

绘图控制

简单绘图

  新建一个单文档类型的MFC AppWizard(exe)工程,取名:Graphic。
  添加的菜单项:

这里写图片描述
这里写图片描述

  给CGraphicView类中添加一个私有变量:
1
UINT m_nDrawType;

  在视类构造函数中将此变量初始化为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CGraphicView::OnDot() 
{
m_nDrawType = 1;

}

void CGraphicView::OnLine()
{
m_nDrawType = 2;
}

void CGraphicView::OnRectangle()
{
m_nDrawType = 3;
}

void CGraphicView::OnEllipse()
{
m_nDrawType = 4;
}

  CGraphicView类再增加一个CPoint类型的私有成员变量:m_ptOrigin。在CGraphicView类构造函数中,将该变量的值设置为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
26
27
28
29
30
31
32
33
34
35
void CGraphicView::OnLButtonDown(UINT nFlags, CPoint point) 
{
m_ptOrigin = point;

CView::OnLButtonDown(nFlags, point);
}

void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point)
{
CClientDC dc(this);
//为边框设定颜色
CPen pen(PS_SOLID, 1, RGB(255, 0, 0));
dc.SelectObject(&pen);
//能看到图形内部内容(透明)
CBrush *pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
dc.SelectObject(pBrush);

switch(m_nDrawType)
{
case 1:
dc.SetPixel(point,RGB(255, 0, 0));
break;
case 2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 3:
dc.Rectangle(CRect(m_ptOrigin,point));
break;
case 4:
dc.Ellipse(CRect(m_ptOrigin, point));
break;
}
CView::OnLButtonUp(nFlags, point);
}

这里写图片描述

设置对话框

  再增加一个对话框资源,ID为IDD_DLG_SETTING,Caption为Setting,Font为宋体。

设置线宽

  添加一个静态文本框,并将Caption设为“线宽”。再添加一个编辑框,ID:IDC_LINE_WIDTH。为此对话框资源创建一个响应的对话框类,类名为CSettingDlg。对编辑框右键,ClassWizard,为它添加一个成员变量:m_nLineWidth,类型为UINT。为绘图菜单下再增加一个菜单项为“设置”,ID为IDM_SETTING。为此菜单项添加一个命令响应,选择视类做出响应。为CGraphicView类添加一个私有成员变量:m_nLineWidth,类型:UINT,并在CGraphicView类的构造函数初始化为0。

1
2
3
4
5
6
7
8
9
void CGraphicView::OnSetting() 
{
CSettingDlg dlg;
dlg.m_nLineWidth = m_nLineWidth; //将保存的用户先前设置的线宽再传回给该设置对话框
if(IDOK == dlg.DoModal())//点击OK才保持线宽值
{
m_nLineWidth = dlg.m_nLineWidth;
}
}

  在源文件前部添加:

1
Include “SettingDlg.h”

  修改OnLButtonUp函数:

1
2
3
4
5
6
7
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point) 
{
CClientDC dc(this);
//为边框设定颜色(m_nLineWidth定义线宽)
CPen pen(PS_SOLID, m_nLineWidth, RGB(255, 0, 0));
……
}

设置线型

  为对话框资源添加一个组框,Caption设为线型。ID为IDC_LINE_STYLE。在组框内放三个单选按钮,ID不变,名称分别为:实线、虚线、点线(不要改变顺序)。在第一个单选按钮上右键,属性勾上Group,使三个按钮成为一组。再为CGraphicView类添加一个Int类型的私有成员变量m_nLineStyle,在构造函数中初始化为0。
  由于WINGDI.h定义了一些符号常量,(可以在PS_SOLID右键,Go To Definition Of PS_SOLID),刚好PS_SOLID(实线)值本身就是0;PS_DASH(虚线)是1;PS_DOT(点线)是2。所以此处的排列是故意为之。

这里写图片描述
这里写图片描述

这里写图片描述
这里写图片描述

  注意:若要画出虚线和点线,线宽只能为0或1。

颜色对话框

  在绘图下增加一个子菜单,ID为IDM_COLOR,Caption为颜色。为其在视类增加一个命令响应,代码:

1
2
3
4
5
6
7
8
9
10
11
void CGraphicView::OnColor() 
{
CColorDialog dlg;
dlg.m_cc.Flags |= CC_RGBINIT;
dlg.m_cc.rgbResult = m_clr;
if (IDOK == dlg.DoModal())
{
m_clr = dlg.m_cc.rgbResult;
//dlg.m_cc.Flags |= CC_RGBINIT | CC_FULLOPEN;//让颜色对话框完全展开
}
}

  为CGraphicView类再增加一个COLORREF类型的私有成员变量:m_clr,并在构造函数中初始化为红色:

1
m_clr = RGB(255, 0, 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
26
27
28
void CGraphicView::OnLButtonUp(UINT nFlags, CPoint point) 
{
CClientDC dc(this);
//为边框设定颜色(m_nLineStyle定义线型,m_nLineWidth定义线宽,m_clr定义颜色)
CPen pen(m_nLineStyle, m_nLineWidth, m_clr);
dc.SelectObject(&pen);
//能看到图形内部内容(透明)
CBrush *pBrush = CBrush::FromHandle((HBRUSH)GetStockObject(NULL_BRUSH));
dc.SelectObject(pBrush);

switch(m_nDrawType)
{
case 1:
dc.SetPixel(point,m_clr);
break;
case 2:
dc.MoveTo(m_ptOrigin);
dc.LineTo(point);
break;
case 3:
dc.Rectangle(CRect(m_ptOrigin,point));
break;
case 4:
dc.Ellipse(CRect(m_ptOrigin, point));
break;
}
CView::OnLButtonUp(nFlags, point);
}

这里写图片描述

  注意://dlg.m_cc.Flags |= CC_RGBINIT | CC_FULLOPEN;//让颜色对话框完全展开
这句我没能实现展开效果。

字体对话框

  增加一个菜单,ID为IDM_FONT,Caption为字体。在视类增加命令响应,代码:

1
2
3
4
5
6
7
8
9
10
11
12
void CGraphicView::OnFont() 
{
CFontDialog dlg;
if (IDOK == dlg.DoModal())
{
if (m_font.m_hObject) //m_font对象是否已经与某字体资源相关联
m_font.DeleteObject();
m_font.CreateFontIndirect(dlg.m_cf.lpLogFont);
m_strFontName = dlg.m_cf.lpLogFont->lfFaceName;
}

}

1
2
3
4
5
6
7
8
9
10

void CGraphicView::OnDraw(CDC* pDC)
{
CGraphicDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
CFont *pOldFont = pDC->SelectObject(&m_font);
pDC->TextOut(0, 0, m_strFontName);
pDC->SelectObject(pOldFont);
}

示例对话框

  在对话框中增加一个组框,Caption:示例,ID:IDC_SAMPLE。为CSettingDlg类添加编辑框控件的EN_CHANCE响应函数,对三个单选按钮都选择BN_CLICKED消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void CSettingDlg::OnRadio1() 
{
// TODO: Add your control notification handler code here
Invalidate();
}

void CSettingDlg::OnRadio2()
{
// TODO: Add your control notification handler code here
Invalidate();
}

void CSettingDlg::OnRadio3()
{
// TODO: Add your control notification handler code here
Invalidate();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void CSettingDlg::OnPaint() 
{
CPaintDC dc(this); // device context for painting

// TODO: Add your message handler code here
UpdateData();
CPen pen(m_nLineStyle, m_nLineWidth, m_clr);
dc.SelectObject(&pen);

CRect rect;
GetDlgItem(IDC_SAMPLE)->GetWindowRect(&rect);
ScreenToClient(&rect);

dc.MoveTo(rect.left+20, rect.top+rect.Height()/2);
dc.LineTo(rect.right-20, rect.top+rect.Height()/2);
// Do not call CDialog::OnPaint() for painting messages
}
这里写图片描述
这里写图片描述

  现在可以实时修改了。

10.6 改变对话框和控件的背景及文本颜色

改变整个对话框及其子控件的背景色

  为CSettingDlg类添加WM_CTLCOLOR消息,并定义一个CBrush类型的私有成员变量:m_brush,并在构造函数中初始化一个蓝色画刷:

1
m_brush.CreateSolidBrush (RGB(0, 0, 255));

1
2
3
4
5
6
7
8
9
10
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) 
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);

// TODO: Change any attributes of the DC here

// TODO: Return a different brush if the default is not desired
//return hbr;
return m_brush;
}

这里写图片描述

仅改变某个子控件的背景及文本颜色

图形的保存和重绘

坐标空间和转换

坐标空间

  Win32应用程序编程接口(API)使用四种坐标空间:世界坐标系空间、页面空间、设备空间和物理设备空间。Win32 API把世界坐标系空间和页面空间称为逻辑空间。

转换

  转换是把对象从一个坐标空间复制到另一个坐标空间时改变(或转变)这一对象的大小、方位和形态。

图形的保存和重绘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) 
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);

// TODO: Change any attributes of the DC here

// TODO: Return a different brush if the default is not desired
//return hbr;
if (pWnd -> GetDlgCtrlID() == IDC_LINE_STYLE)
{
pDC->SetTextColor(RGB(255, 0, 0));
return m_brush;
}
return hbr;
}

这里写图片描述

  上述程序再加一行:

1
pDC->SetBkMode(TRANSPARENT);

这里写图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
HBRUSH CSettingDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) 
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);

// TODO: Change any attributes of the DC here

// TODO: Return a different brush if the default is not desired
//return hbr;
if (pWnd -> GetDlgCtrlID() == IDC_LINE_STYLE)
{
pDC->SetTextColor(RGB(255, 0, 0));
pDC->SetBkMode(TRANSPARENT);
return m_brush;
}
if (pWnd->GetDlgCtrlID() == IDC_LINE_WIDTH)
{
pDC->SetTextColor(RGB(255, 0, 0));
//pDC->SetBkMode(TRANSPARENT);
pDC->SetBkColor(RGB(0, 0, 255));
return m_brush;
}
return hbr;
}

这里写图片描述

改变控件上的文本字体

  为对话框增加一个静态文本控件,ID:IDC_TEXT,Caption:程序员,为CSettingDlg类增加一个CFont类型的私有成员变量:m_font,在构造函数中添加

1
m_font.CreatePointFont(200, "华文行楷");

  在OnCtlColor函数中添加:

1
2
3
4
if (pWnd->GetDlgCtrlID() == IDC_TEXT)
{
pDC->SelectObject(&m_font);
}

这里写图片描述

改变按钮控件的背景色及文本颜色

  在CSettingDlg类OnCtlColor函数中添加:

1
2
3
4
5
6
7
	if (pWnd->GetDlgCtrlID() == IDOK)
{
pDC->SetTextColor(RGB(255, 0, 0));
return m_brush;
}
return hbr;
}

  点Insert-New Class,选择MFC Class,新增类名:CTestBtn,基类CButton。
  为此类添加DrawItem虚函数重写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void CTestBtn::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) 
{
// TODO: Add your code to draw the specified item
UINT uStyle = DFCS_BUTTONPUSH;

ASSERT(lpDrawItemStruct->CtlType == ODT_BUTTON);

if (lpDrawItemStruct->itemState & ODS_SELECTED)
uStyle |= DFCS_PUSHED;

::DrawFrameControl(lpDrawItemStruct->hDC, &lpDrawItemStruct->rcItem, DFC_BUTTON, uStyle);

CString strText;
GetWindowText(strText);

COLORREF crOldColor = ::SetTextColor(lpDrawItemStruct->hDC, RGB(255, 0, 0));
::DrawText(lpDrawItemStruct->hDC, strText, strText.GetLength(),
&lpDrawItemStruct->rcItem, DT_SINGLELINE | DT_VCENTER | DT_CENTER);
::SetTextColor(lpDrawItemStruct->hDC, crOldColor);

}

  然而,此时我返回双击OK键显示“Cannot add new member”……

  按理,接下来应该是:
  利用ClassWizard打开Add Member Variable对话框,为OK按钮关联一个成员变量,名称为m_btnTest,类型CTestBtn。在SettingDlg.h文件前部添加#include “TestBtn.h”。对OK右键属性,打开Styles,选中Owner draw选项。此时OK文字变红色。

位图的显示

定制应用程序外观

修改应用程序窗口的外观

在窗口创建之前修改

  创建前,打开CMainFrame类的PreCreateWindow成员函数,修改CREATETRUCT结构体中的cx和cy成员。

1
2
3
4
5
6
7
8
9
10
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
if( !CFrameWnd::PreCreateWindow(cs) )
return FALSE;
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
cs.cx = 300;
cs.cy = 200;
return TRUE;
}

  创建运行,可看到初始大小为300x200的应用程序窗口。

  修改窗口标题:在上述 return TRUE; 前添加:

1
2
cs.style &= ~FWS_ADDTOTITLE;
cs.lpszName = "http://www.sunxin.org";

在窗口创建之后修改

  注释掉之前添加的代码。在OnCreate函数中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{


// TODO: Delete these three lines if you don't want the toolbar to
// be dockable
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_wndToolBar);

SetWindowLong(m_hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);


return 0;
}

  创建运行后可看到文档标题去掉了。

  去掉窗口最大化框类型:
  将上述SetWindowLong函数替换为

1
SetWindowLong(m_hWnd, GWL_STYLE, GetWindowLong(m_hWnd, GWL_STYLE) & ~WS_MAXIMIZEBOX);

  创建运行发现最大化框变灰,不能放大窗口了。

修改窗口的光标、图标和背景

在窗口创建之前修改

网络编程

计算机网络基本知识

ISO/OSI七层参考模型
应用层——处理网络应用
Telnet、FTP、HTTP、DNS、SMTP、POP3

表示层——数据表示
TCP、UDP

会话层——主机间通信
传输层——端到端的连接
网络层——寻址和最短路径
IP、ICMP、IGMP

数据链路层——介质访问(接入)
物理层——二进制传输

基于TCP的网络应用程序的编写

服务器端程序

  关闭先前的工作区,新建一个工程,选择Win32 Console Application类型,名为TCPSrv。选择An empty project选项,创建一个空工程。再新建一个C++源文件:TcpSrv.cpp。

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
53
54
55
#include <Winsock2.h>
#include <stdio.h>

void main()
{
//加载套接字库
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1);

err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0){
return;
}

if (LOBYTE(wsaData.wVersion) != 1 ||
HIBYTE(wsaData.wVersion) != 1){
WSACleanup();
return;
}
//创建用于监听的套接字
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);

SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(7000);

//绑定套接字
bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
//将套接字设为监听模式,准备接收客户请求
listen(sockSrv, 5);

SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);

while (1)
{
//等待客户请求到来
SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
char sendBuf[100];
sprintf(sendBuf, "Welcome %s to http://www.sunxin.org", inet_ntoa(addrClient.sin_addr));
//发送数据
send(sockConn, sendBuf, strlen(sendBuf)+1, 0);
char recvBuf[100];
//接收数据
recv(sockConn, recvBuf, 100, 0);
//打印接收的数据
printf("%s\n", recvBuf);
//关闭套接字
closesocket(sockConn);
}
}

  Project-Setting-Link,在Object/library modules编辑框中添加ws2_32.lib文件,注意输入的库文件与前面的库文件之间一定 要有一个空格。

这里写图片描述
这里写图片描述

客户端程序

  在工作区名称上单击鼠标右键,选择Add New Project to Workspace,再创建一个Win32 Console Application类型的应用程序,创建一个空工程。为此增加一个C++源文件:TcpClient.cpp。

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
#include <Winsock2.h>
#include <stdio.h>

void main()
{
//加载套接字库
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1);

err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0){
return;
}

if (LOBYTE(wsaData.wVersion) != 1 ||
HIBYTE(wsaData.wVersion) != 1){
WSACleanup();
return;
}
//创建套接字
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);

SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(7000);

//向服务器发出连接请求
connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

//接收数据
char recvBuf[100];
recv(sockClient, recvBuf, 100, 0);
printf("%s\n",recvBuf);
//发送数据
send(sockClient, "This is lisi", strlen("This is lisi")+1, 0);
//关闭套接字
closesocket(sockClient);
WSACleanup();
}

  链接库文件:ws2_32.lib。
  创建运行,首先运行服务器程序,然后再运行客户端程序。

这里写图片描述
这里写图片描述

  注意:当没有报错,服务器端运行结果为“烫烫……烫”(N个烫)时,尝试换一个端口号,有可能你设置的端口号被其它的应用程序占用了。

基于UDP的网络应用程序的编写

服务器端程序

  关闭先前的工作区,新建一个工程,选择Win32 Console Application类型,名为UdpSrv。选择An empty project选项,创建一个空工程。再新建一个C++源文件:UdpSrv.cpp。

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
#include <Winsock2.h>
#include <stdio.h>

void main()
{
//加载套接字库
WORD wVersionRequired;
WSADATA wsaData;
int err;

wVersionRequired = MAKEWORD(1, 1);

err = WSAStartup(wVersionRequired, &wsaData);
if (err != 0)
{
return;
}

if (LOBYTE(wsaData.wVersion) != 1 ||
HIBYTE(wsaData.wVersion) !=1)
{
WSACleanup();
return;
}
//创建套接字
SOCKET sockSrv = socket(AF_INET, SOCK_DGRAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(7000);
//绑定套接字
bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

//等待并接收数据
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
char recvBuf[100];
recvfrom(sockSrv, recvBuf, 100, 0, (SOCKADDR*)&addrClient, &len);
printf("%s\n",recvBuf);
//关闭套接字
closesocket(sockSrv);
WSACleanup();

}

  在工程设置对话框的链接选项卡下添加库文件:Ws2_32.lib的链接。

客户端程序

  在同一个UdpSrv工作区中创建客户端应用程序。创建一个空的Win32 Console Application类型的工程,名为:UdpClient。为该工程添加一个C++源文件:UdpClient.cpp。

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
#include <Winsock2.h>
#include <stdio.h>

void main()
{
//加载套接字库
WORD wVersionRequired;
WSADATA wsaData;
int err;

wVersionRequired = MAKEWORD(1, 1);

err = WSAStartup(wVersionRequired, &wsaData);
if (err != 0)
{
return;
}

if (LOBYTE(wsaData.wVersion) != 1 ||
HIBYTE(wsaData.wVersion) !=1)
{
WSACleanup();
return;
}

//创建套接字
SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(7000);
//发送数据
sendto(sockClient, "Hello", strlen("Hello")+1, 0,
(SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
//关闭套接字
closesocket(sockClient);
WSACleanup();
}

  链接库文件:ws2_32.lib。

  创建运行。服务器端程序应先启动,然后启动客户端程序。

这里写图片描述
这里写图片描述

  基于TCP和基于UDP的网络应用程序在发送和接收数据时使用的函数是不一样的:前者使用send和recv,后者使用sendto和recvfrom。

基于UDP的简单聊天程序

  在新工作区新建一个空的Win32 Console Application类型的应用程序,名为NetSrv。为该工程添加一个C++源文件:NetSrv.cpp。接着为该工程添加对WinSock库的链接,即在工程设置对话框的Link选项卡上添加ws2_32.lib文件的链接。

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
53
54
55
56
57
58
59
60
61
62
#include <Winsock2.h>
#include <stdio.h>
void main()
{
//加载套接字库
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1);

err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
return;
}

if (LOBYTE(wsaData.wVersion) != 1 ||
HIBYTE(wsaData.wVersion) !=1)
{
WSACleanup();
return;
}

//创建套接字
SOCKET sockSrv = socket(AF_INET, SOCK_DGRAM, 0);

SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(7000);

//绑定套接字
bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));

char recvBuf[100];
char sendBuf[100];
char tempBuf[200];

SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
while(1)
{
//等待并接收数据
recvfrom(sockSrv, recvBuf, 100, 0, (SOCKADDR*)&addrClient, &len);
if ('q' == recvBuf[0])
{
sendto(sockSrv, "q", strlen("q")+1, 0, (SOCKADDR*)&addrClient, len);
printf("Chat end!\n");
break;
}
sprintf(tempBuf, "%s say : %s", inet_ntoa(addrClient.sin_addr), recvBuf);
printf("%s\n", tempBuf);
//发送数据
printf("Please input data:\n");
gets(sendBuf);
sendto(sockSrv, sendBuf, strlen(sendBuf)+1, 0, (SOCKADDR*)&addrClient, len);
}
//关闭套接字
closesocket(sockSrv);
WSACleanup();
}

客户端程序

  向已有工作区增加一个空的Win32 Console Application类型的工程:NetClient。为此添加一个C++源文件:NetClient.cpp。为该工程添加ws2_32.lib文件的链接。

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
53
54
55
56
57
58
59
60
#include <Winsock2.h>
#include <stdio.h>
void main()
{
//加载套接字库
WORD wVersionRequested;
WSADATA wsaData;
int err;

wVersionRequested = MAKEWORD(1, 1);

err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
return;
}

if (LOBYTE(wsaData.wVersion) != 1 ||
HIBYTE(wsaData.wVersion) !=1)
{
WSACleanup();
return;
}

//创建套接字
SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0);

SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(7000);

char recvBuf[100];
char sendBuf[100];
char tempBuf[200];

int len = sizeof(SOCKADDR);

while(1)
{
//发送数据
printf("Please input data:\n");
gets(sendBuf);
sendto(sockClient, sendBuf, strlen(sendBuf)+1, 0, (SOCKADDR*)&addrSrv, len);
//等待并接收数据

recvfrom(sockClient, recvBuf, 100, 0, (SOCKADDR*)&addrSrv, &len);
if('q' == recvBuf[0])
{
sendto(sockClient, "q", strlen("q")+1, 0, (SOCKADDR*)&addrSrv, len);
printf("Chat end!\n");
break;
}
sprintf(tempBuf, "%s say : %s", inet_ntoa(addrSrv.sin_addr), recvBuf);
printf("%s\n", tempBuf);
}
//关闭套接字
closesocket(sockClient);
WSACleanup();
}

这里写图片描述
这里写图片描述

多线程

进程

程序和进程

简单多线程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <windows.h>
#include <iostream.h>

DWORD WINAPI Fun1Proc(
LPVOID lpParameter //thread data
);
void main()
{
HANDLE hThread1;
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
CloseHandle(hThread1);
cout<<"main thread is running"<<endl;
Sleep(10);//让主线程暂停运行,进入分线程
}

//线程1的入口函数
DWORD WINAPI Fun1Proc(
LPVOID lpParameter //thread data
)
{
cout<<"thread1 is running"<<endl;
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
26
27
28
29
30
31
32
33
34
#include <windows.h>
#include <iostream.h>

DWORD WINAPI Fun1Proc(
LPVOID lpParameter //thread data
);

int index = 0;

void main()
{
HANDLE hThread1;
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
CloseHandle(hThread1);

while (index ++< 100)
{
cout<<"main thread is running"<<endl;
}
//Sleep(10);//让主线程暂停运行,进入分线程
}

//线程1的入口函数

DWORD WINAPI Fun1Proc(

LPVOID lpParameter //thread data
)

{
while (index++< 100)
cout<<"thread1 is running"<<endl;
return 0;
}

这里写图片描述

线程同步

火车站售票系统模拟程序

  由主线程创建的两个线程(1和2)负责销售火车票。

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <windows.h>
#include <iostream.h>

DWORD WINAPI Fun1Proc(
LPVOID lpParameter //thread data
);

DWORD WINAPI Fun2Proc(
LPVOID lpParameter //thread data
);

int index = 0;
int tickets = 100;
HANDLE hMutex;

void main()
{
HANDLE hThread1;
HANDLE hThread2;

//创建互斥对象
hMutex = CreateMutex(NULL, FALSE, NULL);

//创建线程
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(4000);
}





//线程1的入口函数
DWORD WINAPI Fun1Proc(

LPVOID lpParameter //thread data
)

{
while (TRUE)
{
WaitForSingleObject(hMutex, INFINITE);//实现线程同步
if (tickets > 0)
{
Sleep(1);
cout<<"thread1 sell ticket:"<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);//释放当前线程对互斥对象的所有权
}
return 0;

}


//线程2的入口函数
DWORD WINAPI Fun2Proc(

LPVOID lpParameter //thread data
)


{
while (TRUE)
{
WaitForSingleObject(hMutex,INFINITE);
if (tickets > 0)
{
Sleep(1);
cout<<"thread2 sell ticket:"<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
}

这里写图片描述
  这时所销售的票号正常,没有看到销售了号码为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
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include <windows.h>
#include <iostream.h>

DWORD WINAPI Fun1Proc(
LPVOID lpParameter //thread data
);

DWORD WINAPI Fun2Proc(
LPVOID lpParameter //thread data
);

int index = 0;
int tickets = 100;
HANDLE hMutex;

void main()
{
HANDLE hThread1;
HANDLE hThread2;

//创建互斥对象(注意命名)
hMutex = CreateMutex(NULL, FALSE, "1");
if (hMutex)
{
if (ERROR_ALREADY_EXISTS == GetLastError())
{
cout<<"only one instance can run!"<<endl;
return;
}
}

//创建线程
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
WaitForSingleObject(hMutex, INFINITE);
ReleaseMutex(hMutex);
ReleaseMutex(hMutex);
Sleep(4000);
}

//线程1的入口函数
DWORD WINAPI Fun1Proc(

LPVOID lpParameter //thread data
)

{
while (TRUE)
{
WaitForSingleObject(hMutex, INFINITE);//实现线程同步
if (tickets > 0)
{
Sleep(1);
cout<<"thread1 sell ticket:"<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);//释放当前线程对互斥对象的所有权
}
return 0;

}


//线程2的入口函数
DWORD WINAPI Fun2Proc(

LPVOID lpParameter //thread data
)


{
while (TRUE)
{
WaitForSingleObject(hMutex,INFINITE);
if (tickets > 0)
{
Sleep(1);
cout<<"thread2 sell ticket:"<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
}

网络聊天室程序的实现

  新建一个基于对话框的工程,名为:Chat。

这里写图片描述
这里写图片描述

这里写图片描述
这里写图片描述

加载套接字库

  在CChatApp类的InitInstance函数开始位置

1
2
3
4
5
6
7
8
9
BOOL CChatApp::InitInstance()
{
if (!AfxSocketInit())
{
AfxMessageBox("加载套接字库失败!");
return FALSE;
}
……
}

  在stdafx.h中,添加头文件#include <Afxsock.h>

创建并初始化套接字

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
BOOL CChatDlg::InitSocket()
{
//创建套接字
m_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (INVALID_SOCKET == m_socket)
{
MessageBox("套接字创建失败!");
return FALSE;
}
SOCKADDR_IN addrSock;
addrSock.sin_family = AF_INET;
addrSock.sin_port = htons(7000);
addrSock.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

int retval;
//绑定套接字
retval = bind(m_socket, (SOCKADDR*)&addrSock, sizeof(SOCKADDR));
if (SOCKET_ERROR == retval)
{
closesocket(m_socket);
MessageBox("绑定失败!");
return TRUE;
}
return TRUE;

}
1
2
3
4
5
6
7
8
9
BOOL CChatDlg::OnInitDialog()
{
……

// TODO: Add extra initialization here
InitSocket();

return TRUE; // return TRUE unless you set the focus to a control
}

实现接收端功能

  在CChatDlg类中定义:

1
2
3
4
5
6
7
8
/////////////////////////////////////////////////////////////////////////////
// CChatDlg dialog

struct RECVPARAM
{
SOCKET sock; //已创建的套接字
HWND hwnd; //对话框句柄
};

  在Chatdlg.h中添加:static DWORD WINAPI RecvProc(LPVOID lpParameter);
在OnInitDialog()中添加:

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

……

// TODO: Add extra initialization here
InitSocket();
RECVPARAM *pRecvParam = new RECVPARAM;
pRecvParam->sock = m_socket;
pRecvParam->hwnd = m_hWnd;
//创建接收线程
HANDLE hThread = CreateThread(NULL, 0, RecvProc, (LPVOID)pRecvParam, 0, NULL);
//关闭该接收程句柄,释放其引用计数
CloseHandle(hThread);


return TRUE; // return TRUE unless you set the focus to a control
}

  在CChatDlg类中添加:

1
2
3
4
DWORD WINAPI CChatDlg::RecvProc(LPVOID lpParameter)
{
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
DWORD WINAPI CChatDlg::RecvProc(LPVOID lpParameter)
{
//获取主线程传递的套接字和窗口句柄
SOCKET sock = ((RECVPARAM*)lpParameter)->sock;
HWND hwnd = ((RECVPARAM*)lpParameter)->hwnd;
delete lpParameter;

SOCKADDR_IN addrFrom;
int len = sizeof(SOCKADDR);

char recvBuf[200];
char tempBuf[300];
int retval;
while(TRUE)
{
//接收数据
retval = recvfrom(sock, recvBuf, 200, 0, (SOCKADDR*)&addrFrom, &len);
if (SOCKET_ERROR == retval)
break;
sprintf(tempBuf, "%s 说: %s", inet_ntoa(addrFrom.sin_addr), recvBuf);
::PostMessage(hwnd, WM_RECVDATA, 0, (LPARAM)tempBuf);
}
return 0;
}

  在该类添加头文件 #define WM_RECVDATA WM_USER+1

在CChatDlg类头文件中编写该消息响应函数原型的声明:

1
2
3
4
5
6
7
8
9
// Generated message map functions
//{{AFX_MSG(CChatDlg)
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
//}}AFX_MSG
afx_msg void OnRecvData(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()

在CChatDlg类的源文件中添加WM_RECVDATA消息映射。

1
2
3
4
5
6
7
8
BEGIN_MESSAGE_MAP(CChatDlg, CDialog)
//{{AFX_MSG_MAP(CChatDlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
//}}AFX_MSG_MAP
ON_MESSAGE(WM_RECVDATA, OnRecvData)
END_MESSAGE_MAP()

在构造函数中

1
2
3
4
5
6
7
8
9
10
11
12
void CChatDlg::OnRecvData(WPARAM wParam, LPARAM lParam)
{
//取出接收到的数据
CString str = (char*)lParam;
CString strTemp;
//获得已有数据
GetDlgItemText(IDC_EDIT_RECV, strTemp);
str += "\r\n";
str += strTemp;
//显示所有接收到的数据
SetDlgItemText(IDC_EDIT_RECV, str);
}

实现发送端功能

双击发送,添加响应函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void CChatDlg::OnBtnSend() 
{
//获取对方IP
// TODO: Add your control notification handler code here
DWORD dwIP;
((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);

SOCKADDR_IN addrTo;
addrTo.sin_family = AF_INET;
addrTo.sin_port = htons(7000);
addrTo.sin_addr.S_un.S_addr = htonl(dwIP);

CString strSend;
//获得待发送数据
GetDlgItemText(IDC_EDIT_SEND, strSend);
//发送数据
sendto(m_socket, strSend, strSend.GetLength()+1, 0,
(SOCKADDR*)&addrTo, sizeof(SOCKADDR));
//清空发送编辑框中的内容
SetDlgItemText(IDC_EDIT_SEND, "");
}

  为了让编辑框控件接受换行符,必须设置该控件支持多行数据这一属性。

这里写图片描述
这里写图片描述

  将“发送”设置为Default button,还可以选择取消Visible选项。
  本例在一个程序中同时实现了接收端和发送端的功能,所以只需在聊天双方各自的机器上安装本程序,在聊天时,通过输入对方主机的IP地址,就可以与对方进行通信了。

ActiveX控件

ActiveX控件

OCX是ActiveX控件的一种后缀名。但ActiveX控件对应的文件也可以是其他后缀名,例如DLL。作为一个典型的ActiveX控件,它具有方法、属性、事件这三种特性。在一个文件中可以包含多个ActiveX控件。

MFC ActiveX ControlWizard

动态链接库

  动态链接库有两种加载方式:隐式链接和显式加载

Win32 DLL的创建和使用

  新建一个Win32 Dynamic-Link Library类型的工程,取名为Dll1。并在AppWizard的第一步选择“An empty Dll project”,即创建一个空的动态链接库工程。然后添加一个C++源文件:Dll1.cpp。添加代码:

1
2
3
4
5
6
7
8
9
int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

  Build生成Dll1程序。在该工程的Debug目录下,可看到一个Dll1.dll文件,这就是生成的动态链接库文件。

Dumpbin命令

  应用程序如果想要访问某个DLL中的函数,那么该函数必须是已经被导出的函数。为了查看有哪些导出函数,可以用VS提供的命令行工具:Dumpbin实现。

从DLL中导出函数

  为导出函数,需在每一个将被导出的函数前添加标识符:_declspec(dllexport)。修改上述代码:

1
2
3
4
5
6
7
8
9
_declspec(dllexport) int add(int a, int b)
{
return a + b;
}

_declspec(dllexport) int subtract(int a, int b)
{
return a - b;
}

  编译后可看到又生成了两个新文件,Dll1.lib,它保存Dll1.dll中导出的函数和变量的符号名。以及DALL1.EXP文件。

隐式链接方式加载DLL

  编写一个测试程序测试这个动态链接库。新建一个基于对话框的MFC应用程序,取名DllTest,放置两个按钮,ID和Caption分别为:IDC_BTN_ADD,Add,IDC_BTN_SUBTRACT,Subtract。

利用extern声明外部函数

  为让编译器知道这两个函数,需作出声明,注意放在OnBtnAdd函数和OnBtnSubtract函数前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extern int add(int a, int b);
extern int subtract(int a, int b);

void CDllTestDlg::OnBtnAdd()
{
// TODO: Add your control notification handler code here
CString str;
str.Format("5 + 3 = %d", add(5, 3));
MessageBox(str);
}

void CDllTestDlg::OnBtnSubtract()
{
// TODO: Add your control notification handler code here
CString str;
str.Format("5 - 3 = %d", subtract(5, 3));
MessageBox(str);
}

  Build后报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--------------------Configuration: DllTest - Win32 Debug--------------------
Compiling resources...
Compiling...
StdAfx.cpp
Compiling...
DllTest.cpp
DllTestDlg.cpp
Generating Code...
Linking...
DllTestDlg.obj : error LNK2001: unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z)
DllTestDlg.obj : error LNK2001: unresolved external symbol "int __cdecl subtract(int,int)" (?subtract@@YAHHH@Z)
Debug/DllTest.exe : fatal error LNK1120: 2 unresolved externals
执行 link.exe 时出错.

DllTest.exe - 1 error(s), 0 warning(s)

  可看到编译成功,错误发生在链接时。为解决该问题,需利用动态链接库的引入库文件。
  在Dll1.dll文件所在目录下,复制Dll1.lib文件,并将其复制到DllTest程序所在目录下,这个文件中就包含了Dll1.dll中导出函数的符号名。
  然后在DllTest中,选择Porject\Settings\link,在Object/library modules中输入dll1.lib。
  再次编译,成功生成DllTest.exe文件。
  (可利用dumpbin -imports dlltest.exe查看输入信息)

  运行程序,弹出报错对话框:

报错
报错

  将Dll1.dll放置在DllTest工程所在目录下,就好了。

  效果如图。

效果
效果

Depends工具

  在Microsoft Visual Studio\Common\Tools中有一个DEPENDS.EXE,该工具可以查看可执行程序,还可以查看动态链接库,主要是看它们依赖于哪些动态链接库。
  打开该工具,单击File\Open,选择DllText.exe,将会看到:

DEPENDS
DEPENDS

  DllTest程序需访问Dll1.dll这一动态链接库,但该文件名前有一个问号,说明没有找到Dll1.dll这个动态链接库。这是因为前面将动态链接库文件放在了\DllTest\Debug目录的上一级目录下了。这里,可将Dll1.dll文件再复制到\DllTest\Debug目录下,然后重启Depends工具。这时问号就没有了。(因为Dll1.dll与DllTest.exe位于同一目录,在打开DllTest.exe时,就可找到该动态链接库。)

成功
成功

利用_declspec(dllimport)声明外部函数

  除了使用extern关键字表明函数是外部定义的之外,还可以使用标识符:_declspec(dllimport)来表明函数是从动态链接库中引入的。将之前的extern声明注释起来。添加:

1
2
_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int subtract(int a, int b);

  若调用的函数来自于动态链接库,应采用这种方式声明外部函数,编译器可以生成运行效率更高的代码。

完善Win32 DLL例子

  为知道DLL有哪些导出函数,通常在编写动态链接库时,会提供一个头文件,在此提供DLL导出函数原型的声明,以及函数有关注释文档。

  为DLL1工程添加一个头文件:Dll1.h,并添加代码

1
2
_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int subtract(int a, int b);

  然后将DllTestDlg.cpp先前添加的声明语句注释起来,并在前部添加下面的语句:

1
#include "dll1.h"

  Build并运行,结果和之前一样。

  所以在发布Dll1.dll动态链接库时,可将Dll1.h头文件一起提供给使用者。

  下面对Dll.h进行改造,使其不仅能为调用动态链接库的客户端程序服务,也能由动态链接库程序自身来使用。修改头文件:

1
2
3
4
5
6
7
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif

_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int subtract(int a, int b);

  修改Dll1.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#define DLL1_API _declspec(dllexport)
#include "Dll1.h"

int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

  将重新生成的文件复制,运行,结果也是正确的。

从DLL中导出C++类

  在一个动态链接库中还可以导出一个C++类。
  在Dll1.h中添加如下代码:

1
2
3
4
5
class DLL1_API Point
{
public:
void output(int x, int y);
};

  在Dll1.cpp中改为:

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
#define DLL1_API _declspec(dllexport)
#include "Dll1.h"
#include <Windows.h>
#include <stdio.h>

int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

void Point::output(int x, int y)
{
//返回调用者进程当前正在使用的那个窗口的句柄
HWND hwnd = GetForegroundWindow();
//获取DC
HDC hdc = GetDC(hwnd);
char buf[20];
memset(buf, 0, 20);
sprintf(buf, "x = %d, y = %d", x, y);
//输出坐标
TextOut(hdc, 0, 0, buf, strlen(buf));
//释放DC
ReleaseDC(hwnd, hdc);
}

  将Dll1.dll和Dll1.lib复制到测试工程DllTest所在目录下(本例将对应Dll1.h也放在了DllTest项目工程下,所以Dll1.h也应相应复制过来)。为避免麻烦,也可以把动态链接库文件所在目录添加到系统的环境变量Path中。这样就无需复制。
  为测试这个新生成的DLL,打开DllTest工程,在对话框中增加一个按钮,属性为IDC_BTN_OUTPUT,Capition为Output。双击按钮添加响应函数OnBtnOutput。

1
2
3
4
5
6
void CDllTestDlg::OnBtnOutput() 
{
// TODO: Add your control notification handler code here
Point pt;
pt.output(5, 3);
}

  记得删除Debug下的旧Dll1.dll,放入新的,否则会报错。

Output
Output

  可利用Dumpbin命令的exports选项查看Dll1.dll这一动态链接库的导出情况,利用imports选项查看测试程序的导入情况。

另外,在实现动态链接库时,可以不导出整个类,而只导出该类中的某些函数。
打开Dll1工程,在Dll1.h中将声明Point类时使用的DLL1_API宏注释起来,然后在output函数的声明前放置DLL1_API宏。这样就表示只导出Point类中的成员函数output。为证实这一点,为Point类再添加一个成员函数test,

1
2
3
4
5
6
class /*DLL1_API*/ Point
{
public:
void DLL1_API output(int x, int y);
void test();
};

接着在Dll1.cpp中添加test函数的实现:

1
2
3
void Point::test()
{
}

Build后,利用dumpbin命令的exports可查看Dll1.dll的导出信息。
可将所需文件再次复制到DllTest工程中,运行结果和之前相同。
在导出类的成员函数时,该函数必须具有public类型的访问权限,否则即使能被导出也不能被其他程序访问。

解决名字改编问题

C++编译器在生产厂DLL时,会对导出的函数进行名字改编,由于不同编译器改编规则不同,所以改编后名字不同。若利用不同编译器分别生成DLL和访问该DLL的客户端程序时,后者在访问该DLL的导出函数时就会出现问题。因此希望动态链接库在编译时,导出函数的名称不要发生改变。为此,在定义导出函数时,需加上限定符:extern “C”。C一定要大写。
打开Dll1工程,找到Dll1.cpp和Dll1.h中定义DLL1_API宏的代码,添加限定符。
此时,Dll1.h为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifdef DLL1_API
#else
#define DLL1_API extern "C" _declspec(dllimport)
#endif

DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

/*class Point
{
public:
void DLL1_API output(int x, int y);
void test();
};*/

Dll1.cpp为

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
#define DLL1_API extern "C" _declspec(dllexport)
#include "Dll1.h"
#include <Windows.h>
#include <stdio.h>

int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

/*
void Point::output(int x, int y)
{
//返回调用者进程当前正在使用的那个窗口的句柄
HWND hwnd = GetForegroundWindow();
//获取DC
HDC hdc = GetDC(hwnd);
char buf[20];
memset(buf, 0, 20);
sprintf(buf, "x = %d, y = %d", x, y);
//输出坐标
TextOut(hdc, 0, 0, buf, strlen(buf));
//释放DC
ReleaseDC(hwnd, hdc);
}

void Point::test()
{
}
*/

Build后生成Dll1.dll,用dumpbin命令的exports选项查看该动态链接库的导出信息,发现add和subtract函数名没有被改编。然后利用DllTest工程测试,将Point类的代码注释起来,将发现客户端可访问Dll1中的导出函数。

缺陷:extern “C”只能导出全局函数,不能导出一个类的成员函数。另外,如果导出函数的调用约定发生了改变,即使使用了extern “C”,函数名仍会发生改编。
例如,在Dll1.h中add和subtract函数添加_stdcall关键字标准调用约定。

1
2
3
4
5
6
7
#ifdef DLL1_API
#else
#define DLL1_API extern "C" _declspec(dllimport)
#endif

DLL1_API _stdcall int add(int a, int b);
DLL1_API _stdcall int subtract(int a, int b);

在Dll1.cpp中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define DLL1_API extern "C" _declspec(dllexport)
#include "Dll1.h"
#include <Windows.h>
#include <stdio.h>

int _stdcall add(int a, int b)
{
return a + b;
}

int _stdcall subtract(int a, int b)
{
return a - b;
}

没有_stdcall关键字,函数的调用约定就是C调用约定,标准调用约定是WINAPI调用约定,与C调用约定不同。
Build后生成最新Dll1.dll,利用Dumpbin的exports选项查看该动态链接库的导出情况,可看到名字变为_add@8。

这种情况下,可通过模型定义文件(DEF)的方式来解决名字改编问题。
新建一个Win32 Dynamic-Link Library类型的工程,取名为Dll2,在AppWizard第一步选择“An empty Dll project”选项。添加Dll2.cpp,

1
2
3
4
5
6
7
8
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}

在Dll2工程目录下新建一个空文本文件,改后缀为.def,添加到工程Source文件并打开:

1
2
3
4
5
LIBRARY Dll2

EXPORTS
add
subtract

Bulid后利用Dumpbin的exports查看证明没有发生名字改编。

显示加载方式加载DLL

将最新的Dll2.dll复制到DllTest工程目录下。将DllTestDlg.cpp包含Dll1.h的那行代码注释起来,在link选项卡上删除对Dll1.lib的链接。
需用到LoadLibrary函数。

  To be continued…
  听听那冷雨