MFC学习笔记之程序外观相关操作

如果想在应用程序窗口创建之前修改它的外观和大小的话,就要在CMainFrame类的PreCreateWindow成员函数中修改。该函数的参数是一个CREATETRUCT结构体的引用,该结构体的成员可以在MSDN中查到,是关于窗口大小、类名之类的一些参数。单文档应用程序它的框架的默认窗口样式是 WS_OVERLAPPEDWINDOW 和 FWS_ADDTOTITLE 样式的组合。 其中 FWS_ADDTOTITLE 是MFC特定的一种样式,指示框架将文档标题添加到窗口标题上。因此,如果想要让窗口显示自己的标题,只需要将这个FWS_ADDTOTITLE去掉就可以了。

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.style &= ~FWS_ADDTOTITLE;
	cs.lpszName = "http://www.sunxin.org";
	
	return TRUE;
}

我们知道,应用程序包括两个窗口:应用程序框架窗口和视类窗口,前者包含了后者,后者覆盖在前者上面,所以要修改应用程序的图标的话,可以在框架类中进行,要修改窗口背景还有鼠标光标的话就应该在视图类中进行。

MFC提供了一个全局函数AfxRegisterWndClass,用来设定窗口的类型,图标,光标和背景。函数原型如下:

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

该函数后面三个参数都有默认值,该函数的返回值是注册之后的类名,可以直接将这个返回值作为程序随后创建窗口所依赖的类。例如要在修改窗口的图标,可以在CMainFrame中的PreCreateWindow的return之前添加下面的这一句:

cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW,0,0,LoadIcon(NULL,IDI_WARNING));

在视图类中修改程序的光标和背景,可以在视图类的PreCreateWindow中添加下面的代码:

	cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW,
					LoadCursor(NULL,IDC_CROSS),(HBRUSH)GetStockObject(BLACK_BRUSH),0);

对于AfxRegisterWndClass来说,它的后面三个参数默认的光标是箭头,图标是WIndows 的LOGO ,背景是空白画刷,就是使得窗口透明。

 

在窗口创建之后再去修改窗口的图标背景光标的话,就可以再框架类和视图类中分别使用API全局函数:SetClassLong来实现,该函数可以重置指定窗口所属窗口类的WNDCLASSSEX结构体中的指定数据成员的属性,原型如下:

DWORD SetClassLong(
  HWND hWnd,       // handle to window
  int nIndex,      // index of value to change
  LONG dwNewLong   // new value
);

若是要修改图标的话就可以在CMainFrame类的OnCreate函数中加入下面这句:

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

如果要让窗口上的图标会想动画那样一直切换,可以通过加载几个不同的图标进资源,然后再通过定时器来实现动画效果,比如现在加载三个图标资源分别是IDI_ICON1,IDI_ICON2,IDI_ICON3,现在CMainFrame类中定义图标句柄数组,用来保存这三个图标的句柄,然后在OnCreate函数中加载这个三个图标

	m_hIcons[0]=LoadIcon(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDI_ICON1));
	m_hIcons[1]=LoadIcon(theApp.m_hInstance,MAKEINTRESOURCE(IDI_ICON2));
	m_hIcons[2]=LoadIcon(AfxGetApp()->m_hInstance,MAKEINTRESOURCE(IDI_ICON3));

使用系统图标的话,LoadIcon的第一个参数是NULL,现在使用的是自定义的图标,那么第一个参数就应该是设置为当前应用程序的实例句柄。第二个参数需要的是图标的名称,或者是图标资源的标识符字符串,而我只有ID号,所以要通过MAKEINTERSOURCE宏来将资源ID号转为相应的资源标识符字符串。

LPTSTR MAKEINTRESOURCE( WORD wInteger; );

该宏的返回值是一个字符指针类型。

HICON LoadIcon(
HINSTANCE hInstance, 
LPCTSTR lpIconName);

和LoadIcon的第二个参数的类型是符合的。

这里也给出了三种获取应用程序实例句柄的方法,自己觉得最好的是用第二种方式,但是记住theApp是在应用程序类中定义的全局对象,现在我们是在CMainFrame类中对它进行操作,我们知道,在一个源文件中想调用另一个源文件定义的全局变量,必须在调用这个变量之前声明这个变量的类型是外部定义的,就是在CMainFrame类的OnCreate函数之前加上这样一句:

extern CStyleApp theApp;

图标加载完成之后就要设定定时器来进行图标的切换了,设定一秒切换一次的话,在WM_TIMER的响应函数中加入下面的代码就可以实现三个图标的切换了:

	static int index=0;
	SetClassLong(m_hWnd,GCL_HICON,(LONG)m_hIcons[index]);
	index=++index%3;

