WPF + Winform 解决管理员权限下无法拖放文件的问题

wpf,winform混合解决管理员权限无法拖放文件的问题

学习自:

WPF 实现文件、图标拖放功能(支持UAC的那种) 2021-01-11
WinForm中管理员权限下获取拖拽文件路径的解决方案 2018-11-06

``

本文记录我解决这个问题的心路历程。总体过程是先知道了第一个文档,很好用。但融入项目的时候发现了一个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]

posted @ 2022-02-22 13:05  几个酒菜成这样  阅读(2179)  评论(5编辑  收藏  举报