目录
1. 插入符
1.1 创建文本插入符
1.2 创建图形插入符
2. 文字输出和OnDraw函数
2.1窗口重绘
2.2 添加字符串资源
3. 路径层和剪切区域
3.1 路径
3.2 裁剪区域
4. 字符输入
4.1 字符输入
5. 字幕变色功能的实现
5.1 设置字体
5.2字幕变色功能的实现
6. 总结
1. 插入符
Word和记事本这类文本处理软件可以让我们输入文字,并对文字进行编辑和修改,将介绍与文字处理相关的编程操作。
常用的文本处理程序有Word、记事本和写字板,我们所使用的Visual Studio集成开发环境也可以看成是一个文本处理程序,在它的源代码编辑窗口中可以输入、编辑和修改代码。不难发现,在这些文本处理程序的编辑窗口中都有一条闪烁的竖线,称之为插入符(Caret)。插入符可以用于提示用户:你输入的文字信息将在这个插入符所在的位置显示出来。
1.1 创建文本插入符
在程序中想要创建插入符,可以利用CWnd类的CreateSolidCaret()函数来完成,该函数的原型声明如下:
void CreateSolidCaret(
int nWidth,
int nHeight
);
该函数各个参数的含义如下所述。
■ nWidth指定插入符的宽度(逻辑单位)。如果该参数的值为0,那么系统将其设置为系统定义的窗口边界的宽度。
■ nHeight指定插入符的高度(逻辑单位)。如果该参数的值为0,那么系统将其设置为系统定义的窗口边界的高度。
下面我们利用这个函数在程序窗口中创建一个插入符,首先创建一个单文档类型的MFC应用程序,项目名为Text,解决方案名为ch06。
插入符需要在窗口上创建,单文档类型的工程有两个窗口,即框架类窗口和视类窗口,我们应该选择哪一个窗口来创建插入符呢?在前面曾介绍过,视类窗口始终位于框架类窗口之上,对窗口客户区的鼠标和键盘操作实际上都是在视类窗口上进行的,因此应该在视类窗口上创建插入符。
插入符的创建应该在窗口创建之后进行,可以在 WM_CREATE 消息的响应函数OnCreate 中(在创建窗口的代码之后)添加创建插入符的代码。MFC 应用程序向导所生成的 CTextView 类中没有 OnCreate 函数,我们需要手动添加。为CTextView 类添加WM_CREATE消息的响应函数OnCreate,在此函数中创建一个宽度为20、高度为100的插入符,代码如下所示。
int CTextView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
// TODO: 在此添加您专用的创建代码
CreateSolidCaret(20, 100);
return 0;
}
编译并运行Text程序,发现程序窗口中并没有出现刚才我们创建的插入符,这是为什么呢?实际上,在 CreateSolidCaret 函数创建插入符以后,该插入符初始时是隐藏的,必须调用 ShowCaret 函数来显示它。因此,在上述代码中调用 CreateSolidCaret函数之后,应再添加下面这句代码:
int CTextView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
// TODO: 在此添加您专用的创建代码
CreateSolidCaret(20, 100);
ShowCaret();
return 0;
}
编辑并运行 Text 程序,创建的插入符显示在程序窗口中了,程序运行结果如图所示。
显然,这个插入符看起来不太正常,太高、太宽了。我们在使用Word时,都有这样的经验,插入符的大小会根据当前所选的字号来变化,如果选择了比较大的字号,那么插入符也会相应的大。对Text程序来说,如何才能够让插入符适合于当前字体的大小呢?首先我们需要得到设备描述表中当前字体的信息,也就是文本的信息,然后根据字体的信息来调整插入符的大小。
调用CDC类的GetTextMetrics成员函数可以得到设备描述表中当前字体的度量信息。该函数的原型声明如下所示:
BOOL GetTextMetrics(
LPTEXTMETRIC lpMetrics
) const;
可以看到,该函数的参数要求是一个TEXTMETRIC结构体的指针,也就是说,我们可以定义一个TEXTMETRIC结构体类型的变量,然后将该变量的地址传递给这个参数。通过GetTextMetrics这个函数调用,它会用设备描述表中当前字体的信息来填充这个结构体。
TEXTMETRIC结构体的定义如下:
typedef struct tagTEXTMETRIC
{ /* tm */
int tmHeight;
int tmAscent;
int tmDescent;
int tmInternalLeading;
int tmExternalLeading;
int tmAveCharWidth;
int tmMaxCharWidth;
int tmWeight;
BYTE tmItalic;
BYTE tmUnderlined;
BYTE tmStruckOut;
BYTE tmFirstChar;
BYTE tmLastChar;
BYTE tmDefaultChar;
BYTE tmBreakChar;
BYTE tmPitchAndFamily;
BYTE tmCharSet;
int tmOverhang;
int tmDigitizedAspectX;
int tmDigitizedAspectY;
} TEXTMETRIC;
可以发现TEXTMETRIC这个结构体的信息比较多,但实际上常用的只有几个。为了更好地理解TEXTMETRIC结构体中tmAscent和tmDescent成员的含义,先来看看下图所示的字体信息示意图。
从图中可以看到,对英文字符来说,g和h的高度明显是不一样的。每一种字体都有一条基线(base line),基线以上到图中 h 字符最高点之间的高度称为升序高度(tmAscent),基线以下到图中 g 字符最底点之间的高度称为降序高度(tmDescent)。升序高度加上降序高度就是字体的高度(tmHeight)。这样,当我们在文本程序窗口中输入文字时,下一行的文字才不会覆盖上一行的文字部分。
英文字符的宽度也是不同的。读者可以在任意文本处理程序中输入一个w和i字符,将会很明显地看到这两个字符的宽度是不一样的,显然w要比i宽。因此,字体并没有一个具体的宽度值,只有一个平均宽度(tmAveCharWidth)。另外,TEXTMETRIC结构体中还定义了字体的最大字符宽度(tmMaxCharWidth)。
得到了字体的信息,我们就可以利用字体的高度和平均宽度来计算插入符的高度和宽度。例6-2是具体的实现代码:
int CTextView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
// TODO: 在此添加您专用的创建代码
// 创建设备描述表
CClientDC dc(this);
// 定义文本信息结构体变量
TEXTMETRIC tm;
// 获得设备描述表中的文本信息
dc.GetTextMetrics(&tm);
// 根据字体大小,创建合适的插入符
CreateSolidCaret(tm.tmAveCharWidth / 8, tm.tmHeight);
//显示插入符
ShowCaret();
return 0;
}
为什么上述代码在创建插入符时,要将字体平均宽度除以8呢?这是一个经验值,可以试试其他数值,看看结果是否符合自己的需要。
编译并运行Text程序,这时在应用程序窗口的左上角就出现了一条闪烁的竖线,且其大小也比较符合常规。程序运行结果如图所示。
1.2 创建图形插入符
上面创建的是一般文字处理程序所使用的文本插入符,下面将介绍如何创建图形插入符,这可以利用CWnd类的另一个函数CreateCaret来实现。该函数的声明形式如下所示:
void CreateCaret(
CBitmap* pBitmap
);
这个函数用来创建图形插入符,它有一个参数,要求是CBitmap指针类型。在使用这个函数之前,要先构造一个CBitmap对象,并利用CBitmap的成员函数初始化位图对象,之后,才能使用这个位图对象。
为了创建一个位图对象,既可以新建一个位图资源,也可以导入一个已有的位图资源。导入的方法是在解决方案资源管理器窗口中右键单击“资源文件”文件夹(或者在资源视图窗口中右键单击项目名),从弹出菜单中选择【添加】→【资源】,在出现如图所示的“添加资源”对话框中单击【导入】按钮。
这时会出现如图所示的导入资源对话框。在这个对话框中,找到需要导入的位图文件并选中,最后单击【打开】按钮,即可实现已有位图的导入。本例将新建一个位图,新建位图资源的方法之前已经讲过了。本例新建的位图资源的结果如图所示。
如例所示就是创建图形插入符的具体实现代码。
int CTextView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
CBitmap bitmap;
bitmap.LoadBitmap(IDB_BITMAP1);
CreateCaret(&bitmap);
ShowCaret();
return 0;
}
// 同时记得在CTextView的源文件的头部添加下面的包含语句:
#include "Resource.h"
编辑并运行Text程序,会发现并非如我们所愿,程序窗口上什么也没有。这是为什么呢?我们回顾上述代码,发现定义的bitmap对象是一个局部变量。当OnCreate函数执行完成之后,这个bitmap对象就要发生析构。通常,与资源相关联的对象,在其析构时都会把与之相关联的资源销毁。于是,在本例中,我们就看不到位图插入符。解决的方法就是将这个局部位图对象修改为CTextView类的成员变量。可以剪切该对象的定义代码并粘贴到 CTextView类的头文件中,并将其访问权限设置为private,结果如下所示。
private:
CBitmap bitmap;
再次编译并运行Text程序,这时,我们就会在程序窗口中看到一个位图插入符。程序运行结果如图所示。
int CTextView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
bitmap.LoadBitmap(IDB_BITMAP1);
CreateCaret(&bitmap);
ShowCaret();
return 0;
}
2. 文字输出和OnDraw函数
2.1窗口重绘
在Windows程序运行时,如果程序窗口大小发生变化,窗口会发生重绘,那么窗口中已输入的文字或图形就会被擦除掉。如果希望输入的内容始终保留在窗口上,就要在响应WM_PAINT消息的函数中将内容再次输出。在MFC应用程序向导产生的视类代码中,给我们提供了一个类似于WM_PAINT消息响应函数的OnDraw函数,当窗口发生重绘时,应用程序框架代码就会调用该函数。
在CTextView类中,OnDraw函数的定义如例所示。
void CTextView::OnDraw(CDC* /*pDC*/)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;
// TODO: 在此处为本机数据添加绘制代码
}
可以在此函数处设置一个断点,调试运行程序,看看程序调用这个函数的时机。我们知道当窗口初次出现时,即从无到有时,会产生WM_PAINT 消息,让窗口重绘,这时程序停在所设置的OnDraw函数断点处。继续调试程序,在调试过程中,可以看到,当窗口尺寸发生变化的时候,也会进入OnDraw函数。因此,如果希望输入的图形或文字始终能够在窗口上显示的话,就可以在这个OnDraw函数中进行处理。
另外,从这个函数的定义可知,当它被调用时,应用程序框架会构造一个 CDC 类对象的指针并传递给这个函数,这给我们提供了方便,在这个函数内部就不需要再去构造CDC类的对象,可以直接使用传递进来的CDC对象指针去调用CDC类的成员函数,完成绘制功能。不过,要注意的是,MFC应用程序向导自动生成的OnDraw函数的代码中形参pDC被注释起来了,在使用时,将注释取消即可。
接下来,我们要实现在程序窗口中输出一串文字这一功能,这可以使用前面介绍的TextOut这个函数来实现。在C语言中,如果要使用字符串的话,那么一般是定义一个char*类型的变量。在MFC中,它提供了一个字符串类:CString,这个类没有基类。一个CString对象由一串可变长度的字符组成。在C语言中,利用char类型指针操作字符串时,一旦给它分配了堆内存,那么它就只能存储已分配大小的字符数量。如果想要另外再多存储些字符,就只能对这个指针所指向的堆内存进行再分配。然而,利用 CString 操作字符串时,无论存储多少个字符,我们都不需要对它进行内存分配,因为这些操作在 CString类的内部都已经替我们完成了,这就是CString类的好处。在MFC程序中利用CString类对字符串进行操作是很方便的。读者可以在MSDN中查看CString类的成员,将会发现它重载了多个操作符,这为我们操作CString类的对象提供了极大的便利。可以把CString类型的对象当作简单类型的变量一样进行赋值、相加操作,例如利用“=”操作符,可以直接把一个字符或另一个CString类型的字符串赋给一个CString类型的对象;利用“+”操作符,可以方便地把两个字符串或一个字符与一个字符串联成一个字符串。
CString类提供了多个重载的构造函数(如例代码所示),利用这些构造函数,我们可以构造一个空的CString对象,或者用一个已有的CString对象构造一个新的CString对象,或者用一个字符指针构造一个CString对象。
CString::CString
CString( );
CString( const CString& stringSrc );
throw( CMemoryException );
CString( TCHAR ch, int nRepeat = 1 );
throw( CMemoryException );
CString( LPCTSTR lpch, int nLength );
throw( CMemoryException );
CString( const unsigned char* psz );
throw( CMemoryException );
CString( LPCWSTR lpsz );
throw( CMemoryException );
CString( LPCSTR lpsz );
throw( CMemoryException );
所示代码就是利用CString类在OnDraw函数内实现字符串显示的代码:
void CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CString str(_T("哈哈"));
pDC -> TextOut(100,100, str);
// TODO: 在此处为本机数据添加绘制代码
}
提示: CDC类封装的TextOut函数与SDK提供的全局TextOut函数的区别:前者不需要DC句柄作为参数,因为CDC内部专门有一个成员变量(m_hDC)保存了DC句柄。
提示: 上面代码中有一个_T宏的使用,该宏的作用与在字面常量字符串前面添加大写字母L的作用是一样的,不同的是,字面常量字符串需要放到括号中,与此类似的还有TEXT宏的使用,例如:CString str(TEXT("VC++深入编程"));可以用【转到定义】功能查看一下_T 宏和 TEXT 宏的定义,就能一清二楚了。
编译并运行Text程序,可以看到在程序窗口中输出了我们指定的字符串,如图所示。并且可以发现,当窗口大小发生改变时,字符串仍显示在窗口当中,这是因为我们是在OnDraw函数中实现字符串的显示操作的。
下面是另一种CString字符串构造形式:
CString str;
str = _T("哈哈");
CString 类重载了“=”操作符,可以直接把一个字符串赋给一个 CString 对象。可以尝试运行这段代码,将会发现运行结果是一样的。
void CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CString str;
str = _T("哈哈");
pDC->TextOut(100, 100, str);
// TODO: 在此处为本机数据添加绘制代码
}
2.2 添加字符串资源
CString类还提供了一个成员函数:LoadString,其声明形式如下:
BOOL LoadString(
UINT nID
);
该函数可以装载一个由nID标识的字符串资源。其好处是,我们可以构造一个字符串资源,在需要使用时将其装载到字符串变量中,这样就不需要在程序中对字符串变量直接赋值了。
在Visual Studio开发环境中,如何定义字符串资源呢?切换到资源视图中,可以看到有一项是String Table,表示字符串表(如图所示)。展开“String Table”节点,双击节点下的字符串表,就会在左边的代码编辑区域打开当前程序的字符串表,其中列出了已经定义的各个字符串,如图所示。在这个字符串表中,第一列是字符串资源的ID号,第二列是字符串资源ID对应的数值,第三列是字符串资源的文本内容。
如果想要添加新的字符串资源,可以在这个字符串表最底部的空行上单击一下,就会自动添加一个字符串资源。本例定义一个 ID 为IDS_STRINGVC的字符串资源,内容为“哈哈01”。如图所示。修改上述程序代码,利用LoadString函数加载这个新建的字符串。具体实现代码如下所示。
void CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CString str;
str = _T("哈哈");
pDC->TextOut(100, 100, str);
str.LoadString(IDS_STRINGVC);
pDC -> TextOut(0, 200, str);
// TODO: 在此处为本机数据添加绘制代码
}
3. 路径层和剪切区域
3.1 路径
void CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CString str;
str = _T("哈哈");
pDC->TextOut(100, 100, str);
str.LoadString(IDS_STRINGVC);
pDC -> TextOut(0, 200, str);
// TODO: 在此处为本机数据添加绘制代码
}
在设备描述表中还有一个路径层(path bracket)的概念。什么是路径层呢?路径层的概念就像当年军阀割据时圈地那样,在地域上划定了界线,界线之内的是各自的地盘,别人不能侵犯。
在MFC中,创建路径层是利用CDC类提供的BeginPath和EndPath这两个函数来实现的,首先调用前者BeginPath,该函数的作用是在设备描述表中打开一个路径层;然后利用图形设备接口(GDI)提供的绘图函数进行绘图操作,例如绘制一些点、矩形、椭圆等;最后,在绘图操作完成之后,应用程序通过调用EndPath函数关闭这个路径层。
下面我们在路径层中绘制一个矩形,将先前输出的“哈哈01”字符串框起来。如果要用一个矩形把字符串框起来,就需要知道这个字符串在窗口中的坐标值。在上述代码中,字符串“风灵01”是在坐标(100,100)处输出的,于是我们可以确定矩形的左上角坐标为(100,100),但是如何确定矩形的右下角坐标呢?对一个字符串来说,如果能够知道它的宽度和高度,再加上它的左上角坐标就能够得到包围这个字符串的矩形的右下角坐标了。我们能不能利用C 语言中的strlen 函数来获得字符串的宽度呢?strlen 这个函数获得的是字符串中字符的个数,而字符串在窗口中显示时占据的宽度并不是由其字符数来决定的。例如“w”和“i”同样都是一个字符,但它们所占据的宽度是不一样的。同时,字体的大小也会影响字符串在窗口中显示的宽度。另外,我们在使用Word时,经常会根据需要调整字间距,也就是说,字符和字符之间实际上是有间距的。由此可见,一个字符串在屏幕上显示的宽度是由多个方面的因素决定的,因此,希望利用strlen函数来获得字符串的宽度是根本无法做到的。CDC类为我们提供了一个GetTextExtent函数,利用这个函数可以获得一个字符串在屏幕上显示的宽度和高度,这个函数的一种声明形式如下所示:
CSize GetTextExtent(
const CString& str
) const;
从上述声明中可以得知,我们需要给这个函数传递一个字符串,它会返回一个CSize类型的对象。CSize类类似于Windows的SIZE结构体。Windows的SIZE结构体的定义如下所示:
typedef struct tagSIZE
{
int cx;
int cy;
} SIZE;
该结构体有两个成员变量:cx和cy,分别表示宽度和高度。
GetTextExtent函数需要一个CString对象的引用作为其参数,前面刚刚说过,不同的字符在窗口中显示时,其宽度可能也是不同的,因此,想要得到字符串在窗口中的显示宽度,必须针对特定的字符串调用 GetTextExtent 函数。不要把这个函数和前面讲过的GetTextMetrics函数混淆了,对GetTextMetrics函数来说,它获得的是设备描述表中当前字体的度量信息。而GetTextExtent函数则是获得某个特定的字符串在窗口中显示时所占据的宽度和高度。一定要注意区分这两个函数的作用。
如下所示代码是使用路径层的具体实现代码。
void CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CString str;
str = _T("哈哈");
pDC->TextOut(100, 100, str);
CSize sz = pDC->GetTextExtent(str);
str.LoadString(IDS_STRINGVC);
pDC -> TextOut(0, 200, str);
pDC->BeginPath();
pDC->Rectangle(100, 100, 100 + sz.cx, 100 + sz.cy);
pDC->EndPath();
// TODO: 在此处为本机数据添加绘制代码
}
Build 并运行 Text 程序,将会发现程序窗口与前面的程序结果没有什么不同。但是当我们把上述代码中打开和关闭路径层的两行代码注释起来,也就是不在路径中绘制矩形时,看看程序运行结果会是怎样的。
void CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CString str;
str = _T("哈哈");
pDC->TextOut(100, 100, str);
CSize sz = pDC->GetTextExtent(str);
str.LoadString(IDS_STRINGVC);
pDC -> TextOut(0, 200, str);
//pDC->BeginPath();
pDC->Rectangle(100, 100, 100 + sz.cx, 100 + sz.cy);
//pDC->EndPath();
// TODO: 在此处为本机数据添加绘制代码
}
在设备描述表中有一个默认的白色画刷,当绘制矩形时,它会用这个画刷来填充矩形内部,因此在本例中,调用 Rectangle 函数后,就把先前绘制的文字给覆盖了。前面我们已经看到,如果是在路径层中绘制矩形,那么它对先前输出的文字是没有影响的。既然没有影响,那么路径层有什么作用呢?
下面我们先在窗口中绘制一些网格状线条,这些线条将覆盖已输出的文字。具体代码如例所示。
void CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CString str;
str = _T("哈哈");
pDC->TextOut(100, 100, str);
CSize sz = pDC->GetTextExtent(str);
str.LoadString(IDS_STRINGVC);
pDC -> TextOut(0, 200, str);
pDC->BeginPath();
pDC->Rectangle(100, 100, 100 + sz.cx, 100 + 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);
}
// TODO: 在此处为本机数据添加绘制代码
}
上述代码中利用一个循环来实现网格状线条的绘制,线条与线条之间的间距为10个逻辑单位。在这个循环中,首先是纵坐标不断变化,绘制网格的横线;然后是横坐标不断变化,绘制网格的竖线。编译并运行Text程序,结果如图所示,我们发现多了些网格线把先前输出的文字遮盖住了。此时,我们还是没有看出路径层到底有什么好处。
3.2 裁剪区域
这里,先介绍一下裁剪区域(clipping region)的概念。可以把它理解为一个绘图区域,其大小可以由我们来控制。我们知道对单文档应用程序来说,除了标题栏、菜单栏以外,剩余的就是客户区。通常可以把客户区看作一个大的裁剪区域,但裁剪区域也可以局限于客户区中一个很小的范围之内。例如,可以限制一个矩形区域作为裁剪区域,把后面的绘图操作仅限于这个矩形之内。
CDC类提供了一个SelectClipPath函数,该函数的作用是把当前设置的路径层和设备描述表中已有的裁剪区域按照一种指定的模式进行一个互操作。该函数的声明形式如下所示:
BOOL SelectClipPath(
int nMode
);
其中,参数nMode用来指定互操作的模式,它可以有多种取值,例如RGN_DIFF,该模式的含义是新的裁剪区域包含当前裁剪区域,但排除当前路径层区域。我们可以看看这个模式产生的效果。修改上述代码,在EndPath函数调用之后,添加SelectClipPath函数的调用,结果如例所示。
void CTextView::OnDraw(CDC* pDC)
{
CTextDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CString str;
str = _T("哈哈");
pDC->TextOut(100, 100, str);
CSize sz = pDC->GetTextExtent(str);
str.LoadString(IDS_STRINGVC);
pDC -> TextOut(0, 200, str);
pDC->BeginPath();
pDC->Rectangle(100, 100, 100 + sz.cx, 100 + 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);
}
// TODO: 在此处为本机数据添加绘制代码
}
编译并运行Text程序,结果如图所示。可以发现在窗口中绘制的线条到了程序设置的矩形路径部分就断开了。这正是RGN_DIFF模式的效果,它使新的裁剪区域包含了当前裁剪区域,但把当前路径层的范围排除在外。因此,在程序窗口中就看不到有线条穿过路径范围内的文字,到了这个路径范围线条就终止了。
我们再看看另一种裁剪区域操作模式:RGN_AND的效果。该模式的作用是,新的裁剪区域是当前裁剪区域和当前路径层的交集。把上述例6-9所示代码中的裁剪区域操作模式(即加灰显示的那行代码中的参数:RGN_DIFF)换成RGN_AND,然后编译并运行Text程序,结果如下图所示。可以发现,这时只有矩形路径中显示有线条,其他部分均没有线条。
至此,就可以理解路径层的作用了。以后在绘图时,就可以利用路径层这一特点来实现特殊的效果。例如,如果希望整幅图形中某一部分与其他部分有所区别,就可以把这部分的图形放置到一个路径层中,然后利用 SelectClipPath 函数设置一种模式,让路径层和裁剪区域进行互操作以达到一种特殊的效果。
4. 字符输入
4.1 字符输入
下面要实现字符的输入功能,也就是当用户在键盘上按下某个字符按键后,要把该字符输出到程序窗口上。这就需要程序捕获键盘按下这一消息。之前曾介绍过WM_CHAR消息,这里,我们可以捕获这个消息,在该消息的响应函数中完成字符输出功能。但在字符输出时有一个问题需要注意,利用TextOut函数在窗口中输出字符时,需要提供字符显示位置的x坐标和y坐标,例如,我们打算在(0,0)位置处输出用户按键的字符,如果用户先后按下了“a”“b”“c”这三个字符,对于“a”字符,输出的位置是(0,0),紧接着我们要在“a”字符之后输出字符“b”,但我们如何才能确定“b”字符的输出位置呢?这在实现时有一定的难度,因为每个字符在屏幕上所占据的宽度都是不一样的,这样我们要获得下一个输入点的坐标就不太容易实现。为此,我们可以采取一种简单的方式,把每次输入的字符都存储到一个字符串中,例如,按下了“a”和“b”字符键之后,将这两个字符组成一个字符串:“ab”。当随后再按下“c”键后,再把它与“ab”组成一个字符串:“abc”。在程序中,每当按下新的字符时,都在窗口当前插入符的位置把这个字符串重新输出一次。因为人眼具有视觉残留的效应,因此,用户感觉不到这种重新输出的变化,只能感觉到每按下一个字符,窗口中就都多了一个字符。
遵照这种思路,我们继续在已有的 Text 程序中添加功能,让 CTextView 类捕获WM_CHAR消息,接着为该类定义一个私有的CString类型的成员变量:m_strLine,专门用来存储输入的字符串,并在 CTextView 类的构造函数中将这个变量初始化为空,初始化代码:
public:
afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
CString m_strLine;
//
m_strLine = "";
//
void CTextView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CView::OnChar(nChar, nRepCnt, nFlags);
}
void CTextView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CView::OnLButtonDown(nFlags, point);
}
这里仍有几个问题需要注意,第一个问题是,程序应该在当前插入符的位置输出字符。也就是说,在程序运行时,如果用鼠标左键单击窗口中某个位置,那么插入符就应该移动到这个地方,随后输入的字符都应在此位置处往后输出。这样的话,我们还需要捕获鼠标左键按下消息(WM_ LBUTTONDOWN),在该消息响应函数中,把插入符移动到鼠标左键的单击点处。这可以利用CWnd类的SetCaretPos函数来实现。该函数的声明形式为:
static void PASCAL SetCaretPos(
POINT point
);
从该函数的声明可以知道,它是一个静态函数,并带有一个 POINT 结构体类型的参数,该参数表示一个点。在本例中,这个点就是鼠标左键单击点。接下来,我们就可以在鼠标左键按下这一消息的响应函数中添加如下所示代码中加灰显示的那行代码,以完成插入符移动到当前鼠标左键单击点处的功能。
void CTextView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
SetCaretPos(point);
CView::OnLButtonDown(nFlags, point);
}
编译并运行Text程序,然后用鼠标左键在程序窗口中任意位置处单击,将会发现插入符随着鼠标左键的单击而移动。
第二个需要注意的问题是,用来存储输入的字符串的成员变量m_strLine的取值变化问题。当用鼠标左键单击窗口中一个新的地方时,插入符就会移动到这个新位置,那么以后输入的字符都应从这个位置处开始输出,而以前输入的字符不应再从此位置处重新输出,因此,这时就要把m_strLine中已有的内容清空。这可以利用CString 类的成员函数Empty来实现。于是,我们在上述例代码中添加Empty函数调用,清空m_strLine中的内容,结果如下例所示。
void CTextView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
SetCaretPos(point);
m_strLine.Empty();
CView::OnLButtonDown(nFlags, point);
}
第三个问题是,每次输入的字符串都应在当前插入符位置,也就是鼠标左键单击点处开始显示,这样,就需要把鼠标左键单击点的坐标保存起来,以便在OnChar函数中使用。于是我们为CTextView类再增加一个CPoint类型的成员变量来保存这个坐标值,将这个变量取名为:m_ptOrigin,并将其访问权限设置为私有的。然后在CTextView类的构造函数中设置其初值为0;接着在鼠标左键按下这一消息的响应函数中保存当前鼠标单击点,代码如下例所示。
private:
CPoint m_ptOrigin;
CTextView::CTextView() noexcept
{
// TODO: 在此处添加构造代码
m_strLine = "";
m_ptOrigin = 0;
}
void CTextView::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
SetCaretPos(point);
m_strLine.Empty();
m_ptOrigin = point;
CView::OnLButtonDown(nFlags, point);
}
第四个问题是,在输出字符时,还应考虑到回车字符的处理。在按下回车键后,插入符应换到下一行,随后的输入也应从这一新行开始输出。这样就需要清空上一行保存的字符,并计算插入符在下一行的新位置。这时插入符的横坐标不变,纵坐标发生了变化,而利用已保存的当前插入点的纵坐标加上当前字体的高度就可以得到回车后插入符的新位置的纵坐标。使用前面已经介绍过的GetTextMetrics函数,即可获得当前设备描述表中字体的高度信息。
void CTextView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
TEXTMETRIC tm;
dc.GetTextMetrics(&tm);
if (0x0d == nChar)
{
m_strLine.Empty();
m_ptOrigin.y += tm.tmHeight;
}
CView::OnChar(nChar, nRepCnt, nFlags);
}
第五个问题是,在输出字符时,还要处理一个特殊的字符:退格键(即Backspace键,十进制08)。当按下退格键后,应当删除屏幕上位于插入符前面的那个字符,也就是将这个字符从屏幕上抹掉,同时,插入符的位置应回退一个字符。这个问题的处理也有一定的难度,但我们可以采用一种取巧的方式来实现。我们知道可以使用删除的方式让用户在屏幕上看不见这个字符。另外,如果文本的颜色与背景色一样的话,在屏幕上也看不到这个文本,给用户的感觉就是删除了这个文本。因此,我们可以先把文本的颜色设置为背景色,在窗口中把该文本输出一次。然后从保存输入字符的字符串变量(m_strLine)中把要删除的字符删除,再把文本的颜色设置为原来的颜色,之后再把字符串在窗口中输出一次。这时在屏幕上看到的就是正确的删除效果。因为这些操作都是连续的操作,而且执行的时间非常短,所以给用户的感觉就是一按退格键,就删除了插入符前面的那个字符。
在具体实现时,为了获取背景色,可以利用CDC类的GetBkColor函数。而设置文本颜色,可以利用CDC类提供的另一个成员函数SetTextColor,该函数的声明如下所示:
virtual COLORREF SetTextColor(
COLORREF crColor
);
这个函数将会返回文本先前的颜色。我们需要把这个返回值保存起来,因为后面还要把文本的颜色设置回先前的颜色再次显示。如果想要实现从字符串中删除一个字符,则可以利用CString类的Left函数,该函数的声明形式如下所示:
CStringT Left(
int nCount
) const;
这个函数返回一个CString对象,即返回指定字符串左边指定数目(nCount参数指定)的字符。例如字符串的值为“Windows”,如果指定参数值为3,调用Left函数,那么将返回字符串:“Win”,即“Windows”左边的三个字符。因为本例要删除字符串最右边的那个字符,所以可以将Left函数的参数指定为待显示字符串中字符个数减去数值1后得到的数值。利用CString类提供的GetLength函数,可以得到指定字符串中字符的个数。
void CTextView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
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);
}
CView::OnChar(nChar, nRepCnt, nFlags);
}
如果当前输入的字符不是以上这两种特殊字符(回车键和退格键),就应该把它添加到m_strLine变量中,以便在屏幕上输出。
知识点 回车字符的ASCII码十六进制值是0x0d。退格键的ASCII码十六进制值是0x08。
解决了以上这些问题,就可以在WM_CHAR消息响应函数中进行字符输出的处理了,具体实现代码如下例所示。
void CTextView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
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 += (TCHAR)nChar;
}
dc.TextOut(m_ptOrigin.x, m_ptOrigin.y, m_strLine);
CView::OnChar(nChar, nRepCnt, nFlags);
}
提示:上面代码使用了一个TCHAR数据类型,这主要是为了应对程序中对于ANSI和UNICODE字符的不同处理而定义的类型,便于我们对字符操作采用统一的处理方式。Visual Studio的编译器通过条件预处理指令来决定实际使用类型的字符。TCHAR的定义方式如下:
#ifdef UNICODE
typedef WCHAR TCHAR;
#else
typedef CHAR TCHAR;
编译并运行 Text 程序,将会发现程序现在能够在当前插入符位置处显示输入字符了.
但是这个程序还有点问题,当在屏幕上输出字符时,插入符的位置并没有改变。正常来说,插入符应该随着字符的输入而移动。我们已经知道可以利用SetCaretPos函数来设置插入符的位置,但移动的位置如何确定呢?实际上,对于同一行上的输入来说,插入符横向移动的距离就是输入字符的宽度,而其纵坐标是不变的。根据前面的内容,我们知道利用GetTextExtent函数就可以得到字符串的宽度。因此,在上述例添加如下所示代码,以实现插入符随字符的输入而移动这一功能。
CSize sz = dc.GetTextExtent(m_strLine);
CPoint pt;
pt.x = m_ptOrigin.x + sz.cx;
pt.y = m_ptOrigin.y;
SetCaretPos(pt);
void CTextView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
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 += (TCHAR)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);
}
再次编译并运行Text程序,并输入任意字符,可以发现程序屏幕上插入符随着字符的输入而移动了。Text程序运行结果如图所示。这时,我们也可以试试回车键和退格键的效果,可以发现均成功实现所需功能。
5. 字幕变色功能的实现
5.1 设置字体
MFC提供了一个CFont类专门用来设置字体。这个类派生于CGdiObject类,封装了一个Windows图形设备接口(GDI)的字体。在实际编程时,在构造了一个CFont对象后,还必须利用该类提供的几个初始化函数之一对该对象进行初始化,然后才能使用这个对象。CFont类提供的初始化函数有:
■ CreateFont
■ CreateFontIndirect
■ CreatePointFont
■ CreatePointFontIndirect
这些初始化函数的作用主要是将CFont这个C++对象与字体资源关联起来。本例将使用CreatePointFont这个初始化函数,其声明形式如下所示。
CFont::CreatePointFont
BOOL CreatePointFont(int nPointSize, LPCTSTR lpszFaceName, CDC* pDC = NULL);
该函数带有三个参数,各个参数的含义如下所述。
■ nPointSize设置将要创建的字体的高度,单位是一个点的十分之一。例如,如果该参数值为120,那么要求创建一个12个点的字体。
■ lpszFaceName字体的名称,就像Word中使用的“楷体”“宋体”这些字体名称一样。
在VisualStudio开发环境中,可以看到这些字体的名称,方法是单击【工具】菜单下的【选项】菜单命令,这时会弹出“选项”对话框,在左边的列表框中展开“环境”节点,选中“字体和颜色”,接下来就可以在“字体”的下拉列表中看到所有可用的字体名称,如下图所示
但是, Visual Studio开发环境支持的字体还是少了一些,我们可以在机器的系统目录下看到系统已安装的所有字体。系统目录是Windows。在系统目录下有一个Fonts目录,在该目录下列出的内容就是操作系统已安装的字体。我们编写的程序并不能够对所有这些字体提供支持,程序支持哪些字体,需要通过试验才能知道。
■ pDC这是一个 CDC 对象的指针,用来把 nPointSize 中指定的高度转换为逻辑单位。如果其值为空,就使用一个屏幕设备描述表来完成这种转换。
在程序中,与其他GDI对象一样,当创建了一个字体对象并初始化后,还必须将它选入设备描述表,之后这个新字体才能发挥作用。这可以利用CDC类的SelectObject函数来实现,同样,该函数会返回先前的字体,我们可以保存这个字体,在使用完新字体后,再把设备描述表中的字体恢复为先前的字体。
在OnChar函数中添加了字体的设置之后,该函数完整的代码如例所示。
CFont font;
font, CreatePointFont(300, _T("华文行楷"), NULL);
CFont *pOldFont = dc.SelectObject(&font);
dc.SelectObject(pOldFont);
void CTextView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CClientDC dc(this);
CFont font;
font, CreatePointFont(300, _T("华文行楷"), NULL);
CFont *pOldFont = dc.SelectObject(&font);
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 += (TCHAR)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);
dc.SelectObject(pOldFont);
CView::OnChar(nChar, nRepCnt, nFlags);
}
重新编译并运行Text程序,在程序窗口中输入文字看看效果,发现字体改变了。
这里创建的这个文本程序功能很简单,如果要实现一个功能完整的字处理程序的话,那么所要做的工作还有很多。MFC提供了CEditView和CRichEditView这两个类,可以用来帮助我们实现功能强大的字处理程序,其中,后者提供的功能比前者更为强大。如果让程序的视类直接派生于这两个类之一的话,程序就已经具备字处理程序的一些基本功能了,例如输出字符、回车键的功能,还有一些简单的编辑功能。因此,以后要实现字处理程序,则可以让程序的视类直接派生于这两个类中的其中之一即可。
5.2字幕变色功能的实现
读者平时在唱卡拉OK时,应该注意到歌曲字幕会随着曲调的播放而有一个平滑的变色过程。如何在程序中实现这种效果呢?如果我们先把字符串输出到屏幕上,接着把文本的颜色设置为新的颜色,然后一个字符、一个字符地输出显示该字符串,那么也可以达到一种变色效果,但不能达到平滑的变色效果。为了达到卡拉OK字幕那样平滑的变色效果,我们需要利用CDC类提供的另一个输出文字的函数DrawText来实现。DrawText函数的作用是在指定的矩形范围内输出文字。该函数的一种声明形式如下所示:
int DrawText(
const CString& str,
LPRECT lpRect,
UINT nFormat
);
该函数三个参数的含义如下所述:
■ str指定要输出的字符串。
■ lpRect指定文字显示范围的矩形。
■ nFormat指定文本的输出格式。
DrawText函数实际上是把文字的输出局限在一个矩形范围内。当输出的文字太多,以至于超过设定的矩形范围时,DrawText函数就会截断输出的文字,只显示在设定矩形内能够显示的那部分文字。利用 DrawText 函数的这个特点,我们可以将文本设置为一个新的颜色,在窗口中已有文本的位置重新输出一遍该文本,在初始输出文本时先把矩形的宽度设置为一个较小的值,然后不断地加大矩形的宽度,这样就可以不断地增加显示文字的内容,从而实现文字的平滑变色效果。
- lpRect 指向RECT结构或CRect对象的指针,结构(或对象)中包含有矩形(逻辑单位表示),其中的文本带有格式。
- str 含有要被绘制的文本的CString对象。
- nFormat 指定格式化文本的方法。它可以是下列值的组合(可用运算符或位操作符进行组合):
- DT_BOTTOM 底部对齐的文本。该值必须同DT_SINGLELINE组合使用。
- DT_CALCRECT 决定矩形的宽度和高度,如果是多行文字,DrawText将会使用lpRect指向的矩形,并扩展矩形以容纳文本。如果是单行文本,DrawText将会调整矩形的右边以便容纳该行文字。两种情况下,DrawText都返回格式化文本的高度,但并不绘制它们。
- DT_CENTER 将文本水平居中。
- DT_END_ELLIPSIS或DT_PATH_ELLIPSIS 如果必要,将用椭圆替代部分指定的文本,以便适合给定的矩形。除非指定DT_MODIFYSTRING 标志,否则给定的字符串不会被修改。可以指定DT_END_ELLIPSIS在字符串末端替代字符,或者指定DT_PATH_ELLIPSIS在字符串中间替代字符。如果字符串中含有反斜杠(\),DT_PATH_ELLIPSIS将尽可能保存在最后一个反斜杠后的文本。
- DT_EXPANDTABS 扩展制表符,每一制表符所含字符的缺省数目是8。
- DT_EXTERNALLEADING 包括在行高中的字体外部间隔。通常外部间隔不包括在一行文本的高度之中。
- DT_LEFT 文本左对齐。
- DT_MODIFYSTRING 修正给定字符串以便与显示文本匹配。只有在指定了DT_END_ELLIPSIS 或DT_PATH_ELLIPSIS 标志时,该标志才起作用。注意:某些uFormat标志组合能导致传递的字符串受到修改。DT_MODIFYSTRING同DT_END_ELLIPSIS 或DT_PATH_ELLIPSIS 一起使用会导致字符串修改,导致在CString覆盖中插入断言。
- DT_NOCLIP 绘制但不剪切, DrawText在使用DT_NOCLIP后总能运行得快一些。
- DT_NOPREFIX 关闭前缀字符,通常DrawText将助记前缀&理解为其后字符加上下划线,&&前缀符表示显示一个&字符,指定DT_NOPREFIX后,也关闭了这种处理。
- DT_PATH_ELLIPSIS
- DT_RIGHT 文本右对齐。
- DT_SINGLELING 指定单行,回车与换行不会中断该行。
- DT_TABSTOP 设置制表位,nFormat的高位字节即为每一制表符所含字符的数目,缺省值为8。
文字变色是一个不断变化、自动进行的过程,这意味着我们需要不断地调用DrawText函数,同时增大包含文本的矩形宽度。要实现这个功能,我们需要用到定时器,通过定时器来自动控制文字变色的进程。
定时器与我们日常生活中使用的闹钟有些相似,我们可以把闹钟定在某个时刻,当时间到达这个时刻,闹钟就会振铃。当我们听到振铃声,就知道我们定的时间到了。定时器的功能也是这样的,当它到了一定的时间,就会发送一个消息。我们收到消息,就知道时间到了。但定时器与闹钟不同的地方是,定时器是每隔一定的时间发送一条消息,而闹钟是固定在某个时刻,只有到了这一时刻才会振铃。例如,我们把闹钟定在早上8点这个时刻,那么只有到了这个时刻,闹钟才会振铃;而定时器则可以设置为间隔10分钟发送一条消息,这样,每隔10分钟,我们就会收到一条定时器发送的消息。
利用定时器不断发送消息的特点,我们可以在响应定时器消息的响应函数中,不断增加显示文字的矩形宽度,从而实现平滑的文字变色效果。
利用CWnd类的SetTimer成员函数可以设置定时器,该函数的声明形式如下所示:
UINT_PTR SetTimer(
UINT_PTR nIDEvent,
UINT nElapse,
void (CALLBACK* lpfnTimer
)(HWND,
UINT,
UINT_PTR,
DWORD
)
);
如果这个函数调用成功,那么它将返回新定时器的标识。该函数各参数的含义如下所述:
■ nIDEvent指定一个非零值的定时器标识。
也就是说,当我们定义定时器时,可以为它设置一个标识。如果该函数调用成功,那么这个标识将作为返回值返回。这就是说,如果这个函数执行成功的话,它的第一个参数和返回值就是相等的。
■ nElapse指定定时器的时间间隔,也就是指定定时器每隔多长时间发送一次定时器消息(WM_TIMER)。
需要注意的是,它是以毫秒为单位的。例如,如果将该值设置为1000,那么每隔1秒钟,就发送一次定时器消息。
■ lpfnTimer这是一个函数指针,并且要求是一个回调函数。
在前面已经介绍了CALLBACK的含义。这个回调函数的写法已经在上述声明中列出了。当设定好定时器之后,每隔设定的时间间隔,它就会发送一条定时器消息。如果在这里设置了回调函数,那么操作系统就会调用这个回调函数来处理定时器消息。如果我们将此参数设置为NULL值,那么定时器消息,即WM_TIMER消息就会被放到应用程序的消息队列中,然后由程序中响应此消息的窗口对象来处理。
在这个Text例子中,我们在视类的OnCreate函数中设置定时器。在此函数中,设置一个时间间隔为100ms,标识为1的定时器。实现代码如例所示。
int CTextView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
bitmap.LoadBitmap(IDB_BITMAP1);
CreateCaret(&bitmap);
ShowCaret();
SetTimer(1, 100, NULL);
return 0;
}
另外,本例是在视类中对定时器消息进行处理,因此需要给 CTextView 类添加WM_TIMER消息的响应函数,该函数的初始定义如例所示。
void CTextView::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
CView::OnTimer(nIDEvent);
}
可以看到,这个响应函数有一个参数:nIDEvent,这是定时器的标识。在一个应用程序中,我们可以设置多个定时器,每个定时器都有自己的时间间隔和标识符。但所有的定时器都发送WM_TIMER消息,这时就可以通过这个nIDEvent参数来获得当前是哪个定时器发送的消息,然后针对不同的定时器做不同的处理。本例中只有一个定时器,因此就不需要对此参数进行判断了。
因为需要让DrawText函数的第二个参数(即显示文字的矩形范围)不断增加,所以需要设置一个变量,让它的值不断增加,然后在程序中把这个变量赋给矩形的宽度成员,从而实现该矩形的宽度值不断增加。因此,在CTextView类中再添加一个int类型的私有成员变量:m_nWidth,并在视类的构造函数中将其初始化为0。这一步很重要,如果不初始化这个变量的话,那么它的值将是一个随机值,在随后程序中对它进行自加或自减操作时,结果将很难被确定。
private:
int m_nWidth;
m_nWidth = 0;
提示:在对一个变量进行自加或自减操作前,一定要初始化这个变量。否则,结果是不确定的。
本程序将对前面已在窗口中显示的那行由字符串资源(IDS_STRINGVC)定义的文字实现平滑变色效果。需要先获得包围这行文字的矩形的位置,实际上,只需要获得这个矩形的高度就可以了,因为矩形的左上角坐标就是这行文字显示时的起始坐标。而这个矩形的宽度并不需要知道,它是由m_nWidth变量决定的,从0开始按某个值不断增加。为了获得这个矩形的高度,也就是要获得设备描述表中当前字体的高度,可以通过GetTextMetrics函数来实现。
在OnTimer函数中实现文字平滑变色效果,具体实现代码如下所示。
void CTextView::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
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);
CView::OnTimer(nIDEvent);
}
在上述代码中,首先设置m_nWidth变量的值,按5个像素点增加,也就是说后面调用的DrawText函数的第二个参数即限制显示文字范围的那个矩形的宽度按5个像素点不断增加。接着,根据设备描述表中当前字体的高度得到这个矩形的高度,并利用这些信息初始化矩形对象。接下来,程序将设备描述表中文本颜色设置为红色,并根据字符串资源获得要显示的字符串。然后就调用DrawText 函数,完成在指定矩形范围内文字的输出。因为定时器每隔100ms 就会发出一次WM_TIMER消息,也就是每隔100ms,OnTimer函数就会被调用一次,每调用一次,这个矩形的宽度就会增加5个像素点,所以,以红色输出的文字范围就会增加一些,从而实现了一种文字平滑变色的效果。
编译并运行Text程序,将会看到一种很平滑的变色效果,而不是一个字、一个字地变色。
在上述例所示代码中,DrawText 函数使用的输出格式(即它的第三个参数)是DT_LEFT,这是一种左对齐格式。我们可以再试试其他格式(例如DT_RIGHT)看看效果。将下面这几行代码添加到上述例所示OnTimer函数的第15行代码的后面。
void CTextView::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
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);
CView::OnTimer(nIDEvent);
}
编译并运行Text程序,程序运行结果如图所示。我们可以发现DT_LEFT输出格式从字符串的左边开始,逐渐向右输出文字。而 DT_RIGHT 输出格式从要输出字符串的最右边的那个字符开始输出,逐渐向左输出文字。
在这个Text程序运行时,我们发现还有一些问题。其中一个问题是,当以DT_RIGHT输出格式显示文字时,在字符串全部输出完毕后,应该让它从头开始输出,而不是随着限制显示范围的矩形的宽度不断加大慢慢地从程序窗口上消失。
此外,当我们唱卡拉OK时,会发现字幕会随着音乐的播放而平滑变色,当一句话唱完后,它会变成另外一种颜色,用另外一种颜色表明这句话已经唱过了。那么在程序中要实现这个功能,需要判断限制显示范围的矩形宽度是否超过了需要显示的字符串在屏幕上显示时的宽度。而要获取字符串在屏幕上显示时的宽度,需要用到GetTextExtent函数。这时完整的OnTimer函数的实现代码如下例所示。
void CTextView::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
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);
}
上述所示OnTimer函数中新增的代码段(加灰显示的部分)首先利用GetText Extent函数得到需要显示的字符串的尺寸。接着判断限制显示范围的矩形宽度是否超过了该字符串在屏幕上显示时的宽度。一旦发现其超过了,就将该矩形宽度设置为0,让文本重新开始输出。并将设备描述表中文本的颜色设置为绿色,但此时,先前已输出到窗口中的文本的颜色并未改变,因此还需要再调用一次TextOut函数,在原位置以新的颜色重新输出文本。
编译并运行Text程序,可以看到当字符串全部显示完毕后,会立即从头开始重新显示,并且(0,200)处的字符串在全部显示完毕后会变成绿色。另外,还可以看看把DrawText函数的第三个参数设置为DT_CENTER时的效果。这时,我们会发现文字是从字符串的中间字符开始向两边扩展显示的。以上就是模拟卡拉OK字幕变色效果的实现。这个程序的功能比较简单,可以遵照这样的思路,去实现一个卡拉OK这样的系统。
6. 总结
本章主要介绍了一些文字处理编程方面的知识,以及在处理文字时的一些技巧。