SetClassLong的第三个参数是LONG类型的,这里需要一个强制转换。

	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("Failed to create toolbar\n");
		return -1;      // fail to create
	}

	m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
	EnableDocking(CBRS_ALIGN_ANY);
	DockControlBar(&m_wndToolBar);

在CMainFrame的OnCreate函数中有上面几句代码,这几句代码完成的是工具栏的创建,m_wndToolBar是CToolBar类型的变量,CToolBar类派生与CControlBar类,而后者又派生于CWnd类,所以工具栏也是一个窗口。

 MFC为我们创建的工具栏和主菜单资源ID是一样的,所以,在MFC编程中,一个ID可以表示多种资源 。

MFC先用CreateEx函数创建程序的工具栏对象,然后调用LoadIcon还是加载工具栏资源。

接着调用工具栏对象的EnableDocking成员函数设置工具栏停靠的位置

void EnableDocking( 
DWORD dwStyle ); 
Parameters
dwStyle 
Specifies whether the control bar supports docking and the sides of its parent window to which the control bar can be docked, if supported. Can be one or more of the following flags: 
CBRS_ALIGN_TOP   Allows docking at the top of the client area. 
CBRS_ALIGN_BOTTOM   Allows docking at the bottom of the client area. 
CBRS_ALIGN_LEFT   Allows docking on the left side of the client area. 
CBRS_ALIGN_RIGHT   Allows docking on the right side of the client area. 
CBRS_ALIGN_ANY   Allows docking on any side of the client area. 
CBRS_FLOAT_MULTI   Allows multiple control bars to be floated in a single mini-frame window. 
If zero, that is, indicating no flags, the control bar does not dock. 

接下来,又调用了一次EnableDocking函数,不过这次调用的是CFrameWnd对象的EnableDocking函数目的是让主框架闯进欧可以停靠。
自定义工具栏的话 就是先载入工具栏资源,然后声明一个工具栏对象,然后就是和上面的创建过程一样就可以了 当然最后不用再调用一次CFrameWnd类的

EnableDocking(CBRS_ALIGN_ANY);函数。

状态栏可以分为两个部分,左边最长的部分称为提示行,当我们把鼠标移动到某个菜单项或工具按钮上的时候,该部分将显示相应的提示信息。第二部分是右边的三个窗格,主要用来显示 Caps Lock,Num LOck和Scroll Lock键的状态,称为状态栏指示器。

	if (!m_wndStatusBar.Create(this) ||
		!m_wndStatusBar.SetIndicators(indicators,
		  sizeof(indicators)/sizeof(UINT)))
	{
		TRACE0("Failed to create status bar\n");
		return -1;      // fail to create
	}

在OnCreate函数中看到这样一段创建状态栏的代码,首先调用了Create函数来创建状态栏对象,接着是调用SetIndicators函数设置状态栏指示器,其中一个参数是indicators,这是一个数组,定义的代码如下:

static UINT indicators[] =
{
	ID_SEPARATOR,           // status line indicator
	ID_INDICATOR_CAPS,
	ID_INDICATOR_NUM,
	ID_INDICATOR_SCRL,
};

这个数组是一个静态变量,数组元素是一些ID,第一个是提示行,后面三个是状态指示器的三个ID。

后面的三个ID是MFC为我们定义好的字符串资源ID我们可以在String Table中找到他们的定义。要修改状态栏的外观,比如添加或减少状态栏上的窗格,则只需要在indicators数组中添加或减少相应的字符串资源ID即可,例如现在要增加一个显示当前系统时间和进度条控件,因此要先增加两个新的字符串资源,其ID及Caption如下:

IDS_TIMER 时钟 IDS_PROGRESS 进度栏 ,然后将两个新的字符串资源ID添加到indicators数组中,如下所示:

static UINT indicators[] =
{
	ID_SEPARATOR,           // status line indicator
	IDS_TIMER,
	IDS_PROGRESS,
	ID_INDICATOR_CAPS,
	ID_INDICATOR_NUM,
	ID_INDICATOR_SCRL,
};

这样我们运行后就会看到状态烂多了两个窗格了。

接下来要显示系统时间的话,就要利用CTime类了,在OnCreate啊哈念书加入下面的代码:

	CTime t=CTime::GetCurrentTime();
	CString str=t.Format("%H:%M:%S");
	m_wndStatusBar.SetPaneText(1,str);

要将字符串显示到状态栏的窗格上,可以调用CStatusBar类的SetPaneText函数,函数原型如下:

BOOL SetPaneText( int nIndex, LPCTSTR lpszNewText, BOOL bUpdate = TRUE );

第一个参数是窗格在指示器数组(indicators)中的索引,由上面的定义可知这里的索引是1.第三个参数不去改它。

如果不知道窗体的索引,那么就可以利用CStatusBar的另一个成员函数:CommandToIndex,通过将指定资源ID来得到相应的索引。

