泥庭

2015年4月11日

[WPF]描画パフォーマンスのお話(Polylineの場合)

Filed under: C#, performance, WPF — タグ: , — yone64 @ 11:06 午後
WPFの描画速度には、いつも悩ませてもらってます。
最近、Polylineで描画している際にちょっと不思議(?)な現象に出会ったので、一応メモ程度に。

(more…)

2015年4月6日

【WPF】カスタムメッセージボックス

Filed under: C#, WPF — タグ: , , , — yone64 @ 11:56 午後
WPF標準のメッセージボックスは、何かと機能不足でカスタマイズ性が低くて困ってしまいますね。
そこで、通常のWindowをMessageBoxっぽく見せることを検討してみたいと思います。

なお、Windows7以降のみをターゲットとできる方は、Windows API Code PackのTaskDialogをお勧めします。
良記事→ http://grabacr.net/archives/tag/windows-api-code-pack
# いまさらXP対応はないよね。Windows Server 2003ももうすぐサポート切れるし…
# Embedded とか、知らない子ですし。
というわけで、誰得なエントリーです。

(more…)

2015年3月10日

【ReactiveProperty】きちんとDisposeしよう

Filed under: .NET, C#, WPF — タグ: , , , — yone64 @ 3:05 午後
ReactivePropertyが依存しているReactiveExtensionsのお話なのですが、きちんとにDisposeしないと予期しない動作の原因になります。

下記サンプル
MainWindow.xaml
<Window x:Class="WpfApplication3.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Viewbox>
            <TextBlock Text="{Binding DateTime.Value}" />
        </Viewbox>
    </Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApplication3
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private IDisposable _disposable;

        public MainWindow()
        {
            InitializeComponent();

            var vm = new MainViewModel();
            this.DataContext = vm;

            this._disposable = vm as IDisposable;
            this.Closing += (s, e) =>
            {
                if (this._disposable != null) this._disposable.Dispose();
            };
        }
    }
}
MainViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Reactive.Bindings;
 
namespace WpfApplication3
{
    public class MainViewModel : IDisposable
    {
        public ReactiveProperty<string> DateTime { get; private set; }
 
        public MainViewModel()
        {
            this.DateTime = 
                Observable.Interval(TimeSpan.FromMilliseconds(10), NewThreadScheduler.Default)
                          .Select(_ => System.DateTime.Now.ToString("HH:mm:ss.fff")).ToReactiveProperty();
        }
 
        void IDisposable.Dispose()
        {
            this.DateTime.Dispose();
        }
    }
}
タイマーで時間を表示するだけの簡単なサンプルですが、ReactivePropertyをDisposeしないと終了しないアプリケーションになってしまいます。
この場合は、NewThreadSchedulerによって新しいThreadが立っているので当然の結果なのですが、Rxを使うと簡単にThreadを起こせるので、神経質にDisposeしていたほうが良い気がしました。

というわけで、基底クラスとして次のようなものを用意するようにしています。
ViewModelBase.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Text;
using System.Threading.Tasks;

namespace WpfApplication3
{
    public abstract class ViewModelBase : IDisposable
    {
        protected CompositeDisposable Disposable { get; private set; }

        public ViewModelBase()
        {
            this.Disposable = new CompositeDisposable();
        }

        void IDisposable.Dispose()
        {
            this.Disposable.Dispose();
        }
    }
}
CompositeDisposableは、Rxが提供しているIDisposableをまとめるためのクラスです。
これを利用すると、MainViewModelが次のように少し簡易になります。
MainViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using Reactive.Bindings;
 
namespace WpfApplication3
{
    public class MainViewModel : ViewModelBase
    {
        public ReactiveProperty<string> DateTime { get; private set; }
 
        public MainViewModel()
        {
            this.DateTime = 
                Observable.Interval(TimeSpan.FromMilliseconds(10), NewThreadScheduler.Default)
                          .Select(_ => System.DateTime.Now.ToString("HH:mm:ss.fff"))
                          .ToReactiveProperty()
                          .AddTo(this.Disposable); // かずきさん指摘により修正

            // this.Disposable.Add(this.DateTime);
        }
    }
}
ToReactiveProperty()とDisposable.Add()をまとめてうまく書けるといいんですけどね。↓のような、拡張メソッドを増やすのしか思いつきませんでした。
public static class ViewModelBaseEx
{
    public static ReactiveProperty<T> ToReactiveProperty<T>(this IObservable<T> souce, CompositeDisposable disposable)
    {
        var rp = souce.ToReactiveProperty();
        disposable.Add(rp);
        return rp;
    }
}

