WPF + Winform 解決管理員許可權下無法拖放檔案的問題

幾個酒菜成這樣發表於2022-02-22

wpf,winform混合解決管理員許可權無法拖放檔案的問題

學習自:

https://zhuanlan.zhihu.com/p/343369663

https://zhuanlan.zhihu.com/p/48735364?from_voters_page=true

``

本文記錄我解決這個問題的心路歷程。總體過程是先知道了第一個文件,很好用。但融入專案的時候發現了一個BUG,而這個BUG卡了好幾天怎麼也解決不掉。後續準備放棄,又看到了第二個文件,解決了我的困惑,找到了問題關鍵。解決了BUG。

問題描述

深層次的原因,上述兩個連結都提到了。簡單來說就是,如果我們編寫的應用程式使用了控制元件的拖放功能(DragDrop),當我們使用“管理員許可權”啟動這個應用程式時。拖放功能就失效了。

解決思路

利用Windows API中的一些方法與訊息處理機制,可以越過系統預設的安全機制進行操作,如圖:

關鍵問題

如何在wpf進行預winform窗體、控制元件進行訊息互動

using System.Windows.Forms.Integration;

WindowsFormsHost.EnableWindowsFormsInterop();

一些輔助類與API方法

/*
     * 函式原型
     * LONG SetWindowLong(
        [in] HWND hWnd,
        [in] int  nIndex,
        [in] LONG dwNewLong
        );
        注:這個函式就使用官方文件宣告不要使用的GWL_HWNDPARENT(-8)來更改視窗的父子級。但這裡使用了也沒出啥問題

     * BOOL WINAPI ChangeWindowMessageFilterEx(
        __in HWND hWnd,
        __in UINT message,
        __in DWORD action,
        __inout_opt PCHANGEFILTERSTRUCT pChangeFilterStruct
        );
        注:DWORD 雙字 就是4個位元組。每個位元組8位,就是無符號32位。在這個C#裡,應該就是uint

     * BOOL ChangeWindowMessageFilter(
        __in UINT  message,
        __in DWORD dwFlag
        );
        注:不推薦使用,主要用於相容舊機器。現在都應該使用上面的ChangeWindowMessageFilterEx

     * UINT DragQueryFileA(
        [in] HDROP hDrop,
        [in] UINT iFile,
        [out] LPSTR lpszFile,
        UINT cch
        );
        注:關於引數iFile。
        如果此引數的值為 0xFFFFFFFF,則 DragQueryFile返回放置檔案的數量。
        如果此引數的值介於0和放置檔案總數之間,則DragQueryFile將具有相應值的檔名複製到lpszFile引數指向的緩衝區。
        關於引數lpszFile:
        當函式返回時,用於接收放置檔名字的快取區地址。再通俗一點說,函式成功後,這個引數存的的就是放置檔案的Name。
        此檔名是一個以空字元結尾的字串。
        如果此引數為NULL,則 DragQueryFile返回此緩衝區所需的大小(以字元為單位)
        關於返回值:
        返回非0值,即標識呼叫成功。
        當函式將檔名複製到緩衝區時,返回值是複製的字元數,不包括終止空字元。通俗講就是:當lpszFile接收到了檔名,返回值就是這個檔名的字元數,不包括終止空字元。
        如果索引值為 0xFFFFFFFF,則返回值為放置檔案的數量。請注意,索引變數本身返回不變,因此保持為 0xFFFFFFFF。
        如果索引值介於0和放置檔案總數之間,並且lpszFile緩衝區地址為NULL,則返回值是緩衝區所需的大小(以字元為單位),不包括終止的空字元。

     * WM_DROPFILES 訊息 0x0233
        當使用者在已將自身註冊為放置檔案的接收者的應用程式的視窗上放置檔案時傳送。
     *  WM_COPYDATA 訊息 0x004A
        應用程式傳送 WM _ COPYDATA 訊息以將資料傳遞到其他應用程式。
     * WM_COPYGLOBALDATA 訊息 0x0049
        從 Win3.1 開始可能與 WM_COPYDATA 有關,現在很可能從 MSDN 中刪除。每個於此相關的功能還是帶著這個訊息。

     * Marshal
        提供了一個方法集合,這些方法用於分配非託管記憶體、複製非託管記憶體塊、將託管型別轉換為非託管型別,此外還提供了在與非託管程式碼互動時使用的其他雜項方法。
        繼承 Object -> Marshal
        這裡用到的:
        Marshal.GetLastWin32Error() 返回由上一個非託管函式返回的錯誤程式碼,該函式是使用設定了 SetLastError 標誌的平臺呼叫來的。
        Marshal.SizeOf(Type) 返回非託管型別的大小(以位元組為單位)。     

     * Environment.OSVersion 可以判斷當前作業系統
     
     * MarshalAsAttribute 類
        指示如何在託管程式碼與非託管程式碼之間封送資料。
        繼承 Object -> Attribute -> MarshalAsAttribute
     * UnmanagedType 列舉
        繼承 Object -> ValueType -> Enum -> UnmanagedType
        指定如何將引數或欄位封送到非託管程式碼。

     *Application.AddMessageFilter(System.Windows.Forms.IMessageFilter value)
        value :準備載入的,介面IMessageFilter的實現
        新增訊息過濾器以在 Windows 訊息路由到其目的地時對其進行監控。
     */

