捕获最小化窗口:一个孩子的把戏






4.72/5 (42投票s)
即使窗口最小化或完全被其他窗口隐藏,也能捕获窗口。

背景
在我一个项目中,我需要从最小化窗口捕获。在几次尝试使用普通方法失败后;我查了很多网络(好吧,不是很多),但找不到解决方案,所以我决定研究一个新的方法。
开篇
首先,感谢上帝保佑我的脑袋没有炸开,因为我确实需要做到这一点。
也许这种方法不是很可靠,但它解决了我的问题,并且可能对其他人有用,所以我认为将其发布在 Code Project 上比不发布要好。
顺便说一句,这是我第一篇 CodeProject 文章,如果您发现任何瑕疵,请原谅我,并请您提出建议,帮助我进一步改进。
问题
好的,这种方法的核心是 `PrintWindow` API,它将窗口的界面绘制到指定的设备上下文中。
此方法可用于捕获窗口,无论其顺序和透明度如何(可以捕获部分或完全隐藏的窗口),但主要问题是,它无法捕获最小化窗口,实际上当窗口最小化时,它将不再绘制,所以如果我们使用 `PrintWindow`,它只会返回窗口的标题。
我尝试了几种捕获最小化窗口的方法,但都失败了,除了这个。
如何捕获
我假设无法从最小化窗口捕获,因此这个假设改变了方法。
我尝试将窗口恢复为正常状态然后捕获,之后再将其最小化;为了实现隐藏捕获,在我们从最小化状态弹出窗口之前,它必须是透明的,这可以通过 `SetLayeredWindowAttributes` API 来实现,但还有另一个问题。再次,窗口弹出动画仍然显示,即使窗口是完全透明的。
通过使用 SPY++,我监控了 explorer 的消息,然后取消勾选“最小化和最大化窗口时进行动画”,并发现了这个消息 `SPI_SETANIMATION`,然后,经过一些搜索,我发现可以通过 `SystemParametersInfo` API 来更改它。
可以通过注册表禁用此效果,但不是实时的。
HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics\MinAnimate
如果值为 `0`,则禁用该效果,值为 `1` 则启用。
因此,以下是我为捕获最小化窗口所遵循的步骤
- 禁用 `MinAnimate` 效果
- 使窗口透明(`PrintWindow` 可以从此类窗口截屏)
- 恢复窗口
- 捕捉
- 再次将其最小化
- 移除透明度
- 启用 `MinAnimate` 效果
现在让我们检查代码。
1 & 7 - 禁用/启用 MinAnimate 效果
我忘了提,我说 `MinAnimate` 是因为它在注册表中的名称。
如前所述,可以通过使用 `SystemParametersInfo` 来实现,代码如下
(此代码片段在 _XPAppearance.cs_ 中)
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SystemParametersInfo(SPI uiAction, uint uiParam,
ref ANIMATIONINFO pvParam, SPIF fWinIni);
`SPI` 是 `SPI` 消息的枚举,我们想要禁用/启用 `MinAnimate`,所以我们必须将 `uiAction` 设置为 `SPI_SETANIMATION`。
我们可以在 `ANIMATIONINFO` 结构中 `获取`/`设置` 该效果的状态。
请注意,`ANIMATIONINFO` 的 `cbsize` 成员必须设置为结构的大小,并且 `pvParam` 也必须等于 `cbSize`。
以下是结构
(此代码片段在 _XPAppearance.cs_ 中)
[StructLayout(LayoutKind.Sequential)]
private struct ANIMATIONINFO
{
public ANIMATIONINFO(bool iMinAnimate)
{
this.cbSize = GetSize();
if (iMinAnimate) this.iMinAnimate = 1;
else this.iMinAnimate = 0;
}
public uint cbSize;
private int iMinAnimate;
public bool IMinAnimate
{
get
{
if (this.iMinAnimate == 0) return false;
else return true;
}
set
{
if (value == true) this.iMinAnimate = 1;
else this.iMinAnimate = 0;
}
}
public static uint GetSize()
{
return (uint)Marshal.SizeOf(typeof(ANIMATIONINFO));
}
}
以下是我用于禁用/启用 `MinAnimate` 的方法
(此代码片段在 _XPAppearance.cs_ 中)
public static void SetMinimizeMaximizeAnimation(bool status)
{
ANIMATIONINFO animationInfo=new ANIMATIONINFO(status);
SystemParametersInfo(SPI.SPI_GETANIMATION, ANIMATIONINFO.GetSize(),
ref animationInfo, SPIF.None);
if (animationInfo.IMinAnimate != status)
{
animationInfo.IMinAnimate = status;
SystemParametersInfo(SPI.SPI_SETANIMATION, ANIMATIONINFO.GetSize(),
ref animationInfo, SPIF.SPIF_SENDCHANGE);
}
}
我认为上面的代码没什么需要解释的。
2 & 6 - 添加/移除透明度
为了使窗口不可见但仍可捕获,我们可以使其透明。
可以这样做
(此代码片段在 _WindowSnap.cs_ 中)
[DllImport("user32")]
private static extern int GetWindowLong(IntPtr hWnd, int index);
[DllImport("user32")]
private static extern int SetWindowLong(IntPtr hWnd, int index, int dwNewLong);
[DllImport("user32")]
private static extern int SetLayeredWindowAttributes(IntPtr hWnd, byte crey,
byte alpha, int flags);
(此代码片段在 _WindowSnap.cs_ 的 `EnterSpecialCapturing` 方法中)
winLong = GetWindowLong(hWnd, GWL_EXSTYLE);
SetWindowLong(hWnd, GWL_EXSTYLE, winLong | WS_EX_LAYERED);
SetLayeredWindowAttributes(hWnd, 0, 1, LWA_ALPHA);
要移除已应用的透明度,可以通过重新设置旧的 `winLong` 来实现。
(此代码片段在 _WindowSnap.cs_ 的 `ExitSpecialCapturing` 方法中)
SetWindowLong(hWnd, GWL_EXSTYLE, winLong);
3 & 5 - 恢复/最小化窗口
这部分很容易,可以通过 `ShowWindow` API 来实现。
(此代码片段在 _WindowSnap.cs_ 中)
[DllImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool ShowWindow(IntPtr hWnd, ShowWindowEnum flags);
private enum ShowWindowEnum{Hide = 0,
ShowNormal = 1,ShowMinimized = 2,ShowMaximized = 3,
Maximize = 3,ShowNormalNoActivate = 4,Show = 5,
Minimize = 6,ShowMinNoActivate = 7,ShowNoActivate = 8,
Restore = 9,ShowDefault = 10,ForceMinimized = 11};
(此代码片段在 _WindowSnap.cs_ 的 `EnterSpecialCapturing` 方法中)
ShowWindow(hWnd, ShowWindowEnum.Restore);
再次最小化可以使用
(此代码片段在 _WindowSnap.cs_ 的 `ExitSpecialCapturing` 方法中)
ShowWindow(hWnd, ShowWindowEnum.Minimize);
4 - 捕获
没什么可说的,有很多关于此的文章;代码如下
(此代码片段在 _WindowSnap.cs_ 的 `GetWindowImage` 方法中)
Bitmap bmp = new Bitmap(size.Width, size.Height);
Graphics g = Graphics.FromImage(bmp);
IntPtr dc = g.GetHdc();
PrintWindow(hWnd, dc, 0);
g.ReleaseHdc();
g.Dispose();
捕获子窗口
从 MDI 子窗口捕获与从普通窗口捕获非常相似,但有一个例外,如果子窗口被父窗口(而不是另一个子窗口)部分隐藏,则该部分会显示为空白。为了解决这个问题,我们可以使用 `GetParent` 和 `SetParent` API 将其作为普通窗口处理,然后再恢复。
[DllImport("user32")]
private static extern IntPtr GetParent(IntPtr hWnd);
[DllImport("user32")]
private static extern IntPtr SetParent(IntPtr child, IntPtr newParent);
我们可以将 `IntPtr.Zero` 传递给 `GetParent` 方法的 `newParent` 参数,使其恢复正常(非子窗口)。
最后的话
就是这样,最后,您可以使用 `WindowSnap` 类从 Windows 捕获屏幕。它有两个方法,`GetAllWindows` 和 `GetWindowSnap`。第一个返回所有可用窗口的屏幕截图集合,第二个返回特定窗口的屏幕截图。
最后,我感谢您花费时间阅读本文。希望它能有所帮助。请您提出建议和想法,使本文更好。
更新
- 2007 年 10 月 6 日
- 捕获子窗口:添加了对子窗口的支持。特别感谢 mmjc23 的提及。更多详情,请参阅 **捕获子窗口** 部分。
- MinAnimate Bug 修复:在上一版本中,如果 `MinAnimate` 效果被禁用,`WindowSnap` 会在 `SpecialCapturing`(从最小化窗口捕获)之后启用它。特别感谢 Kalabuli 通知我此 bug。
- 2007 年 10 月 10 日
- 子对话框捕获修复:修复了从模态子对话框捕获的问题。特别感谢 Hitesh 通知我此 bug。