王正良 發表於 2025-6-14 17:38:00

C# WinForms 实现打印监听组件

<h2 id="一组件简介">一、组件简介</h2>
<p>打印监听组件是一款集成于 Windows 桌面环境的打印任务管理与监控工具,适用于企业级应用场景。它不仅支持多打印机任务的实时监控,还能通过 WebSocket 与外部系统集成,实现自动化打印、任务状态反馈、远程控制等功能。</p>
<h2 id="二界面功能介绍">二、界面功能介绍</h2>
<h3 id="1-主界面与托盘集成">1. 主界面与托盘集成</h3>
<ul>
<li>主窗体:采用 WinForms 界面,包含多标签页(TabControl),每个标签页对应一台本地打印机,便于分组管理。<br>
<img src="https://images.codeobservatory.cn/hexo/202506/14/2025061415494301.png" alt="C# WinForms 实现打印监听组件" loading="lazy"></li>
<li>托盘图标:程序最小化后驻留于系统托盘,双击可快速还原主界面,支持右键菜单操作(如退出、重启、服务设置等)。<br>
<img src="https://images.codeobservatory.cn/hexo/202506/14/2025061415541902.png" alt="C# WinForms 实现打印监听组件" loading="lazy"></li>
</ul>
<h3 id="2-打印机管理">2. 打印机管理</h3>
<ul>
<li>打印机列表:自动检测本地所有已安装打印机,支持设置默认打印机、查看打印机属性。</li>
</ul>
<pre><code class="language-csharp">/// &lt;summary&gt;
/// 绑定本地打印机列表到菜单
/// &lt;/summary&gt;
internal void BindPrintersToMenu()
{
    默认打印机ToolStripMenuItem.DropDownItems.Clear();
    // 获取当前系统默认打印机
    string defaultPrinter = new System.Drawing.Printing.PrinterSettings().PrinterName;

    // 先添加默认打印机(始终第一行)
    var defaultItem = new ToolStripMenuItem(defaultPrinter)
    {
      Checked = true
    };
    defaultItem.Click += (s, e) =&gt; SetDefaultPrinterUI(defaultPrinter);
    // 添加“首选项”子菜单
    var prefItem = new ToolStripMenuItem("首选项");
    prefItem.Click += (s, e) =&gt; ShowPrinterProperties(defaultPrinter);
    defaultItem.DropDownItems.Add(prefItem);
    默认打印机ToolStripMenuItem.DropDownItems.Add(defaultItem);

    // 再添加其他打印机(排除默认打印机)
    foreach (string printer in System.Drawing.Printing.PrinterSettings.InstalledPrinters)
    {
      if (printer == defaultPrinter)
            continue;

      var item = new ToolStripMenuItem(printer)
      {
            Checked = false
      };
      item.Click += (s, e) =&gt; SetDefaultPrinterUI(printer);

      var prefItem2 = new ToolStripMenuItem("首选项");
      prefItem2.Click += (s, e) =&gt; ShowPrinterProperties(printer);
      item.DropDownItems.Add(prefItem2);

      默认打印机ToolStripMenuItem.DropDownItems.Add(item);
    }

}
/// &lt;summary&gt;
/// UI和系统都设置默认打印机
/// &lt;/summary&gt;
/// &lt;param name="printerName"&gt;&lt;/param&gt;
private void SetDefaultPrinterUI(string printerName)
{
   foreach (ToolStripMenuItem item in 默认打印机ToolStripMenuItem.DropDownItems)
         item.Checked = item.Text == printerName;

   // 如需设置为系统默认打印机,可调用 Win32 API(可选)
   SetSystemDefaultPrinter(printerName);
}
/// &lt;summary&gt;
/// 显示打印机首选项对话框
/// &lt;/summary&gt;
/// &lt;param name="printerName"&gt;&lt;/param&gt;
private void ShowPrinterProperties(string printerName)
{
    // 使用rundll32调用打印机属性对话框
    //string args = $"printui.dll,PrintUIEntry /p /n \"{printerName}\"";
    //•        /e 参数表示直接打开“首选项”对话框
    string args = $"printui.dll,PrintUIEntry /e /n \"{printerName}\"";
    var psi = new System.Diagnostics.ProcessStartInfo
    {
      FileName = "rundll32.exe",
      Arguments = args,
      UseShellExecute = false,
      CreateNoWindow = true
    };
    try
    {
      System.Diagnostics.Process.Start(psi);
    }
    catch (Exception ex)
    {
      MessageBox.Show("无法打开打印机首选项窗口:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}
</code></pre>
<ul>
<li>TabControl:每台打印机一个标签页,便于查看和管理各自的打印任务。</li>
</ul>
<pre><code class="language-csharp"> /// &lt;summary&gt;
/// 绑定本地打印机列表到TabControl
/// &lt;/summary&gt;
private void BindPrintersToTabControl()
{
   tabControl1.TabPages.Clear();

   string defaultPrinter = new System.Drawing.Printing.PrinterSettings().PrinterName;
   List&lt;string&gt; printers = new List&lt;string&gt;();

   // 先将默认打印机添加到列表首位
   printers.Add(defaultPrinter);

   // 再添加其他打印机(排除默认打印机)
   foreach (string printer in System.Drawing.Printing.PrinterSettings.InstalledPrinters)
   {
         if (printer != defaultPrinter)
             printers.Add(printer);
   }

   foreach (string printer in printers)
   {
         var tabPage = new TabPage(printer);
         // 创建DataGridView
         var dgv = new DataGridView
         {
             Dock = DockStyle.Fill,
             ReadOnly = true,
             AllowUserToAddRows = false,
             AllowUserToDeleteRows = false,
             RowHeadersVisible = false,
             AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill
         };

         // 添加列
         dgv.Columns.Add("clientIp", "来源");
         dgv.Columns.Add("taskId", "任务ID");
         dgv.Columns.Add("taskName", "任务名称");
         dgv.Columns.Add("realName", "模板");
         dgv.Columns.Add("requestTime", "开始时间");
         dgv.Columns.Add("status", "任务状态");

         //绑定菜单
         dgv.ContextMenuStrip = dgvContextMenu;
         dgv.MouseDown += Dgv_MouseDown;

         // 创建TextBox
         var txtSearch = new TextBox
         {
             PlaceholderText = "任务ID",
             Width = 120,
             Anchor = AnchorStyles.Left | AnchorStyles.Bottom
         };

         // 创建Button
         var btnSearch = new Button
         {
             Text = "查找",
             Width = 60,
             Anchor = AnchorStyles.Left | AnchorStyles.Bottom
         };

         // 查找事件
         btnSearch.Click += (s, e) =&gt;
         {
             string searchId = txtSearch.Text.Trim();
             bool found = false;
             foreach (DataGridViewRow row in dgv.Rows)
             {
               if (row.IsNewRow) continue;
               if (row.Cells["taskId"].Value?.ToString() == searchId)
               {
                     row.Selected = true;
                     dgv.CurrentCell = row.Cells["taskId"];
                     found = true;
               }
               else
               {
                     row.Selected = false;
               }
             }
             if (!found)
             {
               MessageBox.Show("未找到对应任务ID!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
             }
         };

         // 使用Panel布局
         var panel = new Panel
         {
             Dock = DockStyle.Bottom,
             Height = 40
         };
         txtSearch.Location = new Point(10, 8);
         btnSearch.Location = new Point(140, 6);
         panel.Controls.Add(txtSearch);
         panel.Controls.Add(btnSearch);

         tabPage.Controls.Add(panel);
         tabPage.Controls.Add(dgv);
         tabControl1.TabPages.Add(tabPage);
   }
}
</code></pre>
<h3 id="3-打印任务监控">3. 打印任务监控</h3>
<ul>
<li>任务列表:每个打印机标签页下方为 DataGridView,实时显示当前打印任务,包括来源、任务ID、任务名称、模板、开始时间、任务状态等信息。<br>
<img src="https://images.codeobservatory.cn/hexo/202506/14/2025061415494301.png" alt="C# WinForms 实现打印监听组件" loading="lazy"></li>
<li>右键菜单:支持对单个任务进行“取消打印”、“重新打印”、“删除记录”等操作。<br>
<img src="https://images.codeobservatory.cn/hexo/202506/14/2025061416011103.png" alt="C# WinForms 实现打印监听组件" loading="lazy"></li>
<li>任务搜索:支持按任务ID快速定位任务。</li>
</ul>
<h3 id="4-其他功能">4. 其他功能</h3>
<ul>
<li>服务控制:可一键启动/停止 WebSocket 服务,支持与外部系统通信。</li>
<li>模板设计与预览:集成 FastReport 设计器和预览器,方便模板维护。</li>
</ul>
<blockquote>
<p>因为FastReport.Net 是需要购买授权的,所以我使用的是FastReport.OpenSource(开源版),开源版功能太少,不能直接从程序内部调用FastReport设计器和预览器,只能通过启动本地安装的.exe来实现。</p>
</blockquote>
<pre><code class="language-csharp">/// &lt;summary&gt;
/// 设计菜单项点击事件,启动 FastReport 设计器
/// &lt;/summary&gt;
/// &lt;param name="sender"&gt;&lt;/param&gt;
/// &lt;param name="e"&gt;&lt;/param&gt;
private void 设计ToolStripMenuItem_Click(object sender, EventArgs e)
{
    string designerPath = GetConfigValue("designer_path");
    string templatePath = GetTemplatePathFromConfig();
    if (string.IsNullOrEmpty(designerPath) || !System.IO.File.Exists(designerPath))
    {
      MessageBox.Show("未找到 FastReport 设计器,请检查 config.ini 配置!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
      return;
    }
    if (!System.IO.File.Exists(templatePath))
    {
      MessageBox.Show("未找到模板文件,请检查路径!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
      return;
    }
    try
    {
      System.Diagnostics.Process.Start(designerPath, $"\"{templatePath}\"");
    }
    catch (Exception ex)
    {
      MessageBox.Show("启动 FastReport 设计器失败:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}
private void 预览ToolStripMenuItem_Click(object sender, EventArgs e)
{
    string viewerPath = GetConfigValue("viewer_path");
    string templatePath = GetTemplatePathFromConfig();
    if (string.IsNullOrEmpty(viewerPath) || !System.IO.File.Exists(viewerPath))
    {
      MessageBox.Show("未找到 FastReport 预览器,请检查 config.ini 配置!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
      return;
    }
    if (!System.IO.File.Exists(templatePath))
    {
      MessageBox.Show("未找到模板文件,请检查路径!", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
      return;
    }
    try
    {
      System.Diagnostics.Process.Start(viewerPath, $"\"{templatePath}\"");
    }
    catch (Exception ex)
    {
      MessageBox.Show("启动 FastReport 预览器失败:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}
</code></pre>
<ul>
<li>自动更新:支持在线检查和自动更新程序版本。</li>
</ul>
<blockquote>
<p>这里使用的是:<code>Autoupdater.NET</code></p>
</blockquote>
<ul>
<li>帮助与支持:内置开发者联系方式,便于用户反馈和技术支持。<br>
<img src="https://images.codeobservatory.cn/hexo/202506/14/2025061416103304.png" alt="开发者联系方式:wxscc@foxmail.com" loading="lazy"></li>
</ul>
<h2 id="三技术要点">三、技术要点</h2>
<h3 id="1-打印任务监听与管理">1. 打印任务监听与管理</h3>
<ul>
<li>WMI 打印作业监控<br>
通过 <code>System.Management</code> 命名空间,使用 <code>WMI</code> 查询 <code>Win32_PrintJob</code>,实现对打印队列的实时监控。可根据任务ID或文档名唯一标识,精确定位和管理打印作业。</li>
</ul>
<pre><code class="language-csharp">using System.Management;

/// &lt;summary&gt;
/// 打印机监听方法实现
/// &lt;/summary&gt;
/// &lt;param name="printerName"&gt;&lt;/param&gt;
/// &lt;param name="taskId"&gt;&lt;/param&gt;
private void StartMonitorPrintJob(string printerName, int taskId, string taskName)
{
   Task.Run(() =&gt;
   {
         try
         {
             string query = $"SELECT * FROM Win32_PrintJob WHERE Name LIKE '%{printerName}%'";
             using (var searcher = new ManagementObjectSearcher(query))
             {
               while (true)
               {
                     var jobs = searcher.Get();
                     bool found = false;
                     foreach (ManagementObject job in jobs)
                     {
                         found = true;
                         int JobId = Convert.ToInt32(job["JobId"]);
                         if (JobId == taskId)
                         {
                           // 匹配到本任务,更新状态
                           string jobStatus = job["JobStatus"]?.ToString() ?? "";
                           string status = job["Status"]?.ToString() ?? "";
                           string displayStatus = string.IsNullOrEmpty(jobStatus) ? status : jobStatus;
                           UpdateTaskStatusOnUI(printerName, taskName, displayStatus);
                           if (displayStatus.Contains("Printed") || displayStatus.Contains("Completed") || displayStatus.Contains("Deleted"))
                                 return;
                         }
                     }
                     if (!found)
                     {
                         // 作业已消失,认为已完成
                         UpdateTaskStatusOnUI(printerName, taskName, "已完成");
                         return;
                     }
                     Thread.Sleep(1000); // 1秒轮询
               }
             }
         }
         catch (Exception ex)
         {
             UpdateTaskStatusOnUI(printerName, taskName, "状态监听失败");
         }
   });
}
</code></pre>
<ul>
<li>任务状态同步<br>
通过轮询方式定时查询打印队列,自动更新任务状态(如“正在打印”、“已完成”、“已取消”等),并在 UI 上实时反馈。</li>
</ul>
<h3 id="2-打印任务操作">2. 打印任务操作</h3>
<ul>
<li>取消打印<br>
通过 WMI 删除指定打印作业,确保任务被及时从队列中移除,并同步更新界面状态。</li>
</ul>
<pre><code class="language-csharp"> /// &lt;summary&gt;
/// 取消打印
/// &lt;/summary&gt;
/// &lt;param name="sender"&gt;&lt;/param&gt;
/// &lt;param name="e"&gt;&lt;/param&gt;
private void CancelPrint_Click(object sender, EventArgs e)
{
   var dgv = GetCurrentDgv();
   if (dgv == null) return;
   var row = dgv.SelectedRows.Count &gt; 0 ? dgv.SelectedRows : null;
   if (row == null) return;
   int taskId = Convert.ToInt32(row.Cells["taskId"].Value);
   string printerName = tabControl1.SelectedTab.Text;
   // 查询打印队列,找到文档名包含 taskId 的作业
   string query = $"SELECT * FROM Win32_PrintJob WHERE Name LIKE '%{printerName}%'";
   using (var searcher = new System.Management.ManagementObjectSearcher(query))
   {
         foreach (System.Management.ManagementObject job in searcher.Get())
         {
             int JobId = Convert.ToInt32(job["JobId"]);
             if (JobId == taskId)
             {
               try
               {
                     job.Delete(); // 删除打印任务
                     row.Cells["status"].Value = "已取消";
                     MessageBox.Show($"已取消打印任务:{taskId}", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
               }
               catch (Exception ex)
               {
                     MessageBox.Show("取消打印失败:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
               }
               return;
             }
         }
   }
   MessageBox.Show("未找到对应的打印任务,可能已完成或被清除。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
</code></pre>
<ul>
<li>重新打印<br>
首次打印时将所有参数(文件路径、数据、模板名等)保存在 DataGridView 行的 Tag 属性,重新打印时直接复用原始参数,保证打印一致性。</li>
</ul>
<pre><code class="language-csharp"> /// &lt;summary&gt;
/// 重新打印
/// &lt;/summary&gt;
/// &lt;param name="sender"&gt;&lt;/param&gt;
/// &lt;param name="e"&gt;&lt;/param&gt;
private void Reprint_Click(object sender, EventArgs e)
{
   var dgv = GetCurrentDgv();
   if (dgv == null) return;
   var row = dgv.SelectedRows.Count &gt; 0 ? dgv.SelectedRows : null;
   if (row == null) return;
   if (row.Tag is PrintTaskInfo info)
   {
         // 复用原 taskName,或可选生成新 taskName
         string taskName = row.Cells["taskName"].Value.ToString();
         string status = row.Cells["status"].Value.ToString();
         if (status == "已完成")
             PrintFile(info.FilePath, info.Data, taskName);
   }
   else
   {
         MessageBox.Show("未找到原始打印信息,无法重新打印。", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
   }
}
</code></pre>
<ul>
<li>删除记录<br>
支持在任务未完成时先删除打印队列中的作业,再移除界面记录,防止“假删除”导致队列堆积。</li>
</ul>
<pre><code class="language-csharp">/// &lt;summary&gt;
/// 删除打印记录
/// &lt;/summary&gt;
/// &lt;param name="sender"&gt;&lt;/param&gt;
/// &lt;param name="e"&gt;&lt;/param&gt;
private void DeleteRecord_Click(object sender, EventArgs e)
{
    var dgv = GetCurrentDgv();
    if (dgv == null) return;
    var row = dgv.SelectedRows.Count &gt; 0 ? dgv.SelectedRows : null;
    if (row == null) return;

    int taskId = Convert.ToInt32(row.Cells["taskId"].Value);
    string status = row.Cells["status"].Value?.ToString();
    string printerName = tabControl1.SelectedTab.Text;
    // 如果未完成,先删除打印队列中的任务
    if (status != "已完成" &amp;&amp; status != "已取消")
    {
      string query = $"SELECT * FROM Win32_PrintJob WHERE Name LIKE '%{printerName}%'";
      using (var searcher = new System.Management.ManagementObjectSearcher(query))
      {
            foreach (System.Management.ManagementObject job in searcher.Get())
            {
                int JobId = Convert.ToInt32(job["JobId"]);
                if (JobId == taskId)
                {
                  try
                  {
                        job.Delete(); // 删除打印任务
                  }
                  catch (Exception ex)
                  {
                        MessageBox.Show("删除打印任务失败:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                  }
                  break;
                }
            }
      }
    }
    dgv.Rows.Remove(row);
}
</code></pre>
<h3 id="3-websocket-通信">3. WebSocket 通信</h3>
<ul>
<li>Fleck 组件集成<br>
使用 <code>Fleck</code> 实现 WebSocket 服务端,支持外部系统通过网络下发打印任务、查询状态、远程控制等。</li>
<li>消息协议设计<br>
采用 JSON 协议,支持多种命令(如 <code>print</code>、<code>show</code>、<code>ping</code> 等),并能将打印结果、错误信息实时反馈给客户端。</li>
</ul>
<pre><code class="language-csharp">socket.OnMessage = message =&gt;
{
    var msg = message?.Trim().ToLowerInvariant();
    // 处理不同的消息
    if (msg == "ping")
    {
      // 回复 pong
      socket.Send("pong");
    }
    else if (msg == "show")
    {
      // 显示主窗体
      this.Invoke(() =&gt;
      {
            this.Show();
            this.WindowState = FormWindowState.Normal;
            this.ShowInTaskbar = true;
            this.Activate();
      });
    }
    else if (msg != null &amp;&amp; msg.TrimStart().StartsWith("{"))
    {
      // 反序列化为 JsonNode 便于动态访问
      var json = JsonNode.Parse(msg);
      var cmd = json?["cmd"]?.ToString();
      string requestId = json?["requestid"]?.ToString();
      //处理打印任务
      if (cmd == "print")
      {
            // 取出 printIniInfo 和 data
            var printIniInfo = json["data"]?["printiniinfo"];
            var data = json["data"]?["data"];
            string filePath = printIniInfo?["filepath"]?.ToString();
            string realName = printIniInfo?["realname"]?.ToString();

            // 获取来源IP和端口
            string clientIp = socket.ConnectionInfo.ClientIpAddress;
            int clientPort = socket.ConnectionInfo.ClientPort;
            string requestTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            string status = "作业正在后台处理";
            // 获取当前系统默认打印机
            string printerName = new System.Drawing.Printing.PrinterSettings().PrinterName;
            int taskId = 0;
            // 任务名称为当前时间
            string taskName = DateTime.Now.ToString("yyyyMMddHHmmssfff");
            // 查找对应TabPage和DataGridView
            this.Invoke(() =&gt;
            {
                foreach (TabPage tab in tabControl1.TabPages)
                {
                  // 支持“(默认)”后缀
                  if (tab.Text.StartsWith(printerName))
                  {
                        var dgv = tab.Controls.OfType&lt;DataGridView&gt;().FirstOrDefault();
                        if (dgv != null)
                        {
                            int rowIndex = dgv.Rows.Add(
                                 $"{clientIp}:{clientPort}", // 来源
                                 taskId,                     // 任务ID
                                 taskName,                   // 任务名称
                                 realName,                   // 模板
                                 requestTime,                // 开始时间
                                 status                      // 任务状态
                           );
                            var row = dgv.Rows;
                            row.Tag = new PrintTaskInfo
                            {
                              FilePath = filePath,
                              Data = data
                            };
                            // 添加后排序
                            dgv.Sort(dgv.Columns["requestTime"], ListSortDirection.Descending);
                        }
                        break;
                  }
                }
            });
            // 调用实际打印方法
            this.Invoke(() =&gt; PrintFile(filePath, data, taskName, socket, requestId));

            //监听打印机状态
            StartMonitorPrintJob(printerName, taskId, taskName);
      }
      else
      {
            // 处理其他cmd
            Console.WriteLine($"收到未知cmd: {cmd}");
      }
    }
    else
    {
      Console.WriteLine($"收到未知消息: {message}");
    }
};
</code></pre>
<ul>
<li>异常处理与反馈<br>
打印过程中如遇异常(如文件不存在、数据格式错误等),会捕获异常并通过 WebSocket 回复详细错误信息,便于外部系统及时处理。</li>
</ul>
<h3 id="4-打印文件类型支持">4. 打印文件类型支持</h3>
<ul>
<li>多格式兼容<br>
支持 TXT、图片(JPG/PNG/BMP/GIF)、PDF、FastReport 模板(FRX)等多种文件类型的打印。<br>
<img src="https://images.codeobservatory.cn/hexo/202506/14/2025061416305105.png" alt="C# WinForms 实现打印监听组件" loading="lazy"><br>
<img src="https://images.codeobservatory.cn/hexo/202506/14/2025061416290206.png" alt="C# WinForms 实现打印监听组件" loading="lazy"></li>
<li>模板数据绑定<br>
对于报表类打印,支持将 JSON、DataTable、DataSet 等多种数据源动态绑定到模板,实现灵活的数据驱动打印。</li>
</ul>
<h3 id="5-用户体验优化">5. 用户体验优化</h3>
<ul>
<li>界面交互友好<br>
采用右键菜单、弹窗提示、托盘集成等方式,提升用户操作便捷性。</li>
<li>错误提示与日志<br>
所有关键操作均有明确的错误提示,便于用户定位问题;可扩展日志记录功能,方便后期维护。</li>
</ul>
<h2 id="四总结">四、总结</h2>
<p>打印监听组件通过对打印队列的实时监控、任务的精细化管理、与外部系统的高效集成,极大提升了企业打印自动化和可控性。其灵活的界面、丰富的功能和健壮的技术架构,适用于多种业务场景,值得在企业信息化建设中推广应用。</p>
<hr>
<p>版权声明:本文为作者原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。<br>
作者: 码农刚子<br>
原文链接: https://www.codeobservatory.cn/archives/bdd5c491.html</p><br><br>
来源:https://www.cnblogs.com/shenchuanchao/p/18928583
頁: [1]
查看完整版本: C# WinForms 实现打印监听组件