はじめに
欲しいと思ったので時計ウィジェット作りました。
思ったより規模が大きくなった……
せっかく作ったので公開してみようかなって。
テストとかあんましてないけど、まあ、おおむねちゃんと動いてるのでいいかなって。
ネイティブ API バチバチに使ってるので、 Windows のみです。
Impact と Yu Gothic UI Semibold のフォントを使ってるので、それがないと表示がおかしくなるかも。
公開場所
ここに置いてます。
github.com
注意事項
天気情報は気象庁の非公式 API を使用し取得しています。
非公式のため、予告なく変更・停止などされる可能性があります。
取得部分のコードの再利用、アプリの使用については 気象庁ホームページの利用規約 を確認の上、気象庁のサーバーに過度な負荷を与えないよう心がけ、自己責任のもと使用してください。
詳細
見た目はこんな感じになってます。

個人的な好みでアナログです。
表示できる情報
- 時間(当然)
- 日付
- 国民の祝日
- 天気(ほぼリアルタイム)
設定項目
設定ウィンドウで OK ボタンを押すまで反映されません。
キャンセル ボタンを押したり、設定ウィンドウを閉じると変更は破棄されます。
ネットワーク接続をしないモードです。
国民の祝日のリストと天気はネットから持ってきてるので、そういうのをしません。
天気は API をボコボコ叩いてるので、スタンドアロンでは意味がないので強制的に非表示になります。
サイズ
時計全体のサイズを変更します。
常に最前面
常に最前面に表示されるようにします。
日付表示
日付を表示するかどうかを決めます。
天気表示
天気を表示するかどうかを決めます。
非表示にすると API を叩きに行くのをやめます。
各部色設定
をそれぞれ設定できます。
特にプリセットみたいな機能は付けてません。(面倒だし別に要らんかなって)
その他
ドラッグで移動できます。
文字盤、それぞれの針は ClockResources 以下に SVG で置いてあるので、それを入れ替えれば好きなデザインにできます。
針の SVG は、針自体の色を 黒 0xFF000000 、背景に文字盤と同じ大きさの 透明 0x00000000 な円か何かを置いてください。
コンテキストメニュー(右クリック or タスクトレイのアイコンをクリック)からもいくつか直接設定できます。
技術的な話
WPF もりもり書くのは久しぶりだったので、結構大変でした。 Prism/DryIoc で MVVM です。
ChatGPT に結構助けられました。すごいね、 ChatGPT 。
NuGet パッケージ
マテリアルデザインでいい感じにするやつとか、いい感じにログするやつとかを入れてます。
Hardcodet.NotifyIcon.Wpf はタスクトレイにアイコン表示するやつですね。
Polly は API 叩きに行くときのリトライ制御とかをいい感じにするやつです。
メインウィンドウ
何もしないと Alt + Tab のウィンドウ切り替え(個人的に多用する)の一覧に出てきて邪魔なのでツールウィンドウにしてます。
Win32 API とか叩きたくなかったけどしょうがない……
public MainWindow()
{
InitializeComponent();
this.SourceInitialized += this.MainWindow_SourceInitialized;
}
private void MainWindow_SourceInitialized(object sender, EventArgs e)
{
IntPtr hwnd = new WindowInteropHelper(this).Handle;
int exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
exStyle |= WS_EX_TOOLWINDOW;
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle);
}
private const int GWL_EXSTYLE = -20;
private const int WS_EX_TOOLWINDOW = 0x00000080;
[DllImport("user32.dll", SetLastError = true)]
static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", SetLastError = true)]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
あとは WindowStyle="None" とか色々いい感じにしとけばいい感じになります。
天気取得
気象庁の非公式 API ってやつを叩いてます。
JSON が降ってくるので、デシリアライズ先のクラスとか、コンバーターとかを作るのが面倒でしたね。
天気データについて
取ってくるのはアメダスの天気データです。
アメダスのデータ構造とかはいろんなところで解説されてるので、詳しい説明はしません。デシリアライズ用クラスとか見たら雰囲気がわかるかもしれません。
このアプリで使うのは以下の3つです。
全部アメダス観測所の ID をキーとしたディクショナリになっています。つまり、全観測所のデータがいっぺんに降ってきます。
データ整理
アメダス観測所の ID で突合してます。
ついでに、アメダス観測所の緯度経度と現在地の緯度経度から距離を計算して、近い順に並び変えます。
var amedas = amedasLocations
.Select(x => new
{
x.LocationId,
Amedas = x,
Data10m = amedasData10m.FirstOrDefault(y => y.LocationId == x.LocationId),
Data1h = amedasData1h.FirstOrDefault(y => y.LocationId == x.LocationId),
Distance = geoCoordinate.DistanceTo(x.Latitude, x.Longitude)
})
.OrderBy(x => x.Distance);
現在地の緯度経度
ip-api.com から取得してます。
ip-api.com
IP アドレスから取得するので、プロキシとか通して全然違うところからアクセスしたら変な緯度経度が返ってくるかもしれません。
初期化時にしかアクセスしないので、移動しながら現在地を更新とかもできません。
データ抽出
このアプリでは気温と天気(晴れとか雨とか)を表示しますが、それぞれ見るべきデータが違います。
10分データと1時間データで構造はどちらも同じですが、天気は1時間データにしか含まれず、天気の項目を配信する観測所も一部しかありません。
まとめると……
- 気温
- 天気
- 1時間データで、天気の項目が存在し、一番近い観測所のデータ
これをフィルターして、それぞれで見るべき観測所 ID を保存し、以降は保存した観測所 ID のデータを見ています。
var data10m = amedas.FirstOrDefault(x => x.Data10m is not null);
var data1h = amedas.FirstOrDefault(x => x.Data1h?.Weather is not null);
// _semaphoreAmedas10m -> _semaphoreAmedas1h の順で取得する(デッドロック対策)
await this._semaphoreAmedas10m.WaitAsync();
await this._semaphoreAmedas1h.WaitAsync();
try
{
this._target10mAmedasLocationId = data10m?.LocationId;
this._target1hAmedasLocationId = data1h?.LocationId;
this._cacheAmedasData10m = data10m?.Data10m;
this._lastAmedasData10mUpdated = now;
this._cacheAmedasData1h = data1h?.Data1h;
this._lastAmedasData1hUpdated = now;
}
finally
{
// 逆順に開放(デッドロック対策)
this._semaphoreAmedas1h.Release();
this._semaphoreAmedas10m.Release();
}
取得タイミング
観測所一覧
そうそう変化しないと思うので、一度取得したらローカルに保存して、以降はローカルのデータを読むようにしてます。
更新は半年(180日)です。
10分データ
更新は5分に1回です。
現在時刻を基に取得しますが、タイミングによっては配信されてない場合もあるので、最大20分前まで遡って取得できるようにしてます。
public async Task<IEnumerable<AmedasData>> GetAllAmedasData10mAsync()
{
using var _ = new LoggerScope(this._logger);
var now = DateTime.Now;
for (int i = 0; i < 3; i++)
{
var target = $"{now.AddMinutes(-10 * i).ToString("yyyyMMddHHmm")[..11]}000.json";
var url = $"{AMEDAS_API_URL}{target}";
try
{
this._logger.LogInformation("10分間天気データ({Target})取得開始", target);
this._logger.LogInformation("{Url} アクセス実行", url);
using var res = await this.GetWithRetryAsync(url, HttpCompletionOption.ResponseHeadersRead);
res.EnsureSuccessStatusCode();
this._logger.LogInformation("{Url} アクセス成功", url);
this._logger.LogDebug("{Url} レスポンス取得", url);
var json = await res.Content.ReadAsStringAsync();
this._logger.LogDebug("JSON デシリアライズ");
var amedasDataRaw = JsonSerializer.Deserialize<IDictionary<string, Json.Data.AmedasData>>(json);
var amedasData = amedasDataRaw.MapToAmedasData();
this._logger.LogInformation("10分間天気データ({Target})取得完了", target);
return amedasData;
}
catch (HttpRequestException ex)
{
if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
this._logger.LogWarning("10分間天気データ({Target})未配信のため再取得", target);
continue;
}
this._logger.LogError(ex, "10分間天気データ取得失敗({Url} アクセス失敗)", url);
break;
}
catch (JsonException ex)
{
this._logger.LogError(ex, "10分間天気データ取得失敗(JSON デシリアライズ失敗)");
break;
}
catch (Exception ex)
{
this._logger.LogError(ex, "10分間天気データ取得失敗(不明なエラー)");
break;
}
}
return Enumerable.Empty<AmedasData>();
}
5分間隔以上の頻度でアクセスする場合は、サービス側でキャッシュを返すようにしています。
1時間データ
更新は30分に1回です。
これもタイミングによっては配信されてない場合もあるので、最大1時間前まで遡って取得できるようにしてます。
public async Task<IEnumerable<AmedasData>> GetAllAmedasData1hAsync()
{
using var _ = new LoggerScope(this._logger);
var now = DateTime.Now;
for (int i = 0; i < 2; i++)
{
var target = $"{now.AddHours(-1 * i):yyyyMMddHH0000}.json";
var url = $"{AMEDAS_API_URL}{target}";
try
{
this._logger.LogInformation("1時間天気データ({Target})取得開始", target);
this._logger.LogInformation("{Url} アクセス実行", url);
using var res = await this.GetWithRetryAsync(url, HttpCompletionOption.ResponseHeadersRead);
res.EnsureSuccessStatusCode();
this._logger.LogInformation("{Url} アクセス成功", url);
this._logger.LogDebug("{Url} レスポンス取得", url);
var json = await res.Content.ReadAsStringAsync();
this._logger.LogDebug("JSON デシリアライズ");
var amedasDataRaw = JsonSerializer.Deserialize<IDictionary<string, Json.Data.AmedasData>>(json);
var amedasData = amedasDataRaw.MapToAmedasData();
this._logger.LogInformation("1時間天気データ({Target})取得完了", target);
return amedasData;
}
catch (HttpRequestException ex)
{
if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
this._logger.LogWarning("1時間天気データ({Target})未配信のため再取得", target);
continue;
}
this._logger.LogError(ex, "1時間天気データ取得失敗({Url} アクセス失敗)", url);
break;
}
catch (JsonException ex)
{
this._logger.LogError(ex, "1時間天気データ取得失敗(JSON デシリアライズ失敗)");
break;
}
catch (Exception ex)
{
this._logger.LogError(ex, "1時間天気データ取得失敗(不明なエラー)");
break;
}
}
return Enumerable.Empty<AmedasData>();
}
30分間隔以上の頻度でアクセスする場合は、サービス側でキャッシュを返すようにしています。
これも政府が CSV で配信してくれているので、そいつを取ってきてます。
Shift-JIS なのが微妙に面倒で、 UTF-8 に文字コードを変換して扱うようにしています。
当然、ローカルに保存して半年ごとに更新するようにしてます。
初期化
遅延初期化をしています。
スタートアップに登録してますが、とりあえず PC の電源付けて放置……みたいなことをよくやるので、画面ロックが開けるまで初期化を遅らせたりしてます。
Initializer クラスを作って、 IEnumerable<IAsyncInitializable> を DI で突っ込んでもらって、 foreach で初期化メソッドを叩いてるだけです。
DI 登録の時に優先度とか初期化フラグ突っ込んで、ある程度柔軟に制御できるようにしてます。
詳しくは ...Models.Initialization 以下を見てください。
設定によっては起動時にサービスの初期化がスキップされることもあるので、あとから初期化できるようにしてるし、初期化されてない状態でデータ取得メソッド叩かれても問題ないようにしてます。
システムモニター
天気とかは API を叩きに行きますが、画面ロックやらしてるときは見えない(= 取得しても意味ない)ので叩かないようにしてます。
そこら辺を確認するためのロジックです。ネイティブをバチバチに叩いてます。
internal class SystemMonitorService : ISystemMonitorService, IDisposable
{
private const int DEBOUNCE_INTERVAL = 500; // ミリ秒
private readonly ILogger _logger;
private readonly IEventAggregator _eventAggregator;
private bool _disposedValue;
private HwndSource _hwndSource;
private IntPtr _hwnd;
private int _lastPowerMode = -1;
private int _lastSessionChange = -1;
private DateTime _lastPowerModeChangeTime = DateTime.MinValue;
private DateTime _lastSessionChangeTime = DateTime.MinValue;
public SystemMonitorService(ILogger<SystemMonitorService> logger, IEventAggregator eventAggregator)
{
this._logger = logger;
this._eventAggregator = eventAggregator;
}
public void Initialize()
{
var window = System.Windows.Application.Current.MainWindow ?? throw new InvalidOperationException("MainWindow が初期化されていません");
if (SystemMonitor.IsInteractiveSessionActive())
{
this._logger.LogInformation("起動時セッション状態: アクティブ");
this._eventAggregator.GetEvent<SessionActiveEvent>().Publish();
}
else
{
this._logger.LogInformation("起動時セッション状態: 非アクティブ");
}
this._hwnd = new WindowInteropHelper(window).Handle;
this._hwndSource = HwndSource.FromHwnd(this._hwnd);
this._hwndSource?.AddHook(this.WndProc);
WTSRegisterSessionNotification(this._hwnd, NOTIFY_FOR_THIS_SESSION);
}
protected virtual void Dispose(bool disposing)
{
if (!this._disposedValue)
{
if (disposing)
{
if (this._hwnd != IntPtr.Zero)
{
WTSUnRegisterSessionNotification(this._hwnd);
}
if (this._hwndSource is not null)
{
this._hwndSource.RemoveHook(this.WndProc);
this._hwndSource.Dispose();
this._hwndSource = null;
}
}
this._disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch (msg)
{
case WM_POWERBROADCAST:
this.HandlePowerModeChange((int)wParam);
break;
case WM_WTSSESSION_CHANGE:
this.HandleSessionChange((int)wParam);
break;
}
return IntPtr.Zero;
}
private void HandlePowerModeChange(int mode)
{
var now = DateTime.UtcNow;
if (this._lastPowerMode == mode && (now - this._lastPowerModeChangeTime).TotalMilliseconds < DEBOUNCE_INTERVAL)
{
// 同じモードの変更が短時間で連続して発生した場合は無視
this._logger.LogDebug("同じモードの変更が短時間で発生: {Mode}", mode);
return;
}
this._lastPowerMode = mode;
this._lastPowerModeChangeTime = now;
switch (mode)
{
case PBT_APMSUSPEND:
this._eventAggregator.GetEvent<SystemSuspnedEvent>().Publish();
this._logger.LogDebug("システムサスペンド");
break;
case PBT_APMRESUME:
this._eventAggregator.GetEvent<SystemResumeEvent>().Publish();
this._logger.LogDebug("システムレジューム");
break;
}
}
private void HandleSessionChange(int changeType)
{
var now = DateTime.UtcNow;
if (this._lastSessionChange == changeType && (now - this._lastSessionChangeTime).TotalMilliseconds < DEBOUNCE_INTERVAL)
{
// 同じセッション変更が短時間で連続して発生した場合は無視
this._logger.LogDebug("同じセッション変更が短時間で発生: {ChangeType}", changeType);
return;
}
this._lastSessionChange = changeType;
this._lastSessionChangeTime = now;
switch (changeType)
{
case WTS_SESSION_LOGON:
this._eventAggregator.GetEvent<SessionLogonEvent>().Publish();
this._logger.LogDebug("セッションログオン");
break;
case WTS_SESSION_LOGOFF:
this._eventAggregator.GetEvent<SessionLogoffEvent>().Publish();
this._logger.LogDebug("セッションログオフ");
break;
case WTS_SESSION_LOCK:
this._eventAggregator.GetEvent<SessionLockEvent>().Publish();
this._logger.LogDebug("セッションロック");
break;
case WTS_SESSION_UNLOCK:
this._eventAggregator.GetEvent<SessionUnlockEvent>().Publish();
this._logger.LogDebug("セッションアンロック");
break;
}
}
private const int WM_POWERBROADCAST = 0x0218;
private const int PBT_APMSUSPEND = 0x0004;
private const int PBT_APMRESUME = 0x0007;
private const int WM_WTSSESSION_CHANGE = 0x02B1;
private const int WTS_SESSION_LOGON = 0x0005;
private const int WTS_SESSION_LOGOFF = 0x0006;
private const int WTS_SESSION_LOCK = 0x0007;
private const int WTS_SESSION_UNLOCK = 0x0008;
private const int NOTIFY_FOR_THIS_SESSION = 0x0000;
[DllImport("Wtsapi32.dll")]
private static extern IntPtr WTSRegisterSessionNotification(IntPtr hWnd, int dwFlags);
[DllImport("Wtsapi32.dll")]
private static extern bool WTSUnRegisterSessionNotification(IntPtr hWnd);
}
public static class SystemMonitor
{
public static bool IsInteractiveSessionActive()
{
int sessionId = WTSGetActiveConsoleSessionId();
if (sessionId == -1) return false;
return IsActive(sessionId) && TryGetUserName(sessionId, out _) && !IsLocked(sessionId);
}
private static bool IsActive(int sessionId)
{
if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WTS_CONNECT_STATE, out var buffer, out _))
{
var state = Marshal.ReadInt32(buffer);
WTSFreeMemory(buffer);
return state == WTS_ACTIVE;
}
return false;
}
private static bool TryGetUserName(int sessionId, out string result)
{
result = null;
if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WTS_USER_NAME, out var buffer, out _))
{
result = Marshal.PtrToStringAnsi(buffer);
WTSFreeMemory(buffer);
return true;
}
return false;
}
private static bool IsLocked(int sessionId)
{
if (WinStationQueryInformationW(IntPtr.Zero, sessionId, WIN_STATION_LOCK_STATE, out var isLocked, sizeof(int), out _))
{
return isLocked != 0;
}
return false;
}
private const int WTS_USER_NAME = 5;
private const int WTS_CONNECT_STATE = 8;
private const int WTS_ACTIVE = 0;
private const int WIN_STATION_LOCK_STATE = 28;
[DllImport("kernel32.dll")]
private static extern int WTSGetActiveConsoleSessionId();
[DllImport("Wtsapi32.dll")]
private static extern bool WTSQuerySessionInformation(IntPtr hServer, int sessionId, int infoClass, out IntPtr buffer, out uint bytesReturned);
[DllImport("Wtsapi32.dll")]
private static extern void WTSFreeMemory(IntPtr pointer);
// 非推奨 API
// https://learn.microsoft.com/en-us/previous-versions/aa383827(v=vs.85)
// 使えるので使う
[DllImport("winsta.dll", CharSet = CharSet.Unicode)]
private static extern bool WinStationQueryInformationW(IntPtr hServer, int sessionId, int infoClass, out int buffer, int bufferLength, out int returnedLength);
}
普通ならロックからの復帰やらはネイティブ叩かなくても SystemEvents.SessionSwitch とかで取れますが、「ツールウィンドウにしてる」「ShowInTaskbar="False", WindowStyle="None" にしてる」あたりのせいでうまく取れないみたいなのでネイティブ叩いてます。
よくわからんけど二重発火とかするのでデバウンス処理を入れてます。
SystemMonitor.IsInteractiveSessionActive() は起動時に一回だけ実行されますが、起動時点でロック画面かどうかというのはわからん(ロックに入った/復帰とかはイベントでわかる)ので、非推奨 API まで使ってます。(起動時にアクティブなら初期化が走り、そうでないならスキップしてロックやらから復帰したときに走る)
完全にダメになったらどうしようもないですね……
ネットワーク接続ポリシー
設定で機能が切られてたり、ロックやらでネットワーク接続を一時停止してるとかを管理してます。
ネットワーク接続が必要なサービスはみんなこいつを参照して内部で制御してるので、サービスを使う側はネットワーク接続を気にせず叩けます。
ロックやらでアクセスしていいかどうかは SystemMonitorService が発火するイベントで切り替えてます。
internal class NetworkAccessibilityService : INetworkAccessibilityService
{
public const long ACCESSIBLE = 1;
public const long NOT_ACCESSIBLE = 0;
private readonly ILogger _logger;
private long _accessibility = ACCESSIBLE;
public bool IsAccessible => Interlocked.Read(ref this._accessibility) == ACCESSIBLE;
public NetworkAccessibilityService(ILogger<NetworkAccessibilityService> logger, IEventAggregator eventAggregator)
{
this._logger = logger;
eventAggregator.GetEvent<SessionLogonEvent>()
.Subscribe(() => this.SetAccessibility(true, NetworkAccessibilityChangeReason.Logon), ThreadOption.BackgroundThread);
eventAggregator.GetEvent<SessionLogoffEvent>()
.Subscribe(() => this.SetAccessibility(false, NetworkAccessibilityChangeReason.Logoff), ThreadOption.BackgroundThread);
eventAggregator.GetEvent<SessionUnlockEvent>()
.Subscribe(() => this.SetAccessibility(true, NetworkAccessibilityChangeReason.Unlock), ThreadOption.BackgroundThread);
eventAggregator.GetEvent<SessionLockEvent>()
.Subscribe(() => this.SetAccessibility(false, NetworkAccessibilityChangeReason.Lock), ThreadOption.BackgroundThread);
eventAggregator.GetEvent<SystemResumeEvent>()
.Subscribe(() => this.SetAccessibility(true, NetworkAccessibilityChangeReason.SystemResume), ThreadOption.BackgroundThread);
eventAggregator.GetEvent<SystemSuspnedEvent>()
.Subscribe(() => this.SetAccessibility(false, NetworkAccessibilityChangeReason.SystemSuspend), ThreadOption.BackgroundThread);
}
private void SetAccessibility(bool isAccessible, NetworkAccessibilityChangeReason reason = NetworkAccessibilityChangeReason.Unknown)
{
Interlocked.Exchange(ref this._accessibility, isAccessible ? ACCESSIBLE : NOT_ACCESSIBLE);
this._logger.LogInformation("ネットワークアクセス: {State} ({Reason})", isAccessible ? "有効" : "無効", reason);
}
}
観測所一覧とか国民の祝日一覧とか、そうそう変化がないデータはローカルに保存してローカルから読み込むようにしてます。
それを XxxRepository というクラスで制御しています。
動きとしてはこんな感じです。
- API アクセスが必要か判断
- 必要なら API から取得
- 不要ならローカルから取得
ここら辺は全部 XxxRepository 内部で完結してるので、使う側は外部 API からなのかローカルからなのか意識せずに取得できます。
ほとんどのサービスは非同期に動くし、イベント駆動であっちこっちから呼ばれたりするし、内部キャッシュ持ってたりするし、 IAsyncInitializable の初期化とか設定切り替えるたびに走るし、初期化前中後関係なくデータ取得メソッド叩かれるし、なるべくスレッドセーフになるように作りました。
AmedasService とかなかなかなことになってます。(SemaphoreSlim と Interlocked の乱れ打ち)
ログ
最近のイケてる構造化ログが出せるらしい Serilog というのを使ってます。
……いや、ぶっちゃけこの程度のアプリに構造化ログなんか要りません。
Microsoft.Extensions.Logging との連携が手軽にとれるらしいので使いました。(ChatGPT 情報)
ロガーなんてなんでもいいんですが、パッケージに直接依存するの嫌じゃないですか。パッケージ変えたら全部のログメソッド書き換えとか地獄でしょ。
Microsoft.Extensions.Logging を間に挟んでおけば、ログメソッドは Microsoft.Extensions.Logging.ILogger.LogXxx で統一できて、 DI コンテナ登録の時に差し替えるだけで済むんですから使わない手はないでしょう。
LoggerScope
ChatGPT に教えてもらったテクニックです。
「とりあえずメソッドの入口出口にデバッグログ仕込んどくかなー」と思ったとき、至る所で早期リターンとかしてると、やってらんないじゃないですか。「早期リターンしてるところ全部に出口ログ仕込むの……?」って絶望するじゃないですか。
ソースコード見てるとこんなのがいっぱい書いてあります。
using var _ = new LoggerScope(this._logger);
中身はこんな実装になっています。
#if DEBUG
using System;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;
namespace ClockWidget.Logging
{
public struct LoggerScope : IDisposable
{
private readonly ILogger _logger;
private readonly string _methodName;
public LoggerScope(ILogger logger, [CallerMemberName] string methodName = "")
{
this._logger = logger;
this._methodName = methodName;
this._logger.LogDebug("{MethodName} 実行", this._methodName);
}
public void Dispose()
{
this._logger.LogDebug("{MethodName} 終了", this._methodName);
}
}
}
#endif
IDisposable があるので、メソッド脱出の時に勝手に「<メソッド名> 終了」ってログを出してくれるわけです。便利。
でも当然、こんなのデバッグ以外では使わないので、リリースビルドでは中身のないものに差し替えます。
#if !DEBUG
using System;
namespace ClockWidget.Logging
{
public struct LoggerScope : IDisposable
{
public LoggerScope(object _ = null) { }
public void Dispose() { }
}
}
#endif
デバッグの時はメソッドの入口出口で漏れなくログが出せる、リリースの時は空っぽでパフォーマンスに影響しない、なかなかのテクニックですよね。 ChatGPT すごい。
おわりに
いろいろあって、「何かつくる」ということが嫌になってたけど、どうしても欲しくて気が付いたら作ってました。
まあ、満足いくものはできたので、いいかなって感じです。
というか、 ChatGPT に投げたらしっかりしたレビューしてくれるし、 Copilot はちょっとコード書いたら「こういうの書きたいんやろ?」ってめちゃくちゃ補完してくれるし、「AI すげー……!」ってなりまくりましたね。