WPF自定义控件——顶级控件

     作为一个WPF程序员,我最希望看到的是WPF的应用,或者更确切的说是绚丽的应用,虽然限于自身的实力还不能拿出成绩来,但看到别人的作品时,心里还是有很大的宽慰——WPF是可以做出更加动人地产品的,只要你坚定的走下去,带着不满现状的追求走下去。

     下图是Telerik的WPF控件,我相信很多人也下过他的DEMO,研究过他的代码,并由此激起对WPF的信心。今天我们就来仿造他的DragAndDrop做一个自己的控件。

image

 

一.Windows的窗体

 

       一般来说我们只要打开Visual Studio 2008依次打开 文件(File)->新建(New)->项目(Project),在弹出的新项目(New Project)窗口中选中Windows中的WPF Application,单击确定(OK)便可以开始我们的WPF之旅。

        image

 

之后我们按F5或绿色箭头image 来运行程序会看到一个空白的窗口。

image

 

     这太普通,如果你有过桌面程序开发经历,这个或许都不能带起你的任何激情。况且他上面没有任何东西,也就意味着没有向MM们炫耀的资本。当然还有一点最重要的,这个窗体的出现也是在你预期之内的——你理所当然的认为该有这么个窗体,因为里面有一个继承于Window的类,而App.xaml中的StartupUri属性是指向这个类的XAML的,所以他就应该出现?

     那么如果我再追问App.xaml是什么,作用是什么,为什么把XAML放到StartupUri属性中便会出现?你可能会耸耸肩坦然:WPF自动做的,我们应该注重的是WPF使用本身。当然这话也没错,每个人的兴趣爱好不同,你如果想知道为什么可以跟着我走,如果你认为我说的不好,非常欢迎提出批评指正。

     实际上我对解析XAML也没有任何兴致,对他自动生成的App.g.cs以及XX.g.cs也不怎么关心,我所希望知道的是WPF是如何把图像给绘制出来显示给我们看的,空窗体也是绘制了,要不然你怎么看的到那个窗体。

     可能已经有人不耐烦了,操作系统提供了CreateWindowEx来创建窗体,他肯定是调用了这个。

     那口说无凭,我们端出利器Windbg来看真相是否如此,如果没有可以到http://www.microsoft.com/whdc/devtools/debugging/default.mspx下载。这里我用的是6.8版本,为什么要说版本,这个是因为我之前用了某低版本的有很多问题,又是新手还抱怨是操作系统的问题。

     使用之前我们先设置Symbol的路径,为什么要设置Symbol呢?其实我们的程序执行的时候只是二进字代码,debug的时候只是让我们能得到这些代码段,如果我们看到的内存信息是二进制类似无疑是痛苦的起码也是难于马上理解的,所以微软用了pdb文件来记录,这样就可以使内存地址和函数名相关联,用debug的时候可以先从pdb上看看这段地址是否有对应内容,有的话就显示,当然pdb还记录了一些源码信息,数据类型,数据结构,不过微软提供的Symbol大部分都给去掉了。

在我的路径中 srv*d:\symcache*http://msdl.microsoft.com/download/symbols;d:\symcache    大家看到有两个d:\symcache,这个说明我把要下载的Symbol放到了D盘symcache文件夹下,下次需要读的话先从这个文件夹下取没有的话再到http://msdl.microsoft.com/download/symbols中下载,如果你有需要也可以把d:\symcache换成其他的,比如;E:\Program\Symbol。下载文件的时候旁边会有*BUSY*出现。

image

附加我的进程TopControl.exe:

image

 

然后便是在CreateWindowEx函数这里下断点

0:000> bc *
0:000> BU USER32!NtUserCreateWindowEx

其中bc * 是清除全部断点,为什么是USER32!NtUserCreateWindowEx而不是USER32!CreateWindowEx,这个似乎就要看版本了,我的不支持CreateWindowEx。再者如果你对Windows内核熟悉的话就会知道其实CreateWindowEx内部是调用了NtUserCreateWindowEx,ShowWindow是NtUserShowWindow,怎么被我的才学吓倒了?哈哈,其实这个是和操作系统有关的,我的机器是Windows 7,列表是从这个网页看的http://hi.baidu.com/%B7%FA%CD%DE/blog/item/66658d10fb447af4c2ce7929.html

在Debug菜单中Restart后g

0:000> ~*KL

.  0  Id: b50.82c Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr 
001ef1cc 76f80cfd USER32!NtUserCreateWindowEx
001ef470 76f80e29 USER32!VerNtUserCreateWindowEx+0x1a3
…节约空间省略

   1  Id: b50.ca8 Suspend: 1 Teb: 7ffde000 Unfrozen
ChildEBP RetAddr 
0084f994 77875e4c ntdll!KiFastSystemCallRet
0084f998 75bb6872 ntdll!NtWaitForMultipleObjects+0xc
…节约空间省略

 

~*KL命令是查看全部线程堆栈信息, 如果你不是在当前线程(当前线程看左边,0:00代表0线程,0:01代表1线程,以此类推)请用命令 ~[线程号]s,如~0s 就是转到第0个线程,如果~1s就是到第1个线程。我们看到001ef1cc 76f80cfd USER32!NtUserCreateWindowEx在第0号线程那么我只要~0s转到0号线程。

 

