C#?wpf實現(xiàn)截屏框熱鍵截屏的示例代碼
前言
在《C# wpf 使用DockPanel實現(xiàn)截屏框》中我們實現(xiàn)了一個截屏框,接下來就要實現(xiàn)相應的截屏功能了。獲取截屏區(qū)域然后使用GDI+截屏,在這里不少的細節(jié)需要處理,比如響應熱鍵彈出截屏界面、點擊拖出截屏框、截屏區(qū)域任意反向拖動、處理不同dpi下的坐標位置等等。
一、實現(xiàn)步驟
1、響應熱鍵
我們直接使用win32 api的RegisterHotKey和UnregisterHotKey即可。在Window的SourceInitialized事件中注冊熱鍵,如下是注冊alt+d為熱鍵的示例代碼
[System.Runtime.InteropServices.DllImport("user32")]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint controlKey, uint virtualKey);
[System.Runtime.InteropServices.DllImport("user32")]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);HotKey是對RegisterHotKey、UnregisterHotKey做了封裝的對象,網(wǎng)上可以搜到此處略。
private void Window_SourceInitialized(object sender, EventArgs e)
{
//注冊alt+d熱鍵,0x44為d,其他虛擬鍵值請查看:https://learn.microsoft.com/zh-tw/windows/win32/inputdev/virtual-key-codes
HotKey k = new HotKey(this, HotKey.KeyFlags.MOD_ALT, 0x44);
k.OnHotKey += K_OnHotKey;
Visibility = Visibility.Collapsed;
}2、截屏顯示
(1)獲取屏幕區(qū)域
我們需要使用win32 api獲取屏幕區(qū)域,采用wpf的方法取得的屏幕分辨率是基于dpi的,就算是用PointToScreen進行轉換,在程序運行過程中改了系統(tǒng)dpi后依然會不準確,所以需要直接取得屏幕的實際像素分辨率,用于gdi+截屏。
const int DESKTOPVERTRES = 117;
const int DESKTOPHORZRES = 118;
[DllImport("gdi32.dll")]
static extern int GetDeviceCaps(
IntPtr hdc, // handle to DC
int nIndex // index of capability
);
[DllImport("user32.dll")]
static extern IntPtr GetDC(IntPtr ptr);
[DllImport("user32.dll", EntryPoint = "ReleaseDC")]
static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDc);
/// <summary>
/// 獲取真實設置的桌面分辨率大小
/// </summary>
static Size DESKTOP
{
get
{
IntPtr hdc = GetDC(IntPtr.Zero);
Size size = new Size();
size.Width = GetDeviceCaps(hdc, DESKTOPHORZRES);
size.Height = GetDeviceCaps(hdc, DESKTOPVERTRES);
ReleaseDC(IntPtr.Zero, hdc);
return size;
}
}(2)截取并顯示
利用上面步驟獲取到的截屏區(qū)域,結合《C# wpf 使用GDI+實現(xiàn)截屏》里的簡單截屏即完成。取得Bitmap對象后,參考我的另一篇文章《C# wpf Bitmap轉換成WriteableBitmap(BitmapSource)的方法》將其轉換為轉換成wpf對象,然后通過ImageBrush賦值為控件的Background即可以顯示在控件上。
//截屏并顯示到窗口
void Snapshot()
{
//獲取桌面實際分辨率,可以解決程序運行后修改dpi,截圖區(qū)域不正常的問題
var leftTop = new Point(0, 0);
var rightBottom = new Point(DESKTOP.Width, DESKTOP.Height);
var bitmap = Snapshot((int)leftTop.X, (int)leftTop.Y, (int)(rightBottom.X - leftTop.X), (int)(rightBottom.Y - leftTop.Y));
var bmp = BitmapToWriteableBitmap(bitmap);
bitmap.Dispose();
//顯示到窗口
grdGlobal.Background = new ImageBrush(bmp);
}3、自動捕獲窗口
qq和微信的截屏都有自動捕獲窗口功能,我們也可以自己實現(xiàn)這種功能。
(1)獲取系統(tǒng)所有窗口
通過win32 api可以枚舉系統(tǒng)所有窗口,我們需要將所有窗口的位置大小記錄下來,網(wǎng)上可以找到WindowList相關代碼此處略。
//獲取桌面所有窗口
_windows = WindowList.GetAllWindows();
IntPtr hwnd = new WindowInteropHelper(this).Handle;
//去除不可見窗口以及自己
_windows.RemoveAll((ele) => { return !ele.isVisible || ele.Handle == hwnd; });(2)根據(jù)鼠標位置搜索窗口
//窗口是以z順序排列的查找到第一個匹配的窗口即可
var screenPoint = grdGlobal.PointToScreen(point);
foreach (var window in _windows)
{
if (window.rect.Contains(screenPoint))
//獲取在鼠標所在區(qū)域的窗口
{
try
{
if (window.rect.Right > window.rect.Left && window.rect.Bottom > window.rect.Top)
//
{
var topLeft = grdGlobal.PointFromScreen(window.rect.TopLeft);
var bottomRight = grdGlobal.PointFromScreen(window.rect.BottomRight);
Thickness thickness = new Thickness(topLeft.X, topLeft.Y, grdGlobal.ActualWidth - bottomRight.X, grdGlobal.ActualHeight - bottomRight.Y);
//修正邊界
if (thickness.Left < 0) thickness.Left = 0;
if (thickness.Top < 0) thickness.Top = 0;
if (thickness.Right < 0) thickness.Right = 0;
if (thickness.Bottom < 0) thickness.Bottom = 0;
//將截屏框顯示在窗口位置
leftPanel.Width = thickness.Left;
topPanel.Height = thickness.Top;
rightPanel.Width = thickness.Right;
bottomPanel.Height = thickness.Bottom;
break;
}
}
catch { }
}
}(3)效果預覽

