Loading

SNAP实现(转自SuiFei)

转自:通过IViewObject接口,取浏览器的图象,实现SNAP

非常棒,刚开始我还没没想起来SNAP这东西,真的很cool.下面连同评论一起摘抄。

今天又见到snap实现的文章,看来对此感兴趣的人挺多的.实现这个功能确实很'眩',我也来做一个把玩一下.
我的做法不是 Control.DrawToBitmap ,而是直接QueryInterface 浏览器Com对象的 IViewObject 接口,用它实现的Draw方法,画到图象上.
首先定义IViewObject的接口声名,如下:

IVewObject接口声明
using System;
using System.Collections.Generic;
using System.Text;
using System.Security;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

namespace SnapLibrary
{
/**//// <summary>
/// 从 .Net 2.0 的 System.Windows.Forms.Dll 库提取
/// 版权所有:微软公司
/// </summary>
    [SuppressUnmanagedCodeSecurity]
internal static class UnsafeNativeMethods
{
public static Guid IID_IViewObject = new Guid("{0000010d-0000-0000-C000-000000000046}");

        [ComImport, Guid("0000010d-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IViewObject
{
            [PreserveSig]
int Draw([In, MarshalAs(UnmanagedType.U4)] int dwDrawAspect, int lindex, IntPtr pvAspect, [In] NativeMethods.tagDVTARGETDEVICE ptd, IntPtr hdcTargetDev, IntPtr hdcDraw, [In] NativeMethods.COMRECT lprcBounds, [In] NativeMethods.COMRECT lprcWBounds, IntPtr pfnContinue, [In] int dwContinue);
            [PreserveSig]
int GetColorSet([In, MarshalAs(UnmanagedType.U4)] int dwDrawAspect, int lindex, IntPtr pvAspect, [In] NativeMethods.tagDVTARGETDEVICE ptd, IntPtr hicTargetDev, [Out] NativeMethods.tagLOGPALETTE ppColorSet);
            [PreserveSig]
int Freeze([In, MarshalAs(UnmanagedType.U4)] int dwDrawAspect, int lindex, IntPtr pvAspect, [Out] IntPtr pdwFreeze);
            [PreserveSig]
int Unfreeze([In, MarshalAs(UnmanagedType.U4)] int dwFreeze);
void SetAdvise([In, MarshalAs(UnmanagedType.U4)] int aspects, [In, MarshalAs(UnmanagedType.U4)] int advf, [In, MarshalAs(UnmanagedType.Interface)] IAdviseSink pAdvSink);
void GetAdvise([In, Out, MarshalAs(UnmanagedType.LPArray)] int[] paspects, [In, Out, MarshalAs(UnmanagedType.LPArray)] int[] advf, [In, Out, MarshalAs(UnmanagedType.LPArray)] IAdviseSink[] pAdvSink);
        }
    }
}

该接口.net 自己带了,只是internal形式,所以只有想办法用Reflector 将它弄出来,相关的还有几个类,分别是tagLOGPALETTE,COMRECT,tagDVTARGETDEVICE.
定义如下:

相关定义
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Drawing;

namespace SnapLibrary
{
/**//// <summary>
/// 从 .Net 2.0 的 System.Windows.Forms.Dll 库提取
/// 版权所有:微软公司
/// </summary>
internal static class NativeMethods
{
        [StructLayout(LayoutKind.Sequential)]
public sealed class tagDVTARGETDEVICE
{
            [MarshalAs(UnmanagedType.U4)]
public int tdSize;
            [MarshalAs(UnmanagedType.U2)]
public short tdDriverNameOffset;
            [MarshalAs(UnmanagedType.U2)]
public short tdDeviceNameOffset;
            [MarshalAs(UnmanagedType.U2)]
public short tdPortNameOffset;
            [MarshalAs(UnmanagedType.U2)]
public short tdExtDevmodeOffset;
        }

        [StructLayout(LayoutKind.Sequential)]
public class COMRECT
{
public int left;
public int top;
public int right;
public int bottom;
public COMRECT()
{
            }

public COMRECT(Rectangle r)
{
this.left = r.X;
this.top = r.Y;
this.right = r.Right;
this.bottom = r.Bottom;
            }

public COMRECT(int left, int top, int right, int bottom)
{
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
            }

public static NativeMethods.COMRECT FromXYWH(int x, int y, int width, int height)
{
return new NativeMethods.COMRECT(x, y, x + width, y + height);
            }

public override string ToString()
{
return string.Concat(new object[] { "Left = ", this.left, " Top ", this.top, " Right = ", this.right, " Bottom = ", this.bottom });
            }

        }