0:000> .load C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll
0:000> !clrstack
OS Thread Id: 0x1350 (0)
ESP       EIP    
0041ec8c 76f80d5d [NDirectMethodFrameGeneric: 0041ec8c] MS.Win32.UnsafeNativeMethods.IntCreateWindowEx(Int32, System.String, System.String, Int32, Int32, Int32, Int32, Int32, System.Runtime.InteropServices.HandleRef, System.Runtime.InteropServices.HandleRef, System.Runtime.InteropServices.HandleRef, System.Object)
0041ecd0 69c25bec MS.Win32.UnsafeNativeMethods.CreateWindowEx(Int32, System.String, System.String, Int32, Int32, Int32, Int32, Int32, System.Runtime.InteropServices.HandleRef, System.Runtime.InteropServices.HandleRef, System.Runtime.InteropServices.HandleRef, System.Object)
0041ed18 69c257fc MS.Win32.HwndWrapper..ctor(Int32, Int32, Int32, Int32, Int32, Int32, Int32, System.String, IntPtr, MS.Win32.HwndWrapperHook[])
0041edd8 5aecde40 System.Windows.Application.EnsureHwndSource()
0041edec 5aecd646 System.Windows.Application.RunInternal(System.Windows.Window)
0041ee10 5aeb49d6 System.Windows.Application.Run(System.Windows.Window)
0041ee20 5aeb4999 System.Windows.Application.Run()
0041ee2c 002d0096 TopControl.App.Main()
0041f04c 6a9c1b6c [GCFrame: 0041f04c]

 

除了

0:000> .load C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll
0:000> !clrstack

这两个我打的命令,一个是加载sos以便看到托管信息,一个是看托管信息栈,其他信息请从下往上看,也就是说先执行的在下,后执行的在上。

注意这里有时候你会得到类似信息

OS Thread Id: 0x82c (0)
ESP       EIP    
001ef66c 76f80d5d [DebuggerClassInitMarkFrame: 001ef66c]

没有关系,多g几次就可以看到想要的了。

 

很明显的我们看出TopControl就是选中的控件,在App的Main函数中调用Application的Run函数,然后Run函数中又调用了一个有参的Run函数,从参数类型上我们可以看出是一个窗体类,之后又调了RunInternal 和EnsureHwndSource 并在MS.Win32.HwndWrapper的构造函数中调用了MS.Win32.UnsafeNativeMethods的静态方法CreateWindowEx,后又调用IntCreateWindowEx利用p/invoke引发了user32.dll中的CreateWindowEx,这里不说p/invoke,具体的可以看黄际洲、崔晓源的《精通.NET互操作》,多说一句,我们的托管代码也是要编译为机器码才能运行的,Win32 API也可以说是段机器码,这样当然能调用的。如果你想得到譬如CreateWindowEx结构体类的详细写法,挂接的消息处理函数等具体信息,就可以用Reflector查看HwndWrapper来获得,假如你是个一步步仔细看的人你会发现在RunInternal的这句:

if (window.Visibility != Visibility.Visible)
{
    base.Dispatcher.BeginInvoke(DispatcherPriority.Send, delegate(object obj)
    {
        (obj as Window).Show();
        return null;
    }, window);
}

在Show中你会看到梦寐的ShowWindow函数。

 

可问题又来了,即便我知道了WPF是调用了CreateWindowEx来创建的窗体,并用了个ShowWindow函数可为什么要这么做呢?这个和显示以及画图有什么联系?

这里我们就要了解下调用CreateWindowEx时需要的参数

IntPtr windowHandle = User32Dll.CreateWindowEx(ExtendedWndStyle.WS_EX_LAYOUTRTL//扩展样式
, m_WndClsName  //刚才注册完的名称
, null          //窗体名称
, WndStyle.WS_VISIBLE | WndStyle.WS_CHILD //子窗体
, this.Left //X坐标
, this.Top  //Y 坐标
, this.Width //宽度
, this.Height //高度
, this.Parent.Handle //父对象句柄
, IntPtr.Zero //上下文菜单句柄
, Kernal32Dll.GetModuleHandle(null)//实例句柄
, IntPtr.Zero//指向一个值的指针,该值传递给窗口 WM_CREATE消息
);

     可以看到它需要传入的具体的左上角坐标,大小(宽度,高度),这些信息很明显的可以得到了一个矩形区域,鼠标移动的时候有一个坐标,看看坐标是不是在这个矩形区域就可以了(操作系统还要判断层叠或父子关系),Windows操作系统把它(代码上的参数)存成一个结构体放到系统区域,他所产生的Handle和进程中的Handle表不一样,window handle是全局唯一的,也是说整个操作系统不会有相同的两个window handle,Handle是什么?其实就是一个标示(代号),为什么常会翻译为句柄,这个我不知道,不过看蔡學鏞的.NET 的自動記憶體管理台湾那边似乎翻译为視窗代號,我个人认为更容易理解些。一个线程上可以创建多个window Handle,譬如WPF和WinForm默认都是在同一窗体线程上创建UI控件。

     当我们移动鼠标时,鼠标的位移会产生中断,CPU在开中断的前提下把鼠标中断放到IDT中断表中获取对应处理函数,而Windows操作系统检查所有的结构体来判断应该把这个消息交给哪个窗体处理,毕竟鼠标移动的消息只要求被移动到上面的窗口得知就可以了,这样我们可以根据鼠标的位置重绘图形让人感觉到效果,比如鼠标移上按钮发光。虽然循环判断这么多窗体结构我们按常理就知道很费时间,可更棘手的问题是我们给的结构体毕竟是一个规则的矩形,万一窗口是一个不规则的图形怎么办?像我在WPF自定义控件——使用Win32控件 中产生的不规则图形,或是如淘宝旺旺的这种窗体上凸出一块的做法。那么其实就要给操作系统一些不规则图形的信息,当画面超出不规则的区域时直接裁减掉就可以了,当然不规则信息是我们给的,Windows给了我们两个API进行传递一个叫作SetWindowRgn,如果要实践不规则图形请参看 Windows 中不规则窗体的编程实现;在XP之后操作系统为方便我们只要使用WS_EX_LAYERED样式放入一张带ALPHA通道的PNG图片,不过直到调用SetLayeredWindowAttributes 或者UpdateLayeredWindow 函数才会有效,就可以混合桌面的颜色达到效果然后自动计算不规则图形,当然这个做法有个很大的副作用那就是和它同一个线程上的控件都显示不了,所以有些人就搞了两层,如同我文章里的那样,一层图片,一层控件。当然你可以在图片上自己画控件,或是把控件放在另外一个线程上接受响应。如果你对这种做法很感兴趣那么请看Layered Window,或想对png格式了解请看 了解PNG文件存储格式

     操作系统知道了怎么判别我们的窗体,那么怎么把鼠标消息传递给我们呢?毕竟线程是在内核被创建的,对应的线程结构体也就在内核区而我们的处理函数在用户区,那怎么办获得?估计说到这里已经没有悬念了,大家已经知道了使用win32 API GetMessage或PeekMessage来监听,放个MSG结构让内核填充,如果你不清楚可以详细的看这篇深入GetMessage和PeekMessage,很遗憾它的原文似乎没有了,我没有搜索到,如果哪位大侠还有私人珍藏记得分享哦。

   image  从用户区到内核区?什么叫作内核?什么叫作用户区域,其实IA32架构CPU在保护模式下允许我们的内存区域划分成4块,分别以4种级别来表示(Windows和Liunx均只用了0,和3两层),级别越高数字越小,数字最小的在最内层,在非一致性代码段下,级别低的不能直接访问级别高的,级别高的可以访问级别低的;这很好理解,有些系统区域的重要信息不是谁都能动的,而且CPU规定有些指令只能在0层使用,比如LLDT,SLDT,MOVE CRN,SMSW等等;不过在一致代码段情况下情况正好相反,就是说级别低的可以访问级别高的,可级别高的不能访问级别低的,Level3的代码可以访问Level0的代码,Level0的代码不能访问Level3的,为什么这样呢?这是怕有些用户访问了Level0层的代码然后借以来控制Level3上的代码。这里GetMessage或PeekMessage都可以填写我们用户区的MSG结构体,很明显是属于非一致代码段了。

     说到这可能又有人迷糊了:啥是什么非一致性代码或是一致性代码,其实这只是种说法,简单说他只是内存段描述符上的一个标示C,值为1就代表这个区域内的代码都是一致性代码,0就说明这个区域内的代码是非一致性代码;一致代码段不太常用,一般放些共享数据。

