2.图像平滑
图像平滑主要是为了消除噪声。噪声并不限于人眼所能看的见的失真和变形,有些噪声只有在进行
图像处理时才可以发现。图像的常见噪声主要有加性噪声、乘性噪声和量化噪声等。图像中的噪声往往和信号交织在一起,尤其是乘性噪声,如果平滑不当,就会使图像本身的细节如边界轮廓、线条等变的模糊不清,如何既平滑掉噪声有尽量保持图像细节,是图像平滑主要研究的任务。
一般来说,图像的能量主要集中在其低频部分,噪声所在的频段主要在高频段,同时系统中所要提取的汽车边缘信息也主要集中在其高频部分,因此,如何去掉高频干扰又同时保持边缘信息,是我们研究的内容。为了去除噪声,有必要对图像进行平滑,可以采用低通滤波的方法去除高频干扰。图像平滑包括空域法和频域法两大类,在空域法中,图像平滑的常用方法是采用均值滤波或中值滤波,对于均值滤波,它是用一个有奇数点的滑动窗口在图像上滑动,将窗口中心点对应的图像像素点的灰度值用窗口内的各个点的灰度值的平均值代替,如果滑动窗口规定了在取均值过程中窗口各个像素点所占的权重,也就是各个像素点的系数,这时候就称为加权均值滤波;对于中值滤波,对应的像素点的灰度值用窗口内的中间值代替。实现均值或中值滤波时,为了简便编程工作,可以定义一个n*n的模板数组。另外,读者需要注意一点,在用窗口扫描图像过程中,对于图像的四个边缘的像素点,可以不处理;也可以用灰度值为"0"的像素点扩展图像的边缘。下面给出了采用加权均值滤波的图像平滑函数代码和效果图:void CDibView::OnImagePh()
{
CClientDC pDC(this);
HDC hDC=pDC.GetSafeHdc();//获取当前设备上下文的句柄; SetStretchBltMode(hDC,COLORONCOLOR);
HANDLE data1handle;
LPBITMAPINFOHEADER lpBi;
CDibDoc *pDoc=GetDocument();
HDIB hdib;
unsigned char *hData;
unsigned char *data;
hdib=pDoc->GetHDIB();
BeginWaitCursor();
lpBi=(LPBITMAPINFOHEADER)GlobalLock((HGLOBAL)hdib);
hData=(unsigned char*)FindDIBBits((LPSTR)lpBi);
pDoc->SetModifiedFlag(TRUE);
data1handle=GlobalAlloc(GMEM_SHARE,WIDTHBYTES(lpBi->biWidth*8)*lpBi->biHeight);
data=(unsigned char*)GlobalLock((HGLOBAL)data1handle);
AfxGetApp()->BeginWaitCursor();
int i,j,s,t,ms=1;
int sum=0,sumw=0;
int mask[3][3]=,,};//定义的3x3加权平滑模板;
for(i=0; ibiHeight; i++)
for(j=0; jbiWidth; j++)
{
sumw=0; sum=0;
for(s=(-ms); s<=ms; s++)
for(t=(-ms); t<=ms; t++) if(((i+s)>=0)&&((j+t)>=0)&&((i+s)biHeight)&&((j+t)biWidth))
{
sumw += mask[1+s][1+t];
sum+=*(hData+(i+s)*WIDTHBYTES(lpBi->biWidth*8)+(j+t))*mask[1+s][1+t];
}
if(sumw==0) sumw=1;
sum/=sumw;
if(sum>255)sum=255;
if(sum<0)sum=0;
*(data+i*WIDTHBYTES(lpBi->biWidth*8)+j)=sum;
}
for( j=0; jbiHeight; j++)
for(i=0;ibiWidth;i++) *(hData+i*WIDTHBYTES(lpBi->biWidth*8)+j)=*(data+i*WIDTHBYTES(lpBi->biWidth*8)+j);
StretchDIBits (hDC,0,0,lpBi->biWidth,lpBi->biHeight,0,0,
lpBi->biWidth,lpBi->biHeight,
hData,(LPBITMAPINFO)lpBi,
DIB_RGB_COLORS,
SRCCOPY);//显示图像;
}

(a)LENA原图

(b)平滑后的效果图
图三
中值或均值平滑有时处理图像的效果并不是很好,它虽然去除了一定的噪声,但同时使图像中的边缘变的模糊,这主要和所选取的窗口大小有关,为此下面介绍了一种既能保持边缘清晰又能消除噪声的方法,其算法如图四所示:

(a)

(b)

(c)
图 四 图像平滑模板
上图的含义是在图像中取5*5的区域,包含点(i,j)的五边形和六边形各四个,3*3的区域一个,计算这九个区域的标准差和灰度的平均值,取标准差最小区域的灰度平均值作为点(i,j)的灰度。由于该算法的实现代码和上述代码大同小异,所以代码部分就不再赘述。
3.图像锐化
图像平滑往往使图像中的边界、轮廓变的模糊,为了减少这类不利效果的影响,这就需要利用图像鋭化技术,使图像的边缘变的清晰。图像銳化处理的目的是为了使图像的边缘、轮廓线以及图像的细节变的清晰,经过平滑的图像变得模糊的根本原因是因为图像受到了平均或积分运算,因此可以对其进行逆运算(如微分运算)就可以使图像变的清晰。从频率域来考虑,图像模糊的实质是因为其高频分量被衰减,因此可以用高通滤波器来使图像清晰。
为了要把图像中间任何方向伸展的的边缘和轮廓线变得清晰,我们希望对图像的某种运算是各向同性的。可以证明偏导平方和的运算是各向同性的,既:

式中(

)是图像旋转前的坐标,(

)是图像旋转后的坐标。梯度运算就是在这个式子的基础上开方得到的。图像(x,y)点的梯度值:

为了突出物体的边缘,常常采用梯度值的改进算法,将图像各个点的梯度值与某一阈值作比较,如果大于阈值,该像素点的灰度用梯度值表示,否则用一个固定的灰度值表示。
我们在对图像增强的过程中,采用的是一种简单的高频滤波增强方法:

式中f,g分别为锐化前后的图像,

是与扩散效应有关的系数。

表示对图像f进行二次微分的拉普拉斯算子。这表明不模糊的图像可以由模糊的图像减去乘上系数的模糊图像拉普拉斯算子来得到。

可以用下面的模板H=,,}来近似。在具体实现时,上述模板H中的各个系数可以改变,

这个系数的选择也很重要,太大了会使图像的轮廓过冲,太小了则图像锐化不明显。实验表明,

选取2-8之间往往可以达到比较满意的效果。下面给出

等于4的情况下的实现代码和效果图:
void CDibView::OnMenuitem32785()
{
CClientDC pDC(this);
HDC hDC=pDC.GetSafeHdc();//获取当前设备上下文的句柄;
SetStretchBltMode(hDC,COLORONCOLOR);
CDibDoc *pDoc=GetDocument();
HDIB hdib;
hdib=pDoc->GetHDIB();
BITMAPINFOHEADER *lpDIBHdr;//位图信息头结构指针;
BYTE *lpDIBBits;//指向位图像素灰度值的指针;
lpDIBHdr=( BITMAPINFOHEADER *)GlobalLock(hdib);//得到图像的位图头信息 lpDIBBits=(BYTE*)lpDIBHdr+sizeof(BITMAPINFOHEADER)+256*sizeof(RGBQUAD);//获取图像像素值
BYTE* pData1;
static int a[3][3]=,,};//拉普拉斯算子模板;
int m,n,i,j,sum;
int Width=lpDIBHdr->biWidth;
int Height=lpDIBHdr->biHeight;
pData1=(BYTE*)new char[WIDTHBYTES(Width*8)*Height];
file://进行拉普拉斯滤波运算;
for(i=1;i for(j=1;j {
sum=0;
for(m=-1;m<2;m++)
for(n=-1;n<2;n++) sum+=*(lpDIBBits+WIDTHBYTES(Width*8)*(i+m)+j+n)*a[1+m][1+n];
if(sum<0) sum=0;
if(sum>255) sum=255;
*(pData1+WIDTHBYTES(Width*8)*i+j)=sum;
}
file://原始图像pData减去拉普拉斯滤波处理后的图像pData1
for(i=0;i for(j=0;j { sum=(int)(*(lpDIBBits+WIDTHBYTES(Width*8)*i+j)-4*(*(pData1+WIDTHBYTES(Width*8)*i+j)));
if(sum<0) sum=0;
if(sum>255) sum=255;
*(lpDIBBits+WIDTHBYTES(Width*8)*i+j)=sum;
}
StretchDIBits (hDC,0,0,lpDIBHdr->biWidth,lpDIBHdr->biHeight,0,0,
lpDIBHdr->biWidth,lpDIBHdr->biHeight,
lpDIBBits,(LPBITMAPINFO)lpDIBHdr,
DIB_RGB_COLORS,
SRCCOPY);
}

(a)LENA原图

(b)拉普拉斯锐化图
图 五
本文主要讲解了图像直方图的基本概念和图像点处理运算中的增强、平滑、锐化概念和实现算法,并给处理实现代码和处理效果图和广大读者朋友们交流,希望达到抛砖引玉的作用。
一. 理论和方法介绍
a) 采用颜色检索方法的目的:
对
多媒体数据的检索,早期的方法是用文本将多媒体数据进行标识,这显然不是基于多媒体信息本身内容的检索,对多媒体数据中包含的信息是一中及大的浪费;
基于内容的检索是多媒体
数据库的关键技术,如何实现这块技术,是值得商榷的,而最好的方法是使用无需领域知识的检索方法,因此,基于颜色的方法就是实现的关键;
本文介绍了颜色直方图和颜色对方法在基于内容检索时的实现思路和理论;
其实颜色直方图简单来说,就是统计图像中具有某一特定颜色的象素点数目而形成的各颜色的直方图表示,不同的直方图代表不同图片的特征。
b) 利用颜色直方图进行检索:
该方法也可以应用于视频数据库的查询中,有以下三种方式:
(1)指明颜色组成--该法需要用户对图像中的颜色非常敏感,而且使用起来也不方便,检索的查准率和查全率并不高,因此文章中并未介绍该法的实现思路
(2)指明一幅示例图像--通过与用户确定的图像的颜色直方图的相似性匹配得到查询结果,这是文章介绍的两种方法的根本
(3)指明图像中一个子图--分割图像为各个小块,然后利用选择小块来确定图像中感兴趣的对象的轮廓,通过建立更复杂的颜色关系(如颜色对方法)来查询图像,该方法是文章的重心所在
c) 颜色直方图实现思路的介绍:
两图片是否相似可以采用欧氏距离来描述:
Ed=(G,S)=

(Ed越小相似度就越大)
检索后,全图直方图的相似度的定量度量可以用如下公式表示:
Sim(G,S)=

(N为颜色级数,Sim越靠近1两幅图片越相似)
可以对上面2中的公式加改进对某些相对重要的颜色乘上一个权重,就可以做寻找某一前景或组合的查询。
全图的颜色直方图算法过于简单,因此带来很多问题,如:可能会有两幅根本不同的图像具有完全一样的颜色直方图,不反映颜色位置信息,这样导致查准率和查全率都不高,因此问文章提出了一个改进,即将图像进行了分割,形成若干子块,这样就提供了一定程度的位置信息,而且可以对含用户感兴趣的子块加大权重,提高检索的查询智能性和查准查全率,相应的公式有,子块Gij与Sij的相似性度量为:
(P为所选颜色空间的样点数)
再引入子块权重Wij,选取L个最大的Sim值作Simk(Gk,Sk),就有:
(Wk 的选取应根据图像的特点决定,可以使图像中间或用户指定的区域权重大,以反映图像的位置信息)
d) 颜色对实现思路介绍:
主要目的:借助图像中相邻子块之间的颜色直方图的配对建模,实现对图像中的具体对象的查询,支持对象的移位、旋转和部分变形;
颜色对方法特别适合于对边界明显的对象的查询;
实现思路:计算用户输入图像的子块直方图片à用户选定包含查询对象的子块à计算这些子块与周围相邻的子块的颜色对表à将这些颜色对中差值小于某一域值的颜色对删除以消除颜色噪声à选取颜色对表中数值最大的几个颜色对做为图片的代表特征à搜索目标图像的每一子块的颜色对表寻找与这写代表颜色对的匹配à统计单一匹配次数à若有某一比例以上的颜色对匹配到,图像即被检索到。
相似性度量:
(N为所用查询颜色对数目)
qj、gj:颜色对j在查询图像Q和目标图像G中出现的次数
查询时颜色对的匹配应该是不精确的,应该允许的误差为2%以内
二. 具体程序实现
a) 基于子块颜色直方图方法的程序实现:
将图片分成4×4格局,按从左到右、从上到下的顺序,分别计算各子块的颜色直方图,因此需要设定一个三维数组,前两维为子块的坐标,最后一维为颜色级,但现在采样得到的象素点的颜色值是RGB形式的,因此,需要将RGB形式转换为可以用比较合理的有限数表示的颜色级,而人眼对亮度是最为敏感的,因此可以将RGB转换为亮度值Y,公式为:
Y=R×0.299+G×0.587+B×0.114
这样就确定的一个256级的颜色级别,而统计颜色直方图的三维数组就可以定义为:int Color[4][4][256],当采样到某一颜色级时候,将相应的位置加一即可。
根据以上的子块间的相似公式:

