WPF 多个 StylusPlugIn 的事件触发顺序

如果在 WPF 使用 StylusPlugIn 同时在同一个界面用多个元素都加上 StylusPlugIn 那么事件触发的顺序将会很乱 我建议是不要让 StylusPlugIn 有重叠,在没有理解 StylusPlugIn 之前请不要写出让 StylusPlugIn 有重叠的代码。因为可能有小伙伴移动了一个元素就让你的代码的行为和之前写的不一样

如果多个 StylusPlugIn 附加的元素没有重叠,那么所有元素的工作都会符合预期。也就是点到哪个元素,将会触发对应元素的 StylusPlugIn 方法

因为本文比较复杂,主要是很无聊的原理,所以只想了解现象的小伙伴只看下面图片就可以

我将会使用两个不同的框代表不同的元素,红色的框代表的是普通的容器,而蓝色代表附加StylusPlugIn元素

对同容器内两个重叠元素,将会同时触发两个元素的 StylusPlugIn 事件,不同的是在最底层的元素将会在触摸线程触发,而在最上层的元素将会是主线程触发

对同容器内多个重叠元素,将知道最上层和最底层的元素会触发事件,不同的是在最底层的元素将会在触摸线程触发,而在最上层的元素将会是主线程触发

如果是一个附加 StylusPlugIn 的容器,包含一个附加 StylusPlugIn 的元素,那么只有元素会触发在触摸线程触发事件

代码放在 github 建议下载代码测试

如果不想了解原理,请关闭页面

在阅读本文之前,请先看WPF 高速书写 StylusPlugIn 原理

如果多个元素有重叠,那么就需要分为以下不同的重叠方法

同容器内两个重叠元素

先定义一个自定义控件和一个 StylusPlugIn1 类

    public class CustomControl : Grid
    {
        public CustomControl()
        {
            Background = Brushes.White;
        }

        public StylusPlugInCollection StylusPlugInCollection => StylusPlugIns;
    }

    public class StylusPlugIn1 : StylusPlugIn
    {
        public StylusPlugIn1()
        {
        }

        public string Name { set; get; }

        protected override void OnStylusDown(RawStylusInput rawStylusInput)
        {
            Debug.WriteLine($"{Name} Down 当前线程{Thread.CurrentThread.Name} id={Thread.CurrentThread.ManagedThreadId}");
            base.OnStylusDown(rawStylusInput);
        }

        public override string ToString()
        {
            return $"{Name}";
        }
    }

在界面创建两个 CustomControl 元素加入到相同一个 Grid 作为元素

    <Grid>
        <local:CustomControl x:Name="Control1">
            <local:CustomControl.StylusPlugInCollection>
                <local:StylusPlugIn1 Name="Stylus 1"></local:StylusPlugIn1>
            </local:CustomControl.StylusPlugInCollection>
        </local:CustomControl>
        <local:CustomControl x:Name="Control2" >
            <local:CustomControl.StylusPlugInCollection>
                <local:StylusPlugIn1 Name="Stylus 2"></local:StylusPlugIn1>
            </local:CustomControl.StylusPlugInCollection>
        </local:CustomControl>
    </Grid>

此时尝试触摸一下屏幕,可以看到下面输出

Stylus 1 Down 当前线程Stylus Input id=3
Stylus 2 Down 当前线程 id=1

也就是传入的 Stylus 1Stylus 2 都收到了 Down 但是分别是通过不同的线程传入

这里有一点疑惑是为什么 Control2 的界面层级比 Control1 的高,但是为什么反而是 Stylus 1 先收到按下

WPF 高速书写 StylusPlugIn 原理有说到一点原理,但是在这篇博客我不想在这个方面讲细节,所以将细节放在这篇博客

在 PenContexts.HittestPlugInCollection 方法,将会返回一个 StylusPlugInCollection 用于决定处理 StylusInput 线程的逻辑,而这个方法的代码如下

		private StylusPlugInCollection HittestPlugInCollection(Point pt)
		{
			foreach (StylusPlugInCollection stylusPlugInCollection in this._plugInCollectionList)
			{
				if (stylusPlugInCollection.IsHit(pt))
				{
					return stylusPlugInCollection;
				}
			}
			return null;
		}

