基础语法篇_9——VS2019+MFC:修改应用程序窗口的外观【窗口光标|图标|背景】、模拟动画图标、工具栏编程、状态栏编程、进度栏编程、在状态栏上显示鼠标当前位置、启动画面

MFC 同时被 2 个专栏收录
35 篇文章 8 订阅
84 篇文章 4 订阅

🔳🔳 绘制线条 、画刷绘图、绘制连续线条、绘制扇形效果的线条


🔳🔳 插入符【文本插入符|图形插入符】、窗口重绘、路径、字符输入【设置字体|字幕变色】


🔳🔳 菜单命令响应函数、菜单命令的路由、基本菜单操作、动态菜单操作、电话本实例


🔳🔳 对话框的创建与显示、动态创建按钮、控件的访问【控件调整|静态文本控件|编辑框控件】、对话框伸缩功能、输入焦点的传递、默认按钮的说明


🔳🔳 MFC对话框:逃跑按钮、属性表单、向导创建


🔳🔳 在对话框程序中让对话框捕获WM_KEYDOWN消息


🔳🔳修改应用程序窗口的外观【窗口光标|图标|背景】、模拟动画图标、工具栏编程、状态栏编程、进度栏编程、在状态栏上显示鼠标当前位置、启动画面


🔳🔳设置对话框、颜色对话框、字体对话框、示例对话框、改变对话框和控件的背景及文本颜色、位图显示

一、修改应用程序窗口的外观

  对于MFC应用程序来说,为了改变MFC AppWizard自动生成的应用程序外观和大小,既可以在应用程序窗口创建之前进行,也可以在该窗口创建之后进行。

首先新建一个单文档类型的MFC AppWizard (exe)工程,工程取名为: Style。运行程序如下:

1.1 窗口创建之前修改

如果希望在应用程序窗口创建之前修改它的外观和大小,就应该在CMainFrame类的PreCreateWindow成员函数中进行。该函数的初始定义代码:

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
	if( !CFrameWndEx::PreCreateWindow(cs) )
		return FALSE;
	// TODO: 在此处通过修改
	//  CREATESTRUCT cs 来修改窗口类或样式

	return TRUE;
}

  该函数是一个虚函数,在MFC底层代码中,当调用PreCreateWindow函数时,如果传递了子类对象的指针,根据多态性原理,那么就会调用子类对象的PreCreateWindow函数。

该函数有一个参数,其类型是CREATETRUCT结构:

  1. CREATETRUCT结构中的字段与CreateWindowEx函数的参数是完全一致的,只是先后顺序相反而已。
    typedef struct tagCREATESTRUCTW {
        LPVOID      lpCreateParams;//指向将被用于创建窗口的数据的指针
        HINSTANCE   hInstance;     //应用程序的实例句柄
        HMENU       hMenu;         //窗口菜单句柄
        HWND        hwndParent;    //父窗口句柄
        int         cy;            //指定新窗口的高度,以像素为单位
        int         cx;            //指定新窗口的宽度,以像素为单位
        int         y;             //指定新窗口的左上角y坐标
        int         x;             //指定新窗口的左上角x坐标
        LONG        style;         //指定新窗口的类型(风格)
        LPCWSTR     lpszName;      //指定新窗口的名称
        LPCWSTR     lpszClass;     //指定新窗口类的名称
        DWORD       dwExStyle;     //指定新窗口扩展风格。
    } CREATESTRUCTW, *LPCREATESTRUCTW;
    
  2. PreCreateWindow函数的这个参数声明为引用类型,这样,如果在子类对象中修改了这个参数中成员变量的值,那么这种改变会反映到MFC底层代码中,当MFC底层代码调用CreateWindowEx函数去创建窗口时,它就会使用改变后的参数值去创建这个窗口。

因此,为了修改一个窗口的外观和大小,只需要修改CREATETRUCT结构体中相关成员变量的值

✨1)修改Style应用程序窗口的大小,将其宽度设置为300,高度设置为200,即修改CREATETRUCT结构体的cx、cy成员:

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
	if( !CFrameWndEx::PreCreateWindow(cs) )
		return FALSE;
	// TODO: 在此处通过修改
	//  CREATESTRUCT cs 来修改窗口类或样式
	//通过指针修改当前窗口的长和宽
	cs.cx = 300;
	cs.cy = 200;
	
	return TRUE;
}


✨✨2)修改Style应用程序窗口的标题:

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
	if( !CFrameWndEx::PreCreateWindow(cs) )
		return FALSE;
	// TODO: 在此处通过修改
	//  CREATESTRUCT cs 来修改窗口类或样式
	//通过指针修改当前窗口的长和宽
	cs.cx = 300;
	cs.cy = 200;
	//设置当前窗口的标题
	cs.lpszName = _T("WaitFoF");
	
	return TRUE;
}

运行程序发现标题并未发生改变。

🔳🔲🔳 解决:创建的这个Style应用程序是一个SDI应用程序,在单文档界面(SDI)应用程序中,框架的默认窗口样式是WS_OVERLAPPEDWINDOWFWS_ADDTOTITLE样式的组合。

FWS_ADDTOTITLE是MFC特定的一种样式,指示框架将文档标题添加到窗口标题上。因此如果想让窗口显示自己设置的标题,只需要将窗口的FWS_ADDTOTITLE样式去掉即可。

🟢🟢 2.1)在现有类型的基础上去掉某个类型的方法:就是FWS_ADDTOTITLE类型取反,并与现在窗口类型进行操作,从而就可以将窗口的这个特定类型去掉。

因此在Style程序中:

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
	if( !CFrameWndEx::PreCreateWindow(cs) )
		return FALSE;
	// TODO: 在此处通过修改
	//  CREATESTRUCT cs 来修改窗口类或样式
	//通过指针修改当前窗口的长和宽
	cs.cx = 300;
	cs.cy = 200;
	//设置当前窗口的标题
	cs.lpszName = _T("WaitFoF");
	//法一:让窗口显示自己设置的标题,将窗口的FWS_ADDTOTITLE样式去掉
	cs.style &= ~FWS_ADDTOTITLE;
	
	return TRUE;
}

🟣🟣2.2)把CREATETRUCT结构体中的style成员直接设置为WS_OVERLAPPEDWINDOW

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
	if( !CFrameWndEx::PreCreateWindow(cs) )
		return FALSE;
	// TODO: 在此处通过修改
	//  CREATESTRUCT cs 来修改窗口类或样式
	//通过指针修改当前窗口的长和宽
	cs.cx = 300;
	cs.cy = 200;
	//设置当前窗口的标题
	cs.lpszName = _T("WaitFoF");
	//法一:让窗口显示自己设置的标题,将窗口的FWS_ADDTOTITLE样式去掉
	//cs.style &= ~FWS_ADDTOTITLE;

	//法二:框架的窗口样式设置为:WS_OVERLAPPEDWINDOW
	cs.style = WS_OVERLAPPEDWINDOW ;
	
	return TRUE;
}

运行代码,两种分格的显示效果相同:

1.2 窗口创建之后修改

在窗口创建之后可以利用前面已经介绍过的SetWindowLong这个函数来实现这种功能。

LONG SetWindowLong(HWND hWnd,int nIndex,LONG dwNewLong);
  • ◼ hWnd
    窗口句柄及间接给出的窗口所属的类。
  • ◼ nIndex
    指定将设定的大于等于0的偏移值。可取值:

    当hWnd参数标识了一个对话框时,也可使用下列值:
  • ◼ dwNewLong
    指定的替换值。

✨ 1)为了改变窗口的类型,该函数的第二个参数应指定为GWL_STYLE。第三个参数应指定新的窗口类型。在MFC程序中,如果想在窗口创建之后改变其外观,可以在框架类CMainFrame的OnCreate函数中添加具体的实现代码。

CMainFrame的OnCreate函数首先调用了基类的OnCreate函数,以完成窗口的创建。在该函数的最后,但在return语句之前添加改变窗口外观的代码

注意:
应先将上面在CMainFrame类PreCreateWindow函数中添加的代码注释起来。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    ......
   
    //设置一个新的窗口分格
    SetWindowLong(m_hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
    
	return 0;
}

运行Style程序,将会发现Style应用程序窗口标题栏上去掉了文档的标题。

✨✨ 2)当修改窗口外观时,如果是在已有类型的基础上进行一些修改的话,那么首先要获这个窗口的现有类型,这可以利用GetWindowLong这个函数来实现。该函数的作用是获取指定窗口的信息,返回值就是获取到的窗口信息。它的原型声明:

LONG GetWindowLong(HWND hWnd, int nIndex);
  • ◼ hWnd
    想要获取其信息的窗口的句柄
  • ◼ nIndex
    指定要获取的信息类型。如果将这个参数指定为GWL_STYLE,那么该函数就是获取指定窗口的类型。

利用GetWindowLong函数和SetWindowLong函数修改Style程序窗口外观:去掉窗口的最大化功能。
  前者获取窗口已有的类型,后者在此基础上去掉窗口的最大化框类型。

注意:
首先在Style程序的CMainFrame类的OnCreate函数中,把上面添加的那条修改窗口外观的语句注释起来。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    ......
   
    //SetWindowLong(m_hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
	//获取窗口已有的类型
	LONG oldStyle=GetWindowLong(m_hWnd, GWL_STYLE);
	//设置窗口类型,将窗口最大化去掉
	SetWindowLong(m_hWnd, GWL_STYLE, oldStyle & ~WS_MAXIMIZEBOX);
	
	return 0;
}

运行Style程序,可以看到程序窗口右上角的最大化框变灰。当用鼠标双击程序的标题栏时,窗口也不会放大了。

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

2.1 在窗口创建前修改

对于窗口的类型和大小,是在创建窗口时设定的。然而图标、光标和背景是在设计窗口类时指定的,窗口类的设计和注册是由MFC底层代码自动完成的,自己不可能、也不应该去修改MFC底层代码。可以通过编写自己的窗口类并注册,然后让随后的窗口按照自己编写的窗口类去创建。

✨ 1)在Style程序的CMainframe类的PreCreateWindow函数中编写一个自己的窗口类并注册。


BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
	if( !CFrameWndEx::PreCreateWindow(cs) )
		return FALSE;
	// TODO: 在此处通过修改
	//  CREATESTRUCT cs 来修改窗口类或样式

	WNDCLASS wndcls;
	//类的额外内存
	wndcls.cbClsExtra = 0;
	//窗口的额外内存
	wndcls.cbWndExtra = 0;
	//设置窗口类的背景
	wndcls.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
	//使用帮助光标
	wndcls.hCursor = LoadCursor(NULL, IDC_HELP);
	//使用系统提供的错误图标
	wndcls.hIcon = LoadIcon(NULL, IDI_ERROR);
	//获取当前应用程序的实例句柄
	wndcls.hInstance = AfxGetInstanceHandle();
	//获得窗口的过程
	wndcls.lpfnWndProc = ::DefWindowProc;
	//类的名称
	wndcls.lpszClassName = _T("waitFoF");
	//菜单的名称
	wndcls.lpszMenuName = NULL;
	//窗口类的类型:具有重绘和垂直重绘
	wndcls.style = CS_HREDRAW | CS_VREDRAW;

	RegisterClass(&wndcls);
	cs.lpszClass = _T("waitFoF");

	return TRUE;
}

