找回密码
 立即注册
首页 业界区 业界 C# WinForms 实现打印监听组件

C# WinForms 实现打印监听组件

萧海芷 2025-6-14 16:15:51
一、组件简介

打印监听组件是一款集成于 Windows 桌面环境的打印任务管理与监控工具,适用于企业级应用场景。它不仅支持多打印机任务的实时监控,还能通过 WebSocket 与外部系统集成,实现自动化打印、任务状态反馈、远程控制等功能。
二、界面功能介绍

1. 主界面与托盘集成


  • 主窗体:采用 WinForms 界面,包含多标签页(TabControl),每个标签页对应一台本地打印机,便于分组管理。
    1.png

  • 托盘图标:程序最小化后驻留于系统托盘,双击可快速还原主界面,支持右键菜单操作(如退出、重启、服务设置等)。
    2.png

2. 打印机管理


  • 打印机列表:自动检测本地所有已安装打印机,支持设置默认打印机、查看打印机属性。
  1. /// <summary>
  2. /// 绑定本地打印机列表到菜单
  3. /// </summary>
  4. internal void BindPrintersToMenu()
  5. {
  6.     默认打印机ToolStripMenuItem.DropDownItems.Clear();
  7.     // 获取当前系统默认打印机
  8.     string defaultPrinter = new System.Drawing.Printing.PrinterSettings().PrinterName;
  9.     // 先添加默认打印机(始终第一行)
  10.     var defaultItem = new ToolStripMenuItem(defaultPrinter)
  11.     {
  12.         Checked = true
  13.     };
  14.     defaultItem.Click += (s, e) => SetDefaultPrinterUI(defaultPrinter);
  15.     // 添加“首选项”子菜单
  16.     var prefItem = new ToolStripMenuItem("首选项");
  17.     prefItem.Click += (s, e) => ShowPrinterProperties(defaultPrinter);
  18.     defaultItem.DropDownItems.Add(prefItem);
  19.     默认打印机ToolStripMenuItem.DropDownItems.Add(defaultItem);
  20.     // 再添加其他打印机(排除默认打印机)
  21.     foreach (string printer in System.Drawing.Printing.PrinterSettings.InstalledPrinters)
  22.     {
  23.         if (printer == defaultPrinter)
  24.             continue;
  25.         var item = new ToolStripMenuItem(printer)
  26.         {
  27.             Checked = false
  28.         };
  29.         item.Click += (s, e) => SetDefaultPrinterUI(printer);
  30.         var prefItem2 = new ToolStripMenuItem("首选项");
  31.         prefItem2.Click += (s, e) => ShowPrinterProperties(printer);
  32.         item.DropDownItems.Add(prefItem2);
  33.         默认打印机ToolStripMenuItem.DropDownItems.Add(item);
  34.     }
  35. }
  36. /// <summary>  
  37. /// UI和系统都设置默认打印机
  38. /// </summary>  
  39. /// <param name="printerName"></param>  
  40. private void SetDefaultPrinterUI(string printerName)
  41. {
  42.      foreach (ToolStripMenuItem item in 默认打印机ToolStripMenuItem.DropDownItems)
  43.          item.Checked = item.Text == printerName;
  44.      // 如需设置为系统默认打印机,可调用 Win32 API(可选)  
  45.      SetSystemDefaultPrinter(printerName);
  46. }
  47. /// <summary>
  48. /// 显示打印机首选项对话框
  49. /// </summary>
  50. /// <param name="printerName"></param>
  51. private void ShowPrinterProperties(string printerName)
  52. {
  53.     // 使用rundll32调用打印机属性对话框
  54.     //string args = $"printui.dll,PrintUIEntry /p /n "{printerName}"";
  55.     //•        /e 参数表示直接打开“首选项”对话框
  56.     string args = $"printui.dll,PrintUIEntry /e /n "{printerName}"";
  57.     var psi = new System.Diagnostics.ProcessStartInfo
  58.     {
  59.         FileName = "rundll32.exe",
  60.         Arguments = args,
  61.         UseShellExecute = false,
  62.         CreateNoWindow = true
  63.     };
  64.     try
  65.     {
  66.         System.Diagnostics.Process.Start(psi);
  67.     }
  68.     catch (Exception ex)
  69.     {
  70.         MessageBox.Show("无法打开打印机首选项窗口:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  71.     }
  72. }
复制代码

  • TabControl:每台打印机一个标签页,便于查看和管理各自的打印任务。
  1. /// <summary>
  2. /// 绑定本地打印机列表到TabControl
  3. /// </summary>
  4. private void BindPrintersToTabControl()
  5. {
  6.      tabControl1.TabPages.Clear();
  7.      string defaultPrinter = new System.Drawing.Printing.PrinterSettings().PrinterName;
  8.      List<string> printers = new List<string>();
  9.      // 先将默认打印机添加到列表首位
  10.      printers.Add(defaultPrinter);
  11.      // 再添加其他打印机(排除默认打印机)
  12.      foreach (string printer in System.Drawing.Printing.PrinterSettings.InstalledPrinters)
  13.      {
  14.          if (printer != defaultPrinter)
  15.              printers.Add(printer);
  16.      }
  17.      foreach (string printer in printers)
  18.      {
  19.          var tabPage = new TabPage(printer);
  20.          // 创建DataGridView
  21.          var dgv = new DataGridView
  22.          {
  23.              Dock = DockStyle.Fill,
  24.              ReadOnly = true,
  25.              AllowUserToAddRows = false,
  26.              AllowUserToDeleteRows = false,
  27.              RowHeadersVisible = false,
  28.              AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill
  29.          };
  30.          // 添加列
  31.          dgv.Columns.Add("clientIp", "来源");
  32.          dgv.Columns.Add("taskId", "任务ID");
  33.          dgv.Columns.Add("taskName", "任务名称");
  34.          dgv.Columns.Add("realName", "模板");
  35.          dgv.Columns.Add("requestTime", "开始时间");
  36.          dgv.Columns.Add("status", "任务状态");
  37.          //绑定菜单
  38.          dgv.ContextMenuStrip = dgvContextMenu;
  39.          dgv.MouseDown += Dgv_MouseDown;
  40.          // 创建TextBox
  41.          var txtSearch = new TextBox
  42.          {
  43.              PlaceholderText = "任务ID",
  44.              Width = 120,
  45.              Anchor = AnchorStyles.Left | AnchorStyles.Bottom
  46.          };
  47.          // 创建Button
  48.          var btnSearch = new Button
  49.          {
  50.              Text = "查找",
  51.              Width = 60,
  52.              Anchor = AnchorStyles.Left | AnchorStyles.Bottom
  53.          };
  54.          // 查找事件
  55.          btnSearch.Click += (s, e) =>
  56.          {
  57.              string searchId = txtSearch.Text.Trim();
  58.              bool found = false;
  59.              foreach (DataGridViewRow row in dgv.Rows)
  60.              {
  61.                  if (row.IsNewRow) continue;
  62.                  if (row.Cells["taskId"].Value?.ToString() == searchId)
  63.                  {
  64.                      row.Selected = true;
  65.                      dgv.CurrentCell = row.Cells["taskId"];
  66.                      found = true;
  67.                  }
  68.                  else
  69.                  {
  70.                      row.Selected = false;
  71.                  }
  72.              }
  73.              if (!found)
  74.              {
  75.                  MessageBox.Show("未找到对应任务ID!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  76.              }
  77.          };
  78.          // 使用Panel布局
  79.          var panel = new Panel
  80.          {
  81.              Dock = DockStyle.Bottom,
  82.              Height = 40
  83.          };
  84.          txtSearch.Location = new Point(10, 8);
  85.          btnSearch.Location = new Point(140, 6);
  86.          panel.Controls.Add(txtSearch);
  87.          panel.Controls.Add(btnSearch);
  88.          tabPage.Controls.Add(panel);
  89.          tabPage.Controls.Add(dgv);
  90.          tabControl1.TabPages.Add(tabPage);
  91.      }
  92. }
复制代码
3. 打印任务监控


  • 任务列表:每个打印机标签页下方为 DataGridView,实时显示当前打印任务,包括来源、任务ID、任务名称、模板、开始时间、任务状态等信息。
    3.png

  • 右键菜单:支持对单个任务进行“取消打印”、“重新打印”、“删除记录”等操作。
    4.png

  • 任务搜索:支持按任务ID快速定位任务。
4. 其他功能


  • 服务控制:可一键启动/停止 WebSocket 服务,支持与外部系统通信。
  • 模板设计与预览:集成 FastReport 设计器和预览器,方便模板维护。
因为FastReport.Net 是需要购买授权的,所以我使用的是FastReport.OpenSource(开源版),开源版功能太少,不能直接从程序内部调用FastReport设计器和预览器,只能通过启动本地安装的.exe来实现。
  1. /// <summary>
  2. /// 设计菜单项点击事件,启动 FastReport 设计器
  3. /// </summary>
  4. /// <param name="sender"></param>
  5. /// <param name="e"></param>
  6. private void 设计ToolStripMenuItem_Click(object sender, EventArgs e)
  7. {
  8.     string designerPath = GetConfigValue("designer_path");
  9.     string templatePath = GetTemplatePathFromConfig();
  10.     if (string.IsNullOrEmpty(designerPath) || !System.IO.File.Exists(designerPath))
  11.     {
  12.         MessageBox.Show("未找到 FastReport 设计器,请检查 config.ini 配置!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  13.         return;
  14.     }
  15.     if (!System.IO.File.Exists(templatePath))
  16.     {
  17.         MessageBox.Show("未找到模板文件,请检查路径!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  18.         return;
  19.     }
  20.     try
  21.     {
  22.         System.Diagnostics.Process.Start(designerPath, $""{templatePath}"");
  23.     }
  24.     catch (Exception ex)
  25.     {
  26.         MessageBox.Show("启动 FastReport 设计器失败:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  27.     }
  28. }
  29. private void 预览ToolStripMenuItem_Click(object sender, EventArgs e)
  30. {
  31.     string viewerPath = GetConfigValue("viewer_path");
  32.     string templatePath = GetTemplatePathFromConfig();
  33.     if (string.IsNullOrEmpty(viewerPath) || !System.IO.File.Exists(viewerPath))
  34.     {
  35.         MessageBox.Show("未找到 FastReport 预览器,请检查 config.ini 配置!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  36.         return;
  37.     }
  38.     if (!System.IO.File.Exists(templatePath))
  39.     {
  40.         MessageBox.Show("未找到模板文件,请检查路径!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  41.         return;
  42.     }
  43.     try
  44.     {
  45.         System.Diagnostics.Process.Start(viewerPath, $""{templatePath}"");
  46.     }
  47.     catch (Exception ex)
  48.     {
  49.         MessageBox.Show("启动 FastReport 预览器失败:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  50.     }
  51. }
复制代码

  • 自动更新:支持在线检查和自动更新程序版本。
这里使用的是:Autoupdater.NET


  • 帮助与支持:内置开发者联系方式,便于用户反馈和技术支持。
    5.png

三、技术要点

1. 打印任务监听与管理


  • WMI 打印作业监控
    通过 System.Management 命名空间,使用 WMI 查询 Win32_PrintJob,实现对打印队列的实时监控。可根据任务ID或文档名唯一标识,精确定位和管理打印作业。
  1. using System.Management;
  2. /// <summary>
  3. /// 打印机监听方法实现
  4. /// </summary>
  5. /// <param name="printerName"></param>
  6. /// <param name="taskId"></param>
  7. private void StartMonitorPrintJob(string printerName, int taskId, string taskName)
  8. {
  9.      Task.Run(() =>
  10.      {
  11.          try
  12.          {
  13.              string query = $"SELECT * FROM Win32_PrintJob WHERE Name LIKE '%{printerName}%'";
  14.              using (var searcher = new ManagementObjectSearcher(query))
  15.              {
  16.                  while (true)
  17.                  {
  18.                      var jobs = searcher.Get();
  19.                      bool found = false;
  20.                      foreach (ManagementObject job in jobs)
  21.                      {
  22.                          found = true;
  23.                          int JobId = Convert.ToInt32(job["JobId"]);
  24.                          if (JobId == taskId)
  25.                          {
  26.                              // 匹配到本任务,更新状态
  27.                              string jobStatus = job["JobStatus"]?.ToString() ?? "";
  28.                              string status = job["Status"]?.ToString() ?? "";
  29.                              string displayStatus = string.IsNullOrEmpty(jobStatus) ? status : jobStatus;
  30.                              UpdateTaskStatusOnUI(printerName, taskName, displayStatus);
  31.                              if (displayStatus.Contains("Printed") || displayStatus.Contains("Completed") || displayStatus.Contains("Deleted"))
  32.                                  return;
  33.                          }
  34.                      }
  35.                      if (!found)
  36.                      {
  37.                          // 作业已消失,认为已完成
  38.                          UpdateTaskStatusOnUI(printerName, taskName, "已完成");
  39.                          return;
  40.                      }
  41.                      Thread.Sleep(1000); // 1秒轮询
  42.                  }
  43.              }
  44.          }
  45.          catch (Exception ex)
  46.          {
  47.              UpdateTaskStatusOnUI(printerName, taskName, "状态监听失败");
  48.          }
  49.      });
  50. }
复制代码

  • 任务状态同步
    通过轮询方式定时查询打印队列,自动更新任务状态(如“正在打印”、“已完成”、“已取消”等),并在 UI 上实时反馈。
2. 打印任务操作


  • 取消打印
    通过 WMI 删除指定打印作业,确保任务被及时从队列中移除,并同步更新界面状态。
  1. /// <summary>
  2. /// 取消打印
  3. /// </summary>
  4. /// <param name="sender"></param>
  5. /// <param name="e"></param>
  6. private void CancelPrint_Click(object sender, EventArgs e)
  7. {
  8.      var dgv = GetCurrentDgv();
  9.      if (dgv == null) return;
  10.      var row = dgv.SelectedRows.Count > 0 ? dgv.SelectedRows[0] : null;
  11.      if (row == null) return;
  12.      int taskId = Convert.ToInt32(row.Cells["taskId"].Value);
  13.      string printerName = tabControl1.SelectedTab.Text;
  14.      // 查询打印队列,找到文档名包含 taskId 的作业
  15.      string query = $"SELECT * FROM Win32_PrintJob WHERE Name LIKE '%{printerName}%'";
  16.      using (var searcher = new System.Management.ManagementObjectSearcher(query))
  17.      {
  18.          foreach (System.Management.ManagementObject job in searcher.Get())
  19.          {
  20.              int JobId = Convert.ToInt32(job["JobId"]);
  21.              if (JobId == taskId)
  22.              {
  23.                  try
  24.                  {
  25.                      job.Delete(); // 删除打印任务
  26.                      row.Cells["status"].Value = "已取消";
  27.                      MessageBox.Show($"已取消打印任务:{taskId}", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  28.                  }
  29.                  catch (Exception ex)
  30.                  {
  31.                      MessageBox.Show("取消打印失败:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  32.                  }
  33.                  return;
  34.              }
  35.          }
  36.      }
  37.      MessageBox.Show("未找到对应的打印任务,可能已完成或被清除。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  38. }
复制代码

  • 重新打印
    首次打印时将所有参数(文件路径、数据、模板名等)保存在 DataGridView 行的 Tag 属性,重新打印时直接复用原始参数,保证打印一致性。
  1. /// <summary>
  2. /// 重新打印
  3. /// </summary>
  4. /// <param name="sender"></param>
  5. /// <param name="e"></param>
  6. private void Reprint_Click(object sender, EventArgs e)
  7. {
  8.      var dgv = GetCurrentDgv();
  9.      if (dgv == null) return;
  10.      var row = dgv.SelectedRows.Count > 0 ? dgv.SelectedRows[0] : null;
  11.      if (row == null) return;
  12.      if (row.Tag is PrintTaskInfo info)
  13.      {
  14.          // 复用原 taskName,或可选生成新 taskName
  15.          string taskName = row.Cells["taskName"].Value.ToString();
  16.          string status = row.Cells["status"].Value.ToString();
  17.          if (status == "已完成")
  18.              PrintFile(info.FilePath, info.Data, taskName);
  19.      }
  20.      else
  21.      {
  22.          MessageBox.Show("未找到原始打印信息,无法重新打印。", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  23.      }
  24. }
复制代码

  • 删除记录
    支持在任务未完成时先删除打印队列中的作业,再移除界面记录,防止“假删除”导致队列堆积。
  1. /// <summary>
  2. /// 删除打印记录
  3. /// </summary>
  4. /// <param name="sender"></param>
  5. /// <param name="e"></param>
  6. private void DeleteRecord_Click(object sender, EventArgs e)
  7. {
  8.     var dgv = GetCurrentDgv();
  9.     if (dgv == null) return;
  10.     var row = dgv.SelectedRows.Count > 0 ? dgv.SelectedRows[0] : null;
  11.     if (row == null) return;
  12.     int taskId = Convert.ToInt32(row.Cells["taskId"].Value);
  13.     string status = row.Cells["status"].Value?.ToString();
  14.     string printerName = tabControl1.SelectedTab.Text;
  15.     // 如果未完成,先删除打印队列中的任务
  16.     if (status != "已完成" && status != "已取消")
  17.     {
  18.         string query = $"SELECT * FROM Win32_PrintJob WHERE Name LIKE '%{printerName}%'";
  19.         using (var searcher = new System.Management.ManagementObjectSearcher(query))
  20.         {
  21.             foreach (System.Management.ManagementObject job in searcher.Get())
  22.             {
  23.                 int JobId = Convert.ToInt32(job["JobId"]);
  24.                 if (JobId == taskId)
  25.                 {
  26.                     try
  27.                     {
  28.                         job.Delete(); // 删除打印任务
  29.                     }
  30.                     catch (Exception ex)
  31.                     {
  32.                         MessageBox.Show("删除打印任务失败:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  33.                     }
  34.                     break;
  35.                 }
  36.             }
  37.         }
  38.     }
  39.     dgv.Rows.Remove(row);
  40. }
复制代码
3. WebSocket 通信


  • Fleck 组件集成
    使用 Fleck 实现 WebSocket 服务端,支持外部系统通过网络下发打印任务、查询状态、远程控制等。
  • 消息协议设计
    采用 JSON 协议,支持多种命令(如 print、show、ping 等),并能将打印结果、错误信息实时反馈给客户端。
  1. socket.OnMessage = message =>
  2. {
  3.     var msg = message?.Trim().ToLowerInvariant();
  4.     // 处理不同的消息
  5.     if (msg == "ping")
  6.     {
  7.         // 回复 pong
  8.         socket.Send("pong");
  9.     }
  10.     else if (msg == "show")
  11.     {
  12.         // 显示主窗体
  13.         this.Invoke(() =>
  14.         {
  15.             this.Show();
  16.             this.WindowState = FormWindowState.Normal;
  17.             this.ShowInTaskbar = true;
  18.             this.Activate();
  19.         });
  20.     }
  21.     else if (msg != null && msg.TrimStart().StartsWith("{"))
  22.     {
  23.         // 反序列化为 JsonNode 便于动态访问
  24.         var json = JsonNode.Parse(msg);
  25.         var cmd = json?["cmd"]?.ToString();
  26.         string requestId = json?["requestid"]?.ToString();
  27.         //处理打印任务
  28.         if (cmd == "print")
  29.         {
  30.             // 取出 printIniInfo 和 data
  31.             var printIniInfo = json["data"]?["printiniinfo"];
  32.             var data = json["data"]?["data"];
  33.             string filePath = printIniInfo?["filepath"]?.ToString();
  34.             string realName = printIniInfo?["realname"]?.ToString();
  35.             // 获取来源IP和端口
  36.             string clientIp = socket.ConnectionInfo.ClientIpAddress;
  37.             int clientPort = socket.ConnectionInfo.ClientPort;
  38.             string requestTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
  39.             string status = "作业正在后台处理";
  40.             // 获取当前系统默认打印机
  41.             string printerName = new System.Drawing.Printing.PrinterSettings().PrinterName;
  42.             int taskId = 0;
  43.             // 任务名称为当前时间
  44.             string taskName = DateTime.Now.ToString("yyyyMMddHHmmssfff");
  45.             // 查找对应TabPage和DataGridView
  46.             this.Invoke(() =>
  47.             {
  48.                 foreach (TabPage tab in tabControl1.TabPages)
  49.                 {
  50.                     // 支持“(默认)”后缀
  51.                     if (tab.Text.StartsWith(printerName))
  52.                     {
  53.                         var dgv = tab.Controls.OfType<DataGridView>().FirstOrDefault();
  54.                         if (dgv != null)
  55.                         {
  56.                             int rowIndex = dgv.Rows.Add(
  57.                                  $"{clientIp}:{clientPort}", // 来源
  58.                                  taskId,                     // 任务ID
  59.                                  taskName,                   // 任务名称
  60.                                  realName,                   // 模板
  61.                                  requestTime,                // 开始时间
  62.                                  status                      // 任务状态
  63.                              );
  64.                             var row = dgv.Rows[rowIndex];
  65.                             row.Tag = new PrintTaskInfo
  66.                             {
  67.                                 FilePath = filePath,
  68.                                 Data = data
  69.                             };
  70.                             // 添加后排序
  71.                             dgv.Sort(dgv.Columns["requestTime"], ListSortDirection.Descending);
  72.                         }
  73.                         break;
  74.                     }
  75.                 }
  76.             });
  77.             // 调用实际打印方法
  78.             this.Invoke(() => PrintFile(filePath, data, taskName, socket, requestId));
  79.             //监听打印机状态
  80.             StartMonitorPrintJob(printerName, taskId, taskName);
  81.         }
  82.         else
  83.         {
  84.             // 处理其他cmd
  85.             Console.WriteLine($"收到未知cmd: {cmd}");
  86.         }
  87.     }
  88.     else
  89.     {
  90.         Console.WriteLine($"收到未知消息: {message}");
  91.     }
  92. };
复制代码

  • 异常处理与反馈
    打印过程中如遇异常(如文件不存在、数据格式错误等),会捕获异常并通过 WebSocket 回复详细错误信息,便于外部系统及时处理。
4. 打印文件类型支持


  • 多格式兼容
    支持 TXT、图片(JPG/PNG/BMP/GIF)、PDF、FastReport 模板(FRX)等多种文件类型的打印。
    6.png

    7.png

  • 模板数据绑定
    对于报表类打印,支持将 JSON、DataTable、DataSet 等多种数据源动态绑定到模板,实现灵活的数据驱动打印。
5. 用户体验优化


  • 界面交互友好
    采用右键菜单、弹窗提示、托盘集成等方式,提升用户操作便捷性。
  • 错误提示与日志
    所有关键操作均有明确的错误提示,便于用户定位问题;可扩展日志记录功能,方便后期维护。
四、总结

打印监听组件通过对打印队列的实时监控、任务的精细化管理、与外部系统的高效集成,极大提升了企业打印自动化和可控性。其灵活的界面、丰富的功能和健壮的技术架构,适用于多种业务场景,值得在企业信息化建设中推广应用。
版权声明:本文为作者原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
作者: 码农刚子
原文链接: https://www.codeobservatory.cn/archives/bdd5c491.html

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册