Unity UGUI Button事件流程
场景结构
测试代码
public class TestBtn : MonoBehaviour
{void Start(){var btn = GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}
当添加事件时
// 实例化一个ButtonClickedEvent的事件
[FormerlySerializedAs("onClick")]
[SerializeField]
private ButtonClickedEvent m_OnClick = new ButtonClickedEvent();//常用的onClick.AddListener()就是监听这个事件
public ButtonClickedEvent onClick
{get { return m_OnClick; }set { m_OnClick = value; }
}//Button.cs部分源码
当按钮点击时
//如果按钮处于活跃状态并且可交互(Interactable设置为true),则触发事件
private void Press(){if (!IsActive() || !IsInteractable())return;UISystemProfilerApi.AddMarker("Button.onClick", this);m_OnClick.Invoke();}//鼠标点击时调用该函数,继承自 IPointerClickHandler 接口public virtual void OnPointerClick(PointerEventData eventData){if (eventData.button != PointerEventData.InputButton.Left)return;Press();}
//Button.cs部分源码
是如何执行的呢
private static readonly EventFunction<IPointerClickHandler> s_PointerClickHandler = Execute;public static EventFunction<IPointerClickHandler> pointerClickHandler
{get { return s_PointerClickHandler; }
}//调用关键代码
private static void Execute(IPointerClickHandler handler, BaseEventData eventData)
{handler.OnPointerClick(ValidateEventData<PointerEventData>(eventData));
}//ExecuteEvents.cs部分源码
- 实际调用了目标对象(如 Button)的
OnPointerClick()
方法。
继续跟踪源码在StandaloneInputModule中
private void ReleaseMouse(PointerEventData pointerEvent, GameObject currentOverGo)
{// 执行 PointerUp 事件处理ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);// 获取可以处理点击事件的对象var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);// 处理 PointerClick 和 Drop 事件if (pointerEvent.pointerClick == pointerClickHandler && pointerEvent.eligibleForClick){// 如果当前对象可以接收点击事件并且符合点击条件,则执行 PointerClick 事件ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);}//....省略其他代码...
}//StandaloneInputModule.cs部分源码
关键点:
pointerEvent.eligibleForClick
表示这次点击是否满足触发条件(比如没有被拖动打断)。- 获取当前鼠标释放时的 GameObject (
currentOverGo
)。 - 获取其
IPointerClickHandler
接口实现者(即 Button 组件)。 - 最后通过
ExecuteEvents.Execute(...)
调用OnPointerClick
方法。
pointerEvent.eligibleForClick在下面会设置为true
而ReleaseMouse在UpdateModule中调用
public override void UpdateModule(){if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus()){if (m_InputPointerEvent != null && m_InputPointerEvent.pointerDrag != null && m_InputPointerEvent.dragging){//关键代码,处理鼠标释放事件ReleaseMouse(m_InputPointerEvent, m_InputPointerEvent.pointerCurrentRaycast.gameObject);}m_InputPointerEvent = null;return;}m_LastMousePosition = m_MousePosition;m_MousePosition = input.mousePosition;}//StandaloneInputModule.cs部分源码
- 主要用于记录鼠标位置。
- 如果窗口失焦并且正在拖拽,则调用
ReleaseMouse()
来释放鼠标状态。
而UpdateModule是BaseInputModule 类的方法
public abstract class BaseInputModule : UIBehaviour{public virtual void UpdateModule(){}}
UpdateModule被在EventSystem中的TickModules方法调用
private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();private void TickModules(){var systemInputModulesCount = m_SystemInputModules.Count;for (var i = 0; i < systemInputModulesCount; i++){if (m_SystemInputModules[i] != null)m_SystemInputModules[i].UpdateModule();}}
//EventSystem.cs部分源码
TickModules方法被在EventSystem里的Update调用
protected virtual void Update(){if (current != this)return;TickModules();bool changedModule = false;//m_CurrentInputModule 就是场景里面的StandaloneInputModule组件//判断当前m_CurrentInputModule 有没有被更改或者为空的情况if (!changedModule && m_CurrentInputModule != null)//处理鼠标事件m_CurrentInputModule.Process();//...省略其他代码...
}
EventSystem的Update继承UIBehaviour
public class EventSystem : UIBehaviour
EventSystem
是一个继承自UIBehaviour
的组件,必须挂载在场景中的某个 GameObject 上。- 它每帧调用自身的
Update()
方法(由 Unity 引擎自动调用):
所以,如果在按下鼠标后,EventSystem每帧都会检测何时释放鼠标,然后触发点击事件。如果有的话触发点击事件,那么是如何知道要触发的哪个Button的点击事件的呢
回到EventSystem的Update方法中
protected virtual void Update(){if (current != this)return;TickModules();bool changedModule = false;//m_CurrentInputModule 就是场景里面的StandaloneInputModule组件//判断当前m_CurrentInputModule 有没有被更改或者为空的情况if (!changedModule && m_CurrentInputModule != null)//处理鼠标事件m_CurrentInputModule.Process();//...省略其他代码...
}
- 这里会调用所有注册的
BaseInputModule
子类的UpdateModule()
方法。 - 其中就包括
StandaloneInputModule
。
可以看到,如果没有变更输入模块并且当前输入模块不为空,会在每帧执行Process方法
public abstract class BaseInputModule : UIBehaviour{public abstract void Process();}
而场景里面的StandaloneInputModule 继承了PointerInputModule
public class StandaloneInputModule : PointerInputModule
PointerInputModule实现了BaseInputModule接口
public abstract class PointerInputModule : BaseInputModule
StandaloneInputModule 重写了Process方法
public override void Process(){if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())return;bool usedEvent = SendUpdateEventToSelectedObject();// 案例 1004066 - 在处理导航事件之前应先处理触摸/鼠标事件,// 因为它们可能会改变当前选中的游戏对象,并且提交按钮可能是触摸/鼠标按钮。// 由于存在鼠标模拟层,触摸需要优先处理。if (!ProcessTouchEvents() && input.mousePresent)//处理鼠标事件ProcessMouseEvent();if (eventSystem.sendNavigationEvents){if (!usedEvent)usedEvent |= SendMoveEventToSelectedObject();if (!usedEvent)SendSubmitEventToSelectedObject();}}protected void ProcessMouseEvent(){//鼠标左键事件Id为0ProcessMouseEvent(0);}protected void ProcessMouseEvent(int id)
{//关键函数var mouseData = GetMousePointerEventData(id);var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;// 处理鼠标左键点击ProcessMousePress(leftButtonData);ProcessMove(leftButtonData.buttonData);ProcessDrag(leftButtonData.buttonData);// 处理鼠标右键键点击ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f)){var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);}
}
GetMousePointerEventData方法是StandaloneInputModule 继承PointerInputModule的
protected virtual MouseState GetMousePointerEventData(int id){// Populate the left button...PointerEventData leftData;var created = GetPointerData(kMouseLeftId, out leftData, true);leftData.Reset();if (created)leftData.position = input.mousePosition;Vector2 pos = input.mousePosition;if (Cursor.lockState == CursorLockMode.Locked){// We don't want to do ANY cursor-based interaction when the mouse is lockedleftData.position = new Vector2(-1.0f, -1.0f);leftData.delta = Vector2.zero;}else{leftData.delta = pos - leftData.position;leftData.position = pos;}leftData.scrollDelta = input.mouseScrollDelta;leftData.button = PointerEventData.InputButton.Left;//发射射线eventSystem.RaycastAll(leftData, m_RaycastResultCache);//省略后面的代码----下面会分析
}
上面代码主要作用是发射射线,填充MouseButtonEventData 事件
EventSystem的RaycastAll方法
public class EventSystem : UIBehaviour
{public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults){raycastResults.Clear();var modules = RaycasterManager.GetRaycasters();var modulesCount = modules.Count;for (int i = 0; i < modulesCount; ++i){var module = modules[i];if (module == null || !module.IsActive())continue;//发射射线module.Raycast(eventData, raycastResults);}raycastResults.Sort(s_RaycastComparer);}
}
这个RaycasterManager会收集场景里面所有实现BaseRaycaster接口的类
由于我们场景里面只有Canvas上面挂载了GraphicRaycaster 组件
//这部分详细解析可以查看这篇文章public class GraphicRaycaster : BaseRaycaster{private List<Graphic> m_RaycastResults = new List<Graphic>();public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList) {//。。。省略部分代码。。。//拿到所有继承了Graphic的类,Button继承了该类Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);//。。。省略部分代码。。。int totalCount = m_RaycastResults.Count;for (var index = 0; index < totalCount; index++){//。。。省略部分代码。。。//m_RaycastResults进行结果排序//构建结果返回var castResult = new RaycastResult{gameObject = go,//这个就是我们的Button对象或者Text对象module = this,distance = distance,screenPosition = eventPosition,displayIndex = displayIndex,index = resultAppendList.Count,depth = m_RaycastResults[index].depth,sortingLayer = canvas.sortingLayerID,sortingOrder = canvas.sortingOrder,worldPosition = ray.origin + ray.direction * distance,worldNormal = -transForward};resultAppendList.Add(castResult);}
}
可以看到拿到的结果有两个
继续查看PointerInputModule的GetMousePointerEventData方法
protected virtual MouseState GetMousePointerEventData(int id)
{//省略前面的代码,之前分析过了//发射射线eventSystem.RaycastAll(leftData, m_RaycastResultCache);//拿到第一个结果,即Text所在对象var raycast = FindFirstRaycast(m_RaycastResultCache);//赋值给leftData.pointerCurrentRaycast,后面需要用到leftData.pointerCurrentRaycast = raycast;m_RaycastResultCache.Clear();// copy the apropriate data into right and middle slotsPointerEventData rightData;GetPointerData(kMouseRightId, out rightData, true);rightData.Reset();CopyFromTo(leftData, rightData);rightData.button = PointerEventData.InputButton.Right;PointerEventData middleData;GetPointerData(kMouseMiddleId, out middleData, true);middleData.Reset();CopyFromTo(leftData, middleData);middleData.button = PointerEventData.InputButton.Middle;m_MouseState.SetButtonState(PointerEventData.InputButton.Left, StateForMouseButton(0), leftData);m_MouseState.SetButtonState(PointerEventData.InputButton.Right, StateForMouseButton(1), rightData);m_MouseState.SetButtonState(PointerEventData.InputButton.Middle, StateForMouseButton(2), middleData);return m_MouseState;}
处理点击事件函数
/// <summary>
/// 计算并处理任何鼠标按钮状态的变化。
/// </summary>
protected void ProcessMousePress(MouseButtonEventData data)
{// 获取当前的指针事件数据var pointerEvent = data.buttonData;// 这个对象就是前面拿到的Text所在的对象var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;// 处理按下事件if (data.PressedThisFrame()){// 标记该事件为可点击pointerEvent.eligibleForClick = true; //关键代码,前面释放鼠标需要用到// 重置增量位置pointerEvent.delta = Vector2.zero;// 重置拖动标志pointerEvent.dragging = false;// 使用拖拽阈值pointerEvent.useDragThreshold = true;// 设置按下位置pointerEvent.pressPosition = pointerEvent.position;// 设置按下时的射线检测结果pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;// 如果选择的对象发生了变化,则取消之前的选中状态DeselectIfSelectionChanged(currentOverGo, pointerEvent);// 查找继承了IPointerDownHandler的控件,因为Text没有实现改接口,所以向上查找可以处理点击事件的对象,找到了Button对象var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);var newClick = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);// 如果没有找到按下处理器,则查找点击处理器if (newPressed == null)newPressed = newClick;// Debug.Log("Pressed: " + newPressed);float time = Time.unscaledTime;// 如果新的按下对象与上次相同,则增加点击计数if (newPressed == pointerEvent.lastPress){var diffTime = time - pointerEvent.clickTime;if (diffTime < 0.3f)++pointerEvent.clickCount;elsepointerEvent.clickCount = 1;pointerEvent.clickTime = time;}else{pointerEvent.clickCount = 1;}// 设置按下对象、原始按下对象和点击对象pointerEvent.pointerPress = newPressed;pointerEvent.rawPointerPress = currentOverGo;pointerEvent.pointerClick = newClick;pointerEvent.clickTime = time;// 保存拖拽处理器pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);// 如果有拖拽处理器,执行初始化潜在拖拽操作if (pointerEvent.pointerDrag != null)ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);// 保存输入指针事件m_InputPointerEvent = pointerEvent;}// 处理释放事件if (data.ReleasedThisFrame()){ReleaseMouse(pointerEvent, currentOverGo);}
}
这里面 pointerEvent.eligibleForClick = true;
前面释放鼠标需要用到。
ExecuteEvents最终执行按下事件
public static class ExecuteEvents{private static void GetEventChain(GameObject root, IList<Transform> eventChain){eventChain.Clear();if (root == null)return;var t = root.transform;while (t != null){eventChain.Add(t);t = t.parent;}}public static GameObject ExecuteHierarchy<T>(GameObject root, BaseEventData eventData, EventFunction<T> callbackFunction) where T : IEventSystemHandler{GetEventChain(root, s_InternalTransformList);var internalTransformListCount = s_InternalTransformList.Count;for (var i = 0; i < internalTransformListCount; i++){var transform = s_InternalTransformList[i];//关键函数,执行点击事件if (Execute(transform.gameObject, eventData, callbackFunction))return transform.gameObject;}return null;}public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler{var internalHandlers = ListPool<IEventSystemHandler>.Get();GetEventList<T>(target, internalHandlers);// if (s_InternalHandlers.Count > 0)// Debug.Log("Executinng " + typeof (T) + " on " + target);var internalHandlersCount = internalHandlers.Count;for (var i = 0; i < internalHandlersCount; i++){T arg;try{arg = (T)internalHandlers[i];}catch (Exception e){var temp = internalHandlers[i];Debug.LogException(new Exception(string.Format("Type {0} expected {1} received.", typeof(T).Name, temp.GetType().Name), e));continue;}try{//最终执行事件functor(arg, eventData);}catch (Exception e){Debug.LogException(e);}}var handlerCount = internalHandlers.Count;ListPool<IEventSystemHandler>.Release(internalHandlers);return handlerCount > 0;}//获取实现了IPointerDownHandler接口的组件private static void GetEventList<T>(GameObject go, IList<IEventSystemHandler> results) where T : IEventSystemHandler{// Debug.LogWarning("GetEventList<" + typeof(T).Name + ">");if (results == null)throw new ArgumentException("Results array is null", "results");if (go == null || !go.activeInHierarchy)return;var components = ListPool<Component>.Get();go.GetComponents(components);var componentsCount = components.Count;for (var i = 0; i < componentsCount; i++){if (!ShouldSendToComponent<T>(components[i]))continue;// Debug.Log(string.Format("{2} found! On {0}.{1}", go, s_GetComponentsScratch[i].GetType(), typeof(T)));results.Add(components[i] as IEventSystemHandler);}ListPool<Component>.Release(components);// Debug.LogWarning("end GetEventList<" + typeof(T).Name + ">");}
}
GetEventChain拿到4个结果
分别是
最终触发IPointerDownHandler事件
总结如下
EventSystem
会在每帧检测鼠标左键是否按下,如果按下,发射射线,拿到第一个检查到的物体,执行IPointerDownHandler
事件,同时构造好PointerEventData
给后面释放鼠标时使用,在鼠标释放时,查找点击的GameObject
上是否有组件实现了IPointerClickHandler
接口,如果有就触发事件,如果没有,就向父节点GameObject
查找,直到找到发出射线的Canvas
为止。
✅ 总结:整个流程图解
阶段 | 内容 |
---|---|
📌 Unity 引擎调用 | EventSystem.Update() |
🔁 每帧更新 | TickModules() → StandaloneInputModule.UpdateModule() |
🖱️ 鼠标释放 | ReleaseMouse() 被调用 |
🎯 查找点击对象 | 使用 GetEventHandler<IPointerClickHandler>() |
⚡ 执行点击 | ExecuteEvents.Execute() → handler.OnPointerClick() |
🧱 Button 响应 | OnPointerClick() → Press() → m_OnClick.Invoke() |
📈 用户监听 | onClick.AddListener(() => { ... }) 中的方法被调用 |
相关文章:

Unity UGUI Button事件流程
场景结构 测试代码 public class TestBtn : MonoBehaviour {void Start(){var btn GetComponent<Button>();btn.onClick.AddListener(OnClick);}private void OnClick(){Debug.Log("666");}}当添加事件时 // 实例化一个ButtonClickedEvent的事件 [Formerl…...
Bean 作用域有哪些?如何答出技术深度?
导语: Spring 面试绕不开 Bean 的作用域问题,这是面试官考察候选人对 Spring 框架理解深度的常见方式。本文将围绕“Spring 中的 Bean 作用域”展开,结合典型面试题及实战场景,帮你厘清重点,打破模板式回答,…...

Web后端基础(基础知识)
BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。 优点:维护方便缺点:体验一般 CS架构:Client/Server,客户端/服务器架构模式。需要单独…...

【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)
前言: 双亲委派机制对于面试这块来说非常重要,在实际开发中也是经常遇见需要打破双亲委派的需求,今天我们一起来探索一下什么是双亲委派机制,在此之前我们先介绍一下类的加载器。 目录 编辑 前言: 类加载器 1. …...

c++第七天 继承与派生2
这一篇文章主要内容是 派生类构造函数与析构函数 在派生类中重写基类成员 以及多继承 第一部分:派生类构造函数与析构函数 当创建一个派生类对象时,基类成员是如何初始化的? 1.当派生类对象创建的时候,基类成员的初始化顺序 …...

淘宝扭蛋机小程序系统开发:打造互动性强的购物平台
淘宝扭蛋机小程序系统的开发,旨在打造一个互动性强的购物平台,让用户在购物的同时,能够享受到更多的乐趣和惊喜。 淘宝扭蛋机小程序系统拥有丰富的互动功能。用户可以通过虚拟摇杆操作扭蛋机,实现旋转、抽拉等动作,增…...
WebRTC从入门到实践 - 零基础教程
WebRTC从入门到实践 - 零基础教程 目录 WebRTC简介 基础概念 工作原理 开发环境搭建 基础实践 三个实战案例 常见问题解答 1. WebRTC简介 1.1 什么是WebRTC? WebRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音…...

Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...
tomcat入门
1 tomcat 是什么 apache开发的web服务器可以为java web程序提供运行环境tomcat是一款高效,稳定,易于使用的web服务器tomcathttp服务器Servlet服务器 2 tomcat 目录介绍 -bin #存放tomcat的脚本 -conf #存放tomcat的配置文件 ---catalina.policy #to…...

毫米波雷达基础理论(3D+4D)
3D、4D毫米波雷达基础知识及厂商选型 PreView : https://mp.weixin.qq.com/s/bQkju4r6med7I3TBGJI_bQ 1. FMCW毫米波雷达基础知识 主要参考博文: 一文入门汽车毫米波雷达基本原理 :https://mp.weixin.qq.com/s/_EN7A5lKcz2Eh8dLnjE19w 毫米波雷达基础…...
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分: 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...

TSN交换机正在重构工业网络,PROFINET和EtherCAT会被取代吗?
在工业自动化持续演进的今天,通信网络的角色正变得愈发关键。 2025年6月6日,为期三天的华南国际工业博览会在深圳国际会展中心(宝安)圆满落幕。作为国内工业通信领域的技术型企业,光路科技(Fiberroad&…...
人工智能--安全大模型训练计划:基于Fine-tuning + LLM Agent
安全大模型训练计划:基于Fine-tuning LLM Agent 1. 构建高质量安全数据集 目标:为安全大模型创建高质量、去偏、符合伦理的训练数据集,涵盖安全相关任务(如有害内容检测、隐私保护、道德推理等)。 1.1 数据收集 描…...

MySQL:分区的基本使用
目录 一、什么是分区二、有什么作用三、分类四、创建分区五、删除分区 一、什么是分区 MySQL 分区(Partitioning)是一种将单张表的数据逻辑上拆分成多个物理部分的技术。这些物理部分(分区)可以独立存储、管理和优化,…...

HubSpot推出与ChatGPT的深度集成引发兴奋与担忧
上周三,HubSpot宣布已构建与ChatGPT的深度集成,这一消息在HubSpot用户和营销技术观察者中引发了极大的兴奋,但同时也存在一些关于数据安全的担忧。 许多网络声音声称,这对SaaS应用程序和人工智能而言是一场范式转变。 但向任何技…...

Rust 开发环境搭建
环境搭建 1、开发工具RustRover 或者vs code 2、Cygwin64 安装 https://cygwin.com/install.html 在工具终端执行: rustup toolchain install stable-x86_64-pc-windows-gnu rustup default stable-x86_64-pc-windows-gnu 2、Hello World fn main() { println…...

Golang——7、包与接口详解
包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...
苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会
在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...
python爬虫——气象数据爬取
一、导入库与全局配置 python 运行 import json import datetime import time import requests from sqlalchemy import create_engine import csv import pandas as pd作用: 引入数据解析、网络请求、时间处理、数据库操作等所需库。requests:发送 …...
django blank 与 null的区别
1.blank blank控制表单验证时是否允许字段为空 2.null null控制数据库层面是否为空 但是,要注意以下几点: Django的表单验证与null无关:null参数控制的是数据库层面字段是否可以为NULL,而blank参数控制的是Django表单验证时字…...
日常一水C
多态 言简意赅:就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过,当子类和父类的函数名相同时,会隐藏父类的同名函数转而调用子类的同名函数,如果要调用父类的同名函数,那么就需要对父类进行引用&#…...

给网站添加live2d看板娘
给网站添加live2d看板娘 参考文献: stevenjoezhang/live2d-widget: 把萌萌哒的看板娘抱回家 (ノ≧∇≦)ノ | Live2D widget for web platformEikanya/Live2d-model: Live2d model collectionzenghongtu/live2d-model-assets 前言 网站环境如下,文章也主…...

手机平板能效生态设计指令EU 2023/1670标准解读
手机平板能效生态设计指令EU 2023/1670标准解读 以下是针对欧盟《手机和平板电脑生态设计法规》(EU) 2023/1670 的核心解读,综合法规核心要求、最新修正及企业合规要点: 一、法规背景与目标 生效与强制时间 发布于2023年8月31日(OJ公报&…...

解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用
在工业制造领域,无损检测(NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统,以非接触式光学麦克风技术为核心,打破传统检测瓶颈,为半导体、航空航天、汽车制造等行业提供了高灵敏…...
Linux系统部署KES
1、安装准备 1.版本说明V008R006C009B0014 V008:是version产品的大版本。 R006:是release产品特性版本。 C009:是通用版 B0014:是build开发过程中的构建版本2.硬件要求 #安全版和企业版 内存:1GB 以上 硬盘…...
深入理解Optional:处理空指针异常
1. 使用Optional处理可能为空的集合 在Java开发中,集合判空是一个常见但容易出错的场景。传统方式虽然可行,但存在一些潜在问题: // 传统判空方式 if (!CollectionUtils.isEmpty(userInfoList)) {for (UserInfo userInfo : userInfoList) {…...

从“安全密码”到测试体系:Gitee Test 赋能关键领域软件质量保障
关键领域软件测试的"安全密码":Gitee Test如何破解行业痛点 在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的"神经中枢"。从国防军工到能源电力,从金融交易到交通管控,这些关乎国计民生的关键领域…...
从面试角度回答Android中ContentProvider启动原理
Android中ContentProvider原理的面试角度解析,分为已启动和未启动两种场景: 一、ContentProvider已启动的情况 1. 核心流程 触发条件:当其他组件(如Activity、Service)通过ContentR…...
OD 算法题 B卷【正整数到Excel编号之间的转换】
文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的:a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...
Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成
一个面向 Java 开发者的 Sring-Ai 示例工程项目,该项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和…...