首先定义一个WNDCLASS类型的变量wndcls,然后设置该变量的各个成员:
◼◼类的额外内存cbClsExtra
  这里不需要使用它,将它设置为0。
◼◼窗口的额外内存cbWndExtra
  这里不需要使用它,将它设置为0。
◼◼窗口背景hbrBackground
  GetStockObject函数获取一个黑色的画刷来设置窗口类的背景hbrBackground,该函数返回的是HGDIOBJ类型,而这里需要的是个画刷句柄(HBRUSH),因此进行一个强制类型转换。
◼◼窗口光标hCursor
  LoadCursor函数加载一个光标。如果使用系统标准光标,则该函数的第一个参数必须设置为NULL;第二个参数就是标准光标的ID,本例使用帮助光标:ID为IDC_HELP。
◼◼窗口的图标
  Loadlcon函数加载一个图标。如果使用系统标准图标,则该函数的第一个参数必须设置为NULL;第二个参数就标准图标的ID,本例使用系统提供的错误图标,其ID:IDI_ERROR。
◼◼应用程序事例的句柄hInstance
  自己编写的WinMain函数,系统调用应用程序时,它为该应用程序分配了一个句柄,并把该句柄作为WinMain函数的参数传递进来。于是可以直接通过该参数来对窗口类的hInstance成员赋值。
  但在这里,程序代码是通过AppWizard自动生成的,WinMain函数被隐藏了。MFC为我们提供了一个全局函数: AfxGetInstanceHandle,可以用来获取当前应用程序的实例句柄。

◼◼窗口过程lpfnWndProc
  本例只是想去修改窗口的光标、图标和背景,并不想改变它的窗口过程,也不想对消息进行一些特殊处理,所以直接调用DefWindowProc函数获得窗口的过程。因为CWnd类中也有一个DefWindowProc成员函数,但这里应该调用相应的全局API函数,因此在该函数的前面应该加 ::
◼◼类的名称lpszClassName
  设置为waifFoF。
◼◼菜单的名称lpszMenuName
  设置为NULL。窗口的菜单并不是在设计窗口类时创建的。在创建单文档模板时,将菜单资源标识(IDR_MAINFRAME)作为其中的一个参数传入。当MFC底层代码在创建框架窗口时,就会把此标识转换为相应的菜单句柄,然后去创建菜单和框架窗口。因此,在这里把菜单设置为NULL,并不会影响窗口菜单的创建。
◼◼类型style
  这里并不是窗口的类型,而是窗口类的类型。本例指定窗口类具有重绘和垂直重绘这两种类型。

接下来注册该窗口类,并让程序框架窗口类按照新设计的窗口类来创建,也就是将当前窗口的类名(cs的1pszClass成员)修改为: “waitFoF”。

运行Style程序,但发现窗口的背景仍是白色的,光标仍是MFC SDI应用程序默认的箭头形式,并不是在程序中设定的带一个问号形状的箭头光标(系统标准帮助光标的样子)。同时发现程序的图标发生了改变(即程序窗口左上角位置处的图标)。

🔳🔳背景和光标未变化原因:应用程序包含有两个窗口:应用程序框架窗口和视类窗口。前者包含了后者,后者覆盖在前者的上面。所以当Style程序运行后,看到的窗口实际上是视类窗口,而上述代码修改的实际上是框架窗口的背景和光标。同时,应用程序的图标属于框架窗口,因此上述Style程序运行后,程序窗口左上角的图标发生了改变。也就是说,在应用程序框架类中只能改变程序窗口的图标,而如果想要改变应用程序窗口的背景和光标的话,只能在视类中实现。

📋📋 解决:因此对Style程序来说,应该在其视类(CStyleView )创建窗口之前,即在PreCreateWindow函数中将窗口类设置为先前自定义的那个窗口类。因为这时该窗口类已经注册了,所以在创建视类窗口时,可以直接使用这个窗口类名。

BOOL CStyleView::PreCreateWindow(CREATESTRUCT& cs)
{
	// TODO: 在此处通过修改
	//  CREATESTRUCT cs 来修改窗口类或样式
	//将窗口类设置为先前自定义的窗口类
	cs.lpszClass =_T("waitFoF");

	return CView::PreCreateWindow(cs);
}

📢📢📢 综上所述,在MFC程序中,如果想要修改应用程序窗口的图标,则应在框架类中进行,因为在框架窗口中才有标题栏,所以才能修改位于该标题栏上的图标;如果想要修改程序窗口的背景和光标,就应该在视类中进行。

✨✨ 2)全局函数AfxRegisterWndClass,用来设定窗口的类型、光标、背景和图标。
🔵🔵 2.1)在框架窗口类中只能修改窗口的图标,而为了实现这一功能,需要重写整个窗口类。很显然这是一件很麻烦的事情,于是MFC又提供了一个全局函数: AfxRegisterWndClass,用来设定窗口的类型、光标、背景和图标。该函数的原型声明:

LPCTSTR AFXAPI AfxRegisterWndclass(UINT nClassStyle, HCURSOR  hCursor =0, HBRUSH  hbrBackground = 0, HICON hIcon =0);

该函数后三个参数都有默认值。它的返回值就是注册之后的类名,可以直接将这个返回值作为程序中随后创建窗口时所依赖的类。

  利用这个函数来实现修改窗口图标的功能。首先需要将CMainFrame类和CStyleView类的PreCreateWindow函数中先前添加的修改窗口光标、图标和背景的代码注释起来:

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
	if( !CFrameWndEx::PreCreateWindow(cs) )
		return FALSE;
	// TODO: 在此处通过修改
	//  CREATESTRUCT cs 来修改窗口类或样式
	//通过指针修改当前窗口的长和宽
	//cs.cx = 300;
	//cs.cy = 200;
	//设置当前窗口的标题
	//cs.lpszName = _T("WaitFoF");
	//法一:让窗口显示自己设置的标题,将窗口的FWS_ADDTOTITLE样式去掉
	//cs.style &= ~FWS_ADDTOTITLE;

	//法二:框架的窗口样式设置为:WS_OVERLAPPEDWINDOW
	//cs.style = WS_OVERLAPPEDWINDOW ;


	/*WNDCLASS wndcls;
	//类的额外内存
	wndcls.cbClsExtra = 0;
	//窗口的额外内存
	wndcls.cbWndExtra = 0;
	//设置窗口类的背景
	wndcls.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
	//使用帮助光标
	wndcls.hCursor = LoadCursor(NULL, IDC_HELP);
	//使用系统提供的错误图标
	wndcls.hIcon = LoadIcon(NULL, IDI_ERROR);
	//获取当前应用程序的实例句柄
	wndcls.hInstance = AfxGetInstanceHandle();
	//获得窗口的过程
	wndcls.lpfnWndProc = ::DefWindowProc;
	//类的名称
	wndcls.lpszClassName = _T("waitFoF");
	//菜单的名称
	wndcls.lpszMenuName = NULL;
	//窗口类的类型:具有重绘和垂直重绘
	wndcls.style = CS_HREDRAW | CS_VREDRAW;

	RegisterClass(&wndcls);
	cs.lpszClass = _T("waitFoF");
	*/
	cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW, 0, 0, LoadIcon(NULL, IDI_WARNING));

	return TRUE;
}

直接让cs的lpszClass成员等于AfxRegisterWndClass函数的返回值。并且因为在框架窗口中修改窗口类的光标和背景是毫无意义的,因此在调用这个函数时将这两个参数设置为0。同时将窗口的图标设置为一个警告类型的图标(IDI_WARNING)。

运行Style程序:
🟣🟣 2.2)在视类的PreCreateWindow函数中利用AfxRegisterWndClass函数修改视类窗口的背景和光标。

BOOL CStyleView::PreCreateWindow(CREATESTRUCT& cs)
{
	// TODO: 在此处通过修改
	//  CREATESTRUCT cs 来修改窗口类或样式
	//  cs.lpszClass =_T("waitFoF");
	cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW, LoadCursor(NULL, IDC_CROSS), (HBRUSH)GetStockObject(BLACK_BRUSH), 0);

	return CView::PreCreateWindow(cs);
}

上述代码将视类窗口的光标设置为一个十字形状的光标,背景设置为黑色。同时,对于视类窗口来说,它本身并没有标题栏,所以也就没有图标。因此在AfxRegisterWndClass函数中不需要设置该窗口的图标,将相应的参数赋为0值即可。运行Style程序,可以看到程序视类窗口的背景是黑色十字型,框架窗口上的图标为警告图标。

📢📢📢 创建窗口之前,重新设计窗口类,然后利用这个新的窗口类去创建随后的窗口,从而实现修改程序窗口外观的方法。

2.2窗口创建之后修改

利用全局API函数: SetClassLong来实现,该函数用来重置指定窗口所属窗口类的WNDCLASSEX结构体(是WNDCLASS结构的扩展)中指定数据成员的属性,该函数的原型声明:

DWORD SetclassLong(HWND hWnd, int nindex, LONG dwNewLong);
  • ◼ hWnd
    设置新属性的窗口句柄。
  • ◼ nIndex
    指定要设置的属性的索引,此参数取值如下:
  • ◼ dwNewLong
    指定要设置的新的属性值。

在Style程序中实现在程序窗口创建之后修改窗口的光标、图标和背景这一功能。首先把Style程序中先前在框架类和视类的PreCreateWindow函数中自己添加的代码注释起来或去掉。

✨ 1)在CMainFrame类的OnCreate函数的最后,但要在return语句之前,添加代码:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	...... 
	
	//SetWindowLong(m_hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
	//获取窗口已有的类型
	LONG oldStyle=GetWindowLong(m_hWnd, GWL_STYLE);
	//设置窗口类型,将窗口最大化去掉
	SetWindowLong(m_hWnd, GWL_STYLE, oldStyle & ~WS_MAXIMIZEBOX);

	//设置图标
	SetClassLong(m_hWnd, GCL_HICON, (LONG)LoadIcon(NULL, IDI_ERROR));


	return 0;
}


在框架类中只有对窗口图标的修改会对程序界面产生影响,而对窗口的光标和背景的修改是不会产生什么效果的。因此这里只需要调用SetClassLong函数设置程序窗口的图标就可以了。
✨✨ 2)在Style程序的视类中修改视类窗口的光标和背景。对于CStyleView类来说,AppWizard并没有自动为它创建OnCreate函数。因此需要为该类添加WM_CREATE消息的响应函数,然后在这个响应函数(OnCreate函数)中,调用SetClassLong函数修改视类窗口的光标和背景。

