一、引言
虚拟仪表又称虚拟仪器,是指具有虚拟仪表面板的个人计算机仪器。它是计算机资源、模块化功能硬件与用于数据分析、过程通信及图形用户界面的应用软件的有机结合。其基本思想是用计算机面向对象技术模拟生成各种仪表面板,完成数据采集、分析、显示和存储等功能,最终达到取代传统真实仪表仪器的目的。
本文通过VC++结合三维图形开发库OpenGL来开发虚拟仪表控件,使得开发出来的虚拟仪表具有重用性、通用性、实时性等特点。
二、技术优势
首先,ActiveX是一种标准。使用这个标准可以使不同语言开发的软件构件在网络环境中相互操作。另外,ActiveX也是开放技术的集合,它涵盖了所有流行的Internet标准、语言和平台。ActiveX控件的一个主要优点是:它也能在当前许多流行的编程语言所写的应用程序中重用,如:Java、Visual Basic、Visual C++等。
OpenGL是个专业的3D程序接口,是一个功能强大,调用方便的底层3D图形库。OpenGL可以在不同的平台如Windows 95、Windows NT、Unix、Linux、MacOS、OS/2之间进行移植。因此,支持OpenGL的软件具有很好的移植性,可以获得非常广泛的应用。
三、实现步骤
1. 启动VC++ 6.0,选择菜单File | New,打开New对话框。在New对话框中选择Projects选项卡,在列表中选择MFC ActiveX ControlWizard,建立一个工程名为“OglInstrOcx”的控件工程。
2.通过AppWizard生成的框架应用,是不支持OpenGL的显示。首先需要设置设备显示模式,从而能支持OpenGL的显示。
//填充像素格式的结构:PIXELFORMATDESCRIPTOR
static PIXELFORMATDESCRIPTOR pfd =
{
sizeof(PIXELFORMATDESCRIPTOR),
1,
PFD_DRAW_TO_WINDOW |
PFD_SUPPORT_OPENGL | // 支持OpenGL的显示
PFD_DOUBLEBUFFER, // 支持双缓存的显示
PFD_TYPE_RGBA,
24, // 24位颜色深度
0, 0, 0, 0, 0, 0,
0,
0,
0,
0, 0, 0, 0,
32, // 32深度缓存
0,
0,
PFD_MAIN_PLANE,
0,
0, 0, 0
};
int pixelformat;
//选择设备的像素显示格式,返回像素格式的索引值
if ( (pixelformat = ChoosePixelFormat(hdc, &pfd)) == 0 )
return FALSE;
//利用返回的索引值,设置设备的像素显示格式
if (SetPixelFormat(hdc, pixelformat, &pfd) == FALSE)
return FALSE;
//创建一个与设备关联的OpenGL渲染上下文环境
m_hrc = wglCreateContext(hdc);
//指定此OpenGL渲染环境为当前渲染环境
wglMakeCurrent(hdc, m_hrc);
接着,需要设置OpenGL的视窗大小,投影变换。这里的有些参数可以根据实际需要作相应的调整。
glClearDepth(1.0f);
glEnable(GL_DEPTH_TEST); //打开深度检测
//设置视窗大小
::glViewport(0, 0, rc.right - rc.left, rc.bottom - rc.top );
glMatrixMode(GL_PROJECTION); //指定投影矩阵为当前矩阵
glLoadIdentity();
float x,y,x1,y1;
x = rc.left;
y = rc.top;
x1 = rc.right;
y1 = rc.bottom;
float xRatio = (x1 - x) / (float)( rc.right - rc.left );
float yRatio = (y1 - y) / (float)( rc.bottom - rc.top );
float componentAspect = (x1 - x) / (y1 - y);
//设置当前的可视空间为正投影空间
if (xRatio > yRatio)
glOrtho(0,x1 - x, 0,(float)( rc.bottom - rc.top ) * xRatio, -50000.0,50000.0);
else
glOrtho(0,(float)(rc.right - rc.left) * yRatio, 0,y1 - y, -50000.0,50000.0);
glMatrixMode(GL_MODELVIEW); //指定模型矩阵为当前矩阵
3.为了实时驱动虚拟仪表的显示,在OnDraw函数中利用双缓存技术调用仪表显示函数。
//设置多边形绘制模式(正反面填充模式)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
//以下是关闭纹理和透明检测,打开深度检测和允许线和多边形的光滑处理
glDisable(GL_TEXTURE_2D);
glEnable(GL_POLYGON_SMOOTH);
glEnable(GL_LINE_SMOOTH);
glDisable(GL_ALPHA_TEST);
glEnable(GL_DEPTH_TEST);
RenderScene(hdc); //在这个函数中调用显示虚拟仪表的函数
SwapBuffers(wglGetCurrentDC()); //切换双缓存,以达到实时显示的目的
4.下面具体来讨论实现虚拟仪表的背景显示,包括刻度盘、刻度值等。
首先,需要合理规划仪表面板的显示大小,内部各个显示部件的尺寸及显示颜色。这些尺寸最好与控件绘制的界面大小相关联,以便仪表的显示会跟随控件大小的变化而自动变化。
//取得控件显示界面的大小结构
GetClientRect (&m_rectCtrl);
//根据所得界面大小,计算一个合适的长度作为仪表的半径
m_nRadiusFrame = min(m_rectCtrl.Height(), m_rectCtrl.Width())*9/21;
以下所有跟尺寸有关的变量,都用m_nRadiusFrame来表示,从而实现仪表显示大小的自适应。
根据以上原则,获得仪表盘中心点、内圆弧半径、中心园半径大小和边框厚度。
m_ptMeterCenter = m_rectCtrl.CenterPoint();
int nInnerRadius = m_nRadiusFrame*8/10;
m_nCenterRadius = m_nRadiusFrame/20;
int nFrame = m_nRadiusFrame/18;
利用已获得的圆心、内圆弧半径和边框厚度,我们可以得到绘制色彩刻度范围的所有点的位置。
for(int i=0; i<=PT_NUM*TABNUM; i++)
{
ptGroup1[i].x = m_ptMeterCenter.x + (int)((m_nRadiusFrame-nFrame)*
cos((m_nStartAngleDeg-i*dDeg)*dRadPerDeg));
ptGroup1[i].y = m_ptMeterCenter.y + (int)((m_nRadiusFrame-nFrame)*
sin((m_nStartAngleDeg-i*dDeg)*dRadPerDeg));
ptGroup2[i].x = m_ptMeterCenter.x + (int)(m_nRadiusFrame*8*
cos((m_nStartAngleDeg-i*dDeg)*dRadPerDeg)/10);
ptGroup2[i].y = m_ptMeterCenter.y + (int)(m_nRadiusFrame*8*
sin((m_nStartAngleDeg-i*dDeg)*dRadPerDeg)/10);
}
// 绘制圆盘边框
drawTorus(m_ptMeterCenter,m_nRadiusFrame,nFrame,m_nStartAngleDeg,m_nEndAngleDeg);
// 绘制内圈
glColor3f(1.0,1.0,0.0);
DrawArc(m_ptMeterCenter,nInnerRadius,m_dLeftAngleRad,m_dRightAngleRad);
// 绘制色彩刻度范围
if(m_bColorTick)
{
for(i=0; i<TABNUM; i++)
{
//显示多个区域,每个区域的颜色值不一样
glColor3f(GetRValue(m_colorTable[i])/255.0,GetGValue(m_colorTable[i])/255.0,GetBValue(m_colorTable[i])/255.0);
for(int j=0; j<=PT_NUM-1; j++) //绘制一个指定颜色的区域
{
glBegin( GL_POLYGON) ;
glVertex3f( ptGroup1[i*PT_NUM+j].x,ptGroup1[i*PT_NUM+j].y,0.0) ;
glVertex3f( ptGroup2[i*PT_NUM+j].x,ptGroup2[i*PT_NUM+j].y,0.0 );
glVertex3f( ptGroup2[i*PT_NUM+j+1].x,ptGroup2[i*PT_NUM+j+1].y,0.0 );
glVertex3f( ptGroup1[i*PT_NUM+j+1].x,ptGroup1[i*PT_NUM+j+1].y,0.0) ;
glEnd() ;
}
}
}
在确定了仪表盘中心点、内圆弧半径、中心园半径大小和边框厚度,绘制了圆盘边框、内圈和色彩刻度范围,接下来我们需要绘制主刻度和子刻度。
// 确定x坐标
dTemp = m_ptMeterCenter.x + (m_nRadiusFrame-2*nFrame)*cos(dTickAngleRad);
m_pointBoundary[nRef].x = ROUND(dTemp);
// 确定y坐标
dTemp = m_ptMeterCenter.y + (m_nRadiusFrame-2*nFrame)*sin(dTickAngleRad);
m_pointBoundary[nRef].y = ROUND(dTemp);
// 确定刻度点(主刻度和子刻度)
//主刻度及文本标注点
if(nRef%m_nSubTicks == 0)
{
dTemp = m_ptMeterCenter.x + nInnerRadius*cos(dTickAngleRad);
pointInner[nRef].x = ROUND(dTemp);
dTemp = m_ptMeterCenter.y + nInnerRadius*sin(dTickAngleRad);
pointInner[nRef].y = ROUND(dTemp);
}
// 子刻度
else
{
dTemp = m_ptMeterCenter.x + nSubTickR*cos(dTickAngleRad);
pointInner[nRef].x = ROUND(dTemp);
dTemp = m_ptMeterCenter.y + nSubTickR*sin(dTickAngleRad);
pointInner[nRef].y = ROUND(dTemp);
}
// 绘制刻度
glColor3f(0.0,0.0,0.0);
for(i=0; i<nRef; i++)
{
glBegin( GL_LINES) ;
glVertex3f( m_pointBoundary[i].x,m_pointBoundary[i].y,1.0 ) ;
glVertex3f( pointInner[i].x, pointInner[i].y, 1.0) ;
glEnd() ;
} //在这里设置了Z值,并把深度检测打开,否则刻度被“色彩刻度范围”覆盖了
//我在画DrawArcFilled(中心小圆点)和指针时都设置了Z值
绘制完主刻度和子刻度,接下来就要显示主刻度的刻度值。
ShowKeduvalue(hdc,pointInner);
5. 显示完表盘之后,我们需要画上指针,根据数据变量m_dCurrentValue,计算指针的角度,并且实时地显示,从而生成一个真正有意义的虚拟仪表。
CPoint pointNeedle[4] ; // 指针由四边形组成
// 计算角度并限定指针走的角度
dAngleDeg = m_nStartAngleDeg-(360.0+m_nStartAngleDeg-m_nEndAngleDeg)
*(m_dCurrentValue-m_dMinValue)/(m_dMaxValue-m_dMinValue);
dAngleDeg = min(dAngleDeg, m_nStartAngleDeg);
dAngleDeg = max(dAngleDeg, m_nEndAngleDeg-360.0);
dAngleRad = dAngleDeg*dRadPerDeg;
// 计算三角形底边两个点
pointNeedle[0].x = m_ptMeterCenter.x - (int)(m_nCenterRadius*10*sin(dAngleRad)/8);
pointNeedle[0].y = m_ptMeterCenter.y + (int)(m_nCenterRadius*10*cos(dAngleRad)/8);
pointNeedle[2].x = m_ptMeterCenter.x + (int)(m_nCenterRadius*10*sin(dAngleRad)/8);
pointNeedle[2].y = m_ptMeterCenter.y - (int)(m_nCenterRadius*10*cos(dAngleRad)/8);
// 计算指针顶部坐标
dTemp = m_ptMeterCenter.x + m_nRadiusFrame*cos(dAngleRad)*95/100;
pointNeedle[1].x = ROUND(dTemp);
dTemp = m_ptMeterCenter.y + m_nRadiusFrame*sin(dAngleRad)*95/100;
pointNeedle[1].y = ROUND(dTemp);
// 计算指针尾部坐标
dTemp = m_ptMeterCenter.x - m_nRadiusFrame*cos(dAngleRad)/6;
pointNeedle[3].x = ROUND(dTemp);
dTemp = m_ptMeterCenter.y - m_nRadiusFrame*sin(dAngleRad)/6;
pointNeedle[3].y = ROUND(dTemp);
// 绘制指针
glColor3f(247.0/255.0,241.0/255.0,190.0/255.0);
glBegin( GL_POLYGON) ;
glVertex3f( pointNeedle[0].x, pointNeedle[0].y, 1.5) ;
glVertex3f( pointNeedle[1].x, pointNeedle[1].y, 1.5) ;
glVertex3f( pointNeedle[2].x, pointNeedle[2].y, 1.5) ;
glVertex3f( pointNeedle[3].x, pointNeedle[3].y, 1.5) ;
glEnd() ;
6. 为了增加控件使用的灵活性,我们可以给控件添加属性设置页,以便用户根据自己的需要改变控件的外观、刻度等信息。首先,在资源的Dialog中添加一个IDD_OLE_PROPPAGE_LARGE类型的对话框作为属性页,然后在其中加入用户可能需要改变的一些属性控制项,如:表盘的角度、刻度的范围等,见图1。
图1
通过VC++的ClassWizard的Automation给控件添加方法和属性,以便让使用者能够灵活控制控件的显示。
float COglInstrOcxCtrl::GetStartAngle() //取得仪表指针的最小角度
{
return m_nStartAngleDeg;
}
void COglInstrOcxCtrl::SetStartAngle(float newValue) //设置仪表指针的最小角度
{
m_nStartAngleDeg = newValue;
SetModifiedFlag();
InvalidateControl();
}
float COglInstrOcxCtrl::GetEndAngle()//取得仪表指针的最大角度
{
return m_nEndAngleDeg;
}
void COglInstrOcxCtrl::SetEndAngle(float newValue) //设置仪表指针的最大角度
{
m_nEndAngleDeg = newValue;
SetModifiedFlag();
InvalidateControl();
}
通过ActiveX Events给控件添加相应的事件,如:双击事件,以便使用者能够在运行状态下通过双击控件打开属性页进行设置。
函数ShowProperty实现属性页的显示。
void COglInstrOcxCtrl::ShowProperty()
{
CWnd* pWnd = GetParent();
HWND hWndParent;
if ( pWnd )
hWndParent = pWnd->GetSafeHwnd();
else
hWndParent = NULL;
USES_CONVERSION;
HRESULT hr;
LPUNKNOWN pUnk = GetIDispatch(FALSE);
CWnd* pWndOwner = CWnd::GetSafeOwner(CWnd::FromHandle(hWndParent));
HWND hWndOwner = pWndOwner->GetSafeHwnd();
LCID lcid = AmbientLocaleID();
ULONG cPropPages;
LPCLSID pclsidPropPages = GetPropPageIDs(cPropPages);
RECT rectParent;
RECT rectTop;
::GetWindowRect(hWndParent, &rectParent);
::GetWindowRect(hWndOwner, &rectTop);
TCHAR szUserType[256];
GetUserType(szUserType);
PreModalDialog(hWndOwner);
hr = OleCreatePropertyFrame(hWndOwner, rectParent.left - rectTop.left,
rectParent.top - rectTop.top, T2COLE(szUserType), 1, &pUnk,
cPropPages, pclsidPropPages, lcid, NULL, 0);
PostModalDialog(hWndOwner);
}
四、结论
利用ActiveX技术可以使得开发的虚拟仪表具有可重用性和通用性,可以广泛地应用到网络上和各种开发环境中如:VC++、Visual Basic、Java。利用OpenGL技术,可以充分利用当前各种具有3D加速功能的显卡,使得开发的虚拟仪表具有实时性和可移植性。
图2是在VB程序中测试的效果。
图2
|