MFC OpenGL标签云 (转)

初识标签云是在去年年末,一看到这个应用我就特别感兴趣。还记得08年在北京做Mobile应用的时候就在想,是不是可以通过某种方式做界面扩展,让相对狭小的手机屏幕只显示有效部分,而扩展部分可以在屏幕以外,通过某种方式将他们调到屏幕以内。后来证实做这种思考的人不只是我自己,因为现在划屏应用已经是移动终端的基本应用模式了。当时也想过能不能在纵向做界面延伸,让界面深入到屏幕里面,要知道那可是无限的空间,不过没有想到什么好的方式。见到标签云的那一刻,我知道这就是我想要的,所以当时我就决定用OpenGL模拟一下,然后拿出来和大家共同探讨,看看有没有可能做成一个更成熟些应用。不过过年回来工作比较忙,所以也就搁置了。这两天刚阶段性的完成一个项目,所以得了空可以写这篇博客。
要实现功能就要先分析原理,通过使用和观察我觉得标签云的每一个标签都分布在一个球体上,球在视平面上的投影是一个圆,而整个球就根据鼠标和这个投影圆的位置关系不同而旋转。鼠标离圆心越远转速愉快,反之则越慢。球体旋转是围绕一根通过球心且与视平面平行的转轴进行的,鼠标和投影圆圆心的位置关系同样控制者转轴的方向,那就是转轴垂直于投影圆圆心和鼠标的连线。最后一点,旋转过程总是迎着鼠标进行。
知道了基本原理就要开始分析算法了,实现三维空间用OpenGL这自不必说,我曾经写过一篇日志《OpenGL控件》介绍了一个内置OpenGL基本框架环境的自定义控件,这次就是极基于这个控件的一个扩展应用。我还曾经在《三维向量类》一文中介绍过我自己写的一个进行三维程序设计时很实用的向量类CVector,当然这次建模还是要依靠它。唯一的问题是OpenGL对于中文支持的不好,有没有什么方法可以方便的显示中文呢?我这里采用的方法是在内存中生成一张文字的贴图,然后用Alpha混合的方式赋予一个plan,这样就可以利用系统字库在OpenGL中显示各种类型丰富的文字了。好了,主要问题都解决了,开始写代码。
首先定义一个结构体来代表标签元中的一个标签:
 
typedef struct tagTAGNODE
{
CVector m_vtrPos; // 标签位置
float m_fWidth, // 标签长
m_fHeight; // 标签宽
UINT m_uTexture; // 贴图号
}CloudTag, *lpCloudTag;

然后在COpenGLCtrl里维护一个CloudTag数组用来管理所有标签,也就是整个云。函数AddCloud用来添加一个标签,其代码如下:

view plaincopyprint?void COpenGLCtrl::AddCloud(CString strName, COLORREF clrText, CString strFontName, int nPointSize, int nWeight, UINT uCharset)
{
int nMode;
CSize sizeText;
CRect rectTag;
CBitmap bitmapTemp, *pOldBitmap;
CFont fontTag, *pOldFont;
COLORREF clrOld;
LOGFONT lf;
CDC* pDC = GetDC();
CDC* pMemDC = new CDC;
lpCloudTag pCloudTag = new CloudTag;
int nExtend = 5;
float fTemp;


//设置字体
memset(&lf, 0, sizeof(LOGFONT));

//设置字体样式
wcscpy_s(lf.lfFaceName, strFontName);
lf.lfHeight = nPointSize;
lf.lfWeight = nWeight;
lf.lfCharSet = uCharset;

fontTag.CreateFontIndirect(&lf);

//创建位图内存
pMemDC->CreateCompatibleDC(pDC);
pOldFont = pMemDC->SelectObject(&fontTag);
nMode = pMemDC->SetBkMode(TRANSPARENT);
clrOld = pMemDC->SetTextColor(clrText);

sizeText = pMemDC->GetTextExtent(strName, strName.GetLength());
pCloudTag->m_fWidth = float(sizeText.cx+nExtend)/40;
pCloudTag->m_fHeight = float(sizeText.cy+nExtend)/40;
rectTag.SetRect(0, 0, sizeText.cx+nExtend, sizeText.cy+nExtend);

bitmapTemp.CreateCompatibleBitmap(pDC, rectTag.Width(), rectTag.Height());
pOldBitmap = pMemDC->SelectObject(&bitmapTemp);

//填充客户区
pMemDC->DrawText(strName, strName.GetLength(), rectTag, DT_SINGLELINE|DT_CENTER|DT_VCENTER);
pMemDC->SetBkMode(nMode);
pMemDC->SelectObject(pOldFont);
pMemDC->SelectObject(pOldBitmap);
pMemDC->SetTextColor(clrOld);
delete pMemDC;

BITMAP bmData;
bitmapTemp.GetBitmap(&bmData);
unsigned char* pData = new unsigned char[bmData.bmWidthBytes*bmData.bmHeight];
bitmapTemp.GetBitmapBits(bmData.bmWidthBytes*bmData.bmHeight, pData);
for(int i=0; i<bmData.bmWidth; i++)
{
for(int j=0; j<bmData.bmHeight; j++)
{
if(pData[i*4+j*bmData.bmWidthBytes]!=0 || pData[i*4+j*bmData.bmWidthBytes+1]!=0 || pData[i*4+j*bmData.bmWidthBytes+2]!=0)
pData[i*4+j*bmData.bmWidthBytes+3]=255;
}
}
glGenTextures(1, &pCloudTag->m_uTexture);
glBindTexture(GL_TEXTURE_2D, pCloudTag->m_uTexture);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, bmData.bmWidth, bmData.bmHeight, GL_RGBA, GL_UNSIGNED_BYTE, pData);

delete pData;

fTemp = (rand()%50)/10.0f+3;
pCloudTag->m_vtrPos = CVector(0, 0, fTemp);
fTemp = (rand()%360-180)/360.0f;
pCloudTag->m_vtrPos.Rotate(fTemp, CVector(1, 0, 0));
fTemp = (rand()%360-180)/360.0f;
pCloudTag->m_vtrPos.Rotate(fTemp, CVector(0, 1, 0));
fTemp = (rand()%360-180)/360.0f;
pCloudTag->m_vtrPos.Rotate(fTemp, CVector(0, 0, 1));
m_tcDemo.Add(pCloudTag);
}