,知道某一颜色级对应的数有可能是分母,当两个颜色级的数都为0的时候,显然是不能统计的,因此需要一个数组记录实际统计过的颜色级数,也需要一个数组记录4×4子块的两幅图像的各子块的相似度。
对于用户选定的块其实是代表查询对象的,因此应该加大权重,相对来说就是减小其他块的权重,然后可以将乘过对应权重的块的相似度相加,得到最终的相似度,然后将所有目标图像与用户输入的图像的相似度从大到小排序,选出值最大的几张作为最后的查询结果显示出来返回。
以上是具体实现设想,程序实现如下:
//基于颜色直方图的方法
pDC->TextOut(10,168,"检索结果:");
CBmpProc *pDestBmp;
CString comp_pic_path;
double fsim[15]; file://15张待比较的目标图片与用户输入图片的相似度存放的数组
int psim[15]; file://与fsim想对应的图片编号数组,以便显示
for(int comp_pic=1;comp_pic<=15;comp_pic++){
comp_pic_path.Format("image%d.bmp",comp_pic);
bmp.LoadFromFile(comp_pic_path); // 从库中读入位图
pDestBmp = (CBmpProc*)new(CBmpProc); // 用new分配类目标
pDestBmp->LoadFromObject(bmp, &CRect(0,0,128,128));
// 从bmp中的指定区域读入图像,以便图片匹配的进行
pDestBmp->CalculateColor(*pDC); file://计算目标图片的颜色直方图
int x1,x2,y1,y2,x3,x4,y3,y4;
x1=obj_set.m_x1;x2=obj_set.m_x2;x3=obj_set.m_x3;x4=obj_set.m_x4;
y1=obj_set.m_y1;y2=obj_set.m_y2;y3=obj_set.m_y3;y4=obj_set.m_y4;
file://用户输入的对象所在子块(既用户选定的4个子块)的坐标
double sim[4][4]; file://子块之间的相似度数组
int ccount[4][4]; file://有过统计的颜色数目记录数组
for(int i=0;i<4;i++)
for(int j=0;j<4;j++){
sim[i][j]=0;
ccount[i][j]=0;
}
file://以下两个for按公式计算两幅图像的各对应子块之间的相似度
for(i=0;i<4;i++)
for(int j=0;j<4;j++)
for(int k=0;k<256;k++){
if((pDestBmp->Color[i][j][k]>=pBmp->Color[i][j][k])&&pDestBmp->Color[i][j][k]!=0){
sim[i][j]+=(1-((fabs(pDestBmp->Color[i][j][k]-pBmp->Color[i][j][k]))/(pDestBmp->Color[i][j][k])));
ccount[i][j]++;
}
if((pDestBmp->Color[i][j][k]Color[i][j][k])&&pBmp->Color[i][j][k]!=0){
sim[i][j]+=(1-((fabs(pDestBmp->Colori][j][k]-pBmp->Color[i][j][k]))/(pBmp->Color[i][j][k])));
ccount[i][j]++;
}
}
for(i=0;i<4;i++)
for(int j=0;j<4;j++){
sim[i][j]=sim[i][j]/ccount[i][j];
}
file://计算两图像最终的相似度结果
double final_sim=0;
for(i=0;i<4;i++)
for(int j=0;j<4;j++){
file://对用户指定的块设置权重为1
if((i==x1&&j==y1)||(i==x2&&j==y2)||(i==x3&&j==y3)||(i==x4&&j==y4))
final_sim+=sim[i][j];
else
file://其他块降低权重为0.7,提高对对象匹配的精确度
final_sim+=(sim[i][j]*0.7);
}
file://将15幅被比较图像与用户输入源图像的最后计算出来的相似度结果记录在数组中
fsim[comp_pic-1]=final_sim;
delete (CBmpProc*)pDestBmp;
}
int count=15;double tempf;int tempp;
for(int l=0;l<15;l++){
psim[l]=l+1; file://设定编号数组
}
file://将15个相似度从大到小排列,并且改变次序的时候编号数组和跟着改变
for(int i=count;i>0;i--){
for(int j=0;jif(fsim[j]tempf=fsim[j];
tempp=psim[j];
fsim[j]=fsim[j+1];
psim[j]=psim[j+1];
fsim[j+1]=tempf;
psim[j+1]=tempp;
}
}
int disp=0;
int space=-128;
file://将相似度最大的的两张图片显示出来
for(int disp_pic=1;disp_pic<=2;disp_pic++){
comp_pic_path.Format("image%d.bmp",psim[disp_pic]);
bmp.LoadFromFile(comp_pic_path); // 从库中读入位图
pDestBmp = (CBmpProc*)new(CBmpProc); // 用new分配类目标
pDestBmp->LoadFromObject(bmp, &CRect(0,0,128,128)); // 从bmp中的指定区域读入图像
disp++;
space+=128;
pDC->Rectangle(10+space-1,190-1,138+space+1,318+1);
pDestBmp->Draw(*pDC, &CRect(10+space,190,138+space,318));
// 将pBmp中的图像绘入DC的指定区域
space+=6;
}
delete (CBmpProc*)pBmp; // 删除类目标,delete会自动调用类的析构函数。
AfxMessageBox("检索完成");
}
b) 基于颜色对的方法的程序实现
该方法也需要分成4×4子块,计算颜色直方图,具体计算颜色直方图的方法上面已经有过详细的解释。
该方法主要在于对颜色对表示结构的实现,颜色对是某一图片的代表特征,因此在程序中必须有定量表示,现在采取用两个子块颜色直方图的欧氏距离表示,因此计算某一子块的颜色对表就是按八方向计算其与周围的子块之间的欧氏距离,将结果存放于一个double o_dis[8]的数组中,然后将这个数组从大到小排序,排序完成后再将数组中相互之间值的差小于某一域值(取8个颜色对的平均值的2%)的颜色对祛除(按序两两比较再移动数组里的变量实现),最后将结果先填入图像的特征颜色对表(有4×8=32个变量,是一个结构数组,结构记录用户选定子块的坐标和与其相对应的被选中的颜色对值)。
最后,对4个用户选定的子块依次计算完毕,就可以调用SortColorPair()函数,对特征颜色对表做出处理(先从大到小排序,然后祛除差值小于总平均值的2%的特征颜色对)。
在比较的时候,按顺序计算出目标图像的子块颜色对表,和以上的特征颜色对表匹配,如果匹配到,则标记该颜色对(设定另一标记0数组),并且将匹配数变量加一,如果最后匹配到的数目是60%以上,就算目标图像被搜索到。
具体程序实现如下:
//计算子块(x,y)的颜色对表,采取"八方向邻接技术"
int CBmpProc::CalculateColorPair(int x, int y)
{
file://颜色对采取欧氏距离来描述
double o_dis[8];
for(int k=0;k<8;k++){
o_dis[k]=0;
}
file://计算(x,y)与周围所有子块的颜色直方图的欧氏距离
file://---------------------------------------------
for(int i=0;i<256;i++){
if((x-1)>=0&&(y-1)>=0)
o_dis[0]=o_dis[0]+(Color[x-1][y-1][i]-Color[x][y][i])*(Color[x-1][y-1][i]-Color[x][y][i]);
else
o_dis[0]=-1;
if((y-1)>=0)
o_dis[1]=o_dis[1]+(Color[x][y-1][i]-Color[x][y][i])*(Color[x][y-1][i]-Color[x][y][i]);
else
o_dis[1]=-1;
if((x+1)<=3&&(y-1)>=0)
o_dis[2]=o_dis[2]+(Color[x+1][y-1][i]-Color[x][y][i])*(Color[x+1][y-1][i]-Color[x][y][i]);
else
o_dis[2]=-1;
if((x-1)>=0)
o_dis[3]=o_dis[3]+(Color[x-1][y][i]-Color[x][y][i])*(Color[x-1][y][i]-Color[x][y][i]);
else
o_dis[3]=-1;
if((x+1)<=3)
o_dis[4]=o_dis[4]+(Color[x+1][y][i]-Color[x][y][i])*(Color[x+1][y][i]-Color[x][y][i]);
else
o_dis[4]=-1;
if((x-1)>=0&&(y+1)<=3)
o_dis[5]=o_dis[5]+(Color[x-1][y+1][i]-Color[x][y][i])*(Color[x-1][y+1][i]-Color[x][y][i]);
else
o_dis[5]=-1;
if((y+1)<=3)
o_dis[6]=o_dis[6]+(Color[x][y+1][i]-Color[x][y][i])*(Color[x][y+1][i]-Color[x][y][i]);
else
o_dis[6]=-1;
if((x+1)<=3&&(y+1)<=3)
o_dis[7]=o_dis[7]+(Color[x+1][y+1][i]-Color[x][y][i])*(Color[x+1][y+1][i]-Color[x][y][i]);
else
o_dis[7]=-1;
}
for(int j=0;j<8;j++){
if(o_dis[j]>=0)
o_dis[j]=sqrt(o_dis[j]);
}
file://------------------------------------------------
file://欧氏距离计算结束
int flag=0;
int num=0;
for(int pairnum=0;pairnum<32;pairnum++){
if(pair[pairnum].x!=-1){
num++;
}
}//因为在计算子块的颜色对表的时候已经写了特征颜色对数组,因此要先统计一下特征颜色对数组里已经//有多少有数值了,以便下次的写入可以接在后面,而不至于覆盖了前面的数值
file://计算颜色对差值小于某个"域值"的这个域值
double ave=0;
for(int e=0;e<8;e++){
ave+=o_dis[e];
}
ave=ave/8;ave=ave*0.02; file://采取与子块周围颜色对的平均值的2%计为域值
file://对该子块的颜色对表进行从大到小的排序,采取冒泡排序
int count=8; double temp;
for(i=count;i>0;i--){
for(int j=0;jif(o_dis[j]temp=o_dis[j];
o_dis[j]=o_dis[j+1];
o_dis[j+1]=temp;
}
}
file://消除那些颜色对差值小于某个"域值"的颜色对,以消除那些没有意义的小对象
for(k=0;kif(fabs(o_dis[k]-o_dis[k+1])for(int l=k+1;lo_dis[l]=o_dis[l+1];
}
count--;
k--;
o_dis[count]=-1;
}
}
file://将该字块计算得到的颜色对表填入该图像的特征颜色对表
for(int scan=0;scan<8;scan++){
if(o_dis[scan]>0){
pair[num].x=x;
pair[num].y=y;
pair[num].o_dis=o_dis[scan];
num++;
}
}
return 1;
}
//计算该图像的最终确定的特征颜色对表
BOOL CBmpProc::SortColorPair()
{
file://32个数据项中有count个有实际数值
for(int count=0;count<32;count++){
if(pair[count].x==-1)
break;
}
struct color_pair temp;
file://对颜色对表从大到小排列序(冒泡排序法)
for(int i=count;i>0;i--){
for(int j=0;jif(pair[j].o_distemp=pair[j];
pair[j]=pair[j+1];
pair[j+1]=temp;
}
}
file://计算域值以消除差值小于这个值的颜色对
double ave=0;
for(int e=0;eave+=pair[e].o_dis;
}
ave=ave/count;
ave=ave*0.02;
file://消除差值小于域值的颜色对
for(int k=0;kif(fabs(pair[k].o_dis-pair[k+1].o_dis)for(int l=k+1;lpair[l]=pair[l+1];
}
count--;
k--;
}
}
file://置特征颜色对数目变量
pair_count=count;
return true;
}
将计算颜色直方图的代码表达如下:
file://以下函数计算颜色直方图
BOOL CBmpProc::CalculateColor(CDC &dc)
{
if (!IsValid())
return FALSE;
ASSERT(m_pInfo);
ASSERT(m_pInfo->bmiHeader.biSize == sizeof(BITMAPINFOHEADER));
// 复制源图
CDC compDC;
// 创建与当前显示设备兼容的内存设备描述表
compDC.CreateCompatibleDC(&dc);
compDC.SelectObject(this);
COLORREF clr; file://定义一个COLORREF结构,因为提取的象素点的颜色是以RGB形式表示的
int pix_color;
int red,green,blue;
int x,y;
for(int fd=0;fd<4;fd++)
for(int sd=0;sd<4;sd++)
for(int td=0;td<256;td++){
Color[fd][sd][td]=0;
}
file://计算颜色直方图
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
for(int k=0;k<32;k++)
for(int l=0;l<32;l++){
x=j*32+l;
y=i*32+k;
clr=compDC.GetPixel(x,y);
red=GetRValue(clr);
green=GetGValue(clr);
blue=GetBValue(clr);
file://因为RGB颜色共256^3种,不可能都保存到数组中,因此要先进行一定的提取工作,因为人对亮度的感
file://觉是最明显的,所以可以先将RGB颜色值转成亮度值,这个公式即转换公司,刚好亮度数值是256级的,//就可以统计颜色直方图了
pix_color=red*0.299+green*0.587+blue*0.114;
Color[i][j][pix_color]++;
file://对该象素点的颜色直方图数组中的相信位置加一,是直方图的物理实现
}
return true;
}
以上三个函数实现对某一图像内部的具体计算,而对于基于颜色对方法的外部计算如下:
//计算用户确定的4块位置与其周围位置的颜色对(颜色对现采取用相邻两块的直方图的欧氏距离表示)
pBmp->CalculateColorPair(obj_set.m_x1,obj_set.m_y1);
pBmp->CalculateColorPair(obj_set.m_x2,obj_set.m_y2);
pBmp->CalculateColorPair(obj_set.m_x3,obj_set.m_y3);
pBmp->CalculateColorPair(obj_set.m_x4,obj_set.m_y4);
file://其实在以上的4部计算中,已经形成了初步的颜色对表,在此只不过是将表中的数据从大到小排列出来//并且祛除差值小于某一域值的颜色对
file://计算颜色对结束,形成颜色对表
pBmp->SortColorPair();
file://颜色对表计算出来,表中的数据既是用户输入的该图像的代表特征
pDC->TextOut(10,168,"检索结果:");
CBmpProc *pDestBmp;
CString comp_pic_path;
int disp=0;
int space=-128;
file://读取带比较的图像(在此初定15幅--现定义这15幅图像即图片数据库)
for(int comp_pic=1;comp_pic<=15;comp_pic++){
comp_pic_path.Format("image%d.bmp",comp_pic);
bmp.LoadFromFile(comp_pic_path); // 从库中读入位图
pDestBmp = (CBmpProc*)new(CBmpProc); // 用new分配类目标
pDestBmp->LoadFromObject(bmp, &CRect(0,0,128,128)); // 从bmp中的指定区域读入图像
file://计算当前被比较的图像的颜色直方图
pDestBmp->CalculateColor(*pDC);
int match=0; file://颜色对匹配数目
double ave=0; file://确定匹配时候不能使用精确匹配,所以需要一个差值小于某一域值时的域值
for(int s=0;spair_count;s++){
ave+=pBmp->pair[s].o_dis;
}
ave=ave/pBmp->pair_count; file://这个域值的基数即是用户输入的图片的颜色对表中颜色对的平均值
ave=ave*0.02; file://确定误差小于2%的颜色对均属于这个域值
int pairflag[32]; file://颜色对匹配标志数组,即某一颜色对如果在目标图像中找到,下一次就不能再匹配
for(int t=0;t<32;t++){
pairflag[t]=-1;
}
for(int i=0;i<4;i++){
for(int j=0;j<4;j++){
file://按顺序计算目标图像中一子块与其周围子块的颜色对,然后在用户输入的图像的颜色对表中查询计算出//来的颜色对
pDestBmp->CalculateColorPair(i,j);
for(int scan=0;scan<8;scan++){
if(pDestBmp->pair[scan].x==-1)
break;
}
for(int comp=0;compfor(int count=0;countpair_count;count++){
if((fabs(pBmp->pair[count].o_dis-pDestBmp->pair[comp].o_dis))file://差值小于某域值,则匹配到
pairflag[count]=0; file://置颜色对匹配标志位
match++; file://匹配数加一
break;
}
}
}
file://重新置目标图像的颜色对表为空,因为现在的实现方式是在计算某一子块的颜色对时已经写过了颜色对//表,为保证颜色对表的真确性,必须在查询下一子块的时候重新置颜色对表为空
for(int re=0;repDestBmp->pair[re].x=-1;
}
}
file://如果有60%以上的特征颜色对匹配到,就说明该图像已经被检索到
if(match>=(pBmp->pair_count*0.60)){
file://以下是对检索到的图像的界面上的排版显示
disp++;
space+=128;
file://画图像边框
pDC->Rectangle(10+space-1,190-1,138+space+1,318+1);
pDestBmp->Draw(*pDC, &CRect(10+space,190,138+space,318)); // 将pBmp中的图像绘入DC的指定区域
space+=6;
}
delete (CBmpProc*)pDestBmp; // 删除类目标,delete会自动调用CBmpProc类的析构函数。
}
delete (CBmpProc*)pBmp; // 删除类目标,delete会自动调用类的CBmpProc析构函数。
AfxMessageBox("检索完成");
1、隐藏鼠标
int i = ShowCursor(FALSE);
for ( i; i >= 0 ;i-- )
{
ShowCursor(FALSE);
}
2、显示鼠标
int i = ShowCursor(TRUE);
for ( i;i<= 0;i++ )
{
ShowCursor(TRUE);
}
3、在Picture Control上显示图片
(1)先写一个类
//.h
#pragma once
using namespace Gdiplus;
#pragma comment(lib, "gdiplus.lib")
// CImagePrieviewStatic
class CImagePreviewStatic : public CStatic
{
DECLARE_DYNAMIC(CImagePreviewStatic)
public:
CImagePreviewStatic();
virtual ~CImagePreviewStatic();
virtual BOOL Create();
virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
void SetFilename(LPCTSTR szFilename);
protected:
WCHAR m_wsFilename[_MAX_PATH];
Image *m_img;
Graphics *m_graphics;
DECLARE_MESSAGE_MAP()
};
//.cpp
#include "stdafx.h"
#include "ImagePreviewStatic.h"
// CImagePrieviewStatic
IMPLEMENT_DYNAMIC(CImagePreviewStatic, CStatic)
CImagePreviewStatic::CImagePreviewStatic() : CStatic()
{
m_img = (Image *) NULL;
m_graphics = (Graphics *) NULL;
}
CImagePreviewStatic::~CImagePreviewStatic()
{
//modified by yangjiaxun @ 200701226
if( m_img )
delete m_img;
if( m_graphics )
delete m_graphics;
}
BOOL CImagePreviewStatic::Create()
{
if (GetSafeHwnd() != HWND(NULL))
{
if (m_img != NULL)
{
delete m_img;
m_img = (Image *) NULL;
}
if (m_graphics != NULL)
{
delete m_graphics;
m_graphics = (Graphics *) NULL;
}
m_img = new Image(m_wsFilename);
m_graphics = new Graphics(GetSafeHwnd());
return TRUE;
}
return FALSE;
}
void CImagePreviewStatic::SetFilename(LPCTSTR szFilename)
{
#ifndef _UNICODE
USES_CONVERSION;
#endif
ASSERT(szFilename);
ASSERT(AfxIsValidString(szFilename));
TRACE("%s\n", szFilename);
#ifndef _UNICODE
wcscpy(m_wsFilename, A2W(szFilename));
#else
wcscpy_s(m_wsFilename, szFilename);
#endif
delete m_img;
m_img = new Image(m_wsFilename, FALSE);
Invalidate();
}
void CImagePreviewStatic::DrawItem(LPDRAWITEMSTRUCT /*lpDrawItemStruct*/)
{
Unit units;
CRect rect;
if (m_img != NULL)
{
GetClientRect(&rect);
RectF destRect(REAL(rect.left), REAL(rect.top), REAL(rect.Width()), REAL(rect.Height())),
srcRect;
m_img->GetBounds(&srcRect, &units);
m_graphics->DrawImage(m_img, destRect, srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height, UnitPixel, NULL);
}
}
BEGIN_MESSAGE_MAP(CImagePreviewStatic, CStatic)
END_MESSAGE_MAP()
//在另外的.cpp中进行调用
为该Picture Control添加控件变量 CImagePreviewStatic m_adPic;
m_adPic.Create();
m_adPic.SetFilename(theApp.g_szDefaultADPic);
4、根据屏幕分辨率改变窗体控件的大小
void xx::StretchControl(UINT uID)
{
CRect rcControl;
CRect rcFrame;
GetDlgItem( IDC_FRAME )->GetWindowRect( rcFrame );
ScreenToClient( rcFrame );
GetDlgItem( uID )->GetWindowRect( rcControl );
ScreenToClient( rcControl );
long topRate, leftRate, heightRate, widthRate;
topRate = rcControl.top * GetSystemMetrics( SM_CYSCREEN ) / rcFrame.Height();
leftRate = rcControl.left * GetSystemMetrics( SM_CXSCREEN ) / rcFrame.Width();
heightRate = rcControl.Height() * GetSystemMetrics( SM_CYSCREEN ) / rcFrame.Height();
widthRate = rcControl.Width() * GetSystemMetrics( SM_CXSCREEN ) / rcFrame.Width();
GetDlgItem( uID )->MoveWindow( leftRate, topRate, widthRate, heightRate );
Invalidate();
}
void xx::ShiftControl(UINT uID)
{
CRect rcControl;
CRect rcFrame;
GetDlgItem( IDC_FRAME )->GetWindowRect( rcFrame );
ScreenToClient( rcFrame );
GetDlgItem( uID )->GetWindowRect( rcControl );
ScreenToClient( rcControl );
long topRate, leftRate;
topRate = rcControl.top * GetSystemMetrics( SM_CYSCREEN ) / rcFrame.Height();
leftRate = rcControl.left * GetSystemMetrics( SM_CXSCREEN ) / rcFrame.Width();
GetDlgItem( uID )->MoveWindow( leftRate, topRate, rcControl.Width(), rcControl.Height() );
Invalidate();
}
5、在listctrl中显示图片
(1)addpicture
void xx::addPicture(void)
{
// TODO: Add your control notification handler code here
UpdateData(TRUE);
// validate image directory
// show hour glass cursor
BeginWaitCursor();
// get the names of bitmap files
if ( !GetImageFileNames() )
{
LOG_OUTPUT_WARN(_T("image目录下没有图片。"));
EndWaitCursor();
return;
}
// draw thumbnail images in list control
drawPicture();
// set focus and select the first thumbnail in the list control
//m_ctrList.SetFocus();
m_ctrList.SetItemState(0, LVIS_SELECTED|LVIS_FOCUSED, LVIS_SELECTED|LVIS_FOCUSED);
CRect crt;
int x = 95;
int y = 120;
m_ctrList.GetClientRect(&crt);
x = (crt.Width() - 20)/3;
y = crt.Height()/3;
m_ctrList.SetIconSpacing(x,y);
EndWaitCursor();
}
(2)GetImageFileNames
BOOL xx::GetImageFileNames()
{
CString strExt;
CString strName;
CString strPattern;
BOOL bRC = TRUE;
HANDLE hFind = NULL;
WIN32_FIND_DATA FindFileData;
std::vector<CString> VectorImageNames;
if ( theApp.g_szPicFilePath[theApp.g_szPicFilePath.GetLength() - 1] == TCHAR(''\\'') )
strPattern.Format( TEXT("%s*.*"), theApp.g_szPicFilePath );
else
strPattern.Format( TEXT("%s\\*.*"), theApp.g_szPicFilePath );
hFind = ::FindFirstFile(strPattern, &FindFileData); // strat search
if (hFind == INVALID_HANDLE_VALUE)
{
LPVOID msg;
::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&msg,
0,
NULL);
// MessageBox((LPTSTR)msg, CString((LPCSTR)IDS_TITLE), MB_OK|MB_ICONSTOP);
::LocalFree(msg);
return FALSE;
}
// filter off the system files and directories
if (!(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
!(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM) &&
!(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) &&
!(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_TEMPORARY))
{
// test file extension
strName = FindFileData.cFileName;
strExt = strName.Right(3);
if ( strExt.CompareNoCase( TEXT("jpg") ) == 0 )
{
// save the image file name
VectorImageNames.push_back(strName);
}
}
// loop through to add all of them to our vector
while (bRC)
{
bRC = ::FindNextFile(hFind, &FindFileData);
if (bRC)
{
// filter off the system files and directories
if (!(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
!(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM) &&
!(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) &&
!(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_TEMPORARY))
{
// test file extension
strName = FindFileData.cFileName;
strExt = strName.Right(3);
if ( strExt.CompareNoCase( TEXT("jpg") ) == 0)
{
// save the image file name
//strName = theApp.g_szPicFilePath + strName;
VectorImageNames.push_back(strName);
}
}
}
else
{
DWORD err = ::GetLastError();
if (err != ERROR_NO_MORE_FILES)
{
LPVOID msg;
::FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, err,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&msg, 0, NULL);
//MessageBox((LPTSTR)msg, CString((LPCSTR)IDS_TITLE), MB_OK|MB_ICONSTOP);
::LocalFree(msg);
::FindClose(hFind);
return FALSE;
}
}
} // end of while loop
// close the search handle
::FindClose(hFind);
// update the names, if any
if ( !VectorImageNames.empty() )
{
// reset the image name vector
m_VectorImageNames.clear();
m_VectorImageNames = VectorImageNames;
return TRUE;
}
return FALSE;
}
(3)drawPicture
void xx::drawPicture()
{
CBitmap* pImage = NULL;
HBITMAP hBmp = NULL;
POINT pt;
CString strPath;
int i;
// no images
if (m_VectorImageNames.empty())
return;
// set the length of the space between thumbnails
// you can also calculate and set it based on the length of your list control
int nGap = 6;
// reset our image list
for (i = 0; i<m_ImageListThumb.GetImageCount(); i++)
m_ImageListThumb.Remove(i);
// remove all items from list view
if (m_ctrList.GetItemCount() != 0)
m_ctrList.DeleteAllItems();
//add 20070809
// set the size of the image list
utility uClass;
int m_iSid;
m_iSid = theApp.getSidByMenuSname(theApp.m_strMenuSname);
if (m_iSid == -1)
{
return;
}
theApp.m_menuInfoVec = uClass.getMenuInfoBySid(m_iSid);
m_ImageListThumb.SetImageCount((UINT)theApp.m_menuInfoVec.size());//--modify 20070809
i = 0;
CRect crt;
int iWidth = 95;
int iHeight = 120;
m_ctrList.GetClientRect(&crt);
iWidth = (crt.Width() - 20)/3;
iHeight = (crt.Height() - 20)/3 - 15;
for (UINT k = 0;k < (UINT)theApp.m_menuInfoVec.size();k++)
{
CString strMid;
CString strJpg = _T(".jpg");
CString strMj;
strMid.Format(L"%d",theApp.m_menuInfoVec[k].Mid);
strMj = strMid + strJpg;
std::vector<CString>::iterator iter;
iter = find(m_VectorImageNames.begin(),m_VectorImageNames.end(),strMj);
if(iter == m_VectorImageNames.end())
{
// load the bitmap
strPath.Format( TEXT("%s\\%s"), theApp.g_szPicFilePath, _T("default.jpg") );
USES_CONVERSION;
//Bitmap img( A2W(strPath) );
Bitmap img( strPath);
//Bitmap* pThumbnail = static_cast<Bitmap*>(img.GetThumbnailImage(100, 75, NULL, NULL));
Bitmap* pThumbnail = static_cast<Bitmap*>(img.GetThumbnailImage(iWidth, iHeight, NULL, NULL));
// attach the thumbnail bitmap handle to an CBitmap object
pThumbnail->GetHBITMAP(NULL, &hBmp);
pImage = new CBitmap();
pImage->Attach(hBmp);
// add bitmap to our image list
m_ImageListThumb.Replace(k, pImage, NULL);
// put item to display
// set the image file name as item text
//m_ctrList.InsertItem(i, m_VectorImageNames[i], i);
CString strXS = _T("");
CString strMname = theApp.m_menuInfoVec[k].Mname;
CString strMprice = theApp.m_menuInfoVec[k].Mprice;
CString strMmeasure = theApp.m_menuInfoVec[k].Mmeasure;
LPCTSTR strTemp = LPCTSTR(strMname);
strMname = strTemp;
strTemp = LPCTSTR(strMprice);
strMprice = strTemp;
strTemp = LPCTSTR(strMmeasure);
strMmeasure = strTemp;
strXS = (strMname + _T("\n") + _T("(") + strMprice + _T("/") + strMmeasure + _T(")"));
m_ctrList.InsertItem(k,strXS,k);
// get current item position
m_ctrList.GetItemPosition(k, &pt);
// shift the thumbnail to desired position
pt.x = nGap + k*(75 + nGap);
//m_ctrList.SetItemPosition(k, pt);//delete by wupeng 2007.08.15 for reslove picture''s positon
//i++;
delete pImage;
delete pThumbnail;
}
else{
// load the bitmap
strPath.Format( TEXT("%s\\%s"), theApp.g_szPicFilePath,*iter );
USES_CONVERSION;
//Bitmap img( A2W(strPath) );
Bitmap img( strPath);
//Bitmap* pThumbnail = static_cast<Bitmap*>(img.GetThumbnailImage(100, 75, NULL, NULL));
Bitmap* pThumbnail = static_cast<Bitmap*>(img.GetThumbnailImage(iWidth, iHeight, NULL, NULL));
// attach the thumbnail bitmap handle to an CBitmap object
pThumbnail->GetHBITMAP(NULL, &hBmp);
pImage = new CBitmap();
pImage->Attach(hBmp);
// add bitmap to our image list
m_ImageListThumb.Replace(k, pImage, NULL);
// put item to display
// set the image file name as item text
//m_ctrList.InsertItem(i, m_VectorImageNames[i], i);
CString strXS = _T("");
CString strMname = theApp.m_menuInfoVec[k].Mname;
CString strMprice = theApp.m_menuInfoVec[k].Mprice;
CString strMmeasure = theApp.m_menuInfoVec[k].Mmeasure;
LPCTSTR strTemp = LPCTSTR(strMname);
strMname = strTemp;
strTemp = LPCTSTR(strMprice);
strMprice = strTemp;
strTemp = LPCTSTR(strMmeasure);
strMmeasure = strTemp;
strXS = (strMname + _T("\n") + _T("(") + strMprice + _T("/") + strMmeasure + _T(")"));
m_ctrList.InsertItem(k,strXS,
k);
// get current item position
m_ctrList.GetItemPosition(k, &pt);
// shift the thumbnail to desired position
pt.x = nGap + k*(75 + nGap);
//m_ctrList.SetItemPosition(k, pt);
/*i++;*/
delete pImage;
delete pThumbnail;
}
}
//end add
// let''s show the new thumbnails
m_ctrList.SetRedraw();
}
一、简单介绍
Hough变换是图像处理中从图像中识别几何形状的基本方法之一。Hough变换的基本原理在于利用点与线的对偶性,将原始图像空间的给定的曲线通过曲线表达形式变为参数空间的一个点。这样就把原始图像中给定曲线的检测问题转化为寻找参数空间中的峰值问题。也即把检测整体特性转化为检测局部特性。比如直线、椭圆、圆、弧线等。
二、Hough变换的基本思想
设已知一黑白图像上画了一条直线,要求出这条直线所在的位置。我们知道,直线的方程可以用y=k*x+b 来表示,其中k和b是参数,分别是斜率和截距。过某一点(x0,y0)的所有直线的参数都会满足方程y0=kx0+b。即点(x0,y0)确定了一族直线。方程y0=kx0+b在参数k--b平面上是一条直线,(你也可以是方程b=-x0*k+y0对应的直线)。这样,图像x--y平面上的一个前景像素点就对应到参数平面上的一条直线。我们举个例子说明解决前面那个问题的原理。设图像上的直线是y=x, 我们先取上面的三个点:A(0,0), B(1,1), C(22)。可以求出,过A点的直线的参数要满足方程b=0, 过B点的直线的参数要满足方程1=k+b, 过C点的直线的参数要满足方程2=2k+b, 这三个方程就对应着参数平面上的三条直线,而这三条直线会相交于一点(k=1,b=0)。 同理,原图像上直线y=x上的其它点(如(3,3),(4,4)等) 对应参数平面上的直线也会通过点(k=1,b=0)。这个性质就为我们解决问题提供了方法,就是把图像平面上的点对应到参数平面上的线,最后通过统计特性来解决问题。假如图像平面上有两条直线,那么最终在参数平面上就会看到两个峰值点,依此类推。
简而言之,Hough变换思想为:在原始图像坐标系下的一个点对应了参数坐标系中的一条直线,同样参数坐标系的一条直线对应了原始坐标系下的一个点,然后,原始坐标系下呈现直线的所有点,它们的斜率和截距是相同的,所以它们在参数坐标系下对应于同一个点。这样在将原始坐标系下的各个点投影到参数坐标系下之后,看参数坐标系下有没有聚集点,这样的聚集点就对应了原始坐标系下的直线。
在实际应用中,y=k*x+b形式的直线方程没有办法表示x=c形式的直线(这时候,直线的斜率为无穷大)。所以实际应用中,是采用参数方程p=x*cos(theta)+y*sin(theta)。这样,图像平面上的一个点就对应到参数p---theta平面上的一条曲线上,其它的还是一样。
三、Hough变换推广
1、已知半径的圆
其实Hough变换可以检测任意的已知表达形式的曲线,关键是看其参数空间的选择,参数空间的选择可以根据它的表达形式而定。比如圆的表达形式为 ,所以当检测某一半径的圆的时候,可以选择与原图像空间同样的空间作为参数空间。那么圆图像空间中的一个圆对应了参数空间中的一个点,参数空间中的一个点对应了图像空间中的一个圆,圆图像空间中在同一个圆上的点,它们的参数相同即a,b相同,那么它们在参数空间中的对应的圆就会过同一个点(a,b),所以,将原图像空间中的所有点变换到参数空间后,根据参数空间中点的聚集程度就可以判断出图像空间中有没有近似于圆的图形。如果有的话,这个参数就是圆的参数。
2、未知半径的圆
对于圆的半径未知的情况下,可以看作是有三个参数的圆的检测,中心和半径。这个时候原理仍然相同,只是参数空间的维数升高,计算量增大。图像空间中的任意一个点都对应了参数空间中的一簇圆曲线。 ,其实是一个圆锥型。参数空间中的任意一个点对应了图像空间中的一个圆。
3、椭圆
椭圆有5个自由参数,所以它的参数空间是5维的,因此他的计算量非常大,所以提出了许多的改进算法。
四、总结
图像空间中的在同一个圆,直线,椭圆上的点,每一个点都对应了参数空间中的一个图形,在图像空间中这些点都满足它们的方程这一个条件,所以这些点,每个投影后得到的图像都会经过这个参数空间中的点。也就是在参数空间中它们会相交于一点。所以,当参数空间中的这个相交点的越大的话,那么说明元图像空间中满足这个参数的图形越饱满。越象我们要检测的东西。
Hough变换能够查找任意的曲线,只要你给定它的方程。Hough变换在检验已知形状的目标方面具有受曲线间断影响小和不受图形旋转的影响的优点,即使目标有稍许缺损或污染也能被正确识别。
我如何获得安装在我的系统上的某个特定的 DLL 的版本信息?我尝试着确定系统安装了哪个版本的 comctl32.dll。我见过有些代码调用 GetProcAddress 来获取各种函数,如 InitCommonControlsEx,以确定基于不同版本的函数调用。对于我来说,这是一个坎儿,到底用什么方法获得版本号?
有两种方法:容易的和难的。容易的方法是调用一个专门用于此目的的函数 DllGetVersion。问题是虽然 comctl32.dll 支持该函数,但并不是所有的 DLLs 都具备它。如果不具备 DllGetVersion,那么就得用难的方法——使用 FileVersion API,这可能是你要遭遇到的最为暧昧的 API 之一。我写了一个类 CModuleVersion 来封装两种方法,同时还写了一个Demo程序 VersionDlg 来示范 CModuleVersion 的使用方法。程序画面如 Figure 1 所示。你可以在编辑框中敲入任何系统模块的名字,VersionDlg 将用 DllGetVersion (如果具备这个函数的话)和 FileVersion API 两种方法显示版本信息。源代码参见 Figure 2。
Figure 1 运行中的 VersionDlg 程序
让我们先看容易的方法。DllGetVersion 用 DLL 版本信息填写一个 DLLVERSIONINFO 结构。该结构定义在 Win32 SDK 的 showapi.h 头文件中。许多人可能都没有安装 Platform SDK,那么就得自己定义这个结构了(译者注:实际上,早期的 Developer Studio 不包含这个头文件。后来的 Visual Studio 6.0 安装已经包含该头文件,路经参见:Driver:Program FilesMicrosoft Visual StudioVC98Include),就像我在 VersionDlg 所做的那样。
typedef struct _DllVersionInfo {
DWORD cbSize;
DWORD dwMajorVersion;
DWORD dwMinorVersion;
DWORD dwBuildNumber;
DWORD dwPlatformID;
} DLLVERSIONINFO;
这个结构中的字段基本不用怎么说明就知道是什么意思:dwPlatformID 为 DLLVER_PLATFORM_WINDOWS (value = 1)指 Windows 9x,而 DLLVER_PLATFORM_NT (value = 2)用于 Windows NT。一旦定义了 DLLVERSIONINFO 结构,就可以调用 DllGetVersion 了,该函数的署名如下:
HRESULT DllGetVersion(DLLVERSIONINFO*);
因为并不是任何给定的 Dll 都输出 DllGetVersion 函数,你得按照标准套路来调用它,即调用 GetProcAddress 并判断返回值是否为 NULL。我编写的类 CModuleVersion 中含有一个 DllGetVersion 函数,它把所有细节都进行了封装(参见 Figure 2 中的 ModulVer.cpp。)CModuleVersion 类的使用方法如下:
DLLVERSIONINFO dvi;
if (CModuleVersion::DllGetVersion("comctl32.dll", dvi))
{
// now info is in dvi
}
DllGetVersion 是一个比较新的函数(译者注:在1998年是这样。)对于 comctl32 很好使,因为它实现并输出 DllGetVersion——但是对于那些不输出 DllGetVersion 的 DLLs 来说怎么办呢?例如:shell32.dll 就没有实现 DllGetVersion,如 Figure 3 所示。这时你就得用可怕以及奇怪的 GetFileVersionInfo 和 VerQueryValue 函数,它们在 winver.h 中定义。
Figure 3 No DllGetVersion Info
大多数可执行程序和 DLLs 都具备 VS_VERSION_INFO 资源,在模块的 RC 文件中定义。Figure 4 是 VersionDlg 的 RC 文件中的版本信息。你可以用文本编辑器或者 Visual Studio 直接编辑资源文件中的这段信息。你可以指定文件版本,产品版本等等,以及任何你想要编辑的字段,如:CompanyName、InternalName。文件版本信息与 Exe 或 DLL 文件在资源管理器“属性”页“版本”标签中显示的信息相同(参见 Figure 5)。
Figure 5 Version Tab
等一会儿你就会发现,这些版本 APIs 十分暧昧,很容易把人搞晕菜,但 CModuleVersion 使一切都变得简单明了。这个类派生于 VS_FIXEDFILEINFO(参见 Figure 6),此结构包含“固定的”版本信息,其中有主版本号和次版本号,还有一些 DLLVERSIONINFO 里的东西。使用 CModuleVersion 时,只要像下面这样写即可:
CModuleVersion ver;
if (ver.GetFileVersionInfo(_T("comctl32.dll"))
{
WORD major = HIWORD(ver.dwFileVersionMS);
WORD minor = LOWORD(ver.dwFileVersionMS);
...
}
为了存取 CompanyName 这样的可变信息以及内涵的模块创建信息,你可以用另外一个函数 CModuleVersion:: GetValue,例如,下面代码段执行之后,sCompanyName 的值将类似“XYZ”或“Acme Corporation”这样的公司名称:
CString sCompanyName =
ver.GetValue(_T("CompanyName"));
CModuleVersion 隐藏了获取信息所要做的所有邋遢细节——相信我,都是些邋遢细节!如果你只是想使用 CModuleVersion,那么看到这里就可以打住了;如果你想要了解 CModuleVersion 的工作原理,那就继续往下看。
假设 CModuleVersion::GetFileVersionInfo 能加载模块并获取 HINSTANCE,它调用 ::GetFileVersionInfoSize 来获取版本信息的大小,然后分配一个缓冲并调用 GetFileVersionInfo 来填充该缓冲。原始缓冲(CModuleVersion::m_pVersionInfo)是一个数据块,它包含固定的信息和可变信息。VerQueryValue 将一个指针指向你感兴趣的特定信息的起始位置。例如,为了得到固定的信息(VS_FIXEDFILEINFO),你得这样写
LPVOID lpvi;
UINT iLen;
VerQueryValue(buf, _T("\"), &lpvi, &iLen);
此处 buf 是从 GetFileVersionInfo 返回的完整信息。字符串“”(在 C 中用“\”),你如果把它看作是一个目录,那它就是根信息(有一点像注册表)。VerQueryValue 将 lpvi 置到 VS_FIXEDFILEINFO 的起始处,iLen 为其长度。
以上是获取固定信息的方法,可变信息获取更奇怪,因为你必须首先知道语言 ID 和代码页是什么。在 Winidows 里,代码页指定了一个字符集,它是字符文字与表示它们的 1 或 2 字节值之间映射。标准的 ANSI 代码页是 1252;Unicode 是 1200。Figure 7 是语言ID和代码页的清单。Figure 4 中文件信息里的 Translation 键指定模块的语言ID和代码页。在 CModuleVersion 中,我使用自己的 Translation 结构来获取这个信息。
// in CModuleVersion
struct TRANSLATION {
WORD langID // language ID
WORD charset; // code page
} m_translation;
为了获取语言信息,CModuleVersion 用 VerQueryValue 函数以 VarFileInfoTranslation 作为键。
if (VerQueryValue(m_pVersionInfo,"\VarFileInfo\Translation", &lpvi, &iLen) && iLen >= 4)
{
m_translation = *(TRANSLATION*)lpvi;
}
一旦你知道了语言ID和代码页,你就可以得到 CompanyName 和 InternalName 这样的可变信息。实现方法是构造一个如下形式的查询:
StringFileInfo<langID><codepage><keyname>
这里 <langID> 是十六进制 ASCI 形式的语言ID(中文是 0804;US English 是 0409),<codepage> 是代码页,格式为(1252 即 ANSI 的代码页是04e4),<keyname> 是你想要的键,如:CompanyName。为了构造这个查询,你得用 sprintf 或者 CString::Format 来构造字符串:
\StringFileInfo\040904e4\CompanyName
然后将这个字符串传给 VerQueryValue。如果你对这些繁琐的细节感到晕菜,不用担心——很幸运,CModuleVersion::GetValue 对所有邋遢细节都进行了封装,所以你只要像下面这样写即可:
CString s = ver.GetValue(_T("CompanyName"));
实现了 CModuleVersion,VersionDlg 就简单多了。 它实际上就是一个对话框,这个对话框带有一个编辑框,用于输入模块名称,每当用户在编辑框中敲入模块名称时,MFC 便调用 ON_EN_CHANGE 消息处理例程 CVersionDialog::OnChangedModule。OnChangedModule 例程通过 CModuleVersion 对象及其 GetFileVersionInfo 和 GetDllVersion 函数来获得版本信息,然后将信息显示在对话框的两个静态文本控件中。这个过程很简单。
最后还有个技巧我得提一下。GetFileVersionInfo,VerQueryValue 以及其它有关文件版本函数在一个叫做 version.lib 的库中,你必须将它链接到你程序中。从而避免链接时出现烦人的“undefined symbol”(未定义符号)错误,ModuleVer.h 使用了一个鲜为人知但特别有用的 #pragma comment 语法,即使你忘记在 Project|Settings 的 Link 属性页中添加 Input ==〉Libraries 也没关系,#pragma comment 会告诉链接器与 version.lib 链接。
// 告诉链接器与 version.lib 进行链接
#pragma comment(linker,
"/defaultlib:version.lib")
现在,有人可能会问,为什么这些东西如此重要?以及谁会需要这些东西呢?一般来说,如果你编写的是显示文件属性之类的工具程序,那你只是需要获取诸如 CompanyName 和 LegalCopyright 之类的变量。但你也许发现用 CModuleVersion 从自己的应用程序中吸取文件信息很有用,例如,为了在“关于”对话框和启动屏幕中显示版本信息。如果你使用 CModuleVersion,你只需修改资源文件中相应位置的版本信息即可,“关于”对话框和启动屏幕会自动显示当前最新版本信息。
版本信息另一个重要的用途是确定某个DLL是针对哪种语言编写的,这样你代码能与之对应。随着当今基于 Windows 的编程技术迅猛发展,DLLs 的新版本也随之日新月异,你很快就会发现下面这样的代码越来越多:
if (version <= 470)
// do one thing
else if (version==471)
// do something else
else if (version==472)
// do a third thing
else
// scream
这是一件很郁闷的事情,我敢说这也是微软的大佬们引入 DllGetVersion 来快速获取版本号的一个原因,从而避免了面对让人恐惧的 GetFileVersionInfo 函数,只用它来获取语言 IDs 和代码页(仅在需要获取诸如 CompanyName 这样的信息时使用)。
comctl32.dll 的与众不同也没有什么意外的,这个模块版本问题已经程序员最大的祸害之一,我可怜的邮箱曾被读者关于 comctl32.dll 这个模块的问题撑爆,很多问题都是客户下载了微软最新版本的 comctl32.dll 到机器上之后,应用程序就无法运行了。我会在以后的文章中解释 comctl32.dll 的版本问题,以及新的 toolbar 特性,如何解决 MFC 中 CToolBar 的 bug。现在,由于篇幅所限,我只能点到为止,目前 comctl32.dll 最新的版本为 6.00(随 IE 一起发布)。
最后,感谢上帝,微软已经出台关于可以随你的应用程序一起分发 comctl32.dll!但不是单独分发 comctl32.dll,而是可以随你程序的更新包及其它文件一起分发。详情参见:http://msdn.microsoft.com/developer/downloads/files/40comupd.htm,请在你的新版本出炉之前仔细阅读。
动态连接库的创建步骤:
一、创建Non-MFC DLL动态链接库
1、打开File —> New —> Project选项,选择Win32 Dynamic-Link Library —>sample project
—>工程名:DllDemo
2、新建一个.h文件DllDemo.h
以下是引用片段:
#ifdefDllDemo_EXPORTS
#defineDllAPI__declspec(dllexport)
#else
#defineDllAPI__declspec(dllimport)
extern"C"//原样编译
{
DllAPIint__stdcallMax(inta,intb);//__stdcall使非C/C++语言内能够调用API
}
#endif
3、在DllDemo.cpp文件中导入DllDemo.h文件,并实现Max(int,int)函数
以下是引用片段:
#include"DllDemo.h"
DllAPIint__stdcallMax(inta,intb)
{
if(a==b)
returnNULL;
elseif(a>b)
returna;
else
returnb;
}
4、编译程序生成动态连接库
二、用.def文件创建动态连接库DllDemo.dll。
1、删除DllDemo工程中的DllDemo.h文件。
2、在DllDemo.cpp文件头,删除 #include DllDemo.h语句。
3、向该工程中加入一个文本文件,命名为DllDemo.def并写入如下语句:
LIBRARY MyDll
EXPORTS
Max@1
4、编译程序生成动态连接库。
动态链接的调用步骤:
一、隐式调用
1、 建立DllCnslTest工程
2、 将文件DllDemo.dll、DllDemo.lib拷贝到DllCnslTest工程所在的目录
3、 在DllCnslTest.h中添加如下语句:
以下是引用片段:
#defineDllAPI__declspec(dllimport)
#pragmacomment(lib,"DllDemo.lib")//在编辑器link时,链接到DllDemo.lib文件
extern"C"
{
DllAPIint__stdcallMax(inta,intb);
}
4、在DllCnslTest.cpp文件中添加如下语句:
以下是引用片段:
#include"DllCnslTest.h"//或者#include"DllDemo.h"
voidmain()
{
intvalue;
value=Max(2,9);
printf("TheMaxvalueis%d
",value);
}
5、编译并生成应用程序DllCnslTest.exe
二、显式调用
1、 建立DllWinTest工程
2、 将文件DllDemo.dll拷贝到DllWinTest工程所在的目录或Windows系统目录下。
3、 用vc/bin下的Dumpbin.exe的小程序,查看DLL文件(DllDemo.dll)中的函数结构。
4、 使用类型定义关键字typedef,定义指向和DLL中相同的函数原型指针。
例:
以下是引用片段:
typedefint(*lpMax)(inta,intb);//此语句可以放在.h文件中
5、 通过LoadLibray()将DLL加载到当前的应用程序中并返回当前DLL文件的句柄。
例:
以下是引用片段:
HINSTANCEhDll;//声明一个Dll实例文件句柄
hDll=LoadLibrary("DllDemo.dll");//导入DllDemo.dll动态连接库
6、 通过GetProcAddress()函数获取导入到应用程序中的函数指针。
例:
以下是引用片段:
lpMaxMax;
Max=(lpMax)GetProcAddress(hDLL,"Max");
intvalue;
value=Max(2,9);
printf("TheMaxvalueis%d",value);
7、 函数调用完毕后,使用FreeLibrary()卸载DLL文件。
FreeLibrary(hDll);
8、 编译并生成应用程序DllWinTest.exe
注:显式链接应用程序编译时不需要使用相应的Lib文件。
这是《VC++动态链接库(DLL)编程深入浅出》的第四部分,阅读本文前,请先阅读前三部分:(一)、(二)、(三)。
MFC扩展DLL的内涵为MFC的扩展,用户使用MFC扩展DLL就像使用MFC本身的DLL一样。除了可以在MFC扩展DLL的内部使用MFC以外,MFC扩展DLL与应用程序的接口部分也可以是MFC。我们一般使用MFC扩展DLL来包含一些MFC的增强功能,譬如扩展MFC的CStatic、CButton等类使之具备更强大的能力。
使用Visual C++向导生产MFC扩展DLL时,MFC向导会自动增加DLL的入口函数DllMain:
extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
// Remove this if you use lpReserved
UNREFERENCED_PARAMETER(lpReserved);
if (dwReason == DLL_PROCESS_ATTACH)
{
TRACE0("MFCEXPENDDLL.DLL Initializing! ");
// Extension DLL one-time initialization
if (!AfxInitExtensionModule(MfcexpenddllDLL, hInstance))
return 0;
// Insert this DLL into the resource chain
// NOTE: If this Extension DLL is being implicitly linked to by
// an MFC Regular DLL (such as an ActiveX Control)
// instead of an MFC application, then you will want to
// remove this line from DllMain and put it in a separate
// function exported from this Extension DLL. The Regular DLL
// that uses this Extension DLL should then explicitly call that
// function to initialize this Extension DLL. Otherwise,
// the CDynLinkLibrary object will not be attached to the
// Regular DLL's resource chain, and serious problems will
// result.
new CDynLinkLibrary(MfcexpenddllDLL);
}
else if (dwReason == DLL_PROCESS_DETACH)
{
TRACE0("MFCEXPENDDLL.DLL Terminating! ");
// Terminate the library before destructors are called
AfxTermExtensionModule(MfcexpenddllDLL);
}
return 1; // ok
}
上述代码完成MFC扩展DLL的初始化和终止处理。
由于MFC扩展DLL导出函数和变量的方式与其它DLL没有什么区别,我们不再细致讲解。下面直接给出一个MFC扩展DLL的创建及在应用程序中调用它的例子。
6.1 MFC扩展DLL的创建
下面我们将在MFC扩展DLL中导出一个按钮类CSXButton(扩展自MFC的CButton类),类CSXButton是一个用以取代 CButton的类,它使你能在同一个按钮上显示位图和文字,而MFC的按钮仅可显示二者之一。类CSXbutton的源代码在Internet上广泛流传,有很好的“群众基础”,因此用这个类来讲解MFC扩展DLL有其特殊的功效。
MFC中包含一些宏,这些宏在DLL和调用DLL的应用程序中被以不同的方式展开,这使得在DLL和应用程序中,使用统一的一个宏就可以表示出输出和输入的不同意思:
// for data
#ifndef AFX_DATA_EXPORT
#define AFX_DATA_EXPORT __declspec(dllexport)
#endif
#ifndef AFX_DATA_IMPORT
#define AFX_DATA_IMPORT __declspec(dllimport)
#endif
// for classes
#ifndef AFX_CLASS_EXPORT
#define AFX_CLASS_EXPORT __declspec(dllexport)
#endif
#ifndef AFX_CLASS_IMPORT
#define AFX_CLASS_IMPORT __declspec(dllimport)
#endif
// for global APIs
#ifndef AFX_API_EXPORT
#define AFX_API_EXPORT __declspec(dllexport)
#endif
#ifndef AFX_API_IMPORT
#define AFX_API_IMPORT __declspec(dllimport)
#endif
#ifndef AFX_EXT_DATA
#ifdef _AFXEXT
#define AFX_EXT_CLASS AFX_CLASS_EXPORT
#define AFX_EXT_API AFX_API_EXPORT
#define AFX_EXT_DATA AFX_DATA_EXPORT
#define AFX_EXT_DATADEF
#else
#define AFX_EXT_CLASS AFX_CLASS_IMPORT
#define AFX_EXT_API AFX_API_IMPORT
#define AFX_EXT_DATA AFX_DATA_IMPORT
#define AFX_EXT_DATADEF
#endif
#endif
导出一个类,直接在类声明头文件中使用AFX_EXT_CLASS即可,以下是导出CSXButton类的例子:
#ifndef _SXBUTTON_H
#define _SXBUTTON_H
#defineSXBUTTON_CENTER-1
class AFX_EXT_CLASS CSXButton : public CButton
{
// Construction
public:
CSXButton();
// Attributes
private:
//Positioning
BOOL m_bUseOffset;
CPoint m_pointImage;
CPoint m_pointText;
int m_nImageOffsetFromBorder;
int m_nTextOffsetFromImage;
//Image
HICON m_hIcon;
HBITMAP m_hBitmap;
HBITMAP m_hBitmapDisabled;
int m_nImageWidth, m_nImageHeight;
//Color Tab
char m_bColorTab;
COLORREFm_crColorTab;
//State
BOOL m_bDefault;
UINT m_nOldAction;
UINT m_nOldState;
// Operations
public:
//Positioning
int SetImageOffset( int nPixels );
int SetTextOffset( int nPixels );
CPointSetImagePos( CPoint p );
CPointSetTextPos( CPoint p );
//Image
BOOLSetIcon( UINT nID, int nWidth, int nHeight );
BOOLSetBitmap( UINT nID, int nWidth, int nHeight );
BOOLSetMaskedBitmap( UINT nID, int nWidth, int nHeight, COLORREF crTransparentMask );
BOOLHasImage() { return (BOOL)( m_hIcon != 0 | m_hBitmap != 0 ); }
//Color Tab
voidSetColorTab(COLORREF crTab);
//State
BOOLSetDefaultButton( BOOL bState = TRUE );
private:
BOOLSetBitmapCommon( UINT nID, int nWidth, int nHeight, COLORREF crTransparentMask, BOOL bUseMask );
voidCheckPointForCentering( CPoint &p, int nWidth, int nHeight );
voidRedraw();
// Overrides
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CSXButton)
public:
virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
//}}AFX_VIRTUAL
// Implementation
public:
virtual ~CSXButton();
// Generated message map functions
protected:
//{{AFX_MSG(CSXButton)
afx_msg LRESULT OnGetText(WPARAM wParam, LPARAM lParam);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
#endif
把SXBUTTON.CPP文件直接添加到工程,编译工程,得到“mfcexpenddll.lib”和“mfcexpenddll.dll”两个文件。我们用Visual Studio自带的Depends工具可以查看这个.dll,发现其导出了众多符号(见图15)。
图15 导出类时导出的大量符号 (+放大该图片)
这些都是类的构造函数、析构函数及其它成员函数和变量经编译器处理过的符号,我们直接用__declspec(dllexport)语句声明类就导出了这些符号。
如果我们想用.lib文件导出这些符号,是非常困难的,我们需要在工程中生成.map文件,查询.map文件的符号,然后将其一一导出。如图16,打开DLL工程的settings选项,再选择Link,勾选其中的产生MAP文件(Generate mapfile)就可以产生.map文件了。
打开mfcexpenddll工程生成的.map文件,我们发现其中包含了图15中所示的符号(symbol)
0001:00000380 ?HasImage@CSXButton@@QAEHXZ 10001380 f i SXBUTTON.OBJ
0001:000003d0 ??0CSXButton@@QAE@XZ 100013d0 f SXBUTTON.OBJ
0001:00000500 ??_GCSXButton@@UAEPAXI@Z 10001500 f i SXBUTTON.OBJ
0001:00000570 ??_ECSXButton@@UAEPAXI@Z 10001570 f i SXBUTTON.OBJ
0001:00000630 ??1CSXButton@@UAE@XZ 10001630 f SXBUTTON.OBJ
0001:00000700 ?_GetBaseMessageMap@CSXButton@@KGPBUAFX_MSGMAP@@XZ 10001700 f SXBUTTON.OBJ
0001:00000730 ?GetMessageMap@CSXButton@@MBEPBUAFX_MSGMAP@@XZ 10001730 f SXBUTTON.OBJ
0001:00000770 ?Redraw@CSXButton@@AAEXXZ 10001770 f i SXBUTTON.OBJ
0001:000007d0 ?SetIcon@CSXButton@@QAEHIHH@Z 100017d0 f SXBUTTON.OBJ
……………………………………………………………………..//省略
图16 产生.map文件 (+放大该图片)
所以,对于MFC扩展DLL,我们不宜以.lib文件导出类。
第4节我们对非MFC DLL进行了介绍,这一节将详细地讲述MFC规则DLL的创建与使用技巧。
另外,自从本文开始连载后,收到了一些读者的e-mail。有的读者提出了一些问题,笔者将在本文的最后一次连载中选取其中的典型问题进行解答。由于时间的关系,对于读者朋友的来信,笔者暂时不能一一回复,还望海涵!由于笔者的水平有限,文中难免有错误和纰漏,也热诚欢迎读者朋友不吝指正!
5. MFC规则DLL
5.1 概述
MFC规则DLL的概念体现在两方面:
(1) 它是MFC的
“是MFC的”意味着可以在这种DLL的内部使用MFC;
(2) 它是规则的
“是规则的”意味着它不同于MFC扩展DLL,在MFC规则DLL的内部虽然可以使用MFC,但是其与应用程序的接口不能是MFC。而MFC扩展DLL与应用程序的接口可以是MFC,可以从MFC扩展DLL中导出一个MFC类的派生类。
Regular DLL能够被所有支持DLL技术的语言所编写的应用程序调用,当然也包括使用MFC的应用程序。在这种动态连接库中,包含一个从CWinApp继承下来的类,DllMain函数则由MFC自动提供。
Regular DLL分为两类:
(1)静态链接到MFC 的规则DLL
静态链接到MFC的规则DLL与MFC库(包括MFC扩展 DLL)静态链接,将MFC库的代码直接生成在.dll文件中。在调用这种DLL的接口时,MFC使用DLL的资源。因此,在静态链接到MFC 的规则DLL中不需要进行模块状态的切换。
使用这种方法生成的规则DLL其程序较大,也可能包含重复的代码。
(2)动态链接到MFC 的规则DLL
动态链接到MFC 的规则DLL 可以和使用它的可执行文件同时动态链接到 MFC DLL 和任何MFC扩展 DLL。在使用了MFC共享库的时候,默认情况下,MFC使用主应用程序的资源句柄来加载资源模板。这样,当DLL和应用程序中存在相同ID的资源时(即所谓的资源重复问题),系统可能不能获得正确的资源。因此,对于共享MFC DLL的规则DLL,我们必须进行模块切换以使得MFC能够找到正确的资源模板。
我们可以在Visual C++中设置MFC规则DLL是静态链接到MFC DLL还是动态链接到MFC DLL。如图8,依次选择Visual C++的project -> Settings -> General菜单或选项,在Microsoft Foundation Classes中进行设置。
图8 设置动态/静态链接MFC DLL
5.2 MFC规则DLL的创建
我们来一步步讲述使用MFC向导创建MFC规则DLL的过程,首先新建一个project,如图9,选择project的类型为MFC AppWizard(dll)。点击OK进入如图10所示的对话框。
图9 MFC DLL工程的创建
图10所示对话框中的1区选择MFC DLL的类别。
2区选择是否支持automation(自动化)技术, automation 允许用户在一个应用程序中操纵另外一个应用程序或组件。例如,我们可以在应用程序中利用 Microsoft Word 或Microsoft Excel的工具,而这种使用对用户而言是透明的。自动化技术可以大大简化和加快应用程序的开发。
3区选择是否支持Windows Sockets,当选择此项目时,应用程序能在 TCP/IP 网络上进行通信。 CWinApp派生类的InitInstance成员函数会初始化通讯端的支持,同时工程中的StdAfx.h文件会自动include <AfxSock.h>头文件。
添加socket通讯支持后的InitInstance成员函数如下:
BOOL CRegularDllSocketApp::InitInstance()
{
if (!AfxSocketInit())
{
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
return FALSE;
}
return TRUE;
}
4区选择是否由MFC向导自动在源代码中添加注释,一般我们选择“Yes,please”。
图10 MFC DLL的创建选项
5.3 一个简单的MFC规则DLL
这个DLL的例子(属于静态链接到MFC 的规则DLL)中提供了一个如图11所示的对话框。
图11 MFC规则DLL例子
在DLL中添加对话框的方式与在MFC应用程序中是一样的。
在图11所示DLL中的对话框的Hello按钮上点击时将MessageBox一个“Hello,pconline的网友”对话框,下面是相关的文件及源代码,其中删除了MFC向导自动生成的绝大多数注释(下载本工程附件):
第一组文件:CWinApp继承类的声明与实现
// RegularDll.h : main header file for the REGULARDLL DLL
#if !defined(AFX_REGULARDLL_H__3E9CB22B_588B_4388_B778_B3416ADB79B3__INCLUDED_)
#define AFX_REGULARDLL_H__3E9CB22B_588B_4388_B778_B3416ADB79B3__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#ifndef __AFXWIN_H__
#error include 'stdafx.h' before including this file for PCH
#endif
#include "resource.h" // main symbols
class CRegularDllApp : public CWinApp
{
public:
CRegularDllApp();
DECLARE_MESSAGE_MAP()
};
#endif
// RegularDll.cpp : Defines the initialization routines for the DLL.
#include "stdafx.h"
#include "RegularDll.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
BEGIN_MESSAGE_MAP(CRegularDllApp, CWinApp)
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CRegularDllApp construction
CRegularDllApp::CRegularDllApp()
{
}
/////////////////////////////////////////////////////////////////////////////
// The one and only CRegularDllApp object
CRegularDllApp theApp;
分析:
在这一组文件中定义了一个继承自CWinApp的类CRegularDllApp,并同时定义了其的一个实例theApp。乍一看,您会以为它是一个MFC应用程序,因为MFC应用程序也包含这样的在工程名后添加“App”组成类名的类(并继承自CWinApp类),也定义了这个类的一个全局实例theApp。
我们知道,在MFC应用程序中CWinApp取代了SDK程序中WinMain的地位,SDK程序WinMain所完成的工作由CWinApp的三个函数完成:
virtual BOOL InitApplication( );
virtual BOOL InitInstance( );
virtual BOOL Run( ); //传说中MFC程序的“活水源头”
但是MFC规则DLL并不是MFC应用程序,它所继承自CWinApp的类不包含消息循环。这是因为,MFC规则DLL不包含CWinApp::Run机制,主消息泵仍然由应用程序拥有。如果DLL 生成无模式对话框或有自己的主框架窗口,则应用程序的主消息泵必须调用从DLL 导出的函数来调用PreTranslateMessage成员函数。
另外,MFC规则DLL与MFC 应用程序中一样,需要将所有 DLL中元素的初始化放到InitInstance 成员函数中。
第二组文件 自定义对话框类声明及实现(点击查看附件)
分析:
这一部分的编程与一般的应用程序根本没有什么不同,我们照样可以利用MFC类向导来自动为对话框上的控件添加事件。MFC类向导照样会生成类似ON_BN_CLICKED(IDC_HELLO_BUTTON, OnHelloButton)的消息映射宏。
第三组文件 DLL中的资源文件
//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by RegularDll.rc
//
#define IDD_DLL_DIALOG 1000
#define IDC_HELLO_BUTTON 1000
分析:
在MFC规则DLL中使用资源也与在MFC应用程序中使用资源没有什么不同,我们照样可以用Visual C++的资源编辑工具进行资源的添加、删除和属性的更改。
第四组文件 MFC规则DLL接口函数
#include "StdAfx.h"
#include "DllDialog.h"
extern "C" __declspec(dllexport) void ShowDlg(void)
{
CDllDialog dllDialog;
dllDialog.DoModal();
}
分析:
这个接口并不使用MFC,但是在其中却可以调用MFC扩展类CdllDialog的函数,这体现了“规则”的概类。
与非MFC DLL完全相同,我们可以使用__declspec(dllexport)声明或在.def中引出的方式导出MFC规则DLL中的接口。
5.4 MFC规则DLL的调用
笔者编写了如图12的对话框MFC程序(下载本工程附件)来调用5.3节的MFC规则DLL,在这个程序的对话框上点击“调用DLL”按钮时弹出5.3节MFC规则DLL中的对话框。
图12 MFC规则DLL的调用例子
下面是“调用DLL”按钮单击事件的消息处理函数:
void CRegularDllCallDlg::OnCalldllButton()
{
typedef void (*lpFun)(void);
HINSTANCE hDll; //DLL句柄
hDll = LoadLibrary("RegularDll.dll");
if (NULL==hDll)
{
MessageBox("DLL加载失败");
}
lpFun addFun; //函数指针
lpFun pShowDlg = (lpFun)GetProcAddress(hDll,"ShowDlg");
if (NULL==pShowDlg)
{
MessageBox("DLL中函数寻找失败");
}
pShowDlg();
}
上述例子中给出的是显示调用的方式,可以看出,其调用方式与第4节中非MFC DLL的调用方式没有什么不同。
我们照样可以在EXE程序中隐式调用MFC规则DLL,只需要将DLL工程生成的.lib文件和.dll文件拷入当前工程所在的目录,并在RegularDllCallDlg.cpp文件(图12所示对话框类的实现文件)的顶部添加:
#pragma comment(lib,"RegularDll.lib")
void ShowDlg(void);
并将void CRegularDllCallDlg::OnCalldllButton() 改为:
void CRegularDllCallDlg::OnCalldllButton()
{
ShowDlg();
}
5.5 共享MFC DLL的规则DLL的模块切换
应用程序进程本身及其调用的每个DLL模块都具有一个全局唯一的HINSTANCE句柄,它们代表了DLL或EXE模块在进程虚拟空间中的起始地址。进程本身的模块句柄一般为0x400000,而DLL模块的缺省句柄为0x10000000。如果程序同时加载了多个DLL,则每个DLL模块都会有不同的HINSTANCE。应用程序在加载DLL时对其进行了重定位。
共享MFC DLL(或MFC扩展DLL)的规则DLL涉及到HINSTANCE句柄问题,HINSTANCE句柄对于加载资源特别重要。EXE和DLL都有其自己的资源,而且这些资源的ID可能重复,应用程序需要通过资源模块的切换来找到正确的资源。如果应用程序需要来自于DLL的资源,就应将资源模块句柄指定为DLL的模块句柄;如果需要EXE文件中包含的资源,就应将资源模块句柄指定为EXE的模块句柄。
这次我们创建一个动态链接到MFC DLL的规则DLL(下载本工程附件),在其中包含如图13的对话框。
图13 DLL中的对话框
另外,在与这个DLL相同的工作区中生成一个基于对话框的MFC程序,其对话框与图12完全一样。但是在此工程中我们另外添加了一个如图14的对话框。
图14 EXE中的对话框
图13和图14中的对话框除了caption不同(以示区别)以外,其它的都相同。
尤其值得特别注意,在DLL和EXE中我们对图13和图14的对话框使用了相同的资源ID=2000,在DLL和EXE工程的resource.h中分别有如下的宏:
//DLL中对话框的ID
#define IDD_DLL_DIALOG 2000
//EXE中对话框的ID
#define IDD_EXE_DIALOG 2000
与5.3节静态链接MFC DLL的规则DLL相同,我们还是在规则DLL中定义接口函数ShowDlg,原型如下:
#include "StdAfx.h"
#include "SharedDll.h"
void ShowDlg(void)
{
CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框
dlg.DoModal();
}
而为应用工程主对话框的“调用DLL”的单击事件添加如下消息处理函数:
void CSharedDllCallDlg::OnCalldllButton()
{
ShowDlg();
}
我们以为单击“调用DLL”会弹出如图13所示DLL中的对话框,可是可怕的事情发生了,我们看到是图14所示EXE中的对话框!
惊讶?
产生这个问题的根源在于应用程序与MFC规则DLL共享MFC DLL(或MFC扩展DLL)的程序总是默认使用EXE的资源,我们必须进行资源模块句柄的切换,其实现方法有三:
方法一 在DLL接口函数中使用:
AFX_MANAGE_STATE(AfxGetStaticModuleState());
我们将DLL中的接口函数ShowDlg改为:
void ShowDlg(void)
{
//方法1:在函数开始处变更,在函数结束时恢复
//将AFX_MANAGE_STATE(AfxGetStaticModuleState());作为接口函数的第一//条语句进行模块状态切换
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框
dlg.DoModal();
}
这次我们再点击EXE程序中的“调用DLL”按钮,弹出的是DLL中的如图13的对话框!嘿嘿,弹出了正确的对话框资源。
AfxGetStaticModuleState是一个函数,其原型为:
AFX_MODULE_STATE* AFXAPI AfxGetStaticModuleState( );
该函数的功能是在栈上(这意味着其作用域是局部的)创建一个AFX_MODULE_STATE类(模块全局数据也就是模块状态)的实例,对其进行设置,并将其指针pModuleState返回。
AFX_MODULE_STATE类的原型如下:
// AFX_MODULE_STATE (global data for a module)
class AFX_MODULE_STATE : public CNoTrackObject
{
public:
#ifdef _AFXDLL
AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion);
AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion,BOOL bSystem);
#else
AFX_MODULE_STATE(BOOL bDLL);
#endif
~AFX_MODULE_STATE();
CWinApp* m_pCurrentWinApp;
HINSTANCE m_hCurrentInstanceHandle;
HINSTANCE m_hCurrentResourceHandle;
LPCTSTR m_lpszCurrentAppName;
… //省略后面的部分
}
AFX_MODULE_STATE类利用其构造函数和析构函数进行存储模块状态现场及恢复现场的工作,类似汇编中call指令对pc指针和sp寄存器的保存与恢复、中断服务程序的中断现场压栈与恢复以及操作系统线程调度的任务控制块保存与恢复。
许多看似不着边际的知识点居然有惊人的相似!
AFX_MANAGE_STATE是一个宏,其原型为:
AFX_MANAGE_STATE( AFX_MODULE_STATE* pModuleState )
该宏用于将pModuleState设置为当前的有效模块状态。当离开该宏的作用域时(也就离开了pModuleState所指向栈上对象的作用域),先前的模块状态将由AFX_MODULE_STATE的析构函数恢复。
方法二 在DLL接口函数中使用:
AfxGetResourceHandle();
AfxSetResourceHandle(HINSTANCE xxx);
AfxGetResourceHandle用于获取当前资源模块句柄,而AfxSetResourceHandle则用于设置程序目前要使用的资源模块句柄。
我们将DLL中的接口函数ShowDlg改为:
void ShowDlg(void)
{
//方法2的状态变更
HINSTANCE save_hInstance = AfxGetResourceHandle();
AfxSetResourceHandle(theApp.m_hInstance);
CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框
dlg.DoModal();
//方法2的状态还原
AfxSetResourceHandle(save_hInstance);
}
通过AfxGetResourceHandle和AfxSetResourceHandle的合理变更,我们能够灵活地设置程序的资源模块句柄,而方法一则只能在DLL接口函数退出的时候才会恢复模块句柄。方法二则不同,如果将ShowDlg改为:
extern CSharedDllApp theApp; //需要声明theApp外部全局变量
void ShowDlg(void)
{
//方法2的状态变更
HINSTANCE save_hInstance = AfxGetResourceHandle();
AfxSetResourceHandle(theApp.m_hInstance);
CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框
dlg.DoModal();
//方法2的状态还原
AfxSetResourceHandle(save_hInstance);
//使用方法2后在此处再进行操作针对的将是应用程序的资源
CDialog dlg1(IDD_DLL_DIALOG); //打开ID为2000的对话框
dlg1.DoModal();
}
在应用程序主对话框的“调用DLL”按钮上点击,将看到两个对话框,相继为DLL中的对话框(图13)和EXE中的对话框(图14)。
方法三 由应用程序自身切换
资源模块的切换除了可以由DLL接口函数完成以外,由应用程序自身也能完成(下载本工程附件)。
现在我们把DLL中的接口函数改为最简单的:
void ShowDlg(void)
{
CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框
dlg.DoModal();
}
而将应用程序的OnCalldllButton函数改为:
void CSharedDllCallDlg::OnCalldllButton()
{
//方法3:由应用程序本身进行状态切换
//获取EXE模块句柄
HINSTANCE exe_hInstance = GetModuleHandle(NULL);
//或者HINSTANCE exe_hInstance = AfxGetResourceHandle();
//获取DLL模块句柄
HINSTANCE dll_hInstance = GetModuleHandle("SharedDll.dll");
AfxSetResourceHandle(dll_hInstance); //切换状态
ShowDlg(); //此时显示的是DLL的对话框
AfxSetResourceHandle(exe_hInstance); //恢复状态
//资源模块恢复后再调用ShowDlg
ShowDlg(); //此时显示的是EXE的对话框
}
方法三中的Win32函数GetModuleHandle可以根据DLL的文件名获取DLL的模块句柄。如果需要得到EXE模块的句柄,则应调用带有Null参数的GetModuleHandle。
方法三与方法二的不同在于方法三是在应用程序中利用AfxGetResourceHandle和AfxSetResourceHandle进行资源模块句柄切换的。同样地,在应用程序主对话框的“调用DLL”按钮上点击,也将看到两个对话框,相继为DLL中的对话框(图13)和EXE中的对话框(图14)。
在下一节我们将对MFC扩展DLL进行详细分析和实例讲解,欢迎您继续关注本系列连载。
上节给大家介绍了静态链接库与库的调试与查看(
动态链接库(DLL)编程深入浅出(一)),本节主要介绍非MFC DLL。
4.非MFC DLL
4.1一个简单的DLL
第2节给出了以静态链接库方式提供add函数接口的方法,接下来我们来看看怎样用动态链接库实现一个同样功能的add函数。
如图6,在VC++中new一个Win32 Dynamic-Link Library工程dllTest(单击此处下载本工程附件)。注意不要选择MFC AppWizard(dll),因为用MFC AppWizard(dll)建立的将是第5、6节要讲述的MFC 动态链接库。
图6 建立一个非MFC DLL
在建立的工程中添加lib.h及lib.cpp文件,源代码如下:
/* 文件名:lib.h */
#ifndef LIB_H
#define LIB_H
extern "C" int __declspec(dllexport)add(int x, int y);
#endif
/* 文件名:lib.cpp */
#include "lib.h"
int add(int x, int y)
{
return x + y;
}
与第2节对静态链接库的调用相似,我们也建立一个与DLL工程处于同一工作区的应用工程dllCall,它调用DLL中的函数add,其源代码如下:
#include <stdio.h>
#include <windows.h>
typedef int(*lpAddFun)(int, int); //宏定义函数指针类型
int main(int argc, char *argv[])
{
HINSTANCE hDll; //DLL句柄
lpAddFun addFun; //函数指针
hDll = LoadLibrary("..DebugdllTest.dll");
if (hDll != NULL)
{
addFun = (lpAddFun)GetProcAddress(hDll, "add");
if (addFun != NULL)
{
int result = addFun(2, 3);
printf("%d", result);
}
FreeLibrary(hDll);
}
return 0;
}
分析上述代码,dllTest工程中的lib.cpp文件与第2节静态链接库版本完全相同,不同在于lib.h对函数add的声明前面添加了__declspec(dllexport)语句。这个语句的含义是声明函数add为DLL的导出函数。DLL内的函数分为两种:1)DLL导出函数,可供应用程序调用;
(2) DLL内部函数,只能在DLL程序使用,应用程序无法调用它们。
而应用程序对本DLL的调用和对第2节静态链接库的调用却有较大差异,下面我们来逐一分析。
首先,语句typedef int ( * lpAddFun)(int,int)定义了一个与add函数接受参数类型和返回值均相同的函数指针类型。随后,在main函数中定义了lpAddFun的实例addFun;
其次,在函数main中定义了一个DLL HINSTANCE句柄实例hDll,通过Win32 Api函数LoadLibrary动态加载了DLL模块并将DLL模块句柄赋给了hDll;
再次,在函数main中通过Win32 Api函数GetProcAddress得到了所加载DLL模块中函数add的地址并赋给了addFun。经由函数指针addFun进行了对DLL中add函数的调用;
最后,应用工程使用完DLL后,在函数main中通过Win32 Api函数FreeLibrary释放了已经加载的DLL模块。
通过这个简单的例子,我们获知DLL定义和调用的一般概念:
(1)DLL中需以某种特定的方式声明导出函数(或变量、类);
(2)应用工程需以某种特定的方式调用DLL的导出函数(或变量、类)。
下面我们来对“特定的方式进行”阐述。
4.2 声明导出函数
DLL中导出函数的声明有两种方式:一种为4.1节例子中给出的在函数声明中加上__declspec(dllexport),这里不再举例说明;另外一种方式是采用模块定义(.def) 文件声明,.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。
下面的代码演示了怎样同.def文件将函数add声明为DLL导出函数(需在dllTest工程中添加lib.def文件):
; lib.def : 导出DLL函数
LIBRARY dllTest
EXPORTS
add @ 1
.def文件的规则为:
(1)LIBRARY语句说明.def文件相应的DLL;
(2)EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);
(3).def 文件中的注释由每个注释行开始处的分号 (;) 指定,且注释不能与语句共享一行。
由此可以看出,例子中lib.def文件的含义为生成名为“dllTest”的动态链接库,导出其中的add函数,并指定add函数的序号为1。
4.3 DLL的调用方式
在4.1节的例子中我们看到了由“LoadLibrary-GetProcAddress-FreeLibrary”系统Api提供的三位一体“DLL加载-DLL函数地址获取-DLL释放”方式,这种调用方式称为DLL的动态调用。
动态调用方式的特点是完全由编程者用 API 函数加载和卸载 DLL,程序员可以决定 DLL 文件何时加载或不加载,显式链接在运行时决定加载哪个 DLL 文件。
与动态调用方式相对应的就是静态调用方式,“有动必有静”,这来源于物质世界的对立统一。“动与静”,其对立与统一竟无数次在技术领域里得到验证,譬如静态IP与DHCP、静态路由与动态路由等。从前文我们已经知道,库也分为静态库与动态库DLL,而想不到,深入到DLL内部,其调用方式也分为静态与动态。“动与静”,无处不在。《周易》已认识到有动必有静的动静平衡观,《易.系辞》曰:“动静有常,刚柔断矣”。哲学意味着一种普遍的真理,因此,我们经常可以在枯燥的技术领域看到哲学的影子。
静态调用方式的特点是由编译系统完成对DLL的加载和应用程序结束时 DLL 的卸载。当调用某DLL的应用程序结束时,若系统中还有其它程序使用该 DLL,则Windows对DLL的应用记录减1,直到所有使用该DLL的程序都结束时才释放它。静态调用方式简单实用,但不如动态调用方式灵活。
下面我们来看看静态调用的例子(单击此处下载本工程
附件),将编译dllTest工程所生成的.lib和.dll文件拷入dllCall工程所在的路径,dllCall执行下列代码:
#pragma comment(lib,"dllTest.lib")
//.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息
extern "C" __declspec(dllimport) add(int x,int y);
int main(int argc, char* argv[])
{
int result = add(2,3);
printf("%d",result);
return 0;
}
由上述代码可以看出,静态调用方式的顺利进行需要完成两个动作:
(1)告诉编译器与DLL相对应的.lib文件所在的路径及文件名,#pragma comment(lib,"dllTest.lib")就是起这个作用。
程序员在建立一个DLL文件时,连接器会自动为其生成一个对应的.lib文件,该文件包含了DLL 导出函数的符号名及序号(并不含有实际的代码)。在应用程序里,.lib文件将作为DLL的替代文件参与编译。
(2)声明导入函数,extern "C" __declspec(dllimport) add(int x,int y)语句中的__declspec(dllimport)发挥这个作用。
静态调用方式不再需要使用系统API来加载、卸载DLL以及获取DLL中导出函数的地址。这是因为,当程序员通过静态链接方式编译生成应用程序时,应用程序中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE 文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在 EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL 函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就象调用程序内部的其他函数一样。
4.4 DllMain函数
Windows在加载DLL的时候,需要一个入口函数,就如同控制台或DOS程序需要main函数、WIN32程序需要WinMain函数一样。在前面的例子中,DLL并没有提供DllMain函数,应用工程也能成功引用DLL,这是因为Windows在找不到DllMain的时候,系统会从其它运行库中引入一个不做任何操作的缺省DllMain函数版本,并不意味着DLL可以放弃DllMain函数。
根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用工程中引用DllMain函数,DllMain是自动被调用的。
我们来看一个DllMain函数的例子(单击此处下载本工程附件)。
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
printf(" process attach of dll");
break;
case DLL_THREAD_ATTACH:
printf(" thread attach of dll");
break;
case DLL_THREAD_DETACH:
printf(" thread detach of dll");
break;
case DLL_PROCESS_DETACH:
printf(" process detach of dll");
break;
}
return TRUE;
}
DllMain函数在DLL被加载和卸载时被调用,在单个线程启动和终止时,DLLMain函数也被调用,ul_reason_for_call指明了被调用的原因。原因共有4种,即PROCESS_ATTACH、PROCESS_DETACH、THREAD_ATTACH和THREAD_DETACH,以switch语句列出。
来仔细解读一下DllMain的函数头BOOL APIENTRY DllMain( HANDLE hModule, WORD ul_reason_for_call, LPVOID lpReserved )。
APIENTRY被定义为__stdcall,它意味着这个函数以标准Pascal的方式进行调用,也就是WINAPI方式;
进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识,只有在特定的进程内部有效,句柄代表了DLL模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用,这就是函数参数hModule的来历。
执行下列代码:
hDll = LoadLibrary("..DebugdllTest.dll");
if (hDll != NULL)
{
addFun = (lpAddFun)GetProcAddress(hDll, MAKEINTRESOURCE(1));
//MAKEINTRESOURCE直接使用导出文件中的序号
if (addFun != NULL)
{
int result = addFun(2, 3);
printf(" call add in dll:%d", result);
}
FreeLibrary(hDll);
}
我们看到输出顺序为:
process attach of dll
call add in dll:5
process detach of dll
这一输出顺序验证了DllMain被调用的时机。
代码中的GetProcAddress ( hDll, MAKEINTRESOURCE ( 1 ) )值得留意,它直接通过.def文件中为add函数指定的顺序号访问add函数,具体体现在MAKEINTRESOURCE ( 1 ),MAKEINTRESOURCE是一个通过序号获取函数名的宏,定义为(节选自winuser.h):
#define MAKEINTRESOURCEA(i) (LPSTR)((DWORD)((WORD)(i)))
#define MAKEINTRESOURCEW(i) (LPWSTR)((DWORD)((WORD)(i)))
#ifdef UNICODE
#define MAKEINTRESOURCE MAKEINTRESOURCEW
#else
#define MAKEINTRESOURCE MAKEINTRESOURCEA
4.5 __stdcall约定
来仔细解读一下DllMain的函数头BOOL APIENTRY DllMain( HANDLE hModule, WORD ul_reason_for_call, LPVOID lpReserved )。
APIENTRY被定义为__stdcall,它意味着这个函数以标准Pascal的方式进行调用,也就是WINAPI方式;
进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识,只有在特定的进程内部有效,句柄代表了DLL模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用,这就是函数参数hModule的来历。
执行下列代码:
hDll = LoadLibrary("..DebugdllTest.dll");
if (hDll != NULL)
{
addFun = (lpAddFun)GetProcAddress(hDll, MAKEINTRESOURCE(1));
//MAKEINTRESOURCE直接使用导出文件中的序号
if (addFun != NULL)
{
int result = addFun(2, 3);
printf(" call add in dll:%d", result);
}
FreeLibrary(hDll);
}
我们看到输出顺序为:
process attach of dll
call add in dll:5
process detach of dll
这一输出顺序验证了DllMain被调用的时机。
代码中的GetProcAddress ( hDll, MAKEINTRESOURCE ( 1 ) )值得留意,它直接通过.def文件中为add函数指定的顺序号访问add函数,具体体现在MAKEINTRESOURCE ( 1 ),MAKEINTRESOURCE是一个通过序号获取函数名的宏,定义为(节选自winuser.h):
#define MAKEINTRESOURCEA(i) (LPSTR)((DWORD)((WORD)(i)))
#define MAKEINTRESOURCEW(i) (LPWSTR)((DWORD)((WORD)(i)))
#ifdef UNICODE
#define MAKEINTRESOURCE MAKEINTRESOURCEW
#else
#define MAKEINTRESOURCE MAKEINTRESOURCEA
4.5 __stdcall约定
如果通过VC++编写的DLL欲被其他语言编写的程序调用,应将函数的调用方式声明为__stdcall方式,WINAPI都采用这种方式,而C/C++缺省的调用方式却为__cdecl。__stdcall方式与__cdecl对函数名最终生成符号的方式不同。若采用C编译方式(在C++中需将函数声明为extern "C"),__stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;而__cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。
Windows编程中常见的几种函数类型声明宏都是与__stdcall和__cdecl有关的(节选自windef.h):
#define CALLBACK __stdcall //这就是传说中的回调函数
#define WINAPI __stdcall //这就是传说中的WINAPI
#define WINAPIV __cdecl
#define APIENTRY WINAPI //DllMain的入口就在这里
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
在lib.h中,应这样声明add函数:
int __stdcall add(int x, int y);
在应用工程中函数指针类型应定义为:
typedef int(__stdcall *lpAddFun)(int, int);
若在lib.h中将函数声明为__stdcall调用,而应用工程中仍使用typedef int (* lpAddFun)(int,int),运行时将发生错误(因为类型不匹配,在应用工程中仍然是缺省的__cdecl调用),弹出如图7所示的对话框。
图7 调用约定不匹配时的运行错误
图8中的那段话实际上已经给出了错误的原因,即“This is usually a result of …”。
单击此处下载__stdcall调用例子工程源代码附件。
4.6 DLL导出变量
DLL定义的全局变量可以被调用进程访问;DLL也可以访问调用进程的全局数据,我们来看看在应用工程中引用DLL中变量的例子(单击此处下载本工程附件)。
/* 文件名:lib.h */
#ifndef LIB_H
#define LIB_H
extern int dllGlobalVar;
#endif
/* 文件名:lib.cpp */
#include "lib.h"
#include <windows.h>
int dllGlobalVar;
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
dllGlobalVar = 100; //在dll被加载时,赋全局变量为100
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
;文件名:lib.def
;在DLL中导出变量
LIBRARY "dllTest"
EXPORTS
dllGlobalVar CONSTANT
;或dllGlobalVar DATA
GetGlobalVar
从lib.h和lib.cpp中可以看出,全局变量在DLL中的定义和使用方法与一般的程序设计是一样的。若要导出某全局变量,我们需要在.def文件的EXPORTS后添加:
变量名 CONSTANT //过时的方法
或
变量名 DATA //VC++提示的新方法
在主函数中引用DLL中定义的全局变量:
#include <stdio.h>
#pragma comment(lib,"dllTest.lib")
extern int dllGlobalVar;
int main(int argc, char *argv[])
{
printf("%d ", *(int*)dllGlobalVar);
*(int*)dllGlobalVar = 1;
printf("%d ", *(int*)dllGlobalVar);
return 0;
}
特别要注意的是用extern int dllGlobalVar声明所导入的并不是DLL中全局变量本身,而是其地址,应用程序必须通过强制指针转换来使用DLL中的全局变量。这一点,从*(int*)dllGlobalVar可以看出。因此在采用这种方式引用DLL全局变量时,千万不要进行这样的赋值操作:
dllGlobalVar = 1;
其结果是dllGlobalVar指针的内容发生变化,程序中以后再也引用不到DLL中的全局变量了。
在应用工程中引用DLL中全局变量的一个更好方法是:
#include <stdio.h>
#pragma comment(lib,"dllTest.lib")
extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)导入
int main(int argc, char *argv[])
{
printf("%d ", dllGlobalVar);
dllGlobalVar = 1; //这里就可以直接使用, 无须进行强制指针转换
printf("%d ", dllGlobalVar);
return 0;
}
通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了,笔者建议在一切可能的情况下都使用这种方式。
4.7 DLL导出类
DLL中定义的类可以在应用工程中使用。
下面的例子里,我们在DLL中定义了point和circle两个类,并在应用工程中引用了它们(单击此处下载本工程附件)。
//文件名:point.h,point类的声明
#ifndef POINT_H
#define POINT_H
#ifdef DLL_FILE
class _declspec(dllexport) point //导出类point
#else
class _declspec(dllimport) point //导入类point
#endif
{
public:
float y;
float x;
point();
point(float x_coordinate, float y_coordinate);
};
#endif
//文件名:point.cpp,point类的实现
#ifndef DLL_FILE
#define DLL_FILE
#endif
#include "point.h"
//类point的缺省构造函数
point::point()
{
x = 0.0;
y = 0.0;
}
//类point的构造函数
point::point(float x_coordinate, float y_coordinate)
{
x = x_coordinate;
y = y_coordinate;
}
//文件名:circle.h,circle类的声明
#ifndef CIRCLE_H
#define CIRCLE_H
#include "point.h"
#ifdef DLL_FILE
class _declspec(dllexport)circle //导出类circle
#else
class _declspec(dllimport)circle //导入类circle
#endif
{
public:
void SetCentre(const point ¢rePoint);
void SetRadius(float r);
float GetGirth();
float GetArea();
circle();
private:
float radius;
point centre;
};
#endif
//文件名:circle.cpp,circle类的实现
#ifndef DLL_FILE
#define DLL_FILE
#endif
#include "circle.h"
#define PI 3.1415926
//circle类的构造函数
circle::circle()
{
centre = point(0, 0);
radius = 0;
}
//得到圆的面积
float circle::GetArea()
{
return PI *radius * radius;
}
//得到圆的周长
float circle::GetGirth()
{
return 2 *PI * radius;
}
//设置圆心坐标
void circle::SetCentre(const point ¢rePoint)
{
centre = centrePoint;
}
//设置圆的半径
void circle::SetRadius(float r)
{
radius = r;
}
类的引用:
#include "..circle.h" //包含类声明头文件
#pragma comment(lib,"dllTest.lib");
int main(int argc, char *argv[])
{
circle c;
point p(2.0, 2.0);
c.SetCentre(p);
c.SetRadius(1.0);
printf("area:%f girth:%f", c.GetArea(), c.GetGirth());
return 0;
}
从上述源代码可以看出,由于在DLL的类实现代码中定义了宏DLL_FILE,故在DLL的实现中所包含的类声明实际上为:
class _declspec(dllexport) point //导出类point
{
…
}
和
class _declspec(dllexport) circle //导出类circle
{
…
}
而在应用工程中没有定义DLL_FILE,故其包含point.h和circle.h后引入的类声明为:
class _declspec(dllimport) point //导入类point
{
…
}
和
class _declspec(dllimport) circle //导入类circle
{
…
}
不错,正是通过DLL中的
class _declspec(dllexport) class_name //导出类circle
{
…
}
与应用程序中的
class _declspec(dllimport) class_name //导入类
{
…
}
匹对来完成类的导出和导入的!
我们往往通过在类的声明头文件中用一个宏来决定使其编译为class _declspec(dllexport) class_name还是class _declspec(dllimport) class_name版本,这样就不再需要两个头文件。本程序中使用的是:
#ifdef DLL_FILE
class _declspec(dllexport) class_name //导出类
#else
class _declspec(dllimport) class_name //导入类
#endif
实际上,在MFC DLL的讲解中,您将看到比这更简便的方法,而此处仅仅是为了说明_declspec(dllexport)与_declspec(dllimport)匹对的问题。
由此可见,应用工程中几乎可以看到DLL中的一切,包括函数、变量以及类,这就是DLL所要提供的强大能力。只要DLL释放这些接口,应用程序使用它就将如同使用本工程中的程序一样!
本章虽以VC++为平台讲解非MFC DLL,但是这些普遍的概念在其它语言及开发环境中也是相同的,其思维方式可以直接过渡。
接下来,我们将要研究MFC规则DLL(待续...)
1.概论
先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。在仓库的发展史上经历了“无库-静态链接库-动态链接库”的时代。
静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
对动态链接库,我们还需建立如下概念:
(1)DLL 的编制与具体的编程语言及编译器无关
只要遵循约定的DLL接口规范和调用方式,用各种语言编写的DLL都可以相互调用。譬如Windows提供的系统DLL(其中包括了Windows的API),在任何开发环境中都能被调用,不在乎其是Visual Basic、Visual C++还是Delphi。
(2)动态链接库随处可见
我们在Windows目录下的system32文件夹中会看到kernel32.dll、user32.dll和gdi32.dll,windows的大多数API都包含在这些DLL中。kernel32.dll中的函数主要处理内存管理和进程调度;user32.dll中的函数主要控制用户界面;gdi32.dll中的函数则负责图形方面的操作。
一般的程序员都用过类似MessageBox的函数,其实它就包含在user32.dll这个动态链接库中。由此可见DLL对我们来说其实并不陌生。
(3)VC动态链接库的分类
Visual C++支持三种DLL,它们分别是Non-MFC DLL(非MFC动态库)、MFC Regular DLL(MFC规则DLL)、MFC Extension DLL(MFC扩展DLL)。
非MFC动态库不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用;MFC规则DLL 包含一个继承自CWinApp的类,但其无消息循环;MFC扩展DLL采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。
由于本文篇幅较长,内容较多,势必需要先对阅读本文的有关事项进行说明,下面以问答形式给出。
问:本文主要讲解什么内容?
答:本文详细介绍了DLL编程的方方面面,努力学完本文应可以对DLL有较全面的掌握,并能编写大多数DLL程序。
问:如何看本文?
答:本文每一个主题的讲解都附带了源代码例程,可以随文下载(每个工程都经WINRAR压缩)。所有这些例程都由笔者编写并在VC++6.0中调试通过。
当然看懂本文不是读者的最终目的,读者应亲自动手实践才能真正掌握DLL的奥妙。
问:学习本文需要什么样的基础知识?
答:如果你掌握了C,并大致掌握了C++,了解一点MFC的知识,就可以轻松地看懂本文。
2.静态链接库
对静态链接库的讲解不是本文的重点,但是在具体讲解DLL之前,通过一个静态链接库的例子可以快速地帮助我们建立“库”的概念。
图1 建立一个静态链接库
如图1,在VC++6.0中new一个名称为libTest的static library工程(单击此处下载本工程附件),并新建lib.h和lib.cpp两个文件,lib.h和lib.cpp的源代码如下:
//文件:lib.h
#ifndef LIB_H
#define LIB_H
extern "C" int add(int x,int y); //声明为C编译、连接方式的外部函数
#endif
//文件:lib.cpp
#include "lib.h"
int add(int x,int y)
{
return x + y;
}
编译这个工程就得到了一个.lib文件,这个文件就是一个函数库,它提供了add的功能。将头文件和.lib文件提交给用户后,用户就可以直接使用其中的add函数了。
标准Turbo C2.0中的C库函数(我们用来的scanf、printf、memcpy、strcpy等)就来自这种静态库。
下面来看看怎么使用这个库,在libTest工程所在的工作区内new一个libCall工程。libCall工程仅包含一个main.cpp文件,它演示了静态链接库的调用方法,其源代码如下:
#include <stdio.h>
#include "..lib.h"
#pragma comment( lib, "..debuglibTest.lib" ) //指定与静态库一起连接
int main(int argc, char* argv[])
{
printf( "2 + 3 = %d", add( 2, 3 ) );
}
静态链接库的调用就是这么简单,或许我们每天都在用,可是我们没有明白这个概念。代码中#pragma comment( lib , "..debuglibTest.lib" )的意思是指本文件生成的.obj文件应与libTest.lib一起连接。
如果不用#pragma comment指定,则可以直接在VC++中设置,如图2,依次选择tools、options、directories、library files菜单或选项,填入库文件路径。图2中加红圈的部分为我们添加的libTest.lib文件的路径。
图2 在VC中设置库文件路径
这个静态链接库的例子至少让我们明白了库函数是怎么回事,它们是哪来的。我们现在有下列模糊认识了:
(1)库不是个怪物,编写库的程序和编写一般的程序区别不大,只是库不能单独执行;
(2)库提供一些可以给别的程序调用的东东,别的程序要调用它必须以某种方式指明它要调用之。
以上从静态链接库分析而得到的对库的懵懂概念可以直接引申到动态链接库中,动态链接库与静态链接库在编写和调用上的不同体现在库的外部接口定义及调用方式略有差异。
3.库的调试与查看
在具体进入各类DLL的详细阐述之前,有必要对库文件的调试与查看方法进行一下介绍,因为从下一节开始我们将面对大量的例子工程。
由于库文件不能单独执行,因而在按下F5(开始debug模式执行)或CTRL+F5(运行)执行时,其弹出如图3所示的对话框,要求用户输入可执行文件的路径来启动库函数的执行。这个时候我们输入要调用该库的EXE文件的路径就可以对库进行调试了,其调试技巧与一般应用工程的调试一样。
图3 库的调试与“运行”
通常有比上述做法更好的调试途径,那就是将库工程和应用工程(调用库的工程)放置在同一VC工作区,只对应用工程进行调试,在应用工程调用库中函数的语句处设置断点,执行后按下F11,这样就单步进入了库中的函数。第2节中的libTest和libCall工程就放在了同一工作区,其工程结构如图4所示。
图4 把库工程和调用库的工程放入同一工作区进行调试
上述调试方法对静态链接库和动态链接库而言是一致的。所以本文提供下载的所有源代码中都包含了库工程和调用库的工程,这二者都被包含在一个工作区内,这是笔者提供这种打包下载的用意所在。
动态链接库中的导出接口可以使用Visual C++的Depends工具进行查看,让我们用Depends打开系统目录中的user32.dll,看到了吧?红圈内的就是几个版本的MessageBox了!原来它真的在这里啊,原来它就在这里啊!
图5 用Depends查看DLL
当然Depends工具也可以显示DLL的层次结构,若用它打开一个可执行文件则可以看出这个可执行文件调用了哪些DLL。
好,让我们正式进入动态链接库的世界,先来看看最一般的DLL,即非MFC DLL(待续...)