65.9K
CodeProject 正在变化。 阅读更多。
Home

Excel 的 MFC GDI+ ActiveX 箭头控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.11/5 (7投票s)

2004年10月24日

10分钟阅读

viewsIcon

104507

downloadIcon

3814

一篇关于如何为Excel创建ActiveX控件的文章,以及使用GDI+创建箭头的技术,其中涉及一些中学数学知识,以及将单元格与Excel链接的一些技术。

Sample Image - 2E.jpg

引言

大家好,这是我的第一篇文章。我从Code Project收获了很多,现在是我回馈的时候了。这篇文章是关于什么的?它只是关于Excel中一个简单的控件,当单元格的值发生变化时,仪表板中箭头的位置会随之变化。这意味着,该控件与Excel中的单元格是链接的。从图中你可以看到,无论滑块如何改变其位置,箭头都会改变其位置。这是因为单元格“A1”的值被滑块改变了,所以,由于我的控件与该单元格链接,我的控件中箭头的位置也随之改变。所以,不要误以为我的控件与滑块链接。它实际上是与单元格“A1”链接的。这不是一篇很棒的文章或应用程序。阅读完这篇文章后,我希望你能学到以下几点:

  1. 如何使用GDI+?
  2. 如何使用MFC 6.0创建ActiveX应用程序?
  3. 如何添加属性并将单元格链接到Excel?
  4. 如何运用我们中学学到的数学知识(几何,我在香港学的)?

背景

为什么会有这样一个应用程序?我从大学时代就开始做程序员,毕业后成为一名专业的C++程序员。有一天,我可爱的老板要求我在Excel中创建一个仪表盘控件,以便Excel用户可以通过这种控件随时了解他们的单元格值是如何变化的。有用吗?对我来说,我不认为它有用,但对我来说是一个挑战。然而,对于某些业务,比如一些资源规划软件,数据很重要,如果有数百个值,经理不能只用眼睛来判断哪些资源处于危急状态。但是,借助仪表盘,他们可以轻松地收到通知,并立即做出决策。这个应用程序只是第一步。创建这个应用程序后,我开始创建一个红绿灯,它需要多线程技术来闪烁用户以引起他们的更大关注。我不会在这里发布红绿灯的代码,让我们先看看读者对这里的内容有什么反应;)。这就是为什么会有这样一个控件。我对ActiveX、Excel编程、COM等一无所知。我在互联网上只能找到很少的资源谈论这些。所以,从学习阶段到设计阶段,再到实现阶段,对我来说都是艰苦的工作,让我度过了非常艰难的时光。我希望每个阅读这篇文章的人都能受益。

如何使用

  1. 将文件解压到你知道的某个位置。
  2. 打开命令提示符(通过选择“开始”->“运行”并在弹出的对话框中键入“cmd”... 抱歉,我没有把你们当傻瓜... 但也许你认识某人...)。
  3. 在命令提示符中,"cd" 到包含该文件的目录。
  4. 键入“regsvr32 <文件名>”,在此例中,键入“regsvr32 ActiveXArrow.ocx”以安装ActiveX控件。
  5. 要在Excel中显示它,请先打开Excel。
  6. 通过单击菜单“视图”->“工具栏”->“控件工具栏”来显示“控件工具栏”。
  7. 一个新的工具栏会显示出来,尝试在工具栏上找到“锤子”图标并点击它。
  8. 显示一个文本格式的控件列表,找到名为“ActiveXArrow Control”的项并点击它。
  9. 很好,你成功了。现在,你的鼠标图标应该变成十字而不是箭头。尝试在Excel工作表上拖动一个正方形,然后将显示一个控件。
  10. 右键单击控件,在弹出的菜单中,尝试选择“属性”。
  11. 会显示一个属性列表框。尝试在列表框中找到“LinkedCell”项,并在其属性中键入“A1”。(记住输入后要按“回车”键,以激活更改)。
  12. 在“控件工具栏”上,有一个尺子状的图标(总是在左上角),点击它以完成设计阶段(在此之前你正在设计)。
  13. 尝试更改单元格“A1”的值,看看控件箭头的方向是否发生变化。(应该会变,否则,请重新操作)。

    为了卸载控件,您必须在步骤2中描述的命令提示符中键入“regsvr32 /u ActiveXArrow.ocx”。

使用代码

在讨论代码之前,我想先介绍一下整个控件的结构。当你使用VC++ 6.0打开项目时,你会发现有很多类。对于ActiveX新手来说,这可能看起来很奇怪。实际上,只有三个主要类实现了控件的主要功能。它们是“CActiveXArrowCtrl”、“CArrowObj”和“CPieForm”。

