通过分析读取一个具有特定标志的文本文件,结合CTreeCtrl和CListCtrl建立一个小型灵活的学生信息管理系统。重点在于阐述CTreeCtrl和CListCtrl的使用,unicode环境下乱码解决的简单方案以及图片缩小时的像素挤压现象的解决方法。
关键词 控件,unicode,像素挤压
前言
CTreeCtrl和CListCtrl是MFC中标准通用控件比较复杂的控件,主要在于它们操作时候参数多且不好理解,而实际中这两类控件使用的又相当频繁,最典型的一个例子就是Windows系统的资源管理器,左边是一个CTreeCtrl,而右边是一个CListCtrl控件。本文通过对建立一个比较实用的学生信息管理系统为例,详细分析了这两类控件的使用。
一、基本概念
树控件本质上是一个特殊的窗口,常常被用于显示分层项目列表。每一个项目包含有一个标签和一个可选的图标,并且每一个项目有一个或多个子项目与其关联,用户可以打开或者折叠这些子项目。Windows资源管理器中左边面板中的目录树就是数控件的一个很好例子。列表控件也是一个窗口,用于显示项目集合,每个项目也包含有图表和一个标签。列表控件有四中显示模式:icon view、small icon view、list view和report view。在本文中使用report view风格。为了创建具有report风格的列表控件,首先要调用CListCtrl::InsertColumn 进行表头的设置,然后再调用CListCtrl::InsertItem设置第n行的首列信息,对第n行的第i列(i大于)则要通过调用SetItemText进行设置。
主要目的是从一个具有特定标志的cgstudio.txt文件中分析读取相关的信息,并将这些信息相应的写入到树控件和列表控件中。由于树具有层次结构,而待分析的文件中并不是很简单一层一层关系,所以需要一个辅助栈来动态建立树,以保证合理的层次关系。此外,为了实现树控件和列表控件的关联,需要一个链表来存储从cgstudio.txt提取的数据。
二、具体实现
在Visual Studio 2005中用向导新建一个基于对话框的应用程序,确保“使用unicode库”被打上勾。添加树控件和列表控件。树控件宽高分别为210﹡194,列表控件的宽高分别为369﹡110。将树控件的下列属性设置为true:has button,has line,line at root,scroll。同时将列表控件的view属性设置为report。树状控件和列表控件都需要一个图像列表,所以要先创建一个图像列表供树状列表和列表控件使用。所需的Icon和Bitmap资源需要预先导入。
void CSkeDlg::_CreateImagList(void) { HICON hIcon[2]; m_imageListTree.Create(24,24,ILC_MASK | ILC_COLOR32,8,8);//使用32位深度图标 hIcon[0] = AfxGetApp()->LoadIcon(IDI_ICON_TREE); m_imageListTree.Add(hIcon[0]); hIcon[1] = AfxGetApp()->LoadIcon(IDI_ICON_TREESELECTED); m_imageListTree.Add(hIcon[1]); CBitmap btp; m_imageListList.Create(16,16,ILC_MASK | ILC_COLOR24,8,8); m_imageListList.Add(AfxGetApp()->LoadIcon(IDI_ICON_LISTHEADER)); btp.LoadBitmap(IDB_BITMAP_LISTGIRL); m_imageListList.Add(&btp,RGB(0,128,128));//bmp相对来说,对背静透明颜色的选择具有更多的自由 btp.DeleteObject ();//注意适时的删除对象,以避免内存的泻漏 btp.LoadBitmap(IDB_BITMAP_LISTBOY); m_imageListList.Add(&btp,RGB(0,128,128)); btp.DeleteObject(); }
1.由于需要从文本文件中分析读取数据,所以要先打开文本文件。打开文件的时机可以放在OnInitDialog中,代码如下:
CFile m_file; if(!m_file.Open(_T("E:\\Demo\\cgstudio.txt"),CFile::modeRead)) return TRUE; ULONGLONG filesize = m_file.GetLength(); char* lpFileBuffer = new char[filesize]; m_file.Read(lpFileBuffer,filesize); m_file.Close(); m_pTxtDecode = new CTextDecode; m_pTxtDecode->BufToTextRow(filesize,lpFileBuffer);
请注意,使用了CFile而不是其派生类CStdIoFile,是因为在支持unicode的环境中,利用CStdIoFile::ReadString函数返回的字符串为乱码,所以在上面代码中,使用了自己定义的一个类CTextDecode的BufToTextRow对这种问题处理。具体如下:
在CTextDecode种定义一个结构
struct TEXT_ROW_INFO { int cch; // 文本行的宽度(字节) const char* lpsz; // 指向缓冲区中行起始地址的指针,比“文本行相对于缓冲区首部的偏移量”更加灵活,//特别适于排版以及行编辑后附加缓冲区的情况。 };
解决unicode环境中乱码现象的算法实现:(这段代码请结合cgstudio.txt进行理解)
void CTextDecode::BufToTextRow(int nBufSize, char* pszBuf) //nBufsize为文件总的大小,pszBuf指向文件缓冲区 { TEXT_ROW_INFO rowNodeInfo; rowNodeInfo.cch = 0; rowNodeInfo.lpsz = pszBuf; int nPos = 0; while (nPos < nBufSize) { if (! (pszBuf[nPos] == 0x0D && pszBuf[nPos + 1] == 0x0A) )//如果没有到回车换行的地方 { if(pszBuf[nPos] == '<') { rowNodeInfo.cch ++; nPos++; while(nPos < nBufSize && pszBuf[nPos] != 0x0D && pszBuf[nPos] != '>') { pszBuf[nPos] = toupper(pszBuf[nPos]); //将小写的标志统一转为大写 rowNodeInfo.cch ++; nPos++; } } else { rowNodeInfo.cch ++; nPos++; } } else { // 换行 pszBuf[nPos] = 0; m_rvRowInfo.push_back(rowNodeInfo); nPos += 2; rowNodeInfo.lpsz = &pszBuf[nPos]; rowNodeInfo.cch = 0; } } //处理没有回车换行符的最后一行 if (rowNodeInfo.cch > 0) { m_rvRowInfo.push_back(rowNodeInfo); } }
2.树控件和列表控件信息的设置
void CSkeDlg::_AnalysizeBuildTree(void) { CTreeCtrl * pTreeCtrl = (CTreeCtrl*)GetDlgItem(IDC_TREE_STUDENT); pTreeCtrl->DeleteAllItems(); pTreeCtrl->SetImageList(&m_imageListTree,TVSIL_NORMAL); CListCtrl * pListCtrl = (CListCtrl*)GetDlgItem(IDC_LIST_STUDENTPICTURE); pListCtrl->SetImageList(&m_imageListList,LVSIL_NORMAL); pListCtrl->SetImageList(&m_imageListList,LVSIL_SMALL); //加这句可以让报表形式,小图标形式能正常显示小图标否则显示不出小图标来 LVCOLUMN clmInsert;//用于对报表样式的List Control的表头外观进行设置 clmInsert.mask = LVCF_FMT | LVCF_IMAGE | LVCF_SUBITEM | LVCF_TEXT | LVCF_WIDTH; clmInsert.fmt = LVCFMT_CENTER;//LVCFMT_COL_HAS_IMAGES表示列头有图标 clmInsert.cx = 110; clmInsert.cchTextMax = 40; clmInsert.iImage = 0;//如果加上此句,那么表头也会有图标 stack <STREESTUDENT> stkTreeStudent; STREESTUDENT stackStucterTemp; TV_INSERTSTRUCT tvinsert; tvinsert.hInsertAfter = TVI_LAST; tvinsert.item.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_TEXT; tvinsert.item.hItem = NULL; tvinsert.item.state = 0; tvinsert.item.stateMask = 0; tvinsert.item.cchTextMax = 40; tvinsert.item.iImage = 0; //表示没有选中时候默认显示的图标 tvinsert.item.iSelectedImage = 1; //选中时候显示的图标 tvinsert.item.cChildren = 0; m_blComeToData = false;//用于表示树枝建立后,是否马上跟有数据,有的话取true,否则取false CString strTemp; while (_ReadString(strTemp)) //_ReadString(strTemp)每次读取一行数据 { if (strTemp.Find(_T("<IMG_FOLDER>")) != -1) //之所以在while循环里面对<img_folder>进行判断,是 if (_ReadString(strTemp)) {//为了增加程序的健壮性,因为<img_folder>可能不位 m_imagePath = strTemp;//于cgstudio.txt的首部,对<infolist>也是出于同样的考虑 continue; } if (strTemp.Find(_T("</IMG_FOLDER>")) != -1) continue; if (strTemp.Find(_T("<INFOLIST>")) != -1) { if (_ReadString(strTemp)) { int i = 0; int pos = 0; CString temp; while ((pos = strTemp.Find(',')) != -1) { clmInsert.iSubItem = i; temp = strTemp.Left(pos); clmInsert.pszText = (LPTSTR)(LPCTSTR)temp; pListCtrl->InsertColumn(i,&clmInsert); //建立列表控件的报表头 strTemp.Delete(0,pos + 1); i++; } } continue; } if (strTemp.Find(_T("</INFOLIST>")) != -1) continue; BOOL blComeToPlus = false; int level = -1; if (strTemp.Find('+') != -1) {//说明当前已经来到了树 while (strTemp.Find('+') != -1)//计算树的层次等级 { level++; strTemp.Delete(0,1); blComeToPlus = true; m_blComeToData = true; } if (stkTreeStudent.empty()) {//树根 tvinsert.hParent = NULL; }else{ while (stkTreeStudent.top().level >= level) {//树建立的条件是当前待加入树的level应 //该大于栈顶元素的level stkTreeStudent.pop(); } tvinsert.hParent = stkTreeStudent.top().hParent; } tvinsert.item.lParam = -1; //lparam参数的使用,可以用于在不同树项目间切换时,能自动记 //录切换前所选择的列表项 tvinsert.item.pszText = (LPTSTR)(LPCTSTR)strTemp; stackStucterTemp.hParent = pTreeCtrl->InsertItem(&tvinsert); stackStucterTemp.lparam = tvinsert.item.lParam; stackStucterTemp.level = level; stkTreeStudent.push(stackStucterTemp); } if (blComeToPlus) continue;//应该在合适的位置加一个continue if (strTemp.Find(_T("<INFODATA>")) != -1 && m_blComeToData) {//应该放到一个列表中存储起来 static int iItem = 0; LISTCONTREE listcontreeTemp; listcontreeTemp.hTreeItem = stackStucterTemp.hParent; int image = 1; while (_ReadString(strTemp)) { if (strTemp.Find(_T("</INFODATA>")) != -1 ) break; int iSubItem = 1; int pos = 0; if ((pos = strTemp.Find(',')) != -1) { listcontreeTemp.str += strTemp; listcontreeTemp.str += _T(";");//加入一个分号用于分割 if (strTemp.Find(_T("男")) != -1) { image = 2; } if (strTemp.Find(_T("女")) != -1) { image = 1; } pListCtrl->InsertItem(LVIF_TEXT|LVIF_STATE| LVIF_IMAGE, iItem, strTemp.Left(pos), LVIS_SELECTED, LVIS_SELECTED,image, 0);//要使iMage有效,mask须添加lvif_image选项 strTemp.Delete(0,pos + 1); while ((pos = strTemp.Find(',')) != -1) { pListCtrl->SetItemText(iItem,iSubItem,strTemp.Left(pos)); strTemp.Delete(0,pos + 1); iSubItem++; } } iItem++; } m_listConn.push_back(listcontreeTemp);//压入list表,方便在 //OnTvnSelchangedTreeStudent中//提取CListCtrl所需的信息 continue; } if (strTemp.Find(_T("</INFODATA>")) != -1) continue; } }
3.建立树控件和列表控件的关联,当点击树的某一个项目或者子项目的时候,能相应地在列表控件中显示对应的信息。
void CSkeDlg::_ConTreeAndList(void) { CListCtrl* pListCtrl = (CListCtrl*)GetDlgItem(IDC_LIST_STUDENTPICTURE); pListCtrl->DeleteAllItems (); for (m_listConIterator = m_listConn.begin();m_listConIterator != m_listConn.end();m_listConIterator++) { if ((*m_listConIterator).hTreeItem == m_hCrtSelTreeItem) { CString strTemp = (*m_listConIterator).str; CString strTemp2; int iLocalItem = 0; int image = 1; while (strTemp.Find(';') != -1) { strTemp2 = strTemp.Left(strTemp.Find(';') + 1); int iLocalSubItem = 1; int pos = 0; if ((pos = strTemp2.Find(',')) != -1) { if (strTemp2.Find(_T("男")) != -1) {//和不同的头像相关联 image = 2; } if (strTemp2.Find(_T("女")) != -1) { image = 1; } pListCtrl->InsertItem(LVIF_TEXT | LVIF_STATE | LVIF_IMAGE, iLocalItem, strTemp2.Left(pos), LVIS_SELECTED, LVIS_SELECTED,image, 0); strTemp2.Delete(0,pos + 1); while ((pos = strTemp2.Find(',')) != -1) { pListCtrl->SetItemText(iLocalItem,iLocalSubItem,strTemp2.Left(pos)); strTemp2.Delete(0,pos + 1); iLocalSubItem++; } } strTemp.Delete(0,strTemp.Find(';') + 1); iLocalItem++; } break; } } }
当使用CImage载入一幅图片然后调用Draw或者StretchBlt显示时,如果目标绘图区域小于原始图像的区域,那么会出现图像挤压的现象(如图1所示)。其问题的关键在于没有对原始图像的像素进行适当的取舍,从而造成像素堆积。解决的方法是在缩小图像时,对原始图像像素按缩小比率进行抽取。算法的核心代码如下:
图1 Cimage::Draw绘制缩小图时所产生的像素挤压现象
void CImageEx::_StretchBltFastRGB(CImageEx* pDst, int xDst, int yDst, int cxDst, int cyDst, int xSrc, int ySrc, int cxSrc, int cySrc) ///////////////////////////////////////////////////////////////////////////////////// // 功 能:把当前15~bit CImageEx 图像数据缩放到指定的目标地址。 // 访问权限:私有函数 // 算 法: // 按最邻近插值直接抽取采样点 // 参 数: // pDst - 指向目标图像的指针 // xDst - 目标矩形区域左上角x坐标 // yDst - 目标矩形区域左上角y坐标 // cxDst - 目标区域宽度 // cyDst - 目标区域高度 // xSrc - 源矩形区域左上角x坐标 // ySrc - 源矩形区域左上角y坐标 // cxSrc - 源图像区域宽度 // cySrc - 源图像区域高度 // 返回值:无 ///////////////////////////////////////////////////////////////////////////////////// { // 定位到目标扫描线上的块起始处,事先考虑目标的x、y 偏移量: int nDstPitch = pDst->GetPitch(); int nBytesPerPixel = GetBPP() >> 3; LPBYTE lpDstLine = (LPBYTE)pDst->GetBits() + yDst * nDstPitch + xDst * nBytesPerPixel; if(cxDst == cxSrc) // 宽相等-> 统一优化处理 _StretchBltFastSameWidth(lpDstLine, xSrc, ySrc, cxSrc, cySrc, cyDst, nDstPitch); else { // 宽度不等(当然,不要上面的优化代码,此段也可处理任何情况) LPBYTE pBitsSrc = (LPBYTE)(GetBits()); LPBYTE pBitsDes = (LPBYTE)(pDst->GetBits());// = lpDstLine LPBYTE pixAddrSrc = pBitsSrc; LPBYTE pixAddrDsc = pBitsDes; int psrc = GetPitch(); //pitch有时为负 int pdsc = pDst->GetPitch(); for (int j = 0; j < cyDst; j++) { for (int i = 0; i < cxDst; i++) { pixAddrSrc = pBitsSrc + psrc * (int)(j * cySrc / cyDst) + (int)(i * cySrc / cyDst) * pDst->GetBPP() / 8; pixAddrDsc = pBitsDes + pdsc * j + i * pDst->GetBPP() / 8; *pixAddrDsc = *pixAddrSrc; for (int k = 0;k < pDst->GetBPP() / 8 - 1;k++) { pixAddrSrc += 1 ; pixAddrDsc += 1; *pixAddrDsc = *pixAddrSrc; } } } } }
三、运行结果
图2是程序运行的结果,从cgstudio.txt提取出来的文字不再有乱码现象,这可以从树控件和列表控件的界面上看出,而图片区域显示的图像虽然比原图小,但图像质量依旧很好,没有出现像素挤压的现象。另外,正如在项目中的introduce.txt(该文件对cgstudio.txt的标志进行了简要的分析)所指出的,尽管这里是以学生管理作为分析对象,但是由于在程序和cgstudio.txt的设计上采用了灵活和开放的形式,使得应用程序可以很快地移植到别的应用环境。
图2 简单学生管理系统的效果图
另外,文中使用了标准模版库中的stack和list,对它们的使用相信也有助于读者朋友加强这方面的理解和应用。