简介:面向医疗信息化开发者与C#/.NET技术学习者的全套PACS源码,围绕医学影像存档与通信系统的核心流程展开,可作为理解并搭建同类医疗影像管理系统的参考工程。压缩包约399.87MB,文件数量与类型明细上游暂未提供,下载后需按实际目录核对;源码结合.NET控件构建图像查看器、菜单与按钮等界面,并依赖SQL数据库存储患者信息、检查记录、影像序列及元数据。内容覆盖图像采集设备、DICOM服务器、数据库服务器、工作站与网络连接等典型架构环节,配套部署说明涉及C#编译环境、SQL服务器设置、数据库脚本导入、DICOM服务配置与应用启动,使用说明则指向患者添加、图像上传、报告查看等操作。已有561人学习,适合需要参考完整业务分层、DICOM数据处理与数据库设计的开发者查漏补缺。
1. 拿到一套 C# 写的 PACS 源码,先别急着编译
很多人看到“全套 PACS 源码,C# 编写,使用 .NET 控件”的第一反应是双击 .sln,等 VS 把依赖拉完,再按 F5。结果十有八九卡在缺 DLL、缺数据库脚本、缺图像库授权上——PACS 不是单机小工具,它是一套带 DICOM 通信、影像存储、阅片工作站、报告打印的临床信息系统,任何一环的版本对不上,都会让整个解决方案编译不过。这份资源的真实价值,在于给了一份完整的、用 C# 加 .NET 控件搭起来的 PACS 功能骨架,能让你看清“登记—采集—存储—阅片—报告”这条链路在代码里到底怎么串起来,适合想入门医疗影像软件、或者要把 DICOM 能力嵌进自己系统的从业者。它不是拿来即用的成品,是一份需要你动手补齐环境和参数的工程底子。
2. PACS 的模块分层与 .NET 控件选型:先看懂它怎么搭的
2.1 从业务链路反推代码分层
一套 PACS 在业务上跑的是这样一条线:登记台建患者与检查信息 → 设备端通过 DICOM C-STORE 把影像推过来 → 服务端按 Study、Series、Instance 三级归档 → 阅片端按查询条件 C-FIND 拉取、C-MOVE 取图 → 医生写报告、排版、打印。源码目录结构基本是照这条线切的,常见做法是拆成五块:公共实体与工具层、DICOM 通信层、业务服务层、UI 客户端层、基础设施层(配置、日志、权限)。你打开解决方案后,别按项目名字母顺序看,按这条链路找对应的工程,理解速度会快很多。
判断一份 PACS 源码是不是“真全套”,有个很实用的检查点:看它有没有独立的数据访问层,而不是把 SQL 拼在窗体代码背后。医疗系统的查询条件动辄十几个字段组合(患者 ID、姓名拼音、检查号、设备、时间段、模态),SQL 散落在 UI 层会立刻失控。分层清晰的工程,DAL 里一定有成套的参数化查询方法,服务层只负责业务规则,UI 层只管绑定。这一点也是你后续改代码时最容易踩坑的地方:想加一个查询条件,得先弄清楚它该落在哪一层,硬塞到窗体里能跑通,但下次改需求就要重来。
还有一点值得留意,PACS 的实体模型是有标准的,DICOM 定义了 Patient、Study、Series、Image 的层级关系,HL7 定义了患者与医嘱消息。源码里的实体类如果能和这套层级一一对应(比如 Study 下挂 Series 集合,Series 下挂 Image 集合),说明作者是按行业惯例设计的;如果是一张扁平的大表顶到底,那你后面做归档和检索会非常痛苦,得先重构数据模型。
2.2 .NET 控件在影像界面里的实际分工
标题里特意点了“使用 .NET 控件”,这不是废话。医疗影像界面的交互复杂度远高于普通业务表单,至少要解决三类问题:影像的缩放、平移、窗宽窗位调节、测量标注;多序列的缩略图管理与联动;报告编辑区的富文本排版与模板套用。对应到 WinForms 或 WPF 的技术选型上,常见组合是这样的:
| 界面区域 | 常见控件方案 | 选它的理由 | 要注意的边界 |
|---|---|---|---|
| 影像显示画布 | 自绘 Panel / WPF Image + WriteableBitmap | 可控性强,能自己实现窗宽窗位映射 | 大数据量要自己做双缓冲,否则拖动会闪 |
| 序列缩略图 | ListView / 自定义缩略图列表 | 绑定简单,支持选中联动 | 序列多时要做虚拟化,别一次性全渲染 |
| 测量标注层 | 透明覆盖层 + 图形对象集合 | 标注与像素分离,重绘代价小 | 坐标要按原始像素存,不能存屏幕坐标 |
| 报告编辑器 | RichTextBox / 富文本编辑器控件 | 支持模板、字体、表格排版 | 打印排版和屏幕显示常有偏差,要单独调 |
| 患者检查列表 | DataGridView / 数据表格控件 | 分页、排序、列自定义成熟 | 大结果集必须服务端分页 |
选型上有个血泪经验:影像画布千万不要用现成的图片查看控件硬套。那些控件是为普通图片设计的,缩放是插值缩放,窗宽窗位没法介入,测量标注也没地方挂。自己用 Panel 自绘或者用 WriteableBitmap 直接操作像素,前期多写两百行代码,后期省掉无数次返工。
2.3 环境准备:把编译链先跑通
在动业务代码之前,先把环境对齐。PACS 源码常见的依赖有三类:DICOM 库(自己做协议解析或者引第三方库)、数据库访问组件、报表或打印组件。第一步不是编译,是清点引用。
# 在源码根目录快速盘点解决方案与项目结构 find . -name "*.sln" -maxdepth 3 find . -name "*.csproj" | head -50 # 找出所有引用了但仓库里没有的 DLL(编译前先心里有数) grep -rhoE 'HintPath>[^<]+\.dll' --include="*.csproj" . | sort -u上面三条命令分别做三件事:定位解决方案文件,确认工程数量与命名,把所有 csproj 里以本地路径引用的 DLL 列出来。第三条最关键,很多 PACS 源码包里的第三方 DLL 是作者本机路径,换台机器就找不到。列出来之后逐个确认仓库里有没有对应文件,缺的记在本子上,别等编译报几十个错再一个个找。
# 查看目标框架,决定装哪个版本的 .NET SDK / 运行时 grep -rh "TargetFramework" --include="*.csproj" . | sort | uniq -c这条命令统计所有工程的目标框架。如果混着 .NET Framework 4.x 和老版本,就要在 Windows 上装对应版本的开发包;如果是 .NET 6/8,跨平台会舒服很多,但要注意 WinForms 只支持 Windows。统计结果里出现多个版本时,优先统一到高版本,再逐个处理不兼容的 API,别一开始就多版本并存,那会把依赖问题放大好几倍。
2.4 数据库初始化:脚本顺序不能乱
PACS 的库表不少,患者、检查、序列、影像、报告、用户、权限、字典表,还有日志表。初始化脚本通常按“建库 → 建表 → 建索引 → 建外键 → 插字典数据”的顺序执行,顺序错了外键会失败。
-- 典型 PACS 核心表结构(简化示意,字段按实际源码为准) CREATE TABLE Patient ( PatientId VARCHAR(64) PRIMARY KEY, PatientName NVARCHAR(128) NOT NULL, NamePinyin VARCHAR(64), -- 拼音检索用,检索性能关键 Gender CHAR(1), BirthDate DATE, CreateTime DATETIME DEFAULT GETDATE() ); CREATE TABLE Study ( StudyUid VARCHAR(128) PRIMARY KEY, -- DICOM StudyInstanceUID PatientId VARCHAR(64) NOT NULL, StudyDate DATE, Modality VARCHAR(16), -- CT / MR / CR 等 AccessionNo VARCHAR(64), -- 检查号,与登记系统对齐 StudyDesc NVARCHAR(256), CONSTRAINT FK_Study_Patient FOREIGN KEY (PatientId) REFERENCES Patient(PatientId) ); CREATE INDEX IX_Study_PatientDate ON Study(PatientId, StudyDate); CREATE INDEX IX_Study_Accession ON Study(AccessionNo);这里有两个参数值得单独说。NamePinyin字段是为了支持医生用拼音首字母查患者,没这个字段,中文姓名模糊查询在数据量大时基本不可用。StudyUid直接用 DICOM 的 StudyInstanceUID 做主键,好处是和外部设备、外部系统交换数据时不用再做映射,坏处是字符串长、索引占空间,但对医疗系统这点开销可以接受。索引建在(PatientId, StudyDate)上是因为最高频的查询就是“某患者某时间段的检查”,建在 AccessionNo 上是为了对接登记系统的检查号回查。
执行完建表脚本,务必先插字典数据(性别、模态、检查部位、报告模板分类),再跑业务测试。字典表空了,界面上所有下拉框都是空的,你会误以为是代码问题。
3. DICOM 通信与影像归档:真正让 PACS 跑起来的核心
3.1 DICOM 服务的四个基本动作
PACS 之间、PACS 与设备之间的通信靠 DICOM 协议,最常用的四个服务是:C-ECHO 验证连通、C-STORE 接收/发送影像、C-FIND 查询、C-MOVE 取回。源码里如果实现了 DICOM 服务端,通常会有一个监听端口(常见 104 或 11112),配置里能看到 AE Title(应用实体名)和端口号两项。
// DICOM 服务端启动的典型流程(按实际源码的库 API 调整) var server = new DicomServer<DicomCEchoProvider>(104); // AE Title 与存储路径从配置文件读,不要硬编码 string localAeTitle = ConfigurationManager.AppSettings["LocalAETitle"]; string storageRoot = ConfigurationManager.AppSettings["StorageRoot"]; // 收到 C-STORE 后按 Study/Series/Instance 三级目录落盘 // 目录结构建议:{root}/{StudyUid}/{SeriesUid}/{SopInstanceUid}.dcm这段代码的要点不在 API 本身,而在三个配置项。AE Title 必须和设备端、对方 PACS 配置一致,大小写敏感,多一个空格就通不了,而且报错往往只是“连接被拒绝”,这就是典型的黑匣子场景。端口要确认没有被本机防火墙拦。存储路径必须落在有足够空间、且有备份策略的磁盘上,影像文件一件几 MB 到几十 MB,一个月的量很容易上 TB。
3.2 收图落盘:目录规则与文件命名
收到影像后怎么存,是 PACS 设计里最影响后期维护的决策。按 StudyInstanceUID 作一级目录、SeriesInstanceUID 作二级、SOPInstanceUID 作文件名,是行业默认做法,好处是天然去重(同一张图重复推送不会产生两份文件),且目录结构与 DICOM 层级一致,排查时按 UID 一路找下去就行。
// 落盘路径拼装:用 UID 生成层级目录,避免同名覆盖 string BuildStoragePath(string root, DicomDataset ds) { var studyUid = ds.GetString(DicomTag.StudyInstanceUID); var seriesUid = ds.GetString(DicomTag.SeriesInstanceUID); var sopUid = ds.GetString(DicomTag.SOPInstanceUID); // UID 里可能含点号,做一次清洗防止路径异常 string Safe(string uid) => uid.Replace(".", "_").Replace("/", "_"); string dir = Path.Combine(root, Safe(studyUid), Safe(seriesUid)); Directory.CreateDirectory(dir); // 幂等,已存在不报错 return Path.Combine(dir, Safe(sopUid) + ".dcm"); }这里有个容易翻车的细节:UID 是含点号的数字串,直接拼路径在部分环境会因为点号引起解析歧义,清洗成下划线更稳。另一个是CreateDirectory本身幂等,不需要先判断存在,写了判断反而多一次 IO。还有人喜欢用“患者姓名+检查日期”做目录名,看起来直观,但患者改名、重名、特殊字符都会出问题,强烈不建议。
落盘之后还有一步常被忽略:写数据库索引。文件落盘和数据库记录必须保持一致性,常见做法是先落盘再写库,写库失败时留一条待清理记录,由后台任务补偿;反过来先写库再落盘,一旦落盘失败,数据库里就会有一堆指向空文件的记录,阅片端点开就报错。
3.3 阅片端的取图链路:C-FIND 加 C-MOVE
阅片端不是直接读数据库拿图的,标准链路是先 C-FIND 查检查列表,选中后发 C-MOVE 请求,由 PACS 服务端把影像 C-STORE 推给阅片端指定的接收端。这个链路对新手很绕:为什么不能直接读文件?因为标准要求如此,而且跨机构会诊时对方只认这套。
// 阅片端查询检查列表的核心参数(伪代码,按源码库 API 对应) var query = DicomCFindRequest.CreateStudyQuery( patientId: txtPatientId.Text.Trim(), studyDate: dtpStudyDate.Value.ToString("yyyyMMdd"), modality: cmbModality.SelectedValue?.ToString() ); // 查询级别必须是 STUDY,返回的是检查级摘要,不含像素 query.OnResponseReceived += (req, resp) => BindStudyRow(resp.Dataset); await client.SendAsync(query);参数上有两个硬约束。第一,查询级别(QueryRetrieveLevel)决定了返回什么,查检查列表用 STUDY,查序列用 SERIES,查图像用 IMAGE,级别写错要么查不到,要么返回量巨大。第二,日期格式必须是 DICOM 规定的yyyyMMdd,中间带横杠的日期一定匹配不上,而且很多实现不报错,只是返回空结果——这个坑我见过不止一次。C-MOVE 的目标 AE Title 也要配对,推错地方影像就丢了,日志里只留一行成功记录。
3.4 影像显示:窗宽窗位是绕不开的一关
PACS 阅片最核心的显示逻辑就是窗宽窗位(Window Width / Window Center)映射。CT 影像的像素值范围是几千的灰度值,直接映射到 0-255 显示,软组织全糊成一片。必须按检查部位选窗,肺窗、骨窗、脑窗各有一套默认参数,医生还能手动滚轮调。
// 灰度映射:把原始像素值按窗宽窗位压到 0-255 byte MapToDisplay(int pixel, int windowCenter, int windowWidth) { double low = windowCenter - windowWidth / 2.0; double high = windowCenter + windowWidth / 2.0; if (pixel <= low) return 0; if (pixel >= high) return 255; return (byte)((pixel - low) / (high - low) * 255); }这段逻辑很短,但参数来源有两个:DICOM 文件头里的 WindowCenter / WindowWidth 标签(0028,1050 和 0028,1051),以及按检查部位预设的默认值。优先读文件头,没有再用预设。还要注意像素值的斜率截距(Rescale Slope / Intercept),原始存储值要先做线性变换才是真实的 CT 值(HU),不做这一步窗宽窗位调出来是错的。此外,映射表建议预先算好 65536 项的查找表缓存起来,每个像素现算除法在整幅图上会明显卡顿。
4. 报告、打印与权限:决定能不能落地的外围功能
4.1 报告模板与结构化字段
医生写报告很少从零敲,都是套模板改。源码里的报告模块通常包含模板管理(分类、内容、占位符)和报告正文两部分。模板里用占位符替换患者信息、检查信息是常规做法,比如{PatientName}、{StudyDate}、{Findings}。
// 报告模板占位符替换,注意只替换已知键,未知键原样保留便于排查 string RenderTemplate(string template, IDictionary<string, string> values) { return Regex.Replace(template, @"\{(\w+)\}", m => { var key = m.Groups[1].Value; return values.TryGetValue(key, out var v) ? v : m.Value; }); }参数上要注意两点:正则只匹配字母数字下划线,避免把正文里的花括号误替换;未知占位符保留原样而不是替换成空串,这样模板写错了医生能一眼看出来,而不是报告里莫名少一段。结构化报告更进一步,把“部位、所见、印象”拆成字段存,便于统计和质控,但那属于二期功能,先把纯文本模板跑通更实际。
4.2 打印排版:屏幕好看不等于纸上好看
报告打印是典型的两套渲染。屏幕上用 RichTextBox 显示,打印时要么用 PrintDocument 自绘,要么导出成固定格式再打印。常见翻车点是页边距、字体替换、分页位置。医院常用 A4 纸、宋体小四,行距固定值,页眉放机构名和患者信息,页脚放页码和医师签名位。
具体做法上,我一般会把打印参数单独抽成配置文件:纸张大小、上下左右边距、正文字体与字号、行距、是否打印页眉页脚。这样一来不同科室要不同版式,改配置就行,不用改代码重新发版。另一个细节是打印预览必须和实际打印用同一套排版逻辑,很多实现预览用一套、打印用另一套,医生预览觉得没问题,打出来错位,这是纯纯的自找麻烦。
4.3 权限模型:按角色而不是按人
PACS 涉及患者隐私,权限必须做细。常见角色划分:登记员(建患者、建检查)、技师(采集、质控)、医生(阅片、写报告)、审核医生(审核签发)、管理员(用户与配置)。权限校验要落在服务层,不能只靠界面把按钮置灰——界面隐藏只是体验,服务层校验才是安全。
// 服务层权限校验:动作 + 资源双重判断 public void SignReport(int reportId, int userId) { var user = _userRepo.Get(userId); if (!_authService.HasPermission(user.RoleCode, "REPORT_SIGN")) throw new PermissionDeniedException("无报告签发权限"); var report = _reportRepo.Get(reportId); if (report.AuthorId == userId) throw new PermissionDeniedException("不能签发自己撰写的报告"); _reportRepo.UpdateStatus(reportId, ReportStatus.Signed, userId); }这段的关键是第二条校验:报告签发通常要求“写”和“审”分离,作者不能自己签自己。这个规则不是所有源码都实现了,但真实科室一定会提。加这条的时候要注意,得先确认业务上是否允许单人科室例外,别一刀切把流程堵死。
5. 避坑清单:这套源码最容易翻车的五个地方
5.1 编译通过但运行报缺 DLL
现象:解决方案能编译,一启动就弹“无法加载文件或程序集”。原因多半是 csproj 里引用的本地 DLL 路径指向作者机器,或者依赖了特定版本的运行库,而本机装的是另一个版本。解决:用第 2 章那条 grep 命令把所有 HintPath 列出来,逐个确认文件存在,缺失的补进仓库统一放 lib 目录,然后把引用路径改成相对路径。版本冲突时优先统一到同一版本,不要靠 bindingRedirect 硬压,那只是把问题推迟到运行时。
5.2 DICOM 连通性测试一直失败
现象:C-ECHO 发出去没响应,或者提示关联被拒绝。原因按概率排:AE Title 不一致(含大小写和空格)、端口被防火墙拦、对方只接受特定传输语法(Transfer Syntax)、本机 IP 绑定错网卡。解决:先用工具单独测 C-ECHO,别在业务代码里调;确认双方 AE Title 逐字符比对;关掉防火墙试一次,通了再回来加规则;传输语法先用最通用的隐式小端,验证通了再开压缩语法。
5.3 影像能收但阅片端打不开
现象:文件确实落盘了,数据库也有记录,阅片端点开报错或黑屏。原因通常是三选一:像素数据解析时没处理传输语法,压缩格式(JPEG、JPEG2000)没装解码器;没做 Rescale Slope/Intercept 变换导致窗宽窗位错乱;多帧影像只读了第一帧。解决:先用 DICOM 查看工具打开落盘文件确认文件本身完整,再看代码里像素读取分支是否覆盖了实际传输语法,压缩影像要么装解码库,要么在接收端转成未压缩存储。
5.4 查询列表越来越慢
现象:刚上线很快,数据积累到几十万条后查询要等十几秒。原因基本是分页没做或者做了假分页——把全表拉到内存再分页,数据量一大必然崩。另外拼音检索没走索引、模糊查询用前置通配符(LIKE '%xx%')也会让索引失效。解决:服务端真分页,查询条件按选择性排序,拼音列单独建索引,模糊查询尽量改成前缀匹配,必要时上全文检索。
5.5 报告打印错位、丢页
现象:预览正常,实际打印内容溢出、分页断开、末尾丢内容。原因多是打印逻辑自己算分页但没算页眉页脚占位,或者字体在打印环境被替换导致行高变化。解决:打印和预览共用同一套分页计算,把页眉页脚高度从可用高度里先扣掉;字体显式指定并确认打印环境已安装;打印前做一次实际纸张尺寸的换算,不要写死像素。
6. 二次开发前的验证顺序:我每次都按这个流程走一遍
拿到这类源码,我从来不会一上来就改业务,而是按一条固定顺序做验证,走完再动手,能省掉大量返工。
第一步是编译与启动验证。先确认所有工程能编译,主程序能起来,登录能进。这一步只关心“能不能跑”,不关心功能对不对。起不来就先解决依赖和配置,别往下走。
第二步是数据库连通与初始化验证。用最小数据集跑一遍:建一个患者、一个检查、一条报告,确认增删改查正常,外键约束没有互相打架。这一步能暴露数据模型设计上的硬伤。
第三步是 DICOM 闭环验证。这是 PACS 的命门,必须打通“发图—收图—落盘—入库—查询—取图—显示”整条链路。我一般会在本机搭一个模拟发送端,用测试影像推几十张过去,然后从阅片端完整走一遍。链路任何一环断了,先别看别的功能。
第四步是显示正确性验证。挑几组有代表性的影像:CT 带窗宽窗位标签的、MR 多序列的、CR 单帧的、带压缩传输语法的。逐组确认显示正常、窗宽窗位可调、测量标注坐标准确。这里有个容易被忽略的验证项:把标注存下来再重新打开,看位置是否还在原处,很多实现存的是屏幕坐标,换个缩放比例就飘了。
第五步才是外围功能抽检。报告模板替换、打印预览与实打、权限校验、日志记录,各抽一两条主路径跑通即可,不必全量测。
下面这张表是我自己用的验证清单,可以直接抄:
| 验证环节 | 通过标准 | 失败先查什么 |
|---|---|---|
| 编译启动 | 主程序可登录进入主界面 | 本地 DLL 引用、目标框架版本 |
| 数据初始化 | 患者/检查/报告增删改查正常 | 建表脚本顺序、外键、字典数据 |
| DICOM 收发 | 测试影像可收、可查、可取、可显示 | AE Title、端口、传输语法 |
| 影像显示 | 窗宽窗位正确、标注位置可复原 | Rescale 变换、坐标存储方式 |
| 报告打印 | 预览与实打一致、无丢页 | 分页计算、字体、页边距配置 |
这张表的用法不是打勾就完事,而是每一条失败时都知道往哪个方向查。经验上,卡时间最长的永远是 DICOM 那一条,其次是显示,因为这两块涉及外部协议和二进制数据,出错信息最不友好。
还有一个技巧值得说:先把日志级别调到 Debug,把每一次 DICOM 关联、每一条 SQL、每一次落盘都记下来,等链路跑通再调回 Info。PACS 的问题排查高度依赖日志,没有日志就只能靠猜,而猜在协议层几乎没有胜算。我当时第一次调 C-MOVE,就是靠日志里那行目标 AE Title 才发现配置里多了一个尾随空格,肉眼看了半个小时没看出来。
从那以后我每次接手这类源码,都强制先走一遍上面这五步验证,再谈改需求。顺序乱了,后面每一步都在沙子上盖楼。希望帮到你。
本文还有配套的精品资源,点击获取