2015年3月9日

Thicknessの125000倍の長さのGeometryをOnRenderで描画

Filed under: .NET, C#, WPF — タグ: , — yone64 @ 11:15 午後
前回の続きっぽいもの。

CustomControl等でOnRenderをOverrideして描画することが多々あると思いますが(ありますよね!?)
その際は、少し回避方法が異なります。(というか、かなり謎です)
protected override void OnRender(DrawingContext drawingContext)
{
    var geometry = new StreamGeometry();
    using (var context = geometry.Open())
    {
        context.BeginFigure(new Point(0, 100), false, false);
        context.LineTo(new Point(125000, 100), true, false);
    }

    drawingContext.DrawGeometry(null, new Pen(Brushes.Black, 1), geometry);

    base.OnRender(drawingContext);
}
まず、描画できるパターン。125000倍なので、描画可能です。
protected override void OnRender(DrawingContext drawingContext)
{
    var geometry = new StreamGeometry();
    using (var context = geometry.Open())
    {
        context.BeginFigure(new Point(0, 100), false, false);
        context.LineTo(new Point(125000.1, 100), true, false);
    }

    drawingContext.DrawGeometry(null, new Pen(Brushes.Black, 1), geometry);

    base.OnRender(drawingContext);
}
125000倍を超えると描画不可能です。(Lineと同じ)
protected override void OnRender(DrawingContext drawingContext)
{
    var geometry = new StreamGeometry();
    using (var context = geometry.Open())
    {
        context.BeginFigure(new Point(0, 100), false, false);
        context.LineTo(new Point(125000.1, 100.1), true, false);
    }

    drawingContext.DrawGeometry(null, new Pen(Brushes.Black, 1), geometry);

    base.OnRender(drawingContext);
}
ところが、傾けても描画できるようになりません。
protected override void OnRender(DrawingContext drawingContext)
{
    var geometry = new StreamGeometry();
    using (var context = geometry.Open())
    {
        context.BeginFigure(new Point(0, 100), false, false);
        context.LineTo(new Point(0.1, 100), true, false);
    }

    var geometry2 = new StreamGeometry();
    using (var context = geometry2.Open())
    {
        context.BeginFigure(new Point(0, 200), false, false);
        context.LineTo(new Point(375000, 200), true, false);
    }

    drawingContext.DrawGeometry(null, new Pen(Brushes.Transparent, 1), geometry);
    drawingContext.DrawGeometry(null, new Pen(Brushes.Red, 1), geometry2);

    base.OnRender(drawingContext);
}
ちなみに、一つGeometryをDummyで描画しておくと、2つ目のGeometryに対する制限はなくなってるように見えます。(謎謎

Thicknessの125000倍の長さのLine

Filed under: .NET, C#, WPF — タグ: , — yone64 @ 10:47 午後
WPFの制限(?)なのかはわかりませんが、StrokeThicknessの125000倍を超える水平線は引けません。
<Window x:Class="TooLongLine.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <!-- 水平LineはStrokeThicknessの12500倍以下しか描画できない -->

        <!-- 125000倍ちょうどなので描画可能 -->
        <Line X1="0" X2="125000" Y1="5" Y2="5" StrokeThickness="1" Stroke="Red" />
        <!-- 125000倍を超えるので描画不可能 -->
        <Line X1="0" X2="125000.1" Y1="15" Y2="15" StrokeThickness="1" Stroke="Red" />

        <!-- Thicknessを2にすると、125000倍以下になるので描画可能 -->
        <Line X1="0" X2="125000.1" Y1="25" Y2="25" StrokeThickness="2" Stroke="Blue" />
        <!-- 125000倍ちょうどなので描画可能 -->
        <Line X1="0" X2="250000" Y1="35" Y2="35" StrokeThickness="2" Stroke="Blue" />
        <!-- 125000倍を超えるので描画不可能 -->
        <Line X1="0" X2="250000.1" Y1="45" Y2="45" StrokeThickness="1" Stroke="Blue" />

        <!-- 水平でなければ描画可能 -->
        <Line X1="0" X2="250000.1" Y1="55" Y2="55.001" StrokeThickness="1" Stroke="Green" />
        
        <!-- Polylineで、一つの線分が125000倍以下でも水平になってしまうとダメ -->
        <Polyline Points="0,65 100000,65 200000,65" StrokeThickness="1" Stroke="Orange" />
    </Grid>
</Window>
上記、XAMLを実行すると下記Windowが表示されます。

キャプチャ

どうしてもこの制限に引っかかる場合は、微妙に斜めの線にしてみればよいのかもしれません。
その前に、本当にそんなに長いLineが必要かは要検討ですねw

(参考)Horizontal or vertical WPF Lines limited to 125,000 pixels?
http://stackoverflow.com/questions/13731593/horizontal-or-vertical-wpf-lines-limited-to-125-000-pixels

2015年2月26日

Gridからの脱出

Filed under: C#, WPF — タグ: , , , — yone64 @ 12:38 午前
WPFではPanelにControlを配置することになるわけですが、PanelからControlがはみ出しても表示することができます。

例えば、
<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Canvas Background="Ivory" Margin="50">
        <TextBlock Text="abcdefghi" FontSize="100"/>
    </Canvas>
</Window>
という、XAMLだと

キャプチャ

という、実行結果になります。Canvasの背景を「Ivory」にしてるので、文字がCanvas領域をはみ出しているのがわかると思います。
ちなみに、ClipToBoundというプロパティーが用意されていて、これをTrueに設定することで、Panelの大きさの外にはみ出した部分が切り取られます。
<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Canvas Background="Ivory" Margin="50" ClipToBounds="True">
        <TextBlock Text="abcdefghi" FontSize="100"/>
    </Canvas>
</Window>

キャプチャ

ところが、GridではPanelをはみ出して表示することができません。明示的にClipToBoundをFalseにしても、ダメでした。
<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid Background="Ivory" Margin="50" ClipToBounds="False">
        <TextBlock Text="abcdefghi" FontSize="100"/>
    </Grid>
</Window>

キャプチャ

CanvasのClipToBound=”True”の場合と、同じ表示結果になります。
# 決して同じ画像ではありませんw

そういう場合は、Gridの中にCanvasを配置してください。Canvas内部に配置されたControlが親Gridの範囲をも超えて描画されるようになります、
<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid Background="Ivory" Margin="50">
        <Canvas Background="AliceBlue">
            <TextBlock Text="abcdefghi" FontSize="100"/>
        </Canvas>
    </Grid>
</Window>

キャプチャ

2015年1月16日

WPFでBingMapを使う(その2)

Filed under: C#, WPF — タグ: , — yone64 @ 9:03 午後
前回の続きです。
本編の前に、一つ訂正。
「Bing Map WPF Controlは、NuGetで提供されてないのでとっとと対応すべき。」みたいなことを書きましたが、どうやら提供されていた(過去形)ようです。現在は提供されなくなっていますが、いったい何が。

実行時例外

いきなり身もふたもないタイトルですが、BingMapControlはとある条件で実行時例外を投げます。
例えば、こんな感じのXAMLを用意します。
<Window x:Class="BingMapSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wpf="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ContentControl>
            <ContentControl.Template>
                <ControlTemplate>
                    <wpf:Map CredentialsProvider="ここにAPIキーを入力"/>
                </ControlTemplate>
            </ContentControl.Template>
        </ContentControl>
    </Grid>
</Window>
で、とりあえず実行してみます。すると、なんということでしょう。
System.Windows.Markup.XamlParseException はハンドルされませんでした。
HResult=-2146233087
Message=指定されたバインディング制約に一致する型 ‘Microsoft.Maps.MapControl.WPF.Map’ のコンストラクターの呼び出しで例外がスローされました。
Source=PresentationFramework
LineNumber=0
LinePosition=0
StackTrace:
場所 System.Windows.FrameworkTemplate.LoadTemplateXaml(XamlReader templateReader, XamlObjectWriter currentWriter)
場所 System.Windows.FrameworkTemplate.LoadTemplateXaml(XamlObjectWriter objectWriter)
場所 System.Windows.FrameworkTemplate.LoadOptimizedTemplateContent(DependencyObject container, IComponentConnector componentConnector, IStyleConnector styleConnector, List`1 affectedChildren, UncommonField`1 templatedNonFeChildrenField)
場所 System.Windows.FrameworkTemplate.LoadContent(DependencyObject container, List`1 affectedChildren)
場所 System.Windows.StyleHelper.ApplyTemplateContent(UncommonField`1 dataField, DependencyObject container, FrameworkElementFactory templateRoot, Int32 lastChildIndex, HybridDictionary childIndexFromChildID, FrameworkTemplate frameworkTemplate)
場所 System.Windows.FrameworkTemplate.ApplyTemplateContent(UncommonField`1 templateDataField, FrameworkElement container)
場所 System.Windows.FrameworkElement.ApplyTemplate()
場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
場所 System.Windows.UIElement.Measure(Size availableSize)
場所 System.Windows.Controls.Grid.MeasureOverride(Size constraint)
場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
場所 System.Windows.UIElement.Measure(Size availableSize)
場所 MS.Internal.Helper.MeasureElementWithSingleChild(UIElement element, Size constraint)
場所 System.Windows.Controls.ContentPresenter.MeasureOverride(Size constraint)
場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
場所 System.Windows.UIElement.Measure(Size availableSize)
場所 System.Windows.Documents.AdornerDecorator.MeasureOverride(Size constraint)
場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
場所 System.Windows.UIElement.Measure(Size availableSize)
場所 System.Windows.Controls.Border.MeasureOverride(Size constraint)
場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
場所 System.Windows.UIElement.Measure(Size availableSize)
場所 System.Windows.Window.MeasureOverrideHelper(Size constraint)
場所 System.Windows.Window.MeasureOverride(Size availableSize)
場所 System.Windows.FrameworkElement.MeasureCore(Size availableSize)
場所 System.Windows.UIElement.Measure(Size availableSize)
場所 System.Windows.Interop.HwndSource.SetLayoutSize()
場所 System.Windows.Interop.HwndSource.set_RootVisualInternal(Visual value)
場所 System.Windows.Interop.HwndSource.set_RootVisual(Visual value)
場所 System.Windows.Window.SetRootVisual()
場所 System.Windows.Window.SetRootVisualAndUpdateSTC()
場所 System.Windows.Window.SetupInitialState(Double requestedTop, Double requestedLeft, Double requestedWidth, Double requestedHeight)
場所 System.Windows.Window.CreateSourceWindow(Boolean duringShow)
場所 System.Windows.Window.CreateSourceWindowDuringShow()
場所 System.Windows.Window.SafeCreateWindowDuringShow()
場所 System.Windows.Window.ShowHelper(Object booleanBox)
場所 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
場所 MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
場所 System.Windows.Threading.DispatcherOperation.InvokeImpl()
場所 System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
場所 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
場所 System.Windows.Threading.DispatcherOperation.Invoke()
場所 System.Windows.Threading.Dispatcher.ProcessQueue()
場所 System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
場所 MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
場所 MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
場所 System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
場所 MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
場所 System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
場所 MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
場所 MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
場所 System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
場所 System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
場所 System.Windows.Threading.Dispatcher.Run()
場所 System.Windows.Application.RunDispatcher(Object ignore)
場所 System.Windows.Application.RunInternal(Window window)
場所 System.Windows.Application.Run(Window window)
場所 System.Windows.Application.Run()
場所 BingMapSample.App.Main() 場所 c:\Users\******\Documents\Visual Studio 2013\Projects\BingMapSample\BingMapSample\obj\Debug\App.g.cs:行 0
場所 System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
場所 System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
場所 Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
場所 System.Threading.ThreadHelper.ThreadStart_Context(Object state)
場所 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
場所 System.Threading.ThreadHelper.ThreadStart()
InnerException: System.InvalidOperationException
HResult=-2146233079
Message=ディスパッチャーの処理の中断中は、この操作を実行できません。
Source=WindowsBase
StackTrace:
場所 System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
場所 System.Windows.Threading.DispatcherOperation.Wait(TimeSpan timeout)
場所 System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherOperation operation, CancellationToken cancellationToken, TimeSpan timeout)
場所 System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
場所 System.Windows.Threading.Dispatcher.Invoke(Delegate method, Object[] args)
場所 Microsoft.Maps.MapControl.WPF.Core.MapConfigurationFromWeb.ConfigLoaded(String requestKey, Dictionary`2 sections)
場所 Microsoft.Maps.MapControl.WPF.Core.MapConfigurationFromWeb.GetConfigurationSection(String version, String sectionName, String culture, MapConfigurationCallback callback, Boolean reExecuteCallback, Object userState)
場所 Microsoft.Maps.MapControl.WPF.Core.MapConfiguration.GetSection(String version, String sectionName, String culture, String key, MapConfigurationCallback callback, Boolean reExecuteCallback, Object userState)
場所 Microsoft.Maps.MapControl.WPF.Core.MapConfiguration.GetSection(String version, String sectionName, String culture, String key, MapConfigurationCallback callback, Boolean reExecuteCallback)
場所 Microsoft.Maps.MapControl.WPF.Map..ctor()
InnerException:
結構、えらい感じのStackTraceが吐かれてしまいますね。で、重要なのはこの部分「ディスパッチャーの処理の中断中は、この操作を実行できません。」です。
何故Dispatcherが中断中かは非常に謎ですが、Dispatcher外で初期化処理が呼び出されているようです。
Templateに含まれるコントロールの作成はDispatcher上で実行されているはずなのですが…。

回避

まぁ、そういうわけで原因は不明なのですが、エラーが起こっておりこれを解決しなければいけません。
日本語エラーメッセージをこの辺で検索して、英語でのエラーメッセージに変換し、さらに英語のエラーメッセージで検索すると比較的解決方法はすぐ見つかります。
わかりやすいのは、この辺でしょうか?
要するに、Dispatcher上で非同期実行できるようにBeginInvokeで囲んであげればよいよということですね。
なるほど、では早速適用を…。ちょっと待てよ、初期化処理自体がXAMLで書かれてるのに、BeginInvokeで囲むとかどうするのよ。

決着

XAML上で非同期実行とかできるわけがないので(出来たらゴメンね)、あとはコードで書くしかありません。
でも、TemplateはXAMLで書きたいですよね。あと、Templateの種類によってはコードでは完全には再現できません。
なので、UserControlでWrapしてみました。
/// <summary>
/// UserControl1.xaml の相互作用ロジック
/// </summary>
public partial class UserControl1 : UserControl
{
    public UserControl1()
    {
        InitializeComponent();
        App.Current.Dispatcher.BeginInvoke(
            new Action(() =>
                this.Content =new Map
                {
                    CredentialsProvider =new ApplicationIdCredentialsProvider("ここにAPIキーを入力")
                }));
    }
}
<Window x:Class="BingMapSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:bingMapSample="clr-namespace:BingMapSample"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ContentControl>
            <ContentControl.Template>
                <ControlTemplate>
                    <bingMapSample:UserControl1/>
                </ControlTemplate>
            </ContentControl.Template>
        </ContentControl>
    </Grid>
</Window>
これで無事実行可能に、あとは必要に応じてMapコントロールの各プロパティとメソッドをUserControl側からバイパスしてあげればOKですね。
#なんだろう。解決したのに、この敗北感は。

2015年1月14日

WPFでBingMapを使う

Filed under: .NET, C#, WPF — タグ: , , — yone64 @ 2:29 午前
今回からFont大き目でお届けしたいと思います。少し見やすくなるとよいのですが?

WPFで地図表示がしたい

地図表示といえば、やっぱりGoogleMapですかね。でも、WPF-C#からならBingMapが相性よさそうですよね。(ほら、なんとなくMicrosoft同士だし。)
と思って探しているとやっぱりありました。
Bing Maps WPF Control
しかし、いまどきmsiでインストールしなければいけないとかちょっと残念感が漂う感じ。(NuGetからインストールできるようになるとよいですね。)
というわけで、msiを落としてきてインストールすれば準備完了です。(あ、あとBingMapのAPI Keyも取得しておいてください)

地図表示アプリの作成

インストールが終了したら、さっそくプロジェクトを作成してみましょう。
# なお、ここからの作業はすべて、VisualStudio2013 & Windows8.1で行ってます。
  • 新規プロジェクト作成→参照設定追加
C:\Program Files (x86)\Bing Maps WPF Control\V1\Libraries にインストールされた、Microsoft.Maps.MapControl.WPF.dll への参照を追加します。
キャプチャ

  • コントロール追加
MainWindow.xamlにMapコントロールを追加します。

<Window x:Class="BingMapSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wpf="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <wpf:Map/>
    </Grid>
</Window>
すると、デザインビューで地図が確認できます。
キャプチャ

  • 実行
ここで、一度実行してみましょう。
キャプチャ

地図が表示されましたね。「Invalid Credentials.~」というのはAPI Keyを入力してないがための文字列なのでとりあえずはOKです。

  • API Key設定
では、API Keyを設定して再び実行してみましょう

<wpf:Map CredentialsProvider="ここにAPI Keyを記述"/>
さて、再度実行!
キャプチャ

あれ?地図が表示されない!?

BingMapとの戦い(1回戦)

API Keyを設定したら、地図が表示されなくなってしまいました。仕方がないので、API Keyがない状態に戻して再度実行をしてみます。
やっぱり、地図は表示されません。なんだか不吉な感じがしてきました。ここで試したことを並べていきます。

  • リビルド → ×
  • ソリューションのクリーン → ×
  • Visual Studio再起動 → ×
  • Windows再起動 → ×
OSの再起動までしてダメなら、割ともう打つ手はない感じです。
仕方がないので、別のマシンを取り出して同じことを試してみます。そうこうしているうちに、ある共通点が見つかりました。

OSの1Userにつき、BingMapを表示するアプリケーションは一度目だけ地図が表示される。
これは、VisualStudioからの実行だけでなく、EXEを直接起動した場合も同じである
ということは、ここで一つの仮説が思いつきますね!

BingMapのDLLは、初回実行時にどこかにキャッシュ(?)を作成しており、キャッシュがある場合のみ地図表示に失敗する。(たぶんキャッシュロードのバグか何かで)
比較的善意に解釈してみました。(悪意を持って2回目以降の地図表示を拒否してるとどうしようもないけど、Microsoftのコントロールはそういうことしないよねという前提)
.NETのモジュールがキャッシュファイルを作りそうな箇所はそれほど多くないので探してみます。思いつくのは↓あたりでしょうか

  • C:\ProgramData
  • C:\Users\{各ユーザ}\AppData
で、結局下記にありました。
キャプチャ

IsolatedStorage(分離ストレージ)に保存されていたようです。
【参考】(@IT)分離ストレージを活用するには?[C#、VB]
ファイルを削除することにより、2回目以降の起動時にも地図が表示されることが確認されました。

対策検討会議

というわけで原因を見つけたものの、勝利!とはならないのです。
分離ストレージのファイルを削除すれば地図が表示されるとわかったものの、起動時に乱数で作ったっぽいフォルダ名の中にあるファイルを見つけて削除するというのは、まぁありえないわけです。
ならどうするか?

その前に、ひとつ疑問が。ヒントは2つ。

  • 2回目以降表示されないバグがあれば、さすがにネット検索にヒットしそう
  • 分離ストレージのファイル名末尾に「_JA-JP」とついている
ここから、導き出される答えは?
ひょっとして、日本語環境だけ?
日本語(+α)環境のみの不具合であれば、StackOverflowが引っかかることもないし、まぁありえる話かなと。
これは、英語OSでの実行を試してみるしかないですよね。でも英語OSなんて手元にない、なんてときは、Azureが便利ですよ。
で、結果はBingo。英語OSでは起きていなかったのです。

ここまでくれば、少し光明が見えてきた感じがします。
.NETのDLLが実行時のCultureを判断して何かしてるということは
  • Thread.CurrentThread.CurrentUICulture
  • Thread.CurrentThread.CurrentCulture
のどちらかが絡んでいるはず。(というか、英語OSを試す前に気付きたかったです。)
で、両方とも試した結果、CurrentUICultureが関連していることが判明。アプリケーションの起動時にCultureを変更するコードを入れた結果、無事地図が表示されるようになったのでした。
public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
    }
}
めでたし、めでたし。というわけにはいきませんね。CurrentUICultureがen-US固定になってしまいます。
さらに試行錯誤を加えた結果、次のことが判明しました。

