読者です 読者をやめる 読者になる 読者になる

プログラムの事とか

お約束ですが「掲載内容は私個人の見解です」

WPFのInkCanvasで塗りつぶす

WPF

f:id:puni-o:20161201111926j:plain

InkCanvasは線を描くものっぽくて閉じた領域の塗りつぶし、みたいなことは標準ではできなさそうでした

ということでとりあえず適当に塗りつぶすようにしていみます

塗りつぶす人

System.Windows.Ink.Strokeを継承してDrawCoreで塗りつぶすようにします

ソース

public class CustomInkCanvas : InkCanvas
{
protected override void OnStrokeCollected(InkCanvasStrokeCollectedEventArgs e)
{
    this.Strokes.Remove(e.Stroke);
    var newStroke = new FillStroke(e.Stroke.StylusPoints, e.Stroke.DrawingAttributes);
    this.Strokes.Add(newStroke);

    base.OnStrokeCollected(new InkCanvasStrokeCollectedEventArgs(newStroke));
}
}

public class FillStroke : Stroke
{
public FillStroke(StylusPointCollection stylusPoints) : base(stylusPoints)
{
}
public FillStroke(StylusPointCollection stylusPoints, DrawingAttributes drawingAttributes) : base(stylusPoints, drawingAttributes)
{
}

protected override void DrawCore(DrawingContext drawingContext, DrawingAttributes drawingAttributes)
{
    if (drawingContext == null)
    {
        throw new ArgumentNullException(nameof(drawingContext));
    }
    if (null == drawingAttributes)
    {
        throw new ArgumentNullException(nameof(drawingAttributes));
    }

    if (this.StylusPoints.Count < 3) return;

    var stroke = new Pen(Brushes.Blue, 2);
    stroke.Freeze();
    var fill = new SolidColorBrush(Color.FromArgb(0x80, 0x00, 0xff, 0xff));
    fill.Freeze();

    var streamGeometry = new StreamGeometry();
    using (var geometryContext = streamGeometry.Open())
    {
        geometryContext.BeginFigure(this.StylusPoints[0].ToPoint(), true, false);
        var points = new PointCollection();
        for (var i = 1; i < this.StylusPoints.Count; i++)
        {
            points.Add(this.StylusPoints[i].ToPoint());
        }
        geometryContext.PolyLineTo(points, true, false);
    }

    drawingContext.DrawGeometry(fill, stroke, streamGeometry);
}
}

CustomInkCanvasはStrokeが追加されたときにFillStrokeに置き換える処理をしています

FillStroke.DrawCoreで塗りつぶしています

結果

f:id:puni-o:20161201112430g:plain

塗りつぶせました

あとは閉じた領域だけ塗るようにすればいい感じになりそうなんですが、閉じた領域ってどうやって探すんだろう・・・

WPFのItemsControl.ItemTemplateでリストの一つ前を参照する

WPF

f:id:puni-o:20161115164946j:plain

WPFのItemsControlはすごく強力だとおもいますよね

ItemsControl.ItemsSourceにリストをバインドして、ItemTemplateで中身を定義すると自由なリストが簡単に作れるので一度使うと癖になります

簡単な例だとこんな感じ

<ItemsControl ItemsSource="{Binding Numbers}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal" Margin="5">
                <TextBlock Text="{Binding}"/>
            </StackPanel>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Numbersはランダムな10個の数値リストです

実行結果はランダムな数値が縦に10個並ぶだけなので割愛

普段はこんな感じでお手軽に使えますがごくごくごく偶にアイテムの前の値が欲しい時があります(主にデータ設計に問題がある時ですかねぇ)

そんな時は

{Binding RelativeSource={RelativeSource PreviousData}}

で見ることができるんですね、今日知りました

先ほどのサンプルを更新すると

<ItemsControl Grid.Column="1" ItemsSource="{Binding Numbers}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal" Margin="5">
                <TextBlock Text="{Binding RelativeSource={RelativeSource PreviousData}}"/>
                <TextBlock Text=" -> "/>
                <TextBlock Text="{Binding}"/>
            </StackPanel>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

前の値(リストの一つ上の項目)から今回の値に変わったよ、的な表示をしています

実行結果は

f:id:puni-o:20161115165310p:plain

左側は初めに書いたもにで、右が更新後です

こんなことも簡単にできるんですねー

WPFつよい

WPFでShapeをResourceにして使いまわす

WPF

WPFに限らず今時のアプリではアプリ内で使うアイコン等をベクトルデータで持っていることが多いと思います

IllustratorSVGXAML

って感じでアイコンデータがやってきたりすると思いますが、これをそのままContentに張り付けるのは悪手ですよね(面倒な時を除く)

ということでアイコンデータはResourceDictionaryに入れて、使うところはKeyを指定するようにしましょう

Resource(App.xaml)

<Application.Resources>
    <Canvas x:Key="Icon1" x:Shared="False" Width="100" Height="100">
        <Ellipse Width="100" Height="100" Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type ButtonBase}}}"/>
    </Canvas>
    <Canvas x:Key="Icon2" x:Shared="False" Width="100" Height="100">
        <Rectangle Width="100" Height="100" Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type ButtonBase}}}"/>
    </Canvas>
</Application.Resources>

MainWindow.xaml

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Button Grid.Row="0" Grid.Column="0" Content="{StaticResource Icon1}"/>
    <Button Grid.Row="0" Grid.Column="1" Content="{StaticResource Icon1}" Foreground="Blue"/>
    <Button Grid.Row="1" Grid.Column="0" Content="{StaticResource Icon2}"/>
    <Button Grid.Row="1" Grid.Column="1" Content="{StaticResource Icon2}" Foreground="Blue"/>
</Grid>

結果

f:id:puni-o:20161114160551p:plain

ポイント

Shape.Fill

Shape.Fillの指定ですが、ContentControlのForegroundを参照させたい時(Button.Foregroundとか)は上のような感じにしないと参照してくれません

TextBlockとかは何も指定しなければ親のForegroundを見てくれるのでそのつもりでいると???ってなります

上の場合ButtonBaseを継承しているコントロール内でしか効果ありませんが、まぁそれ以外の状況で色を適宜変えたいとかないよね、ね

ということでMainWindow.xamlのGrid.Column="1"にあるボタンではForegroundの指定色にShapeが変更されていることがわかります

x:Shared="False"

ResourceDictionaryに定義しているリソースはデフォルトで使いまわすようになっています(Brushとかそういうのは使いまわした方が効率いいですからね)

ですがCanvasは使いまわしちゃいけないですよね

x:Sharedを書かないとどこか一つにしか出てこなくなります

x:Shared="False"とすればそれぞれにインスタンスを作ってくれます

x:Shared Attribute

これでかつる!WPFつおい


2016/11/18 追記

Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type ButtonBase}}}"

より

Fill="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type ContentControl}}}"

の方がよさそうですね (ButtonBaseをContentControlに変更)