        [StructLayout(LayoutKind.Sequential)]
public sealed class tagLOGPALETTE
{
            [MarshalAs(UnmanagedType.U2)]
public short palVersion;
            [MarshalAs(UnmanagedType.U2)]
public short palNumEntries;
        }
    }
}

现在可以通过 Marshal.QueryInterface 将浏览器COM实例的IViewObject接口取出:

//获取接口
object hret = Marshal.QueryInterface(Marshal.GetIUnknownForObject(pUnknown),ref UnsafeNativeMethods.IID_IViewObject, out pViewObject);

pUnknown为 com对象实例.
将IViewObject 指针对象 pViewObject 转化为接口对象.

ViewObject = Marshal.GetTypedObjectForIUnknown(pViewObject, typeof(SnapLibrary.UnsafeNativeMethods.IViewObject)) as SnapLibrary.UnsafeNativeMethods.IViewObject;

调用draw方法,绘制到图象上,以下是TakeSnapshot方法的完整代码:

Snapshot类
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Drawing;
using System.Windows.Forms;

namespace SnapLibrary
{
/**//// <summary>
/// ActiveX 组件快照类
/// AcitveX 必须实现 IViewObject 接口
///
/// 作者:随飞
/// http://chinasf.cnblogs.com
/// chinasf@hotmail.com
/// </summary>
public class Snapshot
{
/**//// <summary>
/// 取快照
/// </summary>
/// <param name="pUnknown">Com 对象</param>
/// <param name="bmpRect">图象大小</param>
/// <returns></returns>
public Bitmap TakeSnapshot(object pUnknown, Rectangle bmpRect)
{
if (pUnknown == null)
return null;
//必须为com对象
if (!Marshal.IsComObject(pUnknown))
return null;
//IViewObject 接口
            SnapLibrary.UnsafeNativeMethods.IViewObject ViewObject = null;
            IntPtr pViewObject = IntPtr.Zero;
//内存图
            Bitmap pPicture = new Bitmap(bmpRect.Width, bmpRect.Height);
            Graphics hDrawDC = Graphics.FromImage(pPicture);
//获取接口
object hret = Marshal.QueryInterface(Marshal.GetIUnknownForObject(pUnknown),
ref UnsafeNativeMethods.IID_IViewObject, out pViewObject);
try
{
                ViewObject = Marshal.GetTypedObjectForIUnknown(pViewObject, typeof(SnapLibrary.UnsafeNativeMethods.IViewObject)) as SnapLibrary.UnsafeNativeMethods.IViewObject;
//调用Draw方法
                ViewObject.Draw((int)DVASPECT.DVASPECT_CONTENT,
-1,
                    IntPtr.Zero,
null,
                    IntPtr.Zero,
                    hDrawDC.GetHdc(),
new NativeMethods.COMRECT(bmpRect),
null,
                    IntPtr.Zero,
0);
            }
catch (Exception ex)
{
                Console.WriteLine(ex.Message);
throw ex;
            }
//释放
            hDrawDC.Dispose();
return pPicture;
        }
    }

}

到此既完成了对Com对象的图象抓取.那么现在给它提供一个浏览器的实例,让它实现对 web page 的快照吧.
.net 2.0提供了webbrowser对象,它是对activex对象的包装,它的使用很简单,这里就不详细说明.
WebBrowser 对象的实例的属性ActiveXInstance就是它的原生COM对象,获取它的IVewObject接口,即可调用它实现的Draw方法把网页绘制到指定的DC上.
以下是对webbrowser对象的包装类,结合Snapshot 类的类代码:

web 页面快照类
/**//// <summary>
/// web 页面快照类
/// </summary>
public class WebPageSnapshot : IDisposable
{
string url = "about:blank";

/**//// <summary>
/// 简单构造一个 WebBrowser 对象
/// 更灵活的应该是直接引用浏览器的com对象实现稳定控制
/// </summary>
        WebBrowser wb = new WebBrowser();
/**//// <summary>
/// URL 地址
/// http://www.cnblogs.com
/// </summary>
public string Url
{
get { return url; }
set { url = value; }
        }
int width = 1024;
/**//// <summary>
/// 图象宽度
/// </summary>
public int Width
{
get { return width; }
set { width = value; }
        }
int height = 768;
/**//// <summary>
/// 图象高度
/// </summary>
public int Height
{
get { return height; }
set { height = value; }
        }

/**//// <summary>
/// 初始化
/// </summary>
protected void InitComobject()
{
try
{
                wb.ScriptErrorsSuppressed = false;
                wb.ScrollBarsEnabled = false;
                wb.Size = new Size(1024, 768);
                wb.Navigate(this.url);
//因为没有窗体,所以必须如此
while (wb.ReadyState != WebBrowserReadyState.Complete)
                    System.Windows.Forms.Application.DoEvents();
                wb.Stop();
if (wb.ActiveXInstance == null)
throw new Exception("实例不能为空");
            }
catch (Exception ex)
{
                Console.WriteLine(ex.Message);
throw ex;
            }
        }

/**//// <summary>
/// 获取快照
/// </summary>
/// <returns>Bitmap</returns>
public Bitmap TakeSnapshot()
{
try
{
                InitComobject();
//构造snapshot类,抓取浏览器ActiveX的图象
                SnapLibrary.Snapshot snap = new SnapLibrary.Snapshot();
return snap.TakeSnapshot(wb.ActiveXInstance, new Rectangle(0, 0, this.width, this.height));
            }
catch (Exception ex)
{
                Console.WriteLine(ex.Message);
throw ex;
            }
        }

public void Dispose()
{
            wb.Dispose();
        }

    }

这里提供一个测试用的代码:

class Program
{
/**//// <summary>
/// 测试
/// </summary>
/// <param name="args"></param>
        [STAThread]
static void Main(string[] args)
{
//web 页面快照
            WebPageSnapshot wps = new WebPageSnapshot();

if (args != null && args.Length > 1)
                wps.Url = args[0];
else
                wps.Url = "http://www.cnblogs.com";

try
{
//保存到文件
                wps.TakeSnapshot().Save("1.bmp");
            }
catch (Exception ex)
{
                Console.WriteLine(ex.Message);
                Console.ReadLine();
            }
            wps.Dispose();
        }


    }

工程原始代码下载:
/Files/Chinasf/SnapLibrary.rar
当然,这样做可能太复杂了,因为.net 为我们简化了所有的工作,简单到任意的contrl对象都支持DrawToBitmap 方法.不过想要了解机制的朋友们,可以研究一下.
2006年12月26日 8:55:14 修正:请到Snapshot类中增加一句释放引用接口的代码.
Snapshot..

try
{
//ViewObject = Marshal.GetObjectForIUnknown(pViewObject) as SnapLibrary.UnsafeNativeMethods.IViewObject;
                ViewObject = Marshal.GetTypedObjectForIUnknown(pViewObject, typeof(SnapLibrary.UnsafeNativeMethods.IViewObject)) as SnapLibrary.UnsafeNativeMethods.IViewObject;
//调用Draw方法
                ViewObject.Draw((int)DVASPECT.DVASPECT_CONTENT,
-1,
                        IntPtr.Zero,
null,
                        IntPtr.Zero,
                        hDrawDC.GetHdc(),
new NativeMethods.COMRECT(bmpRect),
null,
                        IntPtr.Zero,
0);
Marshal.Release(pViewObject);
            }
catch (Exception ex)
{
                Console.WriteLine(ex.Message);
throw ex;
            }

红色加粗位置.

回复 引用

#1楼 2006-12-25 18:00 | zoti [未注册用户]

不错,楼主真厉害。
我试试。

回复 引用

#2楼 2006-12-25 18:04 | zoti [未注册用户]

好像速度有点慢,snap的一下子就出来了,这个还需要改进一下性能,顶。

回复 引用 查看

#3楼 2006-12-25 19:23 | 贫嘴老赵

呵呵,看看学习一下

回复 引用 查看

#4楼 2006-12-25 20:51 | Kai.Ma[匿名]

学习,不知道能否解决blank image的问题。

回复 引用 查看

#5楼 2006-12-25 21:10 | Kai.Ma[匿名]

我试验了一下,能解决blank image问题,非常不错。
速度提高一下就ok。
我回去包装一下,回头散发出来。
多谢楼主&老乡,湖南人,真聪明

回复 引用 查看

#6楼 [楼主]2006-12-25 22:03 | 萧寒

@Kai.Ma
速度问题主要是因为 webbrowser 的下载WEB PAGE速度问题,如果你的带宽足够,速度应该还可以;另外 webbrowser 实例不要每次都注销和构造.
你可以考虑引入Snapshot cache 机制,我有时间会继续优化一下,并作成B/S的.
你叫我老乡,看来也是湖南人了;幸会

回复 引用 查看

#7楼 2006-12-25 23:36 | Kai.Ma

@萧寒兄
http://www.cnblogs.com/kaima/archive/2006/12/25/603519.html
我改进了一下
代码我发你信箱吧。你看看有没有改进的余地?
你信箱是多少?

回复 引用 查看

#8楼 [楼主]2006-12-25 23:45 | 萧寒

@Kai.Ma
老乡,我信箱是 chinasf at hotmail.com

回复 引用 查看

#9楼 2006-12-26 00:17 | Kai.Ma

@萧寒
已发~

回复 引用 查看

#10楼 2006-12-26 08:16 | kiler

原来楼主是我的老乡,厉害啊。

回复 引用 查看

#11楼 2006-12-26 08:45 | skyover

偶也是湖南老乡,永州的。哈哈。
看来俺们湖南人高手也蛮多的。:)

回复 引用

#12楼 2006-12-26 08:47 | 虫子[匿名] [未注册用户]

牛人, 学习.

回复 引用 查看

#13楼 2006-12-26 09:10 | Go_Rush

湖南老乡,支持一把,我是益阳的,呵呵,以后多交流
http://ashun.cnblogs.com/

回复 引用

#14楼 2006-12-26 09:15 | 忧郁的火柴头 [未注册用户]

可算找到根了
真牛啊

回复 引用

#15楼 2006-12-26 09:57 | watcher [未注册用户]

有什么用处吗?

回复 引用 查看

#16楼 2006-12-26 22:03 | 横刀天笑

佩服啊,我看到snap的时候,隐隐约约有些思路,可是毫无头绪,。。。。。。还要加紧学习啊。。tks共享

回复 引用 查看

#17楼 2006-12-26 22:49 | Kai.Ma

萧兄,再想想,snap.com,一个页面上,他是怎么处理那么多连接的“并发”的,我估计他是支持了多线程。不信你鼠标移到一个连接,然后立即移到另一个连接,一会再移回来,发现第一个的图已经抓好了。这就证明snap.com对每次触发抓图 开了一个新线程。
但是 WebBrowser这个东西怎么应用到多线程里面呢?
因为WebBrowser需要进入单元线程(STA)....我对单元线程应用还不是很熟。

回复 引用 查看

#18楼 2006-12-26 22:54 | S.Sams

支持一个!
三年前的想法, 一直都没去做, 竟然有人帮我实现啦,惭愧!
学习中...

回复 引用 查看

#19楼 [楼主]2006-12-27 00:51 | 萧寒

@Kai.Ma
如果snap.com 的实现机制也是基于WEBBROWSER的话,那么它不能是多线程的,它只能是多进程的,也就是CGI的运行模式;
单线程单元模型 (STA):进程中一个或多个线程使用 COM ,并且 COM 对象的调用由 COM 进行同步。在线程间对接口进行编组。单线程单元模型的退化情况(其中,在给定的进程中只有一个线程使用 COM)被称为单线程模型。以前的 Microsoft 信息与文档曾经将 STA 模型简单地称为“单元模型”。
它的运行线程应该是消息或用户界面 (UI) 线程。
具体解决办法正在尝试.

回复 引用 查看

#20楼 2006-12-27 14:47 | cwbboy

asp.net程序本身就是运行在多线程环境下的,所以, 我认为萧寒 说的问题根本不存在。每一次提交请求时,都是在单独的线程执行的。你可以随意使用多个线程,虽然可以随意使用,但或许根本用不着显式地使用线程对像去执行多线程,每次请求都是一个新的线程,你已经身处于多线程之中了。可以直接使用ajax 达到想要的效果。

回复 引用 查看

#21楼 2006-12-27 15:11 | Kai.Ma

@cwbboy
若真 如你这么说,我试试看,把我那代码的WebPreview静态方法改成实例化对象看看。

回复 引用 查看

#22楼 [楼主]2006-12-27 15:29 | 萧寒

@cwbboy
你理解错我的意思;系统的开销在于webbrowser 实例在每个线程中创建的sta线程内构造和消亡,并且你无法创建一个静态的webbrowser ,它必须存在于一个sta的线程,也就是说它做不到唯一实例.我设想的是,创建一个进程,构造一个webbrowser ,再一个线程中处理所有web 的请求.
webbrowser 的构造副本对象很简单,也就是可以对每个请求构造一个副本,就象ie多页面的实现那样;另外需要解决进程于asp.net的通讯问题,比如通过内存映射,登记一些信息,等待进程的处理.最终asp.net端获得的是image file.

回复 引用 查看

#23楼 2006-12-27 15:30 | Kai.Ma

@cwbboy
谢谢你的提醒,不过这样(每次都实例化对象)的话,请求的数量一下子太大了,一段时间内对服务器CPU也有很高的占用。
不知道snap.com他们怎么处理高【并发】的

回复 引用 查看

#24楼 2006-12-27 15:35 | Kai.Ma

我想只有借助ThreadPool了。:)