關鍵的介面

/*
IMessageFilter:
定義訊息篩選器介面。
用它來接收,我們篩選我們所需的訊息,進行處理
IDisposable:
定義一種釋放分配的資源的方法。
用它來釋放我們自定義的接收,處理拖放訊息的類
*/
public class ElevatedDragDropManagerRevise : IMessageFilter, IDisposable

終極大坑

如圖②,③是原始碼中的窗體。顯示,隱藏,接收拖放檔案都沒有問題。但當我增加了一個①的選單檔案(即從①開啟②)時。第一次開啟②③,功能正常。關閉②③再重新開啟,拖放功能就失效了。通過把窗體Handle值列印出來可知,第二次開啟的窗體其觸發的拖放事件繫結的還是第一次窗體的Handle值。

通過查閱文件,我一直認為是SetWindowLong的問題。共有2個依據

/*
1.您不能使用GWL_HWNDPARENT索引呼叫SetWindowLong來更改子視窗的父級。而是使用SetParent函式。
2.某些視窗資料被快取,因此您使用SetWindowLong所做的更改在呼叫SetWindowPos函式之前不會生效。具體來說,如果您更改任何框架樣式,則必須使用SWP_FRAMECHANGED標誌呼叫SetWindowPos才能正確更新快取。
*/

然而嘗試後,都不能解決問題。

後續,我嘗試儲存一個全域性的固定的窗體來儲存這個拖放的視窗。然而更詭異的情況發生了。當我關閉父窗體後,輸出儲存的全域性子窗體的Handle時(沒有Show),子窗體會顯示出來,但子窗體上的控制元件不會顯示。而且,雖然全域性儲存了窗體,但窗體的Handle值在Closed後改變了。而且只有新建時關閉會改變,後續重新開啟後不會改變。

出現這樣的情況後,我一直認為是SetWindowLong的繫結沒有更新或自定義事件繫結沒有更新。然而嘗試了各種方法,如使用其他API,父窗體的ClosedDisposed,子窗體自身的ClosedDisposed。父窗體控制子窗體,甚至子窗體通過SendMessage去控制父窗體等等等等,都不好使。大體的報錯範圍是,在父窗體中進行釋放,可以新建接收拖放的視窗,但視窗控制程式碼與接收放置的控制程式碼匹配不上,觸發不了後續事件。在子窗體中進行釋放,父窗體中的操作會直接報錯“無法訪問已釋放的物件”。

最後,當我放棄第一個連結的方法去尋找其他解決辦法時,看到了第二個連結。發現我沒有釋放訊息過濾器,也就是

Application.RemoveMessageFilter(this);

當我把這個加到我的工程中,一切問題就解決了。不得不讓我感嘆沒文化可真可怕,就一句程式碼的問題能卡我2,3天。

一些小坑

SetWindowLong的使用