clip_image002

      用户态的代码不能直接访问到是Level0的代码,有时候就是需要用到Level0的代码来操纵CPU的特殊指令怎么办?虽然你不能随便访问特权级的代码,但CPU给了你一些“曲线救国”的方案,比如调用门,中断。在Windows和Liunx系统中均没有采用调用门、任务门,原因是使用那些指令很麻烦,况且速度也不快要200多个CPU周期,Windows采用了中断直接来操作,这就好比,你没有权利进入军区,可如果你买了门票找个向导还是可以进入,不过需要从游客通道规规矩矩的走,当然游客通道就表明了禁区你是不可以去的,能强制进入禁区的人,我们称为hacker。所以我们只好找“导游”绕一圈进入特定参观“景点”,下中断如int 152进入中断表,选择代码地址到GDT再到LDT读取,最后才能进入内核段,在实际操作中的这个导游就是win32 API,在他的引导下你就完成了参观,当然这个参观过程是黑箱,只能得到结果。从这一过程中也可以知道从用户态到内核是很耗时间的,绕路了嘛。

      什么是IDT,GDT,LDT?这个都是CPU保护模式的概念,建议你看下杨季文的《80x86汇编语言程序设计教程》还有Intel自己的说明书《IA-32 Intel Architecture Software Developer’s Manual》卷3

      这里还要再啰嗦几句,我们知道线程是操作系统调度的最小单位,也就是说在单CPU情况下其实一个时间只能使用一个线程,但是如果切换的快,就会给我们造成同时进行的假象。这就好比几个人演出,可舞台在同一时刻只能一个人演出,一个人演出一段时间然后换另外一个人出场。

     在下图中我们把身上带旗头上长触角(我不知道专业术语叫啥)的称为A,另外一个女的称为B,男的称为C。

1

图一.刚开始是C已下台,A正在舞台演出,B等待演出


 

 

 

2

图二.紧接着A演出结束,换B演出,C等待

 

 

 

3

图三.C演出,B退场,A等待

 

 

 

4

图四.和图一样这样进行了一个循环

 

 

 

5-1

图五.减少每个人出场时间,提高循环速度

 

 

 

6

图六.当循环超过到我们眼睛的延迟就会感觉他们一起演出

 

 

 

7

图七.如果演出A演出时候剧目说需要C怎么办?A还要继续演出

 

 

 

8

图八.只需要把当前剧目记录到C的笔记中,等他上台让他演

 

 

 

9-1

图九.但下次演出时还是需要按照顺序来执行,依旧是B演出

 

 

 

10