显示时间由于窗格的宽度不够,因而时间字符串没办法显示完整,为了显示完整,需要将它的宽度加大一些。可以调用CStatusBar函数的SetPanInfo来实现。

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

参数的相关意义可以在MSDN中找到,所以加上这些后,显示时间窗格的代码就为:

	int index=0;
	index=m_wndStatusBar.CommandToIndex(IDS_TIMER);
	m_wndStatusBar.SetPaneInfo(index,IDS_TIMER,SBPS_NORMAL,sz.cx);
	m_wndStatusBar.SetPaneText(index,str);

虽然时间可以完整显示了,但是这是一个静止的时间,我们希望它可以动起来的话,就需要将实现的获取和显示的函数放在先前写的相应定时器消息的函数里面:

void CMainFrame::OnTimer(UINT nIDEvent) 
{
	// TODO: Add your message handler code here and/or call default
	static int index=0;
	SetClassLong(m_hWnd,GCL_HICON,(LONG)m_hIcons[index]);
	index=++index%3;
	
	CTime t=CTime::GetCurrentTime();
	CString str=t.Format("%H:%M:%S");
	CClientDC dc(this);
	CSize sz=dc.GetTextExtent(str);
	m_wndStatusBar.SetPaneInfo(1,IDS_TIMER,SBPS_NORMAL,sz.cx);
	m_wndStatusBar.SetPaneText(1,str);

	m_progress.StepIt();

	CFrameWnd::OnTimer(nIDEvent);
}

接下来弄个进度栏,在MFC中进度栏的类是:CprogressCtrl 该类派生于CWnd类。要创建一个进度栏,需要先构造一个CProgressCtrl类的对象,然后调用CProgressCtrl类的Create函数来创建进度栏控件。

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

该函数第一个参数是进度栏类型,因为进度栏也是一个窗口,所以具有窗口的所有类型,同时也具有自己的类型:PBS_VERTICAL 和 PBS_SMOOTH 前者是进度栏垂直显示。 利用 CProgressCtrl类的SetPos成员函数可以设置进度栏上当前的进度。

	m_progress.Create(WS_CHILD | WS_VISIBLE | PBS_SMOOTH,
			CRect(100,100,200,120),this,123);
	m_progress.SetPos(50);

在CMainFrame的OnCreate函数中加入上面的代码就可以创建一个进度栏了,并且有一半的进度。其中PBS_SMOOTH属性是使得平滑。

要在程序状态栏的窗格中显示进度栏,首先需要获得该窗格的区域,然后将这个区域的大小作为进度栏的大小,可以用CStatusBar类的GetItemRect函数来完成。

void GetItemRect( int nIndex, LPRECT lpRect ) const;

第一个参数是指定窗格索引,第二个参数是用来接收指定窗格的矩形区域。修改上面显示进度栏的代码为下面的样子:

	CRect rect;
	m_wndStatusBar.GetItemRect(2,&rect);
	m_progress.Create(WS_CHILD | WS_VISIBLE,rect,&m_wndStatusBar,123);
	m_progress.SetPos(50);

这时运行后看到窗体并未显示出进度栏,原因是因为此时状态栏的初始化工作还没有完成,我们知道,CMainFrame类的OnCreate函数是在响应WM_CREATE消息时调用的,只有在这个函数完成之后才可以获得正确的窗口状态栏上的矩形区域。我们是否可以自己定义一个消息,然后在OnCreate函数中在其返回之前发送这条消息,然后在自定义消息的响应函数中获取状态栏窗格上的矩形区域。 在Windows中,所有的消息都是用一个特定的整数值来表示的,为了避免我们自定义的消息和其他已有的消息值发生冲突,应该利用Windows提高的一个常量:WM_USER.小于这个常量的值都是Windows系统保留的,我们自定义的消息只需要大于这个常量就可以了。在这里 我们比他大1就可以了。

在CMainFrame类的头文件中定义一条自定义消息,定义代码是这样的:

#define UM_PROGRESS		WM_USER+1

下面就要为这条消息加消息响应函数的原型声明,在CMainFrame的头文件的注释宏外面添加这条声明语句:

	//{{AFX_MSG(CMainFrame)
	afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
	afx_msg void OnTimer(UINT nIDEvent);
	afx_msg void OnTest();
	afx_msg void OnViewNewtoolbar();
	afx_msg void OnUpdateViewNewtoolbar(CCmdUI* pCmdUI);
	afx_msg void OnPaint();
	//}}AFX_MSG
	afx_msg void OnProgress();

