VC++深入详解(7):定制应用程序的外观

这一小节的内容很杂,包扩程序的窗口大小、图标、光标、背景、工具栏、状态栏等内容。

窗口创建之前修改在框架类的PreCreateWindow函数中,我们可以通过改变引用形参cs的某些成员来修改窗口显示的特性,比如:

	cs.cx = 300;
	cs.cy = 200;

会将窗口的大小修改为200*200。假如你想修改程序的标题,那么可以:

	cs.style &= ~FWS_ADDTOTITLE;
	cs.lpszName = "ABCDEFG";

为什么多了一句呢?原因在于,单文档应用程序,在默认情况下,会有一个默认的标题,而当你打开一个文档以后,会将标题改为自己的标题。而通过去掉FWS_ADDTOTITLE属性,才能改变这种设置。
窗口创建之后修改是使用SetWindowLong函数。这个函数用来修改窗口的一些属性。我们可以在创建窗口的最后加上:

	SetWindowLong(m_hWnd,GWL_STYLE,WS_OVERLAPPEDWINDOW);

如果我们是对某写现有属性进行修改,我们可以先通过GetWindowLong获取属性,然后通过逻辑运算符修改属性:

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

下面看看如何修改窗口的光标、图标和背景
这3样是在设计窗口类时设定的,而这个工作又是MFC底层代码替我们完成的,我们不应该修改这些代码,但是,我们可以创建自己窗口类,然后创建我们编写的窗口,比如在PreCreateWindow中:

	WNDCLASS wndclass;
	wndclass.cbClsExtra = 0;
	wndclass.cbWndExtra = 0;
	wndclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
	wndclass.hCursor = LoadCursor(NULL,IDC_HELP);
	wndclass.hIcon = LoadIcon(NULL,IDI_ERROR);
	wndclass.hInstance = AfxGetInstanceHandle();
	wndclass.lpfnWndProc = ::DefWindowProc;
	wndclass.lpszClassName = "ABCDEFG";
	wndclass.lpszMenuName = NULL;
	wndclass.style = CS_HREDRAW | CS_VREDRAW;
	RegisterClass(&wndclass);
	cs.lpszClass = "ABCDEFG";

但是当程序运行时我们发现,图标的确发生了修改,但是光标和背景并没有变,这是为什么呢?因为VIew是覆盖在框架类上的,所以这段代码修改了框架类的背景和光标。如果想要修改视类,那么应该在视类的PreCreateWindow中,将窗口类设置为先前定义的那个类,因为这个窗口类已经注册了,所以在创建窗口时,可以直接使用这个窗口名称:

BOOL CCH_9_STYLEView::PreCreateWindow(CREATESTRUCT& cs)
{
	// TODO: Modify the Window class or styles here by modifying
	//  the CREATESTRUCT cs
	cs.lpszClass = "ABCDEFG";
	return CView::PreCreateWindow(cs);
}
再回到前面的问题。在框架类中,为了修改窗口的图标,我们不得不重新定义一个窗口类,然后注册它,这是一件很麻烦的事,我们可以利用mfc提供的函数AfxRegisterWndClass  来简化这个处理:

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

同理,我们可以在view类的PreCreateWindow中修改:

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

上面的修改方法都是在窗口创建之前完成的,下面介绍如何在窗口创建之后修改程序的外观。这主要是通过全局API函数:SetClassLong实现的,在框架类中的OnCreate中修改图标:

	SetClassLong(m_hWnd,GCL_HICON,(LONG)LoadIcon(NULL,IDI_ERROR));
在视类的OnCreate中修改光标和背景:

	SetClassLong(m_hWnd,GCL_HBRBACKGROUND,(LONG)GetStockObject(BLACK_BRUSH));
	SetClassLong(m_hWnd,GCL_HCURSOR,(LONG)LoadCursor(NULL,IDC_HELP));

下面看一个简单的应用,如何让程序的图标不断地变化。
这个问题的解决思路是:设置一个定时器,当定时器消息到来时,在消息响应函数中装载一个新的图标。

