一、概述
在图像上添加标记和文本是图像编辑中经常碰到的操作,如在一幅图像中添加标题、注释和尺寸数据等,许多图像应用软件如PhotoShop、Illustrator和Windows系统自带的Paint等都具备在图像上添加文本的功能。实现添加文本的方法有多种,文献中定义了一个文本类,处理有关文本输入消息,实现了类似Paint中的文本输入方式。实际上,可以调用MFC中的有关函数比较简单地实现这一功能。
这种方法的主要特点有: 给文档类的图像成员添加文本,而不是通过操作OnDraw()函数显示在视图窗口中的内容;逐字显示添加的文本;用视图类成员函数TextToDIB()访问位图内存数据块,把完整的文本串写在DIB图像上,设置文档修改标志为真,提示关闭文档时把位图数据块回写到磁盘文件中,以实现永久保存。而这些并不是通过文档序列化操作完成。
二、分析
文本是逐个字地添加在图像上并在应用程序窗口中显示的,可以想象,如果每写一个字就用函数TextToDIB()对DIB内存数据块进行存取、修改,然后显示修改后的整幅图像,直到最终把整串文本写在图像上,那么程序运行无疑会耗费相当一些资源,然而添加文本操作本身却很简单!另外,用户也可能不满意添加的文本而要重新操作。然而一旦修改了像素数据,虽然没有改变磁盘上的图像文件,但内存中图像的内容就不可以再恢复到原来的样子。程序运行在内存空间,如果错误操作了图像内存块,再去打开图像文件重新读取必将延长执行时间。
MFC应用程序的绘图功能都是在设备上下文(Device Context)中实现的。设备上下文类CDC封装了API中的设备操作函数。内存设备上下文则是在内存中实现的设备上下文。我们的方法不是把每次添加的文字都反映在图像的内存数据块上,相反,用字符串变量textContents记录添加的文本,并在设备上下文中逐个显示出来,直到确定不再添加,才把所有的文本一次性地借助于内存设备上下文修改到图像数据块上。把显示设备上下文和内存设备上下文相结合,实现对图像交互式处理、显示并保存处理结果。
1.添加和显示文本
用CFontDialog类生成一个标准的字体对话框,选定文本输出采用的字体样式,保存在字体变量m_font中。由于字体变量并没有颜色信息,于是用变量m_color记录用户选择的字体颜色。
下面介绍两种添加和显示文本的方法。
(1)利用对话框添加文本,响应时钟消息逐字显示文本
第一步,自定义一个添加文本的对话框类CTextAdd,为编辑框控件绑定一个字符串变量m_strDraw,在编辑框中输入文本,通过对话框的DDX/DDV机制更新文本串textContents。考虑到添加的文本一般不会太长,限定字符串长度不超过50,如图1所示。
图1 添加文本对话框
第二步,多数图像软件添加文本时,都会在一个有背景色的虚线矩形框中进行。当用户在图像的适当位置上按下鼠标左键后,映射OnLButtonDown(CPoint point)消息,用虚线画笔绘制一个以point为左上顶点,宽度为当前字体的最大字符宽度tmMaxCharWidth,高度为tmHeight+tmExternalLeading的蓝边白底矩形,提示将在该矩形框中显示文本。但是此时并没有输出任何一个字符。
实际上,一旦通过对话框添加了文本,字符串变量textContents的值就固定了。所谓的逐字显示也就是先在矩形框中显示文本串的首字符,然后循环执行:在上一次显示的字符串上再向右增加显示一个字符,直到整个文本串得以显示,当然矩形框也每次增加一个字符平均宽度tmAveCharWidth,容纳当前的文本。而且矩形框始终以该point点作为文本输出的起点。具体的绘制操作用CDC类成员函数TextOut()来实现。
第三步,响应时钟消息WM_TIMER来执行每次循环。如果时间间隔设置恰当就会有逐个字符输出的效果。第二步的鼠标释放消息处理函数OnLButtonUp()既保存了文本框的左上顶点(定义文本输出的起点,即文本框的左上顶点textStart=point),也启动了时钟,并设定超时为200毫秒。具体实现见代码部分的OnTimer()定义。
(2)实时显示文本
该方法仍然是鼠标左键按下后,先以当前鼠标点的位置为左上顶点画一个字符平均宽度和文本行高度的矩形,标志着开始添加文本。但不同于对话框方式的是:文本串变量textContents初始为空;释放鼠标后只保存矩形的左上顶点,并不启动时钟;文本串textContents的更新通过键盘输入,映射消息处理函数OnChar()实现,并实时显示在屏幕上,直到用户按下回车键确认,或者文本的长度超过了图像的右边界将截断字符串,结束操作。
2.修改DIB像素数据
设备无关位图(Device Independent Bitmap,即DIB)由位图信息结构头、颜色表和像素数据构成;它可以存放在内存中,也可以存储成文件,扩展名为DIB或者BMP。本文处理的是内存DIB。MFC并没有设计处理DIB位图的类,我们直接调用Win32 SDK的有关API函数。在应用程序的文档类中,用句柄m_hDIB指向该片内存。实际使用时,获得DIB位图信息结构,调用函数CreateDIBSection创建一个DIB项。函数原型为:
HBITMAP CreateDIBSection(
HDC hdc, //设备上下文句柄
const BITMAPINFO *pbmi, //DIB信息结构指针
UINT iUsage, //使用的颜色类型:DIB_RGB_COLORS或DIB_PAL_COLORS
void *ppvBits, //存放位图图像位值的地址
HANDLE hSection, //可选用的文件映射对象的句柄
DWORD dwOffset //文件映射对象中位图位值的偏移量
);
该函数赋予DIB项中像素数据地址的指针ppvBits,使得访问像素数据更容易。创建一个与显示设备上下文相兼容的内存设备上下文,把DIB项选入到内存设备上下文,用当前的字体、字体颜色把textContents中的文本写到DIB项上。可供使用的CDC类的文本操作函数有DrawText()和TextOut()。一般的图像处理软件在添加文本过程中,文本块的背景是白色或者蓝色,书写完确认后文本块的背景是图像本身。相比之下,在这一操作中,DrawText()似乎更合乎要求,当然先用函数SetBkMode设置背景模式为透明。当把DIB项选出设备环境后,一定要用内存拷贝函数memcpy()把添加了文本的DIB项中位图数据复制到原来DIB的像素数据空间,以修改DIB数据块。这样,就可以在文档关闭时把DIB数据块保存到磁盘文件中了。这一过程的代码实现在函数TextToDIB()中。实现的核心代码如下:
void CFontDIBView::OnTimer(UINT nIDEvent)
{
//字符串没有输出完,或者文本长度没有超过图像右边界
if(delta<textLength && textEnd.x<m_rcDIB.right)
{
delta++;//字符个数标记,或者中断标记
textEnd.x=textEnd.x+mTextMetric.tmAveCharWidth;//每加一个字符,修改终点
CRect rc(textStart,textEnd);
CClientDC dc(this);
OnPrepareDC(&dc);
CFont *pOldFont=(CFont*)dc.SelectObject(&m_font);//选入字体到设备环境中
dc.SetTextColor(m_color); //设置文本颜色
CPen pen(PS_DASH,1,RGB(0,0,255));
CPen* pOldPen=(CPen*)dc.SelectObject(&pen);
dc.Rectangle(rc); //画文本所在的矩形边框
CString str;
str=textContents.Left(delta);//获得textContents首字符到当前字符的子串
dc.SetBkMode(TRANSPARENT);
dc.TextOut(textStart.x,textStart.y,str);//在textStart点处画出文本子串
dc.SelectObject(pOldFont);
dc.SelectObject(pOldPen);
pOldPen=NULL;
pOldFont=NULL; }
else if(delta>=textLength)
{
//字符串输出完毕
KillTimer(idTimer); //字符输出结束,取消时钟
m_bAddText=FALSE;//结束添加文本操作
AfxMessageBox("length of texts have reached its limit!");
BOOL bSuccess=TextToDIB(textStart,textEnd); //调用函数在内存中修改图像
Invalidate();//刷新客户区,显示修改后的图像
if(bSuccess)
{
CShowDIBDoc* pDOC=GetDocument();
pDOC->SetModifiedFlag(TRUE); //设置文档修改标记为真
}
}
else if(textEnd.x>=m_rcDIB.right)//文本长度超过了图像右边界,截断处理
{
KillTimer(idTimer);
m_bAddText=FALSE;//结束添加文本操作
AfxMessageBox("text added is too long, reaching the edge of the image.");
BOOL bSuccess=TextToDIB(textStart,textEnd);
Invalidate();
if(bSuccess)
{
CShowDIBDoc* pDOC=GetDocument();
pDOC->SetModifiedFlag(TRUE);
}
}
CScrollView::OnTimer(nIDEvent);
}
BOOL CFontDIBView::TextToDIB(CPoint &pStart,CPoint &pEnd)
{
BOOL bSuccess=FALSE;
CShowDIBDoc* pDOC=GetDocument();
if(pDOC->m_hDIB==NULL) return FALSE;
LPBITMAPINFO lpbi=(LPBITMAPINFO)GlobalLock(pDOC->m_hDIB);
CClientDC dc(this);//显示设备环境
CDC textDC;
textDC.CreateCompatibleDC(&dc);//内存设备环境
LPBYTE lpDIBBits=NULL;//指向DIB像素数据的变量
HBITMAP hTextBMP=NULL, hOldBMP=NULL;
hTextBMP=(HBITMAP)CreateDIBSection(textDC.GetSafeHdc(), lpbi,
DIB_RGB_COLORS,(VOID**)&lpDIBBits,NULL,0);//创建DIB项,并返回位图句柄
DWORD dwDIBSize=lpbi->bmiHeader.biHeight*BytesPerLine((LPBYTE)lpbi);
//计算图像像素数据的总字节数
memcpy(lpDIBBits,FindDIBBits((LPBYTE)lpbi),dwDIBSize);//给DIB项的像素位值赋值
hOldBMP=(HBITMAP)SelectObject(textDC.GetSafeHdc(),hTextBMP);
//把当前创建的DIB项选入到内存设备环境
CFont* pOldFont=(CFont*)textDC.SelectObject(&m_font);
textDC.SetTextColor(m_color);
textDC.SetBkMode(TRANSPARENT);//设置背景模式为透明
CRect rc(pStart,pEnd);
textDC.DrawText(textContents,rc,DT_CENTER);//参数rc背景透明,即为图像本身
SelectObject(textDC.GetSafeHdc(),hOldBMP); //恢复设备环境
textDC.SelectObject(pOldFont);
DeleteDC(textDC);
DeleteDC(dc);
pOldFont=NULL;
hOldBMP=NULL;
GdiFlush();//刷新GDI批量数据,便于操作DIB像素
memcpy(FindDIBBits((LPBYTE)lpbi),lpDIBBits,dwDIBSize);//修改DIB像素数据
GlobalUnlock(pDOC->m_hDIB);
bSuccess=TRUE;
return bSuccess;
}
三、运行结果
程序运行效果如图2和图3所示。
四、结语
本文用VC6.0实现了许多图像处理软件具有的基本功能,即给图像添加文本,既可以用对话框创建显示文本,也可以键盘输入文本。其中键盘只能输入英文字符和数字等特定文本,以后将探讨通过键盘输入汉字。以上所有的代码都在VC++6.0环境下运行通过。
|