Mapコントロールをアプリケーションで最初に初期化する時にen-USだったらよい

つまり、いったんCultureをen-USにして、Mapコントロールを初期化したのち、再びCultureを元に戻せばよさそうです。
結局、↓のようなコードを起動時に仕込むことになりました。

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        var old = Thread.CurrentThread.CurrentUICulture;
        Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
        var dummy = new Map();
        Thread.CurrentThread.CurrentUICulture = old;
    }
}

余談

設定するCultureは、en-US以外にもen-GBでもよいようです。その他のCultureもいくつか試してみましたが、ダメでした。
この2つのCultureに共通する点として、一つ気付いたのは、距離の単位です。

キャプチャ

milesになっているのがわかると思います。ちなみに、公式にヤード・ポンド法が使われている国は、英国と米国だけだそうです。
ひょっとするとこのことが関係しているのかもしれません。(裏は取ってないので単なる想像ですが。)

後半へ続く

大きな落とし穴が開いていたBingMapControlですが、ひとまず表示できるようになりました。
ただ、戦いがこれで終わったわけではありません。というわけで、続きは次回。さらに大きな落とし穴が待ち受けています。(死

2014年12月11日

Bindingを整理しよう

Filed under: .NET, WPF — タグ: — yone64 @ 11:08 午前

この記事は、XAML AdventCalendarの11日目の記事です。

皆さんがハードル上げまくってますが、ちょっと基本に立ち返り、XAMLと切っても切れない縁のBindingを整理してみたいと思います。
とはいえ、Bindingだけでも範囲がめっちゃ広いので、どこから値を持ってくるか?という点に絞ります。

基本編

BindingはFrameworkElement.DataContextプロパティーに設定されたオブジェクトを参照します。

/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        this.DataContext = "Hello World!";
    }
}
<Window x:Class="BindingSamples.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Viewbox>
            <TextBlock Text="{Binding}" />
        </Viewbox>
    </Grid>