Chat.JPG

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控件,可能会有所不同。

  1. 打开VC++ 6.0。
  2. 单击“文件”->“新建”,然后在项目列表中选择“MFC ActiveX Control Wizard”,键入项目名称。这里,我们将其命名为“MyFirstTest”。
  3. 在第一个弹出向导中,只需单击“下一步”,所有内容都保留为默认设置。
  4. 在下一个向导中,您将看到一个“高级”按钮,点击它会弹出一个对话框,您应该启用“无闪烁激活”选项。
  5. 点击完成。你成功了!... 尝试先编译项目。
  6. 运行Excel并执行与创建我的控件相同的步骤,在Excel中创建你的“MyFirstTest”对象。你将在Excel工作表上看到一个椭圆形。

如何添加属性并将单元格链接到Excel

如果您成功创建了“MyFirstTest”ActiveX控件,您可以尝试右键单击该控件并选择“属性”。您会发现有一些默认属性。但是,您永远不会看到我的控件属性列表中显示的“LinkedCell”属性以及“Max”、“Min”等其他属性。为了添加自定义属性,您必须遵循以下步骤:

  1. 按“Ctrl-W”弹出类向导。
  2. 选择“自动化”页面。
  3. 点击“添加属性...”按钮
  4. 在弹出的对话框中,在“外部名称”字段中,尝试键入“Max”。在“类型”字段中,将其选择为“long”类型。
  5. 在对话框的中间,选择“获取/设置方法”而不是默认的“成员变量”。
  6. 点击“确定”。
  7. 然后,您会看到添加了一个新属性。您可以尝试在Excel中创建新控件并右键单击以查看是否添加了新属性。但是,您可能会发现该属性未添加到Excel属性列表中,为什么?我花了一周时间才找到原因。答案是,MS Excel总会将一份属性列表副本保存到硬盘上的某个位置;每当您创建一个控件时,它都会尝试从该位置而不是从控件加载属性列表。因此,您必须删除该文件。该文件始终位于“C:\DOCUME~1\LIU\LOCALS~1\Temp\Excel8.0\”。您会发现有很多扩展名为“.exd”的文件。您必须删除名称与您的控件名称相同的.exd文件。请注意,您不必每次都删除它,只是在您添加了新属性的情况下,您必须执行此操作才能让Excel正确加载属性列表。通过同样的方法,您可以添加可以链接到Excel单元格的属性。需要执行以下额外步骤:
  8. 按照步骤1到5,尝试添加一个名为“Value”、类型为“long”的属性。尝试选择该属性并点击“数据绑定...”按钮,会弹出一个对话框,尝试启用“默认可绑定属性”。
  9. 重新编译项目并记住删除相应的 .exd 文件。
  10. 打开Excel并创建控件。查看属性列表,一个新属性被添加了,它是“LinkedCell”。

完成了。因此,每当属性值发生变化时(例如,“Value”属性),都会调用Set方法(实际上,Set方法是一个回调函数)。因此,程序员应该尝试编写代码来处理属性值的变化,以便可以实时更新绘图。为此,我们只需添加一行代码“InvalidateControl()”来强制控件应用所有新值并重新绘制自身。

如何运用我们中学学到的数学知识(几何,我在香港学的)

好的,这是一个有趣的观点。我非常喜欢数学和物理。然而……在香港,我很难选择成为一名纯理科学生。我选择计算机的原因是……在我高中最后一年的时候,我甚至不知道如何关闭电脑……

layout.JPG

上面的圆圈是仪表板的简单布局。有趣的是,它不总是圆圈,当用户将控件拖动为矩形时,它可能是一个椭圆。因此,通过使用简单的椭圆公式,我们可以计算出“a”和“b”值,并将它们传递给GDI+函数来绘制椭圆。

maths.JPG

对于箭头,有一个规则,箭头的角度θ始终保持不变。对我来说,作为一名程序员,我需要知道三个点才能让GDI+函数绘制箭头。这三个点是 (Px, Py),两个切点 (Ux, Uy) (<-- 有两个 Ux, Uy 点)。那么,这里已知的值是什么?未知的值又是什么?

已知值

  1. 中心点 (Cx, Cy) // 如何得到 Cx, Cy?太简单了..想想看..提示是..使用简单几何。
  2. 顶点 (Px, Py) // 如何得到 Px, Py?很简单,想想看,提示是...使用简单的椭圆方程。
  3. 长度 L // 如何得到 L?很简单..使用简单的几何。
  4. 角度 θ (<- 我希望它保持不变)。

未知值

  1. 两个切点 (Ux, Uy)

    所以,经过长时间的计算,我得出了以下方程

    Uy = Py * sin2(θ/2) + Cy * cos2(θ/2) ± 
        (1/2) * sin(θ) * √(L2 – (Cy – Py)2)

    Ux 呢?我把它留作练习给你...

就这些,感谢您的阅读。

© . All rights reserved.