就是上面的 OnProgress()函数。接下来就是要为这条消息添加消息映射,前面介绍的命令消息是用ON_COMMAND宏讲消息与消息响应函数关联起来,而对于自定义消息来说,使用的是ON_MESSGE宏来实现这个功能。因此,在CMainFrame类的源文件之中,在消息映射表的两个注释宏之外,添加一个UM_PROGRESS这一定义消息的消息映射。

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
	//{{AFX_MSG_MAP(CMainFrame)
	ON_WM_CREATE()
	ON_WM_TIMER()
	ON_COMMAND(IDM_TEST, OnTest)
	ON_COMMAND(IDM_VIEW_NEWTOOLBAR, OnViewNewtoolbar)
	ON_UPDATE_COMMAND_UI(IDM_VIEW_NEWTOOLBAR, OnUpdateViewNewtoolbar)
	ON_WM_PAINT()
	//}}AFX_MSG_MAP
	ON_MESSAGE(UM_PROGRESS,OnProgress)
END_MESSAGE_MAP()

最后,当然就是添加这个消息响应函数的实现:

void CMainFrame::OnProgress()
{
	CRect rect;
	m_wndStatusBar.GetItemRect(2,&rect);
	m_progress.Create(WS_CHILD | WS_VISIBLE |PBS_SMOOTH,
		rect,&m_wndStatusBar,123);
	m_progress.SetPos(50);
}

然后在CMainFrame的OnCreate函数那里添加SendMessage(UM_PROGRESS);

编译运行后,发现还是没有出现进度栏。

造成这个结果的原因主要是:SendMessage函数发送消息的机制,它是直接把消息发送给消息响应函数,由消息响应函数处理完成后,SendMessage函数才返回。这样使得我们的整个创建进度栏的过程还是在OnCreate函数里面,所以结果就和上面的一样,进度栏创建不成功。

因此在这里不可以使用SendMessage函数,而应该使用PostMessage函数。PostMessage函数是将消息放到消息队列中,然后立即返回,之后由程序的GetMessage函数才从消息队列中取出UM_PROGRESS这条自定义消息,此时才轮到OnProgress去执行,这时候因此OnCreate函数已经执行完成,因此状态栏的初始化工作已经全部完成,就可以得到状态栏窗格的矩形区域,从而就可以创建进度栏了。

新的问题又出现了,虽然进度栏是创建成功了,但是当窗口的大小改变的时候,进度栏的显示位置就会发生错误了。为了改变这种情况,我们就需要在程序窗口尺寸改变的时候,重新去获取索引为2的状态栏窗格区域,然后将进度栏移动到这个区域中。窗口尺寸发生变化,会发送WM_PAINT消息,重绘窗口,我们需要在OnPaint函数中加入下面的代码:

void CMainFrame::OnPaint() 
{
	CPaintDC dc(this); // device context for painting
	
	// TODO: Add your message handler code here
	CRect rect;
	m_wndStatusBar.GetItemRect(2,&rect);
	if(!m_progress.m_hWnd)
		m_progress.Create(WS_CHILD | WS_VISIBLE | PBS_SMOOTH,
			rect,&m_wndStatusBar,123);
	else
		m_progress.MoveWindow(rect);
	m_progress.SetPos(50);
	// Do not call CFrameWnd::OnPaint() for painting messages
}

如果不加一个判断,看进度栏是否已经创建,那么就会由于重复将进度栏对象和和进度栏资源进行关联,导致内存访问错误。

移动窗口,可以用MoveWindow函数。

为了更加逼真,现在要让进度栏动起来,即在进度栏上以某种显示方式不断增加当前位置,这可以通过CProgressCtrl类的StepIt函数来完成,该函数将使进度栏控件的当前位置按照一定的步长前进。至于每次前进的步长则可以用SetStep来设置,一旦调用这个函数设置了步长,那么StepIt就会按照这个步长进行前进了。这一次我们只要让进度栏每次前进一步就可以了,所以在OnTimer中添加StepIt函数就可以了。

最后再实现一下让状态栏显示鼠标的当前位置:比较方便的一个方法呢就是下面的这个,先为View类添加一个ONMOUSEMOVE的响应函数,然后在里面写下下面的代码:

void CStyleView::OnMouseMove(UINT nFlags, CPoint point) 
{
	// TODO: Add your message handler code here and/or call default
	CString str;
	str.Format("x=%d,y=%d",point.x,point.y);
	
	((CMainFrame*)GetParent())->SetMessageText(str);
	
	CView::OnMouseMove(nFlags, point);
}

因为状态栏是属于框架窗口的,而框架窗口刚好又是视图窗口的父窗口,所以可以用GetParent函数来获取到框架窗口的指针。然后呢,这里是用SetMessageText函数来实现在状态栏上显示鼠标的坐标,这个函数的作用是在ID为0值的状态栏窗格上(通常是值状态栏上最左边的那个最长的窗格)设置一个字符串。但是这个函数是CFrameWnd类的成员函数,而CMainFrame类派生于这个类,所以将上面获取到的框架窗口的指针装换为CMainFrame的指针后就可以直接调用该函数了。

你可能感兴趣的:(MFC学习笔记之程序外观相关操作)