dotnet DirectX 做一个简单绘制折线笔迹的 D2D 应用

news/2024/10/23 13:34:35

前置博客: dotnet DirectX 通过 Vortice 控制台使用 ID2D1DeviceContext 绘制画面

本文属于 D2D 系列博客,更多 D2D 相关博客,请参阅 博客导航

在开始之前,我十分推荐大家先阅读 分享一个在 dotnet 里使用 D2D 配合 AOT 开发小而美的应用开发经验 这篇博客,通过阅读此博客,可以让大家理解一些常用概念

本文实现的 D2D 应用,由于触摸数据是从 WM_Pointer 获取的,这就限制了在 Win7 下是不可用的

依然按照 dotnet DirectX 通过 Vortice 控制台使用 ID2D1DeviceContext 绘制画面 博客提供的方法,从控制台开始创建 Win32 窗口,挂上交换链,初始化绘制上下文信息

本文内容里面只给出关键代码片段,如需要全部的项目文件,可到本文末尾找到本文所有代码的下载方法

修改 NativeMethods.txt 文件,替换为如下代码,以下为本文例子代码所需要用到的所有 Win32 方法和常量等内容

GetModuleHandle
PeekMessage
TranslateMessage
DispatchMessage
GetMessage
RegisterClassExW
DefWindowProc
LoadCursor
PostQuitMessage
CreateWindowExW
DestroyWindow
ShowWindow
GetSystemMetrics
AdjustWindowRectEx
GetClientRect
GetWindowRect
IDC_ARROW
WM_KEYDOWN
WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP
WM_DESTROY
WM_QUIT
WM_PAINT
WM_CLOSE
WM_ACTIVATEAPP
VIRTUAL_KEY
GetPointerTouchInfo
ScreenToClient
GetPointerDeviceRects
ClientToScreen
WM_POINTERDOWN
WM_POINTERUPDATE
WM_POINTERUP

略过创建窗口和获取 D2D 上下文相关代码,如对这部分代码感兴趣,请参阅 dotnet DirectX 通过 Vortice 控制台使用 ID2D1DeviceContext 绘制画面

以下为已经获取到 ID2D1RenderTarget 的代码,继续添加对触摸数据的处理

        // 在窗口的 dxgi 的平面上创建 D2D 的画布,如此即可让 D2D 绘制到窗口上D2D.ID2D1RenderTarget d2D1RenderTarget =d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);d2D1RenderTarget.AntialiasMode = D2D.AntialiasMode.PerPrimitive;var renderTarget = d2D1RenderTarget;

定义一个基础数据结构,用于记录点的信息

    readonly record struct Point2D(double X, double Y);

这些基础数据结构我在很多个项目里面都有定义,基础数学相关类型我也重复定义了很多次,且受限于我的数学知识,有些类型定义还是不正确的。好在我的伙伴 SeWZC 在 GitHub 上开源了数学库,这个数学库是按照正确的数学实现,实现了许多数学相关的类型。详细请看 https://github.com/dotnet-campus/DotNetCampus.Numerics

开个消息循环等待,防止控制台退出,顺带在此消息循环里面处理 Pointer 消息

        // 开个消息循环等待Windows.Win32.UI.WindowsAndMessaging.MSG msg;while (true){...}

根据 dotnet 读 WPF 源代码笔记 从 WM_POINTER 消息到 Touch 事件 博客提供的方法进行对 WM_POINTER 消息的处理

处理逻辑如下

        // 开个消息循环等待Windows.Win32.UI.WindowsAndMessaging.MSG msg;while (true){if (PeekMessage(out msg, default, 0, 0, PM_REMOVE) != false){if (msg.message is PInvoke.WM_POINTERDOWN or PInvoke.WM_POINTERUPDATE or PInvoke.WM_POINTERUP){...}}}