图十.轮到C上场的时候先检查自己的笔记,如果笔记有内容则当剧目演出

 

 

 

 

     图画的人物A、B、C就是线程的缩影,线程是一个个轮流执行的,如果操作系统让线程每个线程执行的时间少些,比如10毫秒,这样循环很快就让我们感觉这么多线程其实是一同执行的,可一个线程执行过程中需要另外一个线程做事,如果传递消息就要建立一个窗口如上的CreateWindowEx或CreateWindow,为什么一定要建立窗体?这个是操作系统规定的,整个报文系统是建立在窗体结构之下的;如若传递的消息是异步的(PostMessage),异步的意思是我这边先运行没关系到时候你那边运行一遍就行的情况,那么就把信息记录到另一个线程的某队列中,当运行到那个线程,操作系统会判断这个队列是否为空,不为空的话就用那个线程的GetMessage方法去调用(GetMessage方法有点复杂,这里只是简单描述,实际情况复杂的多,查验的队列也有7个)。如果是同步的(SendMessage),同步的意思是一定要你做完了我才能做,可这里并不是说用到A线程了就马上运行A线程(A线程权限突然提高除外),还是要把信息放到A线程的消息队列中,然后自己休眠(告诉操作系统不需要再运它,可以转向运行其他线程),等操作系统安排A运行的时候,等A读完了这条消息,才告诉发送这条信息的线程,A已经OK了,你可以醒了起来干活了,告诉这件事其实就是设置一个标志位。可能有些人马上会问,他的数据结构是怎么样的,如果他等了多个线程怎么办?可实际情况是一个时间他只能执行一次SendMessage然后就开始睡觉了(设了一个标志位告诉操作系统不需要再运行它),所以他只能等待一个线程,假如他等待的线程也在等待他的话,那么这个就是死锁。死锁不怎么占用CPU,都不运行怎么会占用,不过一直不执行,那么你会发现画面不动(无法得到处理队列中的各种重绘,移动消息),感觉“卡”了, 然后我们就会说这个程序“死掉”了,说到这里还是有人不信,明明程序“当了”操作系统会出现提示说什么“没有响应”,实际上操作系统会向程序的队列发送一个消息,而这个消息上有计数器,当主线程长时间休眠,自然不会去执行那个消息,消息上的计数器递减到0时,操作系统便会告诉我们这个程序没有响应了,回想下当你主线程中有个长循环的时候是不是也会出现这种提示。

     这里又出现了一个点,操作系统怎么会知道哪个是主线程?操作系统会在创建进程的时候创建一个线程,那个线程一般作为主线程,到这里有人终于长长的呼了口气:我就是不在那个所谓的主线程创建窗体,他怎么通知?你一直不创建窗体一路走到黑确实没有必要通知,如果在其他线程创建窗口,操作系统会判断是否已经有UI线程(就是建立过窗体的),如果之前没有,那么会把第一个创建窗口的线程作为以后传递消息的线程,这一过程具体是怎么样的?这我还真不知道了,不过我知道你问的这个东西绝对有价值,有意义,支持你继续探究,个人认为只有不断刨根问底的程序员才能看到真正的全貌掌握真正的实质。

     说到GetMessage,顺便也来说说常用的这种结构:

while (true)
{
    if (!this.GetMessage(ref msg, IntPtr.Zero, 0, 0))
    {
        break;
    }

    TranslateMessage(ref msg);
    DispatchMessage(ref msg);
}

    

      为什么要加个While循环?GetMessage只能得到一次信息,信息在线程关闭前自然是要不停获取处理的,这类“死循环”不是会很耗么?实际当GetMessage没有东西的时候,他会自动休眠,得到消息然后翻译(Translate)消息,转为unicode或Ansi编码,DispatchMessage是把消息转发到对应的窗口处理函数中去,这个处理函数什么时候定的呢?就是在注册窗口类的时候(RegisterClassEx),其中的lpfnWndProc指针就是传这个,说到这里可能要问了,为什么操作系统不在GetMessage里直接转发,而需要通过DispatchMessage转发一次,这里因为有些消息你可以率选是否要处理,要处理的才给DispatchMessage分配,GetMessage是告诉操作系统这个消息已经发送给这个线程了,如果另一个线程在等待(SendMessage)那么就可以醒了,DispatchMessage是通知自己的处理函数。

     上面说的都是概念上的,为什么说是概念上,因为Windows是不开源的,刚才的说法从很大一部分来说是一种臆断,不过研究Windows的从来不乏人才,比如开源的仿照Windows的ReactOS,如果你对window内核处理感兴趣,建议你看下毛德操的《Windows 内核情景分析》或David A. Solomon 的《Microsoft Windows Internals》,最近在看Raymond Chen 的《The Old New Thing》感觉也可以对Windows的设计有一定了解。

 

二.WPF的顶级窗口

 

     顶级窗口的概念是相对于二级窗口的,顶级窗口是坐标相对于桌面或者说是屏幕的左上角坐标,二级窗口的坐标是相对于父容器的左上角坐标。顶级窗口是以桌面为参照物是针对全局而言的,全局就要遵循Win32 的规则,刚才我们看到了WPF其实也是一个Window handle,如果他是一个不规则窗体,必然要保存一个描述不规则图形的结构,而结构体是在内核,一定要通过相应Win32 API才能放的进去,刚才说了系统给我们提供了两个方案一个是设置区域(SetWindowRgn),一个是设置层(WS_EX_LAYERED),WPF选择的是哪一种呢?

     我们在刚才创建的窗口中写如下代码:

<Window x:Class="TopControl.Window2"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300" AllowsTransparency="True" WindowStyle="None">
    <Window.Background>
        <VisualBrush>
            <VisualBrush.Visual>
                <TextBlock>Curry</TextBlock>
            </VisualBrush.Visual>
        </VisualBrush>
    </Window.Background>
</Window>
运行之后当然很容易看到这样的不规者窗体,如果自己实在懒的动手,你也可以下载MSDN的示例Non-Rectangular Windows Sample

image

依照刚才的做法用Debug在NtUserUpdateLayeredWindow中下断点。

BU USER32!NtUserUpdateLayeredWindow

得到信息如下:

#  3  Id: 1c48.270c Suspend: 1 Teb: 7ffdc000 Unfrozen
ChildEBP RetAddr 
03d7f820 76f7b636 USER32!NtUserUpdateLayeredWindow
03d7f850 5f4abcca USER32!UpdateLayeredWindowIndirect+0x3f
WARNING: Stack unwind information not available. Following frames may be wrong.
03d7f8b0 5f45ad21 wpfgfx_v0300!MilUtility_PathGeometryCombine+0x46616
03d7f918 5f45adb3 wpfgfx_v0300!MilCompositionEngine_DeinitializePartitionManager+0x4f4c
03d7f958 5f478d5d wpfgfx_v0300!MilCompositionEngine_DeinitializePartitionManager+0x4fde
03d7f97c 5f478dfe wpfgfx_v0300!MilUtility_PathGeometryCombine+0x136a9
03d7f9c4 5f478e42 wpfgfx_v0300!MilUtility_PathGeometryCombine+0x1374a
03d7f9dc 5f433726 wpfgfx_v0300!MilUtility_PathGeometryCombine+0x1378e
03d7fa34 5f4337b5 wpfgfx_v0300!MilChannel_AppendCommandData+0x61a9
03d7fa4c 5f42ddb8 wpfgfx_v0300!MilChannel_AppendCommandData+0x6238
03d7fa74 5f42dbe9 wpfgfx_v0300!MilChannel_AppendCommandData+0x83b
03d7fac0 5f42dc61 wpfgfx_v0300!MilChannel_AppendCommandData+0x66c
03d7fae8 5f42b6a2 wpfgfx_v0300!MilChannel_AppendCommandData+0x6e4
03d7fb00 779d1174 wpfgfx_v0300!MilChannel_SetNotificationWindow+0x26f3
03d7fb0c 7788b3f5 KERNEL32!BaseThreadInitThunk+0xe
03d7fb4c 7788b3c8 ntdll!__RtlUserThreadStart+0x70
03d7fb64 00000000 ntdll!_RtlUserThreadStart+0x1b

 

untitled

其中wpfgfx_v0300模块就是WPF的核心模块,之前叫做Milcore,我们常常看到的图说的就是它。

这很明显的是Render线程,看看我们的主线程中有些什么,也就是常说的UI线程。

 

   0  Id: 1c48.16a4 Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr 
0025df78 77875e4c ntdll!KiFastSystemCallRet
0025df7c 75bb6872 ntdll!NtWaitForMultipleObjects+0xc
0025e018 779cf12a KERNELBASE!WaitForMultipleObjectsEx+0x100
0025e060 779cf29e KERNEL32!WaitForMultipleObjectsExImplementation+0xe0
0025e07c 5f4635ef KERNEL32!WaitForMultipleObjects+0x18
WARNING: Stack unwind information not available. Following frames may be wrong.
0025e1a4 5f46364a wpfgfx_v0300!MilUtility_PolygonHitTest+0x1e63
0025e1f4 5bd98571 wpfgfx_v0300!MilComposition_WaitForNextMessage+0x43
0025e214 5bd98571 PresentationCore_ni+0x1d8571
0025e238 5bdbad90 PresentationCore_ni+0x1d8571
0025e288 5bd9ffba PresentationCore_ni+0x1fad90
0025e2b0 5bd9d0ba PresentationCore_ni+0x1dffba
0025e2cc 69c2668e PresentationCore_ni+0x1dd0ba
0025e318 69c265ba WindowsBase_ni+0x9668e
0025e338 69c264aa WindowsBase_ni+0x965ba
0025e354 69c2639a WindowsBase_ni+0x964aa
0025e394 69c24504 WindowsBase_ni+0x9639a
0025e3b8 69c23661 WindowsBase_ni+0x94504
0025e3f4 69c235b0 WindowsBase_ni+0x93661
0025e424 69c25cfc WindowsBase_ni+0x935b0
0025e474 76f886ef WindowsBase_ni+0x95cfc

注意红色标示的说明他竟然等待了,休眠了。

转到主线程 ~0s,看CLR堆栈信息

0:000> !clrstack
OS Thread Id: 0x16a4 (0)
ESP       EIP    
0025e20c 778764f4 [NDirectMethodFrameStandalone: 0025e20c] System.Windows.Media.Composition.DUCE+UnsafeNativeMethods.MilComposition_WaitForNextMessage(IntPtr, Int32, IntPtr[], Int32, UInt32, Int32 ByRef)
0025e22c 5bd98571 System.Windows.Media.MediaContext.CompleteRender()
0025e240 5bdbad90 System.Windows.Interop.HwndTarget.OnResize()

可以看出UI线程是通过System.Windows.Media.Composition.DUCE模块和Render线程进行的交互,而进行交互传输的是MediaContext。

 

     说到这里可能引来众多崇拜的目光,居然一下就debug出来,完全应该去买体育彩票,要不然3.5个亿就是你的了,可实际上我刚开始用的是NtUserSetWindowRgn来下断点,为什么用那个呢?这个还要从 Nick的Transparent and non-rectangular windows博文说起,他里面说了因为技术原因所以使用了SetWindowRgn,我也痴痴的,坚定不移的相信了很长一段时间,直到我写WPF Win32控件的时候,尝试着去验证下,居然不能断不到,才慌了神,Nick如在身边的话我应该会思量着用尽各种酷刑逼其招供而后继续摧残^-^。当时刚用上Win7也不久,下了UpdateLayeredWindow(当然没有加上NtUser)也没断到,还在想是不是邪恶的微软又搞了什么内部API,于是查了各种Vista的资料,特别是Windows Vista Developer Story都快被我翻到烂,也没看出个所以来。

     直到不久前阅读了由Dflying Chen翻译的 Windows Vista for Developers——第三部分:桌面窗口管理器(首先感谢翻译,这里提个小意见翻译的时候能不能把图片Down下来重贴,如今原文没了,图片也没了),回想起Lester 博文 Part III: Non-Rectangular Window in WPF (use of Thumb)中的sParams.UsesPerPixelOpacity = true;一句让我坚定了UpdateLayeredWindow的信心,抱着试试看的心理(感觉在打啥广告)用了NtUserUpdateLayeredWindow来下,居然成功了,那个高兴啊有种喜极而泣的冲动,要知道这问题可困扰了我半年多,真是好奇心害死猫。回头想想Nick也在后面说了以后会在操作系统里加支持的,毕竟对每个象素点作变化需求还是有的,说到这里还要再强调下,我的这个是在Win7下试验的,因为有Win7的DWM(Desktop Window Manager)的支持,不知道XP会不会不同,所以你的操作系统是XP下不到的话换个NtUserSetWindowRgn试试。

     为什么要说这些呢?,因为它让让我知道了一直努力下去上帝也是会开眼的,挫折总会有的,弯路也会有的,怎么越过?有时候只需要再坚持下,这样如果我碰到 WPF Window的AllowsTransparency=True导致WebBrowser不可视临时解决方法 我想我用SetWindowRgn也会脸不红,心不跳。

 

     窗体的结构信息得到落实,窗体的结构信息告诉操作系统是为了啥?为了让操作系统正确的把各种消息发送给我们,很明显要使用GetMessage这个API来指定消息接收入口,那WPF的窗体又是怎样使用GetMessage的呢?

