1. 程式人生 > >剛剛做完的一個屏幕截圖程序,分享一下

剛剛做完的一個屏幕截圖程序,分享一下

容易 reg ID psi change 控件 des tro 剛才

原文:剛剛做完的一個屏幕截圖程序,分享一下

剛剛做完的一個屏幕截圖程序,分享一下

周銀輝

好長時間沒有更新博客了啊,把絕大多數時間花在了那個開源的WPFToolkit 和 《深入理解操作系統》上, 然後見縫插針地做了這個截圖程序。

你可以在這裏下載DEMO程序試試效果(我感覺還行~)

拖拽的效果和Windows7自帶的snipping tool 差不多,拖拽區域之外是半透明遮罩,拖拽區域之內被鏤空的,但其拖拽完成後並不立即截圖,你可拖拽手柄來重新調節截圖區域,然後雙擊截圖區域,完成截圖

技術分享圖片

1, 選用C++,WinForm還是WPF來完成該程序

選用C++來做的話,我們可以很方便地用bitblt函數來進行屏幕圖像的拷貝,似乎大多數截圖程序都是這麽幹的

選用WinForm的話,可以采用Graphics對象的CopyFromScreen函數來屏幕圖像的拷貝,這也很方便,不過其相對於WPF更大的好處在於,GDI+是實時繪圖的,在你繪制上圖中的那個藍色框時不會有明顯的滯後感,特別是對應高分辨率多顯示器這樣的環境下,WPF的OnRender函數的滯後感是很嚴重的

選用WPF嘛,從程序本身看,沒什麽好處,並且WPF貌似沒有截圖的API吧~~ 況且還有上面所說的滯後感呢。

但我還是選擇了WPF,原因是,需要和其他WPF應用集成,我希望是清一色的WPF。潔癖??不是啦,主要的原因還是想偷懶,因為以前做過一個ImageEditor控件,其中的拖拽控件(就是上圖的藍色框)是可以重用的,至於剛才所說的弊端嘛,有辦法可以繞過去。

2, 基本思路

截圖前,先拷貝整個屏幕圖像到一個Image中,我們稱之為ScreenSnapshot, 然後用戶通過鼠標操作,確定一個矩形區域Rect,將Rect傳遞給函數BitmapSource Clip(Rect r) , Clip函數在ScreenSnapshot上截取Rect對於的那一部分圖像,並返回。

3, 如何截取屏幕圖像

WPF沒有內置的函數,但可以借用WinForm的Graphics來完成,其將圖像截取並保存在一個System.Drawing.Bitmap上,然後我們使用一個輔助函數將System.Drawing.Bitmap轉化為WPF版本的System.Media.Imaging.BitmapSource對象就可以了

public static Bitmap GetScreenSnapshot()
{
Rectangle rc
= SystemInformation.VirtualScreen;
var bitmap
= new Bitmap(rc.Width, rc.Height, PixelFormat.Format32bppArgb);

using (Graphics g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(rc.X, rc.Y,
0, 0, rc.Size, CopyPixelOperation.SourceCopy);
}

return bitmap;
}

public static BitmapSource ToBitmapSource(this Bitmap bmp)
{
BitmapSource returnSource;

try
{
returnSource
= Imaging.CreateBitmapSourceFromHBitmap(bmp.GetHbitmap(),IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
}
catch
{
returnSource
= null;
}

return returnSource;

}

4,如何繪制選擇框

所謂選擇框,就是用戶拖拽鼠標時顯示的那個框選線框,專業一點的術語叫rubber band

我最開始的做法是在OnRender函數中drawingContext.DrawingRectangle(....) ,對於單屏顯示器而言,效果還不錯,但在雙屏(1920x1200x2)上效果很差,明顯的滯後感,另外一位帥哥在“OnRender is not as cheap as OnPaint”中也說到了該問題,總結了下面一段話,讓人看了很傷心:

It is ok to use OnRender for things that won’t change, but what you render changes on MouseOver or SizeChanged, you’ll may be causing a layout perf problem for your app.
The way to avoid the OnRender tax is to predefine your UI in a template, then for the bits that change, use triggers within the template to change it.

不過,這個問題可以輕松繞過去:不就想畫一個框嗎?給你一個框(System.Windows.Shapes.Rectangle)便是。也就是這個框不是靠我們先前的DrawingContext繪制出來的,而是作為一個子控件添加進去的

至於何時去刷新選擇框的大小和位置嘛,你可以再用戶鼠標拖拽時立即更新,也可以定時更新,我選擇的後者,當然,不是自己寫定時器,而是才用了CompositionTarget.Rendering事件, 這是一個幀回調,這裏的幀就是FPS(frame per second)中的F,所以,這個刷得到多快,就取決於你計算機的FPS了

5, 如何實現遮罩和鏤空效果

遮罩很容易實現:在你要遮蓋的東西上放置一個半透明控件(比如canvas)就可以了,不過,不要使用讓被遮蓋物體半透明的方式來實現,比如窗口半透明了,窗口上的子控件也會半透明顯示,這不是我們所需要的,我們那個選擇框就不是半透的。

鏤空嘛,以前會有很學院派的想法,然需要鏤空的區域使用OpacityMask或者Xor畫刷刷一下不就OK了麽?No,太學院派了,費力不討好。OpacityMask效率很低啦。

實際情況是這樣的,如下圖, 用四個面板(圖中的綠色,橙色,淡藍,紫色)排列在一起,中間留個洞就可以了,那四個面板共同構成我們的半透明遮罩。

技術分享圖片

當用戶拖拽鼠標時,我們重新排列那四個遮罩面板來改變中間鏤空區域的大小和位置,用戶就會感覺真的像是在屏幕上畫了洞。

6, 如何根據用戶拖拽區域截圖

這就非常簡單了,同樣利用Graphics對象在先前的ScreenSnapshot上截取一部分就可以了

internal BitmapSource CopyFromScreenSnapshot(Rect region)
{
var sourceRect
= region.ToRectangle();
var destRect
= new Rectangle(0, 0, sourceRect.Width, sourceRect.Height);

if (screenSnapshot != null)
{
var bitmap
= new Bitmap(sourceRect.Width, sourceRect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
using (Graphics g = Graphics.FromImage(bitmap))
{
g.DrawImage(screenSnapshot, destRect, sourceRect, GraphicsUnit.Pixel);
}

return bitmap.ToBitmapSource();
}

return null;
}

7, 其他的

省著點用CompositionTarget.Rendering事件,也就是說,讓其回調函數的效率盡量的高,因為其會被不間斷地頻繁調用。

剛剛做完的一個屏幕截圖程序,分享一下