回复 引用 查看

#25楼 [楼主]2006-12-27 15:37 | 萧寒

@Kai.Ma
支持你一下;
等待结果:)

回复 引用 查看

#26楼 2006-12-28 16:27 | 阿蒙[匿名]

每次生成截图,CPU占用都很高

回复 引用 查看

#27楼 2006-12-31 10:44 | Wu.Country@侠缘

关注一下。

回复 引用 查看

#28楼 2007-03-18 14:37 | 在北京的湖南人

哈哈,我也是湖南人,泪汪汪.....

回复 引用

#29楼 2007-03-28 09:53 | l初学者 [未注册用户]

在Web中出现 "当前线程不在单线程单元中,因此无法实例化 ActiveX 控件“8856f961-340a-11d0-a96b-00c04fd705a2”"。怎样解决

回复 引用 查看

#30楼 [楼主]2007-03-31 00:14 | 萧寒

@l初学者
注意,构造webbrowser 对象必须位于单线程单元模型 (STA)内。

回复 引用

#31楼 2007-07-12 16:26 | 阿甘 [未注册用户]

在Web中出现 "当前线程不在单线程单元中,因此无法实例化 ActiveX 控件“8856f961-340a-11d0-a96b-00c04fd705a2”"。
具体的解决方法是怎样呢?不明白