我们把运行中的程序断下,并检查CLR堆栈很容易看到如下信息:

0:000> !clrstack
OS Thread Id: 0x24f0 (0)
ESP       EIP    
002fec98 778764f4 [NDirectMethodFrameStandalone: 002fec98] MS.Win32.UnsafeNativeMethods.IntGetMessageW(System.Windows.Interop.MSG ByRef, System.Runtime.InteropServices.HandleRef, Int32, Int32)
002fecb4 69c28e95 MS.Win32.UnsafeNativeMethods.GetMessageW(System.Windows.Interop.MSG ByRef, System.Runtime.InteropServices.HandleRef, Int32, Int32)
002feccc 69c23d88 System.Windows.Threading.Dispatcher.GetMessage(System.Windows.Interop.MSG ByRef, IntPtr, Int32, Int32)
002fed08 69c23c6b System.Windows.Threading.Dispatcher.PushFrameImpl(System.Windows.Threading.DispatcherFrame)
002fed58 69c23379 System.Windows.Threading.Dispatcher.PushFrame(System.Windows.Threading.DispatcherFrame)
002fed64 69c2331c System.Windows.Threading.Dispatcher.Run()
002fed70 5aece37e System.Windows.Application.RunDispatcher(System.Object)
002fed7c 5aecd67f System.Windows.Application.RunInternal(System.Windows.Window)
002feda0 5aeb49d6 System.Windows.Application.Run(System.Windows.Window)
002fedb0 5aeb4999 System.Windows.Application.Run()
002fedbc 005e0096 TopControl.App.Main()
002fefe4 6a9c1b6c [GCFrame: 002fefe4]

 

     其实这里的很多信息我们在CreateWindow中已经看到过,说明他们是在同一个线程中,我们注意从System.Windows.Application.RunDispatcher(System.Object)这句开始起了变化,打开Reflector一直看,会发现PushFrameImpl最有料,GetMessage方法也在其中,他压入的frame用frame.Continue来执行,如何退出消息循环?执行Dispatcher.ExitAllFrames();这里GetMessage是一个内部函数,说来惭愧之前都看到这层就忽略了直到写这篇文章的时候才打开看了下,发现他居然不是直接调用的win32 API而是先判断有没有Com服务,这让我傻了眼,不知道咋回事了,不过幸好周大哥在,问了下才恍然,是为了调用Text Services Framework, 其实认真看自己也能明白的,不过确实说明了心态,遇事要镇静坦然处之,这是我还要向各位高手学习的。Text Services Framework这块我也不懂,要了解的自己google吧 ^-^

//<SecurityNote> 
// Critical - as this calls critical methods (GetMessage, TranslateMessage, DispatchMessage).
// TreatAsSafe - as the critical method is not leaked out, and not controlled by external inputs. 
//</SecurityNote> 
[SecurityCritical, SecurityTreatAsSafe ]
private void PushFrameImpl(DispatcherFrame frame) 
{
    SynchronizationContext oldSyncContext = null;
    SynchronizationContext newSyncContext = null;
    MSG msg = new MSG(); 

    _frameDepth++; 
    try 
    {
        // Change the CLR SynchronizationContext to be compatable with our Dispatcher. 
        oldSyncContext = SynchronizationContext.Current;
        newSyncContext = new DispatcherSynchronizationContext(this);
        SynchronizationContext.SetSynchronizationContext(newSyncContext);
 
        try
        { 
            while(frame.Continue) 
            {
                if (!GetMessage(ref msg, IntPtr.Zero, 0, 0)) 
                    break;

                TranslateAndDispatchMessage(ref msg);
            } 

            // If this was the last frame to exit after a quit, we 
            // can now dispose the dispatcher. 
            if(_frameDepth == 1)
            { 
                if(_hasShutdownStarted)
                {
                    ShutdownImpl();
                } 
            }
        } 
        finally 
        {
            // Restore the old SynchronizationContext. 
            SynchronizationContext.SetSynchronizationContext(oldSyncContext);
        }
    }
    finally 
    {
        _frameDepth--; 
        if(_frameDepth == 0) 
        {
            // We have exited all frames. 
            _exitAllFrames = false;
        }
    }
} 

 

     这份代码是Reference Source中拿过来的,其他都没什么好说的了,主要是他把CLR中的SynchronizationContext 换成了DispatcherSynchronizationContext,没了?没了。

