Mr.Victor

梦里不知身是客,一晌贪欢!
posts - 21, comments - 7, trackbacks - 0, articles - 0
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

公告

2011年12月3日

  首先说一下,我是个还未毕业的学生,目前在成都一家公司上班,之前也做过一些项目,主要是C++和嵌入式方面的。由于实在不想在学校耗着,所以就先出来上班了,公司目前的项目需要用.NET开发,是一个关于工业流程监控的项目,主要用到的技术是:WCF做底层设备与服务器、服务器与客户端的通讯,WPF做客户端呈现,WF做工作流控制,以及其他一些诸如EF、驱动等技术,我负责WF这一块。

  由于之前没接触过.NET,对于C#也只是上过这门课,未深究过。来公司花了4天时间看了一下C#的语法而已,对于我而言,语言只是个工具,上手起来也比较快。然后就开始学习WF,说实话,WF的资料少得可怜,而且WF3.X和WF4.0差别也很大,园子里的WXWinter是个WF牛人,我是看他写得《WF4.0技术文章》http://www.cnblogs.com/foundation/category/215023.html和MSDN上手的,《WF4.0技术文章》有很多例子,MSDN上有WF框架说明,所以我是将两者结合起来学习的,写这个系列教程主要是为了对这一个月来的学习情况做一个总结,如果能够帮助更多的朋友学习WF那当然更好不过了,由于我真的是这方面的菜鸟,所以难免有错误的地方,如果有幸园子里的高手们看到有错误的地方,欢迎指正,大家多讨论交流才能进步得更快,总之,一家之言,欢迎拍砖。

  好了,现在进入正题,首先我们来认识一下WF。

  WF,也就是Windows Workflow Foundation,这里先说一下工作流(Workflow)。工作流不是个新东西,工作流的概念很早就提出来了的,关于它的历史可以看看百度百科上的解释,反正它就是一种编程概念,通过将工作分解成定义良好的任务或角色,按照一定的规则和过程来执行这些任务并对其进行监控,达到提高工作效率、更好的控制过程、增强对客户的服务、有效管理业务流程等目的(这是百度百科上的解释),在我看来,工作流就是一种程序逻辑由用户自定义的一种概念,在OA系统、行文审批以及流程控制等方面用得比较多。比如说,一套行文审批系统,如果我们不用工作流会怎么开发呢?首先要去了解这套行文审批是哪个客户或者机构使用的,然后根据那个客户或者机构里的行文审批流程来具体写代码,比如请个假,首先要将请假条发送给项目组长,项目组长同意了要发给部门负责人,然后是经理等等,我们会根据这个流程来写代码。那么要是这个行文审批系统又要给另一家公司使用呢,而这家公司的流程不一样,是不是又要重新组织代码逻辑结构呢?这就是我们说的硬代码,全都写死了,那么维护起来肯定特别麻烦,要是用户说,我们公司最近内部出现了些调整,你那个行文审批系统要修改。那麻烦就大了,要动大手术的,公司光拿精力来维护这套系统就差不多了。

  在这里,有人肯定会说,你怎么这么笨啊,可以写些配置信息啊,或者说可以弄些人机交互的东西啊。对的,这里我们就用到了工作流的概念了,其实我们或多或少的都用到过工作流,比如拿最简单的HelloWorld来举例,我们可以在控制台程序里添加一句:
  Console.WriteLine("HelloWorld");
  启动程序,控制台就会输出一句"HelloWorld",如果我们的代码如下:
  string key = "123";
  if(Console.ReadLine() == "123")
    Console.WriteLine("Hello World");
  else
    Console.WriteLine("error");
  像上面这段代码,当启动程序时,程序会停住来等待你的输入,这就是一个人机交互了,你要是隔个半年(只要电脑不坏)再输入,它一样会执行下去,这也是个工作流的例子。但是具体的项目中肯定不会有这么简单的流程,对于具体项目而言,我们希望流程能由用户自己定义,而且是可视化的,支持各种复杂流程,具有安全可靠性,能够跟踪流程的执行情况等等,那么这个时候就该介绍WF了。

  WF是微软根据工作流的思想开发出来的一套工作流编程框架,类似的框架还有很多,WF让程序具有更高地灵活性,具有更好的人机交互能力,也让你开发出来的程序能够适应更多现实场景。最初我接触WF的时候最大的疑问在于,怎么让用户来使用我的工作流程序啊,用我们以往惯有的思维,我们程序员负责开发,用户只需要使用就是了,用户只需要会一些基本的计算机操作就可以了啊,但是现在用WF来开发,难道用户还必须学习工作流么?答案是:是的。

  WF基本上颠覆了我们以往的编程思维,以往的程序逻辑结构全部是由我们程序写死了的,我们为用户提供多少种功能或者说逻辑,用户就只能使用这么多种,而WF程序才是我们以往常说的“搭积木”式的编程,在这里面角色有些调换,我们以往说的面向对象、COM组件、代码复用性等等是搭积木式的,但这是针对程序员而言的,而现在,搭积木的活交给用户来做,我们程序需要做的是制作积木,为用户搭积木提供一个环境,然后用户将积木搭好之后,提供一个积木的安全的执行环境,也就是为工作流的执行提供一个稳定安全的环境。所以说,用户需要学会怎么搭积木,需要学会怎么配置各种积木的参数等等。

  上面说到的“积木”在WF中叫做Activity,你可以翻译为“活动”,一个活动也就是一个功能模块,微软的WF提供了一些内置的活动,比如最基本的开发语言当中的If...Else,While,Do...While,For循环等等,当然微软提供的这些内置活动肯定不能满足我们实际项目中的需求,我们还可以自定义活动,将我们项目中实际的功能封装成活动,以供用户来搭建工作流。

  WF中还有很多高级功能,比如持久性、跟踪、WF服务等等,这些待到我们以后的章节中来讲。总之,对于我们程序员来讲,需要学习工作流怎么搭建,怎么在自己的应用程序里承载工作流设计器,怎么自定义活动,怎么提供安全可靠的工作流执行环境,以及怎么优化工作流执行期间的性能等问题,而用户也需要学习怎么来设计工作流,当然这是用户自己的事了,作为程序员我们就负责我们应该掌握的东西就好了。

  之后的章节我会先介绍WF内置活动的使用,然后介绍更深层次的东西,在这期间我也要更深入的了解WF才行,总之,WF的目的和整个框架的概述大致就是如此了,我之前也没怎么写过东西,我觉得这是个好的学习方式,所以希望可以跟园子里的高手们多多交流,共同进步,这是我的QQ号:294636255,欢迎指正错误,谢谢!

posted @ 2011-12-03 19:12 Mr-Victor 阅读(2368) 评论(5) 编辑

2011年9月27日

在OpenGL中有两个比较重要的投影变换函数,glViewport和glOrtho。

glOrtho是创建一个正交平行的视景体。 一般用于物体不会因为离屏幕的远近而产生大小的变换的情况。比如,常用的工程中的制图等。需要比较精确的显示。 而作为它的对立情况, glFrustum则产生一个透视投影。这是一种模拟真是生活中,人们视野观测物体的真实情况。例如:观察两条平行的火车到,在过了很远之后,这两条铁轨是会相交于一处的。还有,离眼睛近的物体看起来大一些,远的物体看起来小一些。

glOrtho(left, right, bottom, top, near, far), left表示视景体左面的坐标,right表示右面的坐标,bottom表示下面的,top表示上面的。这个函数简单理解起来,就是一个物体摆在那里,你怎么去截取他。这里,我们先抛开glViewport函数不看。先单独理解glOrtho的功能。 假设有一个球体,半径为1,圆心在(0, 0, 0),那么,我们设定glOrtho(-1.5, 1.5, -1.5, 1.5, -10, 10);就表示用一个宽高都是3的框框把这个球体整个都装了进来。 如果设定glOrtho(0.0, 1.5, -1.5, 1.5, -10, 10);就表示用一个宽是1.5, 高是3的框框把整个球体的右面装进来;如果设定glOrtho(0.0, 1.5, 0.0, 1.5, -10, 10);就表示用一个宽和高都是1.5的框框把球体的右上角装了进来。上述三种情况可以见图:

glViewport()函数和glOrtho()函数的理解(转) - WAYNE - 闲逛

glViewport()函数和glOrtho()函数的理解(转) - WAYNE - 闲逛

glViewport()函数和glOrtho()函数的理解(转) - WAYNE - 闲逛

从上述三种情况,我们可以大致了解glOrtho函数的用法。glOrtho函数只是负责使用什么样的视景体来截取图像,并不负责使用某种规则把图像呈现在屏幕上。

glViewport主要完成这样的功能。它负责把视景体截取的图像按照怎样的高和宽显示到屏幕上。

比如:如果我们使用glut库建立一个窗体:glutInitWindowSize(500, 500); 然后使用glutReshapeFunc(reshape); reshape代码如下:

void reshape(int width, int height)

{

glViewport(0, 0, (GLsizei)width, (GLsizei)height);

glMatrixModel(GL_PROJECTION);

glLoadIdentity();

glOrtho(-1.5, 1.5, -1.5, 1.5, -10, 10);

....

}

这样是可以看到一个正常的球体的。但是,如果我们创建窗体时glutInitWindowSize(800, 500),那么看到的图像就是变形的。上述情况见图。

glViewport()函数和glOrtho()函数的理解(转) - WAYNE - 闲逛

因为我们是用一个正方形截面的视景体截取的图像,但是拉伸到屏幕上显示的时候,就变成了glViewport(0, 0, 800, 500);也就是显示屏变宽了, 倒是显示的时候把一个正方形的图像“活生生的给拉宽了”。就会产生变形。这样,就需要我们调整我们的OpenGL显示屏了。我们可以不用800那么宽,因为我们是用的正方形的视景体,所以虽然窗体是800宽,但是我们只用其中的500就够了。修改一下程序。

void reshape(int width, int height)

{

int dis = width < height ? width : height;

glViewport(0, 0, dis, dis); /*这里dis应该是500*/

glMatrixModel(GL_PROJECTION);

glLoadIdentity();

glOrtho(-1.5, 1.5, -1.5, 1.5, -10, 10);

.....

}

glViewport()函数和glOrtho()函数的理解(转) - WAYNE - 闲逛

OK。如果你能看明白我写的内容。你可能对glViewport函数有个大致的了解。

不过,我们采用上面的办法,就是只使用了原来屏幕的一部分(宽度从501到800我们没有用来显示图像)。如果我们想用整个OpenGL屏幕显示图像,但是又不使图像变形怎么办?

那就只能修改glOrtho函数了。也就是说,我们使用一个和窗体一样比例的视景体(而不再是正方形的视景体)来截取图像。例如,对于(800, 500)的窗体,我们使用glOrtho(-1.5 * 800/500, 1.5 * 800/500, -1.5, 1.5, -10, 10),就是截取的时候,我们就使用一个“扁扁”的视景体截取,那么,显示的到OpenGL屏幕时(800, 500),我们只要正常把这个扁扁的截取图像显示(扁扁的截取图像是指整个截取的图像,包括球形四周的黑色部分。 球形还是正常圆形的),就可以了。如:

void reshape(int width , int height)

{

glViewport(width, height); //按照窗体大小制作OpenGL屏幕

glMatrixMode(GL_PROJECTION);

glLoadIdentity();

if (width <= height)

glOrtho(-1.5, 1.5, -1.5 * (GLfloat)height/(GLfloat)width, 1.5 * (GLfloat)height/(GLfloat)width, -10.0, 10.0);

else

glOrtho(-1.5*(GLfloat)width/(GLfloat)height, 1.5*(GLfloat)w/(GLfloat)h, -1.5, 1.5, -10.0, 10.0);

....

}

glViewport()函数和glOrtho()函数的理解(转) - WAYNE - 闲逛

另外,关于glViewport()函数,我们还可以用来调整图像的分辨率。例如,保持目前的窗体大小不变,我们如果用这个size来只显示整个物体的一部分,那么图像的分辨率就必然会增大。例如:

void reshape(int w, int h)

{

glViewport(0, 0, (GLsizei)w, (GLsizei)h);

glMatrixMode(GL_PROJECTION);

glLoadIdentity();

if (w <= h)

glOrtho(0, 1.5, 0, 1.5 * (GLfloat)h/(GLfloat)w, -10.0, 10.0);

else

glOrtho(0, 1.5*(GLfloat)w/(GLfloat)h, 0, 1.5, -10.0, 10.0);

}

可以把分辨率扩大4倍。

glViewport()函数和glOrtho()函数的理解(转) - WAYNE - 闲逛

而如果再修改一下glViewport(0, 0, 2 * (GLsizei)w, 2 * (GLsizei)h); 则可以把分辨率扩大16倍。

glViewport()函数和glOrtho()函数的理解(转) - WAYNE - 闲逛

完整的测试程序:

/*Build on ubuntu 9.04*/

#include <GL/gl.h>

#include <GL/glu.h>

#include <GL/glut.h>

void init(void)

{

GLfloat mat_specular[] = {1.0, 1.0, 1.0, 1.0};

GLfloat mat_shininess[] = {50.0};

GLfloat light_position[] = {1.0, 1.0f, 1.0, 0.0};

GLfloat white_light[] = {1.0, 1.0, 1.0, 1.0};

GLfloat lmodel_ambient[] = {0.1, 0.1, 0.1, 1.0};

glClearColor(0.0, 0.0, 0.0, 0.0);

glShadeModel(GL_SMOOTH);

glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);

glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);

glLightfv(GL_LIGHT0, GL_POSITION, light_position);

glLightfv(GL_LIGHT0, GL_DIFFUSE, white_light);

glLightfv(GL_LIGHT0, GL_SPECULAR, white_light);

glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);

glEnable(GL_LIGHTING);

glEnable(GL_LIGHT0);

glEnable(GL_DEPTH_TEST);

}

void display(void)

{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glutSolidSphere(1.0, 20, 16);

glFlush();

}

void reshape(int w, int h)