首先,我们将3个新的图标资源放到这个工程的res文件里,然后插入->资源->导入它们。(当然,我们也可以自己动手画3个难看的图标。)然后给框架类加一个图标数组:HICON m_hIcon[3];在OnCreate函数中,初始化这个数组。当装载自己的图标时,LoadIcon需要获取获取当前应用程序的句柄,我们有多种方法获取它们:

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

其中,通过AfxGetInstanceHandle获取当前实例的句柄,而通过MAKEINTRESOURCE将ID号转化为宏。或者可以直接使用全局对象theApp的成员m_hInstance,但是需要声明全局对象是在别的文件中定义的:extern CCH_9_STYLEApp theApp;或者使用AfxGetApp来获取全局对象。
接下来,我们可以设计一个定时器,间隔为1秒:SetTimer(1,1000,NULL);然后响应它:

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_hIcon[index]);
	index = ++index % 3;
	CFrameWnd::OnTimer(nIDEvent);
}

当程序运行时,虽然一开始还是系统的标准图标,但是随着过1秒钟后,就开始在我们装载的3个图标间进行切换了。如果我们想让程序已启动就是我们定义的图标,只需要在最开始装载的时候装载我们的图标就行了:
SetClassLong(m_hWnd,GCL_HICON,(LONG)m_hIcon[0]);
注意,因为一开装载的是第一幅图标,所以应该把index的初值修改为1。


下面看工具栏编程。工具栏其实是把一些常用的命令菜单整合了出来,让大家使用起来更方便。在资源视图下,我们可以看到我们的程序自带的工具栏,我们可以发现,它的ID与菜单中的ID是一致的。
我们下面看如何给工具栏添加一个按钮,与菜单类似,双击最后的那个空按钮,然后设置ID为IDM_TEST,然后,为其添加消息响应函数:

void CMainFrame::OnTest() 
{
	// TODO: Add your command handler code here
	MessageBox("Test!");
}

再强调一个细节,我们发现默认的工具栏下,“新建”、“打开”、“保存”后面,有一个分隔符。这个分隔符也是提示我们这3个功能联系比较紧密。我们可以把自己的工具栏向后拖一点,也能实现同样的效果。如果想去掉这个工具栏按钮,直接把它拖到别的地方就可以了。


接下来看看如何创建一个自己的工具栏大体上分为以下几个步骤:
第一种方法:
1.创建工具栏资源。
2.构造CToolBar对象。
3.调用Create或CreateEx函数来创建Windows工具栏,并把它与创建的CToolBar对象关联起来。
4.调用LoadToolBar函数加载工具栏资源。
第二种方法:
1.构造CToolBar对象。
2.调用Create或CreateEx函数来创建Windows工具栏,并把它与创建的CToolBar对象关联起来。
3.调用LoadBitmap函数加载包含工具栏按钮图像的位图。
4,调用SetButtons函数设置按钮样式,并把工具栏的一个按钮与位图中的一个图像相关联。
在MFC中,类向导会自动生成关联代码:

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

	//........

	// 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);

其中首先调用了CreateEx来创建工具栏,然后调用了LoadToolBar来加载工具栏资源。下面调用了EnableDocking函数来让这个工具栏可以被停靠,调用DockControlBar是让他停靠在框架上。


分析了这么多,我们可以创建自己的工具栏了:
1.新建一个工具栏资源,然后画3个按钮上去。
2.构造一个CToolBar对象:CToolBar m_newToolBar;
3.调用create或createex创建工具栏,并与CToolBar相关联。可以拷贝并修改MFC生成的代码:

	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;      // fail to create
	}
	m_newToolBar.EnableDocking(CBRS_ALIGN_ANY);

只不过因为我们实现它的功能,所以他们都是灰色的。
MFC生成的工具栏中可以通过菜单中的查看->工具栏来选择是否显示。下面我们模拟这个功能:
首先在菜单的查看下面增加一个菜单项,将其ID设为:IDM_VIEW_NEWTOOLBAR,命名为“新的工具栏”。然后为其添加消息响应函数:

void CMainFrame::OnViewNewtoolbar() 
{
	// TODO: Add your command handler code here
	if(m_newToolBar.IsWindowVisible())
	{
		m_newToolBar.ShowWindow(SW_HIDE);
	}
	else
	{
		m_newToolBar.ShowWindow(SW_SHOW);
	}

}