!!!本来MS想把事情简单化的,这要从Winform说起,SynchronizationContext 是抽象类,所以具体的实现是WindowsFormsSynchronizationContext,他做了什么?或者说他有什么用,其实这都是分层的罪,分层讲究前后端分开,后端的类不知道前端的具体的实现,那么自然就不能知道前端的具体控件,如果在后端创建了一个线程,在线程中要把数据给UI线程,怎么办?Control.Invoke?NO! 分层是不允许知道具体控件的,那怎么办?凉拌?好吧,分的清楚其实也是有个人知道联系的,这个联系人就是SynchronizationContext ,当Application创建的时候,微软把当前的控件(Form)放入,然后就可以了,我们后端就就可以使用SynchronizationContext.Current来得到UI线程,作SynchronizationContext.Post就相当于做了Control.BeginInvoke。Winform和WPF的传递不同,为了兼容所以做了这个动作,当然WPF的后端类也可以用这个来操作,统一嘛,比Application.Current.Dispatcher总归要好看些,用Dispather.CurrentDispatcher只能拿到当前线程的。

     换了DispatcherSynchronizationContext自然是有原因的,那什么原因呢?用的话主要是Post方法不同,WPF他是为了实现自己的消息队列用了BeginInvoke(DispatcherPriority.Normal)来实现,BeginInvoke使用对我们一般来说,无非就是以下这个样子

this.Dispatcher.BeginInvoke(((Action)delegate()
{
    //Do Method
}),
System.Windows.Threading.DispatcherPriority.Normal);

     在这点上,我感觉微软做的不太好,可又限于自身功力说不上来,从我的角度和用法来看BeginInvoke(Invoke内部其实也是BeginInvoke)的作用就是用于主线程,就是UI线程用的,可他或许为了统一还是为了别的什么其他的目的,为每个线程都分配了Dispatcher,这里的配分是指你用过类似Dispather.CurrentDispatcher的用法后创建的,为什么要调用这个方法后才创建?我认为他区分不了UI线程了普通后台线程,用了宁可错杀1000也不放过1个的做法;并在Dispatcher初始化的时候新建了一个Message-Only Window,这个纯消息窗口显然只是为了接受和发送消息消息的,怎么发送?BeginInvoke负责发送信息;怎么接受?他Hook了一个函数(WndProcHook)进行接受;它怎么知道消息的类别?他自己定义了一个消息类别进行判断:

private static int _msgProcessQueue = UnsafeNativeMethods.RegisterWindowMessage("DispatcherProcessQueue");

等等,BeginInvoke为什么要发信息给自己接受?这是为了维护DispatcherPriority,他先把数据存成DispatcherOperation放到一个PriorityQueue<T>结构体中,再根据一定的算法取出相对权限最大的那个,具体算法?还是自己看看吧,其实我也没细看,然后判断取出的这个DispatcherPriority属于哪个区域,区域?他把DispatcherPriority分为4个区,无效区(这个概念是我自己加的)、空闲区,后台区,前台区:

public enum DispatcherPriority
{
    Invalid = -1,
    Inactive = 0,

    //IdlePriorityRange
    SystemIdle = 1,
    ApplicationIdle = 2,
    ContextIdle = 3,

    //BackgroundPriorityRange
    Background = 4,
    Input = 5,

    // ForegroundPriorityRange
    Loaded = 6,
    Render = 7,
    DataBind = 8,
    Normal = 9,
    Send = 10        
}

然后根据每个区的标准看看,设置标志位(刚开始为0或1),判断是否到了用的时候了(标志为是否加到1或2),到了则返回执行,如果没有用到,先改变标志位(加1),之后设置一个Timer(SetTimer),下次线程切换时再执行,Timer时间到的时候操作系统会发送(PostMessage)WM_TIMER消息给Message-only window, 然后在它的Hook函数(WndProcHook)中执行——继续判断取出队列判断直到运行我们传入的函数。这里要注意发送和接收消息都在同一个线程只是时机不同, 这个过程对于UI线程肯定是有意义的,可对于非UI线程,似乎意义不大吧?还请各位高手指点;而且区域里的每项目的作用是什么?为什么设置个Timer就会有如此效果也还望高人指点。

这一块更详细的内容请参阅周大哥的一站式WPF--线程模型和Dispatcher ,消息循环处理请看 Win32 和 WPF 之间共享消息循环

我Windbg也是新手,学习Windbg主要通过以下几个博客

http://blog.csdn.net/eparg

http://www.cnblogs.com/juqiang

http://www.titilima.cn/

 

 

三.图像的显示

 

暂留,图像缩放,API,矢量图形,不规则图形。

 

四.控件拖动

 

     说了这么多关于窗体的,我们的拖动当然是要跨窗体的,跨窗体还需有个箭头跟着,那这个箭头该如何画呢?能跨窗体的必然也是窗体,只需要把窗体大小设置为屏幕大小,屏幕大小?那不是会把东西给全部遮住?其实这里就用到了NtUserUpdateLayeredWindow来计算不规则图形,毕竟箭头只是一小块。鼠标可以跨窗体那么消息就要监听全局的,就是要 用SetWindowHookEX做个全局钩子。

     首先我们来回顾下Windows下的拖放操作:

对于拖拽方(Drag),托拽源

1.用户用鼠标拖拽了一个对象后,告知操作系统是在拖拽而不是在简单的移动鼠标,这需要调用ole32.dll的DoDragDrop(dataObject, dropSource, okEffect, effects)函数来实现,dropSource要注册接口。

2.拖动过程中需要改变鼠标?其实操作系统在回调拖拽方,怎么回调?其实需要拖动的对象都需要继承一些COM接口.GiveFeedback便是为了实现拖动事件源能够修改鼠标指针的外观。

3.有时在拖动过程中发现鼠标或键盘状态改变了,比如用户按了ESC,或是特殊快捷键按钮需要取消,怎么来判断和中断这个操作,操作系统通过QueryContinueDrag接口操作。