這個是在尋找解決辦法的過程中發現的。因為此前我對Windows API毫無瞭解。所以一直以為SetWindowLong是這個繫結關係的關鍵,不得不說這讓我走了好大的彎路。當我決心要解決這個問題,並開始逐句研究程式碼後。我發現這句話根本沒啥作用,就算不用,也可以用父窗體中的子窗體例項來實現它的功能。

如上可知:我們使用的是SetWindowLong 標識“-8“ 的功能。也就是宣告窗體的父子關係。但宣告瞭父子關係之後會有什麼樣的特性呢?經過測試我在此工程中只發現以下2個功能:

  1. 繫結父子關係後,關閉、最小化父窗體,子窗體會跟著一起最小化。
  2. 繫結父子關係後,在Show之前設定子窗體的Left、Top可以生效。

我不保證正確,但我確實就發現了這2個功能。而且還蘊含著一個非常坑的問題。

即:使用SetWindowLong,或官方建議的SetParent繫結父子關係後,關閉父窗體,無法觸發子窗體的Closeing,Closed事件。

使用MessageBox後程式直接卡死

這個問題出現在連結2的程式碼中,在控制元件的DragEnter事件中輸出MessageBox就會直接卡死。可以解決,但我很費解是什麼原因。

Close(),Dispose()與 =null

先說Close()Dispose()。這個要按照具體情況分析,可以給一個大致的分析方向。Close的含義大都是開啟關閉,而Dispose的含義大都是建立釋放。有時一樣有時不一樣。比如這裡的窗體,Close後就會Dispose,不新建就無法再顯示窗體。再比如比較常用的資料庫連線SqlConnection connconn.Close()後,即使不新建,也可以再次使用conn.Open()繼續使用。

當然在窗體這裡還有一個重要區別,就是Dispose()無法觸發ClosingClosed事件。

=null是另一種情況。可以這麼理解,CloseDispose處理的都是記憶體上的例項,而=null處理的是指向記憶體的指標。也就是說,當你對一個物件執行Close()Dispose()後,雖然該物件的記憶體被釋放,但它不為null,該物件依舊指向該記憶體。只有當你將它設為null,切斷它與記憶體的關係。它才會變為null

這也就上述“無法訪問已釋放的物件”問題的根源。子窗體經由父窗體建立,父窗體保留著子窗體的例項進行操縱。此時子窗體關閉,釋放了自身。但父窗體中的例項依舊指向原來的記憶體,不為null。但通過這個例項進行操作就會報錯,因為該例項實際上已被釋放。

IsDisposed

有一種冷叫你媽覺得你冷,有一種累叫微軟覺得你累

緊接上一條,微軟已經把這種情況的判斷標識做好了,就是IsDisposed。加入判斷條件即可,非常方便。

再提一嘴,一般看到這都會想:“那我直接將物件設為null,也沒有釋放過,會不會佔用記憶體越積越多,影響效率”。這個完全不用擔心,因為C#有全自動的垃圾處理機制(GC)來解決這些問題。但通過這個問題的逐步排查,我也認為多瞭解一些記憶體相關的知識,對於自定義類進行主動的釋放處理不失為一種好習慣。我要是之前瞭解過這些知識,也不會因為這麼1,2行程式碼的問題卡了2,3天。

父子窗體

瞭解了3種方式

  • SetSetWindowLong 標識-8
  • SetParent
  • MdiParent

具體的特性沒有過多瞭解。用上再說吧。

程式碼

Menu是目錄,TestData是用來輸出全域性Handle的類。

MainWindow.xaml、AppManagerForm.cs、ElevatedDragDropManager.cs、_1_Form、_1、_2、_3的Window都是連結1及相關測試程式碼

FileDragDropmanager.cs、FileDragDropForm、4_AnotherWindow.xaml是連結2的相關測試程式碼

程式碼裡也有註釋。我個人推薦第二種方法。

寫了半天,程式碼忘放了……
專案地址:[https://github.com/TwdcbiG/Demo/tree/main/CSharp/WPF/解決管理員許可權下執行拖放(DragDrop)的問題/FileDragDrop]

相關文章