4、點擊拖出截屏框
出現(xiàn)截屏界面之后,參考qq或微信的實現(xiàn),第一次點擊是可以拖出截屏框框選的。如果是采樣繪制的方法很簡單,直接繪制矩形就可以了。但是基于控件要實現(xiàn)這個功能需要一定的技巧,在《C# wpf 使用DockPanel實現(xiàn)截屏框》的基礎上實現(xiàn)這個功能。
(1)移動到點擊位置
在鼠標按下事件或移動實現(xiàn)中
//將截屏框移動到點擊位置 leftPanel.Width = p.X; topPanel.Height = p.Y; rightPanel.Width = grdGlobal.ActualWidth - p.X; bottomPanel.Height = grdGlobal.ActualHeight - p.Y;
(2)模擬按下事件
接著上面的代碼,thumb為右下角拖動點。
//手動觸發(fā)截屏框滑塊拖動事件
MouseButtonEventArgs downEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left)
{ RoutedEvent = FrameworkElement.MouseLeftButtonDownEvent };
thumb.RaiseEvent(downEvent);(3)修正偏移
由于是模擬的點擊事件,可能會出現(xiàn)鼠標不在Thumb上的情況,此時需要對thumb位置進行修正,在Thumb的DragStarted事件中記錄偏移。
//滑塊需要的偏移量
Point? _thumbOffset;
var thumb = sender as FrameworkElement;
if (!new Rect(0, 0, thumb.ActualWidth, thumb.ActualHeight).Contains(new Point(e.HorizontalOffset, e.VerticalOffset)))
//鼠標起始位置超出了控件范圍,則記錄中心點偏移在拖動時修正
{
_thumbOffset = new Point(e.HorizontalOffset - thumb.ActualWidth / 2, e.VerticalOffset - thumb.ActualHeight / 2);
}在Thumb的DragDelta事件中添加修正邏輯
var horizontalChange = e.HorizontalChange;
var verticalChange = e.VerticalChange;
if (_thumbOffset != null)
//修正偏移
{
horizontalChange += _thumbOffset.Value.X;
verticalChange += _thumbOffset.Value.Y;
}(4)效果預覽

5、反向拖動
這一步不是必須的,但是有的話操作體驗會更好,比如qq和微信的截圖就支持反向拖動。如果我們使用gdi或gdi+繪制截屏框則天然支持反向拖動,因為RECT的大小可以為負數(shù)。但是基于控件則有一定的難度了,由于控件寬高不能為負數(shù),我們需要實現(xiàn)事件轉移機制,依然是在《C# wpf 使用DockPanel實現(xiàn)截屏框》的基礎上實現(xiàn)這個功能。
(1)判斷邊界
原本《C# wpf 使用DockPanel實現(xiàn)截屏框》的邏輯的Thumb到了邊界就不進行任何操作了,現(xiàn)在要拓展為到達邊界則進行事件轉移。橫向的Thumb
if (width >= 0)
{
leftPanel.Width = left >= 0 ? left : 0;
rightPanel.Width = right >= 0 ? right : 0;
}
else{
//此處將事件轉移到反方向的Thumb
}縱向的Thumb
if (height >= 0)
{
topPanel.Height = top >= 0 ? top : 0;
bottomPanel.Height = bottom >= 0 ? bottom : 0;
}
else
{
//此處將事件轉移到反方向的Thumb
}(2)事件轉移
//當前的Thumb觸發(fā)鼠標彈起事件,結束拖動
MouseButtonEventArgs upEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left)
{ RoutedEvent = FrameworkElement.MouseLeftButtonUpEvent };
thumb.RaiseEvent(upEvent);
//反方向的Thumb觸發(fā)鼠標按下事件,開始拖動
MouseButtonEventArgs downEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left)
{ RoutedEvent = FrameworkElement.MouseLeftButtonDownEvent };
t.RaiseEvent(downEvent);(3)修正邊界
完成上述兩步之后已經(jīng)可以做到反向拖動了,但是會有個問題,當多動過快的時截屏框的位置會發(fā)生移動,要解決這個問題則需要在事件轉移時修正邊界位置,即使兩條邊重合。橫向的Thumb
if (thumb.HorizontalAlignment == HorizontalAlignment.Left)
//從左到右轉移的修正
{
leftPanel.Width = grdGlobal.ActualWidth - rightPanel.Width;
}
else
//從右到左轉移的修正
{
rightPanel.Width = grdGlobal.ActualWidth - leftPanel.Width;
}縱向的Thumb
if (thumb.VerticalAlignment == VerticalAlignment.Top)
//從上到下轉移的修正
{
topPanel.Height = grdGlobal.ActualHeight - bottomPanel.Height;
}
else
//從下到上轉移的修正
{
bottomPanel.Height = grdGlobal.ActualHeight - topPanel.Height;
}(4)效果預覽