这里的 _plugInCollectionList 就是全局添加到元素的 StylusPlugInCollection 列表,从上面代码可以看到没有做任何的排序,也就是拿到第一个可以命中的元素就返回。而这个字段的添加是依赖于视觉树添加的顺序,这也就是本文开始告诉大家的,不要做出重叠的原因

关于 _plugInCollectionList 字段是如何添加的,将会在下文说到,现在回到开始的问题

在触摸线程 StylusInput 线程拿到的 StylusPlugInCollection 是第一个满足条件的,而刚好按照视觉树是 Control1 先添加到视觉树,所以返回的就是第一个元素

在第一个元素返回之后,触发了 Down 就完成了触摸线程的逻辑了。而 Control2 是如何触发的?

请看 Control2 的调用堆栈

 	PresentationCore.dll!System.Windows.Input.StylusPlugIns.StylusPlugInCollection.FireRawStylusInput.AnonymousMethod__0()
 	PresentationCore.dll!System.Windows.Input.StylusPlugIns.StylusPlugInCollection.ExecuteWithPotentialLock(System.Action action)
 	PresentationCore.dll!System.Windows.Input.StylusPlugIns.StylusPlugInCollection.FireRawStylusInput(System.Windows.Input.StylusPlugIns.RawStylusInput args)
 	PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.VerifyStylusPlugInCollectionTarget(System.Windows.Input.RawStylusInputReport rawStylusInputReport)
 	PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.PreNotifyInput(object sender, System.Windows.Input.NotifyInputEventArgs e)
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessStagingArea()
 	PresentationCore.dll!System.Windows.Input.InputManager.ProcessInput(System.Windows.Input.InputEventArgs input)
 	PresentationCore.dll!System.Windows.Input.StylusWisp.WispLogic.InputManagerProcessInput(object oInput)

可以从上面堆栈看到这是主线程的调用堆栈,通过上面的输出可以看到这个方法是在主线程触发

WispLogic.VerifyStylusPlugInCollectionTarget 方法将调用触摸命中的元素的方法

            UIElement newTarget = InputElement.GetContainingUIElement(rawStylusInputReport.StylusDevice.DirectlyOver as DependencyObject) as UIElement;
            if (newTarget != null)
            {
                targetPIC = rawStylusInputReport.PenContext.Contexts.FindPlugInCollection(newTarget);
            }

现在 WPF 开放源代码了,以上代码在 WispLogic.cs#L2638 可以看到在找到 newTarget 的时候将会调用 FindPlugInCollection 方法寻找

而 targetPIC 的定义如下

            // See if we have a plugin for the target of this input.
            StylusPlugInCollection targetPIC = null;

也就是在当前命中的是元素,同时在这个元素可以找到 StylusPlugInCollection 那么将给这个变量赋值

调用代码请看代码

               // Now fire RawStylusInput if needed to the right plugincollection.
                if (sendRawStylusInput)
                {
                    // We are on the pen thread, just call directly.
                    targetPIC.FireRawStylusInput(rawStylusInputReport.RawStylusInput);
                    updateEventPoints = (updateEventPoints || rawStylusInputReport.RawStylusInput.StylusPointsModified);

                    // Indicate we've used a stylus plugin
                    Statistics.FeaturesUsed |= StylusTraceLogger.FeatureFlags.StylusPluginsUsed;
                }

所以将会看到有 Stylus 1Stylus 2 的 Down 都被调用,但是不同的是 Stylus 2 是在主线程调用

同容器内多个重叠元素

在上面告诉大家同容器内两个重叠元素将会都触发事件

但是千万不要认为多个重叠的元素都会被触发,其实只有最先加入视觉树的元素和命中到的元素会触发,如下面代码

       <local:CustomControl x:Name="Control1">
            <local:CustomControl.StylusPlugInCollection>
                <local:StylusPlugIn1 Name="Stylus 1"></local:StylusPlugIn1>
            </local:CustomControl.StylusPlugInCollection>
        </local:CustomControl>
        <local:CustomControl x:Name="Control2" >
            <local:CustomControl.StylusPlugInCollection>
                <local:StylusPlugIn1 Name="Stylus 2"></local:StylusPlugIn1>
            </local:CustomControl.StylusPlugInCollection>
        </local:CustomControl>
        <local:CustomControl x:Name="Control3" >
            <local:CustomControl.StylusPlugInCollection>
                <local:StylusPlugIn1 Name="Stylus 3"></local:StylusPlugIn1>
            </local:CustomControl.StylusPlugInCollection>
        </local:CustomControl>