本文这里先不考虑多指,也不考虑多笔,直接就是相邻点连接为折线。先按照 dotnet 读 WPF 源代码笔记 从 WM_POINTER 消息到 Touch 事件 博客提供的方法对收到的 Pointer 点进行处理,这里将使用的是高精度的点

                    var wparam = msg.wParam;var pointerId = (uint)(ToInt32((IntPtr)wparam.Value) & 0xFFFF);PInvoke.GetPointerTouchInfo(pointerId, out var info);POINTER_INFO pointerInfo = info.pointerInfo;global::Windows.Win32.Foundation.RECT pointerDeviceRect = default;global::Windows.Win32.Foundation.RECT displayRect = default;PInvoke.GetPointerDeviceRects(pointerInfo.sourceDevice, &pointerDeviceRect, &displayRect);var point2D = new Point2D(pointerInfo.ptHimetricLocationRaw.X / (double)pointerDeviceRect.Width * displayRect.Width +displayRect.left,pointerInfo.ptHimetricLocationRaw.Y / (double)pointerDeviceRect.Height * displayRect.Height +displayRect.top);point2D = new Point2D(point2D.X - screenTranslate.X, point2D.Y - screenTranslate.Y);private static int ToInt32(IntPtr ptr) => IntPtr.Size == 4 ? ptr.ToInt32() : (int)(ptr.ToInt64() & 0xffffffff);

以上拿到的 Point2D 就是 Pointer 消息收到的触摸点

为了简单起见,咱这里不获取历史点,只获取最新的点即可。将最新的点和上一个点连接做折线在屏幕上显示出来,如此即可获取很高的性能,很低的延迟

有双缓存的存在,推荐每次都是重新绘制,在实际使用中,即使每次都绘制整个界面,对整理的性能影响也几乎可以忽略。但为了方便演示,本文这里限制了点的数量,如果超过了一定数量,则将记录的部分点删掉

        var pointList = new List<Point2D>();var screenTranslate = new Point(0, 0);PInvoke.ClientToScreen(hWnd, ref screenTranslate);// 开个消息循环等待Windows.Win32.UI.WindowsAndMessaging.MSG msg;while (true){if (PeekMessage(out msg, default, 0, 0, PM_REMOVE) != false){if (msg.message is PInvoke.WM_POINTERDOWN or PInvoke.WM_POINTERUPDATE or PInvoke.WM_POINTERUP){...point2D = new Point2D(point2D.X - screenTranslate.X, point2D.Y - screenTranslate.Y);pointList.Add(point2D);if (pointList.Count > 200){// 不要让点太多,导致绘制速度太慢pointList.RemoveRange(0, 100);}...}}}

为了在屏幕显示出笔迹折线,这里需要先创建画刷。按照 dotnet C# 使用 Vortice 创建 Direct2D1 的 ID2D1SolidColorBrush 纯色画刷 博客介绍的方法创建简单的纯色画刷,代码如下

                    var color = new Color4(0xFF0000FF);using var brush = renderTarget.CreateSolidColorBrush(color);

接着开始构成折线,开始之前和结束之后别忘了调用 renderTarget.BeginDraw();renderTarget.EndDraw(); 方法

                    renderTarget.BeginDraw();renderTarget.AntialiasMode = AntialiasMode.Aliased;renderTarget.Clear(new Color4(0xFFFFFFFF));for (var i = 1; i < pointList.Count; i++){var previousPoint = pointList[i - 1];var currentPoint = pointList[i];renderTarget.DrawLine(new Vector2((float)previousPoint.X, (float)previousPoint.Y),new Vector2((float)currentPoint.X, (float)currentPoint.Y), brush, 5);}renderTarget.EndDraw();

以上代码通过多次 DrawLine 的方式完成笔迹折线的。完成绘制之后,调用一下 swapChain.Present 切换交换链,从而在界面显示笔迹折线

                    renderTarget.EndDraw();swapChain.Present(1, DXGI.PresentFlags.None);// 等待刷新d3D11DeviceContext.Flush();