6、截取圖片
由于前面截取是整個桌面的圖像,保存時需要根據(jù)截屏框截取畫面,我們使用WriteableBitmap對象就可以實現(xiàn)。
//獲取截屏框的圖片
WriteableBitmap GetClipImage()
{
var bursh = grdGlobal.Background as ImageBrush;
if (bursh != null)
{
//裁剪
//全屏圖片
var screenWb = bursh.ImageSource as WriteableBitmap;
//獲取截取區(qū)域
var leftTop = clipRect.PointToScreen(new Point(0, 0));
var rightBottom = clipRect.PointToScreen(new Point(clipRect.ActualWidth, clipRect.ActualHeight));
var rect = new Int32Rect((int)leftTop.X, (int)leftTop.Y, (int)(rightBottom.X - leftTop.X), (int)(rightBottom.Y - leftTop.Y));
//創(chuàng)建截取圖片對象
var wb = new WriteableBitmap(rect.Width, rect.Height, 0, 0, screenWb.Format, null);
//寫入截取區(qū)域數(shù)據(jù)
wb.WritePixels(rect, screenWb.BackBuffer, screenWb.PixelHeight * screenWb.BackBufferStride, screenWb.BackBufferStride, 0, 0);
return wb;
}
return null;
}7、設置粘貼板
直接使用Clipboard.SetImage即可,參數(shù)類型為BitmapSource,是WriteableBitmap的基類。
Clipboard.SetImage(GetClipImage());
二、關于dpi
1、適配不同dpi
有處理dpi不同的情況,在任意dpi下都能正常截圖。
2、不支持dpi實時修改
(1)現(xiàn)象
程序啟動后實時修改dpi,截屏顯示的畫面會模糊,主要原因是不同api之間的dpi計算不統(tǒng)一。系統(tǒng)dpi實時修改后wpf界面會響應oloaded自動調(diào)整大小,但部分程序內(nèi)部的dpi(比如getWindowRect)是不會變化的,尤其是渲染圖片依然按照程序啟動時的dpi去計算,所以會進行縮放,顯示的畫面必然模糊。
這里舉一個具體的例子流程如下:
win11 分辨率1920x1080
1、初始系統(tǒng)dpi為120(1.25倍)
2、程序啟動
3、程序dpi為120
5、全屏窗口大小1536x864,通過winapi獲取則是1920x1080,截屏1920x1080顯示,截屏畫面無損
6、系統(tǒng)dpi設置為96(1倍)
7、此時程序dpi為120
8、全屏窗口大小1920x1080,通過winapi獲取則是2400x1350,截屏1920x1080顯示,截屏畫面模糊。
按像素點繪制,畫面顯示在左上角無法充滿窗口。
(2)嘗試的解決方案
筆者采樣了多種方式嘗試解決
1、提前縮放圖片再顯示。
2、參考微軟解決dpi問題的方法。
3、使用gdi+的graphics直接通過hdc以像素點為單位繪制。
4、使用gdi的bitblt進行hdc拷貝。
以上方法都沒效果畫面依然模糊。
3、建議
需要支持dpi實時改變,可以將截圖功能作為單獨的程序,響應熱鍵后再啟動。
三、效果預覽
1、截屏粘貼到qq

2、截屏保存到文件

總結
本文介紹了wpf截屏框熱鍵截屏的方法。需要實現(xiàn)的功能還是比較多的,而且有些功能難度也不小,幾經(jīng)嘗試才找到合適的實現(xiàn)方法,至于實時改變dpi的模糊的問題,這個目前的結論是無法解決,這并不是wpf的局限,用c++ mfc也不行,除非存在一個設置程序全局dpi的winapi接口筆者沒有發(fā)現(xiàn)。所以這個問題目前只能通過獨立程序啟動解決。但是總的來說實現(xiàn)的效果是很不錯的,尤其是反向拖動,通過事件轉移的方式實現(xiàn),界面操作還是很流暢。
到此這篇關于C# wpf實現(xiàn)截屏框熱鍵截屏的示例代碼的文章就介紹到這了,更多相關C# wpf截屏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
C#通過創(chuàng)建Windows服務啟動程序的方法詳解
這篇文章主要介紹了C#通過創(chuàng)建Windows服務啟動程序的方法,較為詳細的分析了C#創(chuàng)建Windows服務應用程序的步驟與相關注意事項,需要的朋友可以參考下2016-06-06
C# Lambda表達式及Lambda表達式樹的創(chuàng)建過程
這篇文章主要介紹了C# Lambda表達式及Lambda表達式樹的創(chuàng)建過程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02

