[翻译] 让Windows 2000/XP中的任意窗口透明起来

Windows 2000/XP中的任意窗口透明起来

简介

  已经有很多的文章展示了如何通过使用新的系统函数在Windows 2000或Windows XP中建立透明窗口的应用程序,本文在此基础上为您展现了一种可以让任意应用程序窗口透明起来的方法,哪怕您根本没有那个应用程序的源程序。

  使用本文介绍的“WinTrans”程序,您只需把程序中的“魔棒”(程序左上角的那个图标)拖曳到另一个正在运行的程序的标题栏上就可以使它的窗口变得透明。您还可以通过拖到滑动杆来调节透明度。“WinTrans”的界面很像SPY程序的界面,它演示了如何使用Win32 API函数来捕获一个位于鼠标光标下的窗口并获取它的窗口类、窗口标题等信息。

  当您需要在一个最大化的窗口中工作而同时又需要查看另外一个在后台运行的程序的状态时,您会发现“WinTrans”程序是一个很实用的程序。

背景

  在Windows 2000和Windows XP中,User32.dll中添加了一个新的函数,名字是SetLayeredWindowAttributes。如果要在应用程序中使用这个函数,需要在创建窗口时为窗口的window style设置WS_EX_LAYERED(0x00080000)位,也可以在创建窗口后用SetWindowLong函数添加这个位。一旦这个标志位被设定了,我们就可以通过调用SetLayeredWindowsAttributes函数,并把窗口的句柄传给它来使得窗口或窗口上特定的颜色变得透明。这个函数的参数如下:

  • HWND hWnd: 窗口的名柄
  • COLORREF col: 希望变透明的颜色
  • BYTE bAlpha: 如果这个值设为0,窗口会变得完全透明。如果设为255,窗口会变得完全不透明。
  • DWORD dwFlags: 如果这个标志设为1,只有col指定的颜色会变得透明。如果这个标志设为2,则整个窗口会按bAlpha指定的程度变得透明。

代码解释

  首先,在WinTransDlg.h中的主对话框类中加下如下的成员变量。

bool m_bTracking;   //  当鼠标被捕捉时会置为true

HWND m_hCurrWnd;    //  鼠标最后一次指向的窗口的句柄

HCURSOR m_hCursor;  //  魔棒光标

  我们还定义了一个指向SetLayeredWindowAttributes函数的指针。这个函数位于User32.dll中。

//  全局定义

typedef BOOL (WINAPI *lpfn) (HWND hWnd, COLORREF cr,

              BYTE bAlpha, DWORD dwFlags);

lpfn g_pSetLayeredWindowAttributes;

  在OnInitDialog消息响应函数中,我们获取了SetLayeredWindowAttributes函数的地址并把它保存在g_pSetLayeredWindowAttributes变量中。同时,我们还加载了魔棒光标并把它的句柄放在m_hCursor中

BOOL CWinTransDlg::OnInitDialog()

{

    ….

    //  获取User32.dllSetLayeredWindowAttributes 函数的地址

    HMODULE hUser32 = GetModuleHandle(_T(“USER32.DLL”));

    g_pSetLayeredWindowAttributes = (lpfn)GetProcAddress(hUser32,

               “SetLayeredWindowAttributes”);

    if (g_pSetLayeredWindowAttributes == NULL)

        AfxMessageBox (

            “Layering is not supported in this version of Windows”,

             MB_ICONEXCLAMATION);

    //  加载魔棒光标

    HINSTANCE hInstResource = AfxFindResourceHandle(

         MAKEINTRESOURCE(IDC_WAND), RT_GROUP_CURSOR);

    m_hCursor = ::LoadCursor( hInstResource, MAKEINTRESOURCE(IDC_WAND) );

    …

}

  下面定义WM_LBUTTONDOWN, WM_LBUTTONUP和WM_MOUSEMOVE消息的响应函数。WM_LBUTTONDOWN的响应函数如下:

void CWinTransDlg::OnLButtonDown(UINT nFlags, CPoint point)