{

glViewport(0, 0, (GLsizei)w, (GLsizei)h);

glMatrixMode(GL_PROJECTION);

glLoadIdentity();

if (w <= h)

glOrtho(-1.5, 1.5, -1.5 * (GLfloat)h/(GLfloat)w, 1.5 * (GLfloat)h/(GLfloat)w, -10.0, 10.0);

else

glOrtho(-1.5*(GLfloat)w/(GLfloat)h, 1.5*(GLfloat)w/(GLfloat)h, -1.5, 1.5, -10.0, 10.0);

glMatrixMode(GL_MODELVIEW);

glLoadIdentity();

}

int main(int argc, char **argv)

{

glutInit(&argc, argv);

glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH);

glutInitWindowSize(500, 500);

glutInitWindowPosition(100, 100);

glutCreateWindow(argv[0]);

init();

glutDisplayFunc(display);

glutReshapeFunc(reshape);

glutMainLoop();

return 0;

}

/*CMakeLists.txt*/

PROJECT(s5)

CMAKE_MINIMUM_REQUIRED(VERSION 2.6)

ADD_EXECUTABLE(s5 main.cpp)

FIND_PACKAGE(OpenGL)

FIND_PACKAGE(GLUT)

IF(OPENGL_FOUND)

INCLUDE_DIRECTORIES(${OPENGL_INCLUDE_DIR})

TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${OPENGL_LIBRARIES})

ELSE(OPENGL_FOUND)

MESSAGE(FATAL_ERROR "OpenGL not found")

ENDIF(OPENGL_FOUND)

IF(GLUT_FOUND)

INCLUDE_DIRECTORIES(${GLUT_INCLUDE_DIR})

TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${GLUT_LIBRARIES})

ELSE(GLUT_FOUND)

ENDIF(GLUT_FOUND)

posted @ 2011-09-27 07:57 Mr-Victor 阅读(49) 评论(0) 编辑

2011年9月11日

本文解决方案摘自:http://support.microsoft.com/kb/2517589

 

  这几天一直在写一个股票软件,用的ACCESS来存储数据,用的ADO方式来操作数据库,在自己机子上运行得好好的,在别人机子上就完全用不起,调试了很久才发现问题锁定在hr = m_pLocalConnection.CreateInstance(__uuidof(Connection));这条语句上,这条语句是创建一个Connection对象,正确执行后是能创建的,但是在别人机子上就是不能正确执行,弄了一天也没弄明白,后来跟一个朋友聊天,从他那里得知原来在Win7 SP1的操作系统上编译的ACCESS应用程序不能在比Win7 SP1的低版本操纵系统上运行,而别人的机子恰好都不是Win7 SP1的操作系统,找到原因就好办了,一下是微软给出的官方解释和解决办法。

症状:

  请考虑下面的方案。在计算机上正在运行 Windows 7 Service Pack 1 (SP1) 或 Windows 服务器 2008 R2 SP 1 或具有 KB9823246 安装,您通过使用以下应用程序之一重新编译 Microsoft ActiveX 数据对象 (ADO) 应用程序:

  • Microsoft Visual c + +
  • Microsoft Visual Basic for Applications (VBA)
  • Microsoft Visual Basic 6
  • Microsoft。NET 应用程序

  在这种情况下,您发现应用程序不在级别较低的操作系统上运行。例如,它不会运行 Windows 7 的发行版、 Windows Vista 中,和其他早期的 Windows 版本。具体取决于您的实施,您还会收到类似于下列内容之一的错误消息。(您可能收到其他错误消息。)

  错误消息 1

    REGDB_E_CLASSNOTREG (0X80040154)

  错误消息 2

    E_POINTER (0X80004003)

  错误消息 3

    E_NOINTERFACE (0X80004002)

  错误消息 4

    无法将对象强制转换 COM 类型为接口类型的 System.__ComObject 的 ADODB。连接。此操作失败,因为 COM 组件的接口 IID {00001550-0000-0010-8000-00AA006D2EA4} 上的 QueryInterface 调用失败,出现以下错误: 不支持此界面 (从 HRESULT 异常: 0x80004002 (E_NOINTERFACE))。"
 

以下 Visual c + + 代码段复制此问题。

#import " msado15.dll" no_namespace rename("EOF","EndOfFile") 
int main()
{
  CoInitialize(NULL);
  _ConnectionPtr pConnection = NULL;
  HRESULT hr = pConnection.CreateInstance(__uuidof(Connection)); //hr gets E_NOINTERFACE here
}

  下面的 Visual Basic for Applications,代码段复制此问题。

Private Sub Form_Load()
   Dim Conn As New ADODB.Connection ‘Runtime error here: Class does not support Automation or does not support expected interface
End Sub

  VBA 错误:运行时错误"430": 类不支持自动化或不支持所需的接口

  请注意 Microsoft 不能再用于 ADO 支持的主互操作程序集,并不再支持 Visual Basic 6。有关 Visual Basic 6 可支持性的详细信息,请访问下面的 MSDN 网页:

  有关 ADO 支持能力的主互操作程序集的详细信息,请单击下面的文章编号,以查看 Microsoft 知识库中相应的文章:

318559 (http://support.microsoft.com/kb/318559/ ) 有关 ADO (ADODB) 在 Visual Studio 中使用的主互操作程序集.NET
 
原因:
  因为有些 ADO 接口更改与新实例标识符 (IIDs) 相关联的 Windows 7 SP1 中,将出现此问题。较旧的 IID 接口分配以下后缀:
_Deprecated
  例如,接口 _Connection 已更新,如下所示:
  • 在 Windows 7 和更早版本的 Windows 中,_Connection IID 是 00000550-0000-0010-8000-00AA006D2EA4。
  • 在 Windows 7 SP1 中,_Connection IID 00001550-0000-0010-8000-00AA006D2EA4,并且该 IID 的 _Connection_Deprecated 00000550-0000-0010-8000-00AA006D2EA4。

  如果您的应用程序使用早期绑定到 _Connection,新 IID 存储在应用程序二进制文件在编译过程中。IID 不存在,因此,在低级别的操作系统上运行应用程序时,这会导致错误。

  一些 ADO Api 取决于平台,ado 2.7 及更高版本。在 64 位版本的 Windows 中,这些 ADO Api 处理参数使用 64 位数据类型 (如 LONGLONG 数据类型)。但是,使用这些 Api 的应用程序仍然使用 长 数据类型。因此,当您尝试运行该宏时收到"类型不匹配"错误消息。

替代方法:
  要解决此问题,请使用下列方法之一。
方法 1

  若要解决此问题的 Visual c + + 应用程序、 Visual Basic 6 的应用程序,以及 Microsoft.NET 应用程序,请按照下列步骤。

备注

  • 方法 2 不应用于应用程序的 VBA。读取下载类型库 (.tlb) 文件,在运行时,有的已编译的 Access 文件 (*.mde 或 *.accde),则可能会出现在最终用户计算机上下载的.tlb 文件。
  • 若要下载的文件,请转到此节中的"下载"小节。
  • 以管理员身份运行 CMD 提示符下,类型库的注册过程中可能需要在 Windows 7 计算机上。

对于 32 位计算机

  1. 下载到本地目录 (例如 C:\temp) 的 Msado60_Backcompat_i386.tlb。
  2. 将下载的文件复制到部署目录中。例如:
    C:\temp\Msado60_Backcompat_i386.tlb"%commonprogramfiles%\system\ado\msado60_backcompat.tlb"
  3. 注册下载的文件系统上。例如:
    %windir%\Microsoft.NET\Framework\v4.0.30319\regtlibv12"%commonprogramfiles%\system\ado\msado60_backcompat.tlb"

    请注意 如果调整为 regtlbv12.exe 的路径。NET 框架 4.0 的系统上未安装。
  4. 验证已创建以下注册表项:
    HKEY_CLASSES_ROOT\TypeLib\{0C0FF45D-87C8-4333-9075-3D9B4D64F9FC}\6.0
  5. 验证以下注册表值指向"%commonprogramfiles%\system\ado\msado60_backcompat.tlb"(在注册表中的路径可能会扩展为绝对路径):
    HKEY_CLASSES_ROOT\TypeLib\{0C0FF45D-87C8-4333-9075-3D9B4D64F9FC}\6.0\0\win32

AMD 64 计算机

  1. 下载到本地目录 (例如 C:\temp) 的 Msado60_Backcompat_i386.tlb。
  2. 下载到本地目录 (例如 C:\temp) 的 Msado60_Backcompat_x64.tlb。
  3. 将下载的文件复制到部署目录中。例如:

    C:\Temp\Msado60_Backcompat_i386.tlb"%commonprogramfiles (x86) %\System\ado\Msado60_Backcompat.tlb"
    C:\Temp\Msado60_Backcompat_x64.tlb"%commonprogramfiles%\system\ado\msado60_backcompat.tlb"
  4. 注册下载的文件系统上。例如:

    %windir%\Microsoft.NET\Framework\v4.0.30319\regtlibv12"%commonprogramfiles%\system\ado\msado60_backcompat.tlb"
    %windir%\Microsoft.NET\Framework\v4.0.30319\regtlibv12"%commonprogramfiles (x86) %\System\ado\Msado60_Backcompat.tlb"

    请注意 如果调整为 regtlbv12.exe 的路径。NET 框架 4.0 的系统上未安装。
  5. 验证已创建以下注册表项:
    HKEY_CLASSES_ROOT\TypeLib\{0C0FF45D-87C8-4333-9075-3D9B4D64F9FC}\6.0
  6. 验证以下注册表值指向"%commonprogramfiles(x86) %\System\ado\msado60_Backcompat.tlb"(在注册表中的路径可能会扩展为绝对路径):
    HKEY_CLASSES_ROOT\TypeLib\{0C0FF45D-87C8-4333-9075-3D9B4D64F9FC}\6.0\0\win32
  7. 验证以下注册表值指向"%commonprogramfiles%\system\ado\msado60_backcompat.tlb"(在注册表中的路径可能会扩展为绝对路径):
    HKEY_CLASSES_ROOT\TypeLib\{0C0FF45D-87C8-4333-9075-3D9B4D64F9FC}\6.0\0\win64

对于 IA64 的计算机

使用相同的过程作为对于 AMD64 的计算机,但下载的 Msado60_Backcompat_x64.tlb (而不是 Msado60_Backcompat_ia64.tlb。

下载

对于 c + + 开发人员

  1. 第 1 行替换第 2 行:重新编译应用程序。
    1. #import "msado15.dll" no_namespace rename("EOF","EndOfFile")
    2. #import "msado60_Backcompat.tlb" no_namespace rename("EOF","EndOfFile")

Visual Basic 6 开发人员

  1. 在项目菜单上单击引用。
  2. 单击以清除 Microsoft ActiveX 数据对象 * 库Microsoft ActiveX 数据对象的记录集 * 库 条目。
  3. 单击以选中 Microsoft ActiveX 数据对象 6.0 BackCompat 库 条目。
  4. 重新编译应用程序。
 

方法 2

您可以更改您的应用程序,以使其使用后期绑定。例如,您就可以通过 IDispatch 界面 c + + 中调用 ADO Api。

请注意 这种解决方法并不适用于 Visual Basic for Applications 的应用程序。

posted @ 2011-09-11 09:15 Mr-Victor 阅读(589) 评论(2) 编辑

2011年8月21日

本实例是《杨老师之Blog——COM组件设计与应用(四)》中的实例四,本人实现后并加以注释。

在阅读代码之前,先看一下关于“快捷方式”组件的结构示意图。


   快捷方式组件的接口结构示意图

  从结构图中可以看出,“快捷方式”组件(CLSID_ShellLink),有3个(其实不止)接口,每个接口完成一组相关功能的函数。IShellLink 接口(IID_IShellLink)提供快捷方式的参数读写功能(见下图),IPersistFile 接口(IID_IPersistFile)提供快捷方式持续性文件的读写功能。对象的持续性,是一个非常常用,并且功能强大的接口家族。但今天,我们只要了解其中两函数,就可以了:IPersistFile::Save()和IPersistFile:Load()。


    快捷方式中的各种属性
程序代码:
void CCreateShortcutDlg::OnBnClickedButton1()
{
  ::CoInitialize(NULL);    //COM初始化
 
  CString m_strExe = _T("C:\\Windows\\notepad.exe");    //EXE文件全路径名
  CString m_strLnk = _T("C:\\Users\\Mr.Victor\\Desktop\\我的记事本.lnk");   //快捷方式文件全路径名
  IShellLink *psl = NULL;
  IPersistFile *ppf = NULL;

  HRESULT hr = ::CoCreateInstance(    //启动组件
    CLSID_ShellLink,                        //快捷方式CLSID
    NULL,                         //聚合用
    CLSCTX_INPROC_SERVER,      //进程内(Shell32.dll)服务
    IID_IShellLink,              //IShellLink的IID
    (LPVOID*)&psl                //得到接口指针
  );

  if(SUCCEEDED(hr))
  {
    psl->SetPath(m_strExe);   //全路径程序名(上图中的 目标(T):
//     psl->SetArguments();   //命令行参数
//     psl->SetDescription();   //备注
//     psl->SetHotkey();    //快捷键
//     psl->SetIconLocation();   //图标
//     psl->SetShowCmd();    //窗口尺寸

    //根据EXE的文件名,得到路径名
    TCHAR szWorkPath[MAX_PATH];
    ::lstrcpy(szWorkPath,m_strExe);
    LPTSTR lp = szWorkPath;
    while(*lp)
      lp++;
    CString str = "\\";
    while(str != *lp)
      lp--;
    *lp = 0;

    //设置EXE程序的默认工作目录
    psl->SetWorkingDirectory(szWorkPath);   //上图中的 起始位置(S):

    hr = psl->QueryInterface( //查找持续性文件接口指针
      IID_IPersistFile,          //持续性接口IID
      (LPVOID*)&ppf);        //得到接口指针
   
    if(SUCCEEDED(hr))
    {
      USES_CONVERSION;  //转换为UNICODE字符串
      ppf->Save(T2COLE(m_strLnk),TRUE); //保存
    }
  }

  if(ppf)
    ppf->Release();
  if(psl)
    psl->Release();

  ::CoUninitialize();
}

总结:
ISheelLink

The IShellLink interface has the following methods.

MethodDescription
GetArguments

Gets the command-line arguments associated with a Shell link object.  获得命令行参数

GetDescription

Gets the description string for a Shell link object.  获得描述信息(备注行)

GetHotkey

Gets the keyboard shortcut (hot key) for a Shell link object.  获得快捷键

GetIconLocation

Gets the location (path and index) of the icon for a Shell link object.  获得图标

GetIDList

Gets the list of item identifiers for a Shell link object.  获得快捷方式的目标对象的item identifier list  (Windows外壳中的每个对象如文件,目录和打印机等都有唯一的item identifiler list)

GetPath

Gets the path and file name of a Shell link object.  获得快捷方式的目标文件或目录的全路径

GetShowCmd

Gets the show command for a Shell link object.  获得快捷方式的运行方式,比如常规窗口,最大化

GetWorkingDirectory

Gets the name of the working directory for a Shell link object.  获得工作目录

Resolve

Attempts to find the target of a Shell link, even if it has been moved or renamed.  按照一定的搜索规则试图获得目标对象,即使目标对象已经被删除和移动,重命名

SetArguments

Sets the command-line arguments for a Shell link object. 设置命令行参数

SetDescription

Sets the description for a Shell link object. The description can be any application-defined string. 设置描述信息(备注行)

SetHotkey

Sets a keyboard shortcut (hot key) for a Shell link object. 设置快捷键

SetIconLocation

Sets the location (path and index) of the icon for a Shell link object.  设置图标

SetIDList

Sets the PIDL for a Shell link object.  设置快捷方式的目标对象的item identifier list 

SetPath

Sets the path and file name of a Shell link object. 设置快捷方式的目标文件或目录的全路径

SetRelativePath

Sets the relative path to the Shell link object.

SetShowCmd

Sets the show command for a Shell link object. The show command sets the initial show state of the window.  设置快捷方式的运行方式,比如常规窗口,最大化

SetWorkingDirectory

Sets the name of the working directory for a Shell link object.  设置工作目录

 IPersistFile
IPersistFile主要用到两个成员函数:
1、Save
HRESULT Save(
  [in]  LPCOLESTR pszFileName,      
  [in]  BOOL fRemember    
);
参数:
  pszFileName:The absolute path of the file to which the object should be saved. If pszFileName is NULL, the object should save its data to the current file, if there is one.
  fRemember:Indicates whether the pszFileName parameter is to be used as the current working file. If TRUE, pszFileName becomes the current file and the object should clear its dirty flag after the save. If FALSE, this save operation is a Save A Copy As ... operation. In this case, the current file is unchanged and the object should not clear its dirty flag. If pszFileName is NULL, the implementation should ignore the fRemember flag.
返回值:
  If the object was successfully saved, the return value is S_OK. Otherwise, it is S_FALSE. This method can also return various storage errors.

2、Load
HRESULT Load(
  [in]  LPCOLESTR pszFileName,
  [in]  DWORD dwMode
);
参数:
  pszFileName:The absolute path of the file to be opened.
  dwMode:The access mode to be used when opening the file. Possible values are taken from the STGM enumeration. The method can treat this value as a suggestion, adding more restrictive permissions if necessary. If dwMode is 0, the implementation should open the file using whatever default permissions are used when a user opens the file.
  dwMode可取如下值:
    STGM_READ:只读
    STGM_WRITE:只写
    STGM_READWRITE:读写
返回值:
This method can return the following values.

Return codeDescription
S_OK

The method completed successfully.

E_OUTOFMEMORY

The object could not be loaded due to a lack of memory.

E_FAIL

The object could not be loaded for some reason other than a lack of memory.

 
一般情况操作如下:
  一、初始化COM接口
  二、创建IShellLink对象
  三、从IShellLink对象中获取IPersistFile对象接口
  四、操作IShellLink对象
  五、释放IPersistFile对象接口
  六、释放IShellLink对象
  七、释放COM接口

IShellLink接口获取快捷方式
  IShellLink接口获取指定的快捷方式的参数的函数主要有:GetArguments、GetDescription、GetPath、GetWorkingDirectory等。 
  GetPath用于获取该快捷方式链接的应用程序的地址,
  GetArguments用于获取该程序启动时的参数,
  GetDescription用于获取备注内容。       
  GetWorkingDirectory用于获取起始位置。
  GetPath和GetArguments得结果合起来是快捷方式属性中的目标栏的内容。

图中标注的4个点分别是GetPath、GetArguments、GetWorkingDirectory、GetDescription读取的内容。

posted @ 2011-08-21 00:22 Mr-Victor 阅读(173) 评论(0) 编辑

2011年8月20日

本实例是《杨老师之Blog——COM组件设计与应用(四)》中的实例三,本人实现后并加以注释。

void CShowJPGView::OnDraw(CDC* pDC)
{
  CShowJPGDoc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);
  if (!pDoc)
    return;

  // TODO: 在此处为本机数据添加绘制代码
  ::CoInitialize(NULL);    //初始化COM
  HRESULT hr;
  CFile file;

  file.Open("D:\\test.jpg",CFile::modeRead | CFile::shareDenyNone);   //读入文件内容
  DWORD dwSize = file.GetLength();  //获取图片文件的大小,用来分配全局内存
  HGLOBAL hMem = ::GlobalAlloc(GMEM_MOVEABLE,dwSize);   //给图片分配全局内存
  LPVOID lpBuf = ::GlobalLock(hMem);   //锁定内存
  file.Read(lpBuf,dwSize);   //读取图片到全局内存当中
  file.Close();
  ::GlobalUnlock(hMem);   //解锁内存

  IStream *pStream = NULL;   //创建一个IStream接口指针,用来保存图片流
  IPicture *pPicture = NULL;   //创建一个IPicture接口指针,表示图片对象

  //由HGLOBAL得到IStream,参数TRUE表示释放IStream的同时,释放内存
  hr = ::CreateStreamOnHGlobal(hMem,TRUE,&pStream);   //用全局内存初始化IStream接口指针
  ASSERT(SUCCEEDED(hr));

  hr = ::OleLoadPicture(pStream,dwSize,TRUE,IID_IPicture,(LPVOID*)&pPicture);   //用OleLoadPicture获得IPicture接口指针
  ASSERT(hr == S_OK);

  //得到IPicture COM接口对象后,你就可以进行获得图片信息、显示图片等操作
  long nWidth,nHeight;   //宽高,MM_HIMETRIC模式,单位是0.01毫米
  pPicture->get_Width(&nWidth);    //宽
  pPicture->get_Height(&nHeight);    //高

  //原始大小显示
  CSize sz(nWidth,nHeight);
  pDC->HIMETRICtoDP(&sz);  //转换MM_HIMETRIC模式单位为MM_TEXT像素单位
  pPicture->Render(pDC->m_hDC,0,0,sz.cx,sz.cy,0,nHeight,nWidth,-nHeight,NULL);  //在指定的DC上绘出图片
 
  //按窗口尺寸显示
//   CRect rect;
//   GetClientRect(&rect);
//   pPicture->Render(pDC->m_hDC,0,0,rect.Width(),rect.Height(),0,nHeight,nWidth,-nHeight,NULL);

  if(pPicture)
    pPicture->Release();   //释放IPicture指针
  if(pStream)
    pStream->Release();    //释放IStream指针,同时释放了hMem

  ::CoUninitialize();
}
运行结果:
原始大小显示:

按窗口尺寸显示:

注释1:
CFile:: Open

  virtual BOOL Open( LPCTSTR lpszFileName, UINT nOpenFlags, CFileException* pError = NULL );

返回值:
  如果成功打开,则返回非零值,否则为0。pError参数仅在返回0时才有意义。

参数:
  lpszFileName:待打开文件的路径,路径可为绝对、相对或网络名(UNC)。 
  nOpenFlags:一个定义了文件的共享和访问模式的UINT。它指定了打开文件后的动作,可以用OR(|)操作符将选项组合起来,至少应有一个访问权限和一个共享选项,modeCreate和modeNoInherit模式是可选的。可参阅CFile 构造函数中模式选项的列表。 
  具体如下:
    CFile::modeCreate  让构造器创建一个新文件,如果那个文件已经存在,把那个文件的长度重设为
    CFile::modeNoTruncate  可以同modeCreate. 一起用,如果要创建的文件已经存在,并不把它长度设置为0,因而这个文件获取或者作为一个新建文件或者作为一个已存在文件打开。这个功能往往很好用,比如说,当你需要打开一个设置文件,但是你并不清楚这个文件是否已经存在。
    CFile::modeRead  打开文件仅仅供读
    CFile::modeReadWrite  打开文件供读写
    CFile::modeWrite  打开文件只供写
    CFile::modeNoInherit  阻止这个文件被子进程继承
    CFile::shareDenyNone  打开这个文件同时允许其它进程读写这个文件。如果文件被其它进程以incompatibility模式打开,这是create操作会失败。
    CFile::shareDenyRead  打开文件拒绝其它任何进程读这个文件。如果文件被其它进程用compatibility模式或者是读方式打开,create操作失败。
    CFile::shareDenyWrite  打开文件拒绝其它任何进程写这个文件。如果文件被其它进程用compatibility模式或者是写方式打开,create操作失败。
    CFile::shareExclusive  以独占方式打开这个文件,不允许其它进程读写这个文件。 Construction fails if the file has been opened in any other mode for read or write access, even by the current process.
    CFile::shareCompat  这个标志在32位的MFC中无效。 This flag maps to CFile::shareExclusive when used in CFile::Open.
    CFile::typeText  设置成对回车换行对有特殊处理的文本模式(仅用在派生类中)
    CFile::typeBinary  设置二进制模式(仅用在派生类中)
  pError:指向一个存在的文件异常对象,获取失败操作的状态。

注释2:
调用GlobalAlloc函数分配一块内存,该函数会返回分配的内存句柄。
调用GlobalLock函数锁定内存块,该函数接受一个内存句柄作为参数,然后返回一个指向被锁定的内存块的指针。 您可以用该指针来读写内存。
调用GlobalUnlock函数来解锁先前被锁定的内存,该函数使得指向内存块的指针无效。
调用GlobalFree函数来释放内存块。您必须传给该函数一个内存句柄。

GlobalAlloc
  该函数从堆中分配一定数目的字节数。Win32内存管理器并不提供相互分开的局部和全局堆。提供这个函数只是为了与16位的Windows相兼容。
 
  HGLOBAL GlobalAlloc(
    UINT uFlags, // 分配属性(方式)   
    SIZE_T dwBytes // 分配的字节数   
  );
      
返回值:
  若函数调用成功,则返回一个新分配的内存对象的句柄。  
  若函数调用失败,则返回 NULL。可调用 GetLastError 以获得更多错误信息。

参数:
  uFlags:指定如何分配内存,若指定为0,则是默认的GMEM_FIXED.这个值可以是下面其中一个或几个位标识(那些指明不兼容的组合除外)
  具体如下:
    GHND  GMEM_MOVEABLE 和 GMEM_ZEROINIT的组合。
    GMEM_FIXED  分配固定的内存,返回值是一个指针。   
    GMEM_MOVEABLE  分配可移动的内存,在Win32中内存块在物理内存中是不可移动的,但在缺省堆中可以。返回值是该内存对象的句柄,可使用函数 GlobalLock 将该句柄转换为一个指针。这个标识不能与 GMEM_FIXED 组合使用。  
    GMEM_ZEROINIT  将所申请内存初始化为0。
    GPTR  GMEM_FIXED和GMEM_ZEROINIT组合。    
    GMEM_DDESHARE、GMEM_DISCARDABLE、GMEM_LOWER、GMEM_NOCOMPACT、GMEM_NODISCARD、GMEM_NOT_BANKED、GMEM_NOTIFY、GMEM_SHARE 均被忽略,这些标识只是为与 16 位 Windows 相兼容而提供的。
  dwBytes:指定要申请的字节数。若该参数为 0 且参数 uFlags 指定为 GMEM_MOVEABLE 则该函数返回一个内存对象的句柄,该内存对象被标识为discarded(可抛弃的)。

注解:
  如果堆内没有足够的空间满足请求,函数将返回 NULL。因为NULL是用于标明错误的,所以不会分配虚拟0地址。   
  因此很容易检测出是否在使用一个NULL指针。
  使用此函数分配内存可以保证8字节的边界。所有的内存均在执行访问时创建;不需要特别的函数来动态执行所产生的代码。
  若函数调用成功,将至少分配所需内存。若实际分配量超过所需,则内存仍然能够充分利用之。可用函数 GlobalSize 来确定实际所分配的字节数。   
  可使用 GlobalFree 来释放内存。
 
GlobalLock
  锁定内存中指定的内存块,并返回一个地址值,令其指向内存块的起始处。除非用 GlobalUnlock 函数将内存块解锁,否则地址会一直保持有效。Windows 为每个内存对象都维持着一个锁定计数。对这个函数的每次调用都应有一个对应的 GlobalUnlock 调用。
  
  LPVOID GlobalLock(
    HGLOBAL hMem   // handle to global memory object
  );
 
  一般情况下我们在编程的时候,给应用程序分配的内存都是可以移动的或者是可以丢弃的,这样能使有限的内存资源充分利用,所以,在某一个时候我们分配的那块内存的地址是不确定的,因为他是可以移动的,所以得先锁定那块内存块,这儿应用程序需要调用API函数GlobalLock函数来锁定句柄。如下: lpMem=GlobalLock(hMem); 这样应用程序才能存取这块内存。
参数:
  hMem:全局内存对象的句柄。这个句柄是通过GlobalAlloc或GlobalReAlloc来得到的
返回值:
  调用成功,返回指向该对象的第一个字节的指针
  调用失败,返回NULL,可以用GetLastError来获得出错信息
注意:
  调用过GlobalLock锁定一块内存区后,一定要调用GlobalUnlock来解锁。
 
GlobalUnlock
  GlobalUnlock函数解除锁定的内存块,使指向该内存块的指针无效,GlobalLock锁定的内存,一定要用GlobalUnlock解锁。
 
  BOOL GlobalUnlock(
    HGLOBAL hMem   // handle to global memory object
  );
参数:
  hMem:全局内存对象的句柄
返回值:
  非零值,指定的内存对象仍处于被锁定状态
  0,函数执行出错,可以用GetLastError来获得出错信息,如果返回NO_ERROR,则表示内存对象已经解锁了
注意:    
  这个函数实际上是将内存对象的锁定计数器减一,如果计数器不为0,则表示执行过多个GlobalLock函数来对这个内存对象加锁,需要对应数目的GlobalUnlock函数来解锁。
  如果通过GetLastError函数返回错误码为ERROR_NOT_LOCKED,则表示未加锁或已经解锁。
 
注释3:
CreateStreamOnHGlobal
  CreateStreamOnHGlobal函数从指定内存创建流对象。
 
  WINOLEAPI CreateStreamOnHGlobal(
    HGLOBAL hGlobal,              //Memory handle for the stream object
    BOOL fDeleteOnRelease,     //Whether to free memory when the
                                     // object is released
    LPSTREAM *ppstm             //Address of output variable that
                                    // receives the IStream interface pointer
 );

参数:
  hGlobal:由GlobalAlloc函数分配的内存句柄。   
  fDeleteOnRelease:该参数指明上一个参数制定的内存在该对象被释放后是否也自动释放。如果该参数设定为FALSE,那么调用者必须显示的释放hGlobal。如果该参数设置为TRUE,则hGlobal最终会自动释放。   
  ppstm:IStream指针的地址,该指针在该函数执行后指向新创建的流对象。该参数不能未NULL。   如果函数创建流对象成功则返回S_OK。

注释4:
OleLoadPicture
  用API OleLoadPicture来加载JPG、GIF格式的图片(注:不支持PNG格式,另外GIF只能加载第一帧,且不支持透明)
 OleLoadPicture 函数实际上创建了一个IPicture类型的COM接口对象,然后我们可以通过这个COM接口来操作图片(实际上你也可以用API OleCreatePictureIndirect来加载图片,不过相比而言OleLoadPicture函数简化了基于流的IPicture对象的创 建)。

posted @ 2011-08-20 22:45 Mr-Victor 阅读(248) 评论(0) 编辑

本文摘自:http://blog.vckbase.com/teacheryang/archive/2005/07/04/9135.html

一、前言
 在 VCKBASE 的顶力支持下,在各位网友回帖的鼓励下,我才能顺利完成系列论文的前三回。书到本回,我们终于开始写代码啦。写点啥那?恩,有了!咱们先从如何调用现成的简单的组件开始吧,同时也顺便介绍一些相关的知识。

二、组件的启动和释放

  在第三回中,大家用“小本本”记录了一个原则:COM 组件是运行在分布式环境中的 。于是,如何启动组件立刻就遇到了严重的问题,大家看这段代码:

      p = new 对象;
      p->对象函数();
      delete p;

  这样的代码再熟悉不过了,在本地进程中运行是不会有问题的。但是你想想,如果这个对象是在“地球另一边”的计算机上,结果会如何?嘿嘿,C++ 在设计 new 的时候,可没有考虑远程的实现呀(计算机语言当然不会,也没必要去设计)。因此启动组件、调用接口的功能,当然就由 COM 系统来实现了。


图一 组件调用机制
  由上图可以看出,当调用组件的时候,其实是依靠代理(运行在本地)和存根(运行在远端)之间的通讯完成的。具体来说,当客户程序通过 CoCreateInstance() 函数启动组件,则代理接管该调用,它和存根通讯,存根则它所在的本地(相对于客户程序来说就是远程了)执行 new 操作加载对象。对于初学者,你可以不用理它,代理和存根对我们来说是透明的。只要大约知道是怎么一回事就一切OK了。
  问题又来了,这个远程的对象什么时候消灭呢?在第二回介绍接口概念的时候,当时我们特意忽略了两个函数,就是IUnknown::AddRef()和IUnknown::Release(),从函数名就能猜到了,一个是对内部引用记数器(Ref)加1,一个是释放(减1),当记数器减为0的时候,就是释放的机会啦。看起来很复杂,没办法,因为这是在介绍原理。其实在我们写程序的时候到比较简单,请大家遵守几个原则:
  1、启动组件得到一个接口指针(Interface)后,不要调用AddRef()。因为系统知道你得到了一个指针,所以它已经帮你调用了AddRef()函数;
  2、通过QueryInterface()得到另一个接口指针后,不要调用AddRef()。因为......和上面的道理一样;
  3、当你把接口指针赋值给(保存到)另一个变量中的时候,请调用AddRef();
  4、当不需要再使用接口指针的时候,务必执行Release()释放;
  5、当使用智能指针的时候,可以省略指针的维护工作;(注1)


三、内存分配和释放

  自从学习了C语言,老师就教导我们说:对于动态内存的申请和释放,一定要遵守“谁申请,谁释放”的原则。在此原则的指导下,不仅是我、不仅是你,就连特级大师都设计了这样怪怪的函数:

函数 说明 评论
GetWindowText(HWND,LPTSTR,int) 取得窗口标题。需要在参数中给出保存标题所使用的内存指针,和这块内存的尺寸。 晕!我又不知道窗口标题的长度,居然还要我提供尺寸?!没办法,只能估摸着给一个大一些的尺寸吧。
sprintf(char *,const char *,...) 格式化一个字符串。这个函数不用给出缓冲区的长度啦。 恩,虽然不用给出长度了,但你敢给个小尺寸吗?哼!
int CListBox::GetTextLen(int)
CListBox::GetText(int,LPTSTR)
取得列表窗中子项目的标题。需要调用两个函数,先取得长度,然后分配内存,再实际取得标题内容。 真烦!

  说实在的,不但函数调用者感觉别扭,就连函数设计者心情也不会爽的,而这一切都是为了满足所谓“谁申请,谁释放”的原则。 解决这个问题最好的方式就是:函数内部根据实际需要动态申请内存,而调用者负责释放。这虽然违背了上述原则,但 COM 从方便性和效率出发,确实是这么设计的。

  C语言 C++语言 Windows 平台 COM IMalloc 接口 BSTR
申请 malloc() new GlobalAlloc() CoTaskMemAlloc() Alloc() SysAllocString()
重新申请 realloc()   GlobalReAlloc() CoTaskRealloc() Realloc() SysReAllocString()
释放 free() delete GlobalFree() CoTaskMemFree() Free() SysFreeString()

  以上这些函数必须要按类型配合使用(比如:new 申请的内存,则必须用 delete 释放)。在 COM 内部,当然你可以随便使用任何类型的内存分配释放函数,但组件如果需要与客户进行内存的交互,则必须使用上表中的后三类函数族。
  1、BSTR 内存在上回书中,已经有比较丰富的介绍了,不再重复;
  2、CoTaskXXX()函数族,其本质上就是调用C语言的函数(malloc...);
  3、IMalloc 接口又是对 CoTaskXXX() 函数族的一个包装。包装后,同时增强了一些功能,比如:IMalloc::GetSize()可以取得尺寸,使用 IMallocSpy 可以监视内存的使用;


四、参数传递方向
  在C语言的函数声明中,尤其当参数为指针的时候,你是看不出它传递方向的。比如:
void fun(char * p1, int * p2); 请问,p1、p2 哪个是入参?哪个是出参?甚或都是入参或都是出参?由于牵扯到内存分配和释放等问题,COM 需要明确标注参数方向。以后我们写程序,就类似下面的样子:

      HRESULT Add([in] long n1, [in] long n2, [out] long *pnSum);  // IDL文件(注2)
      STDMETHOD(Add)(/*[in]*/ long n1, /*[in]*/ long n2, /*[out]*/ long *pnSum);  // .h文件

如果参数是动态分配的内存指针,那么遵守如下的规定:

方向 申请人 释放人 提示
[in] 调用者 调用者 组件接收指针后,不能重新分配内存
[out] 组件 调用者 组件返回指针后,调用者“爱咋咋地”(注3)
[in,out] 调用者 调用者 组件可以重新分配内存


五、示例程序
  示例一、由 CLSID 得到 ProgID。(程序以 word 为例子。如果运行不正确,嘿嘿,你没有安装 word 吧?)

	::CoInitialize( NULL );

	HRESULT hr;
	// {000209FF-0000-0000-C000-000000000046} = word.application.9
	CLSID clsid = {0x209ff,0,0,{0xc0,0,0,0,0,0,0,0x46}};
	LPOLESTR lpwProgID = NULL;
	
	hr = ::ProgIDFromCLSID( clsid, &lpwProgID );
	if ( SUCCEEDED(hr) )
	{
		::MessageBoxW( NULL, lpwProgID, L"ProgID", MB_OK );

		IMalloc * pMalloc = NULL;
		hr = ::CoGetMalloc( 1, &pMalloc );  // 取得 IMalloc
		if ( SUCCEEDED(hr) )
		{
			pMalloc->Free( lpwProgID );  // 释放ProgID内存
			pMalloc->Release();          // 释放IMalloc
		}
	}

	::CoUninitialize();


示例二、如何使用“浏览文件夹”选择对话窗。

CString BrowseFolder(HWND hWnd, LPCTSTR lpTitle)
{
    // 调用 SHBrowseForFolder 取得目录(文件夹)名称
    // 参数 hWnd: 父窗口句柄
    // 参数 lpTitle: 窗口标题
    
    char szPath[MAX_PATH]={0};
    BROWSEINFO m_bi;

    m_bi.ulFlags = BIF_RETURNONLYFSDIRS  | BIF_STATUSTEXT;
    m_bi.hwndOwner = hWnd;
    m_bi.pidlRoot = NULL;
    m_bi.lpszTitle = lpTitle;
    m_bi.lpfn = NULL;
    m_bi.lParam = NULL;
    m_bi.pszDisplayName = szPath;

    LPITEMIDLIST pidl = ::SHBrowseForFolder( &m_bi );
    if ( pidl )
    {
        if( !::SHGetPathFromIDList ( pidl, szPath ) )  szPath[0]=0;

        IMalloc * pMalloc = NULL;
        if ( SUCCEEDED ( ::SHGetMalloc( &pMalloc ) ) )  // 取得IMalloc分配器接口
        {
            pMalloc->Free( pidl );    // 释放内存
            pMalloc->Release();       // 释放接口
        }
    }
    return szPath;
}

示例三、在窗口中显示一幅 JPG 图象。

void CxxxView::OnDraw(CDC* pDC)
{
	::CoInitialize(NULL);  // COM 初始化
	HRESULT hr;
	CFile file;
	
	file.Open( "c:\\aa.jpg", CFile::modeRead | CFile::shareDenyNone );  // 读入文件内容
	DWORD dwSize = file.GetLength();
	HGLOBAL hMem = ::GlobalAlloc( GMEM_MOVEABLE, dwSize );
	LPVOID lpBuf = ::GlobalLock( hMem );
	file.ReadHuge( lpBuf, dwSize );
	file.Close();
	::GlobalUnlock( hMem );

	IStream * pStream = NULL;
	IPicture * pPicture = NULL;
	
	// 由 HGLOBAL 得到 IStream,参数 TRUE 表示释放 IStream 的同时,释放内存
	hr = ::CreateStreamOnHGlobal( hMem, TRUE, &pStream );
	ASSERT ( SUCCEEDED(hr) );
	
	hr = ::OleLoadPicture( pStream, dwSize, TRUE, IID_IPicture, ( LPVOID * )&pPicture );
	ASSERT(hr==S_OK);
	
	long nWidth,nHeight;  // 宽高,MM_HIMETRIC 模式,单位是0.01毫米
	pPicture->get_Width( &nWidth );    // 宽
	pPicture->get_Height( &nHeight );  // 高
	
	////////原大显示//////
	CSize sz( nWidth, nHeight );
	pDC->HIMETRICtoDP( &sz );  // 转换 MM_HIMETRIC 模式单位为 MM_TEXT 像素单位
	pPicture->Render(pDC->m_hDC,0,0,sz.cx,sz.cy,
		0,nHeight,nWidth,-nHeight,NULL);
		
	////////按窗口尺寸显示////////
//	CRect rect;	GetClientRect(&rect);
//	pPicture->Render(pDC->m_hDC,0,0,rect.Width(),rect.Height(),
//		0,nHeight,nWidth,-nHeight,NULL);

	if ( pPicture ) pPicture->Release();// 释放 IPicture 指针
	if ( pStream ) pStream->Release();  // 释放 IStream 指针,同时释放了 hMem
	
	::CoUninitialize();
}

示例四、在桌面建立快捷方式
在阅读代码之前,先看一下关于“快捷方式”组件的结构示意图。


图二、快捷方式组件的接口结构示意图
  从结构图中可以看出,“快捷方式”组件(CLSID_ShellLink),有3个(其实不止)接口,每个接口完成一组相关功能的函数。IShellLink 接口(IID_IShellLink)提供快捷方式的参数读写功能(见图三),IPersistFile 接口(IID_IPersistFile)提供快捷方式持续性文件的读写功能。对象的持续性(注5),是一个非常常用,并且功能强大的接口家族。但今天,我们只要了解其中两函数,就可以了:IPersistFile::Save()和IPersistFile:Load()。(注6)


图三、快捷方式中的各种属性

#include < atlconv.h >
void CreateShortcut(LPCTSTR lpszExe, LPCTSTR lpszLnk)
{
	// 建立块捷方式
	// 参数 lpszExe: EXE 文件全路径名
	// 参数 lpszLnk: 快捷方式文件全路径名
	
	::CoInitialize( NULL );

	IShellLink * psl = NULL;
	IPersistFile * ppf = NULL;

	HRESULT hr = ::CoCreateInstance(  // 启动组件
		CLSID_ShellLink,      // 快捷方式 CLSID
		NULL,                 // 聚合用(注4)
		CLSCTX_INPROC_SERVER, // 进程内(Shell32.dll)服务
		IID_IShellLink,       // IShellLink 的 IID
		(LPVOID *)&psl );     // 得到接口指针

	if ( SUCCEEDED(hr) )
	{
		psl->SetPath( lpszExe );  // 全路径程序名
//		psl->SetArguments();      // 命令行参数
//		psl->SetDescription();    // 备注
//		psl->SetHotkey();         // 快捷键
//		psl->SetIconLocation();   // 图标
//		psl->SetShowCmd();        // 窗口尺寸
		
		// 根据 EXE 的文件名,得到目录名
		TCHAR szWorkPath[ MAX_PATH ];
		::lstrcpy( szWorkPath, lpszExe );
		LPTSTR lp = szWorkPath;
		while( *lp )    lp++;
		while( ''\\'' != *lp )    lp--;
		*lp=0;

		// 设置 EXE 程序的默认工作目录
		psl->SetWorkingDirectory( szWorkPath );

		hr = psl->QueryInterface(  // 查找持续性文件接口指针
			IID_IPersistFile,      // 持续性接口 IID
			(LPVOID *)&ppf );      // 得到接口指针

		if ( SUCCEEDED(hr) )
		{
			USES_CONVERSION;       // 转换为 UNICODE 字符串
			ppf->Save( T2COLE( lpszLnk ), TRUE );  // 保存
		}
	}
	if ( ppf )	ppf->Release();
	if ( psl )	psl->Release();

	::CoUninitialize();
}

void OnXXX()
{
	CreateShortcut(
		_T("c:\\winnt\\notepad.exe"),  // 记事本程序。注意,你的系统是否也是这个目录?
		_T("c:\\Documents and Settings\\Administrator\\桌面\\我的记事本.lnk")
	);
	// 桌面上建立快捷方式(lnk)文件的全路径名。注意,你的系统是否也是这个目录?
	// 如果用程序实现寻找桌面的路径,则可以查注册表
	// HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders
}


七、小结

  本回介绍的内容比较实用。大家不要只抄袭代码,而一定要理解它。结合 MSDN 的说明去思索代码、理解其含义。好了,想方设法把代码忘掉!三天后(如过你还没有忘记,那就再过三天),你在不参考示例代码,但可以随便翻阅 MSDN 的情况下,自己能独立地再次完成这四个例程,那么恭喜你,你已经入门了:0) 从下回开始,我们要用 ATL 做 COM 的开发工作啦,您老人家准备好了吗?

