C# WinForms结合Halcon与DirectShow实现摄像头二维码识别

发布时间:2026/10/31 6:01:50
C# WinForms结合Halcon与DirectShow实现摄像头二维码识别 简介frmWindowTest.rar 是一份面向C# WinForm开发者的Halcon机器视觉集成示例工程演示通过DirectShow调用笔记本内置摄像头捕获实时视频流并利用Halcon库完成二维码识别与信息解析。压缩包总计36个文件约11.05MB包含源代码文件.cs以及编译生成的.dll、.exe、.pdb等程序集另有配置文件.config/.settings、界面资源.resx/.resources和测试二维码图片.jpg与说明文本.txt目录结构为标准Visual Studio解决方案布局便于直接打开调试与二次开发。资源已吸引461人学习特别适合刚接触WinForm、DirectShow摄像头采集或Halcon机器视觉的读者。通过该工程可快速掌握在C#中托管ActiveX控件、构建滤波图管理器、配置Halcon二维码读取参数等关键流程内置的测试二维码与图像也能帮助验证代码正确性为后续工业视觉项目提供完整的参考起点。1. 拆解 frmWindowTestC# Halcon 调摄像头读二维码一条能跑通的最小实现frmWindowTest 这个压缩包我拿到手拆开以后的第一感觉是它不是一个教学用的空壳窗体而是把整条视觉识别链路闭环了。整个工程用 C# WinForms 做界面摄像头采集交给 DirectShow二维码解码交给 Halcon压缩包里还附带了一张 22.jpg 测试二维码。也就是说你不需要先找打印机打码就能把“摄像头出图 → 图像进算法 → 画面里解码”这条链路完整跑一遍。它适合两类人一类是刚开始把 Halcon 接进 C# 桌面应用、被官方文档绕晕的开发另一类是准备做机器视觉也有人写作“机械视觉”原型选型需要一份现成参照代码的工程师。下面的内容按我实际拆包顺序来先看工程结构再分别拆 DirectShow 和 Halcon 两条技术链最后集中过一遍最容易翻车的坑。2. 工程骨架与两条技术链从 .sln 到 22.jpgDirectShow 和 Halcon 各解哪一段解压以后目录结构大概是这样的frmWindowTest/ ├─ frmWindowTest.sln ├─ frmWindowTest/ │ ├─ frmWindowTest.csproj │ ├─ Form1.cs │ ├─ Form1.Designer.cs │ ├─ Form1.resx │ ├─ Program.cs │ ├─ App.config │ ├─ 22.jpg │ ├─ Properties/ │ ├─ bin/ │ └─ obj/ └─ .vs/.vs是 Visual Studio 的本地缓存目录和源码逻辑无关可以放心忽略。整个解决方案只有一个项目frmWindowTest没有拆多个类库这符合 Demo 的定位所有功能都集中在主窗体代码里跑起来、改起来都直接。2.1 压缩包内各个文件的职责文件/目录职责在这个工程里的角色frmWindowTest.sln解决方案文件Visual Studio 双击打开的入口frmWindowTest.csproj项目文件记录程序集引用、平台目标、.NET 版本Form1.cs主窗体的逻辑代码摄像头初始化和 Halcon 识别的主战场Form1.Designer.cs设计器生成的控件布局面板、按钮、文本框都在这里定义Form1.resx窗体的资源文件图标、字符串等嵌入资源Program.cs程序入口Main 函数启动 Application.Run(new Form1())App.config应用配置文件运行时框架绑定等设置Properties/程序集信息文件AssemblyInfo.cs 控制版本号和特性bin/编译输出目录exe 和引用的 dll 全落在这里obj/中间编译产物编译过程临时文件不需要手动碰22.jpg二维码测试图自带的验证图片用来确认 Halcon 链路刚接触这份代码时重点看 Form1.cs 和 Form1.Designer.cs。Designer 文件会告诉你窗体上实际有哪些控件通常有一个用于显示视频流的 Panel一个触发识别的按钮一个显示解码结果的 ListBox 或 TextBox。而 Form1.cs 里会分成两块一块是 DirectShow 的初始化和预览另一块是 Halcon 模型的建立和解码调用。bin 目录下的 Debug 文件夹里能看到 HalconDotNet.dll 和 halcon.dll这说明工程用的是 Halcon 的 .NET 托管接口不是底层 C 接口。App.config 和 Properties 目录基本不用动除非换目标机器时要调整 .NET 运行时版本。2.2 为什么是 DirectShow Halcon老牌采集链路配上工业视觉解码摄像头采集这块工程选的是 DirectShow 而不是 Media Foundation 或 OpenCV 的 VideoCapture。DirectShow 是 Windows 上非常老牌的流媒体框架核心概念是滤波器图Filter Graph把摄像头设备、视频渲染器、采样器等组件像链条一样连起来数据从源滤波器流出经过中间滤波器处理最后到达渲染器或你的回调。在这个工程里链路要做三件事找到笔记本内置摄像头、把实时视频渲染到窗体的 Panel 控件里、在需要识别时从流中抓取一帧图像。DirectShow 做这类需求非常成熟网上资料多兼容性也稳定换成 USB 工业相机或采集卡时只是换源滤波器的问题整体结构不变。识别部分没有选 OpenCV ZXing而是用 Halcon。Halcon 在工业视觉中的地位不用多说它的二维码解码算子对倾斜、畸变、光照不均的鲁棒性比开源方案好不少而且不少工厂环境里已经买了 Halcon 授权选型时自然会优先用它。这个项目的场景因此非常清晰DirectShow 负责“看到画面”Halcon 负责“看懂画面”两者在 Form1.cs 里通过摄像头抓帧这个动作衔接。理解这个分工后后面调参就有了方向——画质花屏、不出图去查 DirectShow解码率低、识别慢去调 Halcon 侧参数。如果一开始就混在一起排查很容易陷入盲目改代码的循环。2.3 C# 集成 Halcon 的落地方式直接引用 HalconDotNet 的 DLL这个工程集成 Halcon 的方式是最直接的一种项目引用 HalconDotNet.dll代码里直接调用托管接口。它分两层上层 HalconDotNet.dll 是 .NET 包装封装了 HImage、HDataCode2D、HOperatorSet 这些类底层是真正干活的 halcon.dllC 实现的算法核心。部署时这两个文件都必须放在 exe 同目录缺任何一个运行到调用算子的那一步就会抛异常。除了直接引用官方还有两种集成路径一种是 HDevEngine在 C# 里加载 .hdev 脚本执行好处是 HDevelop 里调好的算法不用翻译就能跑缺点是每次调用要解析脚本效率略低另一种是把 HDevelop 脚本导出为 C# 文件再编进工程逻辑固化适合算法基本不变的场景。直接引用的方式前期写起来稍繁琐但每步做了什么都很清楚做自定义逻辑最方便所以是大多数 WinForm 项目的默认选择。这里有个非常容易踩的坑HalconDotNet 的 x86/x64 版本必须和工程的平台目标一致否则经常在启动阶段就报“无法加载 DLL”。在 64 位系统里用 32 位相机 SDK 折腾纯属浪费时间——先把平台目标统一再谈别的。3. DirectShow 摄像头画面接入 WinForm滤波器图构建与预览显示3.1 构建 Capture Graph从 DirectShowLib 引用到出图的关键步骤DirectShow 在 C# 里直接用 COM 接口声明非常啰嗦最常见的做法是引用开源封装库 DirectShowLib。用它的 DsDevice、FilterGraph、CaptureGraphBuilder2 这几个类可以省掉大量重复的接口定义。先看构建滤波器图的核心代码using DirectShowLib; IGraphBuilder graphBuilder (IGraphBuilder)new FilterGraph(); ICaptureGraphBuilder2 capBuilder (ICaptureGraphBuilder2)new CaptureGraphBuilder2(); capBuilder.SetFiltergraph(graphBuilder); // 枚举系统里的摄像头设备取索引 0 的设备 DsDevice[] devices DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice); if (devices.Length 0) { throw new InvalidOperationException(未检测到可用摄像头设备); } Type deviceType Type.GetTypeFromCLSID(devices[0].Clsid); IBaseFilter sourceFilter (IBaseFilter)Activator.CreateInstance(deviceType); graphBuilder.AddFilter(sourceFilter, camera);这段代码做的事情很直白先建一个 Filter Graph 实例再用 CaptureGraphBuilder2 来管理它接着枚举系统里 VideoInputDevice 类目的所有设备取第一台摄像头最后把它的源滤波器加到图中。Activator.CreateInstance的参数是 COM 类标识符CLSID每个摄像头驱动都有唯一的 Clsid不能写死换一台电脑、加一个 USB 摄像头设备列表的顺序都可能变——这也是后面要给工程加设备下拉栏的原因。滤波器加进去以后下一步把预览通路跑起来IBaseFilter renderer (IBaseFilter)new VideoRendererDefault(); graphBuilder.AddFilter(renderer, renderer); // 把摄像头源滤波器的输出引脚连到渲染器建立预览通路 int hr capBuilder.RenderStream(PinCategory.Preview, MediaType.Video, sourceFilter, null, renderer); if (hr ! 0) { throw new COMException(RenderStream 失败HRESULT hr.ToString(X8)); } IMediaControl mediaControl (IMediaControl)graphBuilder; mediaControl.Run();RenderStream是这里最关键的调用它自动找源滤波器的视频输出引脚在中间插入必要的滤波器连到渲染器。参数PinCategory.Preview表示预览通路MediaType.Video限定只匹配视频流如果把它改成PinCategory.Capture抓帧的采样器就要走这条通路拿数据。返回的 HRESULT 为 0 表示成功非 0 基本就是驱动不兼容或设备被占用。很多黑屏问题就是因为忽略了返回值——排查时先确认它是不是 0再去看画面这是个很高效的过滤手段。3.2 把视频窗口嵌进 PanelIVideoWindow 的参数与坑预览通路建立后默认会弹出一个独立视频窗口。要让它显示在窗体的 Panel 里需要通过 IVideoWindow 把视频窗口的所有者改成 Panel 的句柄同时关掉边框、调整尺寸IVideoWindow videoWindow (IVideoWindow)graphBuilder; videoWindow.put_Owner(panelCamera.Handle); videoWindow.put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS); videoWindow.put_Left(0); videoWindow.put_Top(0); videoWindow.put_Width(panelCamera.ClientSize.Width); videoWindow.put_Height(panelCamera.ClientSize.Height);两个样式常量分别是 WS_CHILD0x40000000、WS_CLIPSIBLINGS0x04000000作用是让视频窗口变成 Panel 的子窗口并裁剪兄弟窗口区域。需要注意put_Owner 之后视频窗口就成了 WinForms 体系外的子窗口Panel 每次 Resize 时都必须重设 Width/Height否则视频区域不跟随 Panel底部会留下一片黑边。另外 put_WindowStyle 不只要设 CHILD还要同时清掉原来可能存在的 WS_CAPTION 和 WS_THICKFRAME否则视频左上角会偏移几个像素。这个“偏移”问题我用过一次定位了半小时最后发现是样式没设全气得不行——这些小参数在 DirectShow 集成里就是这么折腾人。3.3 逐帧抓图给 HalconSampleGrabber 回调和缓冲区共享扫二维码是对一帧静止图像做解码不能直接把视频窗口截图喂给 Halcon——窗口截图在画面刷新不完整时会抓到半帧这是最典型的坑。正确做法是加一个 SampleGrabber 滤波器让它在视频流经过时回调我们的代码。抓帧的用法如下ISampleGrabber grabber (ISampleGrabber)new SampleGrabber(); graphBuilder.AddFilter((IBaseFilter)grabber, grabber); AMMediaType mediaType new AMMediaType { majorType MediaType.Video, subType FormatType.RGB24 }; grabber.SetMediaType(mediaType); // 参数 0 表示回调缓冲区模式不阻塞视频流 grabber.SetCallback(new MySampleGrabberCallback(), 0); // Capture 通路连到 grabber再连到渲染器 capBuilder.RenderStream(PinCategory.Capture, MediaType.Video, sourceFilter, (IBaseFilter)grabber, null);把PinCategory从 Preview 换成 Capture 是关键一步Capture 通路上的数据往往是原始视频帧并且我们可以用 SetMediaType 强制指定 RGB24 输出格式这样回调拿到的就是无压缩的 RGB 数据不用自己解析 YUV 格式。这里有常见坑不少摄像头默认输出 MJPG 或 YUY2想让 SampleGrabber 强制转 RGB24 必须在 RenderStream 之前先 SetMediaType连接完成后媒体类型可能已经协商成驱动默认值再改就晚了。回调类写法如下重点注意这里只做拷贝和解码触发不直接调 Halcon 解码class MySampleGrabberCallback : ISampleGrabberCB { public int SampleCB(double sampleTime, IMediaSample pSample) { return 0; // 本工程使用 BufferCB这里直接忽略 } public int BufferCB(double sampleTime, IntPtr pBuffer, int bufferLen) { // 把该帧的 RGB24 数据拷出来避免 DirectShow 复用缓冲区 byte[] frame new byte[bufferLen]; System.Runtime.InteropServices.Marshal.Copy(pBuffer, frame, 0, bufferLen); // 这时的 frame 就是干净的 RGB24 图像数据 OnFrameCaptured?.Invoke(frame); return 0; } }要点是使用 BufferCB 而不是 SampleCBBufferCB 能直接拿到原始内存指针。在回调里做Marshal.Copy拷贝而不是直接引用指针是因为 DirectShow 的缓冲区是复用的——如果不同步拷贝下一帧到达时整份数据就被覆盖。OnFrameCaptured事件把帧交给 UI 线程或独立解码线程回调本身只做拷贝避免阻塞采样线程导致画面丢帧。注意如果摄像头分辨率很高BufferCB 里频繁new byte[bufferLen]会产生大量短生命周期对象建议用对象池或预分配缓冲区复用否则长时间运行会出现 GC 频繁、画面卡顿的问题。4. Halcon 二维码解码模型参数、单帧流程和结果展示4.1 创建二维码模型为什么用 create_data_code_2d_model参数怎么给Halcon 解码二维码入口是一个数据码二维模型Data Code 2D Model。创建代码在 C# 里这样写HDataCode2D dataCode new HDataCode2D(); dataCode.CreateDataCode2dModel(QRCode, new HTuple(), new HTuple());CreateDataCode2dModel 的三个参数分别是码制类型、通用参数名列表、通用参数值列表。工程里 QRCode 是最常见的场景要兼容 DataMatrix把第一参数换成“DataMatrix”再建一个模型就行两个模型可以并存分别去解码。后两个参数传空 HTuple 表示使用默认参数简单场景下直接跑得通。需要精细调参时通过 SetDataCode2dParam 逐项覆盖。结合摄像头扫码场景我常调整的是这几个参数参数名含义常见改动default_param整体识别策略standard 通用robust 对透视畸变更抗噪contrast_tolerance对比度容差曝光不稳定时从 1 调到 2 或 4min_module_size最小模块尺寸像素码离得远时设 8离得近时设 4max_module_size最大模块尺寸像素按视野中二维码的实际占比估算min_module_size 对识别率影响最大设太大小码找不到设太小图像里的噪点会被当成码去训练产生大量误报。经验做法是按图像宽度的 1%2% 给初始值再用固定分辨率相机把二维码放在视野最远端量出它实际占用的像素宽度来回填。4.2 单帧解码流程从静态图到摄像头帧先用工程自带的 22.jpg 把单个环节跑通HImage image new HImage(file, 22.jpg); HXLDCont symbolContours; HTuple decodedStrings; // 模型在初始化时创建这里重复使用 dataCode.FindDataCode2d(image, new HTuple(), new HTuple(), out symbolContours, out decodedStrings);FindDataCode2d的输出参数有两个symbolContours 是找到的符号轮廓XLD可以用来框出二维码的位置decodedStrings 是解码后的字符串列表一个元素对应一个码。输入参数的第二、三个同样是通用参数名和值传空代表使用创建时的属性如果只想对当前这一帧临时提高宽容度可以在这里传contrast_tolerance, 4它会覆盖模型里的默认值。注意这是临时覆盖不影响模型本身。如果现场识别率一直不稳正确的做法是回头改模型参数而不是在调用处反复调临时参数去碰运气。摄像头回调来的 RGB24 字节数组构建 HImage 的方式如下// frame 是 BufferCB 里拷贝出的 RGB24 数据 HImage cameraImage new HImage(rgb, width, height, frame);这里的关键是像素格式匹配DirectShow 端 SetMediaType 设 RGB24HImage 的 rgb 刚好对齐如果某些摄像头驱动不配合依然输出 YUY2HImage 就会出现色偏或花屏。遇到这种情况不要试图在 Halcon 侧硬解应该回到滤波器图里在 SampleGrabber 前插入一个色彩空间转换滤波器或者在 SetMediaType 时先协商好。把像素格式问题丢给 Halcon 侧处理是典型的找错层。4.3 解码结果展示与位置信息提取解码后要把结果亮出来同时显示一下码的位置信息这对调试很有用。取位置信息需要调 GetDataCode2dResultsHTuple row, col, angle; HOperatorSet.GetDataCode2dResults(modelHandle, row, null, out row); HOperatorSet.GetDataCode2dResults(modelHandle, col, null, out col); HOperatorSet.GetDataCode2dResults(modelHandle, angle, null, out angle);这里注意一个容易混淆的点GetDataCode2dResults的第一个参数是模型句柄不是 FindDataCode2d 返回的结果句柄结果句柄是在 FindDataCode2d 内部由运行时管理的用来衔接多次算子调用。我通常要么全程用 HDataCode2D 类方法要么全程用 HOperatorSet 加句柄避免混合使用导致句柄释放顺序混乱。句柄漏了短时间看不出问题长时间运行后内存会缓慢增长这对于机器视觉现场程序来说是很头疼的稳定性问题。如果画面里同时出现多个二维码decodedStrings 就是一个字符串元组长度代表码的数量。Demo 一般显示第一个但我建议把全部遍历出来丢进 ListBox这样你拿 22.jpg 和手机屏幕反复换场景时能直观看到一帧图能解出几个码对后面调尺寸参数很有帮助。多码逻辑也能顺带验证你的遍历代码是否健壮避免现场出现多码时程序只认第一个的尴尬。5. 避坑与排查摄像头不出图、识别率低、退出卡死的高频问题这份工程代码量不大但我从拆包、运行、改动的过程看最容易出事的恰恰是几个看起来不起眼的环节。下面挑五个高频情况按现象、原因、解决三步记录。5.1 摄像头预览黑屏或灰屏现象窗体正常启动Panel 区域是黑色或灰色没有异常弹窗或者启动后能出画面但切换一下窗口再回来就黑了。原因最常见的是mediaControl.Run()没被调用预览通路建好了却不跑渲染器永远拿不到第一帧其次是切换窗口或 Panel 重建句柄后IVideoWindow 的 owner 没有更新旧句柄失效后画面就黑屏。解决先确认 Run 被调用且返回 HRESULT 为 0然后在 Panel 的 Resize 和 ParentChanged 事件里重设 put_Owner 以及宽高不要只设一次就指望它一直生效。切窗口黑屏的土办法是每次窗体激活时重新 put_Owner 一次方法不优雅但是有效。5.2 找不到摄像头或 COM 插件报错现象DsDevice.GetDevicesOfCat 返回空列表或 AddFilter 时抛 COMException同一段代码在同事电脑上正常。原因笔记本内置摄像头被其他程序占用时设备枚举可能为空AddFilter 也会失败另外很多机器有多个摄像头设备前摄、后摄、IR 摄像头固定用索引 0 换台机器就可能选到错误的设备。解决把枚举到的设备做成下拉菜单让使用者手动选择AddFilter 捕获异常后提示“摄像头被占用”加一个重试按钮工业现场 USB 摄像头的插拔也应纳入运行时的错误恢复不能只依赖启动时枚举一次。5.3 实时画面解码率忽高忽低现象22.jpg 静态识别基本百分百成功切到摄像头后识别率掉到一半以下换张二维码更明显。原因分辨率过高例如 1920×1080 下二维码占的像素太少解码时容易被当背景噪声另一个大坑是自动曝光车间灯光一波动画面就糊部分摄像头还有自动对焦来回拉风箱的时候画面模糊解码自然失败。解决把摄像头降到 640×480 或 1280×720让码在画面里占更大的像素面积在滤波器链路上尝试关闭自动曝光、固定焦距不是所有摄像头驱动都支持如果必须大图先在模型里把 min_module_size 和 max_module_size 设准再把 ROI 区域裁出来识别不要整幅图直接丢给解码。5.4 帧回调里直接做 Halcon 解码把预览卡成一帧一帧现象画面像幻灯片一样一格一格跳CPU 占用率很高有时关闭窗体后任务管理器里还有进程残留。原因BufferCB 运行在 DirectShow 的流线程里你在回调里把整帧转成 HImage 再做 FindDataCode2d一次解码 3060 毫秒而视频流一帧只有 33 毫秒左右直接堵死了整条流。解决回调里只做拷贝并入队解码交给 UI 线程的 Timer 或独立线程去消费队列。这个结构改动是让 Demo 走向可用的关键哪怕现在只是一帧一识别也按这个模式搭不然后面联调只会越调越乱。5.5 关闭窗体时进程卡死不退出现象点 X 关闭窗口后进程还挂在任务管理器里窗口不见了但进程不退。原因DirectShow 没停mediaControl 还处于 Running 状态或者解码线程阻塞在队列等待上而线程没有被通知退出。解决在 OnFormClosing 里按顺序处理——先把视频窗口与 Panel 剥离再mediaControl.Stop()然后向解码线程设置退出标志并 Join 等待最后释放 graphBuilder。顺序上我倾向先剥窗口再 Stop否则视频窗口的通知链里会对已销毁句柄发起访问偶发崩溃。把退出流程写好摄像头工程才谈得上稳定机器视觉现场程序往往要长时间挂机退出问题一出就是冒烟事故。6. 验证与进阶让 Demo 从“能跑”变成“能稳定用”6.1 先做静态验证再上摄像头复现完这个工程后我的第一条经验是不要一上来就开摄像头联调否则问题混在一起根本没法定位。先按下面这张表分层验证验证步骤操作预期结果1. Halcon 链路启动后点“读取图片”选择 22.jpg文本框输出二维码内容无异常2. DirectShow 链路点击“启动摄像头”Panel 显示实时画面缩放窗体不黑屏3. 联合链路用手机屏幕展示一个二维码对准摄像头解码结果稳定刷新与真实内容一致跑完三步哪段有问题直接缩小到对应章节排查不用把 DirectShow 和 Halcon 混在一起猜。能省下大量排查时间。6.2 进阶一个队列加一个线程把回调压力隔开如果这个 Demo 要拿去做实际应用建议把帧回调里的逻辑改成“入队 独立解码线程”。这是便宜实用的小改造大部分功能不用动private ConcurrentQueuebyte[] frameQueue new ConcurrentQueuebyte[](); public int BufferCB(double sampleTime, IntPtr pBuffer, int bufferLen) { byte[] copy new byte[bufferLen]; Marshal.Copy(pBuffer, copy, 0, bufferLen); frameQueue.Enqueue(copy); // 只入队不解码 return 0; }识别线程循环从队列里取一帧处理完再取下一帧while (isRunning frameQueue.TryDequeue(out byte[] frame)) { HImage image new HImage(rgb, width, height, frame); HXLDCont contours; HTuple decoded; dataCode.FindDataCode2d(image, paramName, paramValue, out contours, out decoded); // 这里再把结果通过 BeginInvoke 回写到 UI }同时给队列设一个长度上限超限时直接丢弃旧帧保证解码线程处理的是最新画面避免内存持续膨胀。做完这个改造你会发现 UI 不卡了识别率也稳了因为采集和图像处理彻底分开了。从那以后我每次拿到摄像头加图像库的 Demo 包都会先检查回调结构是不是“一个队列加一个线程”如果回调里直接埋了识别逻辑我宁愿先重构再看效果这个习惯帮我避开了很多现场联调的坑。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询