{

    …

    SetCapture();      // 使鼠标消息发回本窗口

    m_hCurrWnd = NULL; // 当前没有窗口被设成透明的

    m_bTracking = true;     // 设置鼠标被捕获的标志

    ::SetCursor(m_hCursor); // 把鼠标光标变成魔棒光标

    …

}

  对于处理鼠标移动消息的响应代码如下

void CWinTransDlg::OnMouseMove(UINT nFlags, CPoint point)

{

    …

    if (m_bTracking)

    {

        …

        //  把鼠标坐标转换为屏幕坐标

        ClientToScreen(&point);

        …

        //  获取鼠标位置的光标

        m_hCurrWnd = ::WindowFromPoint(point);

        …

        // 显示窗口的详细情况,包括窗口类、标题等

        …

    }

    …

}

  这样就使得只要在主窗口中按下鼠标按钮不松开就可以使鼠标光标变为魔棒光标,并且光标下的窗口的一些信息就会显示在WinTrans程序的对话框中。

  当鼠标按钮松开时,WM_LBUTTONUP消息的响应函数就会被调用。

void CWinTransDlg::OnLButtonUp(UINT nFlags, CPoint point)

{

    …

    //  停止捕获鼠标

    ReleaseCapture();

    m_bTracking = false;

    //  如果鼠标光标所指的窗口不是本应用程序本身

    //  就设置它的Layer样式位并按滑动杆的设置值来设置窗口的Alpha

    if (g_pSetLayeredWindowAttributes && m_hCurrWnd != m_hWnd)

    {

        ::SetWindowLong(m_hCurrWnd, GWL_EXSTYLE,

                        GetWindowLong(m_hCurrWnd,

                        GWL_EXSTYLE) ^ WS_EX_LAYERED);

        g_pSetLayeredWindowAttributes(m_hCurrWnd, 0,

                        (BYTE)m_slider.GetPos(), LWA_ALPHA);

        ::RedrawWindow(m_hCurrWnd, NULL, NULL,

                       RDW_ERASE | RDW_INVALIDATE |

                       RDW_FRAME | RDW_ALLCHILDREN);

    }

    …

}

其它值得关注的地方

  当前这个程序只有在魔棒指向窗口的标题栏或一个基于对话框的程序窗口体上才能有效的工作。举例来说:如果你把魔棒指向记事本的窗口体是没有用的。

  如果要去掉窗口透明的效果,您只需再次把魔棒拖曳到相应的窗口上即可。因为在OnLButtonUp中,我们是切换WS_EX_LAYERED标志位的设置状态的,透明效果也会进行相应的切换。

  TransWand程序不能在命令提示行窗口上正确的工作。

本文相关源程序下载:

原始下载地址 (12K)(要在该网站注册后方可下载)

国内镜象 (12K)

Delphi中两个BUG的分析与修复

Delphi中两个BUG的分析与修复

  在使用Delphi 7进行三层数据库开发时,遇到了两个小问题,通过反复试验,终于找出了Delphi 7中的两个小BUG并进行了修复(好像Delphi 6中也有相同的BUG),撰写此文与大家一起分享成功的喜悦。我也是初学Delphi,文中一定存在不少说的不对的地方,还请各位朋友多多指正。

  BUG1.传参时中文被截断的问题:

  BUG再现的方法:

  后台用SQL Server 2000,里面有一个XsHeTong表用于试验,您可以根据您的实际情况进行调整。

  先创建一个数据服务器:新建项目,创建一个远程数据模块,上面放置 ADOConnection、ADODataSet、DataSetProvider各一,并做好相应设置,其中ADODataSet的 ComamndText留空,并把它的Option中的poAllowCommandText设置为True。编译运行。

  再创建客户端程序:新建项目,在窗体上放置DCOMConnection, 连上前面上创建的数据服务器,再放置一个ClientDataSet,把它的连接设成这里的DCOMConnection,并设置它的 ProviderName为上面的服务器上的DataSetProvider的名字。最后放置DataSource和DBGrid各一并作相应设置用于查 看结果,再放置一Button用于测试。

  在Button的OnClick中写下类似于下面的代码(这里我用了XsHeTong的表和它的两个字段HTH(char 15)、GCMC(varchar 100),您可以根据你的实际测试情况进行调整):

  with ClientDataSet1 do
