[{"data":1,"prerenderedAt":2595},["ShallowReactive",2],{"page-\u002Fcsharp\u002Fwpf\u002Fwpf与win32编程":3},{"id":4,"title":5,"body":6,"description":2588,"extension":2589,"meta":2590,"navigation":1421,"path":2591,"seo":2592,"stem":2593,"__hash__":2594},"content\u002Fcsharp\u002FWPF\u002Fwpf与win32编程.md","Wpf与win32编程",{"type":7,"value":8,"toc":2569},"minimark",[9,22,29,37,91,98,126,131,150,156,159,190,195,211,216,234,240,243,343,347,378,381,389,395,404,437,464,475,480,518,532,538,564,571,592,595,605,657,662,722,725,732,834,837,840,846,872,875,881,892,898,914,969,975,978,997,1007,1017,1035,1042,1092,1103,1117,1120,1126,1129,1139,1156,1169,1174,1207,1247,1253,1259,1262,1367,1370,1465,1482,1496,1522,1534,1550,1553,1560,1590,1596,1625,1636,1641,1658,1720,1744,1759,1776,1869,1880,1885,1930,1939,1957,1964,1989,2002,2005,2037,2047,2050,2061,2070,2191,2193,2198,2207,2395,2400,2459,2473,2476,2480,2508,2524,2551,2565],[10,11,12,13,17,18,21],"p",{},"​\tWPF 框架本身已经为我们封装好了 95% 以上常用功能，让我们不必关心底层的窗体句柄（HWND）、消息循环（Message Loop）等复杂细节。但是，总有一些特殊的、更底层的系统功能，",[14,15,16],"strong",{},"WPF 没有直接提供 C# 的接口","。这时，我们就需要“下到地基”，通过 ",[14,19,20],{},"P\u002FInvoke (Platform Invocation，平台调用)"," 技术，直接使用 Win32 API 提供的原生能力。",[23,24,26],"h3",{"id":25},"窗口高级定制与控制-advanced-window-customization-control",[14,27,28],{},"窗口高级定制与控制 (Advanced Window Customization & Control)",[10,30,31,32,36],{},"WPF 的 ",[33,34,35],"code",{},"Window"," 类虽然强大，但并未暴露所有的 Windows 窗口样式。当您想实现一些“反常规”的窗口行为时，就需要 Win32 API。",[38,39,40],"ul",{},[41,42,43,50,51,87,90],"li",{},[14,44,45,46,49],{},"获取窗口句柄 (",[33,47,48],{},"HWND",")",": 这是与几乎所有 Win32 窗口 API 交互的基础。",[38,52,53,70],{},[41,54,55,58,59,61,62,65,66,69],{},[14,56,57],{},"用途",": 将 WPF 窗口句柄传递给其他需要 ",[33,60,48],{}," 的原生库（例如我们之前讨论的 ",[14,63,64],{},"WGC 屏幕采集"," ",[33,67,68],{},"GraphicsCapturePicker","，或者一些渲染引擎）。",[41,71,72,75,76,79,80,83,84],{},[14,73,74],{},"API",": ",[33,77,78],{},"user32.dll"," -> ",[33,81,82],{},"GetWindowLong",", ",[33,85,86],{},"SetWindowLong",[88,89],"br",{},"我们之前有试过平台调用user32.dll的方法来消除alt+tab的窗口显示，这些只有平台调用才能够实现。",[10,92,93,94,97],{},"除此以外，还有很多的可以实现的功能，就比如：",[14,95,96],{},"注册全局热键 (Global Hotkey)",":",[38,99,100,113],{},[41,101,102,104,105,108,109,112],{},[14,103,57],{},": 让您的应用在",[14,106,107],{},"最小化或不处于焦点","时，也能响应一个全局快捷键（例如，",[33,110,111],{},"Ctrl+Alt+P"," 随时随地截图）。这是纯 WPF 无法做到的。",[41,114,115,75,117,79,119,83,122,125],{},[14,116,74],{},[33,118,78],{},[33,120,121],{},"RegisterHotKey",[33,123,124],{},"UnregisterHotKey","。",[10,127,128,97],{},[14,129,130],{},"控制窗口状态",[38,132,133,138],{},[41,134,135,137],{},[14,136,57],{},": 强制将窗口带到最前、在任务栏上闪烁提示用户。 有点像qq那个振动说是。",[41,139,140,75,142,79,144,83,147,125],{},[14,141,74],{},[33,143,78],{},[33,145,146],{},"SetForegroundWindow",[33,148,149],{},"FlashWindowEx",[23,151,153],{"id":152},"底层系统与硬件交互-low-level-system-hardware-interaction",[14,154,155],{},"底层系统与硬件交互 (Low-Level System & Hardware Interaction)",[10,157,158],{},"当您需要获取 WPF 没有直接封装的系统信息或与特定硬件交互时。",[38,160,161],{},[41,162,163,166,167],{},[14,164,165],{},"枚举显示器和获取详细信息",":\n",[38,168,169,178],{},[41,170,171,173,174,177],{},[14,172,57],{},": 获取每个显示器的分辨率、工作区、在虚拟桌面上的位置等。我们之前在",[14,175,176],{},"多显示器 GDI 截图","的例子中就用到了。",[41,179,180,75,182,79,184,83,187,125],{},[14,181,74],{},[33,183,78],{},[33,185,186],{},"EnumDisplayMonitors",[33,188,189],{},"GetMonitorInfoEx",[10,191,192,97],{},[14,193,194],{},"电源管理通知",[38,196,197,202],{},[41,198,199,201],{},[14,200,57],{},": 当系统进入睡眠、休眠或电源模式改变时，让您的应用能够收到通知并做出响应。",[41,203,204,75,206,79,208,125],{},[14,205,74],{},[33,207,78],{},[33,209,210],{},"RegisterPowerSettingNotification",[10,212,213,97],{},[14,214,215],{},"与特定设备通信",[38,217,218,223],{},[41,219,220,222],{},[14,221,57],{},": 与没有现成 .NET 库的硬件通信，例如某些特定的 USB HID 设备。",[41,224,225,75,227,83,230,233],{},[14,226,74],{},[33,228,229],{},"hid.dll",[33,231,232],{},"setupapi.dll"," 中的各种函数。",[23,235,237],{"id":236},"图形与-ui-互操作-graphics-ui-interoperability",[14,238,239],{},"图形与 UI 互操作 (Graphics & UI Interoperability)",[10,241,242],{},"这是最高级、最复杂的领域，也是我们之前讨论最多的。",[38,244,245,285,318],{},[41,246,247,166,250],{},[14,248,249],{},"GDI\u002FGDI+ 绘图",[38,251,252,269],{},[41,253,254,256,257,260,261,268],{},[14,255,57],{},": 进行非常高速的位图操作，或者与只接受 GDI 设备上下文 (",[33,258,259],{},"HDC",") 的旧版代码或库进行交互。我们之前的",[14,262,263,264,267],{},"原生 ",[33,265,266],{},"BitBlt"," 截图","就是典型例子。",[41,270,271,75,273,79,276,83,278,83,281,284],{},[14,272,74],{},[33,274,275],{},"gdi32.dll",[33,277,266],{},[33,279,280],{},"CreateCompatibleDC",[33,282,283],{},"GetDIBits"," 等。",[41,286,287,166,290],{},[14,288,289],{},"DirectX 互操作",[38,291,292,305],{},[41,293,294,296,297,300,301,304],{},[14,295,57],{},": 在 WPF 控件 (",[33,298,299],{},"D3DImage",") 中承载和显示由 DirectX 11\u002F12 渲染的内容。这是 WPF 进行高性能游戏渲染、视频播放、WGC 画面显示等的",[14,302,303],{},"唯一途径","。这个过程深 依赖 Win32 API 来进行设备和资源的共享。",[41,306,307,75,309,83,312,83,315,233],{},[14,308,74],{},[33,310,311],{},"d3d9.dll",[33,313,314],{},"d3d11.dll",[33,316,317],{},"dxgi.dll",[41,319,320,166,323],{},[14,321,322],{},"承载原生 Win32 控件",[38,324,325,333],{},[41,326,327,329,330,332],{},[14,328,57],{},": 在 WPF 应用中嵌入一个由原生 C++ 编写的、只有 ",[33,331,48],{}," 的旧版控件。",[41,334,335,338,339,342],{},[14,336,337],{},"实现",": 通过继承 ",[33,340,341],{},"HwndHost"," 类。",[344,345,346],"h2",{"id":346},"消息机制",[10,348,349,350,353,354,357,358,361,362,365,366,369,370,373,374,377],{},"​\t我觉得刚好我们这里来探讨一下wpf的消息机制。谈起“消息机制”这个词，我们都会想到",[14,351,352],{},"Windows的消息机制","，系统将",[14,355,356],{},"键盘鼠标的行为包装成一个Windows Message","，然后",[14,359,360],{},"系统主动将这些Windows Message派发给特定的窗口","，实际上消息是被Post到特定窗口所在线程的",[14,363,364],{},"消息队列","，应用程序的",[14,367,368],{},"消息循环","再不断的",[14,371,372],{},"从消息队列当中获取消息","，然后再",[14,375,376],{},"派发给特定窗口类的窗口过程来处理","，在窗口过程中完成一次用户交互。",[10,379,380],{},"​\t其实，WPF的底层也是基于Win32的消息系统，那么对于WPF应用程序来说，它是如何跟Win32的消息交互，这里到底存在一个什么样的机制？接下来我会通过下面几篇博文介绍这个消息机制:",[10,382,383],{},[384,385,386],"a",{"href":386,"rel":387},"http:\u002F\u002Fwww.cnblogs.com\u002Fpowertoolsteam\u002Farchive\u002F2010\u002F12\u002F30\u002F1921426.html",[388],"nofollow",[10,390,391,392,125],{},"​\tWPF大部分的对象都是从DispatcherObject派生的，从这里派生的对象具有一个明显的特征，那就是：修改对象时所在的线程，和创建对象时所在线程必须为同一个线程，",[14,393,394],{},"这就是微软所谓的线程亲缘性（Thread affinity）的最简单理解",[10,396,397,398,125,401],{},"​\t这个规则并非 WPF 的独创，而是几乎所有 GUI 框架的共同选择。其根本原因是为了保证 UI 系统的",[14,399,400],{},"稳定、可预测和简化开发",[14,402,403],{},"技术上的原因主要有三点：",[405,406,407,417,427],"ol",{},[41,408,409,412,413,416],{},[14,410,411],{},"防止数据竞争与状态不一致 (Preventing Race Conditions)",": UI 元素（如一个 ",[33,414,415],{},"Button","）有大量的内部状态（宽度、高度、颜色、内容、是否被按下等）。如果允许多个线程同时修改这些状态，就会产生不可预知的后果，例如一个线程正在根据宽度计算布局，另一个线程却突然改变了宽度，导致计算错误甚至程序崩溃。",[41,418,419,422,423,426],{},[14,420,421],{},"简化开发模型 (Simplifying the Development Model)",": 如果没有这个规则，那么开发者在修改任何一个 UI 属性时，都必须手动添加复杂的线程锁 (",[33,424,425],{},"lock",") 来防止多线程冲突。这将使 UI 编程变得极其困难和容易出错，性能也会大打折扣。",[41,428,429,432,433,436],{},[14,430,431],{},"保证用户输入的顺序性 (Guaranteeing Input Order)",": 用户的鼠标点击、键盘输入等操作，都是以消息的形式被操作系统发送到",[14,434,435],{},"一个","特定的线程（UI 线程）上。UI 框架必须在一个线程中按顺序处理这些消息，以保证操作的逻辑正确性。",[10,438,439,440,443,444,447,448,451,452,455,456,459,460,463],{},"​\t然后，我们接下来聊一下STA线程和UI线程的关系。",[33,441,442],{},"STA 线程"," 是对这个线程",[14,445,446],{},"底层工作模式","的一种技术描述，而 ",[33,449,450],{},"UI 线程"," 是从",[14,453,454],{},"应用程序功能层面","对这个线程的称呼。可以说，",[14,457,458],{},"STA 是 UI 线程的“必要属性”或“技术身份证”","。要理解 STA，我们需要追溯到 Windows 的一个更底层、更古老的技术：",[14,461,462],{},"COM (Component Object Model, 组件对象模型)","。 为什么呢？",[10,465,466,467,470,471,474],{},"因为",[14,468,469],{},"STA"," 全称为 ",[14,472,473],{},"Single-Threaded Apartment (单线程套间)","，这是 COM 的一种线程模型！",[10,476,477],{},[14,478,479],{},"我们可以用一个办公室的比喻来理解：",[38,481,482,500],{},[41,483,484,487,488],{},[14,485,486],{},"STA (单线程套间)",": 就像一个**“单人办公室”**。\n",[38,489,490,493],{},[41,491,492],{},"这个办公室里所有的工具和文件（COM 对象），只能由**这一个指定的员工（STA 线程）**来使用。",[41,494,495,496,499],{},"其他办公室的员工（其他线程）如果想用这些工具，不能直接进来拿，必须通过前台（COM ",[14,497,498],{},"封送机制","）递交一个工作请求，由这个办公室的员工亲自来处理。",[41,501,502,505,506],{},[14,503,504],{},"MTA (多线程套间)",": 就像一个**“开放式办公区”**。\n",[38,507,508,511],{},[41,509,510],{},"办公区里的所有工具（COM 对象）都可以被**任何一个员工（任何一个 MTA 线程）**随时使用。",[41,512,513,514,517],{},"这种模式要求工具本身必须非常坚固，能应付多人同时使用（即",[14,515,516],{},"对象本身必须是线程安全的","），否则就会乱套。",[10,519,520,521,524,525,528,529,125],{},"​\tWPF 是一个现代化的框架，但它构建在 Windows 操作系统的基石之上，并且需要与大量",[14,522,523],{},"非线程安全","的底层系统组件进行交互。这些",[14,526,527],{},"组件中的很多都是以 COM 的形式存在的","，并且它们被设计为",[14,530,531],{},"只能在 STA 模式下安全运行",[10,533,534,537],{},[14,535,536],{},"操作系统组件交互 (Interaction with OS Components)",": WPF 的很多功能需要调用底层的 Windows API，例如：",[38,539,540,545,550,555],{},[41,541,542],{},[14,543,544],{},"剪贴板 (Clipboard)",[41,546,547],{},[14,548,549],{},"拖放操作 (Drag-and-Drop)",[41,551,552],{},[14,553,554],{},"文件对话框 (File Dialogs)",[41,556,557,560,561],{},[14,558,559],{},"与 Windows Shell (外壳) 的交互"," 这些功能在底层都是通过非线程安全的 COM 组件实现的，它们",[14,562,563],{},"要求调用它们的线程必须是 STA 线程。",[10,565,566,567,570],{},"​\t",[14,568,569],{},"输入与消息处理 (Input and Message Processing)",": Windows 的用户输入系统（键盘、鼠标）是基于消息队列的。所有输入事件都会被投递到一个特定的线程消息队列中，并由这个线程按顺序处理。这个模型天然就是单线程的，与 STA 模式完美契合。所以我们总结一下就是，",[572,573,574,580,585],"blockquote",{},[10,575,576],{},[577,578,579],"span",{},"!NOTE",[10,581,582],{},[14,583,584],{},"在一个窗口中，所有的 UI 组件（成百上千个按钮、文本框、图片等）都由同一个、唯一的 UI 线程来创建、管理和更新。",[10,586,587],{},[588,589],"img",{"alt":590,"src":591},"image-20250912065523347","image-20250912065523347.png",[23,593,594],{"id":594},"实现方式",[10,596,597,598,604],{},"WPF 通过一个名为 ",[14,599,600,603],{},[33,601,602],{},"Dispatcher"," (调度器)"," 的核心组件来管理这个规则。",[405,606,607,637],{},[41,608,609,615,616,618,619,83,621,623,624,627,628,630,631,633,634,125],{},[14,610,611,614],{},[33,612,613],{},"DispatcherObject"," 的角色",": 提到的 ",[33,617,613],{}," 是这个体系的基石。WPF 中几乎所有的 UI 相关对象（从 ",[33,620,35],{},[33,622,415],{}," 到 ",[33,625,626],{},"Brush","）都继承自 ",[33,629,613],{},"。它最重要的作用就是为每个对象提供一个 ",[33,632,602],{}," 属性，这个属性",[14,635,636],{},"永远指向创建该对象的那个 UI 线程的“调度器”",[41,638,639,75,643,645,646,649,650,656],{},[14,640,641,614],{},[33,642,602],{},[33,644,602],{}," 可以被看作是 UI 线程的**“任务调度中心”",[14,647,648],{},"或","“任务队列”",[14,651,652,653,655],{},"。每个 UI 线程都有且仅有一个 ",[33,654,602],{},"。它的工作就是","维护一个按优先级排序的任务队列**。",[23,658,660],{"id":659},"核心工具",[14,661,659],{},[38,663,664,682,700],{},[41,665,666,671,672,674,675,678,679,125],{},[14,667,668],{},[33,669,670],{},"CheckAccess()",": 一个 ",[33,673,613],{}," 的方法，用于检查“当前代码是否已经在正确的 UI 线程上运行？”。返回 ",[33,676,677],{},"true"," 或 ",[33,680,681],{},"false",[41,683,684,75,689,692,693,695,696,699],{},[14,685,686],{},[33,687,688],{},"Dispatcher.Invoke(...)",[14,690,691],{},"同步","调用。后台线程将任务交给 ",[33,694,602],{}," 后，会",[14,697,698],{},"暂停并一直等待","，直到 UI 线程执行完该任务并返回结果。",[41,701,702,678,707,75,712,692,715,717,718,721],{},[14,703,704],{},[33,705,706],{},"Dispatcher.BeginInvoke(...)",[14,708,709],{},[33,710,711],{},"Dispatcher.InvokeAsync(...)",[14,713,714],{},"异步",[33,716,602],{}," 后，",[14,719,720],{},"不会等待","，而是立即继续执行自己的后续代码。这是更常用的方式，可以避免后台线程被阻塞。",[10,723,724],{},"刚才指的是UI 亲缘性，和我们之前说的CPU亲缘性不一样，差别可以用一个表格来理解：",[10,726,727,728,731],{},"在 WPF 的世界里，您可能更常听到“线程亲缘性”，但这通常指的是一个不同的、",[14,729,730],{},"框架层面","的概念。",[733,734,735,755],"table",{},[736,737,738],"thead",{},[739,740,741,745,750],"tr",{},[742,743,744],"th",{},"方面",[742,746,747],{},[14,748,749],{},"CPU 亲缘性 (OS Level)",[742,751,752],{},[14,753,754],{},"UI 亲缘性 (WPF Framework Level)",[756,757,758,776,795,818],"tbody",{},[739,759,760,766,771],{},[761,762,763],"td",{},[14,764,765],{},"层面",[761,767,768],{},[14,769,770],{},"操作系统内核",[761,772,773],{},[14,774,775],{},"WPF 框架",[739,777,778,783,789],{},[761,779,780],{},[14,781,782],{},"目的",[761,784,785,786,125],{},"性能优化，",[14,787,788],{},"控制线程在哪个 CPU 核心上运行",[761,790,791,792,125],{},"保证 UI 的稳定和一致性，",[14,793,794],{},"规定哪个线程有权修改 UI 元素",[739,796,797,802,808],{},[761,798,799],{},[14,800,801],{},"机制",[761,803,804,807],{},[33,805,806],{},"ProcessThread.ProcessorAffinity"," (位掩码)",[761,809,810,813,814,817],{},[33,811,812],{},"Dispatcher.Invoke"," \u002F ",[33,815,816],{},"BeginInvoke"," (消息队列)",[739,819,820,825,828],{},[761,821,822],{},[14,823,824],{},"规则",[761,826,827],{},"一个线程被绑定到特定 CPU 核心。",[761,829,830,831,833],{},"一个 UI 对象（如 ",[33,832,415],{},"）“属于”创建它的那个线程（UI 线程），其他线程不能直接访问它。",[10,835,836],{},"这两个概念都叫“亲缘性”，但解决的问题完全不同。",[10,838,839],{},"​\t那么谁能保证线程亲缘性呢？那就是Dispacher了。从DispatcherObject派生的类型继承三个重要的成员：Dispatcher属性，CheckAccess(), VerifyAccess()方法。其中后面两个方法就是检验线程亲缘性的。按照WPF的实现，如果你自己定义了个WPF的类型，并且是DispatcherObject的子类，你就必须在public的成员定义的逻辑开始处，调用base.Dispatcher.VerifyAccess()，检验线程亲缘性。那么Dispatcher到底还做了什么事情呢？",[10,841,842],{},[588,843],{"alt":844,"src":845},"clip_image002","201012301025588804.jpg",[10,847,848,849,852,853,856,857,855,860,863,864,867,868,871],{},"​\t通过调用堆栈可以看出，蓝色的部分是启动了一个线程，VisualStudio在Host的进程当中运行当前应用程序；红色的部分是从",[14,850,851],{},"Application.Main函数开始执行","，经过几个函数到达",[14,854,855],{},"Dispatcher.Run()","，最后到达**Dispather.PushFrameInpl()",[14,858,859],{},"方法。那么一个Application在Run之后,为什么要调用",[14,861,862],{},"呢，他做了些什么事情你？如果通过","Reflector仔细查看Application.Run()，**你会发现里面实际起作用的代码并不多，",[14,865,866],{},"最后都是Dispatcher.Run在做事情","。那么一个Application启动之后，",[14,869,870],{},"按照以前对Win32的消息机制的理解，当应用程序启动后，必须进入消息循环，对于WPF，也是一样的","。那么WPF应用程序是在什么地方进入消息循环呢？其实这就是Dispatcher.Run()做的事情。",[10,873,874],{},"​\t查看上图最后一步Dispacther.PushFrameImpl()的代码，你会看到有下面的一段代码：",[10,876,877],{},[588,878],{"alt":879,"src":880},"clip_image004","201012301026044240.jpg",[10,882,883,884,887,888,891],{},"​\t很明显，橙色的部分是一个循环，看起来是不是很眼熟，跟Win32编程碰到的消息循环是否很像？对了，",[14,885,886],{},"这就是WPF应用程序进入了消息循环","。循环调用",[14,889,890],{},"GetMessage方法从当前线程的消息队列当中不停的获取消息","，取出一个msg之后，交给TranslateAndDispatchMessage方法Dispatch到不同的窗口过程去处理。这样以来，任何需要应用程序处理的消息通过这个过程，被不同的窗口处理了，应用程序就动起来了。",[10,893,894,895],{},"​\t其实这一点和js的 event loop机制很像，它们都遵循了同一种基础的、非常重要的计算机科学模型——",[14,896,897],{},"“事件驱动的单线程并发模型”",[10,899,900,901,904,905,908,909],{},"，它们都通过一个",[14,902,903],{},"任务队列来避免多线程直接操作共享资源（UI 或 DOM）所带来的混乱","。因为上面也说过了，WPF 作为一个构建在 Windows 操作系统之上的框架，它的所有窗口和用户输入都必须基于底层的 Win32 消息队列，",[14,906,907],{},"WPF是需要和windows的消息队列打交道","，         ",[14,910,31,911,913],{},[33,912,602],{}," 可以看作是对这个底层 Win32 消息队列的一个更高级、更强大、带有优先级的 .NET 封装。",[10,915,566,916,919,920,923,924,927,928,931,932,935,936,939,940,943,944,947,948,951,952,65,954,957,958,960,961,964,965,968],{},[33,917,918],{},"DispatchMessage(&msg)"," 将一个底层的 ",[14,921,922],{},"Win32 消息","（比如 ",[33,925,926],{},"WM_LBUTTONDOWN","，鼠标",[14,929,930],{},"左键按下","）发送给",[14,933,934],{},"对应的 WPF 窗口","。然后呢，WPF 内部的互操作层 (",[33,937,938],{},"HwndSource",") 接收到这个 Win32 消息。它将这个底层消息",[14,941,942],{},"翻译","成一个高级的 WPF 事件（比如 ",[33,945,946],{},"Button.Click"," 事件）。然后，它将这个事件的执行，作为一个任务项，",[14,949,950],{},"提交","到 UI 线程自己的 ",[33,953,602],{},[14,955,956],{},"优先级队列","中。消息循环继续，",[33,959,602],{}," 在合适的时机（根据优先级）从队列中取出这个任务并",[14,962,963],{},"执行","它（例如，调用您在 C# 中写的 ",[33,966,967],{},"Button_Click"," 事件处理代码）。",[10,970,971],{},[588,972],{"alt":973,"src":974},"image-20250912070811524","image-20250912070811524.png",[23,976,977],{"id":977},"顶级渲染事件",[10,979,980,981,984,985,988,989,992,993,996],{},"​\t我这里再说一下顶级渲染事件的关系，",[33,982,983],{},"CompositionTarget.Rendering"," 是 WPF 渲染引擎对外暴露的一个**“心跳”事件**。",[14,986,987],{},"定义",": 它是一个",[14,990,991],{},"静态事件","，会在 WPF ",[14,994,995],{},"即将渲染（绘制）一帧新画面之前","被触发。",[10,998,999,1002,1003,1006],{},[14,1000,1001],{},"频率",": 它的触发频率与您的",[14,1004,1005],{},"屏幕刷新率","（通常是 60Hz，即每秒约 60 次）以及系统的当前负载保持同步。",[10,1008,1009,1012,1013,1016],{},[14,1010,1011],{},"线程",": 它总是在",[14,1014,1015],{},"主 UI 线程","上被触发。",[10,1018,566,1019,1021,1022,1024,1025,1028,1029,1031,1032,1034],{},[33,1020,983],{}," 事件可以被理解为在 ",[33,1023,602],{}," 队列中",[14,1026,1027],{},"拥有极高优先级的事件","。我们知道，WPF 的 UI 线程由一个 ",[33,1030,602],{}," 来管理，",[33,1033,602],{}," 像一个任务队列，并按优先级处理任务。",[10,1036,1037],{},[14,1038,1039,1041],{},[33,1040,602],{}," 的优先级从高到低大致如下：",[405,1043,1044,1050,1056,1062,1074,1080,1086],{},[41,1045,1046,1049],{},[33,1047,1048],{},"Send"," (内部使用)",[41,1051,1052,1055],{},[33,1053,1054],{},"Input"," (处理键盘、鼠标输入)",[41,1057,1058,1061],{},[33,1059,1060],{},"Loaded"," (处理控件加载事件)",[41,1063,1064,65,1070,1073],{},[14,1065,1066,1069],{},[33,1067,1068],{},"Render"," (处理布局和渲染)",[33,1071,1072],{},"← CompositionTarget.Rendering"," 在这里触发",[41,1075,1076,1079],{},[33,1077,1078],{},"DataBind"," (处理数据绑定更新)",[41,1081,1082,1085],{},[33,1083,1084],{},"Background"," (普通后台任务)",[41,1087,1088,1091],{},[33,1089,1090],{},"Idle"," (系统空闲时)",[10,1093,566,1094,1096,1097,1099,1100,1102],{},[33,1095,983],{}," 事件的触发，是 ",[33,1098,602],{}," 在处理其**",[33,1101,1068],{}," 优先级**任务时的一个核心环节。",[10,1104,1105,1106,1108,1109,1112,1113,1116],{},"​\t这意味着，当 ",[33,1107,983],{}," 事件触发时，UI 线程会",[14,1110,1111],{},"优先处理","您的 ",[33,1114,1115],{},"CompositionTarget_Rendering"," 方法。执行您的代码是渲染流程的一部分，它的优先级高于普通的数据绑定、后台任务等。",[10,1118,1119],{},"​\t除了这个高优先级的事件以外，开发者还有很多经常使用的事件，",[10,1121,1122,1125],{},[14,1123,1124],{},"生命周期事件 (Lifecycle Events)","，这类事件与控件或窗口的“生老病死”有关，让您可以在正确的时机执行初始化或清理代码。",[10,1127,1128],{},"**用户输入事件 (User Input Events)**这类事件是应用程序响应用户操作的核心。",[10,1130,1131,1138],{},[14,1132,1133,1134,1137],{},"路由事件 (Routed Events)",[14,1135,1136],{},"WPF 的","大多数输入事件都是“路由事件”","，这个机制允许您在父控件上拦截和处理子控件的事件，非常强大。它们有两种传播方式：",[10,1140,566,1141,1144,1145,1148,1149,1152,1153,125],{},[14,1142,1143],{},"隧穿 (Tunneling)",": 事件从顶层元素（窗口）",[14,1146,1147],{},"向下","传播到鼠标所在的具体元素。这类事件通常以 ",[33,1150,1151],{},"Preview"," 开头，例如 ",[33,1154,1155],{},"PreviewMouseLeftButtonDown",[10,1157,566,1158,1161,1162,1165,1166,125],{},[14,1159,1160],{},"冒泡 (Bubbling)",": 事件从鼠标所在的具体元素",[14,1163,1164],{},"向上","传播到顶层元素（窗口）。这就是常规的事件，例如 ",[33,1167,1168],{},"MouseLeftButtonDown",[10,1170,1171,97],{},[14,1172,1173],{},"键盘事件",[38,1175,1176,1187],{},[41,1177,1178,1186],{},[14,1179,1180,813,1183],{},[33,1181,1182],{},"KeyDown",[33,1184,1185],{},"KeyUp",": 底层的键盘按下\u002F抬起事件。可以捕获所有按键，包括功能键（如 F1, Ctrl, Shift）。",[41,1188,1189,1194,1195,1198,1199,1204,1205,125],{},[14,1190,1191],{},[33,1192,1193],{},"TextInput",": 更高级的文本输入事件。它只在",[14,1196,1197],{},"可显示的字符","被输入时触发，并且能正确处理输入法（IME）的输入。",[14,1200,1201,1202],{},"对于获取文本输入，应优先使用 ",[33,1203,1193],{}," 而不是 ",[33,1206,1182],{},[10,1208,1209,1210,1212,1213,1216,1217,1220,1221,1224,1225,125,1228,1231,1232,1235,1236,1238,1239,1242,1243,1246],{},"​\t让我们来完整地追踪一次鼠标左键点击 ",[33,1211,415],{}," 的过程，看看这两个阶段是如何衔接的：",[14,1214,1215],{},"Windows 内核",": 内核根据 ",[33,1218,1219],{},"(X, Y)"," 坐标和当前窗口的 Z-order（层叠顺序），判断出鼠标指针",[14,1222,1223],{},"正下方的是您的 WPF 应用程序的窗口","。它知道了这个窗口的",[14,1226,1227],{},"句柄 (HWND)",[14,1229,1230],{},"Win32 消息队列",": Windows 系统将这个点击事件打包成一个",[14,1233,1234],{},"窗口消息","（例如 ",[33,1237,926],{},"），然后将这个消息“",[14,1240,1241],{},"投递","”到创建了该窗口的",[14,1244,1245],{},"线程的消息队列","中。",[10,1248,1249],{},[588,1250],{"alt":1251,"src":1252},"image-20250912162416024","image-20250912162416024.png",[23,1254,1256],{"id":1255},"数据与属性变化事件-data-property-change-events",[14,1257,1258],{},"数据与属性变化事件 (Data & Property Change Events)",[10,1260,1261],{},"这类事件是实现动态 UI 和 MVVM 模式的关键。",[38,1263,1264,1307,1336],{},[41,1265,1266,1271],{},[14,1267,1268],{},[33,1269,1270],{},"SelectionChanged",[38,1272,1273,1295,1301],{},[41,1274,1275,75,1278,83,1281,83,1284,83,1287,1290,1291,1294],{},[14,1276,1277],{},"触发对象",[33,1279,1280],{},"ListBox",[33,1282,1283],{},"ComboBox",[33,1285,1286],{},"DataGrid",[33,1288,1289],{},"TabControl"," 等所有继承自 ",[33,1292,1293],{},"Selector"," 的控件。",[41,1296,1297,1300],{},[14,1298,1299],{},"何时触发",": 当用户选择了一个新的项目时。",[41,1302,1303,1306],{},[14,1304,1305],{},"典型用途",": 根据用户的选择，更新界面的其他部分。例如，在一个列表中选择一个用户，右侧显示该用户的详细信息。",[41,1308,1309,1314],{},[14,1310,1311],{},[33,1312,1313],{},"TextChanged",[38,1315,1316,1326,1331],{},[41,1317,1318,75,1320,83,1323,284],{},[14,1319,1277],{},[33,1321,1322],{},"TextBox",[33,1324,1325],{},"RichTextBox",[41,1327,1328,1330],{},[14,1329,1299],{},": 文本框中的内容发生任何改变时。",[41,1332,1333,1335],{},[14,1334,1305],{},": 实现搜索框的实时搜索\u002F筛选功能、统计输入字数、实时验证输入内容等。",[41,1337,1338,1343],{},[14,1339,1340],{},[33,1341,1342],{},"INotifyPropertyChanged.PropertyChanged",[38,1344,1345],{},[41,1346,1347,1350,1351,1354,1355,1358,1359,1362,1363,1366],{},[14,1348,1349],{},"这不是一个路由事件，而是 MVVM 模式的“心脏”","。当您在 ViewModel 中实现 ",[33,1352,1353],{},"INotifyPropertyChanged"," 接口后，每当一个属性的值发生变化并调用 ",[33,1356,1357],{},"OnPropertyChanged()"," 时，就会触发 ",[33,1360,1361],{},"PropertyChanged"," 事件。WPF 的数据绑定系统会",[14,1364,1365],{},"订阅","这个事件，一旦接收到通知，就会自动更新绑定到该属性的 UI 界面。",[10,1368,1369],{},"我们来看一个数据与属性的变化事件的案例：",[1371,1372,1377],"pre",{"className":1373,"code":1374,"language":1375,"meta":1376,"style":1376},"language-csharp shiki shiki-themes github-light github-dark","    private void OnNavigationSelectionChanged(object sender, RoutedEventArgs e)\n    {\n        if (sender is not NavigationView navigationView)\n        {\n            return;\n        }\n\n        navigationView.SetCurrentValue(\n            NavigationView.HeaderVisibilityProperty,\n            navigationView.SelectedItem?.TargetPageType != typeof(DashboardPage)\n                ? Visibility.Visible\n                : Visibility.Collapsed\n        );\n    }\n","csharp","",[33,1378,1379,1386,1392,1398,1404,1410,1416,1423,1429,1435,1441,1447,1453,1459],{"__ignoreMap":1376},[577,1380,1383],{"class":1381,"line":1382},"line",1,[577,1384,1385],{},"    private void OnNavigationSelectionChanged(object sender, RoutedEventArgs e)\n",[577,1387,1389],{"class":1381,"line":1388},2,[577,1390,1391],{},"    {\n",[577,1393,1395],{"class":1381,"line":1394},3,[577,1396,1397],{},"        if (sender is not NavigationView navigationView)\n",[577,1399,1401],{"class":1381,"line":1400},4,[577,1402,1403],{},"        {\n",[577,1405,1407],{"class":1381,"line":1406},5,[577,1408,1409],{},"            return;\n",[577,1411,1413],{"class":1381,"line":1412},6,[577,1414,1415],{},"        }\n",[577,1417,1419],{"class":1381,"line":1418},7,[577,1420,1422],{"emptyLinePlaceholder":1421},true,"\n",[577,1424,1426],{"class":1381,"line":1425},8,[577,1427,1428],{},"        navigationView.SetCurrentValue(\n",[577,1430,1432],{"class":1381,"line":1431},9,[577,1433,1434],{},"            NavigationView.HeaderVisibilityProperty,\n",[577,1436,1438],{"class":1381,"line":1437},10,[577,1439,1440],{},"            navigationView.SelectedItem?.TargetPageType != typeof(DashboardPage)\n",[577,1442,1444],{"class":1381,"line":1443},11,[577,1445,1446],{},"                ? Visibility.Visible\n",[577,1448,1450],{"class":1381,"line":1449},12,[577,1451,1452],{},"                : Visibility.Collapsed\n",[577,1454,1456],{"class":1381,"line":1455},13,[577,1457,1458],{},"        );\n",[577,1460,1462],{"class":1381,"line":1461},14,[577,1463,1464],{},"    }\n",[10,1466,1467,1468,1471,1472,1475,1476,1478,1479,1481],{},"​\t这个是WPF-UI的导航栏的控制的实现。",[33,1469,1470],{},"OnNavigationSelectionChanged"," 确实是一个响应“数据变化”（被选项 ",[33,1473,1474],{},"SelectedItem"," 变化）的事件，它属于我们昨天讨论的**“数据与属性变化事件”**大类。它在功能层面与 ",[33,1477,1280],{}," 的 ",[33,1480,1270],{}," 事件非常相似",[10,1483,1484,1485,1488,1489,1492,1493,125],{},"来看一下两个行参：第一个就是:它指的是",[14,1486,1487],{},"触发这个事件的对象","，在代码里就是那个 ",[33,1490,1491],{},"NavigationView"," 控件",[14,1494,1495],{},"实例",[10,1497,1498,1499,1505,1506,1513,1514,1517,1518,1521],{},"​\tRoutedEventArgs e：",[14,1500,1501,1504],{},[33,1502,1503],{},"e"," 是一个包含了与该事件相关的所有","附带信息",[14,1507,1508,1509,1512],{},"的对象（例如 ",[33,1510,1511],{},"e.Source"," 指向","最初触发事件的元素**，",[33,1515,1516],{},"e.Handled"," 用于",[14,1519,1520],{},"标记事件是否已被处理","等）。",[10,1523,1524,1525,125,1528],{},"​\t然后我们来讲一下同步上下文的基本操作，提供在各种同步模型中传播同步上下文的基本功能。同步上下文的工作就是",[14,1526,1527],{},"确保调用在正确的线程上执行",[14,1529,1530,1533],{},[33,1531,1532],{},"Current"," 获取当前同步上下文",[572,1535,1536,1540],{},[10,1537,1538],{},[577,1539,579],{},[10,1541,1542,1543,1546,1547,125],{},"当一个 UI 线程（WPF 或 Windows Forms）启动时，.NET 框架会自动在这个线程上“注册”一个与之关联的",[33,1544,1545],{},"SynchronizationContext"," 实例。在 WPF 中，这个实例是 ",[33,1548,1549],{},"DispatcherSynchronizationContext",[10,1551,1552],{},"var context = SynchronizationContext.Current;",[10,1554,1555],{},[14,1556,1557,1559],{},[33,1558,1048],{}," 一个同步消息调度到一个同步上下文。",[1371,1561,1563],{"className":1373,"code":1562,"language":1375,"meta":1376,"style":1376},"SendOrPostCallback callback = o =>\n                                 {\n                                     \u002F\u002FTODO:\n                                 };\ncontext.Send(callback,null);\n",[33,1564,1565,1570,1575,1580,1585],{"__ignoreMap":1376},[577,1566,1567],{"class":1381,"line":1382},[577,1568,1569],{},"SendOrPostCallback callback = o =>\n",[577,1571,1572],{"class":1381,"line":1388},[577,1573,1574],{},"                                 {\n",[577,1576,1577],{"class":1381,"line":1394},[577,1578,1579],{},"                                     \u002F\u002FTODO:\n",[577,1581,1582],{"class":1381,"line":1400},[577,1583,1584],{},"                                 };\n",[577,1586,1587],{"class":1381,"line":1406},[577,1588,1589],{},"context.Send(callback,null);\n",[10,1591,1592,1593],{},"send调用后会阻塞直到调用完成。",[14,1594,1595],{},"Post 将异步消息调度到一个同步上下文。",[1371,1597,1599],{"className":1373,"code":1598,"language":1375,"meta":1376,"style":1376},"SendOrPostCallback callback = o =>\n                                {\n                                      \u002F\u002FTODO:\n                                };\ncontext.Post(callback,null);\n",[33,1600,1601,1605,1610,1615,1620],{"__ignoreMap":1376},[577,1602,1603],{"class":1381,"line":1382},[577,1604,1569],{},[577,1606,1607],{"class":1381,"line":1388},[577,1608,1609],{},"                                {\n",[577,1611,1612],{"class":1381,"line":1394},[577,1613,1614],{},"                                      \u002F\u002FTODO:\n",[577,1616,1617],{"class":1381,"line":1400},[577,1618,1619],{},"                                };\n",[577,1621,1622],{"class":1381,"line":1406},[577,1623,1624],{},"context.Post(callback,null);\n",[10,1626,1627,1628,1631,1632,1635],{},"和",[33,1629,1630],{},"send","的调用方法一样，不过",[33,1633,1634],{},"Post","会启动一个线程来调用，不会阻塞当前线程。",[10,1637,1638],{},[14,1639,1640],{},"使用同步上下文来更新UI内容",[10,1642,1643,1644,1627,1647,1650,1653,1654,1657],{},"无论",[33,1645,1646],{},"WinFroms",[33,1648,1649],{},"WPF",[14,1651,1652],{},"都只能用UI线程来更新界面的内容"," 常用的调用UI更新方法是",[33,1655,1656],{},"Inovke","（WinFroms）：",[1371,1659,1661],{"className":1373,"code":1660,"language":1375,"meta":1376,"style":1376},"private void button_Click(object sender, EventArgs e)\n{\n       ThreadPool.QueueUserWorkItem(BackgroudRun);\n}\n\nprivate void BackgroudRun2(object state)\n{\n            this.Invoke(new Action(() =>\n                                       {\n                                           label1.Text = \"Hello Invoke\";\n                                       }));\n}\n\n",[33,1662,1663,1668,1673,1678,1683,1687,1692,1696,1701,1706,1711,1716],{"__ignoreMap":1376},[577,1664,1665],{"class":1381,"line":1382},[577,1666,1667],{},"private void button_Click(object sender, EventArgs e)\n",[577,1669,1670],{"class":1381,"line":1388},[577,1671,1672],{},"{\n",[577,1674,1675],{"class":1381,"line":1394},[577,1676,1677],{},"       ThreadPool.QueueUserWorkItem(BackgroudRun);\n",[577,1679,1680],{"class":1381,"line":1400},[577,1681,1682],{},"}\n",[577,1684,1685],{"class":1381,"line":1406},[577,1686,1422],{"emptyLinePlaceholder":1421},[577,1688,1689],{"class":1381,"line":1412},[577,1690,1691],{},"private void BackgroudRun2(object state)\n",[577,1693,1694],{"class":1381,"line":1418},[577,1695,1672],{},[577,1697,1698],{"class":1381,"line":1425},[577,1699,1700],{},"            this.Invoke(new Action(() =>\n",[577,1702,1703],{"class":1381,"line":1431},[577,1704,1705],{},"                                       {\n",[577,1707,1708],{"class":1381,"line":1437},[577,1709,1710],{},"                                           label1.Text = \"Hello Invoke\";\n",[577,1712,1713],{"class":1381,"line":1443},[577,1714,1715],{},"                                       }));\n",[577,1717,1718],{"class":1381,"line":1449},[577,1719,1682],{},[10,1721,1722,1723,1726,1727,1729,1730,1733,1734,1736,1737,1743],{},"​\t使用同步上下文也可以",[14,1724,1725],{},"实现相同的效果","，WinFroms和WPF继承了",[33,1728,1545],{},"，使",[14,1731,1732],{},"同步上下文","能够在UI线程或者",[33,1735,602],{},"线程上正确执行。顺便说一下：",[14,1738,1739,1740,1742],{},"由 UI 元素触发的事件处理器（如 ",[33,1741,967],{},"）默认是在 UI 线程上被调用的","。  调用到后台线程中去有两个比较好的办法，一个是使用await Task.Run的方式抛到后台线程，",[1371,1745,1747],{"className":1373,"code":1746,"language":1375,"meta":1376,"style":1376},"System.Windows.Forms. WindowsFormsSynchronizationContext\nSystem.Windows.Threading. DispatcherSynchronizationContext\n",[33,1748,1749,1754],{"__ignoreMap":1376},[577,1750,1751],{"class":1381,"line":1382},[577,1752,1753],{},"System.Windows.Forms. WindowsFormsSynchronizationContext\n",[577,1755,1756],{"class":1381,"line":1388},[577,1757,1758],{},"System.Windows.Threading. DispatcherSynchronizationContext\n",[10,1760,1761,1762,1765,1766,1769,1770,1775],{},"​\t调用方法如下：这段代码，是一段非常经典的、在 ",[33,1763,1764],{},"async\u002Fawait"," 出现之前，用于",[14,1767,1768],{},"从后台线程安全地更新 UI 线程","的示例，它的核心是利用了 .NET 中一个名为 ",[14,1771,1772,1774],{},[33,1773,1545],{}," (同步上下文)"," 的强大抽象机制。",[1371,1777,1779],{"className":1373,"code":1778,"language":1375,"meta":1376,"style":1376},"private void button_Click(object sender, EventArgs e)\n{\n           var context = SynchronizationContext.Current; \u002F\u002F捕获当前线程（UI 线程）的‘专属信使\n           Debug.Assert(context != null);\n           ThreadPool.QueueUserWorkItem(BackgroudRun, context); \u002F\u002F启动一个后台任务，并将我们刚刚捕获的‘信使’作为参数传递过去。会从 .NET 的线程池中取出一个后台线程，并让它执行 BackgroudRun 这个方法。我们通过 state 参数，巧妙地将 UI 线程的“信使”(context) 对象，传递给了这个即将在后台运行的方法。\n}\n\nprivate void BackgroudRun(object state)\n{\n    var context = state as SynchronizationContext; \u002F\u002F传入的同步上下文，后台线程现在拥有了一个可以与 UI 线程沟通的“联络工具”。\n    Debug.Assert(context != null);\n    SendOrPostCallback callback = o => \u002F\u002F创建一个‘工作任务便签’ (callback)。”\n                                      {\n                                          label1.Text = \"Hello SynchronizationContext\";\n                                      };\n    \u002F\u002FSendOrPostCallback 是一个委托类型。我们在这里定义了一个匿名方法，它包含了我们真正想在 UI 线程上执行的操作：label1.Text = \"Hello SynchronizationContext\";。\n    context.Send(callback,null); \u002F\u002F调用,后台线程通过‘信使’，同步地将任务派发给 UI 线程。后台线程把这张“便签” (callback) 交给了 UI 线程的“信使” (context)，并对他说：“请你立即把这个任务送到你的主人（UI线程）那里去执行，我在这里等着，直到你确认他做完了我才继续。”\n}\n\n",[33,1780,1781,1785,1789,1794,1799,1804,1808,1812,1817,1821,1826,1831,1836,1841,1846,1852,1858,1864],{"__ignoreMap":1376},[577,1782,1783],{"class":1381,"line":1382},[577,1784,1667],{},[577,1786,1787],{"class":1381,"line":1388},[577,1788,1672],{},[577,1790,1791],{"class":1381,"line":1394},[577,1792,1793],{},"           var context = SynchronizationContext.Current; \u002F\u002F捕获当前线程（UI 线程）的‘专属信使\n",[577,1795,1796],{"class":1381,"line":1400},[577,1797,1798],{},"           Debug.Assert(context != null);\n",[577,1800,1801],{"class":1381,"line":1406},[577,1802,1803],{},"           ThreadPool.QueueUserWorkItem(BackgroudRun, context); \u002F\u002F启动一个后台任务，并将我们刚刚捕获的‘信使’作为参数传递过去。会从 .NET 的线程池中取出一个后台线程，并让它执行 BackgroudRun 这个方法。我们通过 state 参数，巧妙地将 UI 线程的“信使”(context) 对象，传递给了这个即将在后台运行的方法。\n",[577,1805,1806],{"class":1381,"line":1412},[577,1807,1682],{},[577,1809,1810],{"class":1381,"line":1418},[577,1811,1422],{"emptyLinePlaceholder":1421},[577,1813,1814],{"class":1381,"line":1425},[577,1815,1816],{},"private void BackgroudRun(object state)\n",[577,1818,1819],{"class":1381,"line":1431},[577,1820,1672],{},[577,1822,1823],{"class":1381,"line":1437},[577,1824,1825],{},"    var context = state as SynchronizationContext; \u002F\u002F传入的同步上下文，后台线程现在拥有了一个可以与 UI 线程沟通的“联络工具”。\n",[577,1827,1828],{"class":1381,"line":1443},[577,1829,1830],{},"    Debug.Assert(context != null);\n",[577,1832,1833],{"class":1381,"line":1449},[577,1834,1835],{},"    SendOrPostCallback callback = o => \u002F\u002F创建一个‘工作任务便签’ (callback)。”\n",[577,1837,1838],{"class":1381,"line":1455},[577,1839,1840],{},"                                      {\n",[577,1842,1843],{"class":1381,"line":1461},[577,1844,1845],{},"                                          label1.Text = \"Hello SynchronizationContext\";\n",[577,1847,1849],{"class":1381,"line":1848},15,[577,1850,1851],{},"                                      };\n",[577,1853,1855],{"class":1381,"line":1854},16,[577,1856,1857],{},"    \u002F\u002FSendOrPostCallback 是一个委托类型。我们在这里定义了一个匿名方法，它包含了我们真正想在 UI 线程上执行的操作：label1.Text = \"Hello SynchronizationContext\";。\n",[577,1859,1861],{"class":1381,"line":1860},17,[577,1862,1863],{},"    context.Send(callback,null); \u002F\u002F调用,后台线程通过‘信使’，同步地将任务派发给 UI 线程。后台线程把这张“便签” (callback) 交给了 UI 线程的“信使” (context)，并对他说：“请你立即把这个任务送到你的主人（UI线程）那里去执行，我在这里等着，直到你确认他做完了我才继续。”\n",[577,1865,1867],{"class":1381,"line":1866},18,[577,1868,1682],{},[23,1870,1872],{"id":1871},"send-vs-post同步与异步的区别",[14,1873,1874,1876,1877,1879],{},[33,1875,1048],{}," vs. ",[33,1878,1634],{},"：同步与异步的区别",[10,1881,1882,1884],{},[33,1883,1545],{}," 提供了两种“派发”任务的方式：",[38,1886,1887,1910],{},[41,1888,1889,166,1894],{},[14,1890,1891,1893],{},[33,1892,1048],{}," (同步)",[38,1895,1896,1901],{},[41,1897,1898,1899,125],{},"相当于 ",[33,1900,812],{},[41,1902,1903,1906,1907,125],{},[14,1904,1905],{},"阻塞","调用线程，等待任务在",[14,1908,1909],{},"目标线程完成",[41,1911,1912,166,1917],{},[14,1913,1914,1916],{},[33,1915,1634],{}," (异步)",[38,1918,1919,1924],{},[41,1920,1898,1921,125],{},[33,1922,1923],{},"Dispatcher.BeginInvoke",[41,1925,1926,1929],{},[14,1927,1928],{},"不阻塞","调用线程，把任务“投递”到目标线程的队列后就立刻返回，是“即发即忘”。",[23,1931,1933],{"id":1932},"与-asyncawait-的关系",[14,1934,1935,1936,1938],{},"与 ",[33,1937,1764],{}," 的关系",[10,1940,1941,1942,1945,1946,678,1948,1950,1951],{},"​\t现在看到的这个手动捕获 ",[33,1943,1944],{},"context"," 并调用 ",[33,1947,1048],{},[33,1949,1634],{}," 的过程，",[14,1952,1953,1954,1956],{},"正是 ",[33,1955,1764],{}," 关键字在 UI 线程上为我们自动完成的工作！",[10,1958,1959,1960,1963],{},"当您在一个 UI 线程的方法中 ",[33,1961,1962],{},"await"," 一个任务时：",[405,1965,1966,1972,1983],{},[41,1967,1968,1969,125],{},"C# 编译器会自动生成代码来捕获 ",[33,1970,1971],{},"SynchronizationContext.Current",[41,1973,1974,1975,1977,1978,1478,1980,1982],{},"当 ",[33,1976,1962],{}," 的后台任务完成后，编译器会自动调用这个 ",[33,1979,1944],{},[33,1981,1634],{}," 方法。",[41,1984,1985,1986,1988],{},"它将 ",[33,1987,1962],{}," 之后的所有代码打包成一个回调，安全地送回 UI 线程继续执行。",[10,1990,1991,1992,1994,1995,1998,1999,2001],{},"​\t所以，",[33,1993,1764],{}," 是这个底层模式的一个",[14,1996,1997],{},"极其优雅的语法糖 (Syntactic Sugar)","，它让开发者无需再手动管理 ",[33,2000,1545],{},"，极大地简化了异步 UI 编程。",[23,2003,1923],{"id":2004},"dispatcherbegininvoke",[10,2006,2007,2008,2011,2012,2014,2015,2018,2019,2022,2023,2025,2026,2029,2030,2033,2034,2036],{},"​\t这是 WPF 线程模型中一个非常核心且常用的方法。它是在",[14,2009,2010],{},"后台线程","和 ",[14,2013,450],{},"之间进行",[14,2016,2017],{},"异步通信","的关键桥梁。",[14,2020,2021],{},"一句话总结","：",[33,2024,1923],{}," 的作用是，将一个方法（委托）",[14,2027,2028],{},"异步地","安排到 ",[14,2031,2032],{},"UI 线程的任务队列中去执行","，而",[14,2035,1928],{},"当前正在执行的后台线程。",[10,2038,2039,2040,2042,2043,2046],{},"这个过程就是“",[14,2041,714],{},"”和“",[14,2044,2045],{},"非阻塞","”的，我们通常称之为**“即发即忘” (Fire and Forget)** 模式。",[2048,2049],"hr",{},[23,2051,2053],{"id":2052},"begininvoke-vs-invoke关键区别",[14,2054,2055,1876,2057,2060],{},[33,2056,816],{},[33,2058,2059],{},"Invoke","：关键区别",[10,2062,2063,2064,2066,2067,2069],{},"理解 ",[33,2065,816],{}," 最好的方式就是将它与它的“兄弟”方法 ",[33,2068,2059],{}," 进行对比。",[733,2071,2072,2087],{},[736,2073,2074],{},[739,2075,2076,2079,2083],{},[742,2077,2078],{},"特性",[742,2080,2081,1893],{},[33,2082,812],{},[742,2084,2085,1916],{},[33,2086,1923],{},[756,2088,2089,2106,2125,2148,2167],{},[739,2090,2091,2096,2101],{},[761,2092,2093],{},[14,2094,2095],{},"执行方式",[761,2097,2098],{},[14,2099,2100],{},"同步 (Synchronous)",[761,2102,2103],{},[14,2104,2105],{},"异步 (Asynchronous)",[739,2107,2108,2113,2119],{},[761,2109,2110],{},[14,2111,2112],{},"调用线程",[761,2114,2115,2118],{},[14,2116,2117],{},"被阻塞 (Blocks)","。后台线程会暂停，直到 UI 线程执行完该任务。",[761,2120,2121,2124],{},[14,2122,2123],{},"不被阻塞 (Does not block)","。后台线程提交任务后，立即继续执行自己的代码。",[739,2126,2127,2132,2141],{},[761,2128,2129],{},[14,2130,2131],{},"返回值",[761,2133,2134,2135,2137,2138,125],{},"如果委托有返回值，",[33,2136,2059],{}," 会返回那个",[14,2139,2140],{},"具体的值",[761,2142,2143,2144,2147],{},"返回一个 ",[33,2145,2146],{},"DispatcherOperation"," 对象，代表排队中的任务状态。",[739,2149,2150,2155,2161],{},[761,2151,2152],{},[14,2153,2154],{},"比喻",[761,2156,2157,2160],{},[14,2158,2159],{},"打电话","。您必须在线等待对方给您答复。",[761,2162,2163,2166],{},[14,2164,2165],{},"发邮件\u002F短信","。您发送后就可以去做别的事，对方有空时会处理。",[739,2168,2169,2174,2185],{},[761,2170,2171],{},[14,2172,2173],{},"适用场景",[761,2175,2176,2177,2180,2181,2184],{},"当后台线程",[14,2178,2179],{},"需要","从 UI 元素获取一个值，或者",[14,2182,2183],{},"必须等待"," UI 更新完成后才能继续下一步时。",[761,2186,2187,2190],{},[14,2188,2189],{},"最常用","。当您只需要从后台更新 UI（如更新文本、进度条），而不需要等待其完成时。",[2048,2192],{},[23,2194,2196],{"id":2195},"代码示例",[14,2197,2195],{},[10,2199,2200,2201,2203,2204,125],{},"​\t下面的代码完美地展示了 ",[33,2202,816],{}," 的典型用例：在后台任务中",[14,2205,2206],{},"更新进度条",[1371,2208,2210],{"className":1373,"code":2209,"language":1375,"meta":1376,"style":1376},"public partial class MainWindow : Window\n{\n    public MainWindow()\n    {\n        InitializeComponent();\n    }\n\n    private void StartWorkButton_Click(object sender, RoutedEventArgs e)\n    {\n        \u002F\u002F 启动一个后台任务\n        Task.Run(() =>\n        {\n            for (int i = 1; i \u003C= 100; i++)\n            {\n                \u002F\u002F 模拟耗时的工作\n                Thread.Sleep(50);\n\n                \u002F\u002F 使用 BeginInvoke 异步更新 UI\n                \u002F\u002F 后台线程把更新任务“扔”给 Dispatcher 后，会立刻开始下一次循环\n                \u002F\u002F 而不会等待进度条真的在屏幕上重绘完成\n                this.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, \n                    new Action(() =>\n                    {\n                        MyProgressBar.Value = i;\n                        MyTextBlock.Text = $\"进度: {i}%\";\n                    }));\n            }\n            \n            \u002F\u002F 所有任务提交完毕后，再提交一个最终的完成消息\n            this.Dispatcher.BeginInvoke(() => { \n                MessageBox.Show(\"任务完成！\");\n            });\n        });\n    }\n}\n",[33,2211,2212,2217,2221,2226,2230,2235,2239,2243,2248,2252,2257,2262,2266,2271,2276,2281,2286,2290,2295,2301,2307,2313,2319,2325,2331,2337,2343,2349,2355,2361,2367,2373,2379,2385,2390],{"__ignoreMap":1376},[577,2213,2214],{"class":1381,"line":1382},[577,2215,2216],{},"public partial class MainWindow : Window\n",[577,2218,2219],{"class":1381,"line":1388},[577,2220,1672],{},[577,2222,2223],{"class":1381,"line":1394},[577,2224,2225],{},"    public MainWindow()\n",[577,2227,2228],{"class":1381,"line":1400},[577,2229,1391],{},[577,2231,2232],{"class":1381,"line":1406},[577,2233,2234],{},"        InitializeComponent();\n",[577,2236,2237],{"class":1381,"line":1412},[577,2238,1464],{},[577,2240,2241],{"class":1381,"line":1418},[577,2242,1422],{"emptyLinePlaceholder":1421},[577,2244,2245],{"class":1381,"line":1425},[577,2246,2247],{},"    private void StartWorkButton_Click(object sender, RoutedEventArgs e)\n",[577,2249,2250],{"class":1381,"line":1431},[577,2251,1391],{},[577,2253,2254],{"class":1381,"line":1437},[577,2255,2256],{},"        \u002F\u002F 启动一个后台任务\n",[577,2258,2259],{"class":1381,"line":1443},[577,2260,2261],{},"        Task.Run(() =>\n",[577,2263,2264],{"class":1381,"line":1449},[577,2265,1403],{},[577,2267,2268],{"class":1381,"line":1455},[577,2269,2270],{},"            for (int i = 1; i \u003C= 100; i++)\n",[577,2272,2273],{"class":1381,"line":1461},[577,2274,2275],{},"            {\n",[577,2277,2278],{"class":1381,"line":1848},[577,2279,2280],{},"                \u002F\u002F 模拟耗时的工作\n",[577,2282,2283],{"class":1381,"line":1854},[577,2284,2285],{},"                Thread.Sleep(50);\n",[577,2287,2288],{"class":1381,"line":1860},[577,2289,1422],{"emptyLinePlaceholder":1421},[577,2291,2292],{"class":1381,"line":1866},[577,2293,2294],{},"                \u002F\u002F 使用 BeginInvoke 异步更新 UI\n",[577,2296,2298],{"class":1381,"line":2297},19,[577,2299,2300],{},"                \u002F\u002F 后台线程把更新任务“扔”给 Dispatcher 后，会立刻开始下一次循环\n",[577,2302,2304],{"class":1381,"line":2303},20,[577,2305,2306],{},"                \u002F\u002F 而不会等待进度条真的在屏幕上重绘完成\n",[577,2308,2310],{"class":1381,"line":2309},21,[577,2311,2312],{},"                this.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, \n",[577,2314,2316],{"class":1381,"line":2315},22,[577,2317,2318],{},"                    new Action(() =>\n",[577,2320,2322],{"class":1381,"line":2321},23,[577,2323,2324],{},"                    {\n",[577,2326,2328],{"class":1381,"line":2327},24,[577,2329,2330],{},"                        MyProgressBar.Value = i;\n",[577,2332,2334],{"class":1381,"line":2333},25,[577,2335,2336],{},"                        MyTextBlock.Text = $\"进度: {i}%\";\n",[577,2338,2340],{"class":1381,"line":2339},26,[577,2341,2342],{},"                    }));\n",[577,2344,2346],{"class":1381,"line":2345},27,[577,2347,2348],{},"            }\n",[577,2350,2352],{"class":1381,"line":2351},28,[577,2353,2354],{},"            \n",[577,2356,2358],{"class":1381,"line":2357},29,[577,2359,2360],{},"            \u002F\u002F 所有任务提交完毕后，再提交一个最终的完成消息\n",[577,2362,2364],{"class":1381,"line":2363},30,[577,2365,2366],{},"            this.Dispatcher.BeginInvoke(() => { \n",[577,2368,2370],{"class":1381,"line":2369},31,[577,2371,2372],{},"                MessageBox.Show(\"任务完成！\");\n",[577,2374,2376],{"class":1381,"line":2375},32,[577,2377,2378],{},"            });\n",[577,2380,2382],{"class":1381,"line":2381},33,[577,2383,2384],{},"        });\n",[577,2386,2388],{"class":1381,"line":2387},34,[577,2389,1464],{},[577,2391,2393],{"class":1381,"line":2392},35,[577,2394,1682],{},[10,2396,2397,97],{},[14,2398,2399],{},"工作流程",[405,2401,2402,2420,2429,2443],{},[41,2403,2404,2407,2408,2411,2412,2415,2416,2419],{},[14,2405,2406],{},"View (XAML)"," 的控件属性（如 ",[33,2409,2410],{},"Text=\"{Binding ProgressText}\"","）绑定到 ",[14,2413,2414],{},"ViewModel"," 的一个公开属性 (",[33,2417,2418],{},"public string ProgressText { ... }",")。",[41,2421,2422,2424,2425,2428],{},[14,2423,2010],{},"去修改 ViewModel 的 ",[33,2426,2427],{},"ProgressText"," 属性的值。",[41,2430,2431,2432,2435,2436,2439,2440,2442],{},"ViewModel 属性的 ",[33,2433,2434],{},"set"," 访问器中，会调用 ",[33,2437,2438],{},"OnPropertyChanged(\"ProgressText\")"," 来触发 ",[33,2441,1361],{}," 事件。",[41,2444,2445,2448,2449,2451,2452,1478,2455,2458],{},[14,2446,2447],{},"WPF 的数据绑定引擎","订阅了这个事件，收到“提醒”后，它会自动在 ",[14,2450,450],{},"上更新 ",[33,2453,2454],{},"TextBlock",[33,2456,2457],{},"Text"," 属性。",[10,2460,2461,2462,2464,2465,2468,2469,2472],{},"在这个模式中，",[33,2463,1361],{}," 事件就是您所说的那个必不可少的**“提醒”",[14,2466,2467],{},"。它是连接","数据模型 (ViewModel)** 和",[14,2470,2471],{},"视图 (View)"," 的桥梁。",[10,2474,2475],{},"但是这里使用的是后台代码操作，也就是winforms的时候常见的Code-Behind模式。",[10,2477,2478,97],{},[14,2479,2399],{},[405,2481,2482,2485,2499],{},[41,2483,2484],{},"后台线程并不关心任何 ViewModel 或数据模型。",[41,2486,2487,2488,2490,2491,2494,2495,2498],{},"它通过 ",[33,2489,1923],{},"，将一个",[14,2492,2493],{},"直接操作 UI 元素","的 ",[33,2496,2497],{},"Action"," 委托，排队到 UI 线程。",[41,2500,2501,2502,2504,2505,2507],{},"UI 线程的 ",[33,2503,602],{}," 在轮到这个任务时，执行 ",[33,2506,2497],{}," 里的代码，也就是下面这两行：",[10,2509,2510,2511,2513,2514],{},"​\t为什么这里不需要 ",[33,2512,1361],{}," 这样的“提醒”？答案是：在这段代码里，",[14,2515,2516,2517,83,2520,2523],{},"我们没有通过数据模型来间接更新 UI，而是直接获得了 UI 元素本身 (",[33,2518,2519],{},"MyProgressBar",[33,2521,2522],{},"MyTextBlock",") 的控制权，并直接修改了它们的属性。",[10,2525,2526,2527,2530,2531,2533,2534,2537,2538,2541,2542,2545,2546,65,2548,1246],{},"​\tWPF 框架的",[14,2528,2529],{},"依赖属性 (Dependency Property) 系统在内部知道","，当 ",[33,2532,2457],{}," 属性（或 ",[33,2535,2536],{},"ProgressBar.Value"," 属性）发生变化时，这个控件的外观就需要被更新。因此，它会自动将一个“",[14,2539,2540],{},"重绘","”或“",[14,2543,2544],{},"重新布局","”的任务安排到 ",[33,2547,602],{},[14,2549,2550],{},"更高优先级的渲染队列",[10,2552,2553,2554,2557,2558,2561,2562,125],{},"所以，这里的“提醒”是",[14,2555,2556],{},"隐式的、内建于 WPF 属性系统","的。您对 UI 元素属性的",[14,2559,2560],{},"直接赋值动作本身","，就是",[14,2563,2564],{},"通知 WPF 去更新 UI 的方式",[2566,2567,2568],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":1376,"searchDepth":1388,"depth":1388,"links":2570},[2571,2572,2573,2574],{"id":25,"depth":1394,"text":28},{"id":152,"depth":1394,"text":155},{"id":236,"depth":1394,"text":239},{"id":346,"depth":1388,"text":346,"children":2575},[2576,2577,2578,2579,2580,2582,2584,2585,2587],{"id":594,"depth":1394,"text":594},{"id":659,"depth":1394,"text":659},{"id":977,"depth":1394,"text":977},{"id":1255,"depth":1394,"text":1258},{"id":1871,"depth":1394,"text":2581},"Send vs. Post：同步与异步的区别",{"id":1932,"depth":1394,"text":2583},"与 async\u002Fawait 的关系",{"id":2004,"depth":1394,"text":1923},{"id":2052,"depth":1394,"text":2586},"BeginInvoke vs. Invoke：关键区别",{"id":2195,"depth":1394,"text":2195},"​\tWPF 框架本身已经为我们封装好了 95% 以上常用功能，让我们不必关心底层的窗体句柄（HWND）、消息循环（Message Loop）等复杂细节。但是，总有一些特殊的、更底层的系统功能，WPF 没有直接提供 C# 的接口。这时，我们就需要“下到地基”，通过 P\u002FInvoke (Platform Invocation，平台调用) 技术，直接使用 Win32 API 提供的原生能力。","md",{},"\u002Fcsharp\u002Fwpf\u002Fwpf与win32编程",{"description":2588},"csharp\u002FWPF\u002Fwpf与win32编程","HAFVJf4Onw9Mrns-NUzWCcSf3DY8u4bhYwNAdy8MMkI",1791042465732]