如果理解了上面的逻辑可以知道,第一个元素将会在触摸线程调用,而最后一个元素在主线程命中测试找到也会被调用,那么第二个元素呢

其实第二个元素因为没有在主线程命中测试找到,所以就不会被调用,上面代码在触摸屏幕可以看到下面代码

Stylus 1 Down 当前线程Stylus Input id=3
Stylus 3 Down 当前线程 id=1

容器和包含一个元素

如果容器本身就附加了 StylusPlugIn 同时容器里面也包含一个附加的元素,如下面代码,那么触发效果是什么

        <local:CustomControl x:Name="Control1">
            <local:CustomControl.StylusPlugInCollection>
                <local:StylusPlugIn1 Name="Stylus 1" />
            </local:CustomControl.StylusPlugInCollection>
            <local:CustomControl.Children>
                <local:CustomControl x:Name="Control2">
                    <local:CustomControl.StylusPlugInCollection>
                        <local:StylusPlugIn1 Name="Stylus 2" />
                    </local:CustomControl.StylusPlugInCollection>
                </local:CustomControl>
            </local:CustomControl.Children>
        </local:CustomControl>

通过运行代码可以看到输出的只有 Control2 事件

PenContexts.AddStylusPlugInCollection 方法会将当前元素的 StylusPlugIn 添加到全局的字段,而添加的时候会调用 PenContexts.FindZOrderIndex 方法,在这个方法将会决定添加的 StylusPlugIn 所在字段的顺序,因为在通过命中测试获取点击到的元素是按照字段列表的顺序获取,返回第一个满足的元素。在字段列表的顺序将会决定哪个元素响应

在 FindZOrderIndex 将会让 Control2 添加到最前,也就是在触摸线程命中测试将会返回 Control2 触发,而在主线程命中测试也是返回第二个控件

所以第一个控件没有被触发事件


本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏全栈前端精选

「多图警告」重学 TCP/IP 协议

最近刚看完电影 「无敌破坏王2:大闹互联网」,觉得里面有些动画蛮有意思的,于是想起前不久看的《图解HTTP》和 TCP/IP相关的文章。嗯,是时候展示真正的技术...

6710
来自专栏达达前端

uni app 零基础小白到项目实战-1

uni-app是一个使用vue.js开发跨平台应用的前端框架。 开发者通过编写vue.js代码,uni-app将其编译到Ios,android,微信小程序等多...

10710
来自专栏全栈前端精选

详解vue组件三大核心概念

本文主要介绍属性、事件和插槽这三个vue基础概念、使用方法及其容易被忽略的一些重要细节。如果你阅读别人写的组件,可以从这三个部分展开,它们可以帮助你快速了解一个...

7620
来自专栏全栈前端精选

30 道 Vue 面试题,内含详细讲解(涵盖入门到精通,自测 Vue 掌握程度)

本文以前端面试官的角度出发,对 Vue 框架中一些重要的特性、框架的原理以问题的形式进行整理汇总,意在帮助作者及读者自测下 Vue 掌握的程度。本文章节结构以从...

8510
来自专栏用代码征服天下

Vue.js学习总结——1

双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用 v-html 指令:

6610
来自专栏全栈前端精选

耽误你的十分钟,让MVVM原理还给你

众所周知当下是MVVM盛行的时代,从早期的Angular到现在的React和Vue,再从最初的三分天下到现在的两虎相争。

9130
来自专栏程序员宇说

[Vue 牛刀小试]:第十七章 - 优化 Vue CLI 3 构建的前端项目模板(1)- 基础项目模板介绍

  在上一章中,我们开始通过 Vue CLI 去搭建属于自己的前端 Vue 项目模板,就像我们 .NET 程序员在使用 asp.net core 时一样,我们更...

8810
来自专栏全栈前端精选

「中高级前端面试」JavaScript手写代码无敌秘籍

用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。提供可选的reviver函数用以在返回之前对所得到的对象执行变换(操作)。

14620
来自专栏全栈前端精选

Vue 3.0前的 TypeScript 最佳入门实践

然鹅最近的一个项目中,是 TypeScript+ Vue,毛计喇,学之...…真香!

15920
来自专栏一番码客

electron桌面应用开发(一)

关于electron,前两天一番写了一个开头,搭建开发环境。今天一番尝试去开发一个小的例子,做一些修改。

9720

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励