begin
Close;
CommandText := ‘Insert Into XsHeTong(HTH, GCMC) values(:HTH,:GCMC)’;
Params[0].AsString := ‘12345’;
Params[1].AsString := ‘会截断的中文字’;
Execute;
Close;
CommandText := ‘Select * from XsHeTong’;
Open;
end;

  运行程序,点击按钮,看到记录被插入了,可惜结果并不正确,“会截断的中文字”变成了“会截断”,但没有中文的“12345”倒是正确的插入了。

  BUG分析与修复:

  为了对照起见,我试着直接用一个ADOConnection和ADOCommand、ADOTable进行C/S构架测试,结果是正确的,中文字不会被切断。这说明了此BUG只在三层构架上出现。

   用SQL Server事件探查器探查提交到SQL Server上运行的语句,发现两层构架与三层构架的情况有以下不同:

  两层构架:
exec sp_executesql N’Insert into XsHeTong(HTH, GCMC) values(@P1,@P2)’, N’@P1 varchar(15),@P2 varchar(100)’, ‘12345’, ‘会截断的中文字’

  三层构架:
  exec sp_executesql N’Insert into XsHeTong(HTH, GCMC) values(@P1,@P2)’, N’@P1 varchar(5),@P2 varchar(7)’, ‘12345’, ‘会截断

  显然,两层构架时,参数的长度是按实际库结构传的,三层构架时,参数长度是按实际参数的字符串长度传的,而实际字符串长度又似乎是算错了,没有把一个中文当两个字符长度处理。

  没有办法只好进行跟踪调试,为了调试Delphi的VCL库,需要在工程选项的“Compiler Options”中选上“Use Debug DCUs”。

  先跟踪客户端程序,ClientDataSet1.Execute后,先后 经历了TCustomClientDataSet.Exectue、TCustomeClientDataSet.PackageParams、 TCustomClientDataSet.DoExecute等一系列函数,一直到 AppServer.AS_Execute(ProviderName, CommandText, Params, OwnerData); 把请求提交到服务器均没有什么异常情况,看来问题出在服务器端。

  对服务器进行跟踪,反复试验后,我把重点落在了 TCustomADODataSet.PSSetCommandText函数身上,经过反复细致的跟踪,目标越来越精 确:TCustomADODataSet.PSSetParams、TParameter.Assign、TParameter.SetValue、 VarDataSize。终于找到了BUG的源头:VarDataSize函数,下面是它的代码:

  function VarDataSize(const Value: OleVariant): Integer;
begin
if VarIsNull(Value) then
Result := -1
else if VarIsArray(Value) then
Result := VarArrayHighBound(Value, 1) + 1
else if TVarData(Value).VType = varOleStr then
begin
Result := Length(PWideString(@TVarData(Value).VOleStr)^); //出问题的行
if Result = 0 then
Result := -1;
end
else
Result := SizeOf(OleVariant);
end;

  就是在这个函数中计算实参的长度的,它把Value中的值取出地址,并把它作为一个WideString的指针去求字符串长度,结果就导致了“会截断的中文字”这个字符串的长度变成了7,而不是14。

  问题找到了,解决起来也就不困难了,只要简单的把
Result := Length(PWideString(@TVarData(Value).VOleStr)^); //出问题的行
改成
Result := Length(PAnsiString(@TVarData(Value).VOleStr)^); //没问题了
就可以了。

  但是这样就会导致求英文字符串的长度时长度被加倍了,所以也可以把这一行改成:
Result := Length(Value);

  这样,不管是中文还是英文还是中英混合的字符串就都可求得正确的长度了。这就我至今仍百思不解的问题,为什么Borland要绕个圈子通过指针去求参数值的长度呢?哪位朋友知道的话还请给我解释一下,非常感谢!

  有些朋友可能会有疑问,为什么在不通过三层构架来做的时候不产生这个字符串被截断的问题呢?答案并不复杂,在直接通过ADOCommand来向SQL Server发送命令时,它是按表结构来决定参数长度的。它会先向SQL Server发一条

  SET FMTONLY ON select HTH,GCMC from XsHeTong SET FMTONLY OFF

  来获取表结构。而在三层构架下,TCustomADODataSet内部虽然也是用TADOCommand对象来发命令,但它却在取得表结构的后,并不用这个值来作为传参长度,而是重新去按实际参数来计算长度,结果就导致了错误。

  BUG2.ClientDataSet的Lookup字段的问题:

  BUG再现的方法:

  新建工程,在上面放置两个ClientDataSet,分别为cds1和cds2,它的数据来源任意,其中cds1为主数据集,在里面增加一个新的Lookup字段,这个Lookup字段根据cds1中的一个字符型的字段值到cds2中找出对应值来。

  运行程序,一般来说是正常的,但是一旦cds1的被Lookup字段中的值出现了一个单引号”‘”(您可以修改或新增一条记录,输入单引号试试),立即会导致出错: Unterminated string constant(未结束的字符串常量)。

  BUG分析与修复:

  这个BUG的产生原因要比上一个明显得多,一定是没有正确处理单引号带来的副作用引起的。

  同样的,我们来跟踪VCL的源码:

  运行程序,出错时打开Call Stack窗口(在View->Debug Windows)菜单中,查看函数调用情况,前面的一些调用是显而易见的,没有问题,我们从跟Lookup有关的地方开始查原因,第一个与Lookup有 关的函数调用是TField.CalcLookupValue,我们在这个函数中设置断点,重新运行程序,中断下来后,进行单步调试。

  TCustomClientDataSet.Lookup->TCustomClientDataSet.LocateRecord

  经过上面的几次函数调用,很快的,我们就把目标定在了 LocateRecord过程中,在这个过程中,它根据Lookup字段的设置情况,生成相应的过滤条件,然后到目标数据集中把对应的值找到,错就错在过 滤条件的生成上了。比如,我们要按cds1中Cust字段(假设是001)的值到cds2中按CustID字段值找到对应的CustName字段值。那生 成的条件就应该是[CustID] = ‘001’,但如果Cust的值是aa’bb,按生成的条件就会变成[CustID] = ‘aa’bb’,显然导致了一个未结束的字符串常量。

  通常我们解决单引号中又出现单引号的情况,只需把引号中的引号写两就行了,这里也是一样,只要让生成的条件变成[CustID] = ‘aa”bb’就不会出错了。所以可以这样修改源代码:

  在LocateRecord过程中找到下面的代码:

  ftString, ftFixedChar, ftWideString, ftGUID
if (i = Fields.Count – 1) and (loPartialKey in Options) then
ValStr := Format(”’%s*”’,[VarToStr(Value)]) else
ValStr := Format(”’%s”’,[VarToStr(Value)]);

  改成:

  ftString, ftFixedChar, ftWideString, ftGUID:
if (i = Fields.Count – 1) and (loPartialKey in Options) then
ValStr := Format(”’%s*”’,[ StringReplace(VarToStr(Value),””,”””,[rfReplaceAll])])
else
ValStr := Format(”’%s”’,[ StringReplace(VarToStr(Value),””,”””,[rfReplaceAll])]);

  也就是在生成过滤条件字符串时把条件的过滤值中的单引号全部一个变两。

  为了确保这样修改的正确性,我查看了TCustomADODataSet中 的对应的LocateRecord过程(在用TADODataSet中的Lookup字段时不会因单引号出错,只在用 TCustomClientDataSet时有这样的情况),它的处理方法与TCustomClientDataSet稍有不同,它是通过 GetFilterStr函数来构造过滤条件的,但在GetFilterStr中,它正确处理了单引号的问题。所以这样来看,没有在 TCustomClientDataSet的LocateRecord中正确处理单引号的问题,确实是Borland一个不大不小的疏漏。

谁动了我的指针?

谁动了我的指针?

译者序:
本文介绍了一种在调试过程中寻找悬挂指针(野指针)的方法,这种方法是通过对newdelete运算符的重载来实现的。
这种方法不是完美的,它是以调试期的内存泄露为代价来实现的,因为文中出现的代码是绝不能出现在一个最终发布的软件产品中的,只能在调试时使用。
在VC中,在调试环境下,可以简单的通过把new替换成DEBUG_NEW来实现功能更强更方便的指针检测,详情可参考MSDN。DEBUG_NEW的实现思路与本文有相通的地方,因此文章中介绍的方法虽然不是最佳的,但还算实用,更重要的是,它提供给我们一种新的思路。