作业,留作业啦......
  1、你已经学会如何建立快捷方式了,那么你知道怎么读取它的属性吗?(如果写不出这个程序,那么你就不用继续学习了。因为......动点脑筋呀!我还没有见过象你这么笨的学生呢!)
  2、示例程序三中使用了 IPicture 接口显示一个 JPG 图象。那么你现在去完成一个功能,把 JPG 文件转换为 BMP 文件。


注1:智能指针的概念和用法,后续介绍。
注2:IDL 文件,下回就要介绍啦。
注3:东北话,想干什么都可以,反正我不管啦。
注4:聚合,也许在第30回中介绍吧:-)
注5:持续性,IPersistXXXXXX是一个非常强大的接口家族,后续介绍。
注6:想知道 IShellLink、IPersistFile接口的所有函数吗?别愣着,快去看MSDN呀......

posted @ 2011-08-20 20:59 Mr-Victor 阅读(55) 评论(0) 编辑

本文摘自:http://blog.vckbase.com/teacheryang/archive/2005/06/27/8885.html

一、前言
  上回书介绍了GUID、CLSID、IID和接口的概念。本回的重点是介绍 COM 中的数据类型。咋还不介绍组件程序的设计步骤呀?咳......别着急,别着急!孔子曰:“饭要一口一口地吃”;老子语:“心急吃不了热豆腐”,孙子云:“走一步看一步吧” ...... 先掌握必要的知识,将来写起程序来才会得心应手也:-)
  走入正题之前,请大家牢牢记住一条原则:COM 组件是运行在分布式环境中的。比如,你写了一个组件程序(DLL或EXE),那么使用者可能是在本机的某个进程内加载组件(INPROC_SERVER);也可能是从另一个进程中调用组件的进程(LOCAL_SERVER);也可能是在这台计算机上调用地球那边计算机上的组件(REMOTE_SERVER)。所以在理解和设计的时候,要时时刻刻想起这句话。快!拿出小本本,记下来!