</Window>

image
DataContextに設定した文字が、TextBlockのTextとして表示されました。

Path色々編

通常はDataContextに設定したオブジェクトを他のプロパティーに設定しなおすことはほぼなく、オブジェクトのプロパティーを参照することが多いです。

/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        this.DataContext = new
        {
            Text = "Hello World",
            Hoge = new { Fuga = "PIYO"},
            Collection = new[] { "One", "Two", "Three" }
        };
    }
}
<Window x:Class="BindingSamples.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Viewbox>
            <StackPanel Orientation="Vertical">
                <TextBlock Text="{Binding Text}" />
                <TextBlock Text="{Binding Hoge.Fuga}" />
                <TextBlock Text="{Binding Collection[1]}" />
            </StackPanel>
        </Viewbox>
    </Grid>
</Window>

image
Property名を指定することで、そのオブジェクトのPropertyの値を取得することができます。
また、「.」でプロパティー名を連結することで、階層的に値をたどっていくことも可能です。その他、[ ]を使いIndexerでのアクセスも可能です。
なお、Pathの指定は先頭に限り省略可能で、{Binding Text}と{Biding Path=Text}は同じ意味になります。
「/」を利用することにより、選択されている~といった指定も可能ですが、ここでは割愛。詳しくは↓を参照ください。
http://msdn.microsoft.com/ja-jp/library/ms742451(v=vs.110).aspx