以上就是使用 Vortice 辅助调用 Direct2D1 的功能,配合 WM_Pointer 消息,制作一个简单绘制触摸折线笔迹的 D2D 应用的核心逻辑

本文的例子代码非常简单,可以全部在一个 Program.cs 文件完成,所有代码如下

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using static Windows.Win32.PInvoke;
using static Windows.Win32.UI.WindowsAndMessaging.PEEK_MESSAGE_REMOVE_TYPE;
using static Windows.Win32.UI.WindowsAndMessaging.WNDCLASS_STYLES;
using static Windows.Win32.UI.WindowsAndMessaging.WINDOW_STYLE;
using static Windows.Win32.UI.WindowsAndMessaging.WINDOW_EX_STYLE;
using static Windows.Win32.UI.WindowsAndMessaging.SYSTEM_METRICS_INDEX;
using static Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD;
using Vortice.Mathematics;
using AlphaMode = Vortice.DXGI.AlphaMode;
using D3D = Vortice.Direct3D;
using D3D11 = Vortice.Direct3D11;
using DXGI = Vortice.DXGI;
using D2D = Vortice.Direct2D1;
using System.Drawing;
using Vortice.Direct2D1;
using System.Numerics;
using Windows.Win32;
using Windows.Win32.UI.Input.Pointer;namespace QalberegejeaJawchejoleawerejea;class Program
{// 设置可以支持 Win7 和以上版本。如果用到 WinRT 可以设置为支持 win10 和以上。这个特性只是给 VS 看的,没有实际影响运行的逻辑[SupportedOSPlatform("Windows7.0")]static unsafe void Main(string[] args){// 准备创建窗口// 使用 Win32 创建窗口需要很多参数,这些参数系列不是本文的重点,还请自行了解SizeI clientSize = new SizeI(1000, 600);// 窗口标题var title = "QalberegejeaJawchejoleawerejea";var windowClassName = "lindexi doubi";// 窗口样式,窗口样式含义请执行参阅官方文档,样式只要不离谱,自己随便写,影响不大WINDOW_STYLE style = WS_CAPTION |WS_SYSMENU |WS_MINIMIZEBOX |WS_CLIPSIBLINGS |WS_BORDER |WS_DLGFRAME |WS_THICKFRAME |WS_GROUP |WS_TABSTOP |WS_SIZEBOX;var rect = new RECT{right = clientSize.Width,bottom = clientSize.Height};// Adjust according to window stylesAdjustWindowRectEx(&rect, style, false, WS_EX_APPWINDOW);// 决定窗口在哪显示,这个不影响大局int x = 0;int y = 0;int windowWidth = rect.right - rect.left;int windowHeight = rect.bottom - rect.top;// 随便,放在屏幕中间好了。多个显示器?忽略int screenWidth = GetSystemMetrics(SM_CXSCREEN);int screenHeight = GetSystemMetrics(SM_CYSCREEN);x = (screenWidth - windowWidth) / 2;y = (screenHeight - windowHeight) / 2;var hInstance = GetModuleHandle((string?)null);fixed (char* lpszClassName = windowClassName){PCWSTR szCursorName = new((char*)IDC_ARROW);var wndClassEx = new WNDCLASSEXW{cbSize = (uint)Unsafe.SizeOf<WNDCLASSEXW>(),style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC,// 核心逻辑,设置消息循环lpfnWndProc = new WNDPROC(WndProc),hInstance = (HINSTANCE)hInstance.DangerousGetHandle(),hCursor = LoadCursor((HINSTANCE)IntPtr.Zero, szCursorName),hbrBackground = (Windows.Win32.Graphics.Gdi.HBRUSH)IntPtr.Zero,hIcon = (HICON)IntPtr.Zero,lpszClassName = lpszClassName};ushort atom = RegisterClassEx(wndClassEx);if (atom == 0){throw new InvalidOperationException($"Failed to register window class. Error: {Marshal.GetLastWin32Error()}");}}// 创建窗口var hWnd = CreateWindowEx(WS_EX_APPWINDOW,windowClassName,title,style,x,y,windowWidth,windowHeight,hWndParent: default,hMenu: default,hInstance: default,lpParam: null);// 创建完成,那就显示ShowWindow(hWnd, SW_NORMAL);RECT windowRect;GetClientRect(hWnd, &windowRect);clientSize = new SizeI(windowRect.right - windowRect.left, windowRect.bottom - windowRect.top);// 开始创建工厂创建 D3D 的逻辑var dxgiFactory2 = DXGI.DXGI.CreateDXGIFactory1<DXGI.IDXGIFactory2>();var hardwareAdapter = GetHardwareAdapter(dxgiFactory2)// 这里 ToList 只是想列出所有的 IDXGIAdapter1 在实际代码里,大部分都是获取第一个.ToList().FirstOrDefault();if (hardwareAdapter == null){throw new InvalidOperationException("Cannot detect D3D11 adapter");}else{Console.WriteLine($"使用显卡 {hardwareAdapter.Description1.Description}");}// 功能等级// [C# 从零开始写 SharpDx 应用 聊聊功能等级](https://blog.lindexi.com/post/C-%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E5%86%99-SharpDx-%E5%BA%94%E7%94%A8-%E8%81%8A%E8%81%8A%E5%8A%9F%E8%83%BD%E7%AD%89%E7%BA%A7.html)D3D.FeatureLevel[] featureLevels = new[]{D3D.FeatureLevel.Level_11_1,D3D.FeatureLevel.Level_11_0,D3D.FeatureLevel.Level_10_1,D3D.FeatureLevel.Level_10_0,D3D.FeatureLevel.Level_9_3,D3D.FeatureLevel.Level_9_2,D3D.FeatureLevel.Level_9_1,};DXGI.IDXGIAdapter1 adapter = hardwareAdapter;D3D11.DeviceCreationFlags creationFlags = D3D11.DeviceCreationFlags.BgraSupport;var result = D3D11.D3D11.D3D11CreateDevice(adapter,D3D.DriverType.Unknown,creationFlags,featureLevels,out D3D11.ID3D11Device d3D11Device, out D3D.FeatureLevel featureLevel,out D3D11.ID3D11DeviceContext d3D11DeviceContext);if (result.Failure){// 如果失败了,那就不指定显卡,走 WARP 的方式// http://go.microsoft.com/fwlink/?LinkId=286690result = D3D11.D3D11.D3D11CreateDevice(IntPtr.Zero,D3D.DriverType.Warp,creationFlags,featureLevels,out d3D11Device, out featureLevel, out d3D11DeviceContext);// 如果失败,就不能继续result.CheckError();}// 大部分情况下,用的是 ID3D11Device1 和 ID3D11DeviceContext1 类型// 从 ID3D11Device 转换为 ID3D11Device1 类型var d3D11Device1 = d3D11Device.QueryInterface<D3D11.ID3D11Device1>();var d3D11DeviceContext1 = d3D11DeviceContext.QueryInterface<D3D11.ID3D11DeviceContext1>();// 后续还要创建 D2D 设备,就先不考虑释放咯//// 转换完成,可以减少对 ID3D11Device1 的引用计数//// 调用 Dispose 不会释放掉刚才申请的 D3D 资源,只是减少引用计数//d3D11Device.Dispose();//d3D11DeviceContext.Dispose();// 创建设备,接下来就是关联窗口和交换链DXGI.Format colorFormat = DXGI.Format.B8G8R8A8_UNorm;const int FrameCount = 2;DXGI.SwapChainDescription1 swapChainDescription = new(){Width = (uint)clientSize.Width,Height = (uint)clientSize.Height,Format = colorFormat,BufferCount = FrameCount,BufferUsage = DXGI.Usage.RenderTargetOutput,SampleDescription = DXGI.SampleDescription.Default,Scaling = DXGI.Scaling.Stretch,SwapEffect = DXGI.SwapEffect.FlipSequential,AlphaMode = AlphaMode.Ignore,// https://learn.microsoft.com/zh-cn/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-present// 可变刷新率显示 启用撕裂是可变刷新率显示器的要求//Flags = DXGI.SwapChainFlags.AllowTearing,};// 设置是否全屏DXGI.SwapChainFullscreenDescription fullscreenDescription = new DXGI.SwapChainFullscreenDescription{Windowed = true,};// 给创建出来的窗口挂上交换链DXGI.IDXGISwapChain1 swapChain =dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, hWnd, swapChainDescription, fullscreenDescription);// 不要被按下 alt+enter 进入全屏dxgiFactory2.MakeWindowAssociation(hWnd, DXGI.WindowAssociationFlags.IgnoreAltEnter);D3D11.ID3D11Texture2D backBufferTexture = swapChain.GetBuffer<D3D11.ID3D11Texture2D>(0);// 获取到 dxgi 的平面,这个屏幕就约等于窗口渲染内容DXGI.IDXGISurface dxgiSurface = backBufferTexture.QueryInterface<DXGI.IDXGISurface>();// 对接 D2D 需要创建工厂D2D.ID2D1Factory1 d2DFactory = D2D.D2D1.D2D1CreateFactory<D2D.ID2D1Factory1>();// 方法1:var renderTargetProperties = new D2D.RenderTargetProperties(Vortice.DCommon.PixelFormat.Premultiplied);// 在窗口的 dxgi 的平面上创建 D2D 的画布,如此即可让 D2D 绘制到窗口上D2D.ID2D1RenderTarget d2D1RenderTarget =d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);d2D1RenderTarget.AntialiasMode = D2D.AntialiasMode.PerPrimitive;var renderTarget = d2D1RenderTarget;// 方法2:// 创建 D2D 设备,通过设置 ID2D1DeviceContext 的 Target 输出为 dxgiSurface 从而让 ID2D1DeviceContext 渲染内容渲染到窗口上// 如 https://learn.microsoft.com/en-us/windows/win32/direct2d/images/devicecontextdiagram.png 图// 获取 DXGI 设备,用来创建 D2D 设备//DXGI.IDXGIDevice dxgiDevice = d3D11Device.QueryInterface<DXGI.IDXGIDevice>();//ID2D1Device d2dDevice = d2DFactory.CreateDevice(dxgiDevice);//ID2D1DeviceContext d2dDeviceContext = d2dDevice.CreateDeviceContext();//ID2D1Bitmap1 d2dBitmap = d2dDeviceContext.CreateBitmapFromDxgiSurface(dxgiSurface);//d2dDeviceContext.Target = d2dBitmap;//var renderTarget = d2dDeviceContext;var pointList = new List<Point2D>();var screenTranslate = new Point(0, 0);PInvoke.ClientToScreen(hWnd, ref screenTranslate);// 开个消息循环等待Windows.Win32.UI.WindowsAndMessaging.MSG msg;while (true){if (PeekMessage(out msg, default, 0, 0, PM_REMOVE) != false){if (msg.message is PInvoke.WM_POINTERDOWN or PInvoke.WM_POINTERUPDATE or PInvoke.WM_POINTERUP){var wparam = msg.wParam;var pointerId = (uint)(ToInt32((IntPtr)wparam.Value) & 0xFFFF);PInvoke.GetPointerTouchInfo(pointerId, out var info);POINTER_INFO pointerInfo = info.pointerInfo;global::Windows.Win32.Foundation.RECT pointerDeviceRect = default;global::Windows.Win32.Foundation.RECT displayRect = default;PInvoke.GetPointerDeviceRects(pointerInfo.sourceDevice, &pointerDeviceRect, &displayRect);var point2D = new Point2D(pointerInfo.ptHimetricLocationRaw.X / (double)pointerDeviceRect.Width * displayRect.Width +displayRect.left,pointerInfo.ptHimetricLocationRaw.Y / (double)pointerDeviceRect.Height * displayRect.Height +displayRect.top);point2D = new Point2D(point2D.X - screenTranslate.X, point2D.Y - screenTranslate.Y);pointList.Add(point2D);if (pointList.Count > 200){// 不要让点太多,导致绘制速度太慢pointList.RemoveRange(0, 100);}var color = new Color4(0xFF0000FF);using var brush = renderTarget.CreateSolidColorBrush(color);renderTarget.BeginDraw();renderTarget.AntialiasMode = AntialiasMode.Aliased;renderTarget.Clear(new Color4(0xFFFFFFFF));for (var i = 1; i < pointList.Count; i++){var previousPoint = pointList[i - 1];var currentPoint = pointList[i];renderTarget.DrawLine(new Vector2((float)previousPoint.X, (float)previousPoint.Y),new Vector2((float)currentPoint.X, (float)currentPoint.Y), brush, 5);}renderTarget.EndDraw();swapChain.Present(1, DXGI.PresentFlags.None);// 等待刷新d3D11DeviceContext.Flush();}_ = TranslateMessage(&msg);_ = DispatchMessage(&msg);if (msg.message is WM_QUIT or WM_CLOSE){return;}}}}private static int ToInt32(IntPtr ptr) => IntPtr.Size == 4 ? ptr.ToInt32() : (int)(ptr.ToInt64() & 0xffffffff);private static IEnumerable<DXGI.IDXGIAdapter1> GetHardwareAdapter(DXGI.IDXGIFactory2 factory){DXGI.IDXGIFactory6? factory6 = factory.QueryInterfaceOrNull<DXGI.IDXGIFactory6>();if (factory6 != null){// 先告诉系统,要高性能的显卡for (uint adapterIndex = 0;factory6.EnumAdapterByGpuPreference(adapterIndex, DXGI.GpuPreference.Unspecified,out DXGI.IDXGIAdapter1? adapter).Success;adapterIndex++){if (adapter == null){continue;}DXGI.AdapterDescription1 desc = adapter.Description1;if ((desc.Flags & DXGI.AdapterFlags.Software) != DXGI.AdapterFlags.None){// Don't select the Basic Render Driver adapter.adapter.Dispose();continue;}Console.WriteLine($"枚举到 {adapter.Description1.Description} 显卡");yield return adapter;}factory6.Dispose();}// 如果枚举不到,那系统返回啥都可以for (uint adapterIndex = 0;factory.EnumAdapters1(adapterIndex, out DXGI.IDXGIAdapter1? adapter).Success;adapterIndex++){DXGI.AdapterDescription1 desc = adapter.Description1;if ((desc.Flags & DXGI.AdapterFlags.Software) != DXGI.AdapterFlags.None){// Don't select the Basic Render Driver adapter.adapter.Dispose();continue;}Console.WriteLine($"枚举到 {adapter.Description1.Description} 显卡");yield return adapter;}}private static LRESULT WndProc(HWND hWnd, uint message, WPARAM wParam, LPARAM lParam){return DefWindowProc(hWnd, message, wParam, lParam);}readonly record struct Point2D(double X, double Y);
}

