简介这是一套基于C# WinForm开发的医院挂号管理系统源码面向.NET初学者与中小型医疗信息化项目实践者解决门诊挂号流程数字化、科室医生信息集中管理及患者服务自动化等实际问题。资源包共185个文件含64个核心业务逻辑.cs文件、42张医生照片.jpg、19个本地化资源.resources与.resx文件、3个SQL脚本用于数据库初始化与存储过程部署以及可直接运行的.exe程序和配套帮助文档.chm整体压缩后仅10.03MB结构清晰、分层明确。已有1262人学习下载适合理解C/S架构下MVC分层设计、MD5密码加密实现、报表图文统计展示及照片上传功能集成。代码完整覆盖用户管理、科室医生维护、门急诊挂号、查询打印等全流程模块附带.sln解决方案与.csproj工程文件开箱即用是学习WinForm企业级应用开发的优质参考案例。1. 为什么一个“用C# WinForm做的医院挂号管理系统”至今还在三甲医院信息科跑着不是因为它用了什么高并发架构或微服务——它压根没上云数据库是本地 SQL Server Express主界面还是带阴影边框的Panel套GroupBox也不是因为代码有多优雅——你打开Form1.cs会看到 237 行private void btnReg_Click(object sender, EventArgs e)里嵌了 5 层if-else、硬编码的科室 ID 映射表、以及一段用DateTime.Now.ToString(yyyyMMddHHmmss)拼接挂号单号的逻辑。但它稳定连续 47 个月零崩溃日均处理 1862 人次挂号医保接口调用失败时自动降级为手工录入模式护士站阿姨点错三次后系统会弹出带语音提示的防误操作浮层。这就是 C# WinForm 在医疗一线的真实生存状态不炫技、不赶时髦、不谈DDD但必须扛住早八点门诊高峰的并发点击、兼容十年老打印机、能被 55 岁护士长徒手改科室排班表、且每次 Windows 更新后仍能双击.exe就跑。本文不讲 WPF 或 Blazor 替代方案只聚焦一个现实问题如何用 WinForm 写出一个真正能在医院信息科交付、运维、迭代三年不翻车的挂号系统——从数据库设计边界、UI 线程安全陷阱、到医保对接时那个让RestClient.Execute()突然抛出“远程主机强迫关闭连接”的玄学超时全部拆进可抄作业的步骤里。2. 从零搭起挂号系统骨架WinForm 工程结构与核心模块划分挂号系统不是“窗体拖控件按钮写事件”的线性堆砌。真实场景中一个挂号员要同时应对患者刷身份证读卡、医保实时结算、叫号屏同步更新、医生工作站预约联动、退号时的号源回滚、以及凌晨三点数据库备份失败后的手动补录。这些需求倒逼出四个不可拆分的核心模块而 WinForm 的工程组织必须围绕它们展开。2.1 模块化分层为什么拒绝“Form1.cs 万能文件”新手常把所有逻辑塞进主窗体结果导致修改挂号流程时牵一发而动全身比如加个“优先老人号”规则得在btnReg_Click、btnCancel_Click、timerRefresh_Tick三处改医保接口升级如从旧版 HIS 接口切到国家医保平台需重写整个网络层却不敢动 UI 层测试时无法隔离验证“号源分配算法”只能靠人工点按钮观察结果。我坚持的分层结构VS 2022 .NET 6HospitalRegistrationSystem/ ├── HospitalRegistrationSystem.UI/ // WinForm 界面层仅含 Form、UserControl、资源文件 ├── HospitalRegistrationSystem.Business/ // 业务逻辑层挂号规则、号源调度、退号策略 ├── HospitalRegistrationSystem.Data/ // 数据访问层SQL Server 操作、实体映射、事务封装 ├── HospitalRegistrationSystem.Common/ // 公共工具身份证校验、时间格式化、日志记录器 └── HospitalRegistrationSystem.Tests/ // 单元测试重点覆盖 Business 层算法提示Business 层不引用任何 WinForm 类型如Control、MessageBox所有 UI 交互通过事件或回调函数注入。例如挂号成功后触发OnRegistrationCompleted事件由 UI 层决定是弹窗提示还是更新 ListView。2.2 数据库设计避开医疗场景三大致命坑挂号系统数据库不是简单的PatientDoctorSchedule三张表。真实踩坑来自号源动态释放医生临时停诊已挂出的号必须自动转为“可退号”状态而非直接删除时段冲突同一医生上午 8:00-9:00 号源为 20 个但实际挂号时可能因网络延迟导致超卖历史追溯医保结算失败后需人工补录但原始挂号时间、操作员、终端 IP 必须留痕。关键表结构SQL Server-- 号源主表记录每个时段每个医生的可用号数非实时库存而是调度依据 CREATE TABLE ScheduleSource ( Id INT IDENTITY(1,1) PRIMARY KEY, DoctorId INT NOT NULL, Date DATE NOT NULL, TimeSlot VARCHAR(10) NOT NULL, -- 08:00-09:00 TotalQuota INT NOT NULL, -- 总号源 UsedQuota INT DEFAULT 0, -- 已用号源乐观锁更新 Status TINYINT DEFAULT 1 -- 1正常, 2停诊, 3暂停挂号 ); -- 挂号主表含完整业务状态机 CREATE TABLE Registration ( Id UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY, PatientId NVARCHAR(18), -- 身份证号 DoctorId INT, ScheduleDate DATE, TimeSlot VARCHAR(10), RegNumber NVARCHAR(20), -- 挂号单号格式YYMMDD-XXXXX Status TINYINT DEFAULT 1, -- 1已挂号, 2已缴费, 3已就诊, 4已退号, 5医保失败待补录 OperatorId INT, -- 操作员ID关联员工表 TerminalIP VARCHAR(15), -- 终端IP用于审计 CreatedAt DATETIME2 DEFAULT GETDATE(), UpdatedAt DATETIME2 DEFAULT GETDATE() ); -- 医保结算表分离核心挂号与支付支持失败重试 CREATE TABLE InsuranceSettlement ( Id UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY, RegistrationId UNIQUEIDENTIFIER NOT NULL, SettlementId NVARCHAR(50), -- 医保平台返回的结算流水号 Amount DECIMAL(10,2), Status TINYINT DEFAULT 0, -- 0未结算, 1成功, 2失败, 3待重试 RetryCount INT DEFAULT 0, -- 失败后自动重试次数 LastError NVARCHAR(500), -- 最后一次错误详情 CreatedAt DATETIME2 DEFAULT GETDATE() );参数说明ScheduleSource.UsedQuota更新必须用UPDATE ... SET UsedQuota UsedQuota 1 WHERE Id id AND UsedQuota TotalQuotaROWCOUNT判断是否超卖这是防止并发超挂的底线。Registration.Status设计为状态机而非布尔值后续扩展“爽约锁定”、“转诊标记”等无需改表结构。2.3 WinForm 主窗体初始化避免 UI 线程阻塞的三个动作主窗体MainForm加载时若执行耗时操作如加载科室列表、初始化打印机会导致界面假死。常见错误写法private void MainForm_Load(object sender, EventArgs e) { LoadDepartments(); // 同步加载界面卡顿 InitPrinter(); // 同步初始化可能阻塞10秒 RefreshScheduleGrid(); // 查询当日号源数据库慢则白屏 }正确做法异步 进度反馈private async void MainForm_Load(object sender, EventArgs e) { // 1. 启动后台任务并显示加载动画 var progress new Progressstring(msg statusLabel.Text msg); await Task.Run(() LoadInitialData(progress)); // 2. 初始化打印机失败不中断主流程 _printer TryInitPrinter(); // 3. 启动定时刷新每30秒查一次号源 _scheduleTimer new Timer { Interval 30000 }; _scheduleTimer.Tick (s, ev) RefreshScheduleGridAsync(); _scheduleTimer.Start(); } private void LoadInitialData(IProgressstring progress) { progress.Report(正在加载科室...); Departments DepartmentService.GetAll(); progress.Report(正在加载医生...); Doctors DoctorService.GetByDate(DateTime.Today); progress.Report(正在加载今日号源...); ScheduleSources ScheduleService.GetTodaySources(); }逻辑说明Task.Run将 CPU 密集型操作如 XML 解析科室配置移出 UI 线程IProgressT实现线程安全的进度通知打印机初始化失败时返回null后续挂号时再提示“请检查打印机连接”而非启动失败。3. 挂号核心流程实现从身份证读取到号源分配的完整链路挂号流程看似简单刷身份证 → 选科室医生 → 选时段 → 确认 → 打印凭条。但每个环节都藏着医疗场景特有的约束和异常路径。3.1 身份证读取与校验兼容国产二代证读卡器医院普遍使用华大半导体或神思电子的 USB 读卡器驱动提供IDCardReader.dll但官方 SDK 文档极简。常见问题读卡器插拔后程序崩溃未处理设备热插拔连续刷同一张卡两次第二次返回空数据SDK 内部缓冲未清身份证照片字段为byte[]WinFormPictureBox直接赋值报Parameter is not valid。稳定读卡封装C#public class IdCardReaderService { private readonly object _lock new object(); private bool _isReading false; public async TaskIdCardInfo ReadCardAsync(CancellationToken ct default) { // 防止重复调用 if (Interlocked.CompareExchange(ref _isReading, 1, 0) 1) return null; try { // 调用 SDK此处以神思为例需引用其 .dll var result await Task.Run(() { var reader new IDCardReader(); var ret reader.ReadCard(); // 同步阻塞调用 return ret 0 ? reader.GetCardInfo() : null; }, ct); if (result ! null) { // 修复照片字节数组SDK 返回的 BMP 数据缺文件头 result.Photo FixBmpHeader(result.Photo); } return result; } finally { Interlocked.Exchange(ref _isReading, 0); } } private byte[] FixBmpHeader(byte[] raw) { if (raw null || raw.Length 100) return null; // 添加 BMP 文件头40字节和信息头108字节 var header new byte[148]; Buffer.BlockCopy(new byte[] { 66, 77 }, 0, header, 0, 2); // BM BitConverter.GetBytes(raw.Length 148).CopyTo(header, 2); // 文件大小 BitConverter.GetBytes(0).CopyTo(header, 10); // 保留字段 BitConverter.GetBytes(148).CopyTo(header, 14); // 像素数据偏移 // 后续填充信息头... var fixedData new byte[header.Length raw.Length]; Buffer.BlockCopy(header, 0, fixedData, 0, header.Length); Buffer.BlockCopy(raw, 0, fixedData, header.Length, raw.Length); return fixedData; } }参数说明Interlocked.CompareExchange实现原子性防重入FixBmpHeader是血泪经验——国产读卡器返回的图片数据是裸像素WinFormImage.FromStream()无法识别必须手动补 BMP 头CancellationToken用于在用户点击“取消读卡”时中断等待。3.2 号源分配算法基于乐观锁的实时扣减与冲突回滚挂号时需从ScheduleSource表中扣减一个号源。若直接UPDATE SET UsedQuota UsedQuota 1高并发下必然超卖。正确做法是public async Taskbool ReserveSlotAsync(int scheduleSourceId, CancellationToken ct default) { const int maxRetry 3; for (int i 0; i maxRetry; i) { try { var rows await _context.Database .ExecuteSqlRawAsync( UPDATE ScheduleSource SET UsedQuota UsedQuota 1 WHERE Id {0} AND UsedQuota TotalQuota, scheduleSourceId, ct); if (rows 1) return true; // 扣减成功 else if (i maxRetry - 1) await Task.Delay(50, ct); // 重试前小延迟 } catch (Exception ex) when (ex is SqlException or OperationCanceledException) { if (i maxRetry - 1) throw; // 最后一次重试失败才抛异常 await Task.Delay(100, ct); } } return false; // 所有重试均失败 }逻辑说明ExecuteSqlRawAsync绕过 EF Core 的变更跟踪直连 SQL Server 执行原子更新WHERE UsedQuota TotalQuota是关键确保不会超卖重试机制避免瞬时并发冲突导致用户看到“号源已满”误报。3.3 医保实时结算绕过 RestClient.Execute 异常的底层握手控制当调用国家医保平台 REST API 时RestClient.Execute()常抛出“无法将数据写入传输连接远程主机强迫关闭了一个现有的连接”这不是代码 bug而是医保服务器主动断连如 TLS 版本不匹配、Keep-Alive 超时、或服务端限流。硬编码try-catch无用必须从 HTTP 协议层干预。稳定医保客户端基于 HttpClientFactory// Program.cs 中注册 services.AddHttpClientInsuranceApiClient(client { client.BaseAddress new Uri(https://api.insurance.gov.cn/); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(application/json)); }) .ConfigurePrimaryHttpMessageHandler(() { var handler new HttpClientHandler { // 强制使用 TLS 1.2医保平台不支持 1.0/1.1 SslProtocols System.Security.Authentication.SslProtocols.Tls12, // 禁用代理医院内网通常直连 UseProxy false, // 连接池复用避免频繁建连 MaxConnectionsPerServer 10 }; return handler; }); // 业务层调用 public async TaskSettlementResult SubmitSettlementAsync(Registration reg, CancellationToken ct) { var request new HttpRequestMessage(HttpMethod.Post, v1/settlement); request.Content new StringContent(JsonSerializer.Serialize(reg), Encoding.UTF8, application/json); // 设置超时医保接口平均响应 1.2s设 5s 防止卡死 using var cts CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(5)); var response await _httpClient.SendAsync(request, cts.Token); response.EnsureSuccessStatusCode(); // 抛出 HttpStatus 4xx/5xx var json await response.Content.ReadAsStringAsync(ct); return JsonSerializer.DeserializeSettlementResult(json); }参数说明SslProtocols.Tls12是必设项否则握手失败CancelAfter(5s)比RestClient.Timeout更可靠因后者在 DNS 解析阶段不生效EnsureSuccessStatusCode()将 HTTP 错误转为HttpRequestException便于统一捕获处理。4. 避坑指南WinForm 医院系统开发中 5 个血泪教训这些坑不在教科书里但每个都曾让系统上线当天被护士长堵在机房门口质问。4.1 现象ListView 双击编辑后焦点丢失导致连续点击无效原因WinFormListView默认不支持就地编辑开发者常手动添加TextBox覆盖单元格。但TextBox.LostFocus事件触发时ListView未收到MouseUp导致后续点击不触发ItemSelectionChanged。解决不用TextBox覆盖改用ListView自带的LabelEdit模式并重写BeforeLabelEdit阻止非法字符listView1.LabelEdit true; listView1.BeforeLabelEdit (s, e) { if (e.Item ! 0) e.Cancel true; // 仅允许编辑第一列 }; listView1.AfterLabelEdit (s, e) { if (!string.IsNullOrEmpty(e.Label)) listView1.Items[e.Item].SubItems[0].Text e.Label; };4.2 现象部署到新电脑后DataGridView 显示中文乱码方块原因WinForm 默认使用System.Drawing.Font而医院老旧电脑Windows 7缺少Microsoft YaHei字体回退到MS Sans Serif该字体不支持中文。解决在Program.cs中强制设置全局默认字体Application.SetDefaultFont(new Font(微软雅黑, 9F, GraphicsUnit.Point)); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm());4.3 现象打印挂号凭条时部分打印机输出空白或错位原因PrintDocument使用 GDI 绘图但不同品牌打印机对Graphics.MeasureString计算精度差异极大导致换行位置偏移。解决放弃Graphics.DrawString改用PrintPageEventArgs.Graphics.DrawImage渲染预生成的 PDF 模板用 iTextSharp 生成private void printDocument_PrintPage(object sender, PrintPageEventArgs e) { var pdfBytes GenerateReceiptPdf(registration); // 生成PDF字节数组 var image PdfiumViewer.PdfRenderer.Render(pdfBytes, 300, true); e.Graphics.DrawImage(image, e.MarginBounds); }4.4 现象夜间数据库维护后系统启动时报“无法打开数据库”原因WinForm 应用启动时立即连接 SQL Server但医院 DBA 习惯在凌晨 2:00-3:00 做备份此时 SQL Server 服务重启连接字符串中的Connection Timeout30不足以覆盖重启窗口。解决实现连接重试策略且首次失败后弹窗提示而非崩溃private async Taskbool TestDatabaseConnectionAsync() { for (int i 0; i 5; i) { try { await _context.Database.CanConnectAsync(); return true; } catch { if (i 4) break; await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)), ct); // 指数退避 } } MessageBox.Show(数据库连接失败请检查SQL Server服务是否运行, 系统错误, MessageBoxButtons.OK, MessageBoxIcon.Error); return false; }4.5 现象Win10 22H2 更新后窗体阴影和圆角失效原因Win10 22H2 默认禁用DwmEnableComposition而 WinForm 的FormBorderStyle NoneRegion圆角依赖 DWM 合成。解决在MainForm构造函数中强制启用 DWM需 P/Invoke[DllImport(dwmapi.dll)] private static extern int DwmEnableComposition(bool enable); public MainForm() { InitializeComponent(); // Win10 22H2 兼容启用桌面窗口管理器 if (Environment.OSVersion.Version new Version(10, 0)) DwmEnableComposition(true); }5. 医保对接实战从国家医保平台 v3.0 接口到本地异常熔断国家医保平台NIPv3.0 接口是挂号系统最脆弱的环节——它不承诺 SLA不提供测试沙箱错误码文档缺失且每次升级都伴随字段名变更。与其被动适配不如构建本地熔断层。5.1 接口调用状态机定义 4 种医保状态及对应 UI 反馈状态触发条件UI 反馈后续动作Normal接口返回 200 successtrue绿色“医保结算成功”自动跳转叫号屏生成结算单更新Registration.Status2NetworkErrorHttpRequestException或超时黄色“医保连接异常”按钮变“重试”启动后台重试队列最多 3 次BusinessError返回 200 但successfalse如余额不足红色“医保结算失败{message}”弹窗输入自费金额允许切换为自费模式保存InsuranceSettlement.Status2PlatformDown连续 5 次NetworkError红色横幅“医保平台维护中”隐藏医保相关按钮自动降级为纯自费流程所有挂号走Status1状态机实现State Patternpublic abstract class InsuranceState { public abstract Task Handle(Registration reg, InsuranceContext context); } public class NormalState : InsuranceState { public override async Task Handle(Registration reg, InsuranceContext context) { var result await context.Api.SubmitAsync(reg); if (result.Success) { context.Ui.ShowSuccess(医保结算成功); context.UpdateStatus(reg.Id, 2); } else { context.TransitionTo(new BusinessErrorState(result.Message)); } } } public class PlatformDownState : InsuranceState { public override async Task Handle(Registration reg, InsuranceContext context) { context.Ui.ShowWarning(医保平台维护中已切换至自费模式); context.DisableInsuranceButtons(); // 后台每分钟探测一次恢复后自动切回 NormalState _ Task.Run(() PollPlatformRecovery(context)); } }5.2 本地异常熔断用内存缓存拦截高频失败请求当医保平台故障时若每个挂号请求都尝试调用接口会造成雪崩。需在应用层实现熔断public class InsuranceCircuitBreaker { private readonly MemoryCache _cache new MemoryCache(new MemoryCacheOptions()); private readonly TimeSpan _breakDuration TimeSpan.FromMinutes(5); public bool IsOpen _cache.TryGetValue(InsuranceBreaker, out bool isOpen) isOpen; public async TaskT ExecuteAsyncT(FuncTaskT operation, CancellationToken ct) { if (IsOpen) throw new InvalidOperationException(医保服务熔断中请稍后重试); try { var result await operation(); Reset(); // 成功则重置熔断器 return result; } catch (Exception ex) when (ex is HttpRequestException or TimeoutException) { Trip(); // 失败则触发熔断 throw; } } private void Trip() { _cache.Set(InsuranceBreaker, true, DateTimeOffset.Now.Add(_breakDuration)); } private void Reset() { _cache.Remove(InsuranceBreaker); } } // 使用示例 var result await _circuitBreaker.ExecuteAsync( () _insuranceApi.SubmitAsync(reg), ct);逻辑说明MemoryCache实现轻量级熔断无需引入 Polly 等重型库Trip()设置 5 分钟熔断窗口期间所有医保请求直接抛异常UI 层捕获后显示“平台维护中”Reset()在任意一次成功调用后清除熔断状态。5.3 日志与审计记录每一次医保交互的完整上下文医保问题排查依赖全链路日志。不能只记“结算失败”而要记录请求时间、终端 IP、操作员工号发送的 JSON 请求体脱敏身份证号后 4 位返回的原始 HTTP 响应含 headers 和 body本地生成的挂号单号与医保流水号映射关系。结构化日志模板SerilogLog.Information(医保结算请求 {Request} | 终端:{TerminalIP} | 操作员:{OperatorId}, new { RegistrationId reg.Id, PatientId reg.PatientId?.Substring(0, Math.Max(0, reg.PatientId.Length - 4)) ****, Amount reg.Amount, Timestamp DateTime.Now }, terminalIp, operatorId); Log.Error(ex, 医保结算失败 | 响应状态:{StatusCode} | 响应体:{ResponseBody}, response.StatusCode, response.Content.ReadAsStringAsync().Result.Substring(0, Math.Min(500, response.Content.ReadAsStringAsync().Result.Length)));参数说明Request表示结构化对象序列化避免日志中拼接字符串导致查询困难Substring(0,500)防止响应体过大撑爆日志文件TerminalIP和OperatorId从HttpContext或登录上下文获取确保审计可追溯。6. 界面细节打磨让护士长愿意多看你写的系统一眼WinForm 系统的成败往往藏在护士长手指划过屏幕的 0.3 秒体验里。她不关心你用了 MVVM 还是 MVP只在意点错一次能不能撤回看不清字要不要眯眼刷身份证后光标能不能自动跳到“确认”按钮6.1 防误操作三原则视觉反馈、操作确认、后悔药视觉反馈所有按钮点击后立即Enabled false文字变为“处理中…”防止重复提交。成功后恢复失败则Enabled true并弹窗提示。操作确认退号、删除排班等危险操作必须弹出MessageBox.Show(确定要退号吗此操作不可撤销, 确认, MessageBoxButtons.YesNo)且Yes按钮设为默认。后悔药挂号成功后主窗体右下角显示 5 秒倒计时浮层“刚挂的号点此撤销 →”点击后调用CancelRegistrationAsync()回滚号源。浮层实现无第三方库private void ShowUndoToast(string message, Action onUndo) { var toast new Label { Text message, BackColor Color.FromArgb(45, 117, 231), ForeColor Color.White, Font new Font(微软雅黑, 9F), AutoSize true, Padding new Padding(10, 5, 10, 5), Location new Point(Width - 200, Height - 80) }; var undoBtn new Button { Text 撤销, BackColor Color.White, ForeColor Color.FromArgb(45, 117, 231), Size new Size(50, 23), Location new Point(toast.Width - 50, 0) }; undoBtn.Click (s, e) { onUndo(); this.Controls.Remove(toast); }; toast.Controls.Add(undoBtn); Controls.Add(toast); // 5秒后自动消失 var timer new Timer { Interval 5000 }; timer.Tick (s, e) { Controls.Remove(toast); timer.Stop(); }; timer.Start(); }6.2 字体与 DPI 适配让 1024x768 老显示器也能看清医院大量使用 1024x768 分辨率的瘦客户机WinForm 默认缩放会糊字。解决方案项目属性 → 应用程序 → 目标框架设为.NET 6.0支持高 DPI 自动适配主窗体构造函数中添加public MainForm() { InitializeComponent(); // 启用 DPI 感知 SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE); // 禁用自动缩放由系统接管 AutoScaleMode AutoScaleMode.Dpi; }所有控件字体统一设为微软雅黑大小用Point而非Pixelthis.Font new Font(微软雅黑, 9F, GraphicsUnit.Point); label1.Font new Font(微软雅黑, 9F, GraphicsUnit.Point); button1.Font new Font(微软雅黑, 9F, GraphicsUnit.Point);参数说明PROCESS_PER_MONITOR_DPI_AWARE告诉 Windows 每个显示器独立缩放GraphicsUnit.Point确保字体大小在不同 DPI 下物理尺寸一致9F是实测最佳值——小于 8F 护士长看不清大于 10F 界面拥挤。6.3 打印凭条优化用固定宽度布局规避打印机差异挂号凭条必须在不同品牌打印机佳博、芯烨、斑马上保持一致。秘诀是不用PrintDocument动态计算位置而用预设像素坐标private void printDocument_PrintPage(object sender, PrintPageEventArgs e) { var g e.Graphics; var font new Font(微软雅黑, 10F); var leftMargin 20; var topMargin 20; var lineHeight 22; // 固定坐标打印单位像素 g.DrawString($挂号单号{reg.RegNumber}, font, Brushes.Black, leftMargin, topMargin); g.DrawString($患者姓名{reg.PatientName}, font, Brushes.Black, leftMargin, topMargin lineHeight); g.DrawString($就诊科室{reg.DepartmentName}, font, Brushes.Black, leftMargin, topMargin lineHeight * 2); g.DrawString($就诊医生{reg.DoctorName}, font, Brushes.Black, leftMargin, topMargin lineHeight * 3); g.DrawString($就诊时段{reg.TimeSlot}, font, Brushes.Black, leftMargin, topMargin lineHeight * 4); g.DrawString($请于{reg.ScheduleDate:MM月dd日} {reg.TimeSlot} 准时就诊, font, Brushes.Red, leftMargin, topMargin lineHeight * 6); }逻辑说明lineHeight 22是实测值10号字体在 300dpi 打印机上的行高所有坐标用绝对像素不依赖Graphics.MeasureString红色提示语单独一行确保护士长一眼看到关键信息。我干这行八年亲手交付过 17 个医院挂号系统其中 12 个还在用 WinForm。不是因为我不懂 WPF 或 MAUI而是见过太多项目花三个月重构为 WPF上线后发现护士长不会用触控笔画圈选医生又退回 WinForm 改了个触摸优化补丁。技术没有高低只有适不适合。WinForm 的价值从来不是语法糖或动画效果而是它能让一个 55 岁的护士在凌晨三点数据库崩溃后靠记事本改两行配置、重启服务继续给患者挂号。所以别急着淘汰它——先把它用到极致。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
双目视觉立体标定与校正:从棋盘格到深度图的完整指南 简介:这份资源围绕双目立体视觉的立体标定与立体校正展开,面向已掌握OpenCV基础、希望深入立体匹配与三维重建的开发者与学习者。内容基于VS2013与OpenCV3.0环境,对左右相机采集的棋盘格标定图像完成立体标定与校正,为后续视差计算… · 2026/9/25 23:47:52
阿里云K8s部署Vue2+SpringBoot2.5+Nacos2.0.3实战指南 简介:这份资源面向需要在阿里云Kubernetes集群上落地前后端分离项目的运维与后端开发人员,提供一套可直接参考的部署方案,解决Vue2前端、SpringBoot2.5服务与Nacos2.0.3注册配置中心在k8s中协同编排的问题。包内共16个文件,以8个y… · 2026/9/25 23:47:52
工频与射频电磁辐射测量与防护实战指南 简介:本资源是一份面向公众健康科普与工程防护实践的实用型技术文档,聚焦日常生活中普遍存在的电磁辐射问题,适用于电子电气从业者、环境健康关注者及高校相关专业师生。内容系统梳理了自然源、医疗设备、家用电器与通信基站等多类辐射源的生… · 2026/9/25 23:47:39
Nginx反向代理与性能优化实战:从配置到高可用 Nginx是一款高性能的HTTP和反向代理服务器,以高并发、低内存占用著称,目前占据Web服务器市场份额首位。在生产环境中,Nginx通常作为入口网关,承担反向代理、负载均衡、静态资源服务、SSL终结、限流防刷等职责。本文从基础配置入手… · 2026/9/26 2:51:47
CoreTemp 深度解析:CPU 温度监控原理、配置与实战避坑指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 2:51:41
CIS-CAT:等保测评中的配置审计与合规基线扫描实战指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 2:51:41
Redis缓存设计与常见问题实战:穿透、击穿、雪崩与解决方案 Redis是目前最流行的内存数据库,广泛应用于缓存、会话存储、排行榜、分布式锁等场景。在缓存架构中,缓存穿透、缓存击穿、缓存雪崩是三大经典问题,如果处理不当,可能导致数据库被瞬间打挂。本文系统讲解这三大问题的产生原因和解决… · 2026/9/26 2:51:41
treg upload命令完全指南:从.env一键导入API密钥到注册工具 treg upload命令完全指南:从.env一键导入API密钥到注册工具 【免费下载链接】treg OpenRouter for agent tools. Join community here: https://discord.gg/6mQYYfFMAn 项目地址: https://gitcode.com/GitHub_Trending/treg/treg
treg 是面向 AI agent 工具的… · 2026/9/26 2:51:41
OpenCode模型调用报错?Zen/OpenRouter/Ollama三大免费方案配置指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 2:51:41
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46