我们发现,点击以后可以去掉工具栏,但是它的bug还有很多:
1.虽然点击后会去掉工具栏,但是工具栏原来位置上会留下“痕迹”,这可以通过增加:

	RecalcLayout();
来解决,MSDN对它的解释是Called by the framework when the standard control bars are toggled on or off or when the frame window is resized。这里就不翻译了。
2.即便是加上了这句代码,程序还是有问题:当我们把工具栏拖动到其他地方时,点击“新的工具栏”还是会出现上面的问题,工具栏上的按钮消失了,工具栏仍然存在,这可以通过增加一句:

	DockControlBar(&m_newToolBar);

来解决。这是因为工具栏显示或者隐藏后,需要让他们停靠在框架窗口上。
3.当我们把工具栏拖动到任意一个位置以后,让他隐藏,再让它显示,发现它会出现在在默认的工具栏的下面,而默认的工具栏可不是这样的,你上次在哪里消失,这次就会在那里显示。其实,我们可以用一个函数来搞定整个问题:

	ShowControlBar(&m_newToolBar,!m_newToolBar.IsWindowVisible(),FALSE);

它的第一个参数填工具栏的地址,第二个参数如果为TRUE表示要显示工具栏,如果为FALSE表示隐藏工具栏,第三个参数是是否时延一段时间显示,我们选择否。这就基本完工。
4.最后一点,就是为菜单中的“新的工具栏”添加标记。在类向导中选择IDM_VIEW_NEWTOOLBAR,然后选择UPDATE_COMMAND_UI:

void CMainFrame::OnUpdateViewNewtoolbar(CCmdUI* pCmdUI) 
{
	// TODO: Add your command update UI handler code here
	pCmdUI->SetCheck(m_newToolBar.IsWindowVisible());
}
看完了工具栏,我们在看看状态栏,状态栏就是就是最下面那一块,分为两部分:左边最长的是提示行,通常当我们把鼠标移动到某个菜单项上时,下面都会给出提示。右边有3个小格,默认状态下显示的是Caps Lock、Num Lock、Scroll Lock键的状态。在框架类窗口中,定义了状态栏的变量:CStatusBar  m_wndStatusBar;然后在onCreate函数中添加了如下代码:

	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
	}

其中Create函数的第一个参数是父窗口的指针,第二个参数窗口类型第三个参数子窗口ID都有默认参数。
SetIndicators设置状态栏指示器。第一个参数是一个ID数组,第二个参数是数组元素的个数。这个数组在这个源文件的开始就定义了:

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

第一个ID表示的是提示行,后面一个依次是最右边的那3个。这些ID其实是字符串资源。要想修改ID,只需要在indicators中添加或者删除一些ID就行了。比如,我们添加两个字符串资源,IDS_TIMER,内容为“时钟”,IDS_PROGRESS内容为“进度栏”。并把它们的ID增加到结构体中:

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

下面,我们在时钟区域显示系统的时间。这需要一个新的类CTime。调用成员函数GetCurrentTime 获取当前时间,它的返回值是一个CTime类的对象;Format 再通过成员函数Format 来格式化时间(这个函数的用法很像printf)。最后通过CStatusBar的SetPaneText来将时间显示出来。

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

基本功能已经完成,可是由于格子太小,不能显示很完整,可以通过SetPaneInfo函数来设置状态栏的大小等信息,完整的代码如下:

	CTime t = CTime::GetCurrentTime();
	CString str = t.Format("%H:%M:%S");
	CClientDC dc(this);
	CSize sz = dc.GetTextExtent(str);
	int index = 0;
	index = m_wndStatusBar.CommandToIndex(IDS_TIMER);
	m_wndStatusBar.SetPaneInfo(index,IDS_TIMER,SBPS_NORMAL,sz.cx);
	m_wndStatusBar.SetPaneText(index,str);

可是,这个时间是一个静止的时间,我们需要在定时器的响应函数中也添加上面的代码。