在这里插入图片描述

int CStyleView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	if (CView::OnCreate(lpCreateStruct) == -1)
		return -1;

	// TODO:  在此添加您专用的创建代码\
	//设置背景
	SetClassLong(m_hWnd, GCL_HBRBACKGROUND, (LONG)GetStockObject(BLACK_BRUSH));
	//设置指针
	SetClassLong(m_hWnd, GCL_HCURSOR, (LONG)LoadCursor(NULL,IDC_HELP));
   
	return 0;
}

三、模拟动画图标

平常在使用一些软件时,发现它们的图标一直在不断地循环变化,给人一种动画的效果。这种功能的实现比较简单,就是预先准备好几幅图标,然后在程序中每隔一定的时间按顺序循环显示这几幅图标,从而就实现了一种动画的效果。

在实际编码实现时,利用定时器SetClassLong函数就可以完成这个功能。因为SetClassLong函数可以在窗口创建完成之后修改窗口的图标,所以可以在程序中每隔一定时间就调用一次这个函数,让其显示预先已准备好的一组图标中的下一幅,从而就可以实现所需的动画效果。

3.1 加载图标资源

本例使用三幅图标,文件名分别为: w.ico、h.ico和c.ico。将其直接复制到自己的Style工程的res目录下。

✨ 1)右键项目资源视图中的Icon。选择添加下的资源菜单命令,打开插入资源对话框,单击该对话框上的导入按钮。在弹出的导入资源对话框中,找到本例的res目录,并选中上述三个图标文件,单击打开文件按钮导入新的图标资源。

在资源视图的Icon下多了三个Icon资源。

✨✨2)Style程序的CMainFrame类中,定义一个图标句柄数组成员变量,用于存放这三幅图标的句柄。数组成员变量设置为:

  • 可以直接在CMainFrame类的头文件中添加以下代码:
    private:
        HICON m_hIcons[3];
    
  • 通过在视图中右键点击CMainFrame,添加变量。

接下来在CMainFrame类的OnCreate函数中利用LoadIcon函数加载这三个图标。

extern CStyleApp theApp;

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......

    //SetWindowLong(m_hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
	//获取窗口已有的类型
	LONG oldStyle=GetWindowLong(m_hWnd, GWL_STYLE);
	//设置窗口类型,将窗口最大化去掉
	SetWindowLong(m_hWnd, GWL_STYLE, oldStyle & ~WS_MAXIMIZEBOX);
    //加载后重新设置图标
	SetClassLong(m_hWnd, GCL_HICON, (LONG)LoadIcon(NULL, IDI_ERROR));

	//加载三个图标
	m_hIcons[0] = LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON2));
	m_hIcons[1] = LoadIcon(theApp.m_hInstance, MAKEINTRESOURCE(IDI_ICON3));
	m_hIcons[2] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON4));


	return 0;
}

🔲◼◾ 加载第一幅图标IDI_ICON2
因为在这之前使用的都是系统标准图标,所以将Loadlcon函数的第一个参数都设置为NULL,但这里需要使用自定义的图标,那么该函数的第一个参数应该设置为应用程序的当前实例句柄。AfxGetInstanceHandle函数可以获取应用程序当前的实例句柄。另外,Loadlcon函数的第二个参数需要的是图标的名称,或者是图标资源标识符字符串,这里只有图标资源的ID,所以必须通过MAKEINTRESOURCE宏将资源ID转换为相应的资源标识符字符串。这个宏的定义代码如下所示:

LPTSTR MAKEINTRESOURCE(WORD wInteger);

这个宏的返回值是一个字符串类型,也就是字符指针类型。

🔲◼◾ 加载第二幅图标IDI_ICON3

本例中利用另一种方法来获得应用程序当前的实例句柄。在MFC SDI应用程序中,有一个表示应用程序本身的类,本例中就是CStyleApp,它派生于CWinApp类。该类有一个数据成员:m_hInstance,标识了应用程序当前的实例。也就是说,如果能获取到应用程序的CWinApp对象,就可以利用这个对象来调用它的m_hInstance数据成员,从而得到应用程序当前的实例句柄

在CStyleApp的源文件中已经定义了一个CStyleApp类型的全局变量:theApp。可以利用这个全局对象来调用其内部的数据成员。但是在一个源文件中要想调用另一个源文件中定义的全局变量,必须在调用这个变量之前声明这个变量是在外部定义的,可以把它放到CMainFrame类的OnCreate函数定义之前,声明代码:

extern CStyleApp theApp;


🔲◼◾ 加载第三幅图标IDI_ICON4
再换一种方式来获取应用程序当前的实例句柄。MFC提供了一个全局函数:AfxGetApp,可以获得当前应用程序对象的指针。因为这个函数是全局函数,所以在应用程序的任意地方都可以调用它。在本程序中,利用AfxGetApp函数的返回值来访问应用程序的m_hInstance数据成员。

3.2 定时器的处理

✨1)设置间隔1000毫秒触发一次定时器消息,因此,在CMainFrame类的OnCreate函数中设置:

extern CStyleApp theApp;

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......

    //SetWindowLong(m_hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
	//获取窗口已有的类型
	LONG oldStyle=GetWindowLong(m_hWnd, GWL_STYLE);
	//设置窗口类型,将窗口最大化去掉
	SetWindowLong(m_hWnd, GWL_STYLE, oldStyle & ~WS_MAXIMIZEBOX);
    //加载后重新设置图标
	SetClassLong(m_hWnd, GCL_HICON, (LONG)LoadIcon(NULL, IDI_ERROR));

	//加载三个图标
	m_hIcons[0] = LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON2));
	m_hIcons[1] = LoadIcon(theApp.m_hInstance, MAKEINTRESOURCE(IDI_ICON3));
	m_hIcons[2] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON4));

    SetTimer(1, 1000, NULL);

	return 0;
}

✨✨2)然后为CMainFrame类添加定时器消息WM_TIMER的响应函数。

void CMainFrame::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值

	CFrameWndEx::OnTimer(nIDEvent);
}

在该响应函数中调用SetClassLong函数改变应用程序窗口的图标。

void CMainFrame::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	//用于标记当前的图标下标
	static int index = 0;
	//设置图标
	SetClassLong(m_hWnd, GCL_HICON, (LONG)m_hIcons[index]);
	//index+1,下次切换到下一个坐标
	index = ++index % 3;

	CFrameWndEx::OnTimer(nIDEvent);
}
  • 首先定义了一个图标索引变量:index,并将其初始化为0。
      本例是要循环显示三幅图标,因此index这个索引值就需要在0、1和2这三个值之间循环变化。并且因为程序每次发送定时器消息后就会调用OnTimer这个响应函数,因此应该把index变量定义为静态的。作为一个静态的局部变量,它将存放在程序的数据区中,而不是在栈中分配空间。当第一次调用OnTimer函数时,系统会在数据区中为index变量分配空间,并根据它的定义将其初始化为0。当以后再次调用OnTimer函数时,因为index变量的空间已经存在了,程序将直接引用该地址中已有的值。也可以将index设置为CMainFrame类的成员变量。

  • 然后调用SetClassLong函数改变窗口的图标。
      SetClassLong函数的第三个参数需要一个LONG型的值,而m_hIcons[index]是HICON类型,因此需要进行一个强制转换。

运行程序,将会发现程序初始启动时显示的是应用程序原来的图标,然后就会动态地依次循环显示自定义的三幅图标。

如果希望应用程序启动后不再显示原来的图标,而是直接显示自定义的图标,CMainFrame类的OnCreate函数在加载图标之后,就应该调用SetClassLong函数将应用程序的图标设置为自定义的第一幅图标。

extern CStyleApp theApp;

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......

    //SetWindowLong(m_hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
	//获取窗口已有的类型
	LONG oldStyle=GetWindowLong(m_hWnd, GWL_STYLE);
	//设置窗口类型,将窗口最大化去掉
	SetWindowLong(m_hWnd, GWL_STYLE, oldStyle & ~WS_MAXIMIZEBOX);
    //加载后重新设置图标
	SetClassLong(m_hWnd, GCL_HICON, (LONG)LoadIcon(NULL, IDI_ERROR));

	//加载三个图标
	m_hIcons[0] = LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON2));
	m_hIcons[1] = LoadIcon(theApp.m_hInstance, MAKEINTRESOURCE(IDI_ICON3));
	m_hIcons[2] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON4));
	
    SetClassLong(m_hWnd, GCL_HICON, (LONG)m_hIcons[0]);
    SetTimer(1, 1000, NULL);

	return 0;
}

因为自定义图标数组中的第一幅图标已经被设置为窗口初始的图标了,在程序运行后应该接着显示第二幅图标,所以在OnTimer函数中,应该将index变量的初始值修改为1。

void CMainFrame::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	//用于标记当前的图标下标
	static int index = 1;
	//设置图标
	SetClassLong(m_hWnd, GCL_HICON, (LONG)m_hIcons[index]);
	//index+1,下次切换到下一个坐标
	index = ++index % 3;

	CFrameWndEx::OnTimer(nIDEvent);
}

注意:
用做图标的文件必须是icon文件。普通图像文件可以经过icon转换软件更改成icon文件。否则在导入时会报错:无法加载文件。切记不可直接更改文件的后缀。

四、工具栏编程

首先将先前在CStyleView类的OnCreate函数中添加的设置窗口背景和光标的代码注释起来。工具栏是Windows应用程序中一个非常重要的图形界面元素,它提供了一组顺序排列的带有位图图标的按钮。工具栏是把常用的菜单命令集合起来,以按钮的形式提供给用户使用,目的是为了方便用户的操作。

在Style工程中,在资源视图选项卡的Toolbar文件夹下有一个工具栏资源:IDR_MAINFRAME,双击这个资源ID,即可在资源编辑窗口中打开工具栏资源。可以看到这是一些带有位图图标的按钮,用户通过这些位图就能大概知道每个按钮的功能。

为了查看工具栏上某个按钮的属性,可以在资源编辑窗口中,在该按钮上双击鼠标左键,即可弹出该按钮的属性对话框。

4.1 在工具栏中添加和删除按钮

✨1)下面要在Style程序已有的工具栏上再添加一个按钮,添加方法是单击工具栏上最后一个空白按钮,然后在按钮编辑窗口,利用工具面板上提供的绘图工具设计按钮的外观。将本例添加的按钮资源ID设置为IDM_TEST。


在CMainframe.cpp文件中找到oncreate函数,修改创建工具栏的代码内容:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   ......
   
	/*if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
		!m_wndToolBar.LoadToolBar(theApp.m_bHiColorIcons ? IDR_MAINFRAME_256 : IDR_MAINFRAME))
	{
		TRACE0("未能创建工具栏\n");
		return -1;      // 未能创建
	}*/

	if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
		!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
	{
		TRACE0("未能创建工具栏\n");
		return -1;      // 未能创建
	}
	
	...... 
}