異なるソース編

ここまでのBindingはDataContextプロパティーのオブジェクトを参照していましたが、異なるソースを指定することもできます。

<Window x:Class="BindingSamples.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid x:Name="Grid">
        <Viewbox>
            <StackPanel Orientation="Vertical">
                <TextBlock Text="{Binding Path=MainWindow.Title, Source={x:Static Application.Current}}" />
                <TextBlock Text="{Binding Path=ActualWidth, ElementName=Grid}" />
            </StackPanel>
        </Viewbox>
    </Grid>
</Window>

image

Sourceプロパティーを指定することで、他のオブジェクトを参照することができます。ここには、{x:Static} のほかに{StaticResource}などもよく使います。
また、ElementNameを指定すると、XAML上の他のオブジェクトへの参照を取得することができます。ここでは、最上位のGridが取得できます。

異なるソース編 その2

自分自身から相対参照をもって値を取得することもできます。

<Window x:Class="BindingSamples.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid x:Name="Grid">
        <Viewbox>
            <Grid>
                <StackPanel Orientation="Vertical">
                    <TextBlock Text="{Binding Path=FontFamily, RelativeSource={RelativeSource Self}}" />
                    <TextBlock Text="{Binding Path=Children.Count, RelativeSource={RelativeSource AncestorType={x:Type Grid}}}" />
                    <TextBlock Text="{Binding Path=Children.Count, RelativeSource={RelativeSource AncestorType={x:Type Grid} ,AncestorLevel=2}}" />
                </StackPanel>
            </Grid>
        </Viewbox>
        <Grid/>
    </Grid>