简介:
前几天发生了这样一件事,我正在调试一个程序,这个程序用了一大堆乱七八糟的指针来处理一个 链表,最终在一个指向链表结点的指针上出了问题。我们预计它应当指向的是一个虚基类的对象。我想到第一个问题是:指针所指的地方真的有一个对象吗?出问题 的指针值可以被4整除,并且不是NULL的,所以可以断定它曾经是一个有效的指针。通 过使用Visual Studio的内存查看窗口(View->Debug Windows->Memory)我们发现这个指针所指的数据是FE EE FE EE FE EE …这通常意味着内存是曾经是被分配了的,但现在却处于一种未分配的状态。不知是谁、在什么地方把我的指针所指的内存区域给释放掉了。我想要找出一种方 案来查出我的数据到底是怎么会被释放的。

背景:
我最终通过重载了newdelete运算符找到了我丢失的数据。当一个函数被调用时,参数会首先被压到栈上后,然后返回地址也会被压到栈上。我们可以在newdelete运算符的函数中把这些信息从栈上提取出来,帮助我们调试程序。

代码:
在经历了几次错误的猜测后,我决定求助于重载newdelete运算符来帮我找到我的指针所指向的数据。下面的new运算符的实现把返回地址从栈上提了出来。这个返回地址位于传递过来的参数和第一个局部变量的地址之间。编译器的设置、调用函数的方法、计算机的体系结构都会引响到这个返回地址的实际位置,所以您在使用下面代码的时候,要根据您的实际情况做一些调整。一旦new运算符获得了返回地址,它就在将要实际分配的内存前面分配额外的16个字节的空间来存放这个返回地址和实际的分配的内存大小,并且把实际要分配的内存块首地址返回。
对于delete运算符,你可以看到,它不再释放空间。它用与new同 样的方法把返回地址提取出来,写到实际分配空间大小的后面(译者注:就是上面分配的16个字节的第9到第12个字节),在最后四个字节中填上DE AD BE EF(译者注:四个十六进制数,当成单词来看正好是dead beef,用来表示内存已释放真是很形象!),并且把剩余的空间(译者注:就是原本实际应该分配而现在应该要释放掉的空间)都填上一个重复的值。
现在,如果程序由于一个错误的指针而出错,我只需打开内存查看窗口,找到出错的指针所指的地方,再往前找16个字节。这里的值就是调用new运算符的地址,接下来四个字节就是实际分配的内存大小,第三个四个字节是调用delete运算符的地址,最后四个字节应该是DE AD BE EF。接下的实际分配过的内存内容应该是77 77 77 77。
要通过这两个返回地址在源程序中分别找到对应的newdelete, 可以这样做:首先把表示地址的四个字节的内容倒序排一下,这样才能得到真正的地址,这里因为在Intel平台上字节序是低位在前的。下一步,在源代码上右 击点击,选“Go To Diassembly”。在反汇编的窗口上的左边一栏就是机器代码对应的内存地址。按Ctrl + G或选择Edit->Go To…并输入你找到的地址之一。反汇编的窗口就将滚动到对应的newdelete的函数调用位置。要回到源程序只需再次右键单击,选择“Go To Source”。您就可以看到相应的newdelete的调用了。
现在您就可以很方便的找出您的数据是何时丢失的了。至于要找出为什么delete会被调用,就要靠您自己了。
#include <MALLOC.H>

  void * ::operator new(size_t size)
{
int stackVar;
unsigned long stackVarAddr = (unsigned long)&stackVar;
unsigned long argAddr = (unsigned long)&size;

    void ** retAddrAddr = (void **)(stackVarAddr/2 + argAddr/2 + 2);

    void * retAddr = * retAddrAddr;

    unsigned char *retBuffer = (unsigned char*)malloc(size + 16);

    memset(retBuffer, 0, 16);

    memcpy(retBuffer, &retAddr, sizeof(retAddr));

    memcpy(retBuffer + 4, &size, sizeof(size));

    return retBuffer + 16;
}

  void ::operator delete(void *buf)
{
int stackVar;
if(!buf)
return;

    unsigned long stackVarAddr = (unsigned long)&stackVar;
unsigned long argAddr = (unsigned long)&buf;

    void ** retAddrAddr = (void **)(stackVarAddr/2 + argAddr/2 + 2);

    void * retAddr = * retAddrAddr;

    unsigned char* buf2 = (unsigned char*)buf;

    buf2 -= 8;

    memcpy(buf2, &retAddr, sizeof(retAddr));

    size_t size;

    buf2 -= 4;

    memcpy(&size, buf2, sizeof(buf2));

    buf2 += 8;

    buf2[0] = 0xde;
buf2[1] = 0xad;
buf2[2] = 0xbe;
buf2[3] = 0xef;


buf2 += 4;

    memset(buf2, 0x7777, size);

    // deallocating destroys saved addresses, so don’t
// buf -= 16;
// free(buf);
}