因为Toolbar中有IDR_MAINFRAME_256存在:

✨✨2)在菜单资源的帮助子菜单下再添加一个菜单项,并将其ID设置为刚才新添加的那个按钮的ID: IDM_TEST,Caption设置为: Test。接着为该菜单项添加一个命令消息响应函数:OnTest,并在这个函数中添加下面这条代码:MessageBox (“test”);在这里插入图片描述

void CStyleView::OnTest()
{
	// TODO: 在此添加命令处理程序代码
	MessageBox(_T("Test"));
}

运行程序。因为它们的ID是一样的,因此当单击工具栏上的T按钮时,它仍然由OnTest响应函数进行响应。

✨✨✨3)在Style程序的工具栏上有些按钮之间有一条竖线,这称为“分隔符",主要是用来分隔按钮。

上面的工具栏上前三个按钮代表的是文件子菜单下的新建打开保存菜单命令。接下来的三个按钮分别代表的是编辑子菜单下的剪切复制粘贴菜单命令。这两组按钮之间添加了一条分隔符用以区分这两组按钮。为了在刚才新添加的按钮和已有的帮助按钮之间添加一条分隔符,可以在资源编辑窗口中,用鼠标把T按钮向右拖动一点距离后再松开鼠标。此时可以看到,在帮助按钮和T按钮之间就有了一点空隙:

显示运行的程序界面并未显示分割线。目前还不知道解决方案。待定。。。。。。。

✨✨✨✨ 4)如果想要删除工具栏上的某个按钮时,可能第一个想到的方法就是在资源编辑窗口中选中这个按钮,然后利用键盘上的Del键来删除它。但是这样做并不会删除按钮,而是将按钮上的图像删除了。要删除工具栏上的按钮,方法是在资源编辑窗口中,在此按钮上按下鼠标左键,然后将该按钮拖出工具栏,再松开鼠标左键,这样就可以把该按钮从工具栏上删除。

4.2 创建工具栏

在Style程序中,可以看到在CMainFrame类的头文件中定义了一个CToolBar类型的成员变量:m_wndToolBar。 CToolBar就是工具栏类,它的继承层次结构:

CToolBar类派生于CControlBar类,而后者又派生于CWnd类,因此工具栏也是一个窗口。

4.2.1 创建工具栏的方法

VC++提供了两种创建工具栏的方法。
✨1)第一种方法
步骤如下:
①创建工具栏资源
②构造CMFCToolBar对象
③调用Create或CreateEx函数创建Windows工具栏,并把它与已创建的CToolBar对象关联起来。
④调用LoadToolBar函数加载工具栏资源。

  • 其中,CToolBar类的Create成员函数的原型声明:

    BOOL Create(Cwnd* pParentWnd, DWORD dwStyle = WS_CHILD|WS_VISIBLE|CBRS_TOP, UINT nID = AFX_IDW_TOOLBAR);
    

    ◼ pParentWnd
    CWnd类型的指针,指定工具栏对象的父窗口。
    ◼ dwStyle
    指定工具栏的样式,例如工具栏是一个子窗口(WS_CHILD),工具栏是可视的(WS_VISIBLE)、工具栏停靠在框架窗口的顶部(CBRS_TOP)。
    ◼ nID
    指定工具栏子窗口的ID。

  • CToolBar类的CreateEx成员函数的原型:

    BOOL CreateEx(CWnd* pParentWnd, DWORD dwCtrlStyle= TBSTYLE_FLAT, DWORD dwStyle =WS_CHILD|WS_VISIBLE| CBRS_ALIGN_TOP,CRect rcBorders=CRect(0,0,0,0), UINT nID=AFX_IDW_TOOLBAR);
    

    ◼ pParentWnd
    CWnd类型的指针,指定工具栏对象的父窗口。
    ◼ dwCtrIStyle
    设置内嵌在工具栏上的CToolBarCtrl对象创建时的扩展风格,该参数默认值为:TBSTYLE_FLAT。
    ◼dwStyle
    与Create函数同名参数相同,用于指定工具栏的样式。
    ◼ rcBorders
    定义工具栏窗口边框的宽度。
    ◼nID
    与Create函数同名参数相同,用于指定工具栏子窗口的ID。

✨✨2)第二种方法
步骤如下:
①构造CToolBar对象;
②调用Create或CreateEx函数创建Windows工具栏,并把它与已创建的CToolBar对象关联起来;
③调用LoadBitmap函数加载包含工具栏按钮图像的位图;
在Style程序中看到的工具栏资源: IDR_MAINFRAME,在保存时是以一幅位图的形式保存的,该位图文件名称为Toolbar.bmp,位置是在当前工程所在目录下的res目录下,这幅位图含有很多小的图像,它们分别对应于工具栏上的各个按钮。
④调用SetButtons函数设置按钮样式,并把工具栏上的一个按钮与位图上的一个图像相关联。

4.2.2 MFC创建工具栏的过程

Style程序中,MFC AppWizard自动产生的与工具栏对象相关的代码存在于CMainFrame类的OnCreate函数中。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    ......
    
	/*if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
		!m_wndToolBar.LoadToolBar(theApp.m_bHiColorIcons ? IDR_MAINFRAME_256 : IDR_MAINFRAME))
	{
		TRACE0("未能创建工具栏\n");
		return -1;      // 未能创建
	}*/

	if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
		!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
	{
		TRACE0("未能创建工具栏\n");
		return -1;      // 未能创建
	}
	......

    // TODO: 如果您不希望工具栏和菜单栏可停靠,请删除这五行
	...
	m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
	EnableDocking(CBRS_ALIGN_ANY);
	...
	DockPane(&m_wndToolBar);
 
    ......
}
  1. 在CMainFrame类的OnCreate函数中,首先调用CreateEx函数创建程序的工具栏对象,接着调用LoadToolBar函数加载工具栏资源:IDR_MAINFRAME。

  2. 接着,CMainFrame类的OnCreate函数调用工具栏对象的EnableDocking成员函数设量工具栏停靠的位置,工具栏停靠位置的取值如表:

  3. 接下来,CMainFrame类的OnCreate函数又调用了一个EnableDocking函数。

    • 第一次调用的EnableDocking函数是工具栏对象的成员函数,目的是让工具栏对象可以停靠。
    • 第二次调用的是CFrameWnd对象的EnableDocking成员函数,目的是让主框架窗口可以被停靠。
  4. 最后,CMainFrame类的OnCreate函数调用DockPane函数,让工具栏停靠在主框架窗口上。

4.2.3 创建自定义的工具栏

按照第一种创建工具栏的方案,为Style程序创建一个自己的工具栏。

✨1)创建工具栏资源

方法一:右键程序,选择添加下的资源菜单。在弹出的插入资源对话框中选择Toolbar资源类型,然后单击新建按钮插入新的工具栏。
方法二:在工程的资源视图选项卡上,在Toolbar文件夹上单击鼠标右键,从弹出的快捷菜单中选择插入Toolbar菜单命令。

无论使用上述哪种方法,这时VC++都会在Style程序中插入一个名为IDR_TOOLBAR1的工具栏资源,并且该工具栏上只有一个空白的按钮。然后,就可以根据需要在该按钮上绘制图形。

本例在这个新工具栏上又添加了两个按钮,并分别为这三个按钮绘制了图形:

✨✨2)构造CMFCToolBar对象
为CMainFrame类添加一个CMFCToolBar类型的成员变量,可以直接在CMainFrame类的头文件中添加下面这条语句:CMFCToolBar m newToolBar;

private:
	HICON m_hIcons[3];
	CMFCToolBar m_newToolBar;

✨✨✨3)调用Create或CreateEx函数创建Windows工具栏,并把它与已创建的CToolBar对象关联起来。
在CMainFrame类的OnCreate函数中实现。
解决DockControlBar(&m_newToolBar)引起的问题

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    ......
    
	/*if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
		!m_wndToolBar.LoadToolBar(theApp.m_bHiColorIcons ? IDR_MAINFRAME_256 : IDR_MAINFRAME))
	{
		TRACE0("未能创建工具栏\n");
		return -1;      // 未能创建
	}*/
    .......

	//SetWindowLong(m_hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
	//获取窗口已有的类型
	LONG oldStyle=GetWindowLong(m_hWnd, GWL_STYLE);
	//设置窗口类型,将窗口最大化去掉
	SetWindowLong(m_hWnd, GWL_STYLE, oldStyle & ~WS_MAXIMIZEBOX);

	//设置程序图标为error
	SetClassLong(m_hWnd, GCL_HICON, (LONG)LoadIcon(NULL, IDI_ERROR));


	//加载三个图标
	m_hIcons[0] = LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON2));
	m_hIcons[1] = LoadIcon(theApp.m_hInstance, MAKEINTRESOURCE(IDI_ICON3));
	m_hIcons[2] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON4));

   //设置程序图标
	SetClassLong(m_hWnd, GCL_HICON, (LONG)m_hIcons[0]);
	SetTimer(1, 1000, NULL);

	if (!m_newToolBar.CreateEx(this,TBSTYLE_FLAT,WS_CHILD|WS_VISIBLE|
	CBRS_RIGHT|CBRS_GRIPPER|CBRS_TOOLTIPS|CBRS_FLYBY|CBRS_SIZE_DYNAMIC)
	||!m_newToolBar.LoadToolBar(IDR_TOOLBAR1))
	{
		TRACE0("Failed to create toolbar\n");
		return -1;
	}
	m_newToolBar.EnableDocking(CBRS_ALIGN_ANY);
	DockPane(&m_newToolBar);
	--
	return 0;
}
  • 首先调用CToolBar的成员函数:CreateEx创建工具栏,并与工具栏对象: m_newToolBar相关联,这里将新工具栏的停靠位置设置为CBRS_RIGHT。
  • 然后调用LoadToolBar函数加载刚刚新建的工具栏资源(IDR_TOOLBAR1)。
  • 接着调用工具栏对象的EnableDocking函数允许工具栏停靠于客户区的任意位置(CBRS_ALIGN_ANY),因为在OnCreate函数的前面已经调用了框架类的EnableDocking函数,让主框架窗口可以被停靠,所以这里就不需要再调用这个函数了。
  • 最后调用框架类的DockPane函数,让这个新工具栏停靠在主框架窗口上。

运行Style程序,可以看到在程序主框架窗口的右边多了一个工具栏,就是刚才新建的那个工具栏。程序还没有对该工具栏上的三个按钮进行命令消息响应,所以点击它们没有反应。