</Window>

image

Selfは文字通り、自分自身ですね。ここでは、TextBlock自身。
AncestorTypeは、FindAncestor(省略可能)モードの場合に使うキーワードで、XAMLをさかのぼって指定された型のオブジェクトを見つけ、参照します。これにAncestorLevelも追加すると見つけた2つ目の~みたいな指定ができるようになります。
あと、RelativeSourceでは特別な場合のみに使える指定があります。

<Window x:Class="BindingSamples.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <ControlTemplate x:Key="Template" TargetType="{x:Type Button}">
            <TextBlock Text="{Binding Path=Foreground,RelativeSource={RelativeSource TemplatedParent}}"/>
        </ControlTemplate>
    </Window.Resources>
    <Grid x:Name="Grid">
        <Viewbox>
            <StackPanel Orientation="Vertical">
                <Button Foreground="Red" Template="{StaticResource Template}" />
                <Button Foreground="Blue" Template="{StaticResource Template}"/>
            </StackPanel>
        </Viewbox>
    </Grid>
</Window>

image

まずは、Templateの内部でのみ使えるTemplateParent。文字通りTemplateを保持する親コントロールへの参照を取得します。

<Window x:Class="BindingSamples.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid x:Name="Grid">
        <Viewbox>
            <ItemsControl ItemsSource="{Binding Collection}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Border BorderThickness="1" BorderBrush="Black">
		                    <Grid>
		                        <Grid.ColumnDefinitions>
		                            <ColumnDefinition />
		                            <ColumnDefinition />
		                        </Grid.ColumnDefinitions>
		                        <Grid.RowDefinitions>
		                            <RowDefinition />
		                            <RowDefinition />
		                        </Grid.RowDefinitions>
		                        <TextBlock Text="今"/>
		                        <TextBlock Grid.Row="1" Text="前"/>

		                        <TextBlock Grid.Column="1" Text="{Binding}" />
		                        <TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding RelativeSource={RelativeSource PreviousData}}" />
		                    </Grid>
                        </Border>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Viewbox>
    </Grid>
