我正在开发一个显示图形的程序,该图形具有知识产权,有没有什么办法禁用屏幕拷贝功能(Print Screen)以防止用户将图像拷贝到剪贴板上?
Martin Cruz
禁止屏幕拷贝的办法倒是有一个,但是我得告诉你,要阻止其它应用程序从你的窗口上复制像素内容是不可能的。许多第三方程序都能捕获屏幕内容,这种程序也不难写。要想截获屏幕上的像素,你只要用 BitBlt 从屏幕设备上下文中拷贝它们既可,例如:
1.
CWindowDC dc(NULL);
// 用 NULL 获取整个屏幕
2.
CDC memdc;
3.
...
// 创建, 初始化 memdc
4.
memdc.BitBlt(..., &dc);
// 拷贝屏幕内容
若要复制当前活动窗口的内容,只要获取该窗口的 CWnd 指针,然后用它来构造一个 CWindowDC,即可从中提取内容。
总之,你无法阻止其它程序截获你窗口的像素。那么,如果你仅仅只是要禁用“屏幕拷贝”,或是阻止该功能做些什么,那其实很容易。Windows 通过注册热键来实现“屏幕 拷贝”功能。在我 2000 年 12 月的栏目中,我示范了如何用 RegisterHotKey 来注册应用程序热键(参见 C++ Q&A: Sending Messages in Windows, Adding Hot Keys to your Application),Windows 使用预定义的热键 IDHOT_SNAPDESKTOP 和 IDHOT_SNAPWINDOW 来处理“屏幕 拷贝”。这两个热键分别对应于“Print Screen”和“Alt+Print Screen”,前者用来复制整个屏幕,而后者则仅复制当前活动窗口。 为了禁用这些功能,你只要注册这些热键,当用户按下这些热键时,让 Windows 向你的程序发送 WM_HOTKEY 消息,此时你可以忽略这些消息, 旁路掉默认的屏幕复制行为既可。你的主框架(mainframe)类是最适合做这件事的地方。
01.
// 热键的处理方法
02.
// MainFrame.h
03.
04.
#include "FolderFrame.h"
05.
#include "resource.h"
06.
////////////////
07.
// Typical MFC Main frame window, override to disable PrintScreen.
08.
//
09.
10.
class
CMainFrame :
public
CFrameWnd {
11.
protected
:
12.
...
13.
afx_msg
int
OnCreate(LPCREATESTRUCT lpCreateStruct);
14.
// disable PrintScreen
15.
afx_msg
void
OnActivate(
UINT
nState, CWnd* pWndOther,
BOOL
bMinimized);
16.
afx_msg
LRESULT
OnHotKey(
WPARAM
wp,
LPARAM
lp);
17.
afx_msg
void
OnDestroy();
18.
DECLARE_MESSAGE_MAP()
19.
};
20.
MainFrame.cpp
21.
#include "StdAfx.h"
22.
#include "MainFrm.h"
23.
#include "View.h"
24.
25.
IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)
26.
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
27.
...
28.
// disable PrintScreen:
29.
ON_WM_CREATE()
30.
ON_WM_DESTROY()
31.
ON_WM_ACTIVATE()
32.
ON_MESSAGE(WM_HOTKEY, OnHotKey)
33.
END_MESSAGE_MAP()
34.
...
35.
int
CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
36.
{
37.
...
38.
RegisterHotKey(m_hWnd, IDHOT_SNAPDESKTOP, 0, VK_SNAPSHOT);
39.
return
0;
40.
}
41.
42.
void
CMainFrame::OnDestroy()
43.
{
44.
UnregisterHotKey(m_hWnd, IDHOT_SNAPDESKTOP);
45.
}
46.
47.
//////////////////
48.
// Handle hotkey: should be PrintScreen or Alt-PrintScreen.
49.
// Do nothing (bypass Windows screen capture)
50.
//
51.
LRESULT
CMainFrame::OnHotKey(
WPARAM
wp,
LPARAM
)
52.
{
53.
UNREFERENCED_PARAMETER(wp);
54.
return
0;
// ignore
55.
}
56.
57.
//////////////////
58.
// When window is activated/deactivated, disable/enable Alt-PrintScreen.
59.
// (IDHOT_SNAPWINDOW)
60.
//
61.
void
CMainFrame::OnActivate(
UINT
nState, CWnd* pWndOther,
62.
BOOL
bMinimized)
63.
{
64.
CFrameWnd::OnActivate(nState, pWndOther, bMinimized);
65.
if
(nState)
66.
RegisterHotKey(m_hWnd, IDHOT_SNAPWINDOW, MOD_ALT, VK_SNAPSHOT);
67.
else
68.
UnregisterHotKey(m_hWnd, IDHOT_SNAPWINDOW);
69.
}
上述代码段展示了一个典型的 MFC CMainFrame 类实现。OnCreate/OnDestroy 函数用来注册/注销 IDHOT_SNAPDESKTOP 热键;OnActivate 函数用来在应用程序 处于激活/和非激活状态时注册/注销 IDHOT_SNAPWINDOW 热键。当你的窗口处于非激活状态时,通过重新启用 IDHOT_SNAPWINDOW,当别的应用程序拥有焦点时,用户仍然能用 Alt+Print Screen 来复制屏幕。
你也许会想到用 CS_OWNDC 式样来注册窗口类以防止屏幕拷贝(它导致 Windows 为窗口类分配一个私有设备上下文),但那样做行不通。Windows 还是会把私有 DC 中的 像素位复制到屏幕 DC 中,这样一来,任何存取屏幕 DC 的程序都能看到你的像素。
在 2004 年 11 月的专栏中,你谈到了在托管和非托管代码中调用虚拟函数的问题,参见“调用虚拟函数,持续化视图状态,POD 类型概念”。在 C++ 里,如果我想让派生类的析构函数在释放内存时被调用,我得在基类中将其声明为虚拟函数。那么在 Visual Studio 2005 中,对于某个派生类来说,即使它在基类中未被声明为虚拟的,其析构也会被调用吗?
Jigar Mehta
如果你说的是托管类,那么它是成立的。如果该类是本地类,则标准 C++ 规则适用;如果它是托管类,则析构函数隐含为虚拟。理解析构函数行为最简单的方法是写点代码看看编译器对它做了些什么。Figure 2 示范了一个简单的托管控制台程序,该程序声明了两个托管类,CBase 和 CDerived。构造函数和析构函数用 printf 显示被调用时的诊断信息。如果用 /clr 编译此程序,你会在控制台窗口看到如下的信息:
01.
Figure 2 vdtor.cpp
02.
03.
04.
// To compile:
05.
//
06.
// cl /clr vdtor.cpp
07.
//
08.
include < tchar.h >
09.
#include < stdio.h >
10.
11.
#using < mscorlib.dll >
12.
using
namespace
System;
13.
14.
//////////////////
15.
// Managed base class.
16.
//
17.
public
__gc
class
CBase {
18.
public
:
19.
CBase()
20.
{
21.
printf
(
"ctor: CBase\n"
);
22.
}
23.
24.
// virtual keyword unnecessary for managed class because CBase is
25.
// implicitly derived from Object and the dtor is converted to a
26.
// Finalize method, which is virtual.
27.
28.
/*virtual*/
~CBase()
29.
{
30.
printf
(
"dtor: CBase\n"
);
31.
}
32.
};
33.
34.
//////////////////
35.
// Managed derived class.
36.
//
37.
public
__gc
class
CDerived :
public
CBase {
38.
public
:
39.
CDerived()
40.
{
41.
printf
(
"ctor: CDerived\n"
);
42.
}
43.
~CDerived()
44.
{
45.
printf
(
"dtor: CDerived\n"
);
46.
}
47.
};
48.
49.
//////////////////
50.
// Program entry point.
51.
//
52.
int
_tmain()
53.
{
54.
// Create object: note pointer is declared as CBase* but actually
55.
// points to instance of derived class.
56.
CBase* pBase =
new
CDerived();
57.
58.
// Explicitly delete to see which dtor is called...?
59.
delete
pBase;
60.
61.
return
0;
62.
}
1.
ctor: CBase
2.
ctor: CDerived
3.
dtor: CDerived
4.
dtor: CBase
这个信息说明了即使派生类和基类都不把析构函数声明为虚拟,派生类的析构都会被调用。构造函数和析构函数按期望的顺序被调用,先调用基类构造函数,最后是析构函数。
为什么要将托管析构函数声明为虚拟的呢?回想一下每一个托管类要么必须显式地从另一个托管类派生,要么隐式地从根基类 Object 派生。还要记住的是 C++ 编译器将托管析构转换为 Finalize 方法,该方法在 Object 类中是虚拟的。为了明白这一点,你只要用 ILDASM 反汇编器察看一下所编译的代码既可。
Figure 3 反汇编后的 vdtor.exe
Figure 3 展示了 vdtor.cpp 的反汇编代码。CBase 和 CDerived 都有 Finalize 方法;Figure 4 是派生类 CDerived 的 Finalize 方法。它还说明了编译器为每个类创建了一个特殊的 __dtor 方法。该方法在你调用 delete 时被调用。如果你检查一下主入口函数 main 的微软中间语言(IL)代码,你会看到如下的代码行:
01.
Figure 4 IL
for
Finalize Method
02.
03.
.method family
virtual
instance
void
Finalize() cil managed {
04.
// Code size 18 (0x12)
05.
.maxstack 1
06.
IL_0000: ldsflda valuetype $ArrayType$0x6e2836de modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier) ??_C@_0BA@PPNICPAI@dtor?3?5CDerived?6?$AA@
07.
IL_0005: call vararg int32 modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
printf
(int8 modopt([Microsoft.VisualC]Microsoft.VisualC.NoSignSpecifiedModifier) modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier)*)
08.
IL_000a: pop
09.
IL_000b: ldarg.0
10.
IL_000c: call instance
void
CBase::Finalize()
11.
IL_0011: ret
12.
}
// end of method CDerived::Finalize
1.
// delete pBase;
2.
IL_0008: ldloc.0
3.
IL_0009: call instance
void
CBase::__dtor()
当你 delete 某个托管对象时,编译器产生一个对 __dtor 方法的调用。但编译器调用的是哪个 __dtor 方法呢?因为我将 pBase 声明为一个 CBase 指针(CBase*),编译器便调用 CBase::__dtor,正如前面的代码段所示。这似乎就是说 CDerived 在析构期间被旁路掉了,直到你看到 CBase::__dtor 实现:
1.
// in CBase::__dtor()
2.
IL_0000: ldarg.0
3.
IL_0001: call
void
[mscorlib]System.GC::SuppressFinalize(object)
4.
IL_0006: ldarg.0
5.
IL_0007: callvirt instance
void
CBase::Finalize()
6.
IL_000c: ret
__dtor 函数用 callvirt 调用 Finalize,即使你从没听说过IL,也能猜到那是一个调用虚拟方法的指令。无论实际对象是哪个,公共语言运行时(CLR)都调用 Finalize 方法――此处是 CDerived。为了强制期望的 C++ 析构语义,每个 Finalize 方法显式地调用其基类的 Finalize 方法,正像你在 CDerived::Finalize 所看到的那样:
1.
// in CDerived::Finalize()
2.
IL_000b: ldarg.0
3.
IL_000c: call instance
void
CBase::Finalize()
这里编译器产生一个常规调用指令,而非 callvirt。否则你的程序将出现死循环直到耗尽堆栈。
你会注意到在调用 Finalize 方法之前,CBase::__dtor 调用 SuppressFinalize,为什么呢?因为在 C++ 里,当你清除某个托管对象时,系统不会释放那个对象的存储区。至少不会立即释放。在垃圾收集器运行之前,该对象的内存不会被释放。必须用 SuppressFinalize 来避免对象被终结两次――第一次是调用 delete 时,再一次是垃圾收集器运行的时候。明白吗?
有没有办法从 .NET 框架程序集中调用 MFC 扩展 DLL?我知道如何用 P/Invoke 来调用常规的 DLL 或 COM DLL,但是不知道如何处理 MFC 扩展 DLL。
Ali Zamurad
我的回答恐怕是:不要往那儿走。理论上,从托管代码中调用 MFC 扩展 DLL 是可能的;但实际上那是极其困难的。MFC 扩展 DLL 关系到大量 MFC 状态,很难在托管代码中建立这些状态。例如,当你调用 AfxGetApp 获取应用程序对象时,它假定 CWinApp 指针在 MFC 的模块中已被初始化为一个全局指针。如果你用 MFC 编写 COM 对象,你知道要在每一个初始化 MFC 状态的入口处使用 AFX_MANAGE_STATE 或 METHOD_PROLOGUE 宏。
MFC 扩展 DLLs 与 主 EXE 或 宿主该扩展的 DLL 共享一个 派生的 CWinApp。所以,如果你创建一个新的 EXE(即使是本地的),在没有应用程序对象的情况下,也无法加载 MFC 扩展。主 EXE 必须是一个 MFC 应用程序(某些部分可能用 /clr 编译);或者你必须重写你的 DLL,不要将它做成 MFC 扩展 DLL。如果你有后端代码实现业务逻辑或算法,你应该在不需要状态的 C extern 函数中隔离它们,以便用 P/Invoke 调用;或者还有一个办法,将逻辑包装到托管类中(具体细节参见我在 2005 年 09 月的专栏:“拷贝构造和赋值操作符,C#和本机 C++ 代码的互用性” 或者 .NET 文档)。对于用户接口代码,微软对此有明确忠告,就是没有办法从托管代码中调用 MFC。
我有一个用 C++ 写的类库,我用托管扩展将它们暴露给 .NET。某些函数使用 uint (无符整型),它与 .NET 中的 UInt32 对应。后来我阅读了一些权威资料后发现 UInt32 不是“ CLS 兼容的”。这到底是什么意思啊,我要考虑这些问题吗?
Dave Layton
.NET CLR 是运行时系统,它加载并执行托管程序集。CLR 是一个让你可以用任何你想要的语言编写基于 .NET 应用程序的系统,只要为这些语言提供了 IL 编译器。例如,C++ 有 unsigned int,C# 有 uint,但 Visual Basic .NET 现在没有等同的与无符整型对应的内建类型(到了 Visual Basic 2005 将会支持这种类型)。如果你想让对象完全与其它对象交互,而不管它们是用哪种语言实现的,那么就必须将自己约束在整个类型系统的较小的子集中。该子集由 CLS (Common Language Specification――公共语言规范)定义,在 .NET 框架文档中,你可以在“Cross-Language Interoperability――跨语言户用性”中找到这个主题,CLS 规定哪种内建类型是 CLS 兼容的。
Figure 5 是部分处理整数类型的规范。正像你所看到的,无符类型不是 CLS 兼容的。那是因为在开发框架的时候,Visual Basic 还没有内建的无符类型。
01.
Figure 5 Integral Types
02.
03.
CLR Class Description CLS-Compliant Visual Basic C# C++
04.
Int16 A 16-bit
signed
integer. Yes Short
short
short
05.
Int32 A 32-bit
signed
integer. Yes Integer
int
int
,
long
06.
Int64 A 64-bit
signed
integer. Yes Long
long
__int64
07.
UInt16 A 16-bit unsigned integer. No UInt16. No built-in type. ushort unsigned
short
08.
UInt32 A 32-bit unsigned integer. No UInt32. No built-in type. uint unsigned
int
,unsigned
long
09.
UInt64 A 64-bit unsigned integer. No UInt64. No built-in type. ulong unsigned
__int64
10.
IntPtr A
signed
integer whose size depends on the underlying platform (a 32-bit value on a 32-bit platform and a 64-bit value on a 64-bit platform). Yes IntPtr. No built-in type. IntPtr. No built-in type. IntPtr. No built-in type.
11.
UIntPtr An unsigned integer whose size depends on the underlying platform (a 32- bit value on a 32-bit platform and a 64-bit value on a 64-bit platform). No UIntPtr. No built-in type. UIntPtr. No built-in type. UIntPtr. No built-in type.
UInt32 不是 CLS 兼容的,这是什么意思呢?对于你的库来说,它又意味着什么呢?首先,CLS 规则只应用于你的程序集向外界暴露的类和方法。在内部,你可以使用语言支持的任何类型。其次,UInt32 是一个成熟的 CLR 类,这意味着任何面向 .NET 的语言,包括 Visual Basic在内,都能编译并与你的代码链接,即使它暴露使用 UInt32 的方法。如果你有一个返回 UInt32 的方法,无论是作为值返回,还是作为 [out] 参数返回,任何程序都可以接着将那个值作为输入参数传给另一个方法。但某些类似 Visual Basic 这样的语言可能无法创建无符整型。这可能是个问题,也可能不是问题,这要依赖你的应用程序而定。
一个 Visual Basic 程序总是可以传递负整数,并且当它到达你的库时,它会被作为无符对待――但此 Visual Basic 程序不能进行正确的计算,因为它将大的无符值看成是负整数。如果你需要计算并处理整个32位范围的无符整数,你应该将参数暴露为 Int64,而不是 UInt32。
你怎么知道你的程序是否是 CLS 兼容的呢?如果你用 C# 编写,你可以用 CLSCompliant 属性让编译器检查你的代码的 CLS 兼容性。你可以将 CLSComplisn 应用到整个程序集或者特定的类或方法。例如:
1.
// mark entire assembly as CLS-compliant
2.
[assembly:CLSCompliant(
true
)];
为了标记特定的类或方法是 CLS 兼容的,你得对类/方法应用这个属性。
唉!虽然 C++ 编译器识别 CLSCompliant 属性,但它无法检查兼容性。也就是说,即使你标记了它们的兼容性,C++ 编译器对于非兼容代码不会报错。有一个单独的工具叫 FxCop,这个工具就像托管程序集的绷带(应该有人知道绷带是什么东西吧),然而,FxCop 虽然功能很强,会检查并报错(比如,空析构函数,以“C”开头的类名以及变量名包含非英文字符等),但也它不检查 CLS 兼容性,检查兼容性对你来说更有用。所以在我写此文时,恐怕没有什么好办法自动检查托管 C++ 程序的 CLS 兼容性。