本文代码放在 github 和 gitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin b5109772231d99b403092ce9d29bcbcf0f23b2e2

以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin b5109772231d99b403092ce9d29bcbcf0f23b2e2

获取代码之后,进入 DirectX/D2D/QalberegejeaJawchejoleawerejea 文件夹,即可获取到源代码。欢迎大家拉下来代码跑跑看性能,这个简单的应用能够追得上 WPF 的笔迹应用的性能。本文介绍的这个应用还不能达到 D2D 的最优性能,还有很多优化空间。预计极限性能,笔迹的延迟能和 WPF 追平,部分特殊情况下能够超越 WPF 的性能。本文绘制的笔迹比较粗糙,只是简单的折线,没有带任何笔迹路径平滑和边缘采样优化。如果大家对从触摸收到的点集转换为笔迹路径好奇,请参阅 WPF 笔迹算法 从点集转笔迹轮廓

更多渲染和触摸博客,请参阅 博客导航

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/72109.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

记 X11 里面触摸的一些行为

这是我在学习 CPF 和 Avalonia 过程中,编写的 X11 触摸测试程序所测试到的一些行为前置博客: dotnet 学习 CPF 框架笔记 了解 X11 里如何获取触摸信息 X11 触摸测试程序 测试程序开源代码路径: https://github.com/dotnet-campus/ManipulationDemo/tree/master/Manipulation…