看完状态栏,我们在看看进度栏。在安装或者卸载程序时,经常会见到它们。与它相对的是一个类CProgressCtrl,我们先构造一个这个类的对象,然后通过成员函数Create来创建一个进度栏。这个函数参数如下:
BOOL Create( DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID );
第一个参数是进度栏的类型,其中有两个特别的类型需要注意PBS_VERTICAL表示进度栏是垂直方向的,PBS_SMOOTH 表示进度栏是平滑增加的。第二个参数是一个矩形区域来指定进度栏的位置;第三个参数是父窗口,第四个参数是ID号。
我们也可以通过SetPos来设置当前进度:

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

下面,我们要实现在状态栏中的“进度栏”处显示进度栏,那么答题思路应该是通过GetItemRect先获取状态栏的位置,然后在创建时将位置设为这个获取的位置:

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

但程序运行的结果却并如我们所愿,通过调试我们可以发现rect对象中的位置是有问题的。这是因为程序运行到m_wndStatusBar.GetItemRect(2,&rect);时状态栏的初始化工作还没有完成,所以得不到正确的矩形区域。只有当onCreate函数执行完以后,才能获得完整的框架类窗口。
所以我们换一个思路:自定义一个消息,在CMainFrame的onCreate中发送这个消息,然后在消息响应函数中获取状态栏上的矩形区域。
1.我们应该自定义一条消息:#define UM_PROGRESS WM_USER+1
消息的本质都是数字,为了能让自己定义的消息与系统默认的消息不重复,我们得使用WM_USER之后的数来定义自己的消息。
2.编写自己的消息响应函数:在头文件中声明消息响应函数:afx_msg void OnProgress();在源文件中,增加消息映射:ON_MESSAGE(UM_PROGRESS,OnProgress);在源文件中,编写消息响应函数:

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

3.在OnCreate中,发送用户自定义消息:

	PostMessage(UM_PROGRESS);
注意,这里不能使用SendMessage。因为SendMessage会对消息立即调用消息响应函数,而此时我们的框架类还没有建立完成,所以CRect还是没有正确的值;而PostMessage是把消息投放到等待队列中,然后再依次从队列中取出这些消息进行响应。但是我们的程序还是有一些小的缺陷:当我们改变窗口的大小时,进度条不能随着窗口一起改变。解决这个问题也不是很困难:窗口大小改变会发生重绘消息,在响应重绘的消息响应函数中,重新获取进度栏的位置:

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
}

注意,我们必须先判断m_progress是否已经和一个进度栏相关联,如果没有,则调用create函数创建;否则,仅仅移动它。并且我们应该删除OnCreate下的PostMessage代码。因为窗口第一次显示时,总是会调用OnPaint函数的。


最后,我们让进度栏动起来。这可以通过CProgressCtrl::StepIt来实现,而每次前进多少,可以通过CProgressCtrl::SetStep 来定义。我们就不细说了。


下面我们要实现的功能,是在状态栏的第一个窗格上显示当前鼠标的位置。首先,鼠标位置可以通过OnMouseMove来获得;其次,这个函数应该放在view类中,因为这个view类覆盖在框架类上面。

	CString str;
	str.Format("x = %d, y = %d ",point.x,point.y);

再次,在view类中,如何获得我们在框架类中定义的状态栏变量m_wndStatusBar呢?
我们先看第一种方法:

	((CMainFrame*)GetParent())->m_wndStatusBar.SetWindowText(str);
	CView::OnMouseMove(nFlags, point);

我们通过GetParent获得父窗口(就是框架类)的指针(需要强制类型转换),然后使用它的成员m_wndStatusBar,调用SetWindowText来显示文字。
注意:我们需要将m_wndStatusBar的访问标号改为public,同时还要包含#include "MainFrm.h"头文件。
第二种办法:

	((CMainFrame*)GetParent())->SetMessageText(str);

这样就不用修改m_wndStatusBar的访问属性了。
第三种方法:

	((CMainFrame*)GetParent())->GetMessageBar()->SetWindowText(str);

GetMessageBar来获得指向状态栏的指针,这样也不需要修改属性。
第四种方法:

	GetParent()->GetDescendantWindow(AFX_IDW_STATUS_BAR)->SetWindowText(str);

对于常用的控制条,MFC都已经预先为它们定义了ID号。我们通过GetDescendantWindow来获取它们。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值