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"http://原樣編譯
{
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"http://或者#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(待續...)