其它值得关注的地方:
这段代码同样可以用于内存泄露的检测。只需修改delete运算符使它真正的去释放内存,并且在程序退出前,用__heapwalk遍历所有已分配的内存块并把调用new的地址提取出来,这就将得到一份没有被delete匹配的new调用列表。
还要注意的是:这里列出的代码只能在调试的时候去使用,如果你把它段代码放到最终的产品中,会导致程序运行时内存被大量的消耗。

[翻译] Visual C++的“虚拟属性”功能

Microsoft Visual C++的“虚拟属性”功能

译者注:

本文简单介绍了使用Microsoft Visual C++中的__declspec关键字来实现“属性(Property)”这个C++中没有的特性的方法。有关__declspec关键字的更详细的信息,可以参考MSDN。

__declspec关键字不是标准C++的一部分,因此这种实现“属性”的方法只适用于Visual C++,如果想要了解在标准C++中模拟实现“属性”的方法,请参考:

http://www.csdn.net/develop/read_article.asp?id=18361

 

正文:

很多遗留下来的传统C++代码中常常会出现用public或protected关键字修饰的成员变量,您可以直接去访问它们(译者注,如果是protected,是指可以在其派生类中直接访问),而不是通过一组简单的get/set方法。举个例子来说,如下的结构定义就是这样的情况:

typedef struct tagMyStruct
{
long m_lValue1;
…             // Rest of the structure definition.
} SMyStruct;

在使用这个结构体的客户端程序中就可以看到散布着大量类如下面列出的代码:

SMyStruct       MyStruct;
long            lTempValue;

MyStruct.m_lValue1 = 100;       // Or any other value that is to be assigned to it.

lTempValue = MyStruct.m_lValue1;

在这种情况下,一旦这段代码需要在一个多线程的环境下应用,你就会遇到一个麻烦。因为没有get/set方法的存在,你不可能简单的在SMyStruct的定义中加上一个临界区(或互斥量)来保护包括m_lValue1在内的所有公有成员变量。

如果您是使用Microsoft Visual C++编译器,您就可以很方便的找到一个解决这个问题的方案。

您只需把您的结构体重写为如下的形式:

typedef struct tagMyStruct
{
__declspec(property(get=GetValue1, put=PutValue1))
long  m_lValue1;
…                // Rest of the structure definition.
long GetValue1()
{
// Lock critical section
return m_lInternalValue1;
// Unlock critical section.
}
void PutValue1(long lValue)
{
// Lock critical section
m_lInternalValue1 = lValue;
// Unlock critical section
}
private:
long m_lInternalValue1;
// Define critical section member variable.
} SMyStruct;

这就是您要做的全部!

在这以后,对于如下的代码:

MyStruct.m_lValue1 = 100

编译器会自动转换为:

MyStruct.PutValue(100)

对于如下的代码:

lTempValue = MyStruct.m_lValue1

编译器会自动转换为:

lTempValue = MyStruct.GetValuel()

这样的特性能带来很多有用的功能,您甚至可以用它为您原来的结构体或类加上引用计数的功能!

 

译者补充:

对于类如数组的情况,VC也提供了相应的支持,如下的例子:

#include <iostream>
using namespace std;

