Open-MeteoのレスポンスをWPFの画面に閉じ込める。それだけの話なのに、タイマーの鼓動やHTTP通信の待機が重なった瞬間、胸の奥がざわつきました。焦った。
そこで dotnetProjectForWindows_5 の WeatherForecastApp を題材に、都市カードの手触りから配布用EXEの重さまで丸ごと書き残します。
最初に実測した東京のレスポンスは1,380ms、次に叩いた大阪は1,354ms。静かだ。けれど60秒ごとの自動更新に載せるには、UIとデータの呼吸を合わせ直さなければいけませんでした。
対象読者
- Open-Meteoのcurrent APIをWPFから叩いてみたいデスクトップ開発者
- タイマーとコマンドパターンで自動更新を組みたい人
- 実行時コンパイルの一部をビルド時に済ませるオプションを有効にした単一EXEを複数アーキテクチャへ配りたい制作者
前提知識
この記事は、以下を理解していると読みやすいです:
- .NET 10 / WPF: Windows向けのデスクトップアプリケーション開発フレームワーク
- MVVMパターン: Model-View-ViewModelの設計パターン(データバインディングでUIとロジックを分離)
- HTTP通信: 非同期でHTTPリクエストを送るための仕組み
- JSON解析: JSONデータを解析するための.NETの機能
初心者の方でも、コード例とデモで理解できるように書きました。
記事に書いてあること
- Open-Meteoのcurrentブロックをどう解析し、emojiアイコン付きのWeatherDataへ落とし込んだか
- MainWindowのローディングオーバーレイやBooleanToToggleTextConverterで状態を伝えるUIの作り方
- タイマーと自動更新間隔(10秒以上)を切り替える時の失敗談とメッセージボックスのフォールバック
- ビルドスクリプトでwin-x64/x86/arm64を同時に吐き出し、65.61MB/60.17MB/61.55MBに収めた手順
作ったもの
WeatherForecastAppは、10都市の気象データを切り替えながら体感温度や雲量をカード表示する気象コックピットです。CityInfoの配列をまるっとカードにして、UIデザイナが手で触れるようにしました。以下のインタラクティブ画面は、大体こんな感じの内容というのを把握するためのデモです。実際は、Windows実行ファイルになる。
CTA: 都市カードをクリックし、緯度経度の差分を体感する
最初に書いたのは「キーを何枚持ち、どう伏せるか」というメモだけでした。けれど実際にデモ化してみると、編集者モードと公開モードの切替だけで安心感が段違いです。
タイマーを走らせる前に、APIの揺らぎを掴む必要がありました。1.38秒。コーヒーを一口飲む隙もありません。助かった。大阪との差はたった26ミリ秒ですが、60秒ごとのFetchに積み重なるとジワジワ効いてきます。
Open-Meteoの呼吸をWPFに渡す
天気予報を取得する処理は、BaseUrlを https://api.open-meteo.com/v1/forecast に固定し、current項目だけを狙い撃ちしています。JSON解析機能を直接触って temperature_2m, pressure_msl, cloud_cover を一つずつ取り出し、天気コードをemojiへ翻訳しました。
実際のコードはこうなっています(WeatherService.cs 64-119行目):
// APIリクエストを送信
string url = $"{BaseUrl}?latitude={city.Latitude}&longitude={city.Longitude}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,pressure_msl,wind_speed_10m,wind_direction_10m,weather_code,cloud_cover,visibility&timezone=Asia/Tokyo";
HttpResponseMessage response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException($"HTTPエラー: {response.StatusCode}");
}
// レスポンスからJSONを取得
string jsonContent = await response.Content.ReadAsStringAsync();
var jsonDoc = JsonDocument.Parse(jsonContent);
// currentブロックを取得
if (!jsonDoc.RootElement.TryGetProperty("current", out var currentElement))
{
throw new InvalidOperationException("APIレスポンスに'current'プロパティが見つかりません");
}
// WeatherDataオブジェクトを作成(詳細は省略)
var weatherData = new WeatherData
{
CityName = city.Name,
Latitude = city.Latitude,
Longitude = city.Longitude
};
// JSONから各値を取得してWeatherDataに設定
weatherData.Temperature = GetDoubleValue(currentElement, "temperature_2m");
weatherData.Visibility = GetDoubleValue(currentElement, "visibility");
int weatherCode = GetIntValue(currentElement, "weather_code");
weatherData.Description = GetWeatherDescription(weatherCode);
weatherData.Icon = GetWeatherIcon(weatherCode);
Json→WeatherDataの変換表をデモ化したら、UIチームとの会話が一気にスムーズになりました。特にVisibilityがメートルで返る点をキロ換算で共有できたのが大きいです。
UIと感情を同期させる
MainWindowでは、IsLoading/HasError/HasWeatherDataをBooleanToVisibilityConverterで透明に切り替えています。オーバーレイの重ね順を守るため、StackPanelやBorderにPanel.ZIndexを意識的に付けました。
実際のXAMLはこうなっています(MainWindow.xaml 184-215行目):
<!-- ローディング表示(オーバーレイ) -->
<Grid Visibility="{Binding IsLoading, Converter={StaticResource BooleanToVisibilityConverter}}"
Panel.ZIndex="1000"
Background="#80000000">
<Border Background="{StaticResource CardBackgroundColor}"
CornerRadius="12"
Padding="40"
Effect="{StaticResource CardShadow}"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<StackPanel>
<TextBlock Text="⏳"
FontSize="48"
HorizontalAlignment="Center"
Margin="0,0,0,16"
Opacity="0.8"/>
<TextBlock Text="天気予報を取得中..."
FontSize="18"
FontWeight="SemiBold"
Foreground="{StaticResource ForegroundColor}"
HorizontalAlignment="Center"
Margin="0,0,0,20"/>
<ProgressBar IsIndeterminate="True"
Width="320"
Height="8"
Background="#E9ECEF"
Foreground="{StaticResource PrimaryColor}"/>
</StackPanel>
</Border>
</Grid>
実機ではオーバーレイの切り替え順を誤って、エラー文がローディングの裏側に潜ってしまいました。失敗。デモで可視化したことで、HasWeatherData=false時はカードのOpacityを0.15まで落とすというルールを全員で共有できました。
タイマーと自動更新のせめぎ合い
ViewModelでは自動更新間隔を10秒以上に抑え、タイマーでTick→LoadWeatherAsyncを呼び出しています。値が変更されたら自動更新を再起動する処理を叩き直し、自動更新切り替えコマンドと真偽値からテキストへの変換器でボタン文言を変えました。
実際のコードはこうなっています(WeatherViewModel.cs 161-284行目):
// 自動更新間隔(秒)のプロパティ
public int AutoRefreshInterval
{
get => _autoRefreshInterval;
set
{
// 10秒以上のみ許可
if (_autoRefreshInterval != value && value >= 10)
{
_autoRefreshInterval = value;
OnPropertyChanged();
// 既に自動更新が有効な場合は再起動
if (AutoRefreshEnabled)
{
RestartAutoRefresh();
}
}
}
}
// 自動更新を開始
private void StartAutoRefresh()
{
StopAutoRefresh();
_autoRefreshTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(AutoRefreshInterval)
};
// Tickイベントで天気予報を読み込む
_autoRefreshTimer.Tick += async (s, e) => await LoadWeatherAsync();
_autoRefreshTimer.Start();
AutoRefreshEnabled = true;
}
インターバルを10秒に下げた瞬間、Open-MeteoがHTTP429を返し始めたので、60秒をデフォルトに据えて UI側で再確認できるよう可視化しました。
タイマーの16ミリ秒ループとAPIレスポンス1.3秒のギャップも、心拍図のように並べてチーム内で共有しました。
ビルドスクリプトで3アーキを一気に吐き出す
配布では dotnet publish を3回呼び、実行時コンパイルの一部をビルド時に済ませるオプションと圧縮をオンにした単一EXEに絞りました。dist には WeatherForecastApp-x64.exe (65.61MB)、-x86 (60.17MB)、-arm64 (61.55MB) が並び、READMEもPowerShellで生成しています。
実際のスクリプトはこうなっています(build-package.ps1 12-39行目):
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishReadyToRun=true -p:EnableCompressionInSingleFile=true -o "$distDir\win-x64"
dotnet publish -c Release -r win-x86 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishReadyToRun=true -p:EnableCompressionInSingleFile=true -o "$distDir\win-x86"
dotnet publish -c Release -r win-arm64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishReadyToRun=true -p:EnableCompressionInSingleFile=true -o "$distDir\win-arm64"
CTA: 気象コックピットのx64単一EXEをダウンロードする
BooleanToToggleTextConverterで人間味を足す
AutoRefreshのボタンは「開始/停止」で切り替わります。WPF付属のBooleanToVisibilityでは足りず、自前でConverterを噛ませました。
実際のコードはこうなっています(BooleanToToggleTextConverter.cs 12-19行目):
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? "停止" : "開始";
}
return "開始";
}
この挙動をチームに伝えるためのコンソールも用意しています。ステータスがtrueになった瞬間、「停止」とRelayCommandのStopAutoRefresh()が同時に点灯する様子が一目瞭然です。
失敗から学んだこと
HTTP 429を踏んだとき、LoadWeatherAsync のcatchでMessageBoxを出すしかありませんでした。HasError を立ててもユーザーは気づかない。だからあえてUIを止め、なぜフォールバックに切り替わったかを説明しました。
もう一つ、DispatcherTimerのTickから await LoadWeatherAsync() を投げっぱなしにしていた頃は、例外がUIスレッドに未ハンドルで飛んでアプリが閉じました。甘かった。RelayCommand経由で例外を握り、Debug.WriteLineへ逃がすよう修正してようやく落ち着きました。
実際に試した例
1. 東京 (35.6762, 139.6503) の呼吸
Invoke-RestMethodで1,380ms。temperature_2m=12.1。DispatcherTimer60秒に載せたら安定して周回できました。
2. 大阪 (34.6937, 135.5023) の揺らぎ
1,354ms。temperature_2m=12.3。AutoRefreshIntervalを45秒に下げた途端429を踏み、60秒に戻して落ち着きました。
3. 札幌 (43.0642, 141.3469) の極端テスト
気温-2.4℃、湿度70%。Visibilityのkm換算を忘れて視程が「12000 km」と表示され、即座に visibility / 1000 へ修正。
使ってみて
実際に試してみてください:
AutoRefreshタイムラインを操作し、Tickログを確認する(全画面表示)
WeatherForecastAppは、Open-Meteoのcurrent APIをWPFで扱う最小構成です。
ポイントは以下の3つ:
- JsonDocumentでcurrentブロックを直接解析し、WeatherDataへ安全にマッピングする
- DispatcherTimerとAutoRefreshInterval(10秒以上)で自動更新を実装する
- build-package.ps1でwin-x64/x86/arm64の単一EXE(65.61/60.17/61.55MB)を量産する
同じような気象データアプリを作っている方の参考になれば嬉しいです。
3アーキ配布マトリクスを再確認する
まとめ
今回は、Open-Meteoのcurrent APIをWPFで扱うWeatherForecastAppを実装しました。
ポイントは以下の4つ:
- Open-Meteoのcurrentレスポンス(約1.3秒)をWeatherDataへ安全にマッピングし、emojiアイコンで感情を伝えた
- MainWindowのBoolean To Visibility構造を整理し、エラーとローディングをレイヤー分けした
- タイマーと真偽値からテキストへの変換器で自動更新を人間味のあるUIに落とし込んだ
- ビルドスクリプトでwin-x64/x86/arm64の単一EXE(65.61/60.17/61.55MB)を量産し、配布準備を自動化した
Open-MeteoのAPIレスポンスは1.3秒程度かかりますが、60秒間隔の自動更新なら十分実用的です。同じような統合を考えている方の参考になれば嬉しいです。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
自分の関連記事:
最後まで読んでくださり、ありがとうございました。1.3秒の待機に耳を澄ませながら、タイマーの鼓動をノートに書き留める夜は案外悪くありません。またどこかで、天気とコードの話をしましょう。