void COpenGLCtrl::AddCloud(CString strName, COLORREF clrText, CString strFontName, int nPointSize, int nWeight, UINT uCharset)
{
int nMode;
CSize sizeText;
CRect rectTag;
CBitmap bitmapTemp, *pOldBitmap;
CFont fontTag, *pOldFont;
COLORREF clrOld;
LOGFONT lf;
CDC* pDC = GetDC();
CDC* pMemDC = new CDC;
lpCloudTag pCloudTag = new CloudTag;
int nExtend = 5;
float fTemp;


//设置字体
memset(&lf, 0, sizeof(LOGFONT));

//设置字体样式
wcscpy_s(lf.lfFaceName, strFontName);
lf.lfHeight = nPointSize;
lf.lfWeight = nWeight;
lf.lfCharSet = uCharset;

fontTag.CreateFontIndirect(&lf);

//创建位图内存
pMemDC->CreateCompatibleDC(pDC);
pOldFont = pMemDC->SelectObject(&fontTag);
nMode = pMemDC->SetBkMode(TRANSPARENT);
clrOld = pMemDC->SetTextColor(clrText);

sizeText = pMemDC->GetTextExtent(strName, strName.GetLength());
pCloudTag->m_fWidth = float(sizeText.cx+nExtend)/40;
pCloudTag->m_fHeight = float(sizeText.cy+nExtend)/40;
rectTag.SetRect(0, 0, sizeText.cx+nExtend, sizeText.cy+nExtend);

bitmapTemp.CreateCompatibleBitmap(pDC, rectTag.Width(), rectTag.Height());
pOldBitmap = pMemDC->SelectObject(&bitmapTemp);

//填充客户区
pMemDC->DrawText(strName, strName.GetLength(), rectTag, DT_SINGLELINE|DT_CENTER|DT_VCENTER);
pMemDC->SetBkMode(nMode);
pMemDC->SelectObject(pOldFont);
pMemDC->SelectObject(pOldBitmap);
pMemDC->SetTextColor(clrOld);
delete pMemDC;

BITMAP bmData;
bitmapTemp.GetBitmap(&bmData);
unsigned char* pData = new unsigned char[bmData.bmWidthBytes*bmData.bmHeight];
bitmapTemp.GetBitmapBits(bmData.bmWidthBytes*bmData.bmHeight, pData);
for(int i=0; i<bmData.bmWidth; i++)
{
for(int j=0; j<bmData.bmHeight; j++)
{
if(pData[i*4+j*bmData.bmWidthBytes]!=0 || pData[i*4+j*bmData.bmWidthBytes+1]!=0 || pData[i*4+j*bmData.bmWidthBytes+2]!=0)
pData[i*4+j*bmData.bmWidthBytes+3]=255;
}
}
glGenTextures(1, &pCloudTag->m_uTexture);
glBindTexture(GL_TEXTURE_2D, pCloudTag->m_uTexture);
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, bmData.bmWidth, bmData.bmHeight, GL_RGBA, GL_UNSIGNED_BYTE, pData);

delete pData;

fTemp = (rand()%50)/10.0f+3;
pCloudTag->m_vtrPos = CVector(0, 0, fTemp);
fTemp = (rand()%360-180)/360.0f;
pCloudTag->m_vtrPos.Rotate(fTemp, CVector(1, 0, 0));
fTemp = (rand()%360-180)/360.0f;
pCloudTag->m_vtrPos.Rotate(fTemp, CVector(0, 1, 0));
fTemp = (rand()%360-180)/360.0f;
pCloudTag->m_vtrPos.Rotate(fTemp, CVector(0, 0, 1));
m_tcDemo.Add(pCloudTag);