[ComImport, Guid("00000121-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IOleDropSource
{
    [PreserveSig]
    int OleQueryContinueDrag(int fEscapePressed, [In, MarshalAs(UnmanagedType.U4)] int grfKeyState);
    [PreserveSig]
    int OleGiveFeedback([In, MarshalAs(UnmanagedType.U4)] int dwEffect);
}

 

对于目的地(Drop)方

1.告诉操作系统该窗体是可以拖放的,调用ole32.dll的RegisterDragDrop(hwnd, dropTarget),其中dropTarget要注册接口。

2、当有物体拖拽进了 hwnd 所在的区域时,操作系统回调DragEnter接口。

3、当物体在 hwnd 所在区域内滑动时,操作系统回调DragOver接口。

4、当物体拖拽出 hwnd 所在区域时,操作系统回调DragLeave接口。

5、当拖拽的物体放下时(Mouse Down),操作系统回调Drop接口。

[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("00000122-0000-0000-C000-000000000046")]
public interface IOleDropTarget
{
    [PreserveSig]
    int OleDragEnter([In, MarshalAs(UnmanagedType.Interface)] object pDataObj, [In, MarshalAs(UnmanagedType.U4)] int grfKeyState, [In, MarshalAs(UnmanagedType.U8)] long pt, [In, Out] ref int pdwEffect);
    [PreserveSig]
    int OleDragOver([In, MarshalAs(UnmanagedType.U4)] int grfKeyState, [In, MarshalAs(UnmanagedType.U8)] long pt, [In, Out] ref int pdwEffect);
    [PreserveSig]
    int OleDragLeave();
    [PreserveSig]
    int OleDrop([In, MarshalAs(UnmanagedType.Interface)] object pDataObj, [In, MarshalAs(UnmanagedType.U4)] int grfKeyState, [In, MarshalAs(UnmanagedType.U8)] long pt, [In, Out] ref int pdwEffect);
}

     对WPF而言,很多控件我们已经知道了其实是在同一个window Handle中的,所以在Hwndsource初始化的时候便使用了RegisterDragDrop来进行注册,当鼠标进入区域时根据HitTest的结果来判断是否选中控件,然后再根据控件的AllowDrop来判断是否可以拖动,AllowDrop为True则引发相关事件。当然WPF还定义了DragDrop类更加简单的来调用操作。可以参看Drag and Drop Overview

     同一个WPF程序内的拖拉,请参看我的上篇文章“WPF自定义控件 —— 装饰器”中的各种拖拽示例。  image

     先做一个类似透明窗体的控件MouseTip,对于拖拽跟随的物体,在拖拽的时候只要计算拖拽源的位置和拖拽目标的位置便可以计算出他的距离和角度,其实就是计算出他的向量。然后使用MouseTip,MouseTip实际上也是在window handle的封装基础上,那么就可以用win32 API SetWindowPos来移动。设计一个结构,使得MouseTip的长度为两者的距离,运用Binding这样XAML的设计者就可以搭配出任意的图形。比如示例的就是用一个Rectangle来帮定计算出的长度,然后对整个UI运用RotateTransform来绑定角度,在这个当中您或许会看到我没有把图标的长度减掉而是用了Converter来处理,我这么做的目的是因为我认为没有必要拘泥于这种图标箭头的形式,做个瞄准仪鼠标换个降落伞的图标,选定确定后用导弹形式发射礼包过去也是可以的。

      都说好,未免有失偏颇,在我实际做的过程中确实碰到了很多“意外”,最后的实现和我预期的做法差别还是挺大的,当然这就不能避免BUG的存在。下面是我做的过程中发现的一些BUG和设计方面处理不好的地方:

1.鼠标状态不定,这个是我计算偏离量的时候没有计算好,导致操作系统看到的是拖拉的窗体。

2.有时候比较卡,甚至会拖不动,毕竟操作系统计算不规则图形也是花时间的。

3.箭头和下面的方块没有完全处理好,仔细看会发现。

4.拖拉的图片我或是说东西得到我用的是MouseDown事件参数中的e.OriginalSource,然后做VisualBrush,可以说是通过点击测试拿到的,这个坏处是如果图片下面还有文字说明,那点击的时候可能只会拖动文字或图片,因为点击测试只能拿到一个控件,如果你采用itemsControl.ItemContainerGenerator.ContainerFromItem,你会发现当选中的蓝色背景也会一起拿过来,最安全的方法应该是把拖动的东西模板传进去,这样还能控制在拖动的时候样子,当然这样就要多做些属性支持。

5.拖动过程中可能会崩溃,因为我没有做try,没有做类型检测。

6.传递的类一定要加序列化标签Serializable,而且最好重载你的Equals方法以免不幸,本来想拖动的过程中用 FindWindow来看如果是同一窗体或线程则直接拖拉,不需要序列化,不过这个没有做,有需要的朋友可以自行添加。

2009-12-25 12-34-48

 

 

五.小结

 

      其实这篇东西写的比较久,一个是自己懒散,一个是有些内容开始也只是了解大概,真正去了解才知道博大精深,而且拉的面太广,比如WPF到底如何来呈现RootVisual的具体的步骤,这些都要落在周大哥身上了^-^。如果上文说的太水的地方还请见谅,有什么不对的地方欢迎批评,我一直认为只有认识到自己的不足和狭隘错误才能进步。在这里还要感谢智者千虑,如果不是他的题目和才学,我想我是不会认真去做这个东西,也不会从中发现乐趣。原以为小结才是最重要,在做的时候感概很多,现在居然啥都说不上来。哎~~~

 

附件下载

转载请注明出处

posted @ 2009-12-25 14:18  Curry  阅读(7983)  评论(25编辑  收藏  举报