C#?wpf實現截屏框熱鍵截屏的示例代碼
前言
在《C# wpf 使用DockPanel實現截屏框》中我們實現了一個截屏框,接下來就要實現相應的截屏功能了。獲取截屏區(qū)域然后使用GDI+截屏,在這里不少的細節(jié)需要處理,比如響應熱鍵彈出截屏界面、點擊拖出截屏框、截屏區(qū)域任意反向拖動、處理不同dpi下的坐標位置等等。
一、實現步驟
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做了封裝的對象,網上可以搜到此處略。
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+實現截屏》里的簡單截屏即完成。取得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和微信的截屏都有自動捕獲窗口功能,我們也可以自己實現這種功能。
(1)獲取系統(tǒng)所有窗口
通過win32 api可以枚舉系統(tǒng)所有窗口,我們需要將所有窗口的位置大小記錄下來,網上可以找到WindowList相關代碼此處略。
//獲取桌面所有窗口 _windows = WindowList.GetAllWindows(); IntPtr hwnd = new WindowInteropHelper(this).Handle; //去除不可見窗口以及自己 _windows.RemoveAll((ele) => { return !ele.isVisible || ele.Handle == hwnd; });
(2)根據鼠標位置搜索窗口
//窗口是以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、點擊拖出截屏框
出現截屏界面之后,參考qq或微信的實現,第一次點擊是可以拖出截屏框框選的。如果是采樣繪制的方法很簡單,直接繪制矩形就可以了。但是基于控件要實現這個功能需要一定的技巧,在《C# wpf 使用DockPanel實現截屏框》的基礎上實現這個功能。
(1)移動到點擊位置
在鼠標按下事件或移動實現中
//將截屏框移動到點擊位置 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)修正偏移
由于是模擬的點擊事件,可能會出現鼠標不在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的大小可以為負數。但是基于控件則有一定的難度了,由于控件寬高不能為負數,我們需要實現事件轉移機制,依然是在《C# wpf 使用DockPanel實現截屏框》的基礎上實現這個功能。
(1)判斷邊界
原本《C# wpf 使用DockPanel實現截屏框》的邏輯的Thumb到了邊界就不進行任何操作了,現在要拓展為到達邊界則進行事件轉移。橫向的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)修正邊界
完成上述兩步之后已經可以做到反向拖動了,但是會有個問題,當多動過快的時截屏框的位置會發(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、截取圖片
由于前面截取是整個桌面的圖像,保存時需要根據截屏框截取畫面,我們使用WriteableBitmap對象就可以實現。
//獲取截屏框的圖片 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ū)域數據 wb.WritePixels(rect, screenWb.BackBuffer, screenWb.PixelHeight * screenWb.BackBufferStride, screenWb.BackBufferStride, 0, 0); return wb; } return null; }
7、設置粘貼板
直接使用Clipboard.SetImage即可,參數類型為BitmapSource,是WriteableBitmap的基類。
Clipboard.SetImage(GetClipImage());
二、關于dpi
1、適配不同dpi
有處理dpi不同的情況,在任意dpi下都能正常截圖。
2、不支持dpi實時修改
(1)現象
程序啟動后實時修改dpi,截屏顯示的畫面會模糊,主要原因是不同api之間的dpi計算不統(tǒng)一。系統(tǒng)dpi實時修改后wpf界面會響應oloaded自動調整大小,但部分程序內部的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截屏框熱鍵截屏的方法。需要實現的功能還是比較多的,而且有些功能難度也不小,幾經嘗試才找到合適的實現方法,至于實時改變dpi的模糊的問題,這個目前的結論是無法解決,這并不是wpf的局限,用c++ mfc也不行,除非存在一個設置程序全局dpi的winapi接口筆者沒有發(fā)現。所以這個問題目前只能通過獨立程序啟動解決。但是總的來說實現的效果是很不錯的,尤其是反向拖動,通過事件轉移的方式實現,界面操作還是很流暢。
到此這篇關于C# wpf實現截屏框熱鍵截屏的示例代碼的文章就介紹到這了,更多相關C# wpf截屏內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
C#通過創(chuàng)建Windows服務啟動程序的方法詳解
這篇文章主要介紹了C#通過創(chuàng)建Windows服務啟動程序的方法,較為詳細的分析了C#創(chuàng)建Windows服務應用程序的步驟與相關注意事項,需要的朋友可以參考下2016-06-06C# Lambda表達式及Lambda表達式樹的創(chuàng)建過程
這篇文章主要介紹了C# Lambda表達式及Lambda表達式樹的創(chuàng)建過程,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02