二、HRESULT 函数返回值
  每个人在做程序设计的时候,都有他们各自的哲学思想。拿函数返回值来说,就有好多种形式。

函数 返回值 返回值信息
double sin(double)

浮点数值

计算正玄值
BOOL DeleteFile(LPCTSTR)

布尔值

文件删除是否成功。如失败,需要GetLastError()才能取得失败原因
void * malloc(size_t)

内存指针

内存申请,如果失败,返回空指针 NULL
LONG RegDeleteKey(HKEY,LPCTSTR)

整数

删除注册表项。0表示成功,非0失败,同时这个值就反映了失败的原因
UINT DragQueryFile(HDROP,UINT,LPTSTR,UINT)

整数

取得拖放文件信息。以不同的参数调用,则返回不同的含义:
一会儿表示文件个数,一会儿表示文件名长度,一会儿表示字符长度
...... ......

...

...... ......

  如此纷繁复杂的返回值,如此含义多变的返回值,使得大家在学习和使用的过程中,增加了额外的困难。好了,COM 的设计规范终于对他们进行了统一。组件API及接口指针中,除了IUnknown::AddRef()和IUnknown::Release()两个函数外,其它所有的函数,都以 HRESULT 作为返回值。大家想象一个组件的接口函数比如叫Add(),完成2个整数的加法运算,在C语言中,我们可以如下定义:

      long Add( long n1, long n2 )
      {
          return n1 + n2;
      }

  还记得刚才我们说的原则吗?COM 组件是运行在分布式环境中的。也就是说,这个函数可能运行在“地球另一边”的计算机上,既然运行在那么遥远的地方,就有可能出现服务器关机、网络掉线、运行超时、对方不在服务区......等异常。于是,这个加法函数,除了需要返回运算结果以外,还应该返回一个值------函数是否被正常执行了。

      HRESULT Add( long n1, long n2, long *pSum )
      {
          *pSum = n1 + n2;
          return S_OK;
      }

  如果函数正常执行,则返回 S_OK,同时真正的函数运行结果则通过参数指针返回。如果遇到了异常情况,则COM系统经过判断,会返回相应的错误值。常见的返回值有:

HRESULT 含义
S_OK 0x00000000 成功
S_FALSE 0x00000001 函数成功执行完成,但返回时出现错误
E_INVALIDARG 0x80070057 参数有错误
E_OUTOFMEMORY 0x8007000E 内存申请错误
E_UNEXPECTED 0x8000FFFF 未知的异常
E_NOTIMPL 0x80004001 未实现功能
E_FAIL 0x80004005 没有详细说明的错误。一般需要取得 Rich Error 错误信息(注1)
E_POINTER 0x80004003 无效的指针
E_HANDLE 0x80070006 无效的句柄
E_ABORT 0x80004004 终止操作
E_ACCESSDENIED 0x80070005 访问被拒绝
E_NOINTERFACE 0x80004002 不支持接口


图一、HRESULT 的结构
  HRESULT 其实是一个双字节的值,其最高位(bit)如果是0表示成功,1表示错误。具体参见 MSDN 之"Structure of COM Error Codes"说明。我们在程序中如果需要判断返回值,则可以使用比较运算符号;switch开关语句;也可以使用VC提供的宏:

      HRESULT hr = 调用组件函数;
      if( SUCCEEDED( hr ) ){...} // 如果成功
      ......
      if( FAILED( hr ) ){...} // 如果失败
      ......

三、UNICODE
  计算机发明后,为了在计算机中表示字符,人们制定了一种编码,叫ASCII码。ASCII码由一个字节中的7位(bit)表示,范围是0x00 - 0x7F 共128个字符。他们以为这128个数字就足够表示abcd....ABCD....1234 这些字符了。
  咳......说英语的人就是“笨”!后来他们突然发现,如果需要按照表格方式打印这些字符的时候,缺少了“制表符”。于是又扩展了ASCII的定义,使用一个字节的全部8位(bit)来表示字符了,这就叫扩展ASCII码。范围是0x00 - 0xFF 共256个字符。
  咳......说中文的人就是聪明!中国人利用连续2个扩展ASCII码的扩展区域(0xA0以后)来表示一个汉字,该方法的标准叫GB-2312。后来,日文、韩文、阿拉伯文、台湾繁体(BIG-5)......都使用类似的方法扩展了本地字符集的定义,现在统一称为 MBCS 字符集(多字节字符集)。这个方法是有缺陷的,因为各个国家地区定义的字符集有交集,因此使用GB-2312的软件,就不能在BIG-5的环境下运行(显示乱码),反之亦然。
  咳......说英语的人终于变“聪明”一些了。为了把全世界人民所有的所有的文字符号都统一进行编码,于是制定了UNICODE标准字符集。UNICODE 使用2个字节表示一个字符(unsigned shor int、WCHAR、_wchar_t、OLECHAR)。这下终于好啦,全世界任何一个地区的软件,可以不用修改地就能在另一个地区运行了。虽然我用 IE 浏览日本网站,显示出我不认识的日文文字,但至少不会是乱码了。UNICODE 的范围是 0x0000 - 0xFFFF 共6万多个字符,其中光汉字就占用了4万多个。嘿嘿,中国人赚大发了:0)
  在程序中使用各种字符集的方法:

      const char * p = "Hello";   // 使用 ASCII 字符集
      const char * p = "你好";    // 使用 MBCS 字符集,由于 MBCS 完全兼容 ASCII,多数情况下,我们并不严格区分他们
      LPCSTR p = "Hello,你好";  // 意义同上
      
      const WCHAR * p = L"Hello,你好";    // 使用 UNICODE 字符集
      LPCOLESTR p = L"Hello,你好";    // 意义同上
      
      // 如果预定义了_UNICODE,则表示使用UNICODE字符集;如果定义了_MBCS,则表示使用 MBCS
      const TCHAR * p = _T("Hello,你好"); 
      LPCTSTR p = _T("Hello,你好"); // 意义同上

  在上面的例子中,T是非常有意思的一个符号(TCHAR、LPCTSTR、LPTSTR、_T()、_TEXT()...),它表示使用一种中间类型,既不明确表示使用 MBCS,也不明确表示使用 UNICODE。那到底使用哪种字符集那?嘿嘿......编译的时候决定吧。设置条件编译的方式是:VC6中,"Project\Settings...\C/C++卡片 Preprocessor definitions" 中添加或修改 _MBCS、_UNICODE;VC.NET中,"项目\属性\配置属性\常规\字符集"然后用组合窗进行选择。使用 T 类型,是非常好的习惯,严重推荐!

四、BSTR
  COM 中除了使用一些简单标准的数据类型外(注2),字符串类型需要特别重点地说明一下。还记得原则吗?COM 组件是运行在分布式环境中的。通俗地说,你不能直接把一个内存指针直接作为参数传递给COM函数。你想想,系统需要把这块内存的内容传递到“地球另一 边”的计算机上,因此,我至少需要知道你这块内存的尺寸吧?不然让我如何传递呀?传递多少字节呀?!而字符串又是非常常用的一种类型,因此 COM 设计者引入了 BASIC 中字符串类型的表示方式---BSTR。BSTR 其实是一个指针类型,它的内存结构是:(输入程序片段 BSTR p = ::SysAllocString(L"Hello,你好");断点执行,然后观察p的内存)


图二、BSTR 内存结构
  BSTR 是一个指向 UNICODE 字符串的指针,且 BSTR 向前的4个字节中,使用DWORD保存着这个字符串的字节长度( 没有含字符串的结束符)。因此系统就能够正确处理并传送这个字符串到“地球另一 边”了。特别需要注意的是,由于BSTR的指针就是指向 UNICODE 串,因此 BSTR 和 LPOLESTR 可以在一定程度上混用,但一定要注意:
  有函数 fun(LPCOLESTR lp),则你调用 BSTR p=...; fun(p); 正确
  有函数 fun(const BSTR bstr),则你调用 LPCOLESTR p=...; fun(p); 错误!!!
有关 BSTR 的处理函数:

API 函数 说明
SysAllocString() 申请一个 BSTR 指针,并初始化为一个字符串
SysFreeString() 释放 BSTR 内存
SysAllocStringLen() 申请一个指定字符长度的 BSTR 指针,并初始化为一个字符串
SysAllocStringByteLen() 申请一个指定字节长度的 BSTR 指针,并初始化为一个字符串
SysReAllocStringLen() 重新申请 BSTR 指针

CString 函数

说明

AllocSysString() 从 CString 得到 BSTR
SetSysString() 重新申请 BSTR 指针,并复制到 CString 中

CComBSTR 函数

ATL 的 BSTR 包装类。在 atlbase.h 中定义

Append()、AppendBSTR()、AppendBytes()、ArrayToBSTR()、BSTRToArray()、AssignBSTR()、Attach()、Detach()、Copy()、CopyTo()、Empty()、Length()、ByteLength()、ReadFromStream()、WriteToStream()、LoadString()、ToLower()、ToUpper()
运算符重载:!,!=,==,<,>,&,+=,+,=,BSTR
太多了,但从函数名称不能看出其基本功能。详细资料,查看MSDN 吧。另外,左侧函数,有很多是 ATL 7.0 提供的,VC6.0 下所带的 ATL 3.0 不支持。
由于我们将来主要用 ATL 开发组件程序,因此使用 ATL 的 CComBSTR 为主。VC也提供了其它的包装类 _bstr_t。


五、各种字符串类型之间的转换
  1、函数 WideCharToMultiByte(),转换 UNICODE 到 MBCS。使用范例:

      LPCOLESTR lpw = L"Hello,你好";
      size_t wLen = wcslen( lpw ) + 1;  // 宽字符字符长度,+1表示包含字符串结束符
      
      int aLen=WideCharToMultiByte(  // 第一次调用,计算所需 MBCS 字符串字节长度
		CP_ACP,
		0,
		lpw,  // 宽字符串指针
		wLen, // 字符长度
		NULL,
		0,  // 参数0表示计算转换后的字符空间
		NULL,
		NULL);
	
      LPSTR lpa = new char [aLen];
	
      WideCharToMultiByte(
		CP_ACP,
		0,
		lpw,
		wLen,
		lpa,  // 转换后的字符串指针
		aLen, // 给出空间大小
		NULL,
		NULL);

      // 此时,lpa 中保存着转换后的 MBCS 字符串
      ... ... ... ...
      delete [] lpa;