✨✨✨✨ 4)实现隐藏和显示功能。
首先为Style程序再添加一个菜单项,通过单击该菜单项来隐藏或显示刚才新建的那个工具栏。并且同样要为该菜单项设置一个复选标记,当这个新工具栏出现时,该菜单项带有这个复选标记;当这个新工具栏消失时,该菜单项没有这个复选标记。

1)本例是在Style程序的帮助子菜单下再添加一个菜单项,将其ID属性设置为IDM_VIEW_NEWTOOLBAR, Caption属性设置为:新的工具栏。

2)接着为CMainFrame类添加对这个菜单项的命令响应函数,并在此函数中实现先前新建的那个工具栏的显示和隐藏。工具栏本身也是一个窗口,可以调用Cwnd类的ShowWindow函数让其显示或隐藏

void CMainFrame::OnViewNewtoolbar()
{
	// TODO: 在此添加命令处理程序代码

	//判断工具栏是否当前可见
	if (m_newToolBar.IsWindowVisible()) {
		//若当前是可见状态,将其设置为隐藏状态
		m_newToolBar.ShowWindow(SW_HIDE);
	}
	else
	{
		//若当前是不可见状态,将其设置为可见状态
		m_newToolBar.ShowWindow(SW_SHOW);
	}
}

运行Style程序,当单击查看新的工具栏菜单命令后,可以发现窗口右边的那个新工具栏上的按钮消失了。但是工具栏还存在:

3.1)当窗口上的工具栏被隐藏或显示后,停靠在窗口上的其他控制条对象的位置可能会有所变动,这时需要调用框架类的RecalcLayout成员函数来重新调整它们的位置,这个函数的原型声明:

virtual void RecalcLayout(BOOL bNotify=TRUE );

该函数有一个BOOL类型的参数:bNotify,如果该参数为真,则框架窗口上活动的对象会收到布局变动的通知;如果为假,则不会收到。默认值是TRUE,因此在CMainFrame类的OnViewNewtoolbar函数的最后再添加RecalcLayout函数的调用。

void CMainFrame::OnViewNewtoolbar()
{
	// TODO: 在此添加命令处理程序代码

	//判断工具栏是否当前可见
	if (m_newToolBar.IsWindowVisible()) {
		//若当前是可见状态,将其设置为隐藏状态
		m_newToolBar.ShowWindow(SW_HIDE);
	}
	else
	{
		//若当前是不可见状态,将其设置为可见状态
		m_newToolBar.ShowWindow(SW_SHOW);
	}
	RecalcLayout();
}

3.2)把自定义的工具栏拖动到窗口中间的某个位置,也就是让它处于一种浮动的状态。然后再次单击【查看新的工具栏】菜单命令,会发现此时程序出现问题:这时这个工具栏上的按钮消失了,但工具栏仍存在。
在这里插入图片描述

当工具栏再次显示或隐藏后,需要再次调用框架类的DockPane函数,让工具栏停靠在主框架窗口上。因此为了解决上述问题,需要在CMainFrame类的OnViewNewtoolbar函数的最后再调用一次框架类的DockPane函数:

void CMainFrame::OnViewNewtoolbar()
{
	// TODO: 在此添加命令处理程序代码

	//判断工具栏是否当前可见
	if (m_newToolBar.IsWindowVisible()) {
		//若当前是可见状态,将其设置为隐藏状态
		m_newToolBar.ShowWindow(SW_HIDE);
	}
	else
	{
		//若当前是不可见状态,将其设置为可见状态
		m_newToolBar.ShowWindow(SW_SHOW);
	}
	RecalcLayout();
	DockPane(&m_newToolBar);
}

运行Style程序,并将自定义的工具栏拖动到窗口中间的某个位置,让它处于一种浮动的状态。然后单击查看新的工具栏菜单命令。这时会发现这个工具栏从窗口中消失了。当再次单击查看新的工具栏菜单命令后,会发现这个工具栏停靠在客户区。

3.3)让新建的工具栏在原先浮动的位置处显示出来,需要用到ShowPane函数,该函数的作用是隐藏或显示指定的控制条。该函数的原型声明:

void ShowPane(CBasePane* pBar, BOOL bShow, BOOL bDelay, BOOL bActivate);

◼pBar
指向将要显示或隐藏的控制条。
◼bShow
布尔类型,其值如果为TRUE,表示显示指定的控制条;如果为FALSE,表示隐藏指定的控制条。
◼bDelay
如果为TRUE,延迟显示控制条;如果为FALSE,立即显示控制条。
◼bActivate
如果为TRUE,则显示窗格为活动状态。

void CMainFrame::OnViewNewtoolbar()
{
	// TODO: 在此添加命令处理程序代码

	/*//判断工具栏是否当前可见
	if (m_newToolBar.IsWindowVisible()) {
		//若当前是可见状态,将其设置为隐藏状态
		m_newToolBar.ShowWindow(SW_HIDE);
	}
	else
	{
		//若当前是不可见状态,将其设置为可见状态
		m_newToolBar.ShowWindow(SW_SHOW);
	}
	RecalcLayout();
	DockPane(&m_newToolBar);
	*/
	//IsWindowVisible成员函数,获得工具栏当前的状态,若该函数的返回值为TRUE,则说明工具栏当前是显示状态,那么此时应该取反,隐藏该工具栏。
	ShowPane(&m_newToolBar, !m_newToolBar.IsWindowVisible(), FALSE,TRUE);
}


4)为新的工具栏菜单项设置复选标记,通过类向导为这个菜单项添加一个UPDATE_COMMMAND_UI消息响应函数。

void CMainFrame::OnUpdateViewNewtoolbar(CCmdUI* pCmdUI)
{
	// TODO: 在此添加命令更新用户界面处理程序代码
	//也是利用工具栏对象的1sWindowVisible函数的返回值来设置是否显示复选材记。
	//如果该函数返回TRUE, 说明当前工具栏是显示状态, 因此应该设置复选标记; 
	//如果该函数返回FALSE, 说明当前工具栏是隐藏状态, 因此不应该显示复选标记。
	pCmdUI->SetCheck(m_newToolBar.IsWindowVisible());
}

运行Style程序:

通过为菜单项添加UPDATE_COMMAND_UI消息的响应,可以非常方便地为菜单项设置或取消复选标记。

五、状态栏编程

应用程序的最下方就是状态栏。

状态栏可以分为两部分:

  • 左边最长的那部分称为提示行,当把鼠标移动到某个菜单项或工具按钮上时,该部分将显示相应的提示信息。
  • 状态栏的第二部分是其右边的三个窗格,主要用来显示Caps Lock、Num Lock和Scroll Lock键的状态,称为状态栏指示器。当在程序运行后按下Caps Lock键,程序状态栏上这三个窗格中左边第一个窗格将由灰变亮。

    在Style程序中,状态栏对象也是在CMainFrame类中定义的,在该类的头文件中有下面这行代码,其中CStatusBar类就是与状态栏相关的MFC类
    CStatusBar  m_wndStatusBar;
    

MainFrame类的OnCreate函数中可以看到:

// 允许用户定义的工具栏操作: 
InitUserToolbars(nullptr, uiFirstUserToolBarId, uiLastUserToolBarId);

if (!m_wndStatusBar.Create(this))
{
	TRACE0("未能创建状态栏\n");
	return -1;      // 未能创建
}
m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT));

这段代码首先调用状态栏对象的Create函数创建状态栏对象,这个函数的原型声明:

BOOL Create(Cwnd* pParentWnd, DWORD dwStyle = WS_CHILD|WS_VISIBLE|CBRS_BOTTOM,UINT nID= AFX_IDW_STATUS_BAR);

◼ pParentWnd
指定状态栏的父窗口指针,通常都是指向程序的框架窗口对象。
◼ dwStyle
第二个参数指定状态栏的风格,除了标准的Windows窗口风格外,该参数还可以取下表所列的值:
在这里插入图片描述
◼ nID
指定状态栏这个子窗口的ID,默认值:AFX_IDW_STATUS_BAR

在创建状态栏之后,接着调用SetIndicators函数设置状态栏指示器,其中用到一个数组参数: indicators,这个数组是在CMainFrame类源文件的前部定义的:

static UINT indicators[] =
{
	ID_SEPARATOR,           // 状态行指示器
	ID_INDICATOR_CAPS,
	ID_INDICATOR_NUM,
	ID_INDICATOR_SCRL,
};

此数组是静态变量,数组元素是一些ID:

  • 第一个ID表示状态栏左边窗格,即提示行;
  • 第二个ID表示Caps Lock;
  • 第三个ID表示Num Lock;
  • 第四个ID表示Scroll Lock;

SetIndicators函数第二个参数用于设定indicators数组中元素的个数。

实际上, indicators数组中后三个ID都是MFC预先定义好的字符串资源ID。Style工程的资源视图选项卡,双击String Table文件夹下的String Table项,即可打开Style程序的字符串资源,在其中可以找到这三个ID:
在这里插入图片描述


✨1)如果想要修改状态栏的外观,例如添加或减少状态栏上的窗格,则只需要在indicators数组中添加或减少相应的字符串资源ID即可。

本例想在Style程序的状态栏上显示当前系统的时间和一个进度条控件。因此首先需要为Style程序增加两个新的字符串资源:

  • IDS_TIMER : 时钟
  • IDS_PROGRESS :进度条


两个新的字符串资源ID添加到indicators数组中:

static UINT indicators[] =
{
	ID_SEPARATOR,           // 状态行指示器
	ID_INDICATOR_CAPS,
	ID_INDICATOR_NUM,
	ID_INDICATOR_SCRL,
	IDS_TIMER,
	IDS_PROGRESS,
};

运行程序:
✨✨2)接下来,在状态栏上显示“时钟”字符串的窗格上显示系统当前的时间。

  • 首先就需要获取到系统当前时间,这需要用到一个新的MFC类: CTime,该类有一个静态的成员函数: GetCurrentTime。该函数返回一个CTime类型的对象,表示系统当前的时间。

  • 然后,可以调用CTime类的另一个成员函数:Format,对得到的CTime类型的时间对象进行格式化,得到一个包含格式化时间的字符串。该函数常用的格式如表:

  • 为了将字符串显示到状态栏的窗格上,可以调用CStatusBar类的SetPaneText函数,该函数的原型

    BOOL SetPanerext(int nindex, LPCTSTR lpszNewText, BOOL bUpdate = TRUE );
    

    ◼ nIndex
    是窗格在指示器数组(indicators)中的索引。
    ◼ lpszNewText
    在窗格上显示的文本。
    ◼ bUpdate
    是BOOL类型,如果其值为TRUE,那么在设置窗格的文本之后,该窗格变成是无效的;当下次WM_PAINT消息发送后,该窗格将发生重绘,该参数的默认值是TRUE。

