6.6 属性页对话框
属性页对话框将多个对话框集中起来,通过标签或按钮 来激活各个页面。主要分为一般属性页对话框和向导对话框两类。在一般属性页对话框中,页面的切换通过单击不同的标签实现,在向导对话框中,页面的选择是通 过点击“上一页”、“下一页”、“完成”和“取消”等按钮实现的。
与属性页对话框相关的类主要包括 CPropertySheet类和CPropertyPage类。一个属性页可以包含一个CPropertySheet类(或者其派生类)的对象和多个 CPropertyPage类(或者其派生类)的对象。下面我们详细地来介绍这两个类的概念。
6.6.1 CPropertySheet类
CPropertySheet类是CWnd类的一个 派生类。CProportySheet类的对象作为属性页对话框的窗口框架出现,主要实现管理各个属性页面的作用。
虽然CPropertySheet类不是 CDialog类的派生类,但是在使用该类时却和CDialog类非常相似:首先运行CPropertySheet类的构造函数,然后调用DoModal 函数实现一个模态属性页对话框,或者调用Creat函数实现一个非模态属性页对话框,CPropertySheet类的构造函数有两个。即 CPropertySheet::Construct函数和CPropertySheet::CPropertySheet函数。在属性页对话框中进行数 据交换和在普通对话框中进行数据交换类似,只是成员变量通常作为CPropertyPage类或其派生类的成员变量。
CPropertysheet类的主要成员如下。
— m_psh
说明:m_psh是PROPSHEETHEADER 结构类型的数据成员,主要定义属性页的框架和关于页面的信息。
— CPropertySheet
其原型为:
CPropertySheet( );
CPropertySheet( UINT nIDCaption, CWnd *pParentWnd = NULL, UINT iSelectPage = 0 );
CPropertySheet( LPCTSTR pszCaption, CWnd *pParentWnd = NULL, UINT iSelectPage = 0 );
说明:作为 CPropertySheet类的构造函数之一,可以指定父级窗口,选择当前页面和设置属性页的标题。
— Construct
其原型为:
void Construct( UINT nIDCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0 );
void Construct( LPCTSTR pszCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0 );
说明:另一个 CPropertySheet类的构造函数。当定义CPropertySheet类的对象而没有调用构造函数时,比如定义了CPropertySheet 类的对象数组时,必须显式地调用Construct构造函数。
— GetActiveIndex
其原型为:
int GetActiveIndex( ) const;
说明:用来获得属性框中当前激活页面的索引值。可以 将返回值作为GetPage函数的参数。
— GetPageIndex
其原型为:
int GetPageIndex( CPropertyPage* pPage ) const;
说明:用来获得指定属性页的索引。
— GetPageCount
其原型为:
int GetPageCount( );
说明:用来获得属性页对话框中当前属性页的个数,不 包括已经定义了但是没有调用AddPage函数的属性页。
— GetPage
其原型为:
CPropertyPage* GetPage( int nPage ) const;
说明:用来返回指定索引的属性页的指针。
— GetActivePage
其原型为:
CPropertyPage* GetActivePage( ) const;
说明:用来返回当前激活的属性页的指针。
— SetActivePage
其原型为:
BOOL SetActivePage( int nPage );
BOOL SetActivePage( CPropertyPage* pPage );
说明:用来将指定索引号或指针的属性页设置为激活 页。
— SetTitle
其原型为:
void SetTitle( LPCTSTR lpszText, UINT nStyle = 0 );
说明:用来将设置属性页对话框的标题。
— GetTabControl
其原型为:
CTabCtrl* GetTabControl( );
说明:返回一个指向CTabCtrl类的指针。比如 在初始化过程中想往标签中增加位图时就需要调用GetTabControl函数。
— SetFinishText
其原型为:
void SetFinishText( LPCTSTR lpszText );
说明:在向导对话框中,当完成了所有操作时,在“完 成”(Finish)按钮上设置需要显示的文字,同时隐藏“上一页”( Back)按钮和“下一页”( Next)按钮。
— SetWizardButtons
其原型为:
void SetWizardButtons( DWORD dwFlags );
说明:在向导对话框中设置按钮的显示方式.必须在调 用DoModal函数之后才可以调用SetWizardButtons函数。在属性页中可以通过调用 CPropertyPage::OnSetActive函数来判断。
— SetWizardMode
其原型为:
void SetWizardMode( );
说明:设置属性页对话框为向导对话框模式。应该在调 用DoModal函数之前调用SetWizardMode函数。
— DoModal
其原型为:
virtual int DoModal( );
说明:显示一个模态属性页。对于一般属性页,返回值 为IDOKIDCANCEL或者0;对于向导对话框返回值为ID_WIZFINISH或IDCANCEL。
— Create
其原型为:
BOOL Create( CWnd* pParentWnd = NULL, DWORD dwStyle = (DWORD)–1, DWORD dwExStyle = 0 );
说明:显示一个非模态属性页。
— AddPage
其原型为:
void AddPage( CPropertyPage *pPage );
说明:往属性框中增加属性页。按照调用 AddPage函数的顺序从左至右地显示属性页。在调用AddPage函数后,实际上并没有为该页创建相应的窗口,只有当属性页被激活时才为该属性页创建 窗口。
在调试程序的时候,如果属性页对话框可以正常地显示 并不说明所有属性页都正常,应该逐次激活每一个属性页并测试效果。当某个属性页被选中时出现问题,往往是在该属性页的初始化过程中导致的。
— RemovePage
其原型为:
void RemovePage( CPropertyPage *pPage );
void RemovePage( int nPage );
说明:从属性框中去除一个属性页。去除一个属性页的 同时删除与之关联的窗口,但是CPropertyPage类的对象还存在。只有在属性框窗口被关闭后,CPropertyPage类的对象才被删除。
— PressButton
其原型为:
BOOL PressButton( int nButton );
说明:标识属性框中的特定按钮被按下。
— EndDialog
其原型为:
void EndDialog( int nEndID );
说明:关闭属性框。
6.6.2 CPropertyPage 类
CPropertyPage类是CDialog类的 一个派生类。主要成员如下。
— m_psp
说明:m_psp是PROPSHEETPAGE结构 类型的数据成员,主要定义属性页的信息。
— CPropertyPage
其原型为:
CPropertyPage( );
CPropertyPage( UINT nIDTemplate, UINT nIDCaption = 0 );
CPropertyPage( LPCTSTR lpszTemplateName, UINT nIDCaption = 0 );
说明:CPropertyPage()为 CPropertyPage类的构造函数。
— Construct
其原型为:
void Construct( UINT nIDTemplate, UINT nIDCaption = 0 );
void Construct( LPCTSTR lpszTemplateName, UINT nIDCaption = 0 );
说明:构造一个CPropertyPage类的对 象。和CPropertysheet类一样,当定义了CPropertyPage类的对象数组时也要调用Construct构造函数。
— CancelToClose
其原型为:
void CancelToClose( );
说明:在模态对话框中,当一个不可恢复的过程完毕 后,调用CancelToClose函数将“确定”(OK)按钮改变为“关闭”(Close)按钮,同时将“取消”(Cancel)按钮变灰(不可用)。
— SetModified
其原型为:
void SetModified( BOOL bChanged = TRUE );
说明:将“应用”(Apply)按钮使之变灰。
— QuerySiblings
其原型为:
LRESULT QuerySiblings( WPARAM wParam, LPARAM lParam );
说明:调用QuerySiblings函数向属性框 中的每一个属性页发一个消息。如果某个属性页返回一个非零值,则属性框就不往下面的属性页发消息了。
— OnCancel
其原型为:
virtual void OnCancel( );
说明:当“取消”(Cancel)按钮被单击后调用 OnCancel函数。该函数可以被重载,在其中加入相应的代码。
— OnKillActive
其原型为:
virtual BOOL OnKillActive( );
说明:当前属性页不再是被激活页时,调用 OnKillActive函数。该函数可以被重载。
— OnOK
其原型为:
virtual void OnOK( );
说明:当“确定”(OK)按钮、“应用” (Apply Now)按钮或“关闭”(Close)按钮被单击后调用OnOK函数。该函数可以被重载。
— OnSetActive
其原型为:
virtual BOOL OnSetActive( );
说明:当属性页被激活时,调用 OnSetActive函数。该函数可以被重载。
— OnApply
其原型为:
virtual BOOL OnApply( );
说明:当“应用”(Apply Now)按钮被单击后调用OnApply函数。该函数可以被重载。
— OnReset
其原型为:
virtual void OnReset( );
说明:当“取消”(Cancel)按钮被单击后调用 OnOK函数。该函数可以被重载。
— OnQueryCancel
其原型为:
virtual BOOL OnQueryCancel( );
说明:当“取消”(Cancel)按钮被单击后,在 还没有执行取消的操作之前调用OnQueryCancel函数。该函数可以被重载。
— OnWizardBack
其原型为:
virtual LRESULT OnWizardBack();
说明:在向导对话框中,当“上一页”(Back)按 钮被单击后调用OnWizardBack函数。该函数可以被重载。
— OnWizardNext
其原型为:
virtual LRESULT OnWizardNext();
说明:在向导对话框中,当“下一页”(Next)按 钮被单击后调用OnWizardNext函数。该函可以被重载。
— OnWizardFinish
其原型为:
virtual BOOL OnWizardFinish( );
说明:在向导对话框中,当“完成”(Finish) 按钮被单击后调用OnWizardFinish函数,该函数可以被重载。
6.6.3 创建一般属性页对话框
本小节将介绍如何来创建一个一般属性页对话框。
【实例6-4】 创建一般属性页对话框
光盘路径 /06/dialog_4/
实例目的 创建一般属性页对话框
1.创建工程
在IDE中依次选择“File”→“New”菜单命令,或直接按快捷键“Ctrl+N”, 打开“New”对话框。
在“New”对话框中单击“Projects”选项卡,在列表框中选择“MFC AppWizard(.exe)”项,在“Project name”文本框中输入“dialog_4”,其他使用默认值,单击“OK”按钮,弹出“MFC AppWizard-Step1”对话框。
在“MFC AppWizard-Step1”对话框中,选中“Multiple Document”单选按钮,其他使用默认值,然后单击“Finish”按钮,在弹出的“New Project Information”对话框中单击“OK”按钮,就可以完成工程的创建。
2.创建弹出属性页对话框的菜单
在上面创建 的对话框中添加一个菜单项下挂一个菜单,菜单命令的消息响应函数用来弹出一个属性页对话框。
下面首先在 菜单资源中添加菜单项和下拉菜单。
在工作区的Resource View标签中双击“dialog_4 resource”→“Menu”→“IDR_dialog_4 TYPE”条目,打开菜单资源。
选中菜单资源中的“查看”菜单,弹出下拉的菜单,选中下面的一个空白菜单项。
单击鼠标右键,选中弹出菜单的Properties菜单命令,弹出属性对话框,单击其中的 Keep Visible按钮,使属性对话框始终显示在前端。
选中General标签中的Separator,在菜单资源中添加一个分隔条,同时在最下 面又自动产生一个空白菜单。
选中空白菜单,在属性对话框的General中输入 ID_VIEW_PROPERTIES,在Caption中输入“属性页对话框(&P)…”,同时下面又会自动增加一个空白的菜单。
此时如果编 辑、链接和运行程序,可以看到新增加的菜单项是灰色的。下面为刚刚创建的菜单项增加消息响应函数。
通过菜单“View→ClassWizard”打开MFC ClassWizard对话框,在Class name项中选择CDialog_4View,在Object IDs中选择ID_VIEW_PROPERTIES,在Message中选择消息COMMAND,单击“Add Function”按钮添加菜单命令的消息响应函数。函数名取默认值。
3.添加一般属性页对话框
下面在例程 dialog_4中添加一个一般属性页对话框,响应菜单命令“Dialog”→“属性页对话框…”。具体过程可以通过往工程中添加组件来实现。
通过菜单命令“Project”→“Add to Project”→“Components and Controls…”打开“Components and Controls Gallery”对话框。如图6-39所示。
图6-39 “Components and Controls Gallery”对话框
在“Components and Controls Gallery”对话框中的目录列表中双击Visual C++ Components 条目,目录列表改变,拖动滚动条,找到Property Sheet项并选中。如图6-40所示。
图6-40 在Visual C++ Components 条目中选择Property Sheet项
单击“Insert”按钮,在弹出的“Microsoft Visual C++”消息对话框中单击“确定”按钮后弹出“Property Sheet Wizard”对话框。如图6-41所示。
在“Property Sheet Wizard”对话框中选择所要添加的属性框类型。默认的是一般类型的属性框即选中的是Property Sheet ,单击“下一步”按钮,弹出如图6-42所示的对话框。
图6-41 “Property Sheet Wizard”对话框 图6-42 下一步“Property Sheet Wizard”对话框
在紧接着弹出的对话框中的Support priviewing复选框要求确认是否在属性框中添加预览功能,默认值为不添加预览功能。下部的单选按钮是确认将属性框设置为模态还是非模态类型,默认 值为模态,选中的是NO保持默认值,单击“下一步”按钮。
在接着弹出的对话框中要求选择在哪个类中对属性框进行操作。通过ComboBox控件,选 中CDialog_4View,单击“下一步”按钮。如图6-43所示。
在接着弹出的对话框中,要求确认属性框中的属性页个数,默认值为2。保持默认值,单击“下 一步”按钮。如图6-44所示。
图6-43 ComboBox控件,选中CDialog_4View 图6-44 确认属性框中的属性页个数
在接着弹出的对话框中要求确认将要添加的CPropertySheet类和 CPropertyPage类的派生类的名称。可以通过单击Change按钮来改变。保留默认值,单击“完成”按钮。如图6-45所示。
图6-45 添加的CPropertySheet类和CPropertyPage类
关闭“Components and Controls Gallery”对话框。
通过上述步骤,在程序中加入了下面的一些内容:
— 一个CPropertySheet 类的派生类CMyPropertySheet 。
— 两个CPropertyPage 类的派生类:CMyPropertyPage1和CMyPropertyPage2。
— 与CMyPropertyPage1类和CMyPropertyPage2类相关联的对话框资源。
— 在Dialog_4View.cpp 文件的头部添加了一条包含语句:
# include "MyPropertySheet.h"
— 在CDialog_4View类中添加了CDialog_4View::OnProperties()函数。函数内容如下:
void CDialog_4View::OnProperties()
{
// TODO:The property sheet attached to your project
// via this function is not hooked up to any message
// handler. In order to actually use the property sheet,
// you will need to associate this function with a control
// in your project such as a menu item or tool bar button.
CMyPropertySheet propSheet;
propSheet.DoModal();
// This is where you would retrieve information from the property
// sheet if propSheet.DoModal() returned IDOK. We aren't doing
// anything for simplicity.
}
所以,只要调用 CDialog_4View::OnProperties()函数就可以弹出刚刚创建的属性页对话框。下面在菜单“Dialog”→“属性页对话框…”的 响应函数中调用该函数。
定位到函数CDialog_4View::OnViewProperties(),加入下 面的代码:
void CDialog_4View::OnViewProperties()//菜单"Dialog→属性页对话框…"的消息响应函数
{
//TODO:Add your command handler code here
//调用函数CDialog_4View::OnProperties(),显示属性框
OnViewProperties();
}
编译、链接和运行程序,测试属性页对话框的效果。如图6-46所示。
图6-46 属性页对话框的效果图
创建的两个属性页对话框内容基本一样,可以利用资源 编辑器修改属性页的内容。
6.6.4 创建向导对话框
本小节将介绍如何来创建一个一般属性页对话框。
【实例6-5】 创建向导对话框
光盘路径 /06/dialog_5/
实例目的 创建向导对话框
1.创建工程
在IDE中依次选择“File”→“New”菜单命令,或直接按快捷键“Ctrl+N”, 打开“New”对话框。
在“New”对话框中单击“Projects”选项卡,在列表框中选择“MFC AppWizard(.exe)”项,在“Project name”文本框中输入“dialog_5”,其他使用默认值,单击“OK”按钮,弹出“MFC AppWizard-Step1”对话框。
在“MFC AppWizard-Step1”对话框中,选中“Multiple Document”单选按钮,其他使用默认值,然后单击“Finish”按钮,在弹出的“New Project Information”对话框中单击“OK”按钮,就可以完成工程的创建。
2.创建弹出属性页对话框的菜单
在上面创建的对话框中添加一个菜单项下挂一个菜单, 菜单命令的消息响应函数用来弹出一个向导对话框。
下面首先在菜单资源中添加菜单项和下拉菜单。
在工作区的Resource View标签中双击“dialog_5 resource”→“Menu”→“IDR_dialog_5 TYPE”条目,打开菜单资源。
选中菜单资源中的“查看”菜单,弹出下拉的菜单,选中下面的一个空白菜单项。
单击鼠标右键,选中弹出菜单的Properties菜单命令,弹出属性对话框,单击其中的 Keep Visible按钮,使属性对话框始终显示在前端。
选中General标签中的Separator,在菜单资源中添加一个分隔条,同时在最下 面又自动产生一个空白菜单。
选中空白菜单,在属性对话框的General中输入ID_VIEW_WIZARD,在 Caption中输入“向导对话框(&W)…”,同时下面又会自动增加一个空白的菜单。
此时如果编辑、链接和运行程序,可以看到新增加的菜 单项是灰色的。下面为刚刚创建的菜单项增加消息响应函数。
通过菜单“View”→“ClassWizard”打开MFC ClassWizard对话框,在Class name项中选择CDialog_5View,在Object IDs中选择ID_VIEW_WIZARD,在Message中选择消息COMMAND,单击“Add Function”按钮添加菜单命令的消息响应函数。函数名取默认值。
3.创建向导对话框
通过往工程中添加组件来创建向导对话框的操作与上面 创建一般属性页的过程基本一致。
通过菜单命令“Project”→“Add to Project”→“Components and Controls…”打开对话框“Components and Controls Gallery”,如图6-40所示。
在“Components and Controls Gallery”对话框中的目录列表中双击“Visual C++ Components”条目,目录列表改变,拖动滚动条,找到“Wizard”项并选中,如图6-47所示。
在图6-40中,单击“Insert”按钮,在弹出的“Micrsoft Visual C++”消息对话框中单击“确定”按钮,弹出“Property Sheet Wizard”对话框。
在“Property Sheet Wizard”对话框中选择所要添加的属性框类型。默认的是一般类型的属性框即选中的是“Property Sheet”,单击“下一步”按钮。
在紧接着弹出的对话框中的“Support previewing”复选框要求确认是否在属性框中添加预览功能,默认值为不添加预览功能。保持默认值,单击“下一步”按钮。
在接着弹出的对话框中要求选择在哪个类中对属性框进行操作。通过“ComboBox”控件,选中 CDialog_5View 。单击“下一步”按钮。
在接着弹出的对话框中,要求确认属性框中的属性页个数,默认值为5。保持默认值,单击“下一步”按钮。
在接着弹出的对话框中要求确认将要添加的CPropertySheet类和CPropertyPage类的派生类 的名称。可以通过单击Change按钮来改变。保留默认值,单击“完成”按钮。为了与一般属性页对话框区别,改变默认值如下。
— 将CPropertySheet类的派生类名称由CMyPropertySheet改为CMyWizardSheet,将头文件名和源文件名分别改为 MyWizardSheet.h和MyWizardSheet.cpp。
— 将CPropertyPage1类的派生类名称由CMyPropertyPage1改为CMyWizardPage1,头文件名和源文件名分别改为 MyWizardPage1.h 和MyWizardPage1.cpp。
— 将CMyPropertyPage2改为CMyWizardPage2。
— 将CMyPropertyPage3 改为CMyWizardPage3。
— 将CMyPropertyPage4 改为CMyWizardPage4。
— 将CMyPropertyPage5 改为CMyWizardPage5。
改变完毕,“Property Sheet Wizard”对话框显示如图6-48所示。
图6-47 选中“Wizard”项 图6-48 “Property Sheet Wizard”对话框修改完毕
单击“完成”按钮。
通过上述步骤,在程序中自动加入了下面的一些内容:
— 一个CPropertySheet类的派生类CMyWizardSheet。
— 5个CPropertyPage类的派生类:CMyWizardPage1~CMyWizardPage5。
— 与CMyWizardPage1~CMyWizardPage5类相关联的对话框资源。
— 在Dialog_5View.cpp文件的头部添加了一条包含语句:
#include "MyWizardSheet.h"
在CDialog_5View类中添加了 CDialog_5View::OnWizard ()函数。
只要调用 CDialog_5View::OnWizard ()函数就可以弹出刚刚创建的向导对话框。下面在菜单“Dialog”→“向导对话框…”的响应函数中调用该函数。
定位到函数CDialog_5View::OnWizard (),加入下面的代码:
void CDialog_5View::OnWizard () //菜单"Dialog|向导对话框…"的响应函数
{
//TODO:Add your command handler code here
OnViewWizard(); //调用函数OnViewWizard(),显示向导对话框
}
编译、链接和运行程序,测试向导对话框的效果,如图6-49所示。
通过运行,可以发现目前在5个向导对话框中,页面和 按钮始终是一样的,并没有因为在第一页而使按钮“上一页”变灰,也没有在最后一页将按钮“下一页”变为“完成”按钮。这些工作要通过手工添加一些代码来实 现。
图6-49 向导对话框
另外,如果查看对话框资源,可以发现:虽然在创建向 导对话框的过程中,为了和前面创建的一般属性页对话框区别,将默认的类名称和文件名作了相应的修改。但是,自动创建的对话框资源的标识号却没有作相应的修 改。其中,标识号为IDD_PROPPAGE1和IDD_PROPPAGE2的对话框资源属于一般属性页的资源,标识号为IDD_PROPPAGE11、 IDD_PROPPAGE21、IDD_PROPPAGE3、IDD_PROPPAGE4和IDD_PROPPAGE5的对话框资源属于向导对话框的资 源。如果要修改对话框资源的标识号,仅仅通过修改属性框中的“General ID”项的内容是不够的,而要将工程中所有的相应的标识号改变。具体操作时可以先通过菜单“Edit”→“Find in Files…”查找到所有使用原来标识号的地方,然后一一修改为新的标识号。
下面为CMyWizardShee类定义一个全局指 针,这样使用时比较方便,因为: CMyWizardPage1~CMyWizardPage5类的对象是作为CMyWizardSheet类的成员变量 存在的,如果定义了CMyWizardSheet类的全局指针,可以在CMywizardPage1~CMyWizardPage5类中通过该指针方便地 调用属于CPropertySheet类的成员函数。比如调用对按钮进行控制的SetWizardButtons成员函数。
通过CMyWizardSheet类的全局指针,可 以在一个属性页中方便地调用另一个属性页中的变量,使得向导对话框能够连续起来。
为此,下面在CDialog_5View 类中定义一个CMyWizardSheet类的全局指针,并通过该指针在不同的属性页中对属性框中的按钮模式进行设置。步骤如下。
在文件CDialog_5.cpp的头部,紧接着#include语句后面添加下面的语句:
CMyWizardSheet*m_pWizardSheet ; //全局指针定义
定位到函数CDialog_5View::OnWizard(),将内容改变如下:
vold CDialog_5View::OnWizard()
{
//TODO:The property sheet attached to your project
//via this function is not hooked up to any message
//handler.In order to actualIy use the property sheet ,
//you will need to associate this function a control
//in your project such as a menu item or tool bar button
m_pWizardSheet=new CMyWizardSheet; //动态分配空间,初始化指针
m_pWizardSheet->DoModal (); //显示向导对话框
delete m_pWizardSheet; //释放空间
//This is where you would retrieve information from the property
//sheet if propSheet DoModal()returned IDOK.We are not doing
//anything for simplicity.
}
在文件MyWizardPagel.cpp的头部,其他#include 语句之后,加入对全局指针m_pWizardSheet进行声明的代码:
#include "MyWizardSheet.h"
extern CMyWiZardSheet*m_pwizardSheet;//全局变量声明
类 CMyWizardPage1~CMyWizardPage5共处于同一个源文件CMyWizardPage1.cpp。现在可以在这5个类的成员函数中 使用全局指针m_pWizardSheet了。
通过ClassWizard,分别往CMyWizardPage1~CMyWizardPage5类中添加响应 WM_INITDIALOG消息的函数OnInitDialog ()。
定位到函数CMyWizardPagel::OnInitDialog(),加入以下代码:
BOOL CMyWizardPage1::OnInitDialog()
{
CPropertyPage::OnInitDialog();
//TODO:Add extra initialization here
//第一页只有"下一个"按钮
m_pWizardSheet->SetWizardButtons ( PSWIZB_NEXT ) ;
return TRUE; //return TRUR unless you set the focus a control
//EXCEPTION:OCX Property Pages should return FALSE
}
编译、链接和运行程序,测试向导对话框的效果,如图6-50所示
图6-50 改进后的向导对话框的效果