かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

「Thumb コントロールで Photoshop のナビゲーターを再現する」をWinRTでやる場合

@Grabacr07さんの素敵な記事!

grabacr.net

WinRTでやるときはこうだよっていうのをちょっとだけ。主にScrollViewerがいけてない部分を補足するだけです。ScrollViewerのScrollChangedイベントはWinRTにはないので、ViewChangedイベントと、LayoutUpdatedイベントを組み合わせて同じような動きを再現させます。コード的には以下のような感じ。

private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
    this.UpdateViewportSize();
}

private void ScrollViewer_LayoutUpdated(object sender, object e)
{
    this.UpdateViewportSize();
}

private void UpdateViewportSize()
{
    var xfactor = this.Thumbnail.ActualWidth / this.ScrollViewer.ExtentWidth;
    var yfactor = this.Thumbnail.ActualHeight / this.ScrollViewer.ExtentHeight;

    var left = this.ScrollViewer.HorizontalOffset * xfactor;
    var top = this.ScrollViewer.VerticalOffset * yfactor;

    var width = this.ScrollViewer.ViewportWidth * xfactor;
    if (width > this.Thumbnail.ActualWidth) { width = this.Thumbnail.ActualWidth; }

    var height = this.ScrollViewer.ViewportHeight * yfactor;
    if (height > this.Thumbnail.ActualHeight) { height = this.Thumbnail.ActualHeight; }

    Canvas.SetTop(this.Viewport, top);
    Canvas.SetLeft(this.Viewport, left);

    this.Viewport.Width = width;
    this.Viewport.Height = height;
}

これだけで、WinRTでも動くようになります。(試したのはWin10TPのUAPだけどたぶん大丈夫でしょう…)

f:id:okazuki:20150411195927p:plain

コード

一応XAMLとC#のコードをはっておきます。

<Page
    x:Class="App24.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App24"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        
        <Canvas Margin="10"
                Width="{Binding Path=ActualWidth, ElementName=Thumbnail}"
                Height="{Binding Path=ActualHeight, ElementName=Thumbnail}"
                HorizontalAlignment="Center">
            <Image x:Name="Thumbnail" Width="100" Stretch="Uniform" Source="ms-appx:///Assets/tree.jpg" />
            <Thumb x:Name="Viewport"
                   DragDelta="Viewport_DragDelta">
                <Thumb.Template>
                    <ControlTemplate TargetType="Thumb">
                        <Border BorderThickness="2"
                                BorderBrush="Red"
                                Background="Transparent" />
                    </ControlTemplate>
                </Thumb.Template>
            </Thumb>
        </Canvas>
        <ScrollViewer x:Name="ScrollViewer" 
                      Grid.Column="1" 
                      VerticalScrollBarVisibility="Auto" 
                      HorizontalScrollBarVisibility="Auto"
                      ViewChanged="ScrollViewer_ViewChanged"
                      LayoutUpdated="ScrollViewer_LayoutUpdated">
            <Image Source="ms-appx:///Assets/tree.jpg" />
        </ScrollViewer>
    </Grid>
</Page>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409

namespace App24
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
        {
            this.UpdateViewportSize();
        }

        private void ScrollViewer_LayoutUpdated(object sender, object e)
        {
            this.UpdateViewportSize();
        }

        private void UpdateViewportSize()
        {
            var xfactor = this.Thumbnail.ActualWidth / this.ScrollViewer.ExtentWidth;
            var yfactor = this.Thumbnail.ActualHeight / this.ScrollViewer.ExtentHeight;

            var left = this.ScrollViewer.HorizontalOffset * xfactor;
            var top = this.ScrollViewer.VerticalOffset * yfactor;

            var width = this.ScrollViewer.ViewportWidth * xfactor;
            if (width > this.Thumbnail.ActualWidth) { width = this.Thumbnail.ActualWidth; }

            var height = this.ScrollViewer.ViewportHeight * yfactor;
            if (height > this.Thumbnail.ActualHeight) { height = this.Thumbnail.ActualHeight; }

            Canvas.SetTop(this.Viewport, top);
            Canvas.SetLeft(this.Viewport, left);

            this.Viewport.Width = width;
            this.Viewport.Height = height;
        }

        private void Viewport_DragDelta(object sender, DragDeltaEventArgs e)
        {
            this.ScrollViewer.ScrollToHorizontalOffset(
                this.ScrollViewer.HorizontalOffset + (e.HorizontalChange * this.ScrollViewer.ExtentWidth / this.Viewport.ActualWidth));
            this.ScrollViewer.ScrollToVerticalOffset(
                this.ScrollViewer.VerticalOffset + (e.VerticalChange * this.ScrollViewer.ExtentHeight / this.Viewport.ActualHeight));
        }
    }
}

Windows ストアアプリで、筆圧を取る方法

今日の小ネタです。

筆圧を取るには、Pointer系のイベントでイベント引数のGetCurrentPointを呼び出して、その戻り値のProperties.Pressureで取得できます。

