十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C# WinForm排队叫号系统实战:扫码取号、语音播报与硬件对接

C# WinForm排队叫号系统实战:扫码取号、语音播报与硬件对接 简介本资源是一套基于C# WinForm开发的完整排队叫号系统课程设计项目面向高校计算机专业学生、C/S架构初学者及政务/医疗/银行等窗口服务类软件开发者解决多终端协同、软硬联动与业务可扩展性等实际落地难题。压缩包含1560个文件主体为938个C#源码文件含WinForm主程序、Remoting通信模块、LED屏控制逻辑、135张界面图标与提示图、75个本地化资源文件辅以59个前端交互JS脚本、52个依赖DLL及43个VS工程配置文件整体25.55MB结构清晰体现C/SB/S混合架构分层设计。已有482人学习下载。读者可直接运行调试完整流程从微信预约取号、绿色通道优先调度到LED条屏实时显示、语音播报、服务评价采集再到后台MVC数据维护与安卓端综合显示屏集成预览中大量Chloe ORM相关bat与csproj文件表明已内置成熟数据库访问方案便于快速适配SQL Server等主流数据库。1. 为什么一个 WinForm 排队叫号系统至今仍是银行、政务大厅和医院前台的主力选择你可能在银行柜台前见过那个带红绿灯提示、语音播报“请023号到3号窗口”的屏幕也可能在社区服务中心看到工作人员点击“下一个”大屏立刻跳转号码并同步触发语音——这背后大概率不是 Vue 或 Electron而是一个运行在 .NET Framework 4.7.2 上的 WinForm 程序。它不炫酷但稳定不依赖浏览器却能直连 USB 扫码枪、串口叫号器、LED 显示屏和本地扬声器它没有响应式布局却能在 1366×768 的老旧工控机上十年不重启。这不是技术怀旧而是场景刚性需求低延迟指令响应扫码即叫、离线可靠运行断网仍可取号叫号、与 Windows 设备生态深度集成HID 扫码枪免驱、COM 口 LED 控制、WaveOut 音频播放。本项目【100010339】正是为这类真实生产环境设计的 C# WinForm 排队叫号系统——它不追求跨平台只解决“扫码→生成号→分配窗口→语音播报→大屏同步→历史可查”这一闭环中每个环节的确定性交付。适合需要快速落地、对接硬件、维护成本低的中小型服务网点开发人员也适合作为 C# 桌面应用工程能力的典型训练场。2. 用 C# WinForm 实现排队叫号核心流程从扫码触发到语音播报的最小闭环2.1 扫码枪输入的本质是键盘模拟必须绕过 TextBox 的默认焦点劫持扫码枪接入 USB 后在 Windows 中被识别为 HID 键盘设备其扫描动作等效于快速敲击一串数字回车。若将扫码焦点放在普通TextBox上会因输入法切换、焦点丢失、回车事件未绑定等问题导致漏扫或误扫。正确做法是全局监听KeyDown事件并过滤出纯数字Enter 组合// 在主窗体 Form1.cs 中重写 ProcessCmdKey protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { if (keyData Keys.Enter _scanBuffer.Length 0) { string scannedNumber _scanBuffer.Trim(); if (IsValidQueueNumber(scannedNumber)) { EnqueueNewNumber(scannedNumber); // 触发取号逻辑 } _scanBuffer string.Empty; return true; // 拦截回车防止触发其他按钮 } else if (keyData Keys.D0 keyData Keys.D9) { _scanBuffer (char)(0 (keyData - Keys.D0)); return true; } else if (keyData Keys.NumPad0 keyData Keys.NumPad9) { _scanBuffer (char)(0 (keyData - Keys.NumPad0)); return true; } return base.ProcessCmdKey(ref msg, keyData); }提示_scanBuffer是私有字段private string _scanBuffer string.Empty;用于累积扫码字符。此方案完全规避了TextBox的焦点管理问题且兼容任意品牌扫码枪无需 SDK是 WinForm 场景下最鲁棒的扫码捕获方式。2.2 号码生成与窗口分配策略用 ConcurrentQueue Dictionary 实现线程安全调度叫号系统需同时支持“前台取号”扫码生成新号和“后台叫号”窗口点击叫下一个二者操作共享同一号码队列。WinForm 默认运行在 UI 线程但扫码、语音播放、LED 同步等操作若阻塞主线程会导致界面卡死。因此必须分离数据模型与 UI 更新// 号码数据模型独立于 UI public class QueueItem { public string Number { get; set; } public DateTime CreatedTime { get; set; } public string WindowId { get; set; } // 被叫到的窗口ID public bool IsCalled { get; set; } } // 线程安全的队列与状态字典 private readonly ConcurrentQueueQueueItem _waitingQueue new(); private readonly ConcurrentDictionarystring, QueueItem _currentCalling new(); // key: windowId, value: 正在叫的号 private readonly object _lockObject new(); // 前台扫码取号UI线程调用 private void EnqueueNewNumber(string number) { var item new QueueItem { Number number, CreatedTime DateTime.Now, IsCalled false }; _waitingQueue.Enqueue(item); UpdateWaitingListUI(); // 仅刷新ListView不执行耗时操作 } // 后台窗口叫号例如点击“3号窗口”按钮 private void CallNextForWindow(string windowId) { if (_waitingQueue.TryDequeue(out QueueItem nextItem)) { nextItem.WindowId windowId; nextItem.IsCalled true; _currentCalling[windowId] nextItem; // 异步触发多通道反馈避免阻塞UI Task.Run(() { PlayVoiceAnnouncement(nextItem); // 语音播报 SendToLEDScreen(nextItem); // 发送至LED屏 UpdateCurrentCallingUI(windowId); // 刷新对应窗口显示 }); } }注意ConcurrentQueue和ConcurrentDictionary是 .NET Framework 4.0 原生线程安全集合无需额外加锁。Task.Run将耗时 I/O 操作音频播放、串口通信移出 UI 线程这是解决 “winform 窗体缩放 尺寸改不了” 或 “c# 循环数据采集和ui刷新卡顿” 的根本前提——UI 线程只做轻量状态更新重活交给后台线程。2.3 语音播报的三种实现路径与选型依据叫号系统对语音播报要求低延迟扫码后 0.5s 响应、高可靠性不因音频文件损坏失败、可配置音源支持TTS或WAV。WinForm 提供三种原生方案方案实现方式适用场景关键参数说明System.Media.SoundPlayer播放 WAV 文件需固定音效如“请023号到3号窗口”预录成多个WAVplayer.LoadAsync(call_023_to_3.wav); player.PlaySync();——PlaySync()阻塞当前线程必须在 Task.Run 中调用否则卡 UISpeechSynthesizer.NET Framework 自带Windows TTS 引擎动态合成“请{number}号到{window}号窗口”synth.SetOutputToDefaultAudioDevice(); synth.Speak($请{num}号到{win}号窗口);—— 需提前安装中文语音包控制面板→语音识别→文本转语音NAudio第三方库专业音频处理需混音、音量调节、播放中断控制using (var audioFile new AudioFileReader(prompt.wav)) { ... }—— 需 NuGet 安装NAudio学习成本略高但控制力最强推荐组合日常使用SpeechSynthesizer免资源管理动态灵活关键提示音如“现在开始营业”用SoundPlayer播放 WAV音质稳定。代码示例private void PlayVoiceAnnouncement(QueueItem item) { try { using (var synth new SpeechSynthesizer()) { // 设置语速-10~10默认0、音量0~100、语音需系统已安装 synth.Rate 2; synth.Volume 100; synth.SelectVoiceByHints(VoiceGender.Female, VoiceAge.Adult, 0, zh-CN); // 中文女声 synth.Speak($请{item.Number}号到{item.WindowId}号窗口); } } catch (Exception ex) { // TTS不可用时降级为WAV播放 var fallbackPath Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fallback_call.wav); if (File.Exists(fallbackPath)) { using (var player new SoundPlayer(fallbackPath)) { player.PlaySync(); // 同步播放确保顺序 } } } }3. WinForm 界面工程化解决实际部署中的尺寸适配、硬件对接与视觉一致性3.1 解决 “winform 窗体缩放 尺寸改不了” 的三重保障机制政务大厅常使用 1920×1080 大屏而银行柜台多为 1366×768 工控机。硬编码Size或Location必然失效。正确做法是结合AutoScaleMode、Dock/Anchor与 DPI 感知// 在 Form 构造函数中设置必须在 InitializeComponent() 之后 public Form1() { InitializeComponent(); // 1. 启用DPI感知Windows 10 this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); this.AutoScaleMode AutoScaleMode.Dpi; // 关键按DPI缩放非Font或None // 2. 主容器使用 TableLayoutPanel 布局替代绝对定位 var tableLayoutPanel new TableLayoutPanel { Dock DockStyle.Fill, ColumnCount 2, RowCount 3, Padding new Padding(10), ColumnStyles { new ColumnStyle(SizeType.Percent, 70F), new ColumnStyle(SizeType.Percent, 30F) }, RowStyles { new RowStyle(SizeType.Percent, 60F), new RowStyle(SizeType.Percent, 20F), new RowStyle(SizeType.Percent, 20F) } }; // 3. 内部控件设置 Anchor/Dock如叫号屏 DockFill按钮 AnchorBottomRight _lcdDisplay.Dock DockStyle.Fill; _btnCallNext.Anchor AnchorStyles.Bottom | AnchorStyles.Right; this.Controls.Add(tableLayoutPanel); }提示AutoScaleMode.Dpi是 WinForm 在高分屏下保持清晰度的核心配合TableLayoutPanel的百分比布局可覆盖 100%~200% DPI 缩放。避免使用AutoSizetrue或手动计算ScaleFactor后者在多显示器混合 DPI 下极易崩溃。3.2 USB 扫码枪与串口 LED 屏的硬件对接实操扫码枪无需驱动但需处理 HID 报文干扰部分工业扫码枪如霍尼韦尔 1900在扫描后会额外发送ESC或TAB键。上述ProcessCmdKey已过滤Keys.Escape和Keys.Tab但需在_scanBuffer累积时增加超时清空机制防止单次扫描残留private Timer _scanTimeoutTimer; private void InitializeScanTimer() { _scanTimeoutTimer new Timer { Interval 300 }; // 300ms内无新按键则清空 _scanTimeoutTimer.Tick (s, e) { if (_scanBuffer.Length 0) { _scanBuffer string.Empty; _scanTimeoutTimer.Stop(); } }; } // 在 ProcessCmdKey 的数字键分支末尾启动定时器 _scanTimeoutTimer.Stop(); _scanTimeoutTimer.Start();LED 显示屏通过 SerialPort 发送十六进制指令常见 LED 屏如深圳某品牌使用 RS232 串口协议为0x00 0x01 0xXX ... 0xFF头尾校验。C# 使用SerialPort类private SerialPort _ledPort; private void InitializeLEDPort() { _ledPort new SerialPort(COM3, 9600, Parity.None, 8, StopBits.One); _ledPort.Open(); } private void SendToLEDScreen(QueueItem item) { try { // 构造指令00 01 [02] [03] [04] [05] FF 示例显示023 byte[] cmd new byte[] { 0x00, 0x01, (byte)(0), (byte)(2), (byte)(3), 0xFF }; _ledPort.Write(cmd, 0, cmd.Length); } catch (UnauthorizedAccessException) { // COM口被占用记录日志并提示 MessageBox.Show(LED屏串口不可用请检查连接, 硬件错误, MessageBoxButtons.OK, MessageBoxIcon.Error); } }注意SerialPort非线程安全_ledPort.Write必须在 UI 线程或加锁调用。生产环境建议封装为单例LEDController并内置重试逻辑最多3次间隔200ms。3.3 winform界面美化用 GDI 绘制高对比度叫号屏与状态指示灯政务大厅环境光线复杂LCD 屏需高对比度文字。WinForm 原生控件如Label抗锯齿差、字体渲染模糊。直接使用Graphics绘制是唯一可控方案private void DrawCallingScreen(Graphics g) { // 清空背景深蓝 g.Clear(Color.FromArgb(15, 35, 65)); // 绘制大号数字抗锯齿开启 using (var font new Font(微软雅黑, 120, FontStyle.Bold)) using (var brush new SolidBrush(Color.White)) using (var format new StringFormat { Alignment StringAlignment.Center, LineAlignment StringAlignment.Center }) { RectangleF rect new RectangleF(0, 0, this.Width, this.Height * 0.6f); g.TextRenderingHint TextRenderingHint.ClearTypeGridFit; g.DrawString(023, font, brush, rect, format); } // 绘制窗口号下方小字 using (var font new Font(微软雅黑, 48, FontStyle.Regular)) using (var brush new SolidBrush(Color.FromArgb(100, 200, 255))) { g.DrawString(请到 3 号窗口, font, brush, new PointF(this.Width / 2, this.Height * 0.7f), new StringFormat { Alignment StringAlignment.Center }); } } // 在窗体 Paint 事件中调用 private void lcdDisplay_Paint(object sender, PaintEventArgs e) { DrawCallingScreen(e.Graphics); }提示TextRenderingHint.ClearTypeGridFit是 WinForm 中启用 ClearType 渲染的关键比AntiAlias更锐利。所有文字绘制必须在Paint事件中完成禁止在Timer中CreateGraphics()——后者绘图无法持久化且引发闪烁。4. 生产环境必备日志审计、异常熔断与跨版本兼容性保障4.1 结构化日志记录取号与叫号全链路支持故障回溯WinForm 应用常部署在无网络环境日志必须本地落盘且防丢。使用StreamWriter追加写入配合日期分片与大小轮转private static readonly object _logLock new(); private void LogOperation(string action, string details) { string logLine ${DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{action}] {details}{Environment.NewLine}; string logPath Path.Combine(AppDomain.CurrentDomain.BaseDirectory, logs); Directory.CreateDirectory(logPath); string todayLog Path.Combine(logPath, $queue_{DateTime.Today:yyyyMMdd}.log); // 单文件不超过5MB自动切分 if (new FileInfo(todayLog).Length 5 * 1024 * 1024) { File.Move(todayLog, Path.Combine(logPath, $queue_{DateTime.Now:yyyyMMdd_HHmmss}.log.bak)); } lock (_logLock) // 确保多线程写入安全 { File.AppendAllText(todayLog, logLine); } } // 在关键节点调用 private void EnqueueNewNumber(string number) { _waitingQueue.Enqueue(new QueueItem { Number number, CreatedTime DateTime.Now }); LogOperation(取号, $号码:{number}, 队列长度:{_waitingQueue.Count}); }注意File.AppendAllText内部已加锁但多进程并发写入仍需外部lock。日志格式严格遵循时间 [操作] 详情便于用grep或 Excel 快速筛选如grep 叫号 queue_20240501.log查当日所有叫号记录。4.2 熔断机制当语音引擎或LED屏连续失败时自动降级硬件故障不可预测。若SpeechSynthesizer初始化失败3次或SerialPort.Write抛出IOException超过5次系统应自动关闭对应模块并通知管理员private int _ttsFailureCount 0; private int _ledFailureCount 0; private const int MAX_FAILURES 3; private void PlayVoiceAnnouncement(QueueItem item) { try { // ... TTS 播放逻辑 _ttsFailureCount 0; // 成功则清零计数 } catch (Exception ex) { _ttsFailureCount; if (_ttsFailureCount MAX_FAILURES) { DisableTTSEngine(); // 禁用TTS后续只播WAV LogOperation(熔断, $TTS引擎连续失败{_ttsFailureCount}次已降级); } } } private void DisableTTSEngine() { _isTTSEnabled false; // UI上显示警告图标 _lblTTSStatus.Image Properties.Resources.warning_icon; _lblTTSStatus.Text TTS已禁用; }4.3 .NET Framework 版本兼容性锁定 4.7.2 作为最低运行时项目【100010339】明确要求 WinForm意味着必须基于 .NET Framework非 .NET Core/.NET 5。Windows 7 SP1 默认自带 .NET Framework 3.5而 4.7.2 是 Win10 1803 自带、Win7 需手动安装的平衡点——它支持SpanT、ValueTask等现代特性又避免 4.8 的部分 API 兼容性问题!-- 在 .csproj 文件中显式指定 -- TargetFrameworkVersionv4.7.2/TargetFrameworkVersion SupportedRuntime Versionv4.0 sku.NETFramework,Versionv4.7.2 /验证方法在目标机器运行cmd输入reg query HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full /v Release返回值528040即表示已安装 4.7.2 微软官方对照表 。部署包必须包含NDP472-KB4054530-x86-x64-AllOS-ENU.exe安装程序而非仅复制 DLL。5. 进阶技巧用 Windows 服务托管叫号后台实现开机自启与无人值守5.1 将核心调度逻辑剥离为 Windows ServiceWinForm 仅作前端展示当前架构中WinForm 窗体既是 UI 又承载业务逻辑一旦用户关闭窗体整个系统停止。生产环境要求“开机即运行、断电恢复后自动续接”。解决方案将_waitingQueue、_currentCalling等状态迁移至 Windows ServiceWinForm 通过命名管道NamedPipe与其通信。步骤一创建 Windows Service 项目.NET Framework 4.7.2新建项目 → Windows Service → 重命名为QueueService。在OnStart中启动调度器protected override void OnStart(string[] args) { _scheduler new QueueScheduler(); // 包含ConcurrentQueue等状态 _scheduler.Start(); // 启动内部Timer轮询叫号逻辑 // 创建命名管道服务器等待WinForm连接 _pipeServer new NamedPipeServerStream(QueueServicePipe, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); _pipeServer.BeginWaitForConnection(OnClientConnected, null); }步骤二WinForm 通过命名管道发送取号/叫号指令private void SendCommandToService(string command, string data ) { try { using (var pipe new NamedPipeClientStream(., QueueServicePipe, PipeDirection.Out)) { pipe.Connect(5000); // 5秒超时 using (var writer new StreamWriter(pipe)) { writer.WriteLine(${command}|{data}); writer.Flush(); } } } catch (TimeoutException) { MessageBox.Show(叫号服务未启动请检查Windows服务状态, 连接失败); } } // 扫码后调用 private void EnqueueNewNumber(string number) { SendCommandToService(ENQUEUE, number); }步骤三服务端解析指令并更新状态private void OnClientConnected(IAsyncResult result) { try { _pipeServer.EndWaitForConnection(result); // 读取指令 using (var reader new StreamReader(_pipeServer)) { string line reader.ReadLine(); var parts line.Split(|); switch (parts[0]) { case ENQUEUE: _scheduler.Enqueue(parts[1]); break; case CALLNEXT: _scheduler.CallNext(parts[1]); break; } } } finally { _pipeServer.WaitForPipeDrain(); _pipeServer.Close(); // 重新监听下一个连接 _pipeServer new NamedPipeServerStream(...); _pipeServer.BeginWaitForConnection(OnClientConnected, null); } }优势WinForm 窗体可随时关闭重启不影响队列状态服务可设为“自动延迟启动”避开系统启动高峰管理员可通过services.msc直接启停服务无需接触 UI。这是 WinForm 排队叫号系统走向企业级部署的关键一步——把“桌面程序”真正变成“后台基础设施”。5.2 服务安装脚本用 InstallUtil.exe 一键注册附 PowerShell 替代方案Visual Studio 生成的 Service 项目默认包含ProjectInstaller但需命令行安装# 以管理员身份运行 PowerShell $servicePath C:\QueueService\QueueService.exe $env:windir\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe $servicePath # 启动服务 Start-Service QueueService注意InstallUtil.exe路径需匹配 .NET Framework 版本此处为 v4.0.30319。若目标机器无 VS可预先编译好install.bat内容为echo off %WINDIR%\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe %~dp0QueueService.exe net start QueueService pause服务名称QueueService需在ServiceInstaller属性中显式设置确保与脚本一致。本文还有配套的精品资源点击获取
返回列表