怎么利用 OBS 推送 webrtc 流 ( whip/whep ) 到 smart rtmpd

webrtc whip 推流 & whep 拉流简介 RFC 定义 通用的 webrtc 对于 SDP 协议的交换已经有对应的 RFC 草案出炉了。这就是 WHIP( push stream ) & WHEP ( pull stream ) . WHIP RFC Link: https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html WHEP RFC Link: h…

关于蜂窝模组天线的一些大白话常识

​ 蜂窝模组这个产品形态存在的最大意义,从产业链分工上来说,是提升社会效率。 毕竟让每个需要蜂窝通信的公司自建一个团队重复造轮子,既不经济,也不聪明,就像做衣服的绝大部份公司也没必要自己做拉链一样。 蜂窝模组产品本身最大的特点之一——就是标准化。 无论软件的标…

AT开发HTTP应用:Air780EP低功耗4G模组

​已经写了一篇基于Air780EP模组AT开发的FOTA远程升级指南,有客户朋友询问能否讲讲HTTP应用部分?本期特别安排——涵盖HTTP基本应用流程、GET/POST/SSL请求示例、断点续传、常见问题等内容。 Air780EP是一款低功耗4G全网通模组,兼容模组行业1618经典封装,支持OpenCPU开发及…

MQTT应用:Air780EP低功耗4G模组AT开发