</Window>

image
あとは、ItemsControl系のDataTemplate内部でのみ使用できるPreviousData。これも文字通りで、ItemsSourceに指定されたCollectionのひとつ前の情報を取得できます。
# しかし、実際に使ったことはないのですよ。(どこで使うんだろう。。。)
というわけで、Bindingは結構柔軟にあちこちからデータを取得することができます。ぜひ活用してください。

2014年12月5日

DataGridとDataTable(その2)【WPF編】

Filed under: .NET, WPF — タグ: , — yone64 @ 9:39 午後

さて、前回の続き。
DataGridとDataTableは非常に相性がよいのですが、「AutoGenerateColumn=”True”」で使う際困った問題があります。
例えば、前回のソースコードを、次のように変更してみます。

var table = new DataTable();
table.Columns.Add("Column1");
table.Columns.Add("Column.2");
table.Columns.Add("Column3");
table.Columns.Add("Column4");

for (int i = 0; i < 20; i++)
{
    var row = table.NewRow();
    for (int j = 0; j < table.Columns.Count; j++)
    {
        row[j] = "[" + (i + 1) + "," + (j + 1) + "]";
    }
    table.Rows.Add(row);
}
this.DataContext = table;

違いは、Column名に「.」が入ったこと。そうすると
キャプチャ
あら不思議。表示されなくなりました。
これは各方面で報告されている(例えば、ココとかココとか)通り で、WPFのBindingPathの解決の仕組みとColumn名がバッティングしているためです。