🟢🟢 2.1)为了在状态栏上显示“时钟”字符串的那个窗格上显示系统当前的时间,在Style程序的CMainFrame类的OnCreate函数中获取当前时间。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	if (CFrameWndEx::OnCreate(lpCreateStruct) == -1)
		return -1;

	BOOL bNameValid;

	if (!m_wndMenuBar.Create(this))
	{
		TRACE0("未能创建菜单栏\n");
		return -1;      // 未能创建
	}

	m_wndMenuBar.SetPaneStyle(m_wndMenuBar.GetPaneStyle() | CBRS_SIZE_DYNAMIC | CBRS_TOOLTIPS | CBRS_FLYBY);

	// 防止菜单栏在激活时获得焦点
	CMFCPopupMenu::SetForceMenuFocus(FALSE);

	/*if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
		!m_wndToolBar.LoadToolBar(theApp.m_bHiColorIcons ? IDR_MAINFRAME_256 : IDR_MAINFRAME))
	{
		TRACE0("未能创建工具栏\n");
		return -1;      // 未能创建
	}*/

	if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||
		!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
	{
		TRACE0("未能创建工具栏\n");
		return -1;      // 未能创建
	}

	CString strToolBarName;
	bNameValid = strToolBarName.LoadString(IDS_TOOLBAR_STANDARD);
	ASSERT(bNameValid);
	m_wndToolBar.SetWindowText(strToolBarName);

	CString strCustomize;
	bNameValid = strCustomize.LoadString(IDS_TOOLBAR_CUSTOMIZE);
	ASSERT(bNameValid);
	m_wndToolBar.EnableCustomizeButton(TRUE, ID_VIEW_CUSTOMIZE, strCustomize);

	// 允许用户定义的工具栏操作: 
	InitUserToolbars(nullptr, uiFirstUserToolBarId, uiLastUserToolBarId);

	if (!m_wndStatusBar.Create(this))
	{
		TRACE0("未能创建状态栏\n");
		return -1;      // 未能创建
	}
	m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT));

	// TODO: 如果您不希望工具栏和菜单栏可停靠,请删除这五行
	m_wndMenuBar.EnableDocking(CBRS_ALIGN_ANY);
	m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
	EnableDocking(CBRS_ALIGN_ANY);
	DockPane(&m_wndMenuBar);
	DockPane(&m_wndToolBar);


	// 启用 Visual Studio 2005 样式停靠窗口行为
	CDockingManager::SetDockingMode(DT_SMART);
	// 启用 Visual Studio 2005 样式停靠窗口自动隐藏行为
	EnableAutoHidePanes(CBRS_ALIGN_ANY);

	// 加载菜单项图像(不在任何标准工具栏上): 
	CMFCToolBar::AddToolBarForImageCollection(IDR_MENU_IMAGES, theApp.m_bHiColorIcons ? IDB_MENU_IMAGES_24 : 0);

	// 创建停靠窗口
	if (!CreateDockingWindows())
	{
		TRACE0("未能创建停靠窗口\n");
		return -1;
	}

	m_wndFileView.EnableDocking(CBRS_ALIGN_ANY);
	m_wndClassView.EnableDocking(CBRS_ALIGN_ANY);
	DockPane(&m_wndFileView);
	CDockablePane* pTabbedBar = nullptr;
	m_wndClassView.AttachToTabWnd(&m_wndFileView, DM_SHOW, TRUE, &pTabbedBar);
	m_wndOutput.EnableDocking(CBRS_ALIGN_ANY);
	DockPane(&m_wndOutput);
	m_wndProperties.EnableDocking(CBRS_ALIGN_ANY);
	DockPane(&m_wndProperties);

	// 基于持久值设置视觉管理器和样式
	OnApplicationLook(theApp.m_nAppLook);

	// 启用工具栏和停靠窗口菜单替换
	EnablePaneMenu(TRUE, ID_VIEW_CUSTOMIZE, strCustomize, ID_VIEW_TOOLBAR);

	// 启用快速(按住 Alt 拖动)工具栏自定义
	CMFCToolBar::EnableQuickCustomization();

	if (CMFCToolBar::GetUserImages() == nullptr)
	{
		// 加载用户定义的工具栏图像
		if (m_UserImages.Load(_T(".\\UserImages.bmp")))
		{
			CMFCToolBar::SetUserImages(&m_UserImages);
		}
	}

	// 启用菜单个性化(最近使用的命令)
	// TODO: 定义您自己的基本命令,确保每个下拉菜单至少有一个基本命令。
	CList<UINT, UINT> lstBasicCommands;

	lstBasicCommands.AddTail(ID_FILE_NEW);
	lstBasicCommands.AddTail(ID_FILE_OPEN);
	lstBasicCommands.AddTail(ID_FILE_SAVE);
	lstBasicCommands.AddTail(ID_FILE_PRINT);
	lstBasicCommands.AddTail(ID_APP_EXIT);
	lstBasicCommands.AddTail(ID_EDIT_CUT);
	lstBasicCommands.AddTail(ID_EDIT_PASTE);
	lstBasicCommands.AddTail(ID_EDIT_UNDO);
	lstBasicCommands.AddTail(ID_APP_ABOUT);
	lstBasicCommands.AddTail(ID_VIEW_STATUS_BAR);
	lstBasicCommands.AddTail(ID_VIEW_TOOLBAR);
	lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2003);
	lstBasicCommands.AddTail(ID_VIEW_APPLOOK_VS_2005);
	lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_BLUE);
	lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_SILVER);
	lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_BLACK);
	lstBasicCommands.AddTail(ID_VIEW_APPLOOK_OFF_2007_AQUA);
	lstBasicCommands.AddTail(ID_VIEW_APPLOOK_WINDOWS_7);
	lstBasicCommands.AddTail(ID_SORTING_SORTALPHABETIC);
	lstBasicCommands.AddTail(ID_SORTING_SORTBYTYPE);
	lstBasicCommands.AddTail(ID_SORTING_SORTBYACCESS);
	lstBasicCommands.AddTail(ID_SORTING_GROUPBYTYPE);

	CMFCToolBar::SetBasicCommands(lstBasicCommands);

	//SetWindowLong(m_hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
	//获取窗口已有的类型
	LONG oldStyle=GetWindowLong(m_hWnd, GWL_STYLE);
	//设置窗口类型,将窗口最大化去掉
	SetWindowLong(m_hWnd, GWL_STYLE, oldStyle & ~WS_MAXIMIZEBOX);

	//
	SetClassLong(m_hWnd, GCL_HICON, (LONG)LoadIcon(NULL, IDI_ERROR));


	//加载三个图标
	m_hIcons[0] = LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON2));
	m_hIcons[1] = LoadIcon(theApp.m_hInstance, MAKEINTRESOURCE(IDI_ICON3));
	m_hIcons[2] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON4));

	SetClassLong(m_hWnd, GCL_HICON, (LONG)m_hIcons[0]);
	SetTimer(1, 1000, NULL);


	if (!m_newToolBar.CreateEx(this,TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_RIGHT | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC)||
	    !m_newToolBar.LoadToolBar(IDR_TOOLBAR1))
	{
		TRACE0("Failed to create toolbar\n");
		return -1;
	}

	m_newToolBar.EnableDocking(CBRS_ALIGN_ANY);
    DockPane(&m_newToolBar);
	
	//获取当前时间
	CTime t = CTime::GetCurrentTime();
	//设置时间格式:hh:mm:ss
	CString str = t.Format("%H:%M:%S");
	//设置第4个窗格显示时间
	m_wndStatusBar.SetPaneText(4, str);

	return 0;
}

🔵🔵 2.2)获取索引方法二:如果在设置窗格的文本时不知道窗格的索引,此时可以利用CStatusBar类的另一个成员函数: CommandTolndex,通过指定资源ID来得到相应的索引.

   //获取当前时间
   CTime t = CTime::GetCurrentTime();
   //设置时间格式:hh:mm:ss
   CString str = t.Format("%H:%M:%S");
   int index = 0;
   index = m_wndStatusBar.CommandToIndex(IDS_TIMER);
   //设置第index个窗格显示时间
   m_wndStatusBar.SetPaneText(index, str);
   //m_wndStatusBar.SetPaneText(4, str);

运行程序,发现由于这个窗格的宽度太小,因此未能将时间字符串显示完整,只显示了小时和分钟,没有秒数。

🟡🟡 2.3)为了把时间字符串显示完整,需要把这个窗格的宽度再加大些。可以利用CStatusBar类的另一个成员函数: SetPaneInfo来实现。该函数可以为指定的窗格设置新的ID、样式和宽度。它的原型:

void SetPaneInfo(int nIndex, UINT nID, UINT nStyle, int cxWidth );

◼ nlndex
指定将要设置其样式的窗格索引。
◼nID
为指定窗格重新设置的新ID。
◼nStyle
指示窗格的样式,下表中列出了这个参数能够取的值及其意义。

◼ cxWidth
指定窗格新的宽度。

如果想让窗格把时间文本显示完整,那么首先就要获得这个文本在显示时占据的宽度,然后直接用这个宽度去修改窗格的宽度。

根据前面的知识,调用GetTextExtent函数来获得时间文本的宽度。

//获取当前时间
CTime t = CTime::GetCurrentTime();
//设置时间格式:hh:mm:ss
CString str = t.Format("%H:%M:%S");
//获得当前设备描述表
CClientDC dc(this);
//获取str的宽度
CSize sz=dc.GetTextExtent(str);
//获取IDS_TIMER在窗格上的索引
int index = 0;
index = m_wndStatusBar.CommandToIndex(IDS_TIMER);
//设置窗格的样式
m_wndStatusBar.SetPaneInfo(index, IDS_TIMER, SBPS_NORMAL, sz.cx);
//设置第index个窗格显示时间
m_wndStatusBar.SetPaneText(index, str);

运行如下,但是这个时间是一个静止的时间。

🟠🟠 2.4)实时地显示系统当前时间。
前面已经为Style程序设置了一个定时器,该定时器每隔1秒钟发送一次WM_TIMER定时器消息,那么就可以在该定时器消息响应函数中再次获得系统当前时间,并设置状态栏窗格的文本。

void CMainFrame::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	//用于标记当前的图标下标
	static int index = 0;
	//设置图标
	SetClassLong(m_hWnd, GCL_HICON, (LONG)m_hIcons[index]);
	//index+1,下次切换到下一个坐标
	index = ++index % 3;

	//获取当前时间
	CTime t = CTime::GetCurrentTime();
	//设置时间格式:hh:mm:ss
	CString str = t.Format("%H:%M:%S");
	//获得当前设备描述表
	CClientDC dc(this);
	//获取str的宽度
	CSize sz = dc.GetTextExtent(str);
	//获取IDS_TIMER在窗格上的索引
	int index = 0;
	index = m_wndStatusBar.CommandToIndex(IDS_TIMER);
	//设置窗格的样式
	m_wndStatusBar.SetPaneInfo(index, IDS_TIMER, SBPS_NORMAL, sz.cx);
	//设置第index个窗格显示时间
	m_wndStatusBar.SetPaneText(index, str);

	CFrameWndEx::OnTimer(nIDEvent);
}