private void Grid_PointerMoved(object sender, PointerRoutedEventArgs e)
{
    // 0~1の間で筆圧が取れる
    var pressure = e.GetCurrentPoint(null).Properties.Pressure;
}

指とかだと0.5が返ってきます。

ストアアプリでListViewに左右のスワイプ処理を追加するという記事が面白かったのでReactivePropertyを使って書いてみた

元ネタ

めとべや東京 #7で登壇してきましたその1 ストアアプリでListViewに左右のスワイプ処理を追加する - shinji-japanのブログ

面白い内容です。読んでて記事内容で疑問点がいくつか出てきました。

  • なぜItemTemplate内のBorderの横幅と縦幅を明示してるのか
  • スワイプされた要素はどうやって特定するのか
  • MVVMちっくに書きたい

なぜItemTemplate内のBorderの横幅と縦幅を明示してるのか

これはおそらくイベントを拾うため。ItemContainerStyleでHorizontalContentAlignmentとVerticalContentAlignmentをStretchにしてやることで、ItemTemplateの中身がいっぱいにひろがって明示的にサイズを指定しなくても、イベントが拾えるようになりました。

具体的にはItemContainerStyleプロパティのコピーを以下のように編集します。

<Style x:Key="ListViewItemStyle1" TargetType="ListViewItem">
    <Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}"/>
    <Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="TabNavigation" Value="Local"/>
    <Setter Property="IsHoldingEnabled" Value="True"/>
    <Setter Property="Margin" Value="0,0,18,2"/>
    <Setter Property="HorizontalContentAlignment" Value="Left"/>
    <Setter Property="VerticalContentAlignment" Value="Top"/>
    <!-- この2個を追加 -->
    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    <Setter Property="VerticalContentAlignment" Value="Stretch" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListViewItem">
                <-- 省略 -->
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

スワイプされた要素はどうやって特定するのか

イベント引数のOriginalSourceのDataContextからかっさらってやればよかったです。

MVVMチックに書きたい

ReactivePropertyのEventToReactiveCommandを使ってみました。

こんなConverterを作って、スワイプされた要素とスワイプの方向のTupleを返すようにしました。

public class ManipulationDeltaRoutedEventArgsToStringConverter : ReactiveConverter<ManipulationDeltaRoutedEventArgs, Tuple<string, string>>
{
    protected override IObservable<Tuple<string, string>> OnConvert(IObservable<ManipulationDeltaRoutedEventArgs> source)
    {
        var ox = source
            .Select(x => Tuple.Create(x.OriginalSource as FrameworkElement, x.Delta.Translation.X));
        return Observable.Merge(
            ox
                .Where(x => x.Item2 > 10)
                .Throttle(TimeSpan.FromSeconds(0.5))
                .ObserveOnUIDispatcher()
                .Select(x => Tuple.Create(x.Item1.DataContext as string, "Right")),
            ox
                .Where(x => x.Item2 < -10)
                .Throttle(TimeSpan.FromSeconds(0.5))
                .ObserveOnUIDispatcher()
                .Select(x => Tuple.Create(x.Item1.DataContext as string, "Left")));
    }
}

Throttleを入れてるのはログを吐くようにしてみたら10以上の移動が連続して発生していたので、フィルタリングするためです。これで無駄なイベントの発生を排除しています。

これをViewModelのCommandにBindingします。

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App3"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:Interactivity="using:Microsoft.Xaml.Interactivity" 
    xmlns:Core="using:Microsoft.Xaml.Interactions.Core" 
    xmlns:Interactivity1="using:Reactive.Bindings.Interactivity"
    x:Class="App3.MainPage"
    mc:Ignorable="d">
    <Page.Resources>
        <!-- ITemTemplate内からViewModelに触るためにリソースにVMを登録しておく -->
        <local:MainPageViewModel x:Key="viewModel"/>

        <DataTemplate x:Key="DataTemplate1">
            <Grid ManipulationMode="TranslateX,System" Background="Pink">
                <Interactivity:Interaction.Behaviors>
                    <!-- ManipulationDeltaのイベント引数を変換してコマンドに渡す -->
                    <Core:EventTriggerBehavior EventName="ManipulationDelta">
                        <Interactivity1:EventToReactiveCommand Command="{Binding SwipeCommand, Mode=OneWay, Source={StaticResource viewModel}}">
                            <local:ManipulationDeltaRoutedEventArgsToStringConverter />
                        </Interactivity1:EventToReactiveCommand>
                    </Core:EventTriggerBehavior>
                </Interactivity:Interaction.Behaviors>
                <TextBlock Text="{Binding}" Style="{ThemeResource BodyTextBlockStyle}" />
            </Grid>
        </DataTemplate>
        <Style x:Key="ListViewItemStyle1" TargetType="ListViewItem">
            <!-- さっきの内容なので省略 -->
        </Style>
    </Page.Resources>
    <Page.DataContext>
        <StaticResource ResourceKey="viewModel"/>
    </Page.DataContext>
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <TextBlock TextWrapping="Wrap" Text="{Binding SwipeMessage.Value}" Style="{StaticResource BodyTextBlockStyle}" HorizontalAlignment="Center"/>
        <ListView Grid.Row="1" ItemsSource="{Binding Items}" ItemTemplate="{StaticResource DataTemplate1}" ItemContainerStyle="{StaticResource ListViewItemStyle1}" />

    </Grid>