回复 引用

#32楼 2007-07-12 16:27 | 阿甘 [未注册用户]

有没有b/s的??

回复 引用

#33楼 2007-08-30 10:35 | greystar [未注册用户]

当前线程不在单线程单元中,因此无法实例化 ActiveX 控件“8856f961-340a-11d0-a96b-00c04fd705a2”。
在网页中根本无法直接去调用.
我感觉只能去做个服务器.请求时,去请求该服务.
DEVExpress的网页导出不知是如何做的.可以导出N种格式

回复 引用 查看

#34楼 2007-11-22 18:16 | ithurricane

牛人啊,
PFPF

回复 引用

#35楼 2008-02-01 11:29 | fish man [未注册用户]

請教一下
如果網頁開的是 doc 或是 ppt 文件
似乎就不能抓圖了 有解決方法嗎
感恩

回复 引用 查看

#36楼 2008-05-09 14:40 | ぐ最後①葉ゞ

随风大哥和Kai.Ma大哥的文章写得很不错,我很认真地看了好多遍。。。

回复 引用

#37楼 2008-06-24 09:50 | liuhaizhi [未注册用户]

楼主速加我QQ或者邮箱 我有个急的问题问你关于截取图片的,需要改进下
QQ284914216
email:284914216@163.com

回复 引用

#38楼 2008-08-23 21:53 | 河东村村长 [未注册用户]

敢问楼主,我现在从IE浏览器中获取了一个WebBrowserClass及其一个实例对象,能否抓取其显示的页面为图片呢?

回复 引用

#39楼 2008-08-24 08:13 | 河东村村长 [未注册用户]

敢问楼主,我现在从IE浏览器中获取了一个WebBrowserClass及其一个实例对象,而不是webBrowser控件,能否抓取其显示的页面为图片呢?

回复 引用

#40楼 2009-02-08 16:06 | msii [未注册用户]

可惜不是全屏的,


posted @ 2009-04-06 16:23  today4king  阅读(742)  评论(1编辑  收藏  举报