WPFでReactiveProperty入門 ~アプリケーションのステータスやエラー情報をIObservable で通知する
この記事は、WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ の続編です。
関連記事
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (1. 検索バーの実装) - pierre3のブログ
- WPFでReactiveProperty入門 ~ Rxを使って検索結果のサムネイル画像を一括ダウンロードする - pierre3のブログ
(番外編)
- WPFで「UriSouceプロパティに画像のURLを入れてBitmapImageを初期化する処理」を非同期で実行するとしぬ - pierre3のブログ
- [WPF] BitmapImage の生成・初期化を非同期で行う際のメモリの問題について - pierre3のブログ
サンプルプロジェクト github.com
目次 ReactiveBingViewer(この記事のサンプルアプリ)では、アプリケーション内のステータス情報の通知と記録を以下の通りに行います。
Model側で発生したステータスおよびエラー情報(以降まとめてLog情報と呼びます)は、LogMessage オブジェクトに格納されて LogMessage の定義は以下の通りです。 実際にLog情報の通知を行う、 ViewModelでは、LogMessageNotifier を用途に合わせて以下の様に使用します。 エラー通知メッセージのクリアは、専用のReactiveCommand を実装して行います。 ReactiveCollection 内のアイテムをクリアする場合、IObservableから ReactiveCollection への変換を行う また、エラー通知パネルに表示するメッセージが無い場合に、通知パネル自体を非表示とするために、以下の処理を実装しています。 該当部分のXAMLを以下に示します。 今回の記事に関連するソースコードの一覧です。 Notifier による通知と、ReactiveProperty を組み合わせることで、Modelから通知されるLog情報を、目的に合わせて柔軟かつスッキリと記述できるようになったと思います。
アプリケーションのステータスやエラーの情報の通知と記録
アプリケーションの現在の状況(「検索中...」や「検索が完了しました」などのメッセージ)をWindow下部のステータスバーに表示します。
アプリケーション実行中の例外を補足した場合、以下のような通知パネルに例外の内容を表示します。
この通知パネルは、表示する例外メッセージが存在する場合のみ表示されます。右上の[×]ボタンをクリックするとパネル内のメッセージがクリアされてパネル自体も非表示となります。
上記通知を含め、アプリケーション内で発生したイベントはLog情報としてファイルへ記録します。ModelからViewModelへ ~
IObservable<LogMessage>
による通知LogMessageクラス
IObservable<LogMessage>
に乗ってViewModelへ通知されます。public class LogMessage
{
//作成時刻
public DateTime CreatedAt { get; }
//メッセージ
public string Message { get; }
//Logレベル(Trace < Debug < Info < Warn < Error < Fatal )
public LogLevel Level { get; }
//例外オブジェクト
public Exception Exception { get; }
//例外オブジェクトの有無
public bool HasError { get { return Exception != null; } }
public LogMessage(string message, LogLevel level, Exception e = null)
{
CreatedAt = DateTime.Now;
Message = message;
Level = level;
Exception = e;
}
public override string ToString()
{
return (HasError)?
string.Format("{0}:[{1}] {2} [{3}]", Level.Name, CreatedAt.ToString("G"), Message, Exception.Message) :
string.Format("{0}:[{1}] {2}", Level.Name, CreatedAt.ToString("G"), Message);
}
}
LogMessageNotifierクラス
LogMessageNotifier
クラスを定義します
ScheduledNotifier<T>
を継承して、IObservable<T>
による通知を行いますScheduledNotifier<T>
は Reactive.Bingings.Notifiers 名前空間に含まれるReactiveProperty のクラスの1つで、IObservable<T>
による値の通知を行います。ScheduledNotifier<T>
を含む Notifier系クラスの詳細は ReactivePropertyのNotifier系クラス in C# for Visual Studio 2013 を参照ください。
public class LogMessageNotifier : ScheduledNotifier<LogMessage>, ILogger
{
public LogMessageNotifier(IScheduler scheduler) : base(scheduler) { }
public LogMessageNotifier() : base() { }
public void Trace(string message) => Report(LogMessage.CreateTraceLog(message));
public void Debug(string message) => Report(LogMessage.CreateDebugLog(message));
public void Info(string message) => Report(LogMessage.CreateInfoLog(message));
public void Warn(string message) => Report(LogMessage.CreateWarnLog(message));
public void Error(string message) => Report(LogMessage.CreateErrorLog(message));
public void Fatal(string message) => Report(LogMessage.CreateFatalLog(message));
public void Trace(string message, Exception e) => Report(LogMessage.CreateTraceLog(message, e));
public void Debug(string message, Exception e) => Report(LogMessage.CreateDebugLog(message, e));
public void Info(string message, Exception e) => Report(LogMessage.CreateInfoLog(message, e));
public void Warn(string message, Exception e) => Report(LogMessage.CreateWarnLog(message, e));
public void Error(string message, Exception e) => Report(LogMessage.CreateErrorLog(message, e));
public void Fatal(string message, Exception e) => Report(LogMessage.CreateFatalLog(message, e));}
}
public interface ILogger
{
void Trace(string message);
void Debug(string message);
void Info(string message);
void Warn(string message);
void Error(string message);
void Fatal(string message);
void Trace(string message,Exception e);
void Debug(string message, Exception e);
void Info(string message, Exception e);
void Warn(string message, Exception e);
void Error(string message, Exception e);
void Fatal(string message, Exception e);
}
public class MainWindowViewModel : IDisposable
{
private LogMessageNotifier logger = new LogMessageNotifier();
private WebImageStore webImageStore;
//...
public MainWindowViewModel()
{
//ModelはILoggerインターフェースを受け付ける
webImageStore = new WebImageStore(logger);
}
}
ViewModelからViewへ ~
IObservable<LogMessage>
をReactiveProperty に変換
LogMessageNotifier を、 LogLevel.Info のメッセージのみを通すReactiveProperty に変換します
LogMessageNotifier を、 LogLevel.Warn 以上のメッセージを保持する ReactiveCollection に変換します
LogMessageNotifier を購読(Subscribe) して、OnNext()でLogファイルへの書き込みを行いますpublic class MainWindowViewModel : IDisposable
{
private CompositeDisposable disposables = new CompositeDisposable();
private LogMessageNotifier logger = new LogMessageNotifier();
private WebImageStore webImageStore;
// ステータスバー・メッセージ
public ReadOnlyReactiveProperty<LogMessage> StatusMessage { get; private set; }
// エラー・メッセージ
public ReadOnlyReactiveCollection<LogMessage> ErrorLogs { get; private set; }
// エラー表示状態
public ReactiveProperty<Visibility> ErrorLogsVisibility { get; private set; }
// エラー表示クリア用コマンド
public ReactiveCommand ClearErrorLogsCommand { get; private set; }
public MainWindowViewModel()
{
//WebImageStoreのコンストラクタにLogMessageNotifierのインスタンスを渡す
webImageStore = new WebImageStore(logger);
//Logファイルの設定。今回はTextWriterTraceListenerで書き込む
System.Diagnostics.Trace.Listeners.Add(
new System.Diagnostics.TextWriterTraceListener("log.txt"));
//Logファイルに書き込み
logger.Subscribe(log =>
{
System.Diagnostics.Trace.WriteLine(log);
System.Diagnostics.Trace.Flush();
}).AddTo(disposables);
//Infoレベルのログはステータスバーに表示
StatusMessage = logger.Where(x => x.Level == LogLevel.Info)
.Select(x => x.Message)
.ToReadOnlyReactiveProperty();
//エラー通知クリア用コマンド
ClearErrorLogsCommand = new ReactiveCommand();
//Warn レベル以上のLogメッセージをReactiveCollectionに格納する
ErrorLogs = logger
.Where(x => x.Level >= LogLevel.Warn)
.ToReadOnlyReactiveCollection(ClearErrorLogsCommand.ToUnit());
//エラーリスト表示切替。 ErrorLogs にアイテムがある場合のみ表示する
ErrorLogsVisibility = ErrorLogs
.CollectionChangedAsObservable()
.Select(_ => (ErrorLogs.Count > 0) ? Visibility.Visible : Visibility.Collapsed)
.ToReactiveProperty(Visibility.Collapsed);
}
}
エラー通知パネルのクリアと表示切替
ToReactiveCollection()
または ToReadonlyReactiveCollection()
の引数に ReactiveCommand.ToUnit()
を渡すだけでクリア処理を実装することが出来ます。//エラー通知クリア用コマンド
ClearErrorLogsCommand = new ReactiveCommand();
//Warn レベル以上のLogメッセージをReactiveCollectionに格納する
ErrorLogs = logger
.Where(x => x.Level >= LogLevel.Warn)
.ToReadOnlyReactiveCollection(ClearErrorLogsCommand.ToUnit());
//エラーリスト表示切替。 ErrorLogs にアイテムがある場合のみ表示する
ErrorLogsVisibility = ErrorLogs
.CollectionChangedAsObservable()
.Select(_ => (ErrorLogs.Count > 0) ? Visibility.Visible : Visibility.Collapsed)
.ToReactiveProperty(Visibility.Collapsed);
View の定義
<!-- ステータスバー -->
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem DockPanel.Dock="Right">
<!-- プログレスパー (省略) -->
</StatusBarItem>
<Separator DockPanel.Dock="Right" />
<StatusBarItem>
<!-- ステータスメッセージ -->
<TextBlock Text="{Binding StatusMessage.Value,Mode=OneWay}"/>
</StatusBarItem>
</StatusBar>
<!-- エラー通知 -->
<Grid DockPanel.Dock="Bottom" Margin="4" Visibility="{Binding ErrorLogsVisibility.Value}">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Rectangle Grid.ColumnSpan="2" Stroke="Chocolate" Fill="LightYellow"/>
<!-- エラーメッセージ一覧 -->
<ListBox Grid.Row="3" ItemsSource="{Binding ErrorLogs}" MaxHeight="120" Margin="0,0,4,0"
Background="Transparent" BorderBrush="{x:Null}"/>
<Button Grid.Column="1" Margin="2" VerticalAlignment="Top" HorizontalAlignment="Right"
Command="{Binding ClearErrorLogsCommand}" Background="Transparent"
FontFamily="Segoe UI Symbol"></Button>
</Grid>
ソースコード
処理の詳細は、こちらをご参照ください。
まとめ
[WPF] BitmapImage の生成・初期化を非同期で行う際のメモリの問題について
この記事は、前回のエントリ WPFで「UriSouceプロパティに画像のURLを入れてBitmapImageを初期化する処理」を非同期で実行するとしぬ - pierre3のブログ
の続きになります。
目次
前回は、Web画像のURLからBitmapImageを、UIをブロックする事無く生成するには、以下の様に記述すると良さそう。 しかし、このコードにはメモリリークを引き起こす問題が2つあります。 上のコードでは、EndInit()での初期化が終わった時点でStreamSourceプロパティに設定したMemoryStreamは不要になり、using のスコープを抜けるとDispose()されます。 しかし、MemoryStream では、コンストラクタに渡した byte配列の参照を内部バッファとして保持しているのですが、それをDispose()後も抱えたまま離さないようなのです。
※ Dispose()後も_buffer が残ったまま! 画像データとしては、Bitmapにデコードされた表示用の画素データと、デコード前のbyte配列(内部バッファ)の両方を持つことになります。 対処法は、以下サイトにありました。 MemoryStreamをラップしたStreamクラスを作って Dispose時にMemoryStreamの参照を外すという方法でこの問題を回避するようです。 以下のようなStreamのラップクラスを用意して MemoryStream をこれでラップしたうえで StreamSourceプロパティに設定します。 以下、サムネイル画像50枚分のBitmapImageを作成した状態のスナップショットでヒープを比較したものです。効果は一目瞭然だと思います。 ・MemoryStreamのみの場合
・WrappingStreamを使用した場合
これで問題の1つは解決できました。 そして、もう1つの問題が以下になります。 この問題については、以下のエントリに詳しいので、そちらをご参照ください。 上記エントリで記述されている通り、 最も時間のかかるダウンロード部分のみバックグラウンドで非同期実行して、ダウンロードした結果を基にBitmapImageを生成・初期化する部分はUIスレッド上で行う様にします。 まずは、画像データのダウンロード部分。 (2015/10/26 訂正) 上記メソッドで取得したbyte配列を受け取って、MemoryStream を作ります。 上の2つのメソッドを組み合わせて呼び出します。 Rxを使って一度にダウンロードする場合は、前回のおさらい
というところまで書きました。public async Task<BitmapImage> DownloadImageAsync(string url)
{
using (var web = new System.Net.Http.HttpClient())
{
//HttpClientでbyte配列に先読みしておいて
var bytes = await web.GetByteArrayAsync(url).ConfigureAwait(false);
// そのbyte配列でMemoryStream を作成し、StreamSourceプロパティに設定
using (var stream = new MemoryStream(bytes))
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
return bitmap;
}
}
}
まずは、そのうちの1つからMemoryStream はDisposeしても内部バッファを離さない
この状態のBitmapImageは、単純計算で必要な量の2倍のメモリ空間を占有しているってことです。
特に大きな画像を読み込んだ場合は、なかなか無視できない容量になるのではないでしょうか。Dispose時にMemoryStreamの参照を外すストリームクラスでラップする
/// <summary>
/// ストリームのWrapperクラス
/// </summary>
/// <remarks>
/// Dispose 時に、内部ストリームの参照を外します
/// </remarks>
public class WrappingStream : Stream
{
Stream m_streamBase;
public WrappingStream(Stream streamBase)
{
if (streamBase == null)
{
throw new ArgumentNullException("streamBase");
}
m_streamBase = streamBase; //渡したStreamを内部ストリームとして保持
}
//Streamクラスのメソッドをオーバーライドして、内部ストリームの同じメソッドをそのまま呼ぶだけ
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken)
{
ThrowIfDisposed();
return m_streamBase.ReadAsync(buffer, offset, count, cancellationToken);
}
public new Task<int> ReadAsync(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
return m_streamBase.ReadAsync(buffer, offset, count);
}
//...(中略)...
protected override void Dispose(bool disposing)
{
if (disposing)
{
m_streamBase.Dispose();
m_streamBase = null; //disposeしたら内部ストリームをnullにして参照を外す
}
base.Dispose(disposing);
}
private void ThrowIfDisposed()
{
if (m_streamBase == null)
{
throw new ObjectDisposedException(GetType().Name);
}
}
}
public async Task<BitmapImage> DownloadImageAsync(string url)
{
using (var web = new HttpClient())
{
var bytes = await web.GetByteArrayAsync(url).ConfigureAwait(false);
using (var stream = new WrappingStream(new MemoryStream(bytes)))
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
return bitmap;
}
}
}
DispatcherObjectをバックグラウンドスレッドで作成するとメモリリークする
BeginInvokeShutdown()
と Dispatcher.Run
を使って回避する方法もあるようなのですが、
今回はバックグラウンドスレッド上でBItmapImageを生成しないようにすることで、対処したいと思います。ダウンロード処理とBitmapImage生成処理を分離する
画像データを非同期ダウンロードしてbyte配列で返す
System.Net.Http.HttpClient
の GetByteArrayAsync() の結果をそのまま返すだけです。
System.Net.Http.HttpClient
の GetByteArrayAsync() を await してその結果を返します。// このままではDownload完了前に HttpClient がDispose()されてしまう
//public static Task<byte[]> DownLoadImageBytesAsync(string url)
//{
// using (var web = new HttpClient())
// {
// return web.GetByteArrayAsync(url);
// }
//}
public static async Task<byte[]> DownLoadImageBytesAsync(string url)
{
using (var web = new HttpClient())
{
return await web.GetByteArrayAsync(url);
}
}
受け取ったbyte配列からBitmapImageを作成する
さらに、WrappingStreamにラップしたものをStreamSourceプロパティに設定してBitmapImageを初期化します。public static BitmapImage CreateBitmap(byte[] bytes, bool freezing = true)
{
using (var stream = new WrappingStream(new MemoryStream(bytes)))
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
if (freezing && bitmap.CanFreeze)
{ bitmap.Freeze(); }
return bitmap;
}
}
DownLoadImageBytesAsync()をバックグラウンドで実行後、指定したディスパッチャ上でCreateBitmap()を実行する
CreateBitmap() を実行するDispatcher を引数で指定できるようにしています。
今回はUIスレッド上で処理を行いたいので、App.Current.Dispatcher
を指定します。public static async Task<BitmapImage> DownloadImageAsync(string url,
Dispatcher dispatcherForBitmapCreation)
{
var bytes = await DownLoadImageBytesAsync(url).ConfigureAwait(false);
return await dispatcherForBitmapCreation.InvokeAsync(() => CreateBitmap(bytes));
}
// 使用例
// var bitmap = await DownloadImageAsync(downloadUrl, App.Current.Dispatcher);
おまけ
DownloadImageAsync()
にまとめず、個別に実行する方が分かりやすいかもしれません。private ObservableCollection imageSource;
public void DownloadSearchResult(IEnumerable<Uri> imageUrlList)
{
//IEnumerable<T>を、スレッドプール上で動作するIObservable<T> に変換
disposable = imageUrlList.ToObservable(ThreadPoolScheduler.Instance)
//画像データをまとめてダウンロード
.SelectMany(async uri => await DownLoadImageBytesAsync(uri))
//UIスレッドに処理を移す
.ObserveOn(Reactive.Bindings.UIDispatcherScheduler.Default)
//BitmapImageを作成してコレクションに追加
.Subscribe( bytes => imagesSource.Add(CreateBitmap(bytes));
}
//ここで使用しているReactive.Bindings.UIDispatcherScheduler は
//ReactiveProperty が用意するクラスです。App.xaml.cs でAppクラスのOnStartup()あたりで
// Reactive.Bindings.UIDispatcherScheduler.Initialize();
//としておくと、UIDispatcherScheduler.Default で UIスレッドのDispatcherが取得できます
WPFで「UriSouceプロパティに画像のURLを入れてBitmapImageを初期化する処理」を非同期で実行するとしぬ
この記事は、WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ の続きです。
関連記事
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (1. 検索バーの実装) - pierre3のブログ
- WPFでReactiveProperty入門 ~ Rxを使って検索結果のサムネイル画像を一括ダウンロードする - pierre3のブログ
サンプルプロジェクト github.com
目次
今回は、検索結果として取得した画像のURLから画像データを読み込んでBitmapImageを生成する部分について書くのですが、
先に言ってしまうと、試してみたけど結果うまく行きませんでした。というお話です。 BitmapImageにWeb上の画像を読み込ませるには、UriSourceプロパティを使用するのが最も簡単です。 非常に簡単で便利なのですが、この処理はUIをブロックするようなのです。 そこで、Taskにツッコんで別スレッドで実行するようにしてみます。 しかし、上記コードではImageコントロール等で表示しようとした時点で以下のエラーが発生します。 型 'System.Windows.Markup.XamlParseException' のハンドルされていない例外が PresentationFramework.dll で発生しました UIスレッド以外で生成されたUIオブジェクトはFreeze()してからでないとUIスレッドで使用できないのでした。 UIで使用する前に Freeze しておく必要があるのですが、そのタイミングも問題になります。 ダウンロードの完了を待ってからFreezeすればどうでしょうか? 試してみます。 これでうまく行きそうな気がしますが、今度はまた別の問題が発生します。 しかも、例外で落ちることもなく沈黙する最悪のパターンです。 何か手掛かりは無いか、VisualStudio(2015)の診断ツールで「イベント」を確認すると、何やら謎の例外がスローされて直後にキャッチされているのが分かります。 どうやら、 この EndInig()メソッドの内部で 色々試してみましたが、今回のようなアプローチの仕方ではうまく行きそうにありません。 「UriSource を使った一連の処理は、UIスレッド以外で実行しないようにしましょう。」 では、非同期で処理できないの?というわけではなく、別の方法があります。 以下の様に、画像データを先にダウンロードしておき、MemoryStream でBitmapImageに流してやればOKです。 Web検索等で調べると、このようなやり方が一般的のようですね。 しかし、これにもまだ問題があります。
上のはじめに
BitmapImageの初期化にUriSourceを使うとUIがブロックされる
UriSourceプロパティに画像のURLを指定するだけで、画像データのダウンロードとBitmapへのデコードをBitmapImage自身がやってくれます。public BitmapImage CreateBitmapImage(Uri sourceUri)
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = sourceUri;
bitmap.EndInit();
return bitmap;
}
// 以下の様にコンストラクタにURIを渡すのと同じ
// var bitmap = new BitmapImage(sourceUri);
実際に、上記のコードを使って複数の画像を連続してダウンロードすると、すべての画像が生成されるまでUIの表示が更新されず、操作もできない状態になります。
(ダウンロード完了を待たずにEndInit()を抜けて、DownloadCompletedイベントで完了通知を受け取るようになっているので、ダウンロード自体はバックグラウンドで行われているように見えるのですが)非同期にしてみる
public Task<BitmapImage> CreateBitmapImageAsync(Uri sourceUri)
{
return Task.Run(()=> {
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = sourceUri;
bitmap.EndInit();
return bitmap;
}
}
DependencySource は、DependencyObject と同じ Thread 上で作成する必要があります。
例えば、以下の様にEndInit() の後に Freeze しようとしても、この時点では画像のダウンロードが完了していないので、Freezeできません。public Task<BitmapImage> CreateBitmapImageAsync(Uri sourceUri)
{
return Task.Run(()=> {
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = sourceUri;
bitmap.EndInit();
bitmap.Freeze(); //初期化が終わっていないのでFreezeできません!!
return bitmap;
}
}
ダウンロード完了を待ってからFreezeする
ダウンロードの完了は DownloadCompleted イベントで受け取れます。
また、ダウンロード中に発生した例外は、DownloadFailed イベントで補足できます。private ObservableCollection images = new ObservableCollection();
private Task CreateBitmapImage(Uri sourceUri)
{
return Task.Run(()=> {
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = sourceUri;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.DownloadCompleted += (sender,args) => {
var image =(BitmapImage)sender;
image.Freeze(); //Freezeしてから
images.Add(image); //Binding Sourceのコレクションに追加
};
bitmap.DownloadFailed += (sender,args) => {
//失敗したらここに来る(はず)
Trace.WriteLine($"DL Failed. [{args.ErrorException}]");
};
bitmap.EndInit();
}
}
UIスレッド以外で実行すると謎の沈黙
System.Deployment.Application.InvalidDeploymentException
なる例外が発生して、尚且つそれが握りつぶされているのが原因のようです。InvalidDeploymentException
ってなんなんでしょう? 例外発生時の呼び出し履歴を見てみます。System.Deployment.dll!System.Deployment.Application.ApplicationDeployment.CurrentDeployment.get()
System.Deployment.dll!System.Deployment.Application.ApplicationDeployment.IsNetworkDeployed.get()
PresentationCore.dll!MS.Internal.AppModel.SiteOfOriginContainer.SiteOfOriginForClickOnceApp.get()
PresentationCore.dll!MS.Internal.SecurityHelper.ExtractUriForClickOnceDeployedApp()
PresentationCore.dll!MS.Internal.SecurityHelper.BlockCrossDomainForHttpsApps(System.Uri uri = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.BitmapDownload.BeginDownload(System.Windows.Media.Imaging.BitmapDecoder decoder = {不明}, System.Uri uri = {不明}, System.Net.Cache.RequestCachePolicy uriCachePolicy = {不明}, System.IO.Stream stream = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.LateBoundBitmapDecoder..ctor(System.Uri baseUri = {不明}, System.Uri uri = {不明}, System.IO.Stream stream = {不明}, System.Windows.Media.Imaging.BitmapCreateOptions createOptions = {不明}, System.Windows.Media.Imaging.BitmapCacheOption cacheOption = {不明}, System.Net.Cache.RequestCachePolicy requestCachePolicy = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.BitmapDecoder.CreateFromUriOrStream(System.Uri baseUri = {不明}, System.Uri uri = {不明}, System.IO.Stream stream = {不明}, System.Windows.Media.Imaging.BitmapCreateOptions createOptions = {不明}, System.Windows.Media.Imaging.BitmapCacheOption cacheOption = {不明}, System.Net.Cache.RequestCachePolicy uriCachePolicy = {不明}, bool insertInDecoderCache = {不明})
PresentationCore.dll!System.Windows.Media.Imaging.BitmapImage.FinalizeCreation()
PresentationCore.dll!System.Windows.Media.Imaging.BitmapImage.EndInit()
LateBoundBitmapDecoder
が生成されて、そのコンストラクタ内でBitmapDownload.BeginDownload()
が実行され、 SecurityHelper.BlockCrossDomainForHttpsApps()
メソッド内でClickOnce Deployed がなんちゃら・・・・
うーん、よくわかりません。
これ以上の深追いはやめておきます。UriSourceを使ったBitmapImageの初期化はUIスレッドで
別のアプローチ
public async Task<BitmapImage> DownloadImageAsync(string url)
{
using (var web = new HttpClient())
{
var bytes = await web.GetByteArrayAsync(url).ConfigureAwait(false);
using (var stream = new MemoryStream(bytes))
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
return bitmap;
}
}
}
(だったら最初からそうしろってことですが、結果がダメでもいろいろ試してみることに意義があるという事で)DownloadImageAsync()
を実行するとメモリリークを引き起こすのですが、長くなってきたので続きは次回!
WPFでReactiveProperty入門 ~ Rxを使って検索結果のサムネイル画像を一括ダウンロードする
この記事は、WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ の続きです。
関連記事
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (1. 検索バーの実装) - pierre3のブログ
サンプルプロジェクト
今回は、Bing画像検索を実行し、検索結果からサムネイル画像を取得して一覧表示する処理について、Model側に焦点を当てて見てみたいと思います。
目次
今回は、Model側の処理がメインになりますので、Model側の主要クラスについて簡単にまとめておきます。 Bing Search API をAzure Market Placeから購入(無料含む)すると、AzureポータルからBing Searchのプロキシクラス(BingSearchContainer.csファイル)をダウンロードして使用することが出来ます。 今回はこれをそのまま使用させて頂きました。 このプロキシクラスは、WCFデータサービスの仕組みを利用しており、 画像検索の実行には、Image() メソッドを使用します。 引数で検索条件等を色々指定できるのですが、とりあえずは第1引数に検索ワードを入れたら後は ※ パラメータの詳細は、下記リンクの"Optional Parameters" の項目で確認できます。
https://msdn.microsoft.com/en-us/library/dd250942.aspx
画像検索結果が格納される 上記のような非同期操作の実装はいくつか考えられますが、今回はReactive Extensionsを使用した方法で実装を試してみたいと思います。 IE 今回のサンプルでは、検索ワードを渡すとBing画像検索を実行して結果を ここでは、 検索実行中に発生した例外は、そのままでは 上記の例では、検索実行中に発生した例外は Rxで流れてきた検索結果オブジェクト ここでは、 Rxの中で非同期メソッドを実行するには、 こうすることで、 この一連の処理を 以下に、実装例を示します。(実際には進捗通知等の処理が入り、もう少し複雑になっていますが、ここでは説明のため簡略化しています) これで、検索を実行して、結果をサムネイルの一覧で表示するための枠組みが出来ました。 Modelの概要
クラス
説明
Bing.BingSearchContainer
Microsoftが提供するBing Search APIのプロキシクラス
Bing.ImageResult
Bing Search APIが返す画像検索結果のクラス。検索結果の画像1枚の情報が格納されている
WebImage
検索結果の画像1枚を表すクラス。Bing.ImageResultに格納されている画像URLから画像データをダウンロード後、BitmapImageとして保持する
WebImageStore
WebImageのコレクションを管理するクラス。画像検索を実行し、検索結果からWebImageを生成してコレクションに保持する
WebImageHelper
WebImage、WebImageStoreクラスを補助するメソッドを提供するクラス
Bing Search API プロキシクラス
BingSearchContainer
System.Data.Services.Client.DataServiceContext
を継承したBingSearchContainer
クラスを用いて検索を行います。null
でOKです。//Bing Image検索プロキシクラス
public class BingSearchContainer : System.Data.Services.Client.DataServiceContext
{
//画像検索用のクエリを発行
public DataServiceQuery<ImageResult> Image(String Query, String Options,
String Market, String Adult, Double? Latitude, Double? Longitude, String ImageFilters)
{
DataServiceQuery<ImageResult> query;
query = base.CreateQuery<ImageResult>("Image");
//...(中略)...
return query;
}
}
Image()
メソッドは、画像検索用クエリを表す DataServiceQuery<ImageResult>
を返却します。
DataServiceQuery<ImageResult>
のExecute()
メソッドでクエリを実行すると、検索結果がIEnumerable<ImageResult>
のかたちで返却されます。var bing = new Bing.BingSearchContainer(new Uri("https://api.datamarket.azure.com/Bing/search/"));
bing.Credentials = new NetworkCredential("accountKey", accountKey);
//画像検索用のクエリ(DataServiceQuery<Bing.ImageResult>)を発行
var query = bing.Image(searchWord, null, null, null, null, null, null);
//検索実行
IEnumerable<T> result = query.Execute();
ImageResult
ImageResult
クラスは以下のプロパティを持ちます//画像検索の結果を格納するクラス
public class ImageResult
{
public Guid ID { get; set; }
public String Title { get; set; }
public String MediaUrl { get; set; }
public String SourceUrl { get; set; }
public String DisplayUrl { get; set; }
public Int32? Width { get; set; }
public Int32? Height { get; set; }
public Int64? FileSize { get; set; }
public String ContentType { get; set; }
public Thumbnail Thumbnail { get; set; }
}
public class Thumbnail
{
public String MediaUrl { get; set; }
public String ContentType { get; set; }
public Int32? Width { get; set; }
public Int32? Height { get; set; }
public Int64? FileSize { get; set; }
}
MediaUrl
に画像データをダウンロードするためのURLが格納されています。
また、画像サイズを縮小したサムネイル用の画像データを取得することが可能で、その場合はThumbnail
プロパティのMediaUrl
を使用します。検索結果のシーケンスからサムネイル画像をまとめて非同期ダウンロードする。
IEnumerable<ImageResult>
として返される全ての検索結果(最大50件)のサムネイル画像を一度にダウンロードするのですが、(当然ながら)UIをブロックしないよう非同期で行う必要があります。Reactive Extensions で非同期ダウンロード
IEnumerable<ImageResult>
から IObservable<ImageResult>
に変換IObservable<ImageResult>
で返すヘルパーメソッドを WebImageHelper
クラスに用意しています。DataServiceQuery<ImageResult>
をExecute()して得られるIEnumerable<ImageResult>
を ToObservable()
で IObserbable<ImageResult>
に変換たものをreturn しています。static class WebImageHelper
{
//Bing Image 検索を実行して 結果を IObservable<T> で返す
public static IObservable<Bing.ImageResult> SearchImageAsObservable(
string searchWord, string accountKey, int skip, int top)
{
var bing = new Bing.BingSearchContainer(
new Uri("https://api.datamarket.azure.com/Bing/search/"));
bing.Credentials = new NetworkCredential("accountKey", accountKey);
var query = bing.Image(searchWord, null, null, null, null, null, null);
query = query.AddQueryOption("$skip", skip);
query = query.AddQueryOption("$top", top);
try
{
IEnumerable<T> result = query.Execute();
return result.ToObservable();
}
catch (Exception e)
{
return Observable.Throw<Bing.ImageResult>(e);
}
}
}
例外を
IObserbable<T>
に乗せるには Observable.Throw<T>()
IO<T>
の一連の流れの中では補足できませんが、
Observable.Throw<T>()
メソッドを使用することで、例外をIObservable<T>
として扱うことが可能になります。IObservable<ImageResult>
で包まれて、購読側のOnError()
メソッドで補足できるようになります。SelectMeny()で非同期メソッドを実行する
ImageResult
を基にサムネイル画像のダウンロードを行う処理は、以下のメソッドを用いて行います。ImageResult
オブジェクトを引数にWebImage
のインスタンスを生成後そのDownLoadThumbnailAsync()
メソッドでサムネイル画像のダウンロードを非同期に行います。戻り値はTask<WebImage>
となります。public class WebImageStore : IDisposable
{
//検索結果オブジェクト(ImageResult)でWebImageのインスタンスを生成後、
//ImageResultに格納されているサムネイル画像のURLから画像データをダウンロードする
private async Task<WebImage> CreateWebImageAsync(Bing.ImageResult bingResult)
{
var image = new WebImage(bingResult, logger);
await image.DownLoadThumbnailAsync();
return image;
}
}
SelectMeny()
を使用します。
SelectMeny()
に渡すデリゲートをasync
にして、その中で await CreateWebImageAsync()
とします。CreateWebImageAsync()
メソッドは非同期かつ並列的に実行されます。実行の結果は、完了した順に後続へ渡ります。Subscribe()
すると、OnNext()
にサムネイルのダウンロードが完了したWebImage
オブジェクトが渡ってくるので、これを ObservableCollection に逐次追加して行きます。public class WebImageStore : IDisposable
{
private IDisposable disposable;
private ObservableCollection<WebImage> imagesSource;
public ReadOnlyObservableCollection<WebImage> readonlyImages;
public IReadOnlyObservableCollection<WebImage> Images {get {return readonlyImages;}}
public void DownloadSearchResult(string searchWord)
{
disposable = WebImageHelper.SearchImageAsObservable(searchWord, bingAccountKey)
.SelectMany(async bingResult => await CreateWebImageAsync(bingResult))
.Where(webImage => webImage?.Thumbnail != null)
.Subscribe(
onNext: webImage =>
{
//ObservableCollection に追加
imagesSource.Add(webImage);
},
onError: e =>
{
//SearchImageAsObservable()内でObservable.Throw()した例外はここで補足
logger.Error("画像の検索に失敗しました。", e);
},
onCompleted: () =>
{
logger.Info("検索が完了しました。");
});
}
}
まとめ
次回は、URLからサムネイルの画像データをダウンロードする処理と、そのデータを元にBitmapImage
オブジェクトを生成する処理の詳細について書きたいと思います。
(補足) Roslyn で C#のソースコードからPlantUMLのクラス図を生成する の設計メモ
前回の記事の補足資料です。
目次
使い方
前回、PlantUmlClassDiagramGenerator の使い方を書くのを忘れていましたので載せておきます。
PlantUmlClassDiagramGenerator はコンソールアプリケーションです。以下のパラメータを受け付けます。
PlantUmlClassDiagramGenerator.exe SOURCE_PATH [DEST_DIR]
- 第1引数: SOURCE_PATH
- 変換元のソースファイル名または、ソースファイルを格納したフォルダ名を指定します。
- フォルダを指定した場合は、直下にある .cs ファイルをすべて読み込みます。(フォルダの階層は辿りません)
- 第2引数: DEST_DIR
- 結果のPlantUMLファイルの出力先を指定します。省略可能です。
- 省略した場合は、入力ファイルと同じ階層に"uml" というフォルダを作成してそこに出力します。
C:\ > PlantUmlClassDiagramGenerator.exe SOURCE_PATH DEST_DIR
設計メモ
C#からPlantUMLのクラス図に変換する処理の設計メモです。
- UMLに関する知識不足等で、不適切な部分があるかもしれません。 お気づきの点などありましたらご指摘頂けると嬉しいです。
型定義
インターフェース、クラス、構造体などの型定義に関する仕様です。
C#のキーワードとPlantUMLでの記述との対応付けを表にしてみました。
型キーワード
PlantUMLでは、interface
, class
, abstract class
, enum
が使用可能です。
C# | PlantUML | Memo |
---|---|---|
class |
class |
|
struct |
<<struct>> class |
PlantUMLにはstruct で定義できる型が無いので、class にステレオタイプ<<struct>> を付加することで構造体を表現 |
interface |
interface |
|
abstract class |
abstract class |
抽象クラスはabstract class で宣言可能 |
enum |
enum |
修飾子
修飾子は、(基本的には)ステレオタイプで表現します。
C# | PlantUML | Memo |
---|---|---|
abstract |
abstract |
‘abstract‘ は、‘class‘ キーワードと組み合わせて抽象クラスの宣言時に使用する |
static |
<<static>> |
|
partial |
<<partial>> |
|
sealed |
<<sealed>> |
- 型のアクセス修飾子について
UMLに型自体のアクセス修飾子に関する規定がない(?)ため、型に対するアクセス修飾子は無視するようにしました。
必要な場合は、<<internal>> class ClassA
の様にステレオタイプを付加する?
型引数
ジェネリックの型引数は、PlantUMLでもC#と同じように書けます
class GenericsType<string,int> { }
例
クラス定義の変換例です。
//csharp sealed class ClassA{ } abstract class AbstractClass{ } static class StaticClass{ } struct Structure{ } enum EnumType{ }
'plantuml @startuml class ClassA <<sealed>> abstract class AbstractClass class StaticClass <<static>> class Structure <<struct>> enum EnumType class GenericsType<string,int> @enduml
メンバー定義
フィールド、プロパティ、メソッド、Enum定数 など型のメンバー定義に関する仕様です。
アクセス修飾子(共通)
C# | PlantUML | Comment |
---|---|---|
public |
+ |
|
internal |
<<internal>> |
~ (package) が意味合い的に近いと思うが、 protected internal と合わせてステレオタイプにした |
protected internal |
# <<internal>> |
#~ はPlantUMLではエラーになるので internal をステレオタイプで表現することに |
protected |
# |
|
private |
- |
修飾子 (共通)
C# | PlantUML | Comment |
---|---|---|
abstract |
{abstract} |
ステレオタイプではなく{} で括る。イタリック体の表記になる |
static |
{static} |
ステレオタイプではなく{} で括る。下線付きの文字で表現される |
virtual |
<<virtual>> |
|
override |
<<override>> |
|
readonly |
<<readonly>> |
プロパティ
プロパティの表現は悩みどころですが、変換のしやすさを優先して以下の様に出力するようにしました。
- C#のコードに記述されているアクセサー(get,set)をそのままステレオタイプとして出力
//csharp public int PropA {get; set;} public int PropB {get;} public int PropC {get; protected set;}
'prantuml + PropA : int <<get>> <<set>> + PropB : int <<get>> + PropC : int <<get>> <<protected set>>
- 別候補
他にも以下のように変換することも考えたのですが
(1) は protected set
等、アクセサーごとにアクセス修飾子が付けられた際の表現が難しく、
(2) は 変換処理が少し複雑になるのと、もはやプロパティではなくなるので却下しました。
'prantuml '(1) <<property>> ステレオタイプを付け、getterのみの場合はreadonlyの制約を付ける + PropA : int <<property>> + PropB : int <<property>> { readonly } '(2) Java風にメソッドで表現 + getPropA():int + setPropA(value:int) + getPropB():int + getPropC():int # setPropC(value:int)
初期化子(フィールド、プロパティ)
フィールド、プロパティの初期値を初期化子で設定している場合、初期値がリテラルの場合のみ = (初期値)
を付加するようにしています。
リテラルのみにした理由は、初期化子にコンストラクタやメソッドを使った場合に、 PlantUMLが 初期化子の()
を見て フィールド(プロパティ)ではなくメソッドと判断してしまう為です。
'plantuml ''次のようにフィールドを記述しても、PlantUMLはメソッドと判断してしまう class ClassA{ # int : IList<int> = new List<int>() }
初期化子の変換例
//csharp class ClassA { private readonly int intField = 100; protected double X = 0, Y = 1, Z = 2; internal double PropC { get; } = 3.141592; protected IList<int> list = new List<int>(); }
'plantuml class ClassA { - <<readonly>> intField : int = 100 # X : double = 0 # Y : double = 1 # Z : double = 2 <<internal>> PropC : double <<get>> = 3.141592; ' リテラル以外の初期化子は出力しない # list : IList<int> }
未対応
現在対応できていないコードについてまとめます。
ネストクラス
現状、入れ子になった型の定義は、以下の様にそのまま入れ子の状態で出力されるのですが、PlantUMLではエラーとなり変換できません。
'plantuml '' このコードはエラーになります class NestedClass { + A : int <<get>> + B : InnerClass <<get>> class InnerClass { + X : string <<get>> + MethodX() : void } }
内部クラスを外に出して、+--
で結ぶと入れ子を表現可能なので、以下の様に変換すべきなのですが。。。
class NestedClass { + A : int <<get>> + B : InnerClass <<get>> } class InnerClass { + X : string <<get>> + MethodX() : void } NestedClass +-- InnerClass
演算子のオーバーロード
演算子のオーバーロードを定義してもPlantUMLに出力されません。(未検討)
変換例
最後に、いろいろな変換例を載せておきます。
変換前
変換元C#のコードはこちらを参照ください InputClasses.cs
変換後のPlantUML
'plantuml class ClassA { - <<readonly>> intField : int = 100 - {static} strField : string # X : double = 0 # Y : double = 1 # Z : double = 2 - list : IList<int> # PropA : int <<get>> # <<internal>> PropB : string <<get>> <<protected set>> <<internal>> PropC : double <<get>> = 3.141592 + ClassA() {static} ClassA() # <<virtual>> VirtualMethod() : void + <<override>> ToString() : string + {static} StaticMethod() : string } abstract class ClassB { - field_1 : int {abstract} + PropA : int <<get>> <<protected set>> # <<virtual>> VirtualMethod() : string + {abstract} AbstractMethod(arg1:int, arg2:double) : string } class ClassC <<sealed>> { - {static} <<readonly>> readonlyField : string = "ReadOnly" + <<override>> PropA : int <<get>> <<protected set>> = 100 + <<override>> AbstractMethod(arg1:int, arg2:double) : string # <<override>> VirtualMethod() : string } class Vector <<struct>> { + X : double <<get>> + Y : double <<get>> + Z : double <<get>> + Vector(x:double, y:double, z:double) + Vector(source:Vector) } enum EnumA { AA= 0x0001, BB= 0x0002, CC= 0x0004, DD= 0x0008, EE= 0x0010, } ClassB <|-- ClassC
クラス図
Roslyn で C#のソースコードからPlantUMLのクラス図を生成する
前回 に引き続き、PlantUMLの話になります。
ReactiveProperty の記事もまだ途中なのですが、もうちょっと寄り道します。
今回は、Roslynで C# のソースコードから PlantUMLのクラス図を生成するプログラムを作ってみよう!というお話です。
Roslyn を使って C#のソースコードから PlantUML のクラス図を生成してみる
Roslyn でメタプログラミング
Roslynの導入方法、Roslynを使ったプログラミングについては、Build Insiderの記事が参考になります。
サンプルコード
今回も、サンプルコードをGitHubに上げておきました。
CSharpSyntaxTree および CSharpSyntaxWalker
今回のプログラムですが、基本的な仕組みは以下のようになります。
- ソースコードを構文木(
CSharpSyntaxTree
)にパース CSharpSyntaxWalker
を継承したクラスを用意して、必要な要素をVisitorパターンで訪問- 訪問先で、取得した情報を加工して出力
といった流れで、いたってシンプルです。
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using System; using System.Collections.Generic; using System.IO; using System.Linq; class Program { static void Main(string[] args) { //ソースコード var code = @"class Hoge{ ...(略)"; //SyntaxTree にパース var tree = CSharpSyntaxTree.ParseText(code); var root = tree.GetRoot(); //出力先。今回はStringWriterで書き込みます var output = new StringBuilder(); using (var writer = new StringWriter(output)) { //CSharpSyntaxWalker を継承したクラス //この中で変換処理やら出力やらをやります var gen = new ClassDiagramGenerator(writer, indent:" "); gen.Visit(root); } Console.WriteLine(output); } }
ClassDiagramGenerator
上記のコードで重要なのが、ClassDiagramGenerator
で、このプログラムの心臓部になります。
CSharpSyntaxWalker
を継承しています。
では、ClassDiagramGenerator
をクラス図で見てみましょう。
Visit~ と名前の付いたメソッドがいくつかあります。
これらはCSharpSyntaxWalker
からオーバーライドしたものです。構文木を辿り、対応するノード(ここではクラス、インターフェース、プロパティ、メソッド等の定義部)を訪問した際に実行されます。
このメソッド内に、必要な情報を取得し、適切な文字列に変換して出力する処理を記述します。
細かな実装については、割愛させていただきます。 Roslynのおかげでとてもシンプルなコードになっていると思いますので、詳細は以下のソースコードで確認してみてください。
変換例
実際に、C#のクラス定義がどのように変換されるかの一例を示します。
C# (ClassA.cs)
public class ClassA { private readonly int intField = 100; private static string strField; protected double X = 0, Y = 1, Z = 2; protected int PropA { get; private set; } protected internal string PropB { get; protected set; } internal double PropC { get; } = 3.141592; public ClassA() { } static ClassA() { strField = "static field"; } protected virtual void VirtualMethod() { } public override string ToString() { return intField.ToString(); } public static string StaticMethod() { return strField; } }
PlantUML (ClassA.plantuml)
class ClassA { - <<readonly>> intField : int = 100 - {static} strField : string # X : double = 0 # Y : double = 1 # Z : double = 2 # PropA : int <<get>> # <<internal>> PropB : string <<get>> <<protected set>> <<internal>> PropC : double <<get>> = 3.141592 + ClassA() {static} ClassA() # <<virtual>> VirtualMethod() : void + <<override>> ToString() : string + {static} StaticMethod() : string }
クラス図 (ClassA.svg)
オブジェクト間のリンク
複数オブジェクトの関連付けなどには対応していません。
これは、別途手書きで行います。
@startuml !include ClassA.plantuml !include ClassB.plantuml IDisposable ()-- ClassA ClassBBase <|-- ClassB ClassA o- ClassB : aggregation @enduml
PlantUMLでは、!include [ファイル名]
で別のPlanuUMLファイルを読み込んで使用することが出来ます。
このように、クラス単位で自動生成したファイルをIncludeして、関連のみを別ファイルに記載するとスッキリして良いです。
まとめ
今回の様に、C#のコードから他の言語(DSL)などを生成するといった事が、 Roslyn の力で いとも簡単に出来てしまうことが分かりました。
むしろ生成処理そのものよりも、C#の型定義とUMLとの対応付けをどうするかを決める作業の方が難しいのではないでしょうか。
C#に固有の、UMLでは定義されていない(もしくは微妙に意味が異なる)要素をどう扱うかが非常に悩ましいところでした。
今回は、なるべく変換処理が複雑にならないように、というのも加味して、今の実装に落ち着いたのですが...
この辺りの話は、また別の記事でまとめたいと思います
PlantUMLを自動変換してLiveプレビューしてくれるAtom拡張 PlantUML-Viewer がイイ!
PlantUML とは
テキストベースでUMLのダイアグラムをサクサクかける ドメイン固有言語(DSL)です。
以下のような特徴があります。
参照
- Open-source tool that uses simple textual descriptions to draw UML diagrams.
⇒ PlantUML 公式サイト。リファレンスマニュアルがpdfファイルで公開されています(PlantUML_Language_Reference_Guide.pdf) - http://plantuml-ref-ja.github.io/
⇒ リファレンスマニュアルを日本語訳してくれているサイトです。 - PlantUML の使い方 | プログラマーズ雑記帳
⇒ 分かりやすく豊富な解説記事が参考になります。
Atom Editor でPlantUML
とても便利なPlantUMLなのですが、テキストをUMLに変換して、結果を確認するまでの手順が少々面倒であったりします。
- PlantUMLの書式に従ってテキストを編集する
- テキストファイルをUMLに変換して、画像ファイルとして出力
- できたファイルを画像ビューアやWebブラウザ等で開き、結果を確認する
- 1 に戻る
この辺りの手順を自動化して、結果をプレビュー表示してくれるツールを探していたのですが、最近見つけたAtom Editorのプラグインが素敵だったので、紹介したいと思います。
Atom
plantuml-viewer
- PlantUMLのテキストをUMLに変換して、エディタ右側の分割ペインに表示してくれます。
- テキストの編集に追従して、リアルタイムに図が更新されます。(Live Preview)
- 図はマウス操作で自由に拡大/縮小、移動することができます。
- 図は、PNG, SVG, EPS の3種類の形式で保存可能です。
導入手順
PlantUMLを使う準備
PlantUMLを使用するには、以下がインストールされている必要があります。
Java ランタイム(JRE)
PlanuUMLはJavaで動くので、Javaのランタイムがインストールされている必要があります。
インストールされていない場合は、以下から最新版をインストールすればよいと思います。
Graphviz
シーケンス図以外の図のレンダリングには、 Graphiviz の dot.exe というモジュールが必要になります。
http://www.graphviz.org/Download..php
なお、plantuml本体のモジュール plantuml.jar はplantuml-viewer に組み込まれているので別途インストールする必要はありません
Atomのインストール
以下のリンクからインストーラをダウンロードして、普通にインストールするのが一番簡単です。
Atom プラグインのインストール
Atomを起動したら、[File]メニューのSettings を開き、左側で[Install]を選ぶと、以下の画面になります。
ここで、”plantuml” と入力して表示される一覧からplantuml-viewer
を探して[Install]ボタンをクリックします。
ついでに、PlantUMLで書かれたテキストをシンタックスハイライト表示してくれるパッケージ language-plantuml
も入れておきましょう。
plantuml-viewer の設定
次に、plantuml-viewer の設定を行います。
先ほどのSettingsページで、今度は[Packages]を選択します。
すると、インストールしたプラグインのパッケージ一覧が表示されますので、ここでplantuml-viewer
を探して[Settings]ボタンをクリックします。
設定項目
Charset
変換前のテキストのエンコーディングを指定します。指定しないとシステムのデフォルト(日本語環境のWindowsではCP932(Shift-jis))になります。当初、この設定項目がなく、Shift-jis以外で書くと文字化けしていました。この項目を指定できるようにカスタマイズして、プルリクエストしてみたところ、即日取り込んでいただき、V0.60から使用可能となっています。(感謝)
Config File
変換時に読み込まれる、共通の処理を記載しておくplantUMLファイルのパスを指定します。図の外観のカスタマイズする場合などに使用します。- Grammars
対象となるファイルの拡張子を指定します。 - Graphviz Dot Executable
インストールした Graphivizの dot.exe のパスを指定します。 - Live Update
チェックを外すと、図の自動更新を無効にできます。 - Open in Split Pane
チェックを外すと、図のプレビューを分割ペインではなく、通常のタブに表示するようになります。
最後に
plantuml-viewer はまだリリース間もない(8/9)のと、私も使い始めたばかりなので、もう少し使い込んでみる必要はあるかと思いますが、第一印象としてはかなりいい感じです。
この記事を読んで気になった方は、ぜひ試してみてください!
※ 2016/3/6追記
変換するUMLのサイズが大きいと、極端に重くなったり、入力したテキストの内容がちゃんと反映されなかったりする場合があります。
この辺の話と対処法を以下の記事に書きましたので、こちらもご参照ください。