</Page>

ViewModelはこんな感じ。

using Reactive.Bindings;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;

namespace App3
{
    public class MainPageViewModel
    {
        public ReadOnlyReactiveCollection<string> Items { get; private set; }
        public ReactiveCommand<Tuple<string, string>> SwipeCommand { get; private set; }

        public ReactiveProperty<string> SwipeMessage { get; private set; }

        public MainPageViewModel()
        {
            var source = new ObservableCollection<string>(Enumerable.Range(1, 1000).Select(x => "item " + x));
            this.Items = source.ToReadOnlyReactiveCollection(x => x);

            this.SwipeCommand = new ReactiveCommand<Tuple<string, string>>();
            this.SwipeMessage = this.SwipeCommand
                .Select(x => x + " swiped.")
                .ToReactiveProperty();
        }
    }
}

実行してスワイプしてみた。ちゃんととれてる。

f:id:okazuki:20150222234807p:plain

Windows store appで共有コントラクトに対応する

久しぶり過ぎて忘れてたのでメモです。

まずは、こんな感じでDataTransferManagerのDataRequestedイベントを購読しておく。

var manager = DataTransferManager.GetForCurrentView();
manager.DataRequested += this.ShareDataRequested;

そうすると共有ボタンが押されたときにイベントが呼び出される。イベント内では、イベント引数にでーたを突っ込む。args.Request.Data.Properties.Title, args.Request.Data.Properties.Descriptionの2つと、args.Request.Data.SetXXX(XXXは設定したいデータの型の名前が大体はいる)を設定すればOK。

// なんかのデータとってきて
var currentEntry = this.Model.ItemModel.CurrentEntry;

// データをつめる(dataPackage変数がargs.Request.Dataね)
dataPackage.Properties.Title = currentEntry.Title;
dataPackage.Properties.Description = currentEntry.Summary;
dataPackage.SetWebLink(new Uri(currentEntry.Link));

共有コントラクトの共有画面をアプリから呼び出したいときはDataTransferManagerのShowShareUIを呼び出せばよい。

DataTransferManager.ShowShareUI();

本当久しぶり過ぎて忘れてた。

WinRTのStreamとDataReader, DataWriterって使いにくいよね!

.NETのStream系に慣れてるせいか、とても癖があるように感じてしまうWinRTのStream。ちょっと使ってみたいと思います。

まず、ファイルにデータを書き込むところから。 ファイルに書き込む処理には便利な機能があって、書き込みをトランザクションとして扱ってくれる機能があります。StorageFileオブジェクトのOpenTransactedWriteAsyncメソッドでトランザクション開始して、Streamプロパティで取得できるStreamにデータを書き込めばOKです。

Stream生のままだと低レベルすぎるのでDataWriterでラップして各型ごとのデータ書き込みメソッドでデータを書き込むのが幾分か楽です。

以下の例では、文字列長と、文字列を書き込んでいます。全てIOのある処理は非同期なのがWinRTの特徴ですよね。

var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(
    "sample.txt", 
    CreationCollisionOption.ReplaceExisting);
using (var tx = await file.OpenTransactedWriteAsync())
using (var w = new DataWriter(tx.Stream))
{
    var data = "Hello world " + DateTime.Now;
    var buff = Encoding.UTF8.GetBytes(data);
    w.WriteInt32(buff.Length);
    w.WriteString(data);
    await w.StoreAsync();

    await tx.CommitAsync();
}

とまぁ、書き込みはいいとして読み込みが凄い癖があります。

読み込みはDataReaderでStreamをラップして使えばいいんですが、こいつがLoadAsyncでバッファに指定したサイズだけデータを読み込んで、Read型名って名前のメソッドでバッファからデータを読み込みます。LoadAsyncメソッドが非同期メソッドです。Read型名は同期です。つまりRead型名ではIOが発生しないと。あらかじめ必要なデータをLoadAsyncでバッファに読み込んでおかないといけない。難しい。

ということで、先ほど書き込んだデータを読み込むコードは以下のようになります。LoadAsyncでintのサイズだけデータを読み込んで、ReadInt32で読み込んだデータをint型として取りだしています。そして、同じ要領で文字列も読み込んでいます。

var data = default(string);

var file = await ApplicationData.Current.LocalFolder.TryGetItemAsync("sample.txt");
if (file == null) { return; }

using (var s = await ((StorageFile)file).OpenReadAsync())
using (var r = new DataReader(s))
{
    await r.LoadAsync(sizeof(int));
    var length = r.ReadInt32();
    await r.LoadAsync((uint)length);
    data = r.ReadString((uint)length);
}

var dialog = new MessageDialog(data);
await dialog.ShowAsync();

個人的にはAsStreamとかでSystem.IO.Streamにしてしまって操作するけどね!