前幾天分享把 ASP.NET Core 變成 Windows 桌面常駐程式的小技巧,一不做二不休,再把它包進 Drk.AspNetCore.MinimalApiKit NuGet 程式庫,方便未來應用。

使用程式庫後,開發桌面常駐小工具的步驟再簡化如下:

  1. 建立 ASP.NET Core Minimal API 專案
  2. dotnet add package Drk.AspNetCore.MinimalApiKit 參照程式庫
  3. 加入工具所需功能(網頁介面、定期偵測... 等)
  4. 選一個 .ico 圖示檔加入專案並修改 .csproj:
    1. 設 OutputType 為 WinExe
    2. TargetFramework 加 -windows (net6.0-windows 或 net7.0-windows)
    3. <ApplicationIcon>App.ico</ApplicationIcon><UseWindowsForms>true</UseWindowsForms>
    4. 使用 <None Remove="App.ico" /><EmbeddedResource Include="App.ico" /> 將圖示檔內嵌到 exe 內 範例:
    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net7.0-windows</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <ApplicationIcon>App.ico</ApplicationIcon>
        <UseWindowsForms>true</UseWindowsForms>
      </PropertyGroup>
      <ItemGroup>
        <None Remove="App.ico" />
        <EmbeddedResource Include="App.ico" />
      </ItemGroup>
      <ItemGroup>
        <PackageReference Include="Drk.AspNetCore.MinimalApiKit" Version="0.9.7" />
      </ItemGroup>
    
    </Project>
    
  5. 將原本的 app.Run() 換成 app.RunWithNotifyIcon()。這裡要傳入參數設定右鍵選單:
    var appIcoResName = typeof(Program).Assembly.GetManifestResourceNames().Single(o => o.EndsWith("App.ico"));
    app.RunWithNotifyIcon(new NotifyIconOptions{
        IconStream = typeof(Program).Assembly.GetManifestResourceStream(appIcoResName)!,
        ToolTip = "Notepad Monitor",
        MenuItems = {
            // 設定選單項目開啟網頁,第一個參數是選單文字,第二個參數支援動態決定瀏覽網址
            NotifyIconOptions.CreateLaunchBrowserMenuItem("Check logs", (url) => url + "/logs"),
            // 分隔線
            NotifyIconOptions.CreateMenuSeparator(),
            // 自訂選單動作
            NotifyIconOptions.CreateActionMenuItem("Lauch Notepad", (state) => {
                System.Diagnostics.Process.Start("notepad.exe");
            })
        }
    });
    
    以上設定會產生如下圖的選單(最下方的 Exit 為自動加上的,顯示文字可透過 NotifyIconOptions.ExitMenuItemText 更改)

RunWithNotifyIcon() 封裝了 Notify Icon 程式庫細節,專案不需參照 H.NotifyIcon 程式庫,提供選單文字及動作定義就能宣告工作列圖示之右鍵選單,點 Exit 選單結束的邏輯也由程式庫負責,讓 Program.cs 聚焦在工具功能本身。

我引用 Drk.AspNetCore.MinimalApiKit 程式庫寫了一個有趣的範例 - Notepad 記事本偵測器,使用者每次開啟 Notepad 時會彈出 MessageBox,另外再示範由自訂選單動作啟動 Notepad:

展示影片

看似沒什麼鳥用的展示,背後的運作原理倒挺實用。程序啟動了一個背景服務(IHostedService),用定時器(Timer)跑定期檢查,偵測到目標或符合特定條件時觸發 MessageBox。

「定期輪詢 + 特定條件觸發動作」的概念可以衍生許多實用功能,例如:查詢第三方 WebAPI,有新訊息/新待辦事項時彈 MessageBox 通知、偵測電腦有異常連線時發出警告... 等等。長期監控程式寫成 Windows 服務也有類似效果,但如應用適用於互動登入情境(例如:人在電腦前才能處理的工作)、或機器人需借用使用者當下環境做事,就很適合寫成桌面常駐工具形式。

如果對怎麼實現偵測新開啟 Notepad.exe 及彈出 MessageBox 有興趣,以下是程式碼:(註:程式以驗證可行性為目標,力求簡單,未考慮可維護性及強韌性)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Diagnostics;

namespace NotepadMon
{
    //https://blog.darkthread.net/blog/aspnet-core-background-task/
    public class NotepadMonitor : IHostedService, IDisposable
    {
        static System.Threading.Timer _timer = null!;

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _timer = new System.Threading.Timer(DoWork, null,
                TimeSpan.Zero,
                TimeSpan.FromSeconds(1));
            return Task.CompletedTask;
        }

        bool running = false;
        int[] lastPids = null!;
        public static List<string> Logs = new List<string>();
        public void DoWork(object? state)
        {
            if (running) return; //no reentrancy
            running = true;
            var notepadPids = Process.GetProcessesByName("Notepad")
                .Select(p => p.Id).ToArray();
            //find new processes
            if (lastPids == null) lastPids = notepadPids;
            if (notepadPids.Except(lastPids).Any())
            {
                Task.Factory.StartNew(async () =>
                {
                    var msg = $"new Notepad started!";
                    Logs.Add($"{DateTime.Now:HH:mm:ss} {msg}");
                    await Task.Delay(500);
                    System.Windows.Forms.MessageBox.Show(msg, "Notepad Alert", 
                        MessageBoxButtons.OK, MessageBoxIcon.Information, 
                        MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly);
                });
            }
            lastPids = notepadPids;
            running = false;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _timer?.Change(Timeout.Infinite, 0);
            return Task.CompletedTask;
        }
        public void Dispose()
        {
            _timer?.Dispose();
        }
    }

}

完整範例專案放在 Github,新建專案帶的是 net7.0,我就順勢寫成 net7.0-widnows,大家若在意 LTS,可改為 net6.0-windows,不影響結果。

Tutorial of how to use Drk.AspNetCore.MinimalApiKit to let ASP.NET Core Minimal API applcation run in background with notify icon.


Comments

# by Cash

Github 範例的連結好像 404 ?

# by JD

不用新的PeriodicTimer?

# by Your Name

可以再強化偵測是否有開啟股票看盤軟體。 然後自動輸入帳號與密碼,達到自動登入。

# by W

Github 範例的連結好像錯了

# by Jeffrey

to Cash, W, 忘記設成公開了,已調整。

# by Jeffrey

to JD,沒用的原因是還沒學到。已筆記,謝謝分享。

Post a comment