class MyStruct
{
public:
__declspec(property(get=GetValue1, put=PutValue1))
int t[][]; //以二维数组来演示
int GetValue1(int x, int y) //x,y分别对应第一维和第二维的下标
{
return m_lInternalValue1[x][y];
}
void PutValue1(int x,int y, int lValue) //x,y分别对应第一维和第二维的下标,lValue为要赋的值
   {
m_lInternalValue1[x][y] = lValue;
}
private:
int m_lInternalValue1[3][3];

};

int main()
{
MyStruct ms;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
ms.t[i][j] = i * j;
return 0;
}

VC6VC7中,对于多维数组的处理略有不同,如上面的

   __declspec(property(get=GetValue1, put=PutValue1))
int t[][];

在VC6中可以简单的写成int t[];即可支持两维的数组,而在VC7中必须写成int t[][];才可以。

 

原文选自:http://www.codeproject.com/cpp/virtual_property.asp

[翻译] 在C++中实现“属性 (Property)”

在C++中实现“属性 (Property)”

 

摘要:

本文介绍了在C++中实现“属性 (Property)”的方法,“属性”是我们在C#(或其它一些语言)中常常能用到的一种特性。这里介绍的实现方法使用的是标准的C++,没有用任何其它的语言扩展。而大部分的库或是编译器为了实现“属性”,往往对C++作一些扩展,就像我们在托管的C++或是C++ Builder中看到的那样,也有的是使用普通的set和get方法,这些都不能算是真正的“属性”。

 

正文:

首先,让我们来看看什么是“属性”。“属性”在外观上看起来就像类中的一个普通成员变量(或者称为是“字段”),但它内部是通过一组set/get方法(或称作read/write方法)来访问类中实际的成员变量。

举例来说,如果我有一个类A和它的一个“属性”Count,我就可以写出如下的代码:

A foo;

cout << foo.Count;

Count实际上是调用了一个get函数,并返回了我们所希望得到的成员变量值。使用“属性”而不是直接使用成员变量值的最大好处是你可控制这个“属性”是只读的(您只能读出它的值而不能改变它的值)、只写的、或是可读可写的。让我们一起来实现它吧:

我们希望能实现下面的用法:

int i = foo.Count; //– 实际上调用get函数来获取实际的成员变量的值 —

foo.Count = i;   //– 实际上将调用set函数来设置实际的成员变量的值 —

因此,很明显的,我们需要重载“=”运算符以便可以设置“属性”的值,还要正确处理“属性”的返回类型(稍后就可以看到一点)。

我们将实现一个类,名叫property,它将表现得像一个“属性”,它的结构如下:

template<typename Container, typename ValueType, int nPropType>

class property {}

这个类模版将表现为我们需要的“属性”。Container是一个类的类型(后面我们称之为“容量类”),这个类就是包含要实现为“属性”的实际成员变量、访问这个变量的set/get方法和表现出来的“属性”的类。ValueType是容量类内部的实际成员变量的类型(也将成为“属性”的类型),nPropType表示“属性”的类别:“只读”、“只写”或是“读写”。

我们还需要设置一组指针,指向容器类特定成员变量的set和get方法,同时还要重载“=”运算符,使得“属性”可以表现得像一个变量。下面我们看看property类的完整程序。

#define READ_ONLY 1

#define WRITE_ONLY 2

#define READ_WRITE 3

 

template <typename Container, typename ValueType, int nPropType>

class property

{

public:

property()

{

  m_cObject = NULL;

  Set = NULL;

  Get = NULL;

}

//– This to set a pointer to the class that contain the

//   property —

void setContainer(Container* cObject)

{

  m_cObject = cObject;

}

//– Set the set member function that will change the value —

void setter(void (Container::*pSet)(ValueType value))

{

  if((nPropType == WRITE_ONLY) || (nPropType == READ_WRITE))

    Set = pSet;

  else

    Set = NULL;

}

//– Set the get member function that will retrieve the value —

void getter(ValueType (Container::*pGet)())

{

  if((nPropType == READ_ONLY) || (nPropType == READ_WRITE))

    Get = pGet;

  else

    Get = NULL;

}

//– Overload the ‘=’ sign to set the value using the set

//   member —

ValueType operator =(const ValueType& value)

{

  assert(m_cObject != NULL);

  assert(Set != NULL);

  (m_cObject->*Set)(value);

  return value;

}

//– To make possible to cast the property class to the

//   internal type —

operator ValueType()

{

  assert(m_cObject != NULL);

  assert(Get != NULL);

  return (m_cObject->*Get)();

}

private:

  Container* m_cObject;  //– Pointer to the module that

                         //   contains the property —

  void (Container::*Set)(ValueType value);

                         //– Pointer to set member function —

  ValueType (Container::*Get)();

                         //– Pointer to get member function —

};

 

让我们来一段段的分析程序:

下面这段代码把Container指针指向一个有效的对象,这个对象就是我们要添加“属性”的类(也就是容器类)的对象。

void setContainer(Container * cObject)

{

  m_cObject = cObject;

}

下面这段代码,设置指针指向容器类的set/get成员函数。这里仅有的一点限制是set函数必须是带一个参数且返回void的函数,而get函数必须不带参数且返回ValueType型的值。

//– Set the set member function that will change the value —

void setter(void (Container::*pSet)(ValueType value))

{

  if((nPropType == WRITE_ONLY) || (nPropType == READ_WRITE))

    Set = pSet;

  else

    Set = NULL;

}

//– Set the get member function that will retrieve the value —

void getter(ValueType (Container::*pGet)())

{

  if((nPropType == READ_ONLY) || (nPropType == READ_WRITE))

    Get = pGet;

  else

    Get = NULL;

}

下面这段代码,首先是对“=”运算符进行了重载,它调用了容器类的set成员函数以实现赋值操作。然后是定义了一个转换函数,它返回get函数的返回值,这使整个property类表现得像一个ValueType型的成员变量。

//– Overload the ‘=’ sign to set the value using the set member —

ValueType operator =(const ValueType& value)

{

  assert(m_cObject != NULL);

  assert(Set != NULL);

  (m_cObject->*Set)(value);

  return value;

}

//– To make possible to cast the property class to the

//   internal type —

operator ValueType()

{

  assert(m_cObject != NULL);

  assert(Get != NULL);

  return (m_cObject->*Get)();

}

 

下面让我们看看我们是如何来使用这个property类的:

就像下面的代码中所展示的一样:PropTest类实现了一个名为Count的“属性”。这个“属性”的值实际上是通过get函数从一个名为m_nCount的私有变量获得并通过set函数把这个“属性”值的变动写回到m_nCount中去的。get/set函数可以任意命名,因为它们是通过它们的函数地址传递给property类的,就像您在PropTest类的构造函数中看到的那样。PropTest类中的”property<PropTest,int,READ_WRITE> Count; “一行就使我们的PropTest类就拥有了一个名叫Count可以被读写的整型“属性”了。您可以把Count当成是一个普通的成员变量一样来使用,而实际上,对Count的读写都是通过set/get函数间接的实现的。

PropTest类的构造函数中所做的初始化工作是必须的,只有这样才能保证定义的“属性”可以正常的工作。

class PropTest

{

public:

  PropTest()

  {

    Count.setContainer(this);

    Count.setter(&PropTest::setCount);

    Count.getter(&PropTest::getCount);

  }

  int getCount()

  {

    return m_nCount;

  }

  void setCount(int nCount)

  {

    m_nCount = nCount;

  }

  property<PropTest,int,READ_WRITE> Count;

 

 

private:

  int m_nCount;

};

 

就像下面演示那样,您可把Count“属性”当成是一个普通成员变量一样来使用:

int i = 5,j;

PropTest test;

test.Count = i;    //– call the set method —

j= test.Count;     //– call the get method —

如果您希望您定义的“属性”是只读的,您可以这样做:

property<PropTest,int,READ_ONLY > Count;

如果希望是只写的,就这样做:

property<PropTest,int,WRITE_ONLY > Count;

注意:如果您把“属性”设成是只读的而试图去改写它,将会导致一个assertion(断言)。如果“属性”是只写的而您试图去读它,也会发生同样的情况。

 

总结:

本文介绍了如何仅仅使用标准C++的特性在C++类中实现一个“属性”。当然,直接调用set/get函数会比使用“属性”效率更高,因为要使用 “属性”,您就必须为类的每一个“属性”来实例化一个property类的对象。