我正在開發一個顯示圖形的程序,該圖形具有知識產權,有沒有什麼辦法禁 用屏幕拷貝功能(Print Screen)以防止用戶將圖像拷貝到剪貼板上?
禁止 屏幕拷貝的辦法倒是有一個,但是我得告訴你,要阻止其它應用程序從你的窗口 上復制像素內容是不可能的。許多第三方程序都能捕獲屏幕內容,這種程序也不 難寫。要想截獲屏幕上的像素,你只要用 BitBlt 從屏幕設備上下文中拷貝它們 既可,例如:CWindowDC dc(NULL); // 用 NULL 獲取整個屏幕
CDC memdc;
... // 創建, 初始化 memdc
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)類是最適合做這件事的地方。
// 熱鍵的處理方法
// MainFrame.h
#include "FolderFrame.h"
#include "resource.h"
////////////////
// Typical MFC Main frame window, override to disable PrintScreen.
//
class CMainFrame : public CFrameWnd {
protected:
...
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
// disable PrintScreen
afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized);
afx_msg LRESULT OnHotKey(WPARAM wp, LPARAM lp);
afx_msg void OnDestroy();
DECLARE_MESSAGE_MAP()
};
MainFrame.cpp
#include "StdAfx.h"
#include "MainFrm.h"
#include "View.h"
IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
...
// disable PrintScreen:
ON_WM_CREATE()
ON_WM_DESTROY()
ON_WM_ACTIVATE()
ON_MESSAGE(WM_HOTKEY, OnHotKey)
END_MESSAGE_MAP()
...
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
...
RegisterHotKey(m_hWnd, IDHOT_SNAPDESKTOP, 0, VK_SNAPSHOT);
return 0;
}
void CMainFrame::OnDestroy()
{
UnregisterHotKey(m_hWnd, IDHOT_SNAPDESKTOP);
}
//////////////////
// Handle hotkey: should be PrintScreen or Alt-PrintScreen.
// Do nothing (bypass Windows screen capture)
//
LRESULT CMainFrame::OnHotKey(WPARAM wp, LPARAM)
{
UNREFERENCED_PARAMETER(wp);
return 0; // ignore
}
//////////////////
// When window is activated/deactivated, disable/enable Alt-PrintScreen.
// (IDHOT_SNAPWINDOW)
//
void CMainFrame::OnActivate(UINT nState, CWnd* pWndOther,
BOOL bMinimized)
{
CFrameWnd::OnActivate(nState, pWndOther, bMinimized);
if (nState)
RegisterHotKey (m_hWnd, IDHOT_SNAPWINDOW, MOD_ALT, VK_SNAPSHOT);
else
UnregisterHotKey(m_hWnd, IDHOT_SNAPWINDOW);
}
上述代 碼段展示了一個典型的 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 中,對於某個派生類來說,即使它在 基類中未被聲明為虛擬的,其析構也會被調用嗎?
如果你說的是托管類 ,那麼它是成立的。如果該類是本地類,則標准 C++ 規則適用;如果它是托管 類,則析構函數隱含為虛擬。理解析構函數行為最簡單的方法是寫點代碼看看編 譯器對它做了些什麼。Figure 2 示范了一個簡單的托管控制台程序,該程序聲 明了兩個托管類,CBase 和 CDerived。構造函數和析構函數用 printf 顯示被 調用時的診斷信息。如果用 /clr 編譯此程序,你會在控制台窗口看到如下的信 息:
ctor: CBase
ctor: CDerived
dtor: CDerived
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)代碼,你會看到如下的代碼行:
// delete pBase;
IL_0008: ldloc.0
IL_0009: call instance void CBase::__dtor()
當你 delete 某個托管對象時,編譯器產生一個對 __dtor 方法的調用。但編譯器調用的是哪個 __dtor 方法呢?因為我將 pBase 聲明為一個 CBase 指針(CBase*),編譯器便調用 CBase::__dtor,正如前面 的代碼段所示。這似乎就是說 CDerived 在析構期間被旁路掉了,直到你看到 CBase::__dtor 實現:
// in CBase::__dtor()
IL_0000: ldarg.0
IL_0001: call void [mscorlib]System.GC::SuppressFinalize (object)
IL_0006: ldarg.0
IL_0007: callvirt instance void CBase::Finalize()
IL_000c: ret
__dtor 函數用 callvirt 調 用 Finalize,即使你從沒聽說過IL,也能猜到那是一個調用虛擬方法的指令。 無論實際對象是哪個,公共語言運行時(CLR)都調用 Finalize 方法 ——此處是 CDerived。為了強制期望的 C++ 析構語義,每個 Finalize 方法顯式地調用其基類的 Finalize 方法,正像你在 CDerived::Finalize 所看到的那樣:
// in CDerived::Finalize()
IL_000b: ldarg.0
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。
我的回答恐怕是:不要 往那兒走。理論上,從托管代碼中調用 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 兼容的”。 這到底是什麼意思啊,我要考慮這些問題嗎?
.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 還沒有內建的無符類 型。
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 應用到整個程序集或者特定的類或方法。例如:
// mark entire assembly as CLS-compliant
[assembly:CLSCompliant (true)];
為了標記特定的類或方法是 CLS 兼容的,你得對類/方法應 用這個屬性。
唉!雖然 C++ 編譯器識別 CLSCompliant 屬性,但它無法 檢查兼容性。也就是說,即使你標記了它們的兼容性,C++ 編譯器對於非兼容代 碼不會報錯。有一個單獨的工具叫 FxCop,這個工具就像托管程序集的繃帶(應 該有人知道繃帶是什麼東西吧),然而,FxCop 雖然功能很強,會檢查並報錯( 比如,空析構函數,以“C”開頭的類名以及變量名包含非英文字符 等),但也它不檢查 CLS 兼容性,檢查兼容性對你來說更有用。所以在我寫此 文時,恐怕沒有什麼好辦法自動檢查托管 C++ 程序的 CLS 兼容性。
本文配套源碼