运行Style,这时会看到状态栏上显示的时间实时地反映了系统当前时间,这个时间“动起来”了。

六、进度栏编程

平时在安装软件时通常都会看到有一个进度栏,用以指示当前的安装进度。在MFC中,进度栏也有一个相关的类: CProgressCtrl。该类的继承层次结构:
可以看到CProgressCtrl类派生Wnd类。因此它也是窗口类。如果要在程序中使用进度条,需要构造一个CProgressCtr对象,然后调用CProgressCtr类的Create函数创建进度栏控件。该函数原型:

BOOL Create (DWORD dwStyle,const RECT& rect,CWnd* pParentWnd,UINT nID);

◼ dwStyle
指定进度栏控件的类型。因为进度栏也是窗口,所以它具有窗口所具有的各种类型,同时它还有自己的类型: PBS_VERTICAL和PBS_SMOOTH。如果指定了前者,则进度栏将垂直显示;否则,将创建一个水平显示的进度栏。
◼ rect
指定进度栏控件的大小和位置。
◼pParentWnd
指定进度栏的父窗口。
◼nlD
指定进度栏控件的ID。

6.1 在窗口上创建进度栏

为了在Style程序的窗口中创建进度栏控件,首先需要在CMainFrame类的头文件中定义一个CProgressCtrl类型的成员变量:m_progress

📢📢📢 定义成员变量最简单便捷的方式:直接在头文件中定义。

在这里插入图片描述
1)创建水平状态栏。
在CMainFrame类的OnCreate函数中在窗口创建完成之后,在该函数返回之前创建进度栏控件。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	......
	
	//创建进度栏
	m_progress.Create(WS_CHILD | WS_VISIBLE, CRect(300, 300, 700, 350), this, 123);

	return 0;
}


利用CProgressCtrl类的SetPos成员函数可以设置进度栏上当前进度:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	......
	
	//创建进度栏
	m_progress.Create(WS_CHILD | WS_VISIBLE, CRect(300, 300, 700, 350), this, 123);//设置进度条
	m_progress.SetPos(50);

	return 0;
}


2)创建垂直的进度条。
创建一个垂直的进度栏,这时在创建进度栏时就要指定PBS_VERTICAL类型,同时应注意进度栏的高度值需要设置得大些,宽度值要小些。否则,不能给用户一种直观的感觉。

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	......
	
	//创建进度栏
	//m_progress.Create(WS_CHILD | WS_VISIBLE, CRect(300, 300, 700, 350), this, 123);
	//设置进度条
	//m_progress.SetPos(50);

	//创建垂直进度栏
	m_progress.Create(WS_CHILD | WS_VISIBLE | PBS_VERTICAL, CRect(700, 400, 750, 700), this, 123);
	m_progress.SetPos(50);

	return 0;
}

6.2 在状态栏的窗格上创建进度栏

要实现在程序状态栏的窗格中显示进度栏:

✨ 1) 首先需要获得该窗格的区域,然后将这个区域的大小作为进度栏的大小。为了获得窗格的区域,可以利用CStatusBar类的GetPaneInfo成员函数来完成。该函数的原型
cpp void GetItemRect(int nIndex, LPRECT lpRect) const;
◼ nIndex
指定窗格索引
◼ IpRect
指定用来接收指定索引的窗格的矩形区域。

 因此在Style程序的CMainFrame类的OnCreate函数中实现在状态栏的第三个窗格(即IDS_PROGRESS资源ID指定的窗格,其索引为5)上显示进度栏,此时状态栏对象将作为进度栏的父窗口。
int CMainF
rame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	......
	
	//获取窗格的宽度
	CRect rect;
	m_wndStatusBar.GetItemRect(5, &rect);
	//创建进度栏
	m_progress.Create(WS_CHILD | WS_VISIBLE, rect, &m_wndStatusBar, 123);
	//设置进度条
	m_progress.SetPos(50);

	//创建垂直进度栏
	//m_progress.Create(WS_CHILD | WS_VISIBLE | PBS_VERTICAL, CRect(700, 400, 750, 700), this, 123);
	//m_progress.SetPos(50);

	return 0;
}


这个进度栏还有一些缺陷,当Style程序窗口尺寸发生变化后,会发现进度栏显示的位置发生了错误:

✨✨ 2)发生这种情况主要是因为当程序窗口尺寸区域发生变化时,窗口上的状态栏的窗格尺寸区域也会随之变化,因此最早获得的窗格区域就不正确了,所以就看到进度栏与窗格发生了脱离。

为了改正这种情况,需要在程序窗口尺寸区域发生变化时,重新去获取索引为5的状态栏窗格区域,然后将进度栏移动到这个区域中。当窗口尺寸发生变化时,窗口会发生一个重绘,于是会发生一个WM_PAINT消息。因此,在响应这个消息的函数中,重新去获取索引值为5的状态栏窗格区域,然后将进度栏移动到这个区域中。

为CMainFrame类添加WM-PAINT消息的响应函数:

注释掉OnCreate函数中创建进度栏的代码,移到OnPaint函数中。

void CMainFrame::OnPaint()
{
	CPaintDC dc(this); // device context for painting
					   // TODO: 在此处添加消息处理程序代码
					   // 不为绘图消息调用 CFrameWndEx::OnPaint()

	CRect rect;
	m_wndStatusBar.GetItemRect(5, &rect);

	//创建进度栏
	m_progress.Create(WS_CHILD | WS_VISIBLE, rect, &m_wndStatusBar, 123);
	//设置进度条
	m_progress.SetPos(50);
}

运行一下此时的Style程序,会发现程序初始显示时进度栏显示的位置是正确的,但当程序窗口尺寸发生变化后,程序会弹出一个非法操作提示对话框。

这个问题的发生实际上与前面介绍动态按钮的创建时发生的错误是一样的,当程序窗口尺寸发生变化时,会发送一个WM_PAINT消息对这个窗口进行重绘。于是程序就会调用OnPaint这个消息响应函数。但是,这时程序已经创建了一个进度栏,并与m_progress对象相关联。这里再一次创建进度栏对象并进行关联,就会出错。

📋📋 解决:在程序中需要进行一个判断,如果进度栏还没有被创建,就创建它;如果已经创建了,就把进度栏移动到目标矩形区域中。为了移动一个窗口,可以调用CWnd类的成员函数:MoveWindow实现。

至于实现这种判断的方法最简单的方法:直接判断m_progress对象的窗口句柄,如果该句柄没有值,即为NULL时,就说明该对象还没有被创建,于是就创建进度栏,并与m_progress对象相关联;否则,就应该把进度栏移动到目标矩形区域中。

void CMainFrame::OnPaint()
{
	CPaintDC dc(this); // device context for painting
					   // TODO: 在此处添加消息处理程序代码
					   // 不为绘图消息调用 CFrameWndEx::OnPaint()

	CRect rect;
	m_wndStatusBar.GetItemRect(5, &rect);
	if (!m_progress.m_hWnd) {
		//创建进度栏
		m_progress.Create(WS_CHILD | WS_VISIBLE, rect, &m_wndStatusBar, 123);
	}
	else
	{
		m_progress.MoveWindow(rect);
	}
	//设置进度条
	m_progress.SetPos(50);
}

运行Style程序:
在这里插入图片描述

七、在状态栏上显示鼠标当前位置

要实现这样的功能:当在Style程序窗口中移动鼠标时,把鼠标当前的坐标显示在状态栏的第一个窗格上。

为了完成这个功能,首先就要捕获鼠标移动消息。视类窗口始终覆盖在框架窗口之上,所以如果想要捕获与鼠标相关的消息,应该在视类中完成。

✨ 1)需要为CStyleView类添加WM_MOUSEMOVE消息的响应函数:

在视类源文件中包含框架类的头文件:

// StyleView.cpp: CStyleView 类的实现
//

#include "pch.h"
#include "framework.h"
// SHARED_HANDLERS 可以在实现预览、缩略图和搜索筛选器句柄的
// ATL 项目中进行定义,并允许与该项目共享文档代码。
#ifndef SHARED_HANDLERS
#include "Style.h"
#endif

#include "StyleDoc.h"
#include "StyleView.h"
#include "MainFrm.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

因为视类需要访问框架类的m_wndStatusBar成员变量,所以需要将m_wndStatusBar变量的访问权限设置为public类型。

在程序状态栏的第一个窗格上显示鼠标当前位置。先得到状态栏对象,再把鼠标当前位置信息显示在第一个窗格上了。
⭕⭕ 第一种方法
调用SetWindowText函数设置状态栏窗口文本,就可以把鼠标当前位置信息设置到状态栏的第一个窗格上。

void CStyleView::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CString str;
	str.Format(_T("x=%d,y=%d"), point.x, point.y);
	((CMainFrame*)GetParent())->m_wndStatusBar.SetWindowTextW(str);


	CView::OnMouseMove(nFlags, point);
}

⭕⭕ 第二种方法
利用CFrameWnd类的成员函数:SetMessageText实现。该函数的作用是在ID为0值的状态栏窗格上(通常就是指状态栏上最左边的那个最长的窗格)设置一个字符串。该函数的一种原型:

void SetMessageText( LPCTSTR IpszText );

IpszText为用来设置的字符串。此函数是CFrameWnd类的成员函数,并且该函数直接在状态栏的第一个窗格上放置文本,这样在程序中不需要先获得状态栏对象,然后再进行设置文本的处理。CMainFrame类派生于CFrameWnd类,因此它继承了SetMessageText函数。

于是首先需要调用GetParent函数得到视类的父对象,即CMainFrame对象;然后调用该对象的SetMessageText函数设置状态栏文本。

void CStyleView::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CString str;
	str.Format(_T("x=%d,y=%d"), point.x, point.y);
	((CMainFrame*)GetParent())->SetMessageText(str);


	CView::OnMouseMove(nFlags, point);
}

⭕⭕ 第三种方法
CFrameWnd类的成员函数:GetMessageBar可以返回状态栏对象的指针,因此就不需要再访问CMainFrame类的保护成员变量:m_wndStatusBar。因此也就不需要修改m_wndStatusBar的访问权限类型。

有了状态栏对象的指针,就可以调用SetWindowText函数设置状态栏第一个窗格的文本。

void CStyleView::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CString str;
	str.Format(_T("x=%d,y=%d"), point.x, point.y);
	((CMainFrame*)GetParent())->GetMessageBar()->SetWindowText(str);

	CView::OnMouseMove(nFlags, point);
}

⭕⭕ 第四种方法
利用CWnd类的成员函数:GetDescendantWindow获得程序状态栏对象的指针。这个函数的功能是通过指定的ID来获得子孙窗口。这个函数将搜索当前窗口的整个子窗口树,不仅仅搜索当前窗口的直接子窗口。该函数的原型:

CWnd* GetDescendantWindow(int nID, BOOL bOnlyPerm=FALSE) const;

只要给定状态栏窗口的ID,此函数就可以找到指向状态栏窗口的指针。

注意: 不应该在视类中直接调用这个函数,因为状态栏不属于视类窗口,它属于框架类窗口。所以在调用时应该首先得到框架窗口的指针,然后调用框架窗口的GetDescendantWindow函数,以搜寻框架窗口的子孙窗口,从而得到状态栏窗口的指针。

在状态栏章节中说过,状态栏子窗口的ID是AFX_IDW_STATUS_BAR,利用这个ID调用GetDescendantWindow函数就可以找到指向主框架窗口所拥有的状态栏子窗口的指针。

void CStyleView::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	CString str;
	str.Format(_T("x=%d,y=%d"), point.x, point.y);
	GetParent()->GetDescendantWindow(AFX_IDW_STATUS_BAR)->SetWindowText(str);

	CView::OnMouseMove(nFlags, point);
}

运行Style程序,当在程序窗口中移动鼠标时,在程序状态栏的第一个窗格上就显示了当前鼠标的坐标信息:

八、启动画面

经常看到有些软件在启动时会有一幅启动画面,例如Word。在编写共享软件时,可能也需要让软件带有一个启动画面,上面提供一些版权信息,以及个人信息,例如个人网站和E-mail信息等。利用MFC编程时,让程序带上一个启动画面是非常简单的,利用VC++组件库中提供的一个类就可以完成。

1)利用类向导,创建一个MFC类:CSplashWnd,基类:CWnd。

CSplashWnd.h:

#pragma once
// CSplashWnd

class CSplashWnd : public CWnd
{
	DECLARE_DYNAMIC(CSplashWnd)

public:
	CSplashWnd();
	virtual ~CSplashWnd();

protected:
	DECLARE_MESSAGE_MAP()
};

CSplashWnd.cpp:

// CSplashWnd.cpp: 实现文件
//

#include "pch.h"
#include "Style.h"
#include "CSplashWnd.h"


// CSplashWnd

IMPLEMENT_DYNAMIC(CSplashWnd, CWnd)

CSplashWnd::CSplashWnd()
{

}

CSplashWnd::~CSplashWnd()
{
}


BEGIN_MESSAGE_MAP(CSplashWnd, CWnd)
END_MESSAGE_MAP()



// CSplashWnd 消息处理程序

2)创建一个位图资源,设置ID为:IDB_SPLASH。


制作的bmp图:

3)CSplashWnd的头文件中添加两个成员变量:

protected:
	CBitmap m_bitmap;//初始画面位图
public:
	static CSplashWnd* m_pSplashWnd;//指向初始画面的指针

由于m_pSplashWnd为静态成员变量,应在源文件(.cpp)开头说明:

4)采用类向导向CSplashWnd类加入一个静态公有成员函数ShowSplashScreen,此函数将被主框架窗口调用,显示启动窗口:

生成的函数:


实现代码:

void CSplashWnd::ShowSplashScreen(CWnd* pParentWnd)
{
	// TODO: 在此处添加实现代码.
	if (m_pSplashWnd != NULL){
		return;
	}
	//创建一个启动页类
	m_pSplashWnd = new CSplashWnd;
	if (!m_pSplashWnd->Create(pParentWnd)){
	   delete m_pSplashWnd;
	}
	else
	{
	   m_pSplashWnd->UpdateWindow();
    }
}

5)采用类向导向CSplashWnd类加入一个保护成员函数Create函数,此函数。。。。

BOOL CSplashWnd::Create(CWnd* pParentWnd)
{
	// TODO: 在此处添加实现代码.
	//判断载入位图是否成功   
	if (!m_bitmap.LoadBitmap(IDB_SPLASH))
	   return FALSE;
	BITMAP bm;
	m_bitmap.GetBitmap(&bm);
	//创建主框架窗口的子窗口
	BOOL flag=CreateEx(0, AfxRegisterWndClass(0, AfxGetApp()->LoadStandardCursor(IDC_ARROW)),
		               NULL, WS_POPUP | WS_VISIBLE, 0, 0, bm.bmWidth, bm.bmHeight, 
		               pParentWnd->GetSafeHwnd(), NULL);
	return flag;
}

6)调用OnCreate函数进行启动窗口的初始化。添加WM_CREAT的消息处理函数:
这个画面显示的时间非常短,希望它能够多显示一会儿,以便用户能看清楚上面的信息。在CSplashWnd类的OnCreate函数中,可以设置一个定时器,设置的定时器间隔就是启动画面显示的时间:

int CSplashWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	if (CWnd::OnCreate(lpCreateStruct) == -1)
		return -1;

	// TODO:  在此添加您专用的创建代码
	CenterWindow();
	SetTimer(1, 7000, NULL); //时间控制

	return 0;
}

7)重绘窗口,添加WM_PAINT的消息处理函数:

void CSplashWnd::OnPaint()
{
	CPaintDC dc(this); // device context for painting
					   // TODO: 在此处添加消息处理程序代码
					   // 不为绘图消息调用 CWnd::OnPaint()
	CDC dcImage;
	if (!dcImage.CreateCompatibleDC(&dc)) return;
	BITMAP bm;
	m_bitmap.GetBitmap(&bm);
	CBitmap* pOldBitmap = dcImage.SelectObject(&m_bitmap);
	dc.BitBlt(0, 0, bm.bmWidth, bm.bmHeight, &dcImage, 0, 0, SRCCOPY);
	dcImage.SelectObject(pOldBitmap);
}

8)添加WM_TIMER的消息处理函数,从而在一定时间后销毁启动窗口。启动画面窗口创建7秒之后才发送WM_TIMER消息,然后才能销毁启动窗口:

void CSplashWnd::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	DestroyWindow();
	AfxGetMainWnd()->UpdateWindow();
}

9)为防止内存溢出,窗口销毁后要释放CSplashWnd对象,重载虚函数PostNcDestroy,此函数在窗口销毁后调用:

void CSplashWnd::PostNcDestroy()
{
	// TODO: 在此添加专用代码和/或调用基类
	DestroyWindow();

	CWnd::PostNcDestroy();
}

释放CSplashWnd对象的同时,释放指向初始画面的指针,使其指向为NULL。

CSplashWnd::~CSplashWnd()
{
	ASSERT(m_pSplashWnd == this);
	m_pSplashWnd = NULL;
}

10)在主框架窗口的OnCreate函数最后调用ShowSplashScreen函数,并将CSplashWnd的头文件包含进去。若想让启动画面结束后再弹出主窗口,用Sleep函数设置等待时长:

#include "CSplashWnd.h"
       ......

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	CSplashWnd::ShowSplashScreen(this);
       ......
	return 0;
}	

最终生成的代码:

#pragma once


// CSplashWnd

class CSplashWnd : public CWnd
{
	DECLARE_DYNAMIC(CSplashWnd)

public:
	CSplashWnd();
	virtual ~CSplashWnd();

protected:
	afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
	afx_msg void OnPaint();
	afx_msg void OnTimer(UINT_PTR nIDEvent);
	virtual void PostNcDestroy();
	DECLARE_MESSAGE_MAP()

protected:
	CBitmap m_bitmap;//初始画面位图
public:
	static CSplashWnd* m_pSplashWnd;//指向初始画面的指针
	static void ShowSplashScreen(CWnd* pParentWnd);
	BOOL Create(CWnd* pParentWnd);
	
};
// CSplashWnd.cpp: 实现文件
//

#include "pch.h"
#include "Style.h"
#include "CSplashWnd.h"


// CSplashWnd

CSplashWnd* CSplashWnd::m_pSplashWnd;

IMPLEMENT_DYNAMIC(CSplashWnd, CWnd)
CSplashWnd::CSplashWnd(){
}

CSplashWnd::~CSplashWnd()
{
	ASSERT(m_pSplashWnd == this);
	m_pSplashWnd = NULL;
}


BEGIN_MESSAGE_MAP(CSplashWnd, CWnd)	
	ON_WM_CREATE()
	ON_WM_PAINT()
	ON_WM_TIMER()
END_MESSAGE_MAP()




// CSplashWnd 消息处理程序
void CSplashWnd::ShowSplashScreen(CWnd* pParentWnd)
{
	// TODO: 在此处添加实现代码.
	if (m_pSplashWnd != NULL){
		return;
	}
	//创建一个启动页类
	m_pSplashWnd = new CSplashWnd;
	if (!m_pSplashWnd->Create(pParentWnd)){
	   delete m_pSplashWnd;
	}
	else
	{
	   m_pSplashWnd->UpdateWindow();
    }
}


BOOL CSplashWnd::Create(CWnd* pParentWnd)
{
	// TODO: 在此处添加实现代码.
	//判断载入位图是否成功   
	if (!m_bitmap.LoadBitmap(IDB_SPLASH))
	   return FALSE;
	BITMAP bm;
	m_bitmap.GetBitmap(&bm);
	//创建主框架窗口的子窗口
	BOOL flag=CreateEx(0, AfxRegisterWndClass(0, AfxGetApp()->LoadStandardCursor(IDC_ARROW)),
		               NULL, WS_POPUP | WS_VISIBLE, 0, 0, bm.bmWidth, bm.bmHeight, 
		               pParentWnd->GetSafeHwnd(), NULL);
	return flag;
}


int CSplashWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	if (CWnd::OnCreate(lpCreateStruct) == -1)
		return -1;

	// TODO:  在此添加您专用的创建代码
	CenterWindow();
	SetTimer(1, 7000, NULL); //时间控制


	return 0;
}

void CSplashWnd::OnPaint()
{
	CPaintDC dc(this); // device context for painting
					   // TODO: 在此处添加消息处理程序代码
					   // 不为绘图消息调用 CWnd::OnPaint()
	CDC dcImage;
	if (!dcImage.CreateCompatibleDC(&dc)) return;
	BITMAP bm;
	m_bitmap.GetBitmap(&bm);
	CBitmap* pOldBitmap = dcImage.SelectObject(&m_bitmap);
	dc.BitBlt(0, 0, bm.bmWidth, bm.bmHeight, &dcImage, 0, 0, SRCCOPY);
	dcImage.SelectObject(pOldBitmap);
}

void CSplashWnd::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	DestroyWindow();
	AfxGetMainWnd()->UpdateWindow();
}

void CSplashWnd::PostNcDestroy()
{
	// TODO: 在此添加专用代码和/或调用基类
	DestroyWindow();
	
	CWnd::PostNcDestroy();
}

运行:
在这里插入图片描述

  • 3
    点赞
  • 0
    评论
  • 38
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 博客之星2020 设计师:CY__ 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值