2、函数 MultiByteToWideChar(),转换 MBCS 到 UNICODE。使用范例:

      LPCSTR lpa = "Hello,你好";
      size_t aLen = strlen( lpa ) + 1;
      
      int wLen = MultiByteToWideChar(
		CP_ACP,
		0,
		lpa,
		aLen,
		NULL,
		0);
      
      LPOLESTR lpw = new WCHAR [wLen];
      MultiByteToWideChar(
		CP_ACP,
		0,
		lpa,
		aLen,
		lpw,
		wLen);
      ... ... ... ...
      delete [] lpw;


3、使用 ATL 提供的转换宏。

A2BSTR OLE2A T2A W2A
A2COLE OLE2BSTR T2BSTR W2BSTR
A2CT OLE2CA T2CA W2CA
A2CW OLE2CT T2COLE W2COLE
A2OLE OLE2CW T2CW W2CT
A2T OLE2T T2OLE W2OLE
A2W OLE2W T2W W2T

上表中的宏函数,其实非常容易记忆:
2 好搞笑的缩写,to 的发音和 2 一样,所以借用来表示“转换为、转换到”的含义。
A ANSI 字符串,也就是 MBCS。
W、OLE 宽字符串,也就是 UNICODE。
T 中间类型T。如果定义了 _UNICODE,则T表示W;如果定义了 _MBCS,则T表示A
C const 的缩写

使用范例:

      #include <atlconv.h>      
      void fun()
      {
        USES_CONVERSION;  // 只需要调用一次,就可以在函数中进行多次转换
          
        LPCTSTR lp = OLE2CT( L"Hello,你好") );
            ... ... ... ...
            // 不用显式释放 lp 的内存,因为
            // 由于 ATL 转换宏使用栈作为临时空间,函数结束后会自动释放栈空间。
      }

  使用 ATL 转换宏,由于不用释放临时空间,所以使用起来非常方便。但是考虑到栈空间的尺寸(VC 默认2M),使用时要注意几点:
1、只适合于进行短字符串的转换;
2、不要试图在一个次数比较多的循环体内进行转换;
3、不要试图对字符型文件内容进行转换,因为文件尺寸一般情况下是比较大的;
4、对情况 2 和 3,要使用 MultiByteToWideChar() 和 WideCharToMultiByte();

六、VARIANT
  C++、BASIC、Java、Pascal、Script......计算机语言多种多样,而它们各自又都有自己的数据类型,COM 产生目的,其中之一就是要跨语言(注3)。而 VARIANT 数据类型就具有跨语言的特性,同时它可以表示(存储)任意类型的数据。从C语言的角度来讲,VARIANT 其实是一个结构,结构中用一个域(vt)表示------该变量到底表示的是什么类型数据,同时真正的数据则存贮在 union 空间中。结构的定义太长了(虽然长,但其实很简单)大家去看 MSDN 的描述吧,这里给出如何使用的简单示例:

学生:我想用 VARIANT 表示一个4字节长的整数,如何做?
老师:VARIANT v; v.vt=VT_I4; v.lVal=100;

学生:我想用 VARIANT 表示布尔值“真”,如何做?
老师:VARIANT v; v.vt=VT_BOOL; v.boolVal=VARIANT_TRUE;
学生:这么麻烦?我能不能 v.boolVal=true; 这样写?
老师:不可以!因为

类型 字节长度 假值 真值
bool 1(char) 0(false) 1(true)
BOOL 4(int) 0(FALSE) 1(TRUE)
VT_BOOL 2(short int) 0(VARIANT_FALSE) -1(VARIANT_TRUE)

  所以如果你 v.boolVal=true 这样赋值,那么将来 if(VARIANT_TRUE==v.boolVal) 的时候会出问题(-1 != 1)。但是你注意观察,任何布尔类型的“假”都是0,因此作为一个好习惯,在做布尔判断的时候,不要和“真值”相比较,而要与“假值”做比较。
学生:谢谢老师,你太牛了。我对老师的敬仰如滔滔江水,连绵不绝......

学生:我想用 VARIANT 保存字符串,如何做?
老师:VARIANT v; v.vt=VT_BSTR; v.bstrVal=SysAllocString(L"Hello,你好");

学生:哦......我明白了。可是这么操作真够麻烦的,有没有简单一些的方法?
老师:有呀,你可以使用现成的包装类 CComVariant、COleVariant、_variant_t。比如上面三个问题就可以这样书写:CComVariant v1(100),v2(true),v3("Hello,你好"); 简单了吧?!(注4)

学生:老师,我再问最后一个问题,我如何用 VARIANT 保存一个数组?
老师:这个问题很复杂,我现在不能告诉你,我现在告诉你怕你印象不深......(注5)
学生:~!@#$%^&*()......晕!

七、小结
以上所介绍的内容,是基本功,必须熟练掌握。先到这里吧,休息一会儿......更多精彩内容,敬请关注《COM 组件设计与应用(四)》


注1:在后续的 ISupportErrorInfo 接口中介绍。
注2:常见的数据类型,请参考 IDL 文件的说明。(别着急,还没写那......嘿嘿)
注3:跨语言就是各种语言中都能使用COM组件。但啥时候能跨平台呢?
注4:CComVariant/COlevariant/_variant_t 请参看 MSDN。
注5:关于安全数组 SafeArray 的使用,在后续的文章中讨论。

posted @ 2011-08-20 20:53 Mr-Victor 阅读(46) 评论(0) 编辑

本文摘自:http://blog.vckbase.com/teacheryang/archive/2005/06/27/8884.html

一、前言
  书接上回,话说在 doc(Word) 复合文件中,已经解决了保存 xls(Excel) 数据的问题了。那么,接下来又要解决另一个问题:当 WORD 程序读取复合文件,遇到了 xls 数据的时候,它该如何启动 Excel 呢?启动后,又如何让 Excel 自己去读入、解析、显示 xls 数据呢?

二、CLSID 概念
  有一个非常简单的解决方案,那就是在对象数据的前面,保存有处理这个数据的程序名。(见下图左上)


图一、CLSID 的概念
  这的确是一个简单的方法,但同时问题也很严重。在“张三”的计算机上,Excel 的路径是:"c:\office\Excel.exe",如果把这个 doc 文件复制到“李四”的计算机上使用,而“李四”的 Excel 的路径是:
"d:\Program files\Microsoft Office\Office\Excel.exe",完蛋了:-(
  于是,微软想出了一个解决方案,那就是不使用直接的路径表示方法,而使用一个叫 CLSID(注1)的方式间接描述这些对象数据的处理程序路径。CLSID 其实就是一个号码,或者说是一个16字节的数。观察注册表(上图),在HKCR\CLSID\{......}主键下,LocalServer32(DLL组件使用InprocServer32) 中保存着程序路径名称。CLSID 的结构定义如下:
typedef struct _GUID {
  
DWORD Data1;      // 随机数
  WORD Data2;        // 和时间相关
  WORD Data3;        // 和时间相关
BYTE Data4[8];           // 和网卡MAC相关
} GUID;

typedef GUID CLSID;    // 组件ID
typedef GUID IID;        // 接口ID
#define REFCLSID const CLSID &

// 常见的声明和赋值方法
CLSID CLSID_Excel = {0x00024500,0x0000,0x0000,{0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}};
struct __declspec(uuid("00024500-0000-0000-C000-000000000046")) CLSID_Excel;
class DECLSPEC_UUID("00024500-0000-0000-C000-000000000046") CLSID_Excel;

// 注册表中的表示方法
{00024500-0000-0000-C000-000000000046}

  用一个号码间接表示程序名,的确是个 Good idea,实现了组件位置的透明性,并方便地扩展出 DCOM(远程组件)。但,但,但,但.....CLSID 有16个字节共128位二进制数,干吗用这么长的数字呀?遥想当年......我还在上幼儿园的时候,人们设计了 socket,用 TCP/IP 协议进行网络通讯。每个参与通讯的计算机都有一个4字节的 IP 表示编号地址,范围是 0,0,0,0 ~ 255,255,255,255 共42亿个地址。可是没想到啊,没想到,自从 Internet 选择了TCP/IP 协议后,42亿个地址就不够全世界的劳动人民分配啦。除了劳动人民,还有冰箱、彩电、电饭锅、手机、手提电脑......这些都需要连网呀。在办公室通过网络开启电饭锅给我焖饭,下班回家后就能吃现成的啦,多幸福呀?!(注:在我们家老婆是领导,所以是我做饭。咳......)
  由于前车之鉴,微软这次设计 CLSID/IID 就使用了GUID概念的16个字节,这下好啦,全世界60亿人口,每个人每秒钟分配10亿个号码,那么需要分配1800亿年。反正等到地球没有了都不会使用完的:-)

三、产生 CLSID
  
1、
如果使用开发环境编写组件程序,则IDE会自动帮你产生 CLSID;
  2、
你可以手工写 CLSID,但千万不要和人家已经生成的 CLSID 重复呀,所以严重地不推荐;(可是微软的CLSID都是手工写的,这叫“只许州官放火,不许百姓点灯”) ;
  3、
程序中,可以用函数 CoCreateGuid() 产生 CLSID;
  4、
使用工具产生 GUID(注2);

  vc6.0版本运行:"vc目录\Common\Tools\GuidGen.exe"程序(你可以参照上回文章中介绍的方法,把这个工具程序加到开发环境中,方便调用)。vc.net版本,在菜单“工具\创建GUID”中,就可以执行了。 

四、ProgID 概念
  每一个COM组件都需要指定一个 CLSID,并且不能重名。它之所以使用16个字节,就是要从概率上保证重复是“不可能”的。但是,(世界上就怕“但是”二字)微软为了使用方便,也支持另一个字符串名称方式,叫 ProgID(注3)。见上图注册表的ProgID 子键内容(注4)。由于 CLSID 和 ProgID 其实是一个概念的两个不同的表示形式,所以我们在程序中可以随便使用任何一种。(有些人就是讨厌,说话不算数。明明 GUID 的目的就是禁止重复,但居然又允许使用 ProgID?!ProgID 是一个字符串的名字,重复的可能性就太大了呀。赶明儿我也写个程序,我打算这个程序的 ProgID 叫“Excel.Application”,嘿嘿)下面介绍一下 CLSID 和 ProgID 之间的转换方法和相关的函数:

函数 功能说明
CLSIDFromProgID()、CLSIDFromProgIDEx() 由 ProgID 得到 CLSID。没什么好说的,你自己都可以写,查注册表贝
ProgIDFromCLSID() 由 CLSID 得到 ProgID,调用者使用完成后要释放 ProgID 的内存(注5)
CoCreateGuid() 随机生成一个 GUID
IsEqualGUID()、IsEqualCLSID()、IsEqualIID() 比较2个ID是否相等
StringFromCLSID()、StringFromGUID2()、StringFromIID() 由 CLSID,IID 得到注册表中CLSID样式的字符串,注意释放内存

五、接口(Interface)的来历
  到此,我们已经知道了 CLSID 或 ProgID 唯一地表示一个组件服务程序,那么根据这些ID,就可以加载运行组件,并为客户端程序提供服务了。(启动组件程序的方法,会陆续介绍)。接下来先讨论如何调用组件提供的函数?-----接口。
  作为客户端程序员,它希望或者说他要求:我的程序只写一次,然后不做任何修改就可以调用任意一个组件。举例来说:

  1. 你可以在 Word 中嵌入 Excel,也可以嵌入 Picture,也可以嵌入任何第三方发表的 ActiveX 文档......也就是说,连 Word 自己都不知道使用它的人将会在 doc 里面插入什么东东;

  2. 你可以在 HTML 文件中插入一个 ActiveX,也可以插入一个程序脚本Script,......你自己写的插件也可以插入到 IE 环境中。为了完成你的功能, 你绝对也不会去让微软修改IE吧?!

  这个要求实在有点难度,Office 开发停滞了。说来话巧,一天老O(Office 项目的总工程师)和小B(VB 项目的总工程师)一起喝酒,老O向小B倾诉了他的烦恼:
老O:怎么能让我写的程序C,可以调用其它人写的程序S中的函数?(C表示客户程序,S表示提供服务的程序)
小B:你是不是喝糊涂了?让S作成 DLL,你去 LoadLibrary()、GetProcAddress()、...FreeLibrary()?!
老O:废话!要是这么简单就好了。问题是,连我都不知道这个S程序是干什么的?能干什么?我怎么调用呀?
小B:哦......这个比较高级,但我现在不能告诉你,因为我怕你印象不深。
老O:~!·#¥%……—*......
小B:是这样的,在VB中,我们制定了一个标准,这个标准允许任何一个VB开发者,把他自己写的某个功能的小程序放在VB的工具栏上,这样就好象他扩展了 VB 的功能一样。
老O:哦?就是那个叫什么 VBX 的滥玩意儿?
小B:我呸......别看 VBX 这个东西不起眼儿,的确我也没看上它。但你猜怎么着?现在有成千上万的 VB 程序爱好者把他们写的各式各样功能的 VBX 小程序,放到网上,让大家共享那。
老O:哦~~~,那你们的这个 VBX 标准是什么?
小B:嘿嘿......其实特简单,就是在 VBX 中必须实现7个函数,这7个函数名称和功能必须是:初始化、释放、显示、消息处理......,而至于它内部想干什么,我也管不着。我只是在需要的时候调用我需要的这7个函数。
老O:哦~~~,这样呀......对了,我现有个急事,我先走了。88,你付帐吧......
小B:喂!喂喂...... 走这么急干什么,钱包都掉了:-)
  老O虽然丢了钱包,仍然兴奋地冲回办公室,他开始了思考......

1、我的程序C,要能调用任何人写的程序B。那么B必须要按照我事先的要求,提供我需要的函数F1(),F2(),F3(),K1(),K2()。
2、BASIC 是解释执行,因此它的函数不用考虑书写顺序,只要给出函数名,解释器就能找到。但我使用的是 C++呀......
3、C++编译后的代码中没有函数名,只有函数地址,因此我必须改进为用VTAB(虚函数表)表示函数入口:


图二、VTAB 的结构

4、还不够好,需要改进一下,因为所有的函数地址都放在一个表中会不灵活、不好修改、不易扩展。恩,有了!按照函数功能的类型进行分类:


图三、多个 VTAB 的结构

5、问题又来了,现在有2个 VTAB 虚函数表,那么怎么能够从一个表找到另一个表那?恩又有办法了,我要求你必须要实现一个函数,并且这个函数地址必须放在所有表的开头(表中的第一个函数指针),这个函数就叫 QueryInterface()吧,完成从一个表查找到另一个表的功能:(除了QueryInterface()函数,顺便也完成另外两个函数,叫 AddRef() 和 Release()。这两个函数的功能以后再说)


图四、COM 接口结构

6、为了以后描述方便,不再使用上图(图四)的方法了,而使用图五这样简洁的样式:


图五、COM 接口结构的简洁图示


六、接口(Interface)概念
1、函数是通过 VTAB 虚函数表提供其地址, 从另一个角度来看,不管用什么语言开发,编译器产生的代码都能生成这个表。这样就实现了组件的“二进制特性”轻松实现了组件的跨语言要求。

2、假设有一个指针型变量保存着 VTAB 的首地址,则这个变量就叫“接口指针”(注6), 变量命名的时候,习惯上加上"I"开头。另外为了区分不同的接口,每个接口 也都要有一个名字,该名字就和 CLSID 一样,使用 GUID 方式,叫 IID。
3、接口一经发表,就不能再修改了。不然就会出现向前兼容的问题。这个性质叫“接口不变性”。
4、组件中必须有3个函数,QueryInterface、AddRef、Release,它们3个函数也组成一个接口,叫"IUnknown"。(注7)
5、任何接口,其实都包含了 IUnknown 接口。随着你接触到更多的接口就会了更体会解到接口的另一个性质“继承性”。
6、在任何接口上,调用表中的第一个函数,其实就是调用 QueryInterface()函数,就得到你想要的另外一个接口指针。这个性质叫“接口的传递性”
7、C/C++语言中需要事先对函数声明,那么就 会要求组件也必须提供C语言的头文件。不行!为了能使COM具有跨语言的能力,决定不再为任何语言提供对应的函数接口声明,而是独立地提供一个叫类型库(TLB)的声明。每个语言的IDE环境自己去根据TLB生成自己语言需要的包装。这个性质叫“接口声明的独立性”(注8)

七、客户程序与组件之间的协商调用
  
回到我们的上一个话题,Word中嵌入一个组件,那么Word是如何协商使用这个组件的那?下面是容器和组件之间的一个模拟对话过程:

  容器 协商部分 组件 应答部分
1 根据CLSID启动组件 。
CoCreateInstance()
生成对象,执行构造函数,执行初始化动作。
2 你有IUnknown接口吗? 有,给你!
3 恩,太好了,那么你有IPersistStorage接口吗?(注9)
IUnknown::QueryInterface(IID_IPersistStorage...)
没有!
4 真差劲,连这个都没有。那你有IPersistStreamInit接口吗?(注10)
IUnknown::QueryInterface(IID_IPersistStreamInit...)
哈,这个有,给!
5 好,好,这还差不多。你现在给我初始化吧。
IPersistStreamInit::InitNew()
OK,初始化完成了。
6 完成了?好!现在你读数据去吧。
IPersistStreamInit::Load()
读完啦。我根据数据,已经在窗口中显示出来了。
7 好,现在咱们各自处理用户的鼠标、键盘消息吧...... ......
8 哎呀!用户要保存退出程序了。你的数据被用户修改了吗?
IPersistStreamInit::IsDirty()
改了,用户已经修改啦。
9 那好,那么用户修改后,你的数据需要多大的存储空间呀?
IPersistStreamInit::GetSizeMax()
恩,我算算呀......好了,总共需要500KB。
10 晕,你这么个小玩意居然占用这么大空间?!......好了,你可以存了。
IPersistStreamInit::Save()
谢谢,我已经存好了。
11 恩。拜拜了您那。(注11)
IPersistStreamInit::Release();IUnknown::Release()
执行析构函数,删除对象。
12 我自己也该退出了......
PostQuitMessage()
 

  容器(或者说客户端)就是这样和组件进行对话,协商调用的。如果组件甲实现了 IA 接口,那么容器就会使用它,如果组件乙没有提供 IA 接口,但是它提供了 IB 接口,那么容器就会调用 IB 接口的函数......如此,容器程序根本就不需要知道组件到底是干什么的,组件到底是用什么语言开发的,组件的磁盘位置到底在哪里,它都可以正常运行。太奇妙了!太精彩了!怎一个“爽”字了得!

八、小结
  第二回中,介绍了两个非常重要的概念:CLSID 和 Interface。由于全篇都是概念描述而没有示例程序相配合,可能读者的理解还不太深入、不彻底。别着急,我们马上就要进入到组件程序设计阶段了,到那个时候,你根据具体的程序代码,再回过头来再次阅读本回文章,没读懂?哦......再读!慢慢地您老人家就懂了:-)

留作业啦......
1、IDispatch 接口的 IID 是多少?(哎~~~ 笨笨,在源程序中,用鼠标右键执行Go to definition 呀)
2、IPicture 接口有几个函数?功能是什么?(别玩了!你多大了?想不想在程序中显示 JPG 图像呀,看 MSDN 去)
  想知道为什么COM函数总是返回 HRESULT 吗?想知道如何使用 BSTR、VARIANT 吗?想知道 COM 中应该如何使用内存吗?想知道如何使用 UNICODE 吗?......恩~~~,我现在不能告诉你,我现在告诉你,怕你印象不深!且听下回分解......


注1:CLSID = Class ID 上回书已经介绍了把CLSID写入复合文件的函数:WriteClassStg()、IStorage::SetClass()。
注2:GUID 全局唯一标示符,CLSID/IID 其实是借用了GUID的概念。
注3:ProgID = Program ID,等价于 CLSID, 是用字符串表示的。
注4:注册表子键 ProgID 和 VersionIndependentProgID 分别表示真正的 ProgID 和版本无关的 ProgID。比如在我计算机上安装的 Excel,它的 ProgID = "Excel.Application.9",而 VersionIndependentProgID = "Excel.Application"。
注5:COM 组件的内存管理,见后续的文章。
注6:Interface = 接口,以前微软不叫它接口,而叫协议Protocol。其实我 到认为这个词更贴切一些。
注7:IUnknown 这个名字起的好,居然叫“我不知道”:-),它的 IID 叫 IID_IUnknown,如果用注册表样式表示,那么它的值是{00000000-0000-0000-C000-000000000046}。
注8:TLB是由一个描述接口的文件 IDL 经过编译产生的。IDL 的说明,见后续的文章吧。
注9:IPersistStorage 是用复合文件的存储(Storage)功能来保存/读取数据用的一个接口。
注10:IPersistStreamInit 是用复合文件的流(Stream)功能来保存/读取数据用的一个接口。
注11:拜拜了您那 = 英语北京话,再见。

posted @ 2011-08-20 20:46 Mr-Victor 阅读(94) 评论(0) 编辑

本文摘自:http://blog.vckbase.com/teacheryang/archive/2005/06/27/8883.html


一、前言

  公元一九九五年某个夜黑风高的晚上,我的一位老师跟我说:“小杨呀,以后写程序就和搭积木一样啦。你赶快学习一些OLE的技术吧......”,当时我心里就寻思 :“开什么玩笑?搭积木方式写程序?再过100年吧......”,但作为一名听话的好学生,我开始在书店里“踅摸”(注1)有关OLE的书籍(注2)。功夫不负有心人,终于买到了我的第一本COM书《OLE2 高级编程技术》,这本800多页的大布头花费了我1/5的月工资呀......于是开始日夜耕读.....
功夫不负有心人,我坚持读完了全部著作,感想是:这本书,在说什么呐?
功夫不负有心人,我又读完了一遍大布头,感想是:咳~~~,没懂!
功夫不负有心人,我再,我再,我再读 ... 感想是:哦~~~,读懂了一点点啦,哈哈哈。
...... ......
功夫不负有心人,我终于,我终于懂了。
800页的书对现在的我来说,其实也就10几页有用。到这时候才体会出什么叫“书越读越薄”的道理了。到后来,能买到的书也多了,上网也更方便更便宜了......
  为了让VCKBASE上的朋友,不再经历我曾经的痛苦、不再重蹈我“无头苍蝇”般探索的艰辛、为了VCKBASE的蓬勃发展、为了中国软件事业的腾飞(糟糕,吹的太也高了)......我打算节约一些在 BBS 上赚分的时间,写个系列论文,就叫“COM组件设计与应用”吧。今天是第一部分——起源。

二、文件的存储
  传说350年前,牛顿被苹果砸到了头,于是发现了万有引力。但到了二十一世纪的现在,任何一个技术的发明和发展,已经不再依靠圣人灵光的一闪。技术的进步转而是被社会的需求、商业的利益、竞争的压力、行业的渗透等推动的。微软在Windows平台上的组件技术也不例外,它的发明,有其必然因素。什么是这个因素那?答案是——文件的存储。
  打开记事本程序,输入了一篇文章后,保存。——这样的文件叫“非结构化文件”;
  打开电子表格程序,输入一个班的学生姓名和考试成绩,保存。——这样的文件叫“标准结构化文件”;
  在我们写的程序中,需要把特定的数据按照一定的结构和顺序写到文件中保存。——这样的文件叫“自定义结构化文件”;(比如 *.bmp 文件)
  以上三种类型的文件,大家都见的多了。那么文件存储就依靠上述的方式能满足所有的应用需求吗?恩~~~,至少从计算机发明后的50多年来,一直是够用的了。嘿嘿,下面看看商业利益的推动作用,对文件 的存储形式产生了什么变化吧。30岁以上的朋友,我估计以前都使用过以下几个著名的软件:WordStar(独霸DOS下的英文编辑软件),WPS(裘伯君写的中文编辑软件,据说当年的市场占有率高达90%,各种计算机培训班的必修课程),LOTUS-123(莲花公司出品的电子表格软件)......
微软在成功地推出 Windows 3.1 后,开始垂涎桌面办公自动化软件领域。微软的 OFFICE 开发部门,各小组分别独立地开发了 WORD 和 EXCEL 等软件,并采用“自定义结构”方式,对文件进行存储。在激烈的市场竞争下,为了打败竞争对手,微软自然地产生了一个念头------如果我能在 WORD 程序中嵌入 EXCEL,那么用户在购买了我 WORD 软件的情况下,不就没有必要再买 LOTUS-123 了吗?!“恶毒”(中国微软的同志们看到了这个词,不要激动,我是加了引号的呀)的计划产生后,他们开始了实施工作,这就是 COM 的前身 OLE 的起源(注3)。但立刻就遇到了一个严重的技术问题:需要把 WORD 产生的 DOC 文件和 EXCEL 产生的 XLS 文件保存在一起。

方案

优点

缺点

建立一个子目录,把 DOC、XLS 存储在这同一个子目录中。 数据隔离性好,WORD 不用了解 EXCEL 的存储结构;容易扩展。 结构太松散,容易造成数据的损坏或丢失。
不易携带。
修改文件存储结构,在DOC结构基础上扩展出包容 XLS 的结构。 结构紧密,容易携带和统一管理。 WORD 的开发人员需要通晓 EXCEL 的存储格式;缺少扩展性,总不能新加一个类型就扩展一下结构吧?!

  以上两个方案,都有严重的缺陷,怎么解决那?如果能有一个新方案,能够合并前两个方案的优点,消灭缺点,该多好呀......微软是作磁盘操作系统起家的,于是很自然地他们提出了一个非常完美的设计方案,那就是把磁盘文件的管理方式移植到文件中了------复合文件,俗称“文件中的文件系统”。连微软当年都没有想到,就这么一个简单的想法,居然最后就演变出了 COM 组件程序设计的方法。可以说,复合文件是 COM 的基石。下图是磁盘文件组织方式与复合文件组织方式的类比图:


图一、左侧表示一个磁盘下的文件组织方式,右侧表示一个复合文件内部的数据组织方式。

三、复合文件的特点
  1、
复合文件的内部是使用指针构造的一棵树进行管理的。编写程序的时候要注意,由于使用的是单向指针,因此当做定位操作的时候,向后定位比向前定位要快;
  2、
复合文件中的“流对象”,是真正保存数据的空间。它的存储单位为512字节。也就是说,即使你在流中只保存了一个字节的数据,它也要占据512字节的文件空间。啊~~~,这也太浪费了呀?不浪费!因为文件保存在磁盘上,即使一个字节也还要占用一个“簇”的空间那;
  3、
不同的进程,或同一个进程的不同线程可以同时访问一个复合文件的不同部分而互不干扰;
  4、
大家都有这样的体会,当需要往一个文件中插入一个字节的话,需要对整个文件进行操作,非常烦琐并且效率低下。而复合文件则提供了非常方便的“增量访问”能力;
  5、当频繁地删除文件,复制文件后,磁盘空间会变的很零碎,需要使用磁盘整理工具进行重新整合。和磁盘管理非常相似,复合文件也会产生这个问题,在适当的时候也需要整理,但比较简单,只要调用一个函数就可以完成了。

四、浏览复合文件
  VC6.0 附带了一个工具软件“复合文件浏览器”,文件名是“vc目录\Common\Tools\DFView.exe”。为了方便使用该程序,可以把它加到工具(tools)菜单中。方法是:Tools\Customize...\Tools卡片中增加新的项目。运行 DFView.exe,就可以打开一个复合文件进行观察了(注4)。但奇怪的是,在 Microsoft Visual Studio .NET 2003 中,我反而找不到这个工具程序了,汗!不过这恰好提供给大家一个练习的机会,在你阅读完本篇文章并掌握了编程方法后,自己写一个“复合文件浏览编辑器”程序,又练手了,还有实用的价值。

