最近在做的一個項目是一個Word 2003的插件。項目的一個需求是控制用戶在Word中的拖拽行為。具體來說有三種:
1、用戶完全不能把某些文字Drag起來
2、對于某些文字,可以Drag起來,但是不能Drop到除當前文檔之外的任何地方
3、對于某些文字,任意東西都不能Drop于其上
說實話,我了解到這個是需求的時候, 第一反應就是,這可能嗎?借用阿迪的廣告語,Impossible is Nothing。的確,Windows之所以千瘡百孔,在我看來很大程度上就是它提供了太多的可Hack的手段了。在實現這個可控的拖拽行為之前,已經通過SetWindowsHook控制了用戶的鼠標和鍵盤(當然這種技術已經用爛了,我就不再炒冷飯了)。這次,我們使用Windows API Hook來達到這個目的。
如果不了解拖拽到底是怎么實現的,我們是不可能控制它的行為的。 我們要做的,其實就是找出標準的拖拽實現方式。然后在其中插一腳,把我們感興趣的東西攔截下來,并篡改掉本來的輸出結果。說實話,Hook的善惡就在一念之間。那么我們先來簡單了解一下拖拽的流程:
Drag:
1、應用程序在用戶用鼠標拖拽了一個物體之后,調用 DoDragDrop(dataObject, dropSource, okEffect, effects) 開始拖拽
2、Windows 回調 dropSource 的 QueryContinueDrag 來決定是不是繼續Drag
Drop:
1、應用程序在初始化的時候,調用 RegisterDragDrop(hwnd, dropTarget)
2、當有物體拖拽進了 hwnd 所在的區域時,Windows 回調 dropTarget 的 DragEnter
3、當物體在 hwnd 所在區域內滑動時,Windows 回調 dropTarget 的 DragOver
4、當物體拖拽出 hwnd 所在區域時,Windows 回調 dropTarget 的 DragLeave
5、當拖拽的物體放下是,Windows 回調 dropTarget 的 Drop
所以對于我們來說,重點關注的就是兩個API,兩個Com Interface。分別是 Ole32 的 DoDragDrop 和 RegisterDragDrop。以及 Ole32 中的 IDropTarget, IDropSource。要實現開頭所述的三種行為,我們只需要:
1、 用戶完全不能把某些文字Drag起來:
攔截DoDragDrop的調用,如果dataObject或者用戶當前選中的區域是受到保護的,就不調用Ole32的真實實現,直接返回0。
2、 對于某些文字,可以Drag起來,但是不能Drop到除當前文檔之外的任何地方:
攔截DoDragDrop的調用,真實地去調用Ole32的真實實現,但是不直接使用原裝的DropSource(包裝之后再用)。因為我們要監聽Windows對DropSource的回調,再“恰當”的時候篡QueryContinueDrag的返回值。從而使得用戶無法Drop到當前文檔之外的區域。
3、 對于某些文字,任意東西都不能Drop于其上:
攔截RegisterDragDrop的調用,確實使用Ole32的真實實現,但是也不直接使用原裝的DropTarget(包裝之后再用)。因為我們要監聽Windows對DropTarget的回調。同樣,在“恰當” 的時候篡改DragOver和Drop的返回值。
那么問題就集中在了如何攔截Ole32的兩個API上。這個問題很好解決,直接使EasyHook(http://www.codeplex.com/easyhook)庫就行了。我閱讀了這個庫的所有源代碼,有機會可以給大家講講Windows API Hook的原理,也挺有意思的。而且使用EasyHook,我們還可以做到遠程注入(底層實現是CreateRemoteThread,老套但是可靠,關鍵是支持.NET)。這樣,就不用受限于當前進程了。不過由于RegisterDragDrop一般在初始化的時候調用,所以最好使用EasyHook的CreateAndInject來做,要不然就時機太晚了。在本例中是一個Word 2003的插件,而且恰巧Word 2003是先加載AddIn再RegisterDragDrop,要不然也就Hook無門了。
下面是一些關鍵細節的源代碼:
安裝DoDragDrop的鉤子

Code
using System;
using System.Runtime.InteropServices.ComTypes;
using EasyHook;
namespace xxx
{
public class RangeDragEvents : IDisposable
{
private readonly WordApplication application;
private readonly RangeListener listener;
private readonly Ole32._DoDragDrop doDragDropHandler;
private readonly LocalHook doDragDropHook;
public RangeDragEvents(WordApplication application, RangeListener listener)
{
this.application = application;
this.listener = listener;
doDragDropHandler = HandleDoDragDrop;
doDragDropHook = LocalHook.Create(LocalHook.GetProcAddress("ole32.dll", "DoDragDrop"),
doDragDropHandler, new object());
doDragDropHook.ThreadACL.SetExclusiveACL(new int[0]);
}
private int HandleDoDragDrop(IDataObject rawDataObject, Ole32.DropSource originalDropSource, int allowedEffect,
int[] finalEffect)
{
Ole32.DropSource dropSource = originalDropSource;
switch (GetDragAction())
{
case DragAction.JAILED:
dropSource = new JailDropSource(originalDropSource);
break;
case DragAction.CANCELLED:
return 0;
}
return Ole32.DoDragDrop(rawDataObject, dropSource, allowedEffect, finalEffect);
}
private DragAction GetDragAction()
{
WordSelection selection = application.Selection;
if (selection == null)
{
return DragAction.PROCEEDED;
}
using (var detached = selection.Detach())
{
var range = detached.Range;
if (range.Start == range.End)
{
return DragAction.PROCEEDED;
}
return listener.HandleDrag(range);
}
}
public void Dispose()
{
doDragDropHook.Dispose();
}
}
}
安裝RegisterDragDrop的鉤子

Code
using System;
using EasyHook;
namespace xxx
{
public class RangeDropEvents : IDisposable
{
private readonly Ole32._RegisterDragDrop registerDragDropHandler;
private readonly LocalHook registerDragDropHook;
private readonly RangeListener listener;
private readonly WordApplication application;
public RangeDropEvents(WordApplication application, RangeListener listener)
{
this.listener = listener;
this.application = application;
registerDragDropHandler = HandleRegisterDragDrop;
registerDragDropHook = LocalHook.Create(LocalHook.GetProcAddress("ole32.dll", "RegisterDragDrop"),
registerDragDropHandler, new object());
registerDragDropHook.ThreadACL.SetExclusiveACL(new int[0]);
}
private int HandleRegisterDragDrop(IntPtr hwnd, Ole32.DropTarget target)
{
return Ole32.RegisterDragDrop(hwnd, new RangeAwareDropTarget(application, listener, target));
}
public void Dispose()
{
registerDragDropHook.Dispose();
}
}
}
把Drag的物體“Jail”在當前文檔中

Code
using System;
using System.Drawing;
using System.Windows.Forms;
using log4net;
namespace xxx
{
public class JailDropSource : Ole32.DropSource
{
private static readonly ILog LOGGER = LogManager.GetLogger(typeof (JailDropSource));
private const int DRAGDROP_S_DROP = 0x00040100;
private const int DRAGDROP_S_CANCEL = 0x00040101;
private readonly Ole32.DropSource dropSource;
private readonly IntPtr jailedIn;
public JailDropSource(Ole32.DropSource dropSource)
{
jailedIn = User32.GetForegroundWindow();
this.dropSource = dropSource;
}
public int QueryContinueDrag(int escapePressed, int keyState)
{
int result = dropSource.QueryContinueDrag(escapePressed, keyState);
if (result == DRAGDROP_S_DROP)
{
if (!IsDropable())
{
return DRAGDROP_S_CANCEL;
}
}
return result;
}
public int GiveFeedback(int effect)
{
if (!IsDropable())
{
Cursor.Current = Cursors.No;
return 0;
}
return dropSource.GiveFeedback(effect);
}
private bool IsDropable()
{
try
{
if (jailedIn != User32.GetForegroundWindow())
{
return false;
}
User32.RECT rect = User32.GetWindowRect(jailedIn);
var rectangle = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top);
return rectangle.Contains(Cursor.Position);
}
catch (Exception e)
{
LOGGER.Error("can not decide it is droppable or not, default to false", e);
return false;
}
}
}
}
監聽DropTarget的回調,拒絕某些場合下的Drop下來東西

Code
using System.Windows.Forms;
namespace xxx
{
public class RangeAwareDropTarget : ProxyDropTarget
{
private readonly RangeListener listener;
private readonly WordApplication application;
public RangeAwareDropTarget(WordApplication application, RangeListener listener, Ole32.DropTarget dropTarget)
: base(dropTarget)
{
this.listener = listener;
this.application = application;
}
public override int DragOver(int keyState, long point, ref int effect)
{
var result = base.DragOver(keyState, point, ref effect);
if (ShouldHandle())
{
effect = (int) DragDropEffects.None;
}
return result;
}
public override int Drop(object dataObject, int keyState, long point, ref int effect)
{
if (ShouldHandle())
{
DragLeave();
return 0;
}
return base.Drop(dataObject, keyState, point, ref effect);
}
private bool ShouldHandle()
{
var window = application.ActiveWindow;
if (window == null)
{
return false;
}
using (var detached = window.Detach())
{
var range = detached.RangeFromPoint(Cursor.Position.X, Cursor.Position.Y);
if (range == null)
{
return false;
}
return listener.HandleChange(range, ChangeSource.Drop);
}
}
}
}