C++ 和 ATL 中的 2D 图形 ActiveX 控件(无 MFC 依赖)






4.92/5 (25投票s)
绘制多个数据集、交互式工具提示信息、缩放/平移、编辑颜色/宽度/格式、注释、打印/保存
引言
开发人员经常需要绘制各种数据。他们希望使用一个依赖项最少、轻量级的控件。
背景
NTGraph 控件是一个功能强大的 ActiveX 控件,可以绘制多个数据集。但不幸的是,它依赖于 MFC 库。
这款名为 DMGraph
的新 2D 图表 ActiveX 控件基于 NTGraph
绘图引擎,但消除了 MFC 依赖。对于 DMGraph
,ATL 3.0 被用作框架。唯一的依赖项是一些 Microsoft Windows DLL(C 运行时库 msvcrt.dll 从 Windows 2000 开始就已包含在操作系统中)。这意味着没有部署问题 - DMGraph
可在 Windows 2000 或更高版本上运行。
与旧的 NTGraphCtrl
相比,另一个重大变化是公开的 COM 接口架构。DMGraphCtrl
没有将所有内容打包到一个接口中,而是公开了一个表示绘图所用实体的接口层次结构。
Using the Code
主接口 IDMGraphCtrl
包含项的集合(由 IDMGraphCollection
接口管理)。此集合接口公开了常用方法(如 Add
、Delete
、Count
、Item
)。特有的概念是“选定项”。集合中的一个项可以是“选定”的。有时用户操作(如鼠标拖动)会应用于“选定”项(如果存在)。IDMGraphCollection::Selected
属性获取/设置选定项的索引。
当用户双击图表区域时,将显示一个带有属性页的模态对话框。此对话框也可以通过 ShowProperties
方法以编程方式调用。在这些属性页中修改数据会立即反映在显示的图表中。
CDMGraphCtrl
类实现了 IDMGraphCtrl
接口。在运行时,可以使用 DM Graph 属性页查看或更改某些属性。
CDMGraphCtrl
类维护 IDMGraphCtrl
接口公开的以下集合:
1. 元素集合
get_Elements
属性公开元素集合。
每个项都是 CGraphElement
类的实例,该类公开 IDMGraphElement
接口。图表元素是需要绘制的点集合。图表元素具有定义其绘图样式的各种属性。例如,Linetype
属性定义了用于连接点的线(包括“Null
” - 完全不绘制线条)。可以为点设置颜色、宽度、形状;可以启用/禁用整个点集进行绘制等。每个图表元素都由一个“name
”标识。所有这些都可通过 IDMGraphElement
接口公开的 COM 属性访问。设置此类属性时,整个图表都会重新绘制以反映更改。
要绘制的点集(数据)由客户端通过多种方法提供:
Plot
- 两个大小相同的⼀维数组(一个用于 X,一个用于 Y)将为特定图表元素设置整个点集合。PlotXY
- 只向点集合添加一个点(同时指定 X 和 Y 坐标)。PlotY
- 只向点集合添加一个点(只指定 Y,X 是添加点在点集合中的索引)。
每次修改点集合时,图表都会更新以反映更改,但范围不会更新。如果新点超出范围,则需要调用 SetRange
或 AutoRange
方法。
可以向集合中添加新元素,删除现有元素,更改选定元素索引,并通过“元素”属性页查看/更改选定元素属性。
2. 注释集合
get_Annotations
属性公开注释集合。
一个注释是显示在图表特定位置的文本。此集合维护 CGraphAnnotation
类的实例,该类公开 IDMGraphAnnotation
接口。使用此接口可以访问各种属性,例如标题(显示的文本)、位置、颜色、文本方向、背景启用/禁用。设置此类属性时,整个图表都会重新绘制以反映更改。
可以向集合中添加新注释,删除现有注释,更改选定注释索引,并通过“注释”属性页查看/更改选定注释属性。
3. 光标集合
get_Cursors
属性公开光标集合。
光标由一到两条与 X 或 Y 轴平行的线组成。IDMGraphCursor
接口处理光标特定属性。如果 Style
属性(类型为 Crosshair enum
)设置为“XY
”,则光标将有两条线:一条平行于 X 轴,另一条平行于 Y 轴。如果光标 Mode
设置为 Snap
,则选定的光标在鼠标拖动时会吸附到选定图表元素的最近点。
可以向集合中添加新光标,删除现有光标,更改选定光标索引,并通过“光标”属性页查看/更改选定光标属性。
4. 轴对象
get_Axis
属性公开两个对象:一个用于 X(水平)轴,另一个用于 Y(垂直)轴。这些对象是 CGraphAxis
类的实例,该类公开 IDMGraphAxis
接口。可以为每个轴获取/设置各种属性。如果 put_Time
属性设置为 VARIANT_TRUE
,则该轴的双精度值将被视为日期/时间值。这些值按照 OLE 自动化 VARIANT
联合使用的 DATE
类型进行解释。值根据 Format
属性设置的格式字符串显示。对于日期/时间,可能的格式字符串在 MSDN 中的 strftime
函数中有说明。否则,对于非对数轴,则接受常规的 sprintf
格式字符串。一些轴属性可以在 DM Graph 属性表中(见上文)获得,而另一些属性可以在格式属性页(见下文)中获得。
从“轴”组合框中,可以选择 X(底部)或 Y(左侧)轴。然后可以为选定的轴设置数据类型。对于每种类型,“模板”列表框会填充可用的格式模板。当从左侧选择一个模板项时,“格式”字符串右侧会更新。
基本用法
1. 从使用 ATL 编写的 C++ Windows 客户端
class CMainWnd : public CWindowImpl<CMainWnd>
{
CAxWindow* m_pGraphCtrl;
CComPtr<IDMGraphCtrl> m_spDMGraph;
};
LRESULT CMainWnd::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
m_pGraphCtrl = new CAxWindow;
if(m_pGraphCtrl == NULL)
return -1;
if(!AtlAxWinInit())
return -1;
HRESULT hr;
CComPtr<IAxWinHostWindow> spHost;
hr = m_pGraphCtrl->QueryHost(IID_IAxWinHostWindow, (void**)&spHost);
if(FAILED(hr))
{
Message(hr, NULL, L"Cannot query Ax host");
return -1;
}
hr = spHost->CreateControl(L"DMGraph.DMGraphCtrl", m_pGraphCtrl->m_hWnd, NULL);
if(FAILED(hr))
{
Message(hr, NULL, L"Cannot start DM Graph control");
return -1;
}
CComVariant vData;
hr = m_pGraphCtrl->QueryControl(IID_IDMGraphCtrl, (void**)&m_spDMGraph);
if(FAILED(hr) || m_spDMGraph == NULL)
{
Message(hr, NULL, L"Cannot query DM Graph control");
return -1;
}
return 0;
}
void CMainWnd::SetGraphData(VARIANT* pvarrX, VARIANT* pvarrY, LPCTSTR szName)
{
ATLASSERT(pvarrX);
ATLASSERT(pvarrY);
ATLASSERT(szName);
CComBSTR bsName(szName);
CComPtr<IDMGraphCollection> spElements;
CComPtr<IDMGraphElement> spGraphElement;
HRESULT hr = m_spDMGraph->get_Elements(&spElements);
long i, nElementCount = 0;
BOOL bReplace = FALSE;
hr = spElements->get_Count(&nElementCount);
for(i=0; i<nElementCount; i++)
{
CComBSTR bsElemName;
CComPtr<IDispatch> spDispatch;
hr = spElements->get_Item(i, &spDispatch);
hr = spDispatch.QueryInterface(&spGraphElement);
spGraphElement->get_Name(&bsElemName);
if(_wcsicmp(bsElemName, bsName) == 0)
{
OLECHAR szMsgText[256];
_snwprintf(szMsgText, 256,
L"There is ALREADY an element named '%s'.\n"
L"Do you want to replace it ?", bsElemName);
if(::MessageBoxW(m_hWnd, szMsgText, NULL,
MB_YESNO|MB_ICONQUESTION) != IDYES)
{
return;
}
bReplace = TRUE;
break;
}
else
spGraphElement = NULL;
}
if(bReplace == FALSE || spGraphElement == NULL)
{
CComPtr<IDispatch> spDispatch;
hr = spElements->Add(&spDispatch);
spGraphElement = NULL;
hr = spDispatch.QueryInterface(&spGraphElement);
}
hr = spGraphElement->put_Name(bsName);
hr = spGraphElement->put_PointSymbol( Dots );
hr = spGraphElement->put_PointSize(3);
hr = spGraphElement->Plot(*pvarrX, *pvarrY);
if(FAILED(hr))
{
Message(hr, spGraphElement, L"Failed to plot items");
return;
}
hr = m_spDMGraph->AutoRange();
}
2. 使用 VBScript 从 HTML 页面
在 HTML body 中,使用 object
标签创建 ActiveX。单击按钮将执行脚本来设置要绘制的数据。
<object ID="DMGraphCtrl"
CLASSID="CLSID:AAF89A51-7FC0-43B0-9F81-FFEFF6A8DB43"
width=600 height=400 VIEWASTEXT></object>
<input id=BtnSin value=sin type="button">
<script id=clientEventHandlersVBS language="vbscript">
<!--
Sub BtnSin_onclick
On Error Resume Next
Dim dmGraphCtrl
Set dmGraphCtrl = document.getElementById("DMGraphCtrl")
Dim idx : idx = dmGraphCtrl.Elements.Selected
If idx < 0 Then
MsgBox("Error: please create and select an element first." &_
vbCrLf & "(Double click to see property pages)")
Else
Dim selElement
Set selElement = dmGraphCtrl.Elements.Item(idx)
Dim i
Dim x()
Dim y()
ReDim x(100)
ReDim y(100)
For i=0 To 100
x(i) = i/5
y(i) = Sin( x(i) )
Next
selElement.Plot x, y
dmGraphCtrl.AutoRange()
End If
If Err.number <> 0 Then
MsgBox Err.Description
End If
End Sub
-->
</script>
3. 从使用 MFC 编写的 C++ Windows 客户端
#import "..\DMGraph\DMGraph.tlb" no_namespace raw_interfaces_only
class CDmGraphMfcClientDlg : public CDialog
{
... ... ...
IDMGraphCtrlPtr m_spGraph;
};
BOOL CDmGraphMfcClientDlg::OnInitDialog()
{
... ... ...
CWnd* pwndCtrl = GetDlgItem(IDC_DMGRAPHCTRL1);
ASSERT_VALID(pwndCtrl);
IUnknown* pUnkCtrl = pwndCtrl->GetControlUnknown(); //weak reference
HRESULT hr;
m_spGraph = pUnkCtrl;
IDMGraphCollectionPtr colElements;
hr = m_spGraph->get_Elements(&colElements);
IDispatchPtr spDisp;
IDMGraphElementPtr spElem;
hr = colElements->Add(&spDisp);
spElem = spDisp;
hr = spElem->put_Name(_bstr_t("sin"));
hr = spElem->put_PointSymbol( Dots );
hr = spElem->put_PointSize(1);
hr = spElem->put_PointColor( RGB(255, 0, 0) );
COleSafeArray arrx, arry;
arrx.CreateOneDim(VT_R8, 100);
arry.CreateOneDim(VT_R8, 100);
long i;
for(i=0; i<100; i++)
{
double x, y;
x = i/10.;
y = sin(x);
arrx.PutElement(&i, &x);
arry.PutElement(&i, &y);
}
hr = spElem->Plot(COleVariant(arrx), COleVariant(arry));
hr = m_spGraph->AutoRange();
return TRUE; // return TRUE unless you set the focus to a control
}
关注点
- 需要保护格式化输入数据的“
sprintf
”代码免受异常影响。 - 需要处理
WM_ERASEBKGND
以避免窗口在调整大小时闪烁。 - 来自 VBScript 客户端的
VARIANT
通常需要对包含的 safe array 进行间接引用,并对数组元素进行转换。
历史
- 版本 5.0.0.4
- 更新了“关于”对话框,包含开发者网站的新 URL。
- 版本 5.0.0.3
- 修正了注册错误(在
CLSID
键中使用错误的LIBID
)。 - 为类型库添加了“
control
”标志(在 IDL 中作为属性)。这样,DMGraph
就会出现在 Visual Basic 组件控件列表 / Excel 控件工具箱中,成为一个 ActiveX。 - 更改了
DrawGraphOffScreen
函数,使其即使在提供的 HDC 是图元文件 DC 的情况下也能正常工作。这在 COM 客户端调用IDataObject::GetData
方法时很有用。该方法的 ATL 实现使用了图元文件 DC。
- 修正了注册错误(在
- 版本 5.0.0.2
- 在 DM Graph 属性页中添加了 UI,允许更改网格步长。
- 版本 5.0.0.1
- 修复了一个小缺陷,该缺陷在 ActiveX 未处于活动状态时(例如,当它被 C++ 资源编辑器插入到对话框中时)会在“Debug build”中引发断言。
- 用 MFC 客户端示例代码更新了帮助文件。
- 版本 5.0
- 移除了 MFC 依赖;旧的 .OCX 库现在是一个使用 ATL 的 .DLL。该 DLL 只依赖于标准的 Windows 库和标准 C++ 库(msvcrt.dll),后者从 Windows 2000 开始就已包含在操作系统中。这意味着没有部署问题。
- 公开的 COM 接口进行了重大重新设计。添加了更多接口。它们将应用于同类对象的方法和属性组合在一起。因此,与旧的 NTGraphCtrl.ocx 所公开的 COM 不兼容。因此,所有 GUID 和 ProgID 都是新的。
- 新的 DLL 公开双重接口(与旧的 dispinterface 不同)。
- 修复了打印时的 GDI 和内存泄漏。
- 添加了新的 API,可以使用一个方法调用传递一个点数组。
- 添加了选择性缩放(仅 X 或仅 Y 缩放)。
- 添加了一种新的跟踪模式,该模式在工具提示中显示鼠标附近最近点的值。
- 打印功能使用新的操作系统打印向导对话框。
- 文档(帮助)以 CHM 格式提供。