、复合文件函数
  复合文件的函数和磁盘目录文件的操作非常类似。所有这些函数,被分为3种类型:WIN API 全局函数,存储 IStorage 接口函数,流 IStream 接口函数。什么是接口?什么是接口函数?以后的文章中再陆续介绍,这里大家只要把“接口”看成是完成一组相关操作功能的函数集合就可以了。

WIN API 函数

功能说明

StgCreateDocfile() 建立一个复合文件,得到根存储对象
StgOpenStorage() 打开一个复合文件,得到根存储对象
StgIsStorageFile() 判断一个文件是否是复合文件

 

IStorage 函数

功能说明

CreateStorage() 在当前存储中建立新存储,得到子存储对象
CreateStream() 在当前存储中建立新流,得到流对象
OpenStorage() 打开子存储,得到子存储对象
OpenStream() 打开流,得到流对象
CopyTo() 复制存储下的所有对象到目标存储中,该函数可以实现“整理文件,释放碎片空间”的功能
MoveElementTo() 移动对象到目标存储中
DestoryElement() 删除对象
RenameElement() 重命名对象
EnumElements() 枚举当前存储中所有的对象
SetElementTimes() 修改对象的时间
SetClass() 在当前存储中建立一个特殊的流对象,用来保存CLSID(注5)
Stat() 取得当前存储中的系统信息
Release() 关闭存储对象
 

IStream 函数

功能说明

Read() 从流中读取数据
Write() 向流中写入数据
Seek() 定位读写位置
SetSize() 设置流尺寸。如果预先知道大小,那么先调用这个函数,可以提高性能
CopyTo() 复制流数据到另一个流对象中
Stat() 取得当前流中的系统信息
Clone() 克隆一个流对象,方便程序中的不同模块操作同一个流对象
Release() 关闭流对象
 
WIN API 补充函数 功能说明
WriteClassStg() 写CLSID到存储中,同IStorage::SetClass()
ReadClassStg() 读出WriteClassStg()写入的CLSID,相当于简化调用IStorage::Stat()
WriteClassStm() 写CLSID到流的开始位置
ReadClassStm() 读出WriteClassStm()写入的CLSID
WriteFmtUserTypeStg() 写入用户指定的剪贴板格式和名称到存储中
ReadFmtUserTypeStg() 读出WriteFmtUserTypeStg()写入的信息。方便应用程序快速判断是否是它需要的格式数据。
CreateStreamOnHGlobal() 内存句柄 HGLOBAL 转换为流对象
GetHGlobalFromStream() 取得CreateStreamOnHGlobal()调用中使用的内存句柄

  为了让大家快速地浏览和掌握基本方法,上面所列表的函数并不是全部,我省略了“事务”函数和未实现函数部分。更全面的介绍,请阅读 MSDN。
下面程序片段,演示了一些基本函数功能和调用方法。
示例一:建立一个复合文件,并在其下建立一个子存储,在该子存储中再建立一个流,写入数据。
void SampleCreateDoc()
{
  ::CoInitialize(NULL);   // COM 初始化
                // 如果是MFC程序,可以使用AfxOleInit()替代
  
HRESULT hr;   // 函数执行返回值
  IStorage *pStg = NULL;    // 根存储接口指针
  IStorage *pSub = NULL;   // 子存储接口指针
  IStream *pStm = NULL;   // 流接口指针

  
hr = ::StgCreateDocfile(  // 建立复合文件
        L"c:\\a.stg",    // 文件名称
        STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,    // 打开方式
        0,   // 保留参数
        &pStg);  // 取得根存储接口指针
  ASSERT( SUCCEEDED(hr) );   // 为了突出重点,简化程序结构,所以使用了断言。
                    // 在实际的程序中则要使用条件判断和异常处理

  
hr = pStg->CreateStorage(    // 建立子存储
        L"SubStg",    // 子存储名称
        STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,
        0,0,
        &pSub);     // 取得子存储接口指针
  ASSERT( SUCCEEDED(hr) );

  hr = pSub->CreateStream(  // 建立流
        L"Stm",    // 流名称
        STGM_CREATE | STGM_WRITE | STGM_SHARE_EXCLUSIVE,
        0,0,
        &pStm);  // 取得流接口指针
  ASSERT( SUCCEEDED(hr) );

  hr = pStm->Write(    // 向流中写入数据
        "Hello",   // 数据地址
        5,   // 字节长度(注意,没有写入字符串结尾的\0)
        NULL);   // 不需要得到实际写入的字节长度
  ASSERT( SUCCEEDED(hr) );

  if( pStm ) pStm->Release();   // 释放流指针
  if( pSub ) pSub->Release();   // 释放子存储指针
  if( pStg ) pStg->Release();    // 释放根存储指针

  ::CoUninitialize()   // COM 释放
             // 如果使用 AfxOleInit(),则不调用该函数
}


图二、运行示例程序一后,使用 DFView.exe 打开观察复合文件的效果图

示例二:打开一个复合文件,枚举其根存储下的所有对象。
#include  // ANSI、MBCS、UNICODE 转换

void SampleEnum()

  
// 假设你已经做过 COM 初始化了
  LPCTSTR lpFileName = _T( "c:\\a.stg" );
  HRESULT hr;
  IStorage *pStg = NULL;
 
  USES_CONVERSION;    // (注6)
  LPCOLESTR lpwFileName = T2COLE( lpFileName );    // 转换T类型为宽字符
  hr = ::StgIsStorageFile( lpwFileName );    // 是复合文件吗?
  if( FAILED(hr) )
    return;

  hr = ::StgOpenStorage(   // 打开复合文件
        lpwFileName,   // 文件名称
        NULL,
        STGM_READ | STGM_SHARE_DENY_WRITE,
        0,
        0,
        &pStg);    // 得到根存储接口指针

  IEnumSTATSTG *pEnum=NULL;    // 枚举器
  hr = pStg->EnumElements( 0, NULL, 0, &pEnum );
  ASSERT( SUCCEEDED(hr) );

  STATSTG statstg;
  while( NOERROR == pEnum->Next( 1, &statstg, NULL) )
  {
    // statstg.type 保存着对象类型 STGTY_STREAM 或 STGTY_STORAGE
    // statstg.pwcsName 保存着对象名称
    // ...... 还有时间,长度等很多信息。请查看 MSDN

    ::CoTaskMemFree( statstg.pwcsName );    // 释放名称所使用的内存(注6)
  }
 
  if( pEnum ) pEnum->Release();
  if( pStg ) pStg->Release();
}

六、小结
  复合文件,结构化存储,是微软组件思想的起源,在此基础上继续发展出了持续性、命名、ActiveX、对象嵌入、现场激活......一系列的新技术、新概念。因此理解掌握 复合文件是非常重要的,即使在你的程序中并没有全面使用组件技术,复合文件技术也是可以单独被应用的。祝大家学习快乐,为社会主义软件事业而奋斗:-)

留作业啦......
作业1:写个小应用程序,从 MSWORD 的 doc 文件中,提取出附加信息(作者、公司......)。
作业2:写个全功能的“复合文件浏览编辑器”。

注1:踅摸(xuemo),动词,北方方言,寻找搜索的意思。
注2:问:为什么不上网查资料学习?
答:开什么国际玩笑!在那遥远的1995年代,我的500块工资,不吃不喝正好够上100小时的Internet网。
注3:OLE,对象的连接与嵌入。
注4:可以用 DFView.exe 打开 MSWORD 的 DOC 文件进行复合文件的浏览。但是该程序并没有实现国际化,不能打开中文文件名的复合文件,因此需要改名后才能浏览。
注5:CLSID,在后续的文章中介绍。
注6:关于 COM 中内存使用的问题,在后续的文章中介绍。

posted @ 2011-08-20 19:55 Mr-Victor 阅读(109) 评论(0) 编辑

使用ODBC所需要的文件
1、sql.h : 包含基本的ODBC API的定义。
2、sqlext.h :包含扩展的ODBC的定义。
3、odbc32.lib :库文件。
例如:
#include <sqlext.h>
#include <sql.h>
#include <odbcinst.h>
#pragma comment(lib, "odbccp32.lib")
#pragma comment(lib, "odbc32.lib")

一、配置ODBC数据源
配置ODBC数据源可以通过手动配置 和 程序自动配置 两种方式来实现。
第一种:手动配置(不作说明)
第二种:程序自动配置

SQLRETURN retcode;
retcode = SQLConfigDataSource(NULL,ODBC_ADD_DSN,"Microsoft Access Driver (*.mdb, *.accdb\0","DSN=Victor\0DBQ=D:\\MyDataBase\\Victor.accdb\0DEFAULTDIR=D:\\MyDataBase\0");
if(!retcode)
{
  AfxMessageBox("配置ODBC数据源失败!");
  return;
}

ODBC API提供了动态创建数据源的函数SQLConfig DataSource。该函数的原型如下:  
  BOOL SQLConfigDataSource(HWND hwndParent, WORD fRequest, LPCSTR lpszDriver, LPCSTR lpszAttributes);
参数说明如下:  
1、参数hwndParent用于指定父窗口句柄,在不需要创建数据源对话框时,可以将该参数指定为NULL。 
2、参数fRequest用于指定函数的操作内容,取值如下:  
  ODBC_ADD_DSN: 加入一个新的用户数据源;  
  ODBC_CONFIG_DSN:修改一个存在的用户数据源;  
  ODBC_REMOVE_DSN:除一个存在的用户数据源;  
  ODBC_ADD_SYS_DSN:增加一个新的系统数据源;  
  ODBC_CONFIG_SYS_DSN:配置或者修改一个存在的系统数据源;  
  ODBC_REMOVE_SYS_DSN:删除一个存在的系统数据源;  
  ODBC_REMOVE_DEFAULT_DSN:删除省缺的数据源说明部分。
3、参数lpszDriver用于指定ODBC数据源的驱动  
例如,为了指定Access数据源,该参数应赋以字符串"Microsoft Access Driver (*.mdb, *.accdb)\0";对SQL SERVER数据源,则应赋以字符串"SQL Server"。  
4、参数lpszAttributes用于指定ODBC数据源属性。例如:
 4.1 对Access数据源:  
  "DSN=Victor\0DBQ=D:\\MyDataBase\\Victor.accdb\0DEFAULTDIR=D:\\MyDataBase\0"  
  说明:该字符串指定数据源名称(DNS)为Victor;数据库文件(DBQ)为D:\\MyDataBase\\Victor.accdb;缺省数据库文件路径(DEFAULTDIR)为D:\\MyDataBase。  
4.2 对SQL SERVER数据源: 
  "DSN=Victor\0SERVER=MYET\0 DATABASE=Image"  
  说明:该字符串指定数据源名称(DSN)为MYIMAGE;SQLSERVER 数据库服务器名(SERVER)为MYET;数据库名称(DATABASE)为Image。

二、分配ODBC环境并设置ODBC环境属性
对于任何ODBC应用程序来说,第一步的工作是装载驱动程序管理器,然后初始化ODBC环境,分配环境句柄。ODBC驱动程序管理器使用该环境句柄跟踪每一个ODBC连接及其状态。
//分配环境句柄
SQLHENV henv;
retcode = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv);
if(!(retcode == SQL_SUCCESS || retcode == SQL_SUCCESS_WITH_INFO))
{
  AfxMessageBox("分配环境句柄失败!");
  return;
}
执行该调用语句后,驱动程序分配一个结构,该结构中存放环境信息,然后返回对应于该环境的环境句柄。

然后ODBC应用程序必须调用SQLSetEnvAttr函数,这个函数注册将要使用的ODBC API版本。
//设置ODBC环境属性
retcode = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3,0);
if(!(retcode == SQL_SUCCESS || retcode == SQL_SUCCESS_WITH_INFO))
{
   SQLFreeHandle(SQL_HANDLE_ENV, henv);
   AfxMessageBox("设置ODBC环境属性失败!");
   return;
}

三、分配连接句柄
分配环境句柄后,在建立至数据源的连接之前,必须分配一个连接句柄,每一个到数据源的连接对应于一个连接句柄。
//分配连接句柄
SQLHDBC hdbc;
retcode = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
if(!(retcode == SQL_SUCCESS || retcode == SQL_SUCCESS_WITH_INFO))
{
   SQLFreeHandle(SQL_HANDLE_ENV, henv);
   AfxMessageBox("分配连接句柄失败!");
   return;
}

四、连接数据源
当连接句柄分配完成后,可以设置连接属性,所有的连接属性都有缺省值,但是可以通过调用函数SQLSetConnectAttr()来设置连接属性。用函数SQLGetConnectAttr(0获取这些连接属性。
//设置最长登录时间为5s
SQLSetConnectAttr(hdbc, SQL_LOGIN_TIMEOUT, (SQLPOINTER)5, 0);

完成对属性的设置之后,就可以建立到数据源的连接了。对于不同的程序和用户接口,可以用不同的函数建立连接:SQLConnect、SQLDriverConnect、SQLBrowseConnect.
SQLConnect()提供了最为直接的程序控制方式,只要提供数据源名称、用户ID和口令就可以进行连接了。
//连接到数据源
retcode = SQLConnect(hdbc, (SQLCHAR*)"Victor", SQL_NTS, NULL , 0, NULL, 0);
if(!(retcode == SQL_SUCCESS || retcode == SQL_SUCCESS_WITH_INFO))
{
   SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
   SQLFreeHandle(SQL_HANDLE_ENV, henv);
   AfxMessageBox("连接数据源失败!");
   return;
}

五、分配语句句柄
最后一段ODBC初始化代码是调用SQLAllocHandle函数创建一个语句句柄。该语句句柄用于处理SQL请求。
//分配语句句柄
SQLHSTMT hstmt;
retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
if(!(retcode == SQL_SUCCESS || retcode == SQL_SUCCESS_WITH_INFO))
{
   SQLDisconnect(hdbc);
   SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
   SQLFreeHandle(SQL_HANDLE_ENV, henv);
   AfxMessageBox("分配语句句柄失败!");
   return;
}

posted @ 2011-08-20 18:24 Mr-Victor 阅读(189) 评论(0) 编辑