Excel 的 MFC GDI+ ActiveX 箭头控件






4.11/5 (7投票s)
2004年10月24日
10分钟阅读

104507

3814
一篇关于如何为Excel创建ActiveX控件的文章,以及使用GDI+创建箭头的技术,其中涉及一些中学数学知识,以及将单元格与Excel链接的一些技术。
引言
大家好,这是我的第一篇文章。我从Code Project收获了很多,现在是我回馈的时候了。这篇文章是关于什么的?它只是关于Excel中一个简单的控件,当单元格的值发生变化时,仪表板中箭头的位置会随之变化。这意味着,该控件与Excel中的单元格是链接的。从图中你可以看到,无论滑块如何改变其位置,箭头都会改变其位置。这是因为单元格“A1”的值被滑块改变了,所以,由于我的控件与该单元格链接,我的控件中箭头的位置也随之改变。所以,不要误以为我的控件与滑块链接。它实际上是与单元格“A1”链接的。这不是一篇很棒的文章或应用程序。阅读完这篇文章后,我希望你能学到以下几点:
- 如何使用GDI+?
- 如何使用MFC 6.0创建ActiveX应用程序?
- 如何添加属性并将单元格链接到Excel?
- 如何运用我们中学学到的数学知识(几何,我在香港学的)?
背景
为什么会有这样一个应用程序?我从大学时代就开始做程序员,毕业后成为一名专业的C++程序员。有一天,我可爱的老板要求我在Excel中创建一个仪表盘控件,以便Excel用户可以通过这种控件随时了解他们的单元格值是如何变化的。有用吗?对我来说,我不认为它有用,但对我来说是一个挑战。然而,对于某些业务,比如一些资源规划软件,数据很重要,如果有数百个值,经理不能只用眼睛来判断哪些资源处于危急状态。但是,借助仪表盘,他们可以轻松地收到通知,并立即做出决策。这个应用程序只是第一步。创建这个应用程序后,我开始创建一个红绿灯,它需要多线程技术来闪烁用户以引起他们的更大关注。我不会在这里发布红绿灯的代码,让我们先看看读者对这里的内容有什么反应;)。这就是为什么会有这样一个控件。我对ActiveX、Excel编程、COM等一无所知。我在互联网上只能找到很少的资源谈论这些。所以,从学习阶段到设计阶段,再到实现阶段,对我来说都是艰苦的工作,让我度过了非常艰难的时光。我希望每个阅读这篇文章的人都能受益。
如何使用
- 将文件解压到你知道的某个位置。
- 打开命令提示符(通过选择“开始”->“运行”并在弹出的对话框中键入“cmd”... 抱歉,我没有把你们当傻瓜... 但也许你认识某人...)。
- 在命令提示符中,"cd" 到包含该文件的目录。
- 键入“regsvr32 <文件名>”,在此例中,键入“regsvr32 ActiveXArrow.ocx”以安装ActiveX控件。
- 要在Excel中显示它,请先打开Excel。
- 通过单击菜单“视图”->“工具栏”->“控件工具栏”来显示“控件工具栏”。
- 一个新的工具栏会显示出来,尝试在工具栏上找到“锤子”图标并点击它。
- 显示一个文本格式的控件列表,找到名为“ActiveXArrow Control”的项并点击它。
- 很好,你成功了。现在,你的鼠标图标应该变成十字而不是箭头。尝试在Excel工作表上拖动一个正方形,然后将显示一个控件。
- 右键单击控件,在弹出的菜单中,尝试选择“属性”。
- 会显示一个属性列表框。尝试在列表框中找到“LinkedCell”项,并在其属性中键入“A1”。(记住输入后要按“回车”键,以激活更改)。
- 在“控件工具栏”上,有一个尺子状的图标(总是在左上角),点击它以完成设计阶段(在此之前你正在设计)。
- 尝试更改单元格“A1”的值,看看控件箭头的方向是否发生变化。(应该会变,否则,请重新操作)。
为了卸载控件,您必须在步骤2中描述的命令提示符中键入“regsvr32 /u ActiveXArrow.ocx”。
使用代码
在讨论代码之前,我想先介绍一下整个控件的结构。当你使用VC++ 6.0打开项目时,你会发现有很多类。对于ActiveX新手来说,这可能看起来很奇怪。实际上,只有三个主要类实现了控件的主要功能。它们是“CActiveXArrowCtrl
”、“CArrowObj
”和“CPieForm
”。
CActiveXArrowCtrl
是处理控件绘制的主类。你会发现有一个成员类函数
void CActiveXArrowCtrl::Draw(CDC* pdc, const CRect& rcBounds, CRect* rcClip)
在该函数内部,以下三行代码处理了最复杂的箭头绘制
m_pieFormObj.SetGraphic(g); m_pieFormObj.DrawPie(rcBounds, FALSE, TRUE); m_pieFormObj.DrawArrow(m_angle, TRUE);
当然,这并非如此简单。GDI+将以EMF格式绘制图像,但这与Excel的打印结构不兼容(因为Excel只能识别WMF格式的图像)。因此,我们必须找到一种方法将GDI+图像转换为WMF格式。Draw
函数的完整代码可以在下面找到
// pdc is the device context of the drawing area, that is, // what you drag on the excel worksheet // rcBounds is the rectangle of the drawing area, // with (0,0) at the top left corner void CActiveXArrowCtrl::Draw(CDC* pdc, const CRect& rcBounds, CRect* rcClip) { // Rect is a GDI+ object Rect oRect(rcBounds.left, rcBounds.top, rcBounds.right, rcBounds.bottom); TCHAR lpBuffer[256]; DWORD len = ::GetTempPath(256, lpBuffer); lpBuffer[len]= '\0'; CString stemp; stemp.Format(_T("%s"), lpBuffer); // create the emf file name CString path = stemp + _T("h") + m_myUID + _T("e.emf"); // create the emf object using the filename Metafile* myMeta = new Metafile(path, pdc->m_hDC); { // create the gdi+ graphic object and draw the image // on the emf object created just before Graphics* g = new Graphics(myMeta); g->SetSmoothingMode(SmoothingModeAntiAlias); // draw the image // if the m_BkImage have path exist { if(m_BkImage != _T("")) { // create the background image from the specified image path // (m_BkImage store the path of the background image) Image* img = new Image(m_BkImage.GetBuffer(m_BkImage.GetLength())); Status st; st = g->DrawImage(img, oRect); if(st != Ok) { // if fail to create the background img, try to create the background // using the resources file Bitmap* img2 = Bitmap::FromResource(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDB_BITMAP_BK)); g->DrawImage(img2, oRect); delete img2; } delete img; } // if there is no path exist, just create // the image from the resources file else { Bitmap* img = Bitmap::FromResource(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDB_BITMAP_BK)); if(!img) AfxMessageBox(_T("fail to load bitmap")); g->DrawImage( img, oRect); } // succeed to draw the background, // now, is the time to draw the arrow ... m_pieFormObj.SetGraphic(g); m_pieFormObj.DrawPie(rcBounds, FALSE, TRUE); m_pieFormObj.DrawArrow(m_angle, TRUE); } delete g; } delete myMeta; // OK, now, we succeed to draw all the things, however, // all are in emf format and stored in the file "path" // we have to load it using GDI method and so, it will be // in wmf format and excel can print it out ~ // create the Bitmap object from the path Bitmap mybitmap(path.GetBuffer(path.GetLength())); // get the bitmap handle HBITMAP hbm = NULL; mybitmap.GetHBITMAP(NULL, &hbm); if(!hbm) { // AfxMessageBox(_T("fail to get hbm")); // if fail to get the handle, mean there is no such file, // just load a default image from resources Bitmap* img = Bitmap::FromResource(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDB_BITMAP_BK)); if(!img) AfxMessageBox(_T("fail to load bitmap")); // Rect rect2(0, 0, rcBounds.BottomRight().x, rcBounds.BottomRight().y); // g.DrawImage( // img, // rect2); img->GetHBITMAP(NULL, &hbm); } // create a DC, but don't create it in any device context // but system display CDC memDC; memDC.CreateCompatibleDC( NULL ); // re-draw it ... all are straight forward .. //memDC.SelectObject( &bitmap ); HBITMAP hBmOld = (HBITMAP)::SelectObject( memDC.m_hDC, hbm ); // Get logical coordinates BITMAP bm; ::GetObject( hbm, sizeof( bm ), &bm ); if(!rcClip) pdc->StretchBlt(rcBounds.left, rcBounds.top, rcBounds.Width(), rcBounds.Height(), &memDC, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY); else { pdc->SetStretchBltMode(STRETCH_DELETESCANS); pdc->StretchBlt(rcClip->left, rcClip->top, rcClip->Width(), rcClip->Height(), &memDC, rcClip->left, rcClip->top, rcClip->Width(), rcClip->Height(), SRCCOPY); } ::SelectObject( memDC.m_hDC, hBmOld ); ::DeleteObject(hbm); ::DeleteObject(hBmOld); ::DeleteObject(memDC); }
如何使用GDI+
当我最初设计项目时,我正在考虑应该使用哪个图形库。在使用MFC时,可以使用DirectX、OpenGL、GDI、GDI+。最终,我选择使用GDI+,因为我最多只想向用户展示一个透明的箭头。在使用GDI+时,我受益于CodeProject的一位贡献者(作者:Ryan Johnston,参见文章)。他为我们解决了初始化GDI+库的所有麻烦。非常感谢。在初始化GDI+库时,我们所要做的就是使用他的类创建一个成员变量,并调用几行代码进行初始化,即,请看下面
// ... in stdafx.h, declare sth below #include <gdiplus.h> #pragma comment(lib, "gdiplus.lib") using namespace Gdiplus;
// In class declaration #include "GDIpInitializer.h" class CActiveXArrowCtrl : public COleControl { public: CGDIpInitializer m_gdip; ... }; // In class definition CActiveXArrowCtrl::CActiveXArrowCtrl() { ... m_gdip.Initialize(); ... } CActiveXArrowCtrl::~CActiveXArrowCtrl() { m_gdip.Deinitialize(); } // then, you can see that I declared // a graphic object at the ::Draw function void CActiveXArrowCtrl::Draw(CDC* pdc, const CRect& rcBounds, CRect* rcClip) { ... Graphics* g = new Graphics(myMeta); // this is the gdi+ graphic library ... }
如何使用MFC 6.0创建ActiveX应用程序
很简单,我受益于一本名为《ActiveX Inside Out》的书(大概是这个名字,我忘了确切的书名)。这是一本非常好的书。对于任何想学习ActiveX的人,我都推荐这本书。好的,下面是使用MFC 6.0创建ActiveX应用程序的步骤。该过程仅针对此应用程序,对于其他类型的ActiveX控件,可能会有所不同。
- 打开VC++ 6.0。
- 单击“文件”->“新建”,然后在项目列表中选择“MFC ActiveX Control Wizard”,键入项目名称。这里,我们将其命名为“MyFirstTest”。
- 在第一个弹出向导中,只需单击“下一步”,所有内容都保留为默认设置。
- 在下一个向导中,您将看到一个“高级”按钮,点击它会弹出一个对话框,您应该启用“无闪烁激活”选项。
- 点击完成。你成功了!... 尝试先编译项目。
- 运行Excel并执行与创建我的控件相同的步骤,在Excel中创建你的“MyFirstTest”对象。你将在Excel工作表上看到一个椭圆形。
如何添加属性并将单元格链接到Excel
如果您成功创建了“MyFirstTest”ActiveX控件,您可以尝试右键单击该控件并选择“属性”。您会发现有一些默认属性。但是,您永远不会看到我的控件属性列表中显示的“LinkedCell”属性以及“Max”、“Min”等其他属性。为了添加自定义属性,您必须遵循以下步骤:
- 按“Ctrl-W”弹出类向导。
- 选择“自动化”页面。
- 点击“添加属性...”按钮
- 在弹出的对话框中,在“外部名称”字段中,尝试键入“
Max
”。在“类型”字段中,将其选择为“long
”类型。 - 在对话框的中间,选择“获取/设置方法”而不是默认的“成员变量”。
- 点击“确定”。
- 然后,您会看到添加了一个新属性。您可以尝试在Excel中创建新控件并右键单击以查看是否添加了新属性。但是,您可能会发现该属性未添加到Excel属性列表中,为什么?我花了一周时间才找到原因。答案是,MS Excel总会将一份属性列表副本保存到硬盘上的某个位置;每当您创建一个控件时,它都会尝试从该位置而不是从控件加载属性列表。因此,您必须删除该文件。该文件始终位于“C:\DOCUME~1\LIU\LOCALS~1\Temp\Excel8.0\”。您会发现有很多扩展名为“.exd”的文件。您必须删除名称与您的控件名称相同的.exd文件。请注意,您不必每次都删除它,只是在您添加了新属性的情况下,您必须执行此操作才能让Excel正确加载属性列表。通过同样的方法,您可以添加可以链接到Excel单元格的属性。需要执行以下额外步骤:
- 按照步骤1到5,尝试添加一个名为“
Value
”、类型为“long
”的属性。尝试选择该属性并点击“数据绑定...”按钮,会弹出一个对话框,尝试启用“默认可绑定属性”。 - 重新编译项目并记住删除相应的 .exd 文件。
- 打开Excel并创建控件。查看属性列表,一个新属性被添加了,它是“LinkedCell”。
完成了。因此,每当属性值发生变化时(例如,“Value
”属性),都会调用Set
方法(实际上,Set
方法是一个回调函数)。因此,程序员应该尝试编写代码来处理属性值的变化,以便可以实时更新绘图。为此,我们只需添加一行代码“InvalidateControl()
”来强制控件应用所有新值并重新绘制自身。
如何运用我们中学学到的数学知识(几何,我在香港学的)
好的,这是一个有趣的观点。我非常喜欢数学和物理。然而……在香港,我很难选择成为一名纯理科学生。我选择计算机的原因是……在我高中最后一年的时候,我甚至不知道如何关闭电脑……
上面的圆圈是仪表板的简单布局。有趣的是,它不总是圆圈,当用户将控件拖动为矩形时,它可能是一个椭圆。因此,通过使用简单的椭圆公式,我们可以计算出“a”和“b”值,并将它们传递给GDI+函数来绘制椭圆。
对于箭头,有一个规则,箭头的角度θ始终保持不变。对我来说,作为一名程序员,我需要知道三个点才能让GDI+函数绘制箭头。这三个点是 (Px, Py),两个切点 (Ux, Uy) (<-- 有两个 Ux, Uy 点)。那么,这里已知的值是什么?未知的值又是什么?
已知值
- 中心点 (Cx, Cy) // 如何得到 Cx, Cy?太简单了..想想看..提示是..使用简单几何。
- 顶点 (Px, Py) // 如何得到 Px, Py?很简单,想想看,提示是...使用简单的椭圆方程。
- 长度 L // 如何得到 L?很简单..使用简单的几何。
- 角度 θ (<- 我希望它保持不变)。
未知值
- 两个切点 (Ux, Uy)
所以,经过长时间的计算,我得出了以下方程
Uy = Py * sin2(θ/2) + Cy * cos2(θ/2) ± (1/2) * sin(θ) * √(L2 – (Cy – Py)2)
Ux 呢?我把它留作练习给你...
就这些,感谢您的阅读。