​终于要讲一讲MQTT应用! 本文应各位大佬邀请,详细讲解Air780EP模组MQTT应用的多个AT命令。 Air780EP是低功耗4G模组之一,支持全系列的AT指令以及LuatOS脚本二次开发。 一、准备工作 ​1.1 硬件准备合宙EVB_Air780EP开发板一套,包括天线、SIM卡;USB线PC电脑1.2 软件准备串…

读数据工程之道:设计和构建健壮的数据系统10技术选择

技术选择1. 选择技术 1.1. 架构第一,技术第二 1.2. 现如今数据工程师因技术种类过于繁杂丰富而感到选择困难 1.3. 许多完整并可立即使用的数据技术触手可得1.3.1. 开源代码1.3.2. 托管开源1.3.3. 软件专利1.3.4.…

Pyenv 安装 使用

目录简介如何安装1. 获取Pyenv2. 设置环境变量3. 重启 shell使用指南安装一个 Python 版本。切换 Python 版本。卸载 Python 版本。更新 PyenvPyenv-virtualenv安装创建虚拟环境激活虚拟环境删除虚拟环境 简介 Pyenv 是一款 Python 的版本管理工具,是使用纯 Shell 脚本编写的。…

ROS通信方式(保姆级教程)

目录ROS通信方式主题前言发布器编程实例:小海龟速度控制步骤如下注意:以下是拿小海龟的矩形来写,圆形也一样实现效果订阅器编程实例:小乌龟速度接收 ROS通信方式 主题 前言 工作空间: catkin_ws1 ROS功能包: xhgpfk c++文件: xhgfk.cpp和sudujieshou.cpp 定义一个可执行文件…