開発備忘録

WPF(Xaml + C#)の話題を中心に.Net関連についてのブログです。

(WPF)要素のスクリーンキャプチャーを保存するTriggerActionを作る

WPFで画面上の要素をキャプチャーして画像ファイルとして保存するような要件がワリとありますが、どうせならTriggerActionで作られたプロダクトは無いかなーと思って探してみたところ、すぐには見つからなかったので自作してみました。

以下、今回の開発環境です。

WPFでスクリーンキャプチャーを保存する方法は色々ありますが、今回は以下のページを参考にさせていただきました。

mseeeen.msen.jp

これをTriggerActionで実装してみた結果がこちら

<CaptureAciton.cs>

namespace CaptureTriggerSample.TriggerActions
{
    using System;
    using System.IO;
    using System.Windows;
    using System.Windows.Interactivity;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;

    public class CaptureAction : TriggerAction<FrameworkElement>
    {
        public static readonly DependencyProperty FileNameProperty =
                DependencyProperty.Register("FileName"
                                            , typeof(string)
                                            , typeof(CaptureAction)
                                            , new FrameworkPropertyMetadata(null));

        public string FileName
        {
            get => (string)GetValue(FileNameProperty);
            set => SetValue(FileNameProperty, value);
        }

        protected override void Invoke(object parameter)
        {
            Rect bounds = VisualTreeHelper.GetDescendantBounds(AssociatedObject);
            DrawingVisual drawingVisual = new DrawingVisual();
            using (DrawingContext drawingContext = drawingVisual.RenderOpen())
            {
                VisualBrush visualBrush = new VisualBrush(AssociatedObject);
                drawingContext.DrawRectangle(visualBrush, null, bounds);
            }

            RenderTargetBitmap renderTargetBitmap =
                    new RenderTargetBitmap((int)bounds.Width, (int)bounds.Height, 96, 96, PixelFormats.Pbgra32);
            renderTargetBitmap.Render(drawingVisual);
            renderTargetBitmap.Freeze();

            PngBitmapEncoder png = new PngBitmapEncoder();
            BitmapFrame bitmapFrame = BitmapFrame.Create(renderTargetBitmap);
            bitmapFrame.Freeze();
            png.Frames.Add(bitmapFrame);
            using (FileStream fileStream = File.Create(FileName))
            {
                png.Save(fileStream);
            }

            MessageBox.Show($"キャプチャーを保存しました{Environment.NewLine}"
                            + $"[ファイル名]{Environment.NewLine}"
                            + $" {FileName}"
                            , "キャプチャー"
                            , MessageBoxButton.OK
                            , MessageBoxImage.Information);
        }
    }
}

※ブログ用に可読性を考慮してエラー処理などはしていません。

FileName プロパティに出力するファイル名を指定できるようにしています。

これを使うにはこのようにします。

<MainWindow.xaml

<Window
    x:Class="CaptureTriggerSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:local="clr-namespace:CaptureTriggerSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:triggerActions="clr-namespace:CaptureTriggerSample.TriggerActions"
    Title="MainWindow"
    Width="525"
    Height="350"
    mc:Ignorable="d">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click" SourceObject="{Binding ElementName=CaptureButton}">
            <triggerActions:CaptureAction FileName="Resources\Capture.png" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Button
            x:Name="CaptureButton"
            Grid.Row="0"
            Margin="8"
            Padding="16,8"
            HorizontalAlignment="Left"
            Content="Capture" />
        <Grid Grid.Row="1" Margin="8">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <TextBox x:Name="TextBox" />
            <TextBlock
                Grid.Row="1"
                Margin="0,8"
                Text="{Binding Text, ElementName=TextBox, Mode=OneWay}" />
        </Grid>
    </Grid>
</Window>

これを実行すると以下のような画面が表示されます。
f:id:iyemon018:20171215142227p:plain

CaptureボタンをクリックするとWindow要素のキャプチャーが実行されます。
(実行フォルダに"Resources"フォルダを作成する必要があります。)
f:id:iyemon018:20171215142328p:plain

実際にキャプチャーされた画像ファイルはこちら。
f:id:iyemon018:20171215142417p:plain

このように簡単に指定した領域をキャプチャーすることが可能です。
TriggerActionにしたことにより、例えばKeyTrigger を使ってCtrl + Cキー入力時にキャプチャーを保存したりすることも可能です。

ただし、1点注意することがあります。
レアケースだとは思いますが、以下の様にキャプチャー対象の要素の外側にも子要素が配置されている場合、子要素を含む領域がキャプチャーされてしまいます。
以下はVisual StudioXAMLデザイナープレビューです。
右上にEllipseを配置していますが、Grid の領域外に配置しています。
f:id:iyemon018:20171215143722p:plain

これをキャプチャーすると以下の様になります。
f:id:iyemon018:20171215144027p:plain

原因はここ

Rect bounds = VisualTreeHelper.GetDescendantBounds(AssociatedObject);

VisualTreeHelper.GetDescendantBounds は指定したビジュアル要素の全ての子要素の和集合から領域を算出します。
つまり、AssociatedObject の子要素が配置された領域全てがその対象となります。(ただし原点はあくまでAssociatedObject を基準とする。)

これを回避するには、AssociatedObject もしくはその子要素にClipToBounds="True" を設定します。
こうすることで要素の範囲外にあるものはキャプチャーされなくなります。

画面領域外にコントロールを配置することってあんまりないと思うのですが、移動アニメーション中などに予期せずキャプチャー範囲が期待したもので無くなることはあり得るので注意が必要です。


今回使用したサンプルはこちら↓↓↓↓
github.com

Visual Studioのリモートデバッグ方法メモ

Visual Studioのリモートデバッグ機能を初めて使ったのでメモ

リモートデバッグ機能とは?

リモートデバッグとは、例えば客先から受領している端末上で実行しているアセンブリに対してブレークポイントやステップ実行するための機能です。
詳細は以下のページを参照してください。

リモート デバッグ

使い方

適当にググれば出てくるのですが、一番わかりやすかったのは以下のページでした。

ネットワーク経由でのリモート ユーザーモード デバッグ – Japan WDK Support Blog


と、ここまでですんなり実行できるのかと思いきやいくつかハマりました。
ちなみに実行環境は"Visual Studio2015"のWPF アプリケーションです。

Visual Studio2015 で開発していたのですが、リモートデバッグ ツールのページは404になっています…
以下のページからダウンロードしましょう。

qiita.com

  • 実行フォルダがホストとリモートで同じでなければならない

これは上記MSDNページにも記述されていますが、開発用環境とリモート環境でアセンブリの配置フォルダ パスが同一である必要があります。
私の環境はDドライブ上で実行していたので最初は実行に失敗しました。
仕方なくプロジェクトの[プロパティ] - [ビルド] - [出力パス] をCドライブに変更しました。

  • 毎回実行ファイルをコピーする必要がある

仕方ないことなのですが、実行環境にビルドしたアセンブリを毎回配置する必要があります。
最初はUSBメモリを使用してコピーしていたのですが、面倒なのでリモート環境に共有フォルダを作成しました。
そして、上記の出力パスを共有フォルダに設定することでアセンブリ コピーの手順が不要になりました。
つまり、以下のようなイメージ

Before
f:id:iyemon018:20171205185628p:plain

After
f:id:iyemon018:20171205185630p:plain

これで毎回、開発用PCと同じ感覚でデバッグすることが可能になりました。
リモートの設定は環境に依存するため、リモート デバッグ用の構成を追加しておくのがベターだと思います。

実行環境にリモートツールをインストールしたりネットワークに接続したりといくつか条件はありますが、実行環境でしか発生しない不具合もあるため条件さえ合致すれば有用なデバッグ方法だと思いました。
ただ、この仕組み上、複数人で同時にリモートデバッグを行うといった方法を取ることはできないでしょう。

(Android)ログ出力ライブラリ"Timber"を使ってみる

Androidのログ出力ライブラリ"Timber"でログ出力機能を使ってみたのでメモ。

Timberについて

github.com

APIリファレンスはこちら。
Timber 3.0.1 API

動作環境

パッケージを導入する

  1. [File] - [Project Structure...] からパッケージを追加したいモジュールの[Dependencies]タブを選択する。
  2. 画面右側の[+]をクリック[Library dependency]を選択する。
  3. "com.jakewharton.timber:timber:4.5.1"と入力し、検索する。
  4. "com.jakewharton.timber:timber:4.5.1"を選択して[OK]ボタンをクリックする。
  5. build.grableに以下が追加されていればOK
compile 'com.jakewharton.timber:timber:4.5.1'

Timberを使う

Timberは初期化する際にログ出力機構のTreeを指定します。
以下は予め用意されているデバッグ用のTreeを設定しています。
リリース用のTreeは自前で作成する必要があるようです。

Timber.plant(new DebugTree());

使用する場合は通常のLogクラスと使い方は同じです。


サンプルを作成がてらログ出力機能を作りました。
以下のLoggerクラスはTimberのログ出力機能をラッパーです。
gist.github.com

initializeで初期化しているログTreeは以下のようになっています。
gist.github.com

後はこれを以下の様に使用します。
gist.github.com

これを実行するとLogCatには以下のようなログが出力されます。
f:id:iyemon018:20170912165732p:plain


あとはログの出力先をファイルにするなりクラッシュレポートを送信するなりすると使えそうです。

*1:2017年9月12日時点で最新版です。

ErgoDoxのキーマップ変更時にハマった現象と回避策について

先日、ErgoDoxを購入してウキウキで使用していたんですが、キーマップを変更する際にコンパイル環境の構築やらビルドやらでハマったのでその内容をメモします。
まず、前提条件として私は

  • Windowsユーザー
  • 普段は開発環境はVisual Studioを使用している。
  • キーボードはあんまり詳しくない。
  • C言語は殆どわからない。
  • Windows以外のOSのことは殆どわからない。
  • ErgoDox EZは9割衝動買い。

です。
なのでコンパイル環境は当然Windowsです。

続きを読む

MediaPlayer でファイル再生時のエラー回避方法について

またもやWPF のMediaPlayer ネタです。
MediaPlayer のイベントにMediaFailed がありますが、このイベントで以下のようなメッセージが出力される場合があります。

HRESULT からの例外:0xC00D11BA

これはWindows Media Player が出力している例外メッセージです。
MediaPlayer.Play() を呼び出した後にこの例外がスローされるのであれば、多くの場合、以下の内容が原因とされています。

  • サウンド ドライバーが最新でない。
  • 再生ファイルのコーデックが不足している。

など

しかし、再生中にたまにこの例外がスローされる事があります。
そうなった場合、再度MediaPlayer.Play() を実行しても楽曲が再生されません。

残念ながら確実に発生するオペレーションの確立と原因の究明までには至っていないため、この現象の発生を止めることはできません。
今回は"この現象が発生してももう一度楽曲を再生できる"ような対象法をまとめます。

動作環境

対策

正しいかどうかは別として、"エラー発生後に楽曲ファイルを再読読み込み"することで回避は可能です。
エラーの発生するコードは以下の様なものとしましょう。

	var mediaPlayer = new MediaPlayer();
	mediaPlayer.MediaFailed += (sender, e) =>
	{
		// HRESULT からの例外:0xC00D11BA
		// というエラーメッセージが出力される。
		Console.WriteLine(e.ErrorException);
	};
	
	mediaPlayer.Open(new Uri(@"Resources\sample.wav"));
	mediaPlayer.Play();

これをこう変えます。

	var mediaPlayer = new MediaPlayer();
	mediaPlayer.MediaFailed += (sender, e) =>
	{
		// HRESULT からの例外:0xC00D11BA
		// というエラーメッセージが出力される。
		Console.WriteLine(e.ErrorException);
		
		mediaPlayer.Close();
		mediaPlayer = null;
		
		mediaPlayer = new MediaPlayer();
		mediaPlayer.Open(new Uri(@"Resources\sample.wav"));
	};
	
	mediaPlayer.Open(new Uri(@"Resources\sample.wav"));
	mediaPlayer.Play();

イマイチ発生原因がつかめていないのでモヤモヤは残るのですが、今のところはこれで対応できているので良しとします。

MediaPlayer でファイル再生時のノイズ発生回避方法について

前回同様、WPF のMediaPlayer のお話です。
iyemon018.hatenablog.com

今回は楽曲ファイルの読み込み時ではなく、再生時に発生するノイズについてです。
このノイズも必ず発生するものではなく、特定の条件の楽曲ファイルと環境が揃わなければ発生しません。
かなり局所的な問題だと思われますが、とりあえずノイズの発生した環境は以下の通りです。

動作環境

  • OS : Windows 8.1 with Being (x86)*1
  • CPU : Intel Atom CPU Z3735F 1.33GHz
  • メモリ : 2GB
  • 楽曲ファイル : 再生時間300ミリ秒程度。1音目に振幅の大きい(例えばクリック音のような)楽曲に発生しやすい。

開発環境

現象

WPF のMediaPlayer の再生時に”1音目に振幅の大きい楽曲”を再生した場合にノイズが入る。
同じ楽曲を他のメディアプレイヤーで再生してもこの現象は発生しない。
また、動作環境によっては発生しない。
タブレットなどの低スペックPC上で発生するケースが多い。

原因

今のところわかっていません。
MediaPlayer は内部でWindows Media Player を使用しているのでWMPが怪しい気もするのですが、根本的な原因については今もわかっていません。

対策

楽曲ファイルの調整が可能であれば、楽曲ファイルの先頭に無音時間を10ミリ秒程度追加すると解消されます。
この方法であればコードの修正が必要ないため簡単に対応できるのですが、楽曲ファイルを編集できない場合は使えません。
他には、

  • 楽曲再生までに徐々に音量を上げる
  • 楽曲再生前にインターバルを持たせる

なども考えましたが、いずれも確実に修正できるという確証がなかったため試していません。

*1:Windows 10でも発生しますがハードウェアのスペックがわかりませんでした。

MediaPlayer でファイルオープン時のノイズ発生回避方法について

WPF でWAVE ファイルなどのメディアファイルを再生するとき、System.Windows.Media.MediaPlayer を使用していたのですが、複数ファイルを纏めてオープンしたときに何故か楽曲が少しだけ再生される現象が発生しました。
ここではその回避方法についてまとめています。

動作環境

実現したいこと

単純に複数の楽曲ファイルを予めロードすることで再生時のオーバーヘッドを軽減したい。
SoundPlayer クラス (System.Media) も試したのですが、「楽曲再生中に停止できない」「音量の調節機能が無い」ため、手っ取り早くMediaPlayer で実現することとなりました。

続きを読む