回避方法として、提案されているのは

  • Unicodeで似ている文字を使う
  • BindingPathを”[ ]”で括る

です。Unicodeで似ている文字を使うのは論外として、”[ ]”で括るのは、このケースにおいては、一見うまくいっているように見えます。しかし、WPFのBindingPathの予約文字と被ったってことは、考慮しないといけないのは”.”だけではないことは容易に想像できますね。例えば、”[“とかがColumn名に含まれると非常にまずいわけです。(この場合は問答無用で例外が発生します。)

つまり、ClassのProperty名と違って使用不可文字のないDataColumn.ColumnNameを使用してBindingPathとすることはそもそも無理があるということになります。

ひとまず、自分の回避策を載せておきますが、どうするのが正解かはわかっていません。

var table = new DataTable();
table.Columns.Add(new DataColumn { ColumnName = "C" + table.Columns.Count, Caption = "Column1" });
table.Columns.Add(new DataColumn { ColumnName = "C" + table.Columns.Count, Caption = "Column.2" });
table.Columns.Add(new DataColumn { ColumnName = "C" + table.Columns.Count, Caption = "Column3" });
table.Columns.Add(new DataColumn { ColumnName = "C" + table.Columns.Count, Caption = "Column4" });

for (int i = 0; i < 20; i++)
{
    var row = table.NewRow();
    for (int j = 0; j < table.Columns.Count; j++)
    {
        row[j] = "[" + (i + 1) + "," + (j + 1) + "]";
    }
    table.Rows.Add(row);
}
this.DataContext = table;
private void DataGrid_OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
    var column = e.Column as DataGridBoundColumn;
    var view = (sender as DataGrid).ItemsSource as DataView;
    if (view != null && column != null)
    {
        var dataColumn =
            view.Table.Columns.OfType<DataColumn>().FirstOrDefault(c => c.ColumnName == e.PropertyName);
        if (dataColumn != null)
        {
            column.Header = dataColumn.Caption;
        }
    }
}

ColumnNameは絶対にBindingPathの予約文字とかぶらないように、プログラム的に一意の文字列をふります。そして、Headerに表示したい文字列はCaptionを利用。その後、Column作成時にCaptionをHeaderに代入します。

キャプチャ

ちなみに、DataTableはIndexで各ColumnとBindingができるので、そもそもAutoGenerateColumnでBindingを作成する際は、Indexでの解決にしてくれればよかったのに。。。と思ったりしないわけでもないのですが、どうなんでしょう。
# もしくは、BindingはColumnNameを使って、表示はCaptionを使うとか。

Older Posts »

WordPress.com Blog.