C#?wpf實(shí)現(xiàn)截屏框熱鍵截屏的示例代碼
前言
在《C# wpf 使用DockPanel實(shí)現(xiàn)截屏框》中我們實(shí)現(xiàn)了一個(gè)截屏框,接下來就要實(shí)現(xiàn)相應(yīng)的截屏功能了。獲取截屏區(qū)域然后使用GDI+截屏,在這里不少的細(xì)節(jié)需要處理,比如響應(yīng)熱鍵彈出截屏界面、點(diǎn)擊拖出截屏框、截屏區(qū)域任意反向拖動(dòng)、處理不同dpi下的坐標(biāo)位置等等。
一、實(shí)現(xiàn)步驟
1、響應(yīng)熱鍵
我們直接使用win32 api的RegisterHotKey和UnregisterHotKey即可。在Window的SourceInitialized事件中注冊(cè)熱鍵,如下是注冊(cè)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是對(duì)RegisterHotKey、UnregisterHotKey做了封裝的對(duì)象,網(wǎng)上可以搜到此處略。
private void Window_SourceInitialized(object sender, EventArgs e) { //注冊(cè)alt+d熱鍵,0x44為d,其他虛擬鍵值請(qǐng)查看: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進(jìn)行轉(zhuǎn)換,在程序運(yùn)行過程中改了系統(tǒng)dpi后依然會(huì)不準(zhǔn)確,所以需要直接取得屏幕的實(shí)際像素分辨率,用于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> /// 獲取真實(shí)設(shè)置的桌面分辨率大小 /// </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ū)域,結(jié)合《C# wpf 使用GDI+實(shí)現(xiàn)截屏》里的簡(jiǎn)單截屏即完成。取得Bitmap對(duì)象后,參考我的另一篇文章《C# wpf Bitmap轉(zhuǎn)換成WriteableBitmap(BitmapSource)的方法》將其轉(zhuǎn)換為轉(zhuǎn)換成wpf對(duì)象,然后通過ImageBrush賦值為控件的Background即可以顯示在控件上。
//截屏并顯示到窗口 void Snapshot() { //獲取桌面實(shí)際分辨率,可以解決程序運(yùn)行后修改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、自動(dòng)捕獲窗口
qq和微信的截屏都有自動(dòng)捕獲窗口功能,我們也可以自己實(shí)現(xiàn)這種功能。
(1)獲取系統(tǒng)所有窗口
通過win32 api可以枚舉系統(tǒng)所有窗口,我們需要將所有窗口的位置大小記錄下來,網(wǎng)上可以找到WindowList相關(guān)代碼此處略。
//獲取桌面所有窗口 _windows = WindowList.GetAllWindows(); IntPtr hwnd = new WindowInteropHelper(this).Handle; //去除不可見窗口以及自己 _windows.RemoveAll((ele) => { return !ele.isVisible || ele.Handle == hwnd; });
(2)根據(jù)鼠標(biāo)位置搜索窗口
//窗口是以z順序排列的查找到第一個(gè)匹配的窗口即可 var screenPoint = grdGlobal.PointToScreen(point); foreach (var window in _windows) { if (window.rect.Contains(screenPoint)) //獲取在鼠標(biāo)所在區(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)效果預(yù)覽
4、點(diǎn)擊拖出截屏框
出現(xiàn)截屏界面之后,參考qq或微信的實(shí)現(xiàn),第一次點(diǎn)擊是可以拖出截屏框框選的。如果是采樣繪制的方法很簡(jiǎn)單,直接繪制矩形就可以了。但是基于控件要實(shí)現(xiàn)這個(gè)功能需要一定的技巧,在《C# wpf 使用DockPanel實(shí)現(xiàn)截屏框》的基礎(chǔ)上實(shí)現(xiàn)這個(gè)功能。
(1)移動(dòng)到點(diǎn)擊位置
在鼠標(biāo)按下事件或移動(dòng)實(shí)現(xiàn)中
//將截屏框移動(dòng)到點(diǎn)擊位置 leftPanel.Width = p.X; topPanel.Height = p.Y; rightPanel.Width = grdGlobal.ActualWidth - p.X; bottomPanel.Height = grdGlobal.ActualHeight - p.Y;
(2)模擬按下事件
接著上面的代碼,thumb為右下角拖動(dòng)點(diǎn)。
//手動(dòng)觸發(fā)截屏框滑塊拖動(dòng)事件 MouseButtonEventArgs downEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left) { RoutedEvent = FrameworkElement.MouseLeftButtonDownEvent }; thumb.RaiseEvent(downEvent);
(3)修正偏移
由于是模擬的點(diǎn)擊事件,可能會(huì)出現(xiàn)鼠標(biāo)不在Thumb上的情況,此時(shí)需要對(duì)thumb位置進(jìn)行修正,在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))) //鼠標(biāo)起始位置超出了控件范圍,則記錄中心點(diǎn)偏移在拖動(dòng)時(shí)修正 { _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)效果預(yù)覽
5、反向拖動(dòng)
這一步不是必須的,但是有的話操作體驗(yàn)會(huì)更好,比如qq和微信的截圖就支持反向拖動(dòng)。如果我們使用gdi或gdi+繪制截屏框則天然支持反向拖動(dòng),因?yàn)镽ECT的大小可以為負(fù)數(shù)。但是基于控件則有一定的難度了,由于控件寬高不能為負(fù)數(shù),我們需要實(shí)現(xiàn)事件轉(zhuǎn)移機(jī)制,依然是在《C# wpf 使用DockPanel實(shí)現(xiàn)截屏框》的基礎(chǔ)上實(shí)現(xiàn)這個(gè)功能。
(1)判斷邊界
原本《C# wpf 使用DockPanel實(shí)現(xiàn)截屏框》的邏輯的Thumb到了邊界就不進(jìn)行任何操作了,現(xiàn)在要拓展為到達(dá)邊界則進(jìn)行事件轉(zhuǎn)移。橫向的Thumb
if (width >= 0) { leftPanel.Width = left >= 0 ? left : 0; rightPanel.Width = right >= 0 ? right : 0; } else{ //此處將事件轉(zhuǎn)移到反方向的Thumb }
縱向的Thumb
if (height >= 0) { topPanel.Height = top >= 0 ? top : 0; bottomPanel.Height = bottom >= 0 ? bottom : 0; } else { //此處將事件轉(zhuǎn)移到反方向的Thumb }
(2)事件轉(zhuǎn)移
//當(dāng)前的Thumb觸發(fā)鼠標(biāo)彈起事件,結(jié)束拖動(dòng) MouseButtonEventArgs upEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left) { RoutedEvent = FrameworkElement.MouseLeftButtonUpEvent }; thumb.RaiseEvent(upEvent); //反方向的Thumb觸發(fā)鼠標(biāo)按下事件,開始拖動(dòng) MouseButtonEventArgs downEvent = new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, MouseButton.Left) { RoutedEvent = FrameworkElement.MouseLeftButtonDownEvent }; t.RaiseEvent(downEvent);
(3)修正邊界
完成上述兩步之后已經(jīng)可以做到反向拖動(dòng)了,但是會(huì)有個(gè)問題,當(dāng)多動(dòng)過快的時(shí)截屏框的位置會(huì)發(fā)生移動(dòng),要解決這個(gè)問題則需要在事件轉(zhuǎn)移時(shí)修正邊界位置,即使兩條邊重合。橫向的Thumb
if (thumb.HorizontalAlignment == HorizontalAlignment.Left) //從左到右轉(zhuǎn)移的修正 { leftPanel.Width = grdGlobal.ActualWidth - rightPanel.Width; } else //從右到左轉(zhuǎn)移的修正 { rightPanel.Width = grdGlobal.ActualWidth - leftPanel.Width; }
縱向的Thumb
if (thumb.VerticalAlignment == VerticalAlignment.Top) //從上到下轉(zhuǎn)移的修正 { topPanel.Height = grdGlobal.ActualHeight - bottomPanel.Height; } else //從下到上轉(zhuǎn)移的修正 { bottomPanel.Height = grdGlobal.ActualHeight - topPanel.Height; }
(4)效果預(yù)覽
6、截取圖片
由于前面截取是整個(gè)桌面的圖像,保存時(shí)需要根據(jù)截屏框截取畫面,我們使用WriteableBitmap對(duì)象就可以實(shí)現(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)建截取圖片對(duì)象 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、設(shè)置粘貼板
直接使用Clipboard.SetImage即可,參數(shù)類型為BitmapSource,是WriteableBitmap的基類。
Clipboard.SetImage(GetClipImage());
二、關(guān)于dpi
1、適配不同dpi
有處理dpi不同的情況,在任意dpi下都能正常截圖。
2、不支持dpi實(shí)時(shí)修改
(1)現(xiàn)象
程序啟動(dòng)后實(shí)時(shí)修改dpi,截屏顯示的畫面會(huì)模糊,主要原因是不同api之間的dpi計(jì)算不統(tǒng)一。系統(tǒng)dpi實(shí)時(shí)修改后wpf界面會(huì)響應(yīng)oloaded自動(dòng)調(diào)整大小,但部分程序內(nèi)部的dpi(比如getWindowRect)是不會(huì)變化的,尤其是渲染圖片依然按照程序啟動(dòng)時(shí)的dpi去計(jì)算,所以會(huì)進(jìn)行縮放,顯示的畫面必然模糊。
這里舉一個(gè)具體的例子流程如下:
win11 分辨率1920x1080
1、初始系統(tǒng)dpi為120(1.25倍)
2、程序啟動(dòng)
3、程序dpi為120
5、全屏窗口大小1536x864,通過winapi獲取則是1920x1080,截屏1920x1080顯示,截屏畫面無損
6、系統(tǒng)dpi設(shè)置為96(1倍)
7、此時(shí)程序dpi為120
8、全屏窗口大小1920x1080,通過winapi獲取則是2400x1350,截屏1920x1080顯示,截屏畫面模糊。
按像素點(diǎn)繪制,畫面顯示在左上角無法充滿窗口。
(2)嘗試的解決方案
筆者采樣了多種方式嘗試解決
1、提前縮放圖片再顯示。
2、參考微軟解決dpi問題的方法。
3、使用gdi+的graphics直接通過hdc以像素點(diǎn)為單位繪制。
4、使用gdi的bitblt進(jìn)行hdc拷貝。
以上方法都沒效果畫面依然模糊。
3、建議
需要支持dpi實(shí)時(shí)改變,可以將截圖功能作為單獨(dú)的程序,響應(yīng)熱鍵后再啟動(dòng)。
三、效果預(yù)覽
1、截屏粘貼到qq
2、截屏保存到文件
總結(jié)
本文介紹了wpf截屏框熱鍵截屏的方法。需要實(shí)現(xiàn)的功能還是比較多的,而且有些功能難度也不小,幾經(jīng)嘗試才找到合適的實(shí)現(xiàn)方法,至于實(shí)時(shí)改變dpi的模糊的問題,這個(gè)目前的結(jié)論是無法解決,這并不是wpf的局限,用c++ mfc也不行,除非存在一個(gè)設(shè)置程序全局dpi的winapi接口筆者沒有發(fā)現(xiàn)。所以這個(gè)問題目前只能通過獨(dú)立程序啟動(dòng)解決。但是總的來說實(shí)現(xiàn)的效果是很不錯(cuò)的,尤其是反向拖動(dòng),通過事件轉(zhuǎn)移的方式實(shí)現(xiàn),界面操作還是很流暢。
到此這篇關(guān)于C# wpf實(shí)現(xiàn)截屏框熱鍵截屏的示例代碼的文章就介紹到這了,更多相關(guān)C# wpf截屏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#檢測(cè)是否有危險(xiǎn)字符的SQL字符串過濾方法
這篇文章主要介紹了C#檢測(cè)是否有危險(xiǎn)字符的SQL字符串過濾方法,功能非常實(shí)用,對(duì)于程序設(shè)計(jì)的安全來說至關(guān)重要!需要的朋友可以參考下2014-07-07C#通過創(chuàng)建Windows服務(wù)啟動(dòng)程序的方法詳解
這篇文章主要介紹了C#通過創(chuàng)建Windows服務(wù)啟動(dòng)程序的方法,較為詳細(xì)的分析了C#創(chuàng)建Windows服務(wù)應(yīng)用程序的步驟與相關(guān)注意事項(xiàng),需要的朋友可以參考下2016-06-06C# 實(shí)現(xiàn)Eval(字符串表達(dá)式)的三種方法
這篇文章主要介紹了C# 實(shí)現(xiàn)Eval(字符串表達(dá)式)的三種方法,幫助大家更好的理解和學(xué)習(xí)使用c#,感興趣的朋友可以了解下2021-04-04C# Lambda表達(dá)式及Lambda表達(dá)式樹的創(chuàng)建過程
這篇文章主要介紹了C# Lambda表達(dá)式及Lambda表達(dá)式樹的創(chuàng)建過程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02C#中枚舉類型和radiobox關(guān)聯(lián)操作的方法
這篇文章主要介紹了C#中枚舉類型和radiobox關(guān)聯(lián)操作的方法,實(shí)例分析了C#中枚舉類型及與控件關(guān)聯(lián)操作的相關(guān)技巧,需要的朋友可以參考下2015-04-04c#判斷網(wǎng)絡(luò)連接狀態(tài)的示例分享
這篇文章主要介紹了使用c#判斷網(wǎng)絡(luò)連接狀態(tài)的示例,需要的朋友可以參考下2014-02-02C#中參數(shù)個(gè)數(shù)可變的方法實(shí)例分析
這篇文章主要介紹了C#中參數(shù)個(gè)數(shù)可變的方法,以一個(gè)簡(jiǎn)單實(shí)例分析了C#中參數(shù)個(gè)數(shù)可變的方法,主要是使用params關(guān)鍵字來實(shí)現(xiàn)的,是C#編程中比較實(shí)用的技巧,需要的朋友可以參考下2014-11-11c#多線程中Lock()關(guān)鍵字的用法小結(jié)
本篇文章主要是對(duì)c#多線程中Lock()關(guān)鍵字的用法進(jìn)行了詳細(xì)的總結(jié)介紹,需要的朋友可以過來參考下,希望對(duì)大家有所幫助2014-01-01