这个函数是生成标签的核心函数,在程序里标签表现出来的形式就是文本,所以函数支持对文本的几乎一切定制,这包括内容、颜色、字体、字号、样式等。函数首先会创建一个字体,然后根据文本内容需要的大小创建一个内存位图,将文字绘制于这个位图之上。准备好文字的位图就要通过它来制作材质了,这里有一个问题,位图是没有Alpha通道的,怎么办呢,有没有什么相对简单的方法处理呢?这里我的处理方法是这样的,看代码,通过GetBitmap获取位图信息然后观察bmData.bmWidth和bmData.bmWidthBytes的值,可以发现bmWidthBytes是bmWidth的四倍。原来位图在内存里是以RGBA的方式存储的,所以通过GetBitmapBits获取到的数据就是4位的。众所周知,内存位图创建时是黑色的,这里我认定标签文字不能为黑色,所以检测数据如果是黑色的就把Alpha设为透明,不是就设为不透明,然后创建贴图。最后设置向量,在一个范围内随机生成一个长度做一个指向屏幕外的向量,在以x、y、z三个轴随机旋转一个角度,这样就完成了一个标签的建模。
渲染的部分很简单,就是常规的OpenGL绘制,只不过加上了材质设置了Alpha透明。其实这个Demo做的比较粗糙,文字周围存在黑边,不过做这个例子不是特别追求表现所以也就没有特别处理,代码如下:


 view plaincopyprint?void COpenGLCtrl::OnPaint()   
{
CPaintDC dc(this);

//绘背景色
COLORREF clrBkgnd = GetSysColor(COLOR_BTNFACE);
glClearColor(float(GetRValue(clrBkgnd))/255, float(GetGValue(clrBkgnd))/255, float(GetBValue(clrBkgnd))/255, 0.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

//设置场景坐标系
glLoadIdentity();
//设置观察点
gluLookAt(0, 0, 30, 0, 0, 0, 0, 1, 0);

INT_PTR nCount = m_tcDemo.GetCount();
for(int i=0; i<nCount; i++)
{
lpCloudTag pCloudTag = m_tcDemo.GetAt(i);

glBindTexture(GL_TEXTURE_2D, pCloudTag->m_uTexture);
glEnable(GL_BLEND);
glEnable(GL_ALPHA_TEST);
glAlphaFunc(GL_GREATER, 0.1f);
glBegin(GL_QUADS);
glTexCoord2i(0, 0), glVertex3f(pCloudTag->m_vtrPos.m_fVectorX-pCloudTag->m_fWidth/2, pCloudTag->m_vtrPos.m_fVectorY+pCloudTag->m_fHeight/2, pCloudTag->m_vtrPos.m_fVectorZ);
glTexCoord2i(1, 0), glVertex3f(pCloudTag->m_vtrPos.m_fVectorX+pCloudTag->m_fWidth/2, pCloudTag->m_vtrPos.m_fVectorY+pCloudTag->m_fHeight/2, pCloudTag->m_vtrPos.m_fVectorZ);
glTexCoord2i(1, 1), glVertex3f(pCloudTag->m_vtrPos.m_fVectorX+pCloudTag->m_fWidth/2, pCloudTag->m_vtrPos.m_fVectorY-pCloudTag->m_fHeight/2, pCloudTag->m_vtrPos.m_fVectorZ);
glTexCoord2i(0, 1), glVertex3f(pCloudTag->m_vtrPos.m_fVectorX-pCloudTag->m_fWidth/2, pCloudTag->m_vtrPos.m_fVectorY-pCloudTag->m_fHeight/2, pCloudTag->m_vtrPos.m_fVectorZ);
glEnd();
glDisable(GL_ALPHA_TEST);
glDisable(GL_BLEND);

}

//翻页
SwapBuffers(m_pDC->m_hDC);
}

void COpenGLCtrl::OnPaint()
{
CPaintDC dc(this);

//绘背景色
COLORREF clrBkgnd = GetSysColor(COLOR_BTNFACE);
glClearColor(float(GetRValue(clrBkgnd))/255, float(GetGValue(clrBkgnd))/255, float(GetBValue(clrBkgnd))/255, 0.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

//设置场景坐标系
glLoadIdentity();
//设置观察点
gluLookAt(0, 0, 30, 0, 0, 0, 0, 1, 0);

INT_PTR nCount = m_tcDemo.GetCount();
for(int i=0; i<nCount; i++)
{
lpCloudTag pCloudTag = m_tcDemo.GetAt(i);

glBindTexture(GL_TEXTURE_2D, pCloudTag->m_uTexture);
glEnable(GL_BLEND);
glEnable(GL_ALPHA_TEST);
glAlphaFunc(GL_GREATER, 0.1f);
glBegin(GL_QUADS);
glTexCoord2i(0, 0), glVertex3f(pCloudTag->m_vtrPos.m_fVectorX-pCloudTag->m_fWidth/2, pCloudTag->m_vtrPos.m_fVectorY+pCloudTag->m_fHeight/2, pCloudTag->m_vtrPos.m_fVectorZ);
glTexCoord2i(1, 0), glVertex3f(pCloudTag->m_vtrPos.m_fVectorX+pCloudTag->m_fWidth/2, pCloudTag->m_vtrPos.m_fVectorY+pCloudTag->m_fHeight/2, pCloudTag->m_vtrPos.m_fVectorZ);
glTexCoord2i(1, 1), glVertex3f(pCloudTag->m_vtrPos.m_fVectorX+pCloudTag->m_fWidth/2, pCloudTag->m_vtrPos.m_fVectorY-pCloudTag->m_fHeight/2, pCloudTag->m_vtrPos.m_fVectorZ);
glTexCoord2i(0, 1), glVertex3f(pCloudTag->m_vtrPos.m_fVectorX-pCloudTag->m_fWidth/2, pCloudTag->m_vtrPos.m_fVectorY-pCloudTag->m_fHeight/2, pCloudTag->m_vtrPos.m_fVectorZ);
glEnd();
glDisable(GL_ALPHA_TEST);
glDisable(GL_BLEND);

}

//翻页
SwapBuffers(m_pDC->m_hDC);
}
剩下的事情就是鼠标控制和动画实现了,由于我的向量类很好的支持了旋转功能,而且标签云建模就是通过向量实现的,所以这一步也就好说了。所有标签都是通过以原点为起点的向量定位的,所以转轴也是通过原点的在XOZ面上的向量。在OnMouseHover中计算状态和转速,然后在OnTimer里旋转每个标签的向量就是了,代码如下:
view plaincopyprint?LRESULT COpenGLCtrl::OnMouseHover(WPARAM wParam, LPARAM lParam)  
{
CRect rectView;
CPoint point;

GetWindowRect(rectView);
GetCursorPos(&point);

m_vtrRotate.SetVector(float(-point.x), float(point.y), 0, -float(rectView.right+rectView.left)/2, float(rectView.bottom+rectView.top)/2, 0);
m_vtrRotate.Rotate(-PI/2, CVector(0, 0, 1));

m_fAngle = m_vtrRotate.GetMod()/2000;
return 1;
}

LRESULT COpenGLCtrl::OnMouseHover(WPARAM wParam, LPARAM lParam)
{
CRect rectView;
CPoint point;

GetWindowRect(rectView);
GetCursorPos(&point);

m_vtrRotate.SetVector(float(-point.x), float(point.y), 0, -float(rectView.right+rectView.left)/2, float(rectView.bottom+rectView.top)/2, 0);
m_vtrRotate.Rotate(-PI/2, CVector(0, 0, 1));

m_fAngle = m_vtrRotate.GetMod()/2000;
return 1;
}
[cpp] view plaincopyprint?void COpenGLCtrl::OnTimer(UINT_PTR nIDEvent)
{
switch(nIDEvent)
{
case TIMER_MOVE:
{
if(m_fAngle==0)
return;

INT_PTR nCount = m_tcDemo.GetCount();
for(int i=0; i<nCount; i++)
{
lpCloudTag pCloudTag = m_tcDemo.GetAt(i);
pCloudTag->m_vtrPos.Rotate(m_fAngle, m_vtrRotate);
Invalidate();
}
}break;
}

CWnd::OnTimer(nIDEvent);
}

void COpenGLCtrl::OnTimer(UINT_PTR nIDEvent)
{
switch(nIDEvent)
{
case TIMER_MOVE:
{
if(m_fAngle==0)
return;

INT_PTR nCount = m_tcDemo.GetCount();
for(int i=0; i<nCount; i++)
{
lpCloudTag pCloudTag = m_tcDemo.GetAt(i);
pCloudTag->m_vtrPos.Rotate(m_fAngle, m_vtrRotate);
Invalidate();
}
}break;
}



CWnd::OnTimer(nIDEvent);
} 到此基本的功能就已经全部实现了,在我的资源里上传了这个Demo有兴趣朋友可以下载研究一下,有什么好的想法可以和我进一步交流。做这个东西,写这篇文章权当抛砖引玉,希望能给大家些灵感,从三维的角度作出有更好用户体验的界面设计。最后贴一个效果图,见笑见笑。
[img][/img]


转(http://